#!/usr/local/bin/perl

# class to handle the shadow/passwd files

# A Perl - based passwd and shadow file management module
package Passman;

# class data members 
$defaultpasswd = '/etc/passwd';
$defaultshadow = '/etc/shadow';

# these are just to explicitly declare the variables
$passwdfile = ' ';
$shadowfile = ' ';

# indices of the fields in the passwd arrays
$P_USER = 0;
$P_PASSWD = 1;
$P_UID = 2;
$P_GID = 3;
$P_GCOS = 4;
$P_HOMEDIR = 5;
$P_SHELL = 6;

# indices of the fields in the shadow arrays
$S_USER = 0;
$S_PASSWD = 1;
$S_LASTCHG = 2;
$S_MIN = 3;
$S_MAX = 4;
$S_WARN = 5;
$S_INACTIVE = 6;
$S_EXPIRE = 7;
$S_FLAG = 8;

# The same as the above constants, but these can be referenced by the name
# of the field.
%passwdidx = (
			  'user'     => 0,
			  'passwd'   => 1,
			  'uid'      => 2,
			  'gid'      => 3,
			  'gcos'     => 4,
			  'homedir'  => 5,
			  'shell'    => 6
			  );

# The same as the above constants, but these can be referenced by the name
# of the field.
%shadowidx = (
			  'user'     => 0,
			  'passwd'   => 1,
			  'lastchg'  => 2,
			  'min'      => 3,
			  'max'      => 4,
			  'warn'     => 5,
			  'inactive' => 6,
			  'expire'   => 7,
			  'flag'     => 8
			  );


#DONE
# this is the instance constructor 
sub new {
	# arguments
	my ($class, $pfile, $sfile) = @_;

	# instance data members:
	# %uids holds passwd entries keyed on uid
	# %usernames holds passwd entries keyed on username
	# %shusernames holds shadow entries keyed on username
	# %modified holds modified information keyed on username
	#
	# %usernames and %shusernames hold references to arrays whose elements
	# correspond to the fields of the entries. In order to conserve memory,
	# %uids holds arrays of usernames of accounts that have the given uid. The
	# %modified hash holds elements with the following values:
	# -1 = deleted, 0 = not modified, 1 = modified
    my (%uids, %usernames, %shusernames, %modified);

	$passwdfile = $pfile ? $pfile : $defaultpasswd;
	$shadowfile = $sfile ? $sfile : $defaultshadow;

    open(PASSWD, $passwdfile) || do { warn "$passwdfile: $!"; return undef; };
    while($_ = <PASSWD>) {
		chomp;
		my $line = [split /:/, $_];

	    $usernames{$$line[0]} = $line;
		my $uid = $$line[2];

		if(exists $uids{$uid}) { push @{$uids{$uid}}, $$line[0]; }
		else { $uids{$uid} = [$$line[0]]; }

	    $modified{$$line[0]} = 0;
    }
    close PASSWD;

    open(SHADOW, $shadowfile) || do { warn "$shadowfile: $!"; return undef; };
    while($_ = <SHADOW>) {
		chomp;
		my $line = [split /:/, $_];
		$shusernames{$$line[0]} = $line;
    }
    close SHADOW;

    return bless {'uids'        => \%uids,
                  'usernames'   => \%usernames,
	              'shusernames' => \%shusernames,
	              'modified'    => \%modified
				 }, $class;
}

#DONE
sub DESTROY {
	my $self = shift;
	$self->writeout();
}

#NOTDONE
sub writeout {
	my $self = shift;

    # re-write shadow/passwd after making backups
	`cp $passwdfile $passwdfile.bkup`;
	`cp $shadowfile $shadowfile.bkup`;

	open(PASSWD, '>'.$passwdfile.'tmp') ||
		do { warn "$passwd.tmp: $!\n"; return 0; };
	open(SHADOW, '>'.$shadowfile.'tmp') ||
		do { warn "$shadow.tmp: $!\n"; return 0; };

	foreach $blah ($self->alluids()) {
		my $users = $self->{'uids'}->{$blah};

		foreach $user (@$users) {
			my @pwline = @{$self->{'usernames'}->{$user}};
			$#pwline = 6;
			print PASSWD join(':', @pwline), "\n";
			my @shline = @{$self->{'shusernames'}->{$user}};
			$#shline = 8;
			print SHADOW join(':', @shline), "\n";
		}
	}
	
	close PASSWD; close SHADOW;
    chmod 0644, $passwdfile.'tmp';
	rename $passwdfile.'tmp', $passwdfile;
    chmod 0400, $shadowfile.'tmp';
	rename($shadowfile.'tmp', $shadowfile);

	return 1;
}

#DONE
# Adds a new user to the shadow/passwd files.
# Returns 1 on success, 0 on failure
sub adduser {
	my $self = shift;
	my ($username, $gcos, $dir, $shell, $gid, $uid) = @_;

	# Make sure were were passed a username, any uid/gid passed to us is all
	# numeric, and the username or uid doesn't already exist.
	defined $username or return 0;
	$uid =~ /^\d+$/ or return 0 if defined $uid;
	$gid =~ /^\d+$/ or return 0 if defined $gid;
	exists $self->{'usernames'}->{$username} and return 0;

	# we DO NOT do this test for UIDs because several accounts can have the
	#same UID.
	#exists $self->{'uids'}->{$uid} and return 0 if defined $uid;

	unless(defined $uid) {
		# find the next available uid
		for($uid = 100; $self->{'uids'}->{$uid}; $uid++) {}
	}

	unless(defined $gid) {
		$gid = $uid;
	}

	my @passwdentry = ($username, 'x', $uid, $gid, $gcos, $dir, $shell);
	my @shadowentry = ($username, '*', '', '', '', '', '', '');
	$self->{'usernames'}->{$username} = \@passwdentry;
	$self->{'shusernames'}->{$username} = \@shadowentry;

	$self->uidlistadd($uid, $username);

	$self->{'modified'}->{$username} = 1;

	return 1;
}

# Remove a user form this instance.
# Returns 1 on success and 0 on failure.
sub removeuser {
	my ($self, $user) = @_;

	return 0 unless defined($user) and $self->userexists($user);

	my $uid = $self->get('uid', $user);
	$self->uidlistremove($uid, $user);
	delete($self->{'usernames'}->{$user});
	delete($self->{'shusernames'}->{$user});
	delete($self->{'modified'}->{$user});
	return 1;
}

#-------get methods for passwd

#DONE
# gets the value of the specified field keyed on username (fields listed
# in %passwdidx
sub get {
	my ($self, $field, $user) = @_;

	return ${$self->{'usernames'}->{$user}}[$passwdidx{$field}];
}

#DONE
# gets the username list for a given uid
sub getuid {
	my ($self, $uid) = @_;

	return @{$self->{'uids'}->{$uid}};
}

#--------set methods for passwd
#DONE
sub set {
	my ($self, $field, $user, $newval) = @_;

	return 0 unless defined $passwdidx{$field};

	my $entry;
	return 0 unless defined ($entry = $self->{'usernames'}->{$user});

	if($field eq 'user') {
		return $self->changeusername($$entry[$P_USER], $newval);
	}
	elsif($field eq 'uid') {
		return $self->changeuid($$entry[$P_USER], $newval);
	}

	$$entry[$passwdidx{$field}] = $newval;
	$self->{'modified'}->{$user} = 1;
	return 1;
}

#DONE
# An internal function- not to be used from outside the module. Changes an
# entry's username and makes all the necessary adjustments to the %usernames,
# %shusernames, $uids, and $modified hashes.
# Args: old_username, new_username
sub changeusername {
	my ($self, $olduser, $newuser) = @_;

	my $entry = $self->{'usernames'}->{$olduser};
	return 0 unless defined $entry;
	return 0 if defined $self->{'usernames'}->{$newuser};

	my $shentry = $self->{'shusernames'}->{$olduser};

	$$entry[$P_USER] = $newuser;
	$$shentry[$S_USER] = $newuser;
	$self->{'usernames'}->{$newuser} = $entry;
	$self->{'shusernames'}->{$newuser} = $shentry;
	$self->{'modified'}->{$newuser} = 1;

	delete $self->{'usernames'}->{$olduser};
	delete $self->{'shusernames'}->{$olduser};

	$self->uidlistadd($$entry[$P_UID], $olduser);
	$self->uidlistremove($$entry[$P_UID], $newuser);

	$self->{'modified'}->{$olduser} = -1;
	return 1;
}

#DONE
# An internal function- not to be used from outside the module. Changes an
# entry's uid and makes all the necessary adjustments to the %uids and $shuids
# hashes. The entry is specified by username.
sub changeuid {
	my ($self, $user, $newuid) = @_;

	my $entry = $self->{'usernames'}->{$user};
	return 0 unless defined $entry;

	my $olduid = $$entry[$P_UID];
	$$entry[$P_UID] = $newuid;

	$self->{'usernames'}->{$user} = $entry;

    $self->uidlistadd($newuid, $user);
	$self->uidlistremove($olduid, $user);
	$self->{'modified'}->{$user} = 1;

	return 1;
}

#DONE
# An internal function- it takes the uid and username, then places the username
# in the list for the given uid (if it's not already there).
sub uidlistadd {
	my ($self, $uid, $user) = @_;

	my $uidref = $self->{'uids'}->{$uid};
	if(defined $uidref) {
		grep($_ eq $user, @$uidref) or push @$uidref, $user;
	}
	else {
		$self->{'uids'}->{$uid} = [$user];
	}
}

#DONE
# An internal function- takes the uid and username, then removes the username
# from the list for the given uid (if it's there).
sub uidlistremove {
	my ($self, $uid, $user) = @_;

	my $uidref = $self->{'uids'}->{$uid};

	my @ret = grep { $_ ne $user } @$uidref;
	$self->{'uids'}->{$uid} = \@ret;
}

#-------get methods for shadow
# username:password:lastchg:min:max:warn:inactive:expire:flag

#DONE
# gets the value of the specified field keyed on username (fields listed
# in %shadowidx
sub shget {
	my ($self, $field, $user) = @_;

	return ${$self->{'shusernames'}->{$user}}[$shadowidx{$field}];
}

#-------set methods for shadow

#DONE
sub shset {
	my ($self, $field, $user, $newval) = @_;

	return 0 unless defined $passwdidx{$field};

	my $entry = $self->{'shusernames'}->{$user};
	return 0 unless defined $entry;

    # Flag is a reserved field, so it is not settable. Set username
	# through the passwd set method.
	if($field eq 'flag' or $field eq 'user') {
		return 0;
	}

	$$entry[$shadowidx{$field}] = $newval;
	$self->{'modified'}->{$user} = 1;
	return 1;
}

#DONE
# A convenience function; returns a list (numerically sorted) of all uids.
sub alluids {
	my $self = shift;

	return sort {$a <=> $b} keys(%{$self->{'uids'}});
}

#DONE
# A convenience function; returns an alphabetical list (lexically sorted) of
# all uids.
sub allusers {
	my $self = shift;

	return sort keys %{$self->{'usernames'}};
}

#DONE
sub userexists {
	my ($self, $user) = @_;

	return exists $self->{'usernames'}->{$user};
}

#DONE
sub uidexists {
	my ($self, $uid) = @_;

	return exists $self->{'uids'}->{$uid};
}
