###
###  Copyright 2000-2007 University of Illinois Board of Trustees
###  All rights reserved. 
###
###  Users.pm - user account module for psgconf
###
###  Campus Information Technologies and Educational Services
###  University of Illinois at Urbana-Champaign
###


package PSGConf::Control::Users;

use strict;

use PSGConf::Action::GenerateFile::etc_passwd;
use PSGConf::Action::GenerateFile::etc_group;
use PSGConf::Action::GenerateFile::etc_shadow;
use PSGConf::Action::GenerateFile::etc_security_passwd;
use PSGConf::Action::GenerateFile::etc_master_passwd;
use PSGConf::Action::GenerateFile::etc_user_attr;
use PSGConf::Action::ModifyFile;
use PSGConf::Action::HomeDir;
use PSGConf::Action::Symlink;
use PSGConf::Data::Boolean;
use PSGConf::Data::Hash;
use PSGConf::Data::Integer;
use PSGConf::Util;

use User::pwent;
use User::grent;


###############################################################################
###  sorting function
###############################################################################

# need to use a prototype here because this function will be
# called from other packages
# (see comment in "sort" entry in perlfunc(1) man page)
sub _uid_sort($$)
{
	my ($a, $b) = @_;

	### first entry should always be root
	return -1
		if ($a->{'name'} eq 'root');
	return 1
		if ($b->{'name'} eq 'root');

	### sort identical uids by name
	return ($a->{'name'} cmp $b->{'name'})
		if ($a->{'uid'} == $b->{'uid'});

	### otherwise, sort by uid
	return ($a->{'uid'} <=> $b->{'uid'});
}


###############################################################################
###  PSGConf::Action::ModifyFile plugin for /etc/security/user
###############################################################################

sub _modify_etc_security_user
{
	my ($action, $infh, $fh, $psgconf) = @_;
	my (@text, $text, $entry, $user, $attr);

	### read the existing /etc/security/user file
	@text = <$infh>;

	### save the comment banner at the top of the file
	while ($text[0] =~ m/^\*/)
	{
		print $fh shift(@text);
	}
	while ($text[0] eq "\n")
	{
		shift(@text);
	}

	### print out block for default settings
	$text = join('', @text);
	foreach $entry (split(/\s*\n\n\s*/, $text))
	{
		$user = (split(':', $entry))[0];

		if ($user eq 'default')
		{
			print $fh "\n$entry\n\n";
			last;
		}
	}

	### now we write out the new /etc/security/user file
	foreach $entry (sort _uid_sort values %{$psgconf->data_obj('user_info')->get()})
	{
		print $fh "$entry->{'name'}:\n";
		foreach $attr (sort keys %{$entry->{'attributes'}})
		{
			print $fh "\t$attr = $entry->{'attributes'}->{$attr}\n";
		}
		print $fh "\n";
	}

	return 1;
}


###############################################################################
###  utility function to read /etc/security/user
###############################################################################

sub _read_etc_security_user
{
	my ($self, $psgconf) = @_;
	my (@text, $text, $user, $entry, $line, $attrs, $attr, $value);

	### read the existing /etc/security/user file
	if (!open(SECUSER, '</etc/security/user'))
	{
		warn "\n\t!!! can't open '/etc/security/user': $!\n";
		return -1;
	}
	@text = <SECUSER>;
	close SECUSER;

	### skip the comment banner at the top of the file
	while ($text[0] =~ m/^\*/)
	{
		shift(@text);
	}
	while ($text[0] eq "\n")
	{
		shift(@text);
	}

	### parse the existing file
	$text = join('', @text);
	foreach $entry (split(/\s*\n\n\s*/, $text))
	{
		($user, $entry) = split(':', $entry);

		next
			if ($user eq 'default'
			    || !defined $psgconf->data_obj('user_info')->find($user));

		###
		### Load up the temp hash $attrs with all the values 
		### we need to insert into the user_info hash.
		###
		$attrs = undef;
		foreach $line (grep /\S/, split(/\n/, $entry))
		{
			$line =~ s/^\s*//;
			$line =~ s/\s*$//;
			($attr, $value) = split(/\s*=\s*/, $line);
			$attrs->{$attr} = $value;
		}

		$psgconf->data_obj('user_info')->insert(
			{ $user => {'attributes' => $attrs }}
		) if (defined $psgconf->data_obj('user_info')->find($user));
	}
}


###############################################################################
###  utility function to read /etc/user_attr (Solaris)
###############################################################################

sub _read_etc_user_attr
{
	my ($self, $psgconf) = @_;
	my (@text, $line);
	local *FP;

	### read the existing /etc/user_attr file
	if (!open(FP, '</etc/user_attr'))
	{
		warn "\n\t!!! can't open '/etc/user_attr': $!\n";
		return -1;
	}
	@text = <FP>;
	close FP;

	foreach $line ( @text ) {
		chomp $line;
		$line =~ s/#.*$//;

		next 
			 if (!length($line));

		my (@fields) = split (/:/, $line);

		# Currently, only the first and 4th fields are being
		# used, man user_attr(4)
		$psgconf->data_obj('user_info')->insert(
			{ $fields[0] => {'attributes' => { 'attr' => $fields[4] }}}
		) if (defined $psgconf->data_obj('user_info')->find($fields[0]));
	}
}


###############################################################################
###  policy methods
###############################################################################

sub _policy_merge_existing
{
	my ($self, $psgconf) = @_;
	my ($group_info, $user_info, $pwent, $grent, $field);

	return
		if ($psgconf->data_obj('create_accounts')->equals('false'));

	$user_info = $psgconf->data_obj('user_info')->get();
	$group_info = $psgconf->data_obj('group_info')->get();

	### merge in existing users
	setpwent;
	while ($pwent = getpwent)
	{
	    $user_info->{$pwent->name}->{added_from_passwd} = 1 if (!exists($user_info->{$pwent->name}));
				
	    foreach $field ( 'name', 'passwd', 'uid', 'gid', 'gecos', 'dir', 'shell' ) 
	    {
		
		### Check first to see if someone did not put the values
		### in the structure via a user_info directive.
		if ( ! defined $user_info->{$pwent->name}->{$field} ) 
		{
		    
		    ### The home directory is the only field that does not
		    ### have the same name in the pwent strucutre as it does
		    ### in the user_info structure :(
		    if ( $field eq 'dir') {
			$user_info->{$pwent->name}->{'home'} = $pwent->$field;
		    } else {
			$user_info->{$pwent->name}->{$field} = $pwent->$field;
		    }
		}
	    }
	    
	}
	endpwent;
	$self->_read_etc_security_user($psgconf)
		if ($psgconf->data_obj('platform')->match('aix'));

	$self->_read_etc_user_attr($psgconf)
		if ($psgconf->data_obj('platform')->match('solaris'));

	### merge in existing groups
	setgrent;
	while ($grent = getgrent)
	{
		### Check first to see if someone did not put the values
		### in the structure via a group_info directive.
		foreach $field ( 'name', 'gid', 'passwd', 'members' ) {
			$group_info->{$grent->name}->{$field} = $grent->$field
				if ( ! defined $group_info->{$grent->name}->{$field} );
		}

		### work around User::grent bug under AIX
		$group_info->{$grent->name}->{gid} = 4294967294
			if ($group_info->{$grent->name}->{gid} == -2 
			    && $psgconf->data_obj('platform')->match('aix'));
	}
	endgrent;
}


sub _policy_validate_groups
{
	my ($self, $psgconf) = @_;
	my ($group, $group_info, $last_gid, @gids);

	return
		if ($psgconf->data_obj('create_accounts')->equals('false'));

	$group_info = $psgconf->data_obj('group_info')->get();

	### start assigning free gids here
	@gids = sort map { $group_info->{$_}->{gid} } sort keys %$group_info;
	$last_gid = $gids[-1];

	foreach $group (keys %$group_info)
	{
		### assign gid if missing
		if (!exists($group_info->{$group}->{gid}))
		{
			$group_info->{$group}->{gid} = ++$last_gid;
		}

		### save group name in name field
		$group_info->{$group}->{name} = $group;
	}
}


sub _policy_validate_group_members
{
	my ($self, $psgconf) = @_;
	my ($group, $group_info, $user_info, @members);

	return
		if ($psgconf->data_obj('create_accounts')->equals('false'));

	$user_info = $psgconf->data_obj('user_info')->get();
	$group_info = $psgconf->data_obj('group_info')->get();

	foreach $group (keys %$group_info)
	{
		### weed out non-existent users from member list
		@members = grep { exists($user_info->{$_}) }
			@{$group_info->{$group}->{members}};

		### sort members list by uid
		@members = map { $user_info->{$_} } @members;
		@members = sort _uid_sort @members;
		@members = map { $_->{name}; } @members;

		### save new value
		$group_info->{$group}->{members} = [ @members ];
	}
}


sub _policy_validate_users
{
	my ($self, $psgconf) = @_;
	my ($user, $user_info, $group_info, $last_uid, @uids, $default_gid);

	return
		if ($psgconf->data_obj('create_accounts')->equals('false'));

	$user_info = $psgconf->data_obj('user_info')->get();
	$group_info = $psgconf->data_obj('group_info')->get();

	### start assigning free uids here
	@uids = sort map { $user_info->{$_}->{uid} } sort keys %$user_info;
	$last_uid = $uids[-1];

	### find default gid
	$default_gid = (exists($group_info->{users})
			? $group_info->{users}->{gid}
			: 100);

	foreach $user (keys %$user_info)
	{
		### assign uid if missing
		if (!exists($user_info->{$user}->{uid}))
		{
			$user_info->{$user}->{uid} = ++$last_uid;
		}

		### assign group if missing
		if (!exists($user_info->{$user}->{gid}))
		{
			if (exists($user_info->{$user}->{group}))
			{
				$user_info->{$user}->{gid} = $group_info->{$user_info->{$user}->{group}}->{gid};
			}
			else
			{
				### otherwise, assign the default gid
				$user_info->{$user}->{gid} = $default_gid;
			}
		}

		### assign home directory
		$user_info->{$user}->{home} = "/home/$user"
			if (!exists($user_info->{$user}->{home}));

		### save login in name field
		$user_info->{$user}->{name} = $user;

		### Assign uid/gid/modes for the home directory.
		### But we do not want to change the value for '/'
		if ( $user_info->{$user}->{home} ne '/' ) {
			$user_info->{$user}->{home_uid} = 
				$user_info->{$user}->{uid}
					if (!exists($user_info->{$user}->{home_uid}));

			$user_info->{$user}->{home_gid} = 
				$user_info->{$user}->{gid}
					if (!exists($user_info->{$user}->{home_gid}));

			$user_info->{$user}->{home_mode} = 
					$psgconf->data_obj('default_home_mode')->get()
					if (!exists($user_info->{$user}->{home_mode}));
		} else {
			$user_info->{$user}->{home_uid} = -1
				if (!exists($user_info->{$user}->{home_uid}));
			$user_info->{$user}->{home_gid} = -1
				if (!exists($user_info->{$user}->{home_gid}));
			$user_info->{$user}->{home_mode} = -1
				if (!exists($user_info->{$user}->{home_mode}));
		}
	}
}


###############################################################################
###  decide() method
###############################################################################

sub decide
{
	my ($self, $psgconf) = @_;
	my ($user, $user_info, $group_info, $mode, $entry, @members);
	$mode = PSGConf::Data::Integer->new();

	return
		if ($psgconf->data_obj('create_accounts')->equals('false'));

	$user_info = $psgconf->data_obj('user_info')->get();
	$group_info = $psgconf->data_obj('group_info')->get();

	### add actions
	if (! $psgconf->data_obj('platform')->match('freebsd')) 
	{
		$psgconf->register_actions(
			PSGConf::Action::GenerateFile::etc_passwd->new(
				'name'		=> '/etc/passwd',
				'comment_str'	=> undef,
				'mode'		=> 0444,
				'user_info'	=> $user_info,
				'sort_func'	=> \&_uid_sort,
				'passwd_token'	=> $psgconf->data_obj('passwd_token')->get(),
				'add_passwords' => ($psgconf->data_obj('platform')->match('hpux'))
			),
		);
	}

	$psgconf->register_actions(
		PSGConf::Action::GenerateFile::etc_group->new(
			'name'		=> '/etc/group',
			'comment_str'	=> undef,
			'group_info'	=> $group_info
		)
	);

	if ($psgconf->data_obj('platform')->match('hpux'))
	{
		$psgconf->register_actions(
			PSGConf::Action::Symlink->new(
				name	=> '/etc/logingroup',
				link_to	=> '/etc/group',
			)
		);
	}
	elsif ($psgconf->data_obj('platform')->match('aix'))
	{
		$psgconf->register_actions(
			PSGConf::Action::GenerateFile::etc_security_passwd->new(
				'name'		=> '/etc/security/passwd',
				'comment_str'	=> undef,
				'gid'		=> (getgrnam('security'))[2],
				'mode'		=> 0600,
				'user_info'	=> $user_info,
				'sort_func'	=> \&_uid_sort,
			),
			PSGConf::Action::ModifyFile->new(
				'name'		=> '/etc/security/user',
				'modify_func'	=> \&_modify_etc_security_user,
				'gid'		=> (getgrnam('security'))[2],
				'mode'		=> 0640
			)
		);
	}
	elsif ($psgconf->data_obj('platform')->match('freebsd'))
	{
		$psgconf->register_actions(
			PSGConf::Action::GenerateFile::etc_master_passwd->new(
				'name'		=> '/etc/master.passwd',
				'mode'		=> 0600,
				'user_info'	=> $user_info,
				'sort_func'	=> \&_uid_sort,
			)
		);
	}
	else
	{
		$psgconf->register_actions(
			PSGConf::Action::GenerateFile::etc_shadow->new(
				'name'		=> '/etc/shadow',
				'comment_str'	=> undef,
				'mode'		=> 0400,
				'user_info'	=> $user_info,
				'sort_func'	=> \&_uid_sort,
			)
		);
	}

	if ($psgconf->data_obj('platform')->match('solaris'))
	{
		$psgconf->register_actions(
			PSGConf::Action::GenerateFile::etc_user_attr->new(
				'name'		=> '/etc/user_attr',
				'mode'		=> 0644,
				'uid'		=> 0,
				'gid'		=> 3,
				'user_info'	=> $user_info
			)
		);
	}

	return
		if ($psgconf->data_obj('create_home_dirs')->equals('false'));

	foreach $user (sort keys %$user_info)
	{
		next
			if ($user_info->{$user}->{'home'} eq ''
			    || ! $user_info->{$user}->{'create_home_dir'});

		$mode->set($user_info->{$user}->{home_mode});

		$psgconf->register_actions(
			PSGConf::Action::HomeDir->new(
				user_info	=> $user_info->{$user},
				name		=> $user_info->{$user}->{home},
				uid		=> $user_info->{$user}->{home_uid},
				gid		=> $user_info->{$user}->{home_gid},
				mode		=> $mode->get()
			)
		);
	}
}


###############################################################################
###  Constructor
###############################################################################

sub new
{
	my ($class, $psgconf) = @_;
	my ($self);

	$self = {};
	bless($self, $class);

	$self->{name} = 'Users';

	$psgconf->register_data(
		'create_accounts'	=> PSGConf::Data::Boolean->new(
							value => 'false'
					),
		'create_home_dirs'	=> PSGConf::Data::Boolean->new(
							value => 'false'
					),
		'user_info'		=> PSGConf::Data::Hash->new(
						'value_type' => 'HASH',
						'value_optional' => 1
					),
		'default_home_mode'	=> PSGConf::Data::Integer->new(
							value => -1
					),
		'group_info'		=> PSGConf::Data::Hash->new(
						'value_type' => 'HASH',
						'value_optional' => 1
					),
		passwd_token		=> PSGConf::Data::String->new(
						value => 'x'
					),
	);

	$psgconf->register_policy($self,
		users_merge_existing	=> '_policy_merge_existing',
		validate_groups		=> '_policy_validate_groups',
		validate_users		=> '_policy_validate_users',
		validate_group_members	=> '_policy_validate_group_members'
	);

	return $self;
}

###############################################################################
###  cleanup() method
###############################################################################

sub cleanup
{
	my ($self, $psgconf) = @_;

	unlink($self->{tmpfile})
		if ( -e $self->{tmpfile} &&
			$psgconf->data_obj('platform')->match('freebsd') && 
			$psgconf->{rm_tmpfiles} );
}


###############################################################################
###  documentation
###############################################################################

1;

__END__

=head1 NAME

PSGConf::Control::Users - psgconf control class for user accounts

=head1 SYNOPSIS

In F<psgconf_modules>:

  Control PSGConf::Control::Users

=head1 DESCRIPTION

The B<PSGConf::Control::Users> module provides a B<psgconf> control object
for maintaining user account information.  It supports the following methods:

=over 4

=item new()

The constructor.  Its parameter is a reference to the B<PSGConf>
object.

The constructor registers the following data objects:

=over 4

=item I<create_accounts>

A B<PSGConf::Data::Boolean> object that indicates whether we should be
generating files that contain account information (i.e., F</etc/passwd>
and friends).

=item I<create_home_dirs>

A B<PSGConf::Data::Boolean> object that indicates whether we should
create home directories for new users.

=item I<default_home_mode>

A B<PSGConf::Data::Integer> object that sets the default mode for the
user's home dir.  Defaults to C<-1> which means do not change the current
mode.

=item I<user_info>

A B<PSGConf::Data::Hash> object that contains information about users to
create.  The hash key is the login of the user, and the value is a
reference to a hash of user attributes.

=item I<group_info>

A B<PSGConf::Data::Hash> object that contains information about groups to
create.  The hash key is the name of the group, and the value is a
reference to a hash of group attributes.

=item I<passwd_token>

A B<PSGConf::Data::String> object that contains the value to use in 
F</etc/passwd> as the password place holder.  Defaults to C<x>.

=back

The constructor also registers the following policy methods:

=over 4

=item I<users_merge_existing>

If I<create_accounts> is enabled, seeds the I<user_info> and I<group_info>
data objects with the system's existing user and group information.

=item I<validate_groups>

For each entry in I<group_info> that does not have the I<gid> attribute
set, assign an available gid.

=item I<validate_users>

For each entry in I<user_info>, fill in any missing fields.

If the I<uid> attribute is not set, assign an available uid.

If the I<gid> attribute is not set, but the I<group> attribute is set,
set the I<gid> attribute to the gid of the group named by the I<group>
attribute.  If the I<group> attribute is not set, set the I<gid> attribute
to the gid of group C<users>.  If group C<users> does not exist, assign
gid C<100>.

If the I<home> attribute is not set, set it to C</home/> followed by the
user's login.

If the I<home_uid> attribute is not set, set it to C<uid> if the home
directory is not C</>.

If the I<home_gid> attribute is not set, set it to C<gid> if the home
directory is not C</>.

If the I<home_mode> attribute is not set, set it to C<default_home_mode>
if the home directory is not C</>.

=item I<validate_group_members>

For each entry in I<group_info>, delete any non-existant users from the
group's I<members> list.  Also sorts the I<members> list by the uid of
the users.

=back

=item decide()

If I<create_accounts> is enabled, instantiates and registers action
objects, as follows:

=over 4

=item *

Registers a B<PSGConf::Action::GenerateFile::etc_passwd> action object to
create F</etc/passwd>.

=item *

Registers a B<PSGConf::Action::GenerateFile::etc_group> action object to
create F</etc/group>.

=item *

Under HP-UX, registers a B<PSGConf::Action::Symlink> action object to
create a symlink from F</etc/logingroup> to F</etc/group>.

=item *

Under AIX, registers a
B<PSGConf::Action::GenerateFile::etc_security_passwd> action object to
create F</etc/security/passwd>.

=item *

On platforms other than AIX, registers a
B<PSGConf::Action::GenerateFile::etc_shadow> action object to create
F</etc/shadow>.

=item *

Under AIX, registers a B<PSGConf::Action::ModifyFile> action object to
update F</etc/security/user>.

=item *

If I<create_home_dirs> is enabled, registers B<PSGConf::Action::HomeDir>
action objects to create the home directory for each user with the
I<create_home_dir> attribute enabled.

=back

=back

=head1 SEE ALSO

L<perl>

passwd(4)

group(4)

shadow(4)

L<PSGConf>

L<PSGConf::Action::GenerateFile::etc_passwd>

L<PSGConf::Action::GenerateFile::etc_group>

L<PSGConf::Action::GenerateFile::etc_shadow>

L<PSGConf::Action::GenerateFile::etc_security_passwd>

L<PSGConf::Action::GenerateFile::etc_user_attr>

L<PSGConf::Action::ModifyFile>

L<PSGConf::Action::HomeDir>

L<PSGConf::Action::Symlink>

L<PSGConf::Data::Boolean>

L<PSGConf::Data::Hash>

L<PSGConf::Data::Integer>

L<psgconf-intro>

=cut



syntax highlighted by Code2HTML, v. 0.9.1