### ### 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, '; 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, '; 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: Control PSGConf::Control::Users =head1 DESCRIPTION The B module provides a B 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 object. The constructor registers the following data objects: =over 4 =item I A B object that indicates whether we should be generating files that contain account information (i.e., F and friends). =item I A B object that indicates whether we should create home directories for new users. =item I A B 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 A B 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 A B 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 A B object that contains the value to use in F as the password place holder. Defaults to C. =back The constructor also registers the following policy methods: =over 4 =item I If I is enabled, seeds the I and I data objects with the system's existing user and group information. =item I For each entry in I that does not have the I attribute set, assign an available gid. =item I For each entry in I, fill in any missing fields. If the I attribute is not set, assign an available uid. If the I attribute is not set, but the I attribute is set, set the I attribute to the gid of the group named by the I attribute. If the I attribute is not set, set the I attribute to the gid of group C. If group C does not exist, assign gid C<100>. If the I attribute is not set, set it to C followed by the user's login. If the I attribute is not set, set it to C if the home directory is not C. If the I attribute is not set, set it to C if the home directory is not C. If the I attribute is not set, set it to C if the home directory is not C. =item I For each entry in I, delete any non-existant users from the group's I list. Also sorts the I list by the uid of the users. =back =item decide() If I is enabled, instantiates and registers action objects, as follows: =over 4 =item * Registers a B action object to create F. =item * Registers a B action object to create F. =item * Under HP-UX, registers a B action object to create a symlink from F to F. =item * Under AIX, registers a B action object to create F. =item * On platforms other than AIX, registers a B action object to create F. =item * Under AIX, registers a B action object to update F. =item * If I is enabled, registers B action objects to create the home directory for each user with the I attribute enabled. =back =back =head1 SEE ALSO L passwd(4) group(4) shadow(4) L L L L L L L L L L L L L =cut