3 Functions for Unix user and group management.
5 foreign_require("useradmin", "user-lib.pl");
6 @users = useradmin::list_users();
7 @groups = useradmin::list_groups();
8 ($joe) = grep { $_->{'user'} eq 'joe' } @users;
10 $joe->{'pass'} = useradmin::encrypt_password('smeg');
11 useradmin::making_changes()
12 useradmin::modify_user($joe, $joe);
13 useradmin::made_changes()
18 BEGIN { push(@INC, ".."); };
21 if ($gconfig{'os_type'} =~ /-linux$/) {
25 do "$gconfig{'os_type'}-lib.pl";
28 %access = &get_module_acl();
30 @random_password_chars = ( 'a' .. 'z', 'A' .. 'Z', '0' .. '9' );
31 $disable_string = $config{'lock_prepend'} eq "" ? "!" : $config{'lock_prepend'};
34 $match_modes = [ [ 0, $text{'index_equals'} ], [ 4, $text{'index_contains'} ],
35 [ 1, $text{'index_matches'} ], [ 2, $text{'index_nequals'} ],
36 [ 5, $text{'index_ncontains'} ], [ 3, $text{'index_nmatches'}],
37 [ 6, $text{'index_lower'} ], [ 7, $text{'index_higher'} ] ];
39 =head2 password_file(file)
41 Returns true if some file looks like a valid Unix password file
46 if (!$_[0]) { return 0; }
47 elsif (&open_readfile(SHTEST, $_[0])) {
51 return $line =~ /^\S+:\S*:/;
58 Returns an array of hash references, each containing info about one user. Each
59 hash will always contain the keys :
61 =item user - The Unix username.
63 =item pass - Encrypted password, perhaps using MD5 or DES.
65 =item uid - User's ID.
67 =item gid - User's primary group's ID.
69 =item real - Real name for the user. May also contain office phone, home phone and office location, comma-separated.
71 =item home - User's home directory.
73 =item shell - Shell command to run when the user logs in.
75 In addition, if the system supports shadow passwords it may also have the keys :
77 =item change - Days since 1970 the password was last changed.
79 =item min - Days before password may be changed.
81 =item max - Days after which password must be changed.
83 =item warn - Days before password is to expire that user is warned.
85 =item inactive - Days after password expires that account is disabled.
87 =item expire - Days since Jan 1, 1970 that account is disabled.
89 Or if it supports FreeBSD master.passwd info, it will also have keys :
91 =item class - User's login class.
93 =item change - Unix time at which the password was last changed.
95 =item expire - Unix time at which the password will expire.
100 return @list_users_cache if (scalar(@list_users_cache));
102 # read the password file
103 local (@rv, $_, %idx, $lnum, @pw, $p, $i, $j);
104 local $pft = &passfiles_type();
106 # read the master.passwd file only
108 &open_readfile(PASSWD, $config{'master_file'});
111 if (/\S/ && !/^[#\+\-]/) {
112 @pw = split(/:/, $_, -1);
113 push(@rv, { 'user' => $pw[0], 'pass' => $pw[1],
114 'uid' => $pw[2], 'gid' => $pw[3],
115 'class' => $pw[4], 'change' => $pw[5],
116 'expire' => $pw[6], 'real' => $pw[7],
117 'home' => $pw[8], 'shell' => $pw[9],
118 'line' => $lnum, 'num' => scalar(@rv) });
126 &open_execute_command(PASSWD, "nidump passwd '$netinfo_domain'", 1);
129 if (/\S/ && !/^[#\+\-]/) {
130 @pw = split(/:/, $_, -1);
131 push(@rv, { 'user' => $pw[0], 'pass' => $pw[1],
132 'uid' => $pw[2], 'gid' => $pw[3],
133 'class' => $pw[4], 'change' => $pw[5],
134 'expire' => $pw[6], 'real' => $pw[7],
135 'home' => $pw[8], 'shell' => $pw[9],
136 'num' => scalar(@rv) });
142 # Read directory services dump of users
143 &open_execute_command(PASSWD,
144 "dscl '$netinfo_domain' readall /Users", 1);
146 local $ls = $config{'lock_string'};
150 # End of the current user
153 elsif (/^(\S+):\s*(.*)$/) {
156 $user = { 'num' => scalar(@rv) };
159 local ($n, $v) = ($1, $2);
160 if ($n ne 'RealName' && $v eq '') {
165 local $p = $user_properties_map{$n};
167 # Some OSX users have two names, like _foo foo
168 $v =~ s/\s.*$// if ($p eq 'user');
171 elsif ($n eq "GeneratedUID") {
172 # Given the UID, we can get the password hash
173 $user->{'pass'} = &get_macos_password_hash($v);
174 $user->{'uuid'} = $v;
175 if (substr($user->{'pass'}, 0,
176 length($ls)) eq $ls) {
178 $user->{'pass'} = $ls;
186 # start by reading /etc/passwd
188 &open_readfile(PASSWD, $config{'passwd_file'});
191 if (/\S/ && !/^[#\+\-]/) {
192 @pw = split(/:/, $_, -1);
193 push(@rv, { 'user' => $pw[0], 'pass' => $pw[1],
194 'uid' => $pw[2], 'gid' => $pw[3],
195 'real' => $pw[4], 'home' => $pw[5],
196 'shell' => $pw[6], 'line' => $lnum,
197 'num' => scalar(@rv) });
198 $idx{$pw[0]} = $rv[$#rv];
203 if ($pft == 2 || $pft == 5) {
204 # read the shadow file data
206 &open_readfile(SHADOW, $config{'shadow_file'});
209 if (/\S/ && !/^[#\+\-]/) {
210 @pw = split(/:/, $_, -1);
212 $p->{'pass'} = $pw[1];
213 $p->{'change'} = $pw[2] < 0 ? "" : $pw[2];
214 $p->{'min'} = $pw[3] < 0 ? "" : $pw[3];
215 $p->{'max'} = $pw[4] < 0 ? "" : $pw[4];
216 $p->{'warn'} = $pw[5] < 0 ? "" : $pw[5];
217 $p->{'inactive'} = $pw[6] < 0 ? "" : $pw[6];
218 $p->{'expire'} = $pw[7] < 0 ? "" : $pw[7];
219 $p->{'sline'} = $lnum;
224 for($i=0; $i<@rv; $i++) {
225 if (!defined($rv[$i]->{'sline'})) {
227 for($j=$i; $j<@rv; $j++) { $rv[$j]->{'num'}--; }
228 splice(@rv, $i--, 1);
233 # read the AIX security passwd file
236 &open_readfile(SECURITY, $config{'shadow_file'});
240 $lastuser = $idx{$1};
241 $lastuser->{'sline'} = $lnum;
243 elsif (/^\s*([^=\s]+)\s*=\s*(.*)/) {
244 if ($1 eq 'password') {
245 $lastuser->{'pass'} = $2;
247 elsif ($1 eq 'lastupdate') {
248 $lastuser->{'change'} = $2;
250 elsif ($1 eq 'flags') {
251 map { $lastuser->{lc($_)}++ }
254 $lastuser->{'seline'} = $lnum;
260 # read the AIX security user file
261 &open_readfile(USER, $config{'aix_user_file'});
265 $lastuser = $idx{$1};
267 elsif (/^\s*([^=\s]+)\s*=\s*(.*)/) {
268 if ($1 eq 'expires') {
269 $lastuser->{'expire'} = $2;
271 elsif ($1 eq 'minage') {
272 $lastuser->{'min'} = $2;
274 elsif ($1 eq 'maxage') {
275 $lastuser->{'max'} = $2;
277 elsif ($1 eq 'pwdwarntime') {
278 $lastuser->{'warn'} = $2;
285 @list_users_cache = @rv;
289 =head2 create_user(&details)
291 Creates a new user with the given details, supplied in a hash ref. This must
292 be in the same format as returned by list_users, and must contain at a minimum
293 the user, uid, gid, pass, shell, home and real keys.
299 local $pft = &passfiles_type();
301 # just need to add to master.passwd
302 $lref = &read_file_lines($config{'master_file'});
303 $_[0]->{'line'} = &nis_index($lref);
304 splice(@$lref, $_[0]->{'line'}, 0,
305 "$_[0]->{'user'}:$_[0]->{'pass'}:$_[0]->{'uid'}:".
306 "$_[0]->{'gid'}:$_[0]->{'class'}:$_[0]->{'change'}:".
307 "$_[0]->{'expire'}:$_[0]->{'real'}:$_[0]->{'home'}:".
309 if (scalar(@list_users_cache)) {
310 map { $_->{'line'}++ if ($_->{'line'} >= $_[0]->{'line'}) }
315 # Just invoke the useradd command
316 &system_logged("useradd -u $_[0]->{'uid'} -g $_[0]->{'gid'} -c \"$_[0]->{'real'}\" -d $_[0]->{'home'} -s $_[0]->{'shell'} $_[0]->{'user'}");
317 # And set the password
318 &system_logged("echo ".quotemeta($_[0]->{'pass'}).
319 " | /usr/lib/scoadmin/account/password.tcl ".
320 "$_[0]->{'user'} >/dev/null 2>&1");
323 # Use the niutil command
324 &system_logged("niutil -create '$netinfo_domain' '/users/$_[0]->{'user'}'");
328 # Add to directory services
329 &execute_dscl_command("create", "/Users/$_[0]->{'user'}");
330 local $out = &execute_dscl_command("read", "/Users/$_[0]->{'user'}");
331 if ($out =~ /GeneratedUID:\s+(\S+)/) {
332 $_[0]->{'uuid'} = $1;
334 &set_user_dirinfo($_[0]);
338 $lref = &read_file_lines($config{'passwd_file'});
339 $_[0]->{'line'} = &nis_index($lref);
340 if (scalar(@list_users_cache)) {
341 map { $_->{'line'}++ if ($_->{'line'} >= $_[0]->{'line'}) }
344 splice(@$lref, $_[0]->{'line'}, 0,
346 ($pft == 2 || $pft == 5 ? "x" : $pft == 4 ? "!" :
348 ":$_[0]->{'uid'}:$_[0]->{'gid'}:$_[0]->{'real'}:".
349 "$_[0]->{'home'}:$_[0]->{'shell'}");
350 if ($pft == 2 || $pft == 5) {
351 # Find correct place to insert in shadow file
352 $lref = &read_file_lines($config{'shadow_file'});
353 $_[0]->{'sline'} = &nis_index($lref);
354 if (scalar(@list_users_cache)) {
355 map { $_->{'sline'}++
356 if ($_->{'sline'} >= $_[0]->{'sline'}) }
361 # add to shadow as well..
362 splice(@$lref, $_[0]->{'sline'}, 0,
363 "$_[0]->{'user'}:$_[0]->{'pass'}:$_[0]->{'change'}:".
364 "$_[0]->{'min'}:$_[0]->{'max'}:$_[0]->{'warn'}:".
365 "$_[0]->{'inactive'}:$_[0]->{'expire'}:");
368 # add to SCO shadow file
369 splice(@$lref, $_[0]->{'sline'}, 0,
370 "$_[0]->{'user'}:$_[0]->{'pass'}:$_[0]->{'change'}:".
371 "$_[0]->{'min'}:$_[0]->{'max'}");
374 # add to AIX security passwd file as well..
376 push(@flags, 'ADMIN') if ($_[0]->{'admin'});
377 push(@flags, 'ADMCHG') if ($_[0]->{'admchg'});
378 push(@flags, 'NOCHECK') if ($_[0]->{'nocheck'});
379 $lref = &read_file_lines($config{'shadow_file'});
380 push(@$lref, "", "$_[0]->{'user'}:",
381 "\tpassword = $_[0]->{'pass'}",
382 "\tlastupdate = $_[0]->{'change'}",
383 "\tflags = ".join(",", @flags));
385 # add to AIX security user file as well..
386 $lref = &read_file_lines($config{'aix_user_file'});
387 if ($_[0]->{'expire'} || $_[0]->{'min'} ||
388 $_[0]->{'max'} || $_[0]->{'warn'} ) {
389 push(@$lref, "$_[0]->{'user'}:");
390 push(@$lref, "\texpires = $_[0]->{'expire'}")
391 if ($_[0]->{'expire'});
392 push(@$lref, "\tminage = $_[0]->{'min'}")
394 push(@$lref, "\tmaxage = $_[0]->{'max'}")
396 push(@$lref, "\tpwdwarntime = $_[0]->{'warn'}")
397 if ($_[0]->{'warn'});
402 &flush_file_lines() if (!$batch_mode);
403 push(@list_users_cache, $_[0]) if (scalar(@list_users_cache));
404 &refresh_nscd() if (!$batch_mode);
407 =head2 modify_user(&old, &details)
409 Update an existing Unix user with new details. The user to change must be
410 in &old, and the new values are in &details. These can be references to the
411 same hash if you like.
416 $_[0] || &error("Missing parameter to modify_user");
417 local(@passwd, @shadow, $lref);
418 local $pft = &passfiles_type();
420 # just need to update master.passwd
421 $_[0]->{'line'} =~ /^\d+$/ || &error("Missing user line to modify");
422 $lref = &read_file_lines($config{'master_file'});
423 $lref->[$_[0]->{'line'}] =
424 "$_[1]->{'user'}:$_[1]->{'pass'}:$_[1]->{'uid'}:".
425 "$_[1]->{'gid'}:$_[1]->{'class'}:$_[1]->{'change'}:".
426 "$_[1]->{'expire'}:$_[1]->{'real'}:$_[1]->{'home'}:".
430 # Just use the usermod command
431 &system_logged("usermod -u $_[1]->{'uid'} -g $_[1]->{'gid'} -c \"$_[1]->{'real'}\" -d $_[1]->{'home'} -s $_[1]->{'shell'} $_[1]->{'user'}");
432 &system_logged("echo ".quotemeta($_[1]->{'pass'})." | /usr/lib/scoadmin/account/password.tcl $_[1]->{'user'}");
435 # Just use the niutil command to update
436 if ($_[0]->{'user'} && $_[0]->{'user'} ne $_[1]->{'user'}) {
437 # Need to delete and re-create!
438 &system_logged("niutil -destroy '$netinfo_domain' '/users/$_[0]->{'user'}'");
439 &system_logged("niutil -create '$netinfo_domain' '/users/$_[1]->{'user'}'");
444 # Call directory services to update the user
445 if ($_[0]->{'user'} && $_[0]->{'user'} ne $_[1]->{'user'}) {
447 &execute_dscl_command("change", "/Users/$_[0]->{'user'}",
448 "RecordName", $_[0]->{'user'}, $_[1]->{'user'});
450 $_[1]->{'uuid'} = $_[0]->{'uuid'};
451 &set_user_dirinfo($_[1]);
455 $lref = &read_file_lines($config{'passwd_file'});
456 $_[0]->{'line'} =~ /^\d+$/ || &error("Missing user line to modify");
457 $lref->[$_[0]->{'line'}] =
459 ($pft == 2 || $pft == 5 ? "x" : $pft == 4 ? "!" :
461 ":$_[1]->{'uid'}:$_[1]->{'gid'}:$_[1]->{'real'}:".
462 "$_[1]->{'home'}:$_[1]->{'shell'}";
464 # update shadow file as well..
465 $_[0]->{'sline'} =~ /^\d+$/ ||
466 &error("Missing user line to modify");
467 $lref = &read_file_lines($config{'shadow_file'});
468 $lref->[$_[0]->{'sline'}] =
469 "$_[1]->{'user'}:$_[1]->{'pass'}:$_[1]->{'change'}:".
470 "$_[1]->{'min'}:$_[1]->{'max'}:$_[1]->{'warn'}:".
471 "$_[1]->{'inactive'}:$_[1]->{'expire'}:";
475 $_[0]->{'sline'} =~ /^\d+$/ ||
476 &error("Missing user line to modify");
477 $lref = &read_file_lines($config{'shadow_file'});
478 $lref->[$_[0]->{'sline'}] =
479 "$_[1]->{'user'}:$_[1]->{'pass'}:$_[1]->{'change'}:".
480 "$_[1]->{'min'}:$_[1]->{'max'}";
483 # update AIX shadow passwd file as well..
484 if (defined($_[0]->{'sline'})) {
486 push(@flags, 'ADMIN') if ($_[1]->{'admin'});
487 push(@flags, 'ADMCHG') if ($_[1]->{'admchg'});
488 push(@flags, 'NOCHECK') if ($_[1]->{'nocheck'});
489 local $lref = &read_file_lines($config{'shadow_file'});
490 splice(@$lref, $_[0]->{'sline'},
491 $_[0]->{'seline'} - $_[0]->{'sline'} + 1,
492 "$_[1]->{'user'}:", "\tpassword = $_[1]->{'pass'}",
493 "\tlastupdate = $_[1]->{'change'}",
494 "\tflags = ".join(",", @flags));
495 &flush_file_lines(); # have to flush on AIX
498 # update AIX security user file as well..
499 # use chuser command because it's easier than working
500 # with the complexity issues of the file.
501 &system_logged("chuser expires=$_[1]->{'expire'} minage=$_[1]->{'min'} maxage=$_[1]->{'max'} pwdwarntime=$_[1]->{'warn'} $_[1]->{'user'}");
504 if ($_[0] ne $_[1] && &indexof($_[0], @list_users_cache) != -1) {
505 # Update old object in cache
506 $_[1]->{'line'} = $_[0]->{'line'} if (defined($_[0]->{'line'}));
507 $_[1]->{'uuid'} = $_[0]->{'uuid'} if (defined($_[0]->{'uuid'}));
508 $_[1]->{'sline'} = $_[0]->{'sline'} if (defined($_[0]->{'sline'}));
509 $_[1]->{'seline'} = $_[0]->{'seline'} if (defined($_[0]->{'seline'}));
518 =head2 delete_user(&details)
520 Delete an existing user. The &details hash must be user information as
521 returned by list_users.
527 $_[0] || &error("Missing parameter to delete_user");
528 local $pft = &passfiles_type();
530 # Delete from BSD master.passwd file
531 $_[0]->{'line'} =~ /^\d+$/ || &error("Missing user line to delete");
532 $lref = &read_file_lines($config{'master_file'});
533 splice(@$lref, $_[0]->{'line'}, 1);
534 map { $_->{'line'}-- if ($_->{'line'} > $_[0]->{'line'}) }
538 # Just invoke the userdel command
539 &system_logged("userdel -n0 $_[0]->{'user'}");
542 # Just invoke the rmuser command
543 &system_logged("rmuser -p $_[0]->{'user'}");
546 # Just delete with the niutil command
547 &system_logged("niutil -destroy '$netinfo_domain' '/users/$_[0]->{'user'}'");
550 # Delete from directory services
551 &execute_dscl_command("delete", "/Users/$_[0]->{'user'}");
554 # XXX doesn't delete from AIX file!
555 $_[0]->{'line'} =~ /^\d+$/ || &error("Missing user line to delete");
556 $lref = &read_file_lines($config{'passwd_file'});
557 splice(@$lref, $_[0]->{'line'}, 1);
558 map { $_->{'line'}-- if ($_->{'line'} > $_[0]->{'line'}) }
560 if ($pft == 2 || $pft == 5) {
561 if (defined($_[0]->{'sline'})) {
562 $lref = &read_file_lines($config{'shadow_file'});
563 splice(@$lref, $_[0]->{'sline'}, 1);
564 map { $_->{'sline'}--
565 if ($_->{'sline'} > $_[0]->{'sline'}) }
570 @list_users_cache = grep { $_->{'user'} ne $_[0]->{'user'} } @list_users_cache
571 if (scalar(@list_users_cache));
580 Returns a list of all the local groups as an array of hashes. Each will
583 =item group - The group name.
585 =item pass - Rarely-used encrypted password, in DES or MD5 format.
587 =item gid - Unix ID for the group.
589 =item members - A comma-separated list of secondary group members.
594 return @list_groups_cache if (scalar(@list_groups_cache));
596 local(@rv, $lnum, $_, %idx, $g, $i, $j, @gr);
598 local $gft = &groupfiles_type();
600 # Get groups from netinfo
601 &open_execute_command(GROUP, "nidump group '$netinfo_domain'", 1);
604 if (/\S/ && !/^[#\+\-]/) {
605 @gr = split(/:/, $_, -1);
606 push(@rv, { 'group' => $gr[0], 'pass' => $gr[1],
608 'members' => join(",",split(/\s+/,$gr[3])),
609 'num' => scalar(@rv) });
615 # Read directory services dump of groups
616 &open_execute_command(PASSWD,
617 "dscl '$netinfo_domain' readall /Groups", 1);
622 # End of the current group
625 elsif (/^(\S+):\s*(.*)$/) {
628 $group = { 'num' => scalar(@rv) };
631 local ($n, $v) = ($1, $2);
632 if ($n ne 'GroupMembership' && $v eq '') {
637 local $p = $group_properties_map{$n};
639 # Convert spaces in members list to ,
640 $v =~ s/ /,/g if ($p eq 'members');
641 # Some OSX groups have two names, like _foo foo
642 $v =~ s/\s.*$// if ($p eq 'group');
645 elsif ($n eq "GeneratedUID") {
646 # Given the UUID, we can get the password hash
647 $group->{'pass'} = &get_macos_password_hash($v);
648 $group->{'uuid'} = $v;
655 # Read the standard group file
656 &open_readfile(GROUP, $config{'group_file'});
659 if (/\S/ && !/^[#\+\-]/) {
660 @gr = split(/:/, $_, -1);
661 push(@rv, { 'group' => $gr[0], 'pass' => $gr[1],
662 'gid' => $gr[2], 'members' => $gr[3],
663 'line' => $lnum, 'num' => scalar(@rv) });
664 $idx{$gr[0]} = $rv[$#rv];
671 # read the gshadow file data
673 &open_readfile(SHADOW, $config{'gshadow_file'});
676 if (/\S/ && !/^[#\+\-]/) {
677 @gr = split(/:/, $_, -1);
679 $g->{'pass'} = $gr[1];
680 $g->{'sline'} = $lnum;
685 #for($i=0; $i<@rv; $i++) {
686 # if (!defined($rv[$i]->{'sline'})) {
688 # for($j=$i; $j<@rv; $j++) { $rv[$j]->{'num'}--; }
689 # splice(@rv, $i--, 1);
694 # read the AIX group data
697 &open_readfile(SECURITY, $config{'gshadow_file'});
701 $lastgroup = $idx{$1};
702 $lastgroup->{'sline'} = $lnum;
703 $lastgroup->{'seline'} = $lnum;
705 elsif (/^\s*([^=\s]+)\s*=\s*(.*)/) {
706 $lastgroup->{'seline'} = $lnum;
712 @list_groups_cache = @rv;
716 =head2 create_group(&details)
718 Create a new Unix group based on the given hash. Required keys are
721 pass - Encrypted password
722 members - Comma-separated list of members
727 local $gft = &groupfiles_type();
730 &system_logged("niutil -create '$netinfo_domain' '/groups/$_[0]->{'group'}'");
731 &set_group_netinfo($_[0]);
734 # Use the dscl directory services command
735 &execute_dscl_command("create", "/Groups/$_[0]->{'group'}");
736 &set_group_dirinfo($_[0]);
739 # Update group file(s)
741 $lref = &read_file_lines($config{'group_file'});
742 $_[0]->{'line'} = &nis_index($lref);
743 if (scalar(@list_groups_cache)) {
744 map { $_->{'line'}++ if ($_->{'line'} >= $_[0]->{'line'}) }
747 splice(@$lref, $_[0]->{'line'}, 0,
749 (&groupfiles_type() == 2 ? "x" : $_[0]->{'pass'}).
750 ":$_[0]->{'gid'}:$_[0]->{'members'}");
752 $lref = &read_file_lines($config{'gshadow_file'});
753 $_[0]->{'sline'} = &nis_index($lref);
754 if (scalar(@list_groups_cache)) {
755 map { $_->{'sline'}++
756 if ($_->{'sline'} >= $_[0]->{'sline'}) }
759 splice(@$lref, $_[0]->{'sline'}, 0,
760 "$_[0]->{'group'}:$_[0]->{'pass'}::$_[0]->{'members'}");
763 $lref = &read_file_lines($config{'gshadow_file'});
764 $_[0]->{'sline'} = scalar(@$lref);
765 push(@$lref, "", "$_[0]->{'group'}:", "\tadmin = false");
770 push(@list_groups_cache, $_[0]) if (scalar(@list_groups_cache));
773 =head2 modify_group(&old, &details)
775 Update an existing Unix group specified in old based on the given details hash.
776 These can both be references to the same hash if you like. The hash must be
777 in the same format as returned by list_groups.
782 $_[0] || &error("Missing parameter to modify_group");
783 local $gft = &groupfiles_type();
785 # Call niutil to update the group
786 if ($_[0]->{'group'} && $_[0]->{'group'} ne $_[1]->{'group'}) {
787 # Need to delete and re-create!
788 &system_logged("niutil -destroy '$netinfo_domain' '/groups/$_[0]->{'group'}'");
789 &system_logged("niutil -create '$netinfo_domain' '/groups/$_[1]->{'group'}'");
791 &set_group_netinfo($_[1]);
794 # Call dscl to update the group
795 if ($_[0]->{'group'} && $_[0]->{'group'} ne $_[1]->{'group'}) {
797 &execute_dscl_command("change", "/Groups/$_[0]->{'group'}",
798 "RecordName", $_[0]->{'group'}, $_[1]->{'group'});
800 $_[1]->{'uuid'} = $_[0]->{'uuid'};
801 &set_group_dirinfo($_[1]);
805 local $gs = (&groupfiles_type() == 2 && $_[0]->{'sline'} ne '');
806 &replace_file_line($config{'group_file'}, $_[0]->{'line'},
807 "$_[1]->{'group'}:".($gs ? "x" : $_[1]->{'pass'}).
808 ":$_[1]->{'gid'}:$_[1]->{'members'}\n");
810 &replace_file_line($config{'gshadow_file'}, $_[0]->{'sline'},
811 "$_[1]->{'group'}:$_[1]->{'pass'}::$_[1]->{'members'}\n");
813 elsif (&groupfiles_type() == 4) {
814 &replace_file_line($config{'gshadow_file'},
816 "$_[1]->{'group'}:\n");
819 if ($_[0] ne $_[1] && &indexof($_[0], @list_groups_cache) != -1) {
820 $_[1]->{'line'} = $_[0]->{'line'} if (defined($_[0]->{'line'}));
821 $_[1]->{'sline'} = $_[0]->{'sline'} if (defined($_[0]->{'sline'}));
822 $_[1]->{'uuid'} = $_[0]->{'uuid'} if (defined($_[0]->{'uuid'}));
828 =head2 delete_group(&details)
830 Delete an existing Unix group, whose details are in the hash ref supplied.
835 $_[0] || &error("Missing parameter to delete_group");
836 local $gft = &groupfiles_type();
838 # Call niutil to delete
839 &system_logged("niutil -destroy '$netinfo_domain' '/groups/$_[0]->{'group'}'");
842 # Delete from directory services
843 &execute_dscl_command("delete", "/Groups/$_[0]->{'group'}");
846 # Remove from group file(s)
847 &replace_file_line($config{'group_file'}, $_[0]->{'line'});
848 map { $_->{'line'}-- if ($_->{'line'} > $_[0]->{'line'}) }
850 if ($gft == 2 && $_[0]->{'sline'} ne '') {
851 &replace_file_line($config{'gshadow_file'}, $_[0]->{'sline'});
852 map { $_->{'sline'}-- if ($_->{'sline'} > $_[0]->{'sline'}) }
856 local $lref = &read_file_lines($config{'gshadow_file'});
857 splice(@$lref, $_[0]->{'sline'},
858 $_[0]->{'seline'} - $_[0]->{'sline'} + 1);
862 @list_groups_cache = grep { $_ ne $_[0] } @list_groups_cache
863 if (scalar(@list_groups_cache));
868 =head2 recursive_change(dir, olduid, oldgid, newuid, newgid)
870 Change the UID or GID of a directory and all files in it, if they match the
871 given old UID and/or GID. If either of the old IDs are -1, then they are
872 ignored for match purposes.
877 local(@list, $f, @stbuf);
878 local $real = &translate_filename($_[0]);
879 (@stbuf = stat($real)) || return;
880 (-l $real) && return;
881 if (($_[1] < 0 || $_[1] == $stbuf[4]) &&
882 ($_[2] < 0 || $_[2] == $stbuf[5])) {
884 &set_ownership_permissions(
885 $_[3] < 0 ? $stbuf[4] : $_[3],
886 $_[4] < 0 ? $stbuf[5] : $_[4], undef, $_[0]);
890 @list = readdir(DIR);
893 if ($f eq "." || $f eq "..") { next; }
894 &recursive_change("$_[0]/$f", $_[1], $_[2], $_[3], $_[4]);
899 =head2 making_changes
901 Must be called before changes are made to the password or group file.
906 if ($config{'pre_command'} =~ /\S/) {
907 local $out = &backquote_logged("($config{'pre_command'}) 2>&1 </dev/null");
908 return $? ? $out : undef;
915 Must be called after the password or group file has been changed, to run the
916 post-changes command.
921 if ($config{'post_command'} =~ /\S/) {
922 local $out = &backquote_logged("($config{'post_command'}) 2>&1 </dev/null");
923 return $? ? $out : undef;
928 =head2 other_modules(function, arg, ...)
930 Call some function in the useradmin_update.pl file in other modules. Should be
931 called after creating, deleting or modifying a user.
936 return if (&is_readonly_mode()); # don't even try other modules
938 local $func = shift(@_);
939 foreach $m (&get_all_module_infos()) {
940 local $mdir = &module_root_directory($m->{'dir'});
941 if (&check_os_support($m) &&
942 -r "$mdir/useradmin_update.pl") {
943 &foreign_require($m->{'dir'}, "useradmin_update.pl");
944 local $pkg = $m->{'dir'};
945 $pkg =~ s/[^A-Za-z0-9]/_/g;
946 local $fullfunc = "${pkg}::${func}";
947 if (defined(&$fullfunc)) {
948 &foreign_call($m->{'dir'}, $func, @_);
954 =head2 can_edit_user(&acl, &user)
956 Returns 1 if the given user hash can be edited by a Webmin user whose access
957 control permissions for this module are in the acl parameter.
962 local $m = $_[0]->{'uedit_mode'};
964 if ($m == 0) { return 1; }
965 elsif ($m == 1) { return 0; }
966 elsif ($m == 2 || $m == 3 || $m == 5) {
967 map { $u{$_}++ } &split_quoted_string($_[0]->{'uedit'});
968 if ($m == 5 && $_[0]->{'uedit_sec'}) {
969 # Check secondary groups too
970 return 1 if ($u{$_[1]->{'gid'}});
971 foreach $g (&list_groups()) {
972 local @m = split(/,/, $g->{'members'});
973 return 1 if ($u{$g->{'gid'}} &&
974 &indexof($_[1]->{'user'}, @m) >= 0);
979 return $m == 2 ? $u{$_[1]->{'user'}} :
980 $m == 3 ? !$u{$_[1]->{'user'}} :
985 return (!$_[0]->{'uedit'} || $_[1]->{'uid'} >= $_[0]->{'uedit'}) &&
986 (!$_[0]->{'uedit2'} || $_[1]->{'uid'} <= $_[0]->{'uedit2'});
989 return $_[1]->{'user'} eq $remote_user;
992 return $_[1]->{'user'} =~ /$_[0]->{'uedit_re'}/;
997 =head2 can_edit_group(&acl, &group)
999 Returns 1 if the given group hash can be edited by a Webmin user whose access
1000 control permissions for this module are in the acl parameter.
1005 local $m = $_[0]->{'gedit_mode'};
1007 if ($m == 0) { return 1; }
1008 elsif ($m == 1) { return 0; }
1009 elsif ($m == 2 || $m == 3) {
1010 map { $g{$_}++ } &split_quoted_string($_[0]->{'gedit'});
1011 return $m == 2 ? $g{$_[1]->{'group'}}
1012 : !$g{$_[1]->{'group'}};
1014 else { return (!$_[0]->{'gedit'} || $_[1]->{'gid'} >= $_[0]->{'gedit'}) &&
1015 (!$_[0]->{'gedit2'} || $_[1]->{'gid'} <= $_[0]->{'gedit2'}); }
1018 =head2 nis_index(&lines)
1020 Internal function to return the line number on which NIS includes start
1021 in a password or group file.
1027 for($i=0; $i<@{$_[0]}; $i++) {
1028 last if ($_[0]->[$i] =~ /^[\+\-]/);
1033 =head2 get_skel_directory(&user, groupname)
1035 Returns the skeleton files directory for some user. The groupname parameter
1036 must be the name of his primary group.
1039 sub get_skel_directory
1041 local ($user, $groupname) = @_;
1042 local $uf = $config{'user_files'};
1043 local $shell = $user->{'shell'};
1044 $shell =~ s/^(.*)\///g;
1045 if ($groupname ne '') {
1046 $uf =~ s/\$group/$groupname/g;
1048 $uf =~ s/\$gid/$user->{'gid'}/g;
1049 $uf =~ s/\$shell/$shell/g;
1053 =head2 copy_skel_files(source, dest, uid, gid)
1055 Copies skeleton files from some source directory (such as /etc/skel) to a
1056 destination directory, typically a new user's home. The uid and gid are the
1057 IDs of the new user, which determines file ownership.
1064 foreach $f (split(/\s+/, $_[0])) {
1066 # copy all files in a directory
1068 foreach $df (readdir(DIR)) {
1069 if ($df eq "." || $df eq "..") { next; }
1070 push(@rv, ©_file("$f/$df", $_[1], $_[2], $_[3]));
1075 # copy just one file
1076 push(@rv, ©_file($f, $_[1], $_[2], $_[3]));
1082 =head2 copy_file(file, destdir, uid, gid)
1084 Copy a file or directory and chown it, preserving symlinks and special files.
1085 Mainly for internal use by copy_skel_files.
1090 local($base, $subs);
1091 $_[0] =~ /\/([^\/]+)$/; $base = $1;
1092 if ($config{"files_remap_$base"}) {
1093 $base = $config{"files_remap_$base"};
1095 $subs = $config{'files_remove'};
1096 $base =~ s/$subs//g if ($subs);
1097 local ($opts, $nochown);
1098 local @rv = ( "$_[1]/$base" );
1099 if (-b $_[0] || -c $_[0]) {
1100 # Looks like a device file .. re-create it
1101 local @st = stat($_[0]);
1102 local $maj = int($st[6] / 256);
1103 local $min = $st[6] % 256;
1104 local $typ = ($st[2] & 00170000) == 0020000 ? 'c' : 'b';
1105 &system_logged("mknod ".quotemeta("$_[1]/$base")." $typ $maj $min");
1106 &set_ownership_permissions($_[2], $_[3], undef, "$_[1]/$base");
1109 elsif (-l $_[0] && !$config{'copy_symlinks'}) {
1110 # A symlink .. re-create it
1111 local $l = readlink($_[0]);
1112 &system_logged("ln -s ".quotemeta($l)." ".quotemeta("$_[1]/$base")." >/dev/null 2>/dev/null");
1116 # A directory .. copy it recursively
1117 &system_logged("cp -Rp ".quotemeta($_[0])." ".quotemeta("$_[1]/$base")." >/dev/null 2>/dev/null");
1118 local $glob = "$_[1]/$base/*";
1120 local @g = glob($glob);
1121 if (@g && -r $g[0]) {
1131 # Just a normal file .. copy it
1132 local @st = stat(&translate_filename($_[0]));
1133 &system_logged("cp ".quotemeta($_[0])." ".quotemeta("$_[1]/$base")." >/dev/null 2>/dev/null");
1134 &set_ownership_permissions($_[2], $_[3], $st[2], "$_[1]/$base");
1137 &system_logged("chown $opts -R $_[2]:$_[3] ".quotemeta("$_[1]/$base").
1138 " >/dev/null 2>/dev/null") if (!$nochown);
1142 =head2 lock_user_files
1144 Lock all password, shadow and group files. Should be called before performing
1145 any user or group operations.
1150 &lock_file($config{'passwd_file'});
1151 &lock_file($config{'group_file'});
1152 &lock_file($config{'shadow_file'});
1153 &lock_file($config{'gshadow_file'});
1154 &lock_file($config{'master_file'});
1157 =head2 unlock_user_files
1159 Unlock all password, shadow and group files. Should be called after all user
1160 or group operations are complete.
1163 sub unlock_user_files
1165 &unlock_file($config{'passwd_file'});
1166 &unlock_file($config{'group_file'});
1167 &unlock_file($config{'shadow_file'});
1168 &unlock_file($config{'gshadow_file'});
1169 &unlock_file($config{'master_file'});
1174 The same as Perl's setpwent function, but may read from /etc/passwd directly.
1179 if ($config{'from_files'}) {
1180 @setpwent_cache = &list_users();
1183 else { return setpwent(); }
1188 The same as Perl's getpwent function, but may read from /etc/passwd directly.
1193 if ($config{'from_files'}) {
1194 my_setpwent() if (!@setpwent_cache);
1195 if ($setpwent_pos >= @setpwent_cache) {
1196 return wantarray ? () : undef;
1199 return &pw_user_rv($setpwent_cache[$setpwent_pos++],
1203 else { return getpwent(); }
1208 Should be called when you are done with my_setpwent and my_getpwent.
1213 if ($config{'from_files'}) {
1214 undef(@setpwent_cache);
1216 elsif ($gconfig{'os_type'} eq 'hpux') {
1217 # On hpux, endpwent() can crash perl!
1220 else { return endpwent(); }
1223 =head2 my_getpwnam(username)
1225 Looks up a user by name, like the getpwnam Perl function, but may read
1226 /etc/passwd directly.
1231 if ($config{'from_files'}) {
1233 foreach $u (&list_users()) {
1234 return &pw_user_rv($u, wantarray, 'uid')
1235 if ($u->{'user'} eq $_[0]);
1237 return wantarray ? () : undef;
1239 else { return getpwnam($_[0]); }
1242 =head2 my_getpwuid(uid)
1244 Looks up a user by ID, like the getpwnam Perl function, but may read
1245 /etc/passwd directly.
1250 if ($config{'from_files'}) {
1251 foreach $u (&list_users()) {
1252 return &pw_user_rv($u, wantarray, 'user')
1253 if ($u->{'uid'} eq $_[0]);
1255 return wantarray ? () : undef;
1257 else { return getpwuid($_[0]); }
1260 =head2 pw_user_rv(&user, want-array, username-field)
1262 Internal function to convert a user hash reference into a list in the format
1263 return by the getpw* family of functions.
1268 return $_[1] ? ( $_[0]->{'user'}, $_[0]->{'pass'}, $_[0]->{'uid'},
1269 $_[0]->{'gid'}, undef, undef, $_[0]->{'real'},
1270 $_[0]->{'home'}, $_[0]->{'shell'}, undef ) : $_[0]->{$_[2]};
1275 The same as Perl's setgrent function, but may read from /etc/group directly.
1280 if ($config{'from_files'}) {
1281 @setgrent_cache = &list_groups();
1284 else { return setgrent(); }
1289 The same as Perl's getgrent function, but may read from /etc/group directly.
1294 if ($config{'from_files'}) {
1295 my_setgrent() if (!@setgrent_cache);
1296 if ($setgrent_pos >= @setgrent_cache) {
1300 return &gr_group_rv($setgrent_cache[$setgrent_pos++],
1301 wantarray, 'group');
1304 else { return getgrent(); }
1309 Should be called when you are done with my_setgrent and my_getgrent.
1314 if ($config{'from_files'}) {
1315 undef(@setgrent_cache);
1317 elsif ($gconfig{'os_type'} eq 'hpux') {
1318 # On hpux, endpwent() can crash perl!
1321 else { return endgrent(); }
1324 =head2 my_getgrnam(group)
1326 Looks up a group by name, like the Perl getgrnam function.
1331 if ($config{'from_files'}) {
1333 foreach $g (&list_groups()) {
1334 return &gr_group_rv($g, wantarray, 'gid')
1335 if ($g->{'group'} eq $_[0]);
1337 return wantarray ? () : undef;
1339 else { return getgrnam($_[0]); }
1342 =head2 my_getgrgid(gid)
1344 Looks up a group by GID, like the Perl getgrgid function.
1349 if ($config{'from_files'}) {
1350 foreach $g (&list_groups()) {
1351 return &gr_group_rv($g, wantarray, 'group')
1352 if ($g->{'gid'} eq $_[0]);
1354 return wantarray ? () : undef;
1356 else { return getgrgid($_[0]); }
1361 return $_[1] ? ( $_[0]->{'group'}, $_[0]->{'pass'}, $_[0]->{'gid'},
1362 $_[0]->{'members'} ) : $_[0]->{$_[2]};
1365 =head2 auto_home_dir(base, username, groupname)
1367 Returns an automatically generated home directory, and creates needed
1368 parent dirs. The parameters are :
1370 =item base - Base directory, like /home.
1372 =item username - The user's login name.
1374 =item groupname - The user's primary group name.
1379 local $pfx = $_[0] eq "/" ? "/" : $_[0]."/";
1380 if ($config{'home_style'} == 0) {
1383 elsif ($config{'home_style'} == 1) {
1384 &mkdir_if_needed($pfx.substr($_[1], 0, 1));
1385 return $pfx.substr($_[1], 0, 1)."/".$_[1];
1387 elsif ($config{'home_style'} == 2) {
1388 &mkdir_if_needed($pfx.substr($_[1], 0, 1));
1389 &mkdir_if_needed($pfx.substr($_[1], 0, 1)."/".
1390 substr($_[1], 0, 2));
1391 return $pfx.substr($_[1], 0, 1)."/".
1392 substr($_[1], 0, 2)."/".$_[1];
1394 elsif ($config{'home_style'} == 3) {
1395 &mkdir_if_needed($pfx.substr($_[1], 0, 1));
1396 &mkdir_if_needed($pfx.substr($_[1], 0, 1)."/".
1397 substr($_[1], 1, 1));
1398 return $pfx.substr($_[1], 0, 1)."/".
1399 substr($_[1], 1, 1)."/".$_[1];
1401 elsif ($config{'home_style'} == 4) {
1404 elsif ($config{'home_style'} == 5) {
1405 return $pfx.$_[2]."/".$_[1];
1411 -d $_[0] || &make_dir($_[0], 0755);
1414 =head2 set_netinfo(&user)
1416 Update a NetInfo user based on a Webmin user hash. Mainly for internal use.
1421 local %u = %{$_[0]};
1422 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' passwd '$u{'pass'}'");
1423 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' uid '$u{'uid'}'");
1424 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' gid '$u{'gid'}'");
1425 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' class '$u{'class'}'");
1426 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' change '$u{'change'}'");
1427 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' expire '$u{'expire'}'");
1428 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' realname '$u{'real'}'");
1429 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' home '$u{'home'}'");
1430 &system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' shell '$u{'shell'}'");
1433 =head2 set_group_netinfo(&group)
1435 Update a NetInfo group based on a Webmin group hash. Mainly for internal use.
1438 sub set_group_netinfo
1440 local %g = %{$_[0]};
1441 local $mems = join(" ", map { "'$_'" } split(/,/, $g{'members'}));
1442 &system_logged("niutil -createprop '$netinfo_domain' '/groups/$g{'group'}' gid '$g{'gid'}'");
1443 &system_logged("niutil -createprop '$netinfo_domain' '/groups/$g{'group'}' passwd '$g{'pass'}'");
1444 &system_logged("niutil -createprop '$netinfo_domain' '/groups/$g{'group'}' users $mems");
1447 =head2 set_user_dirinfo(&user)
1449 Update a user in OSX directive services based on a Webmin user hash.
1450 Mainly for internal use.
1453 sub set_user_dirinfo
1455 local %u = %{$_[0]};
1456 foreach my $k (keys %user_properties_map) {
1457 local $v = $u{$user_properties_map{$k}};
1459 &execute_dscl_command("create", "/Users/$u{'user'}", $k, $v);
1462 if ($u{'passmode'} == 3 && defined($u{'plainpass'}) ||
1463 $u{'passmode'} == 0) {
1464 # A new plain password was given - use it
1465 &execute_dscl_command("passwd", "/Users/$u{'user'}", $u{'plainpass'});
1466 if ($user->{'uuid'}) {
1467 $user->{'pass'} = &get_macos_password_hash($user->{'uuid'});
1470 elsif ($u{'passmode'} == 4) {
1471 # Explicitly not changed, so do nothing
1473 elsif ($u{'passmode'} == 1 || $u{'pass'} eq $config{'lock_string'}) {
1474 # Account locked - set hash to match
1475 &set_macos_password_hash($u{'uuid'}, $u{'pass'});
1478 # Has the hash changed?
1479 local $oldpass = &get_macos_password_hash($u{'uuid'});
1480 if (defined($oldpass) && $u{'pass'} ne $oldpass) {
1482 &set_macos_password_hash($u{'uuid'}, $u{'pass'});
1487 =head2 set_group_dirinfo(&group)
1489 Update a group in OSX directive services based on a Webmin group hash.
1490 Mainly for internal use.
1493 sub set_group_dirinfo
1495 local %g = %{$_[0]};
1496 $g{'members'} =~ s/,/ /g;
1497 foreach my $k (keys %group_properties_map) {
1498 local $v = $g{$group_properties_map{$k}};
1500 &execute_dscl_command("create", "/Groups/$g{'group'}", $k, $v);
1505 =head2 check_password_restrictions(pass, username)
1507 Returns an error message if the given password fails length and other
1508 checks, or undef if it is OK.
1511 sub check_password_restrictions
1513 return &text('usave_epasswd_min', $config{'passwd_min'})
1514 if (length($_[0]) < $config{'passwd_min'});
1515 local $re = $config{'passwd_re'};
1516 return &text('usave_epasswd_re', $re)
1517 if ($re && !eval { $_[0] =~ /^$re$/ });
1518 if ($config{'passwd_same'}) {
1519 return &text('usave_epasswd_same') if ($_[0] =~ /\Q$_[1]\E/i);
1521 if ($config{'passwd_dict'} && $_[0] =~ /^[A-Za-z\'\-]+$/ &&
1522 (&has_command("ispell") || &has_command("spell"))) {
1523 # Call spell or ispell to check for dictionary words
1524 local $temp = &transname();
1525 open(TEMP, ">$temp");
1526 print TEMP $_[0],"\n";
1528 if (&has_command("ispell")) {
1529 open(SPELL, "ispell -a <$temp |");
1538 open(SPELL, "spell <$temp |");
1539 local $line = <SPELL>;
1540 $unknown++ if ($line);
1544 return &text('usave_epasswd_dict') if (!$unknown);
1546 if ($config{'passwd_prog'}) {
1548 if ($config{'passwd_progmode'} == 0) {
1549 # Run external validation program with user and password as args
1550 local $qu = quotemeta($_[1]);
1551 local $qp = quotemeta($_[0]);
1552 $out = &backquote_command(
1553 "$config{'passwd_prog'} $qu $qp 2>&1 </dev/null");
1556 # Run program with password as input on stdin
1557 local $temp = &transname();
1558 &open_tempfile(TEMP, ">$temp", 0, 1);
1559 &print_tempfile(TEMP, $_[1],"\n");
1560 &print_tempfile(TEMP, $_[0],"\n");
1561 &close_tempfile(TEMP);
1562 $out = &backquote_command("$config{'passwd_prog'} <$temp 2>&1");
1571 =head2 check_username_restrictions(username)
1573 Returns an error message if a username fails some restriction, or undef if
1577 sub check_username_restrictions
1579 if ($config{'max_length'} && length($_[0]) > $config{'max_length'}) {
1580 return &text('usave_elength', $config{'max_length'});
1582 local $re = $config{'username_re'};
1583 return &text('usave_ere', $re)
1584 if ($re && !eval { $_[0] =~ /^$re$/ });
1588 =head2 can_use_group(&acl, group)
1590 Returns 1 if some group can be used as a primary or secondary, 0 if not.
1595 return 1 if ($_[0]->{'ugroups'} eq '*');
1596 local @sp = &split_quoted_string($_[0]->{'ugroups'});
1597 if ($_[0]->{'uedit_gmode'} == 3) {
1598 return &indexof($_[1], @sp) < 0;
1600 elsif ($_[0]->{'uedit_gmode'} == 4) {
1601 local @ginfo = &my_getgrnam($_[1]);
1602 return (!$_[0]->{'ugroups'} || $ginfo[2] >= $_[0]->{'ugroups'}) &&
1603 (!$_[0]->{'ugroups2'} || $ginfo[2] <= $_[0]->{'ugroups2'});
1606 return &indexof($_[1], @sp) >= 0;
1612 Sends a HUP signal to the nscd process, so that any caches are reloaded.
1617 return if ($nscd_not_running);
1618 if (!&find_byname("nscd")) {
1619 $nscd_not_running++;
1621 elsif ($config{'nscd_restart'}) {
1622 # Run the specified command
1623 &system_logged("$config{'nscd_restart'} >/dev/null 2>&1 </dev/null");
1625 elsif (&has_command("nscd")) {
1626 # Use nscd -i to reload
1627 &system_logged("nscd -i group >/dev/null 2>&1 </dev/null");
1628 &system_logged("nscd -i passwd >/dev/null 2>&1 </dev/null");
1632 local $rv = &kill_byname_logged("nscd", "HUP");
1634 $nscd_not_running++;
1637 sleep(1); # Give ncsd time to react
1640 =head2 set_user_envs(&user, action, [plainpass], [secondaries], [&olduser], [oldplainpass])
1642 Sets up the USERADMIN_ environment variables for a user update of some kind,
1643 prior to calling making_changes or made_changes. The parameters are :
1645 =item user - User details hash reference, in the same format as returned by list_users.
1647 =item action - Must be one of CREATE_USER, MODIFY_USER or DELETE_USER.
1649 =item plainpass - The user's un-encrypted password, if available.
1651 =item secondaries - An array reference of secondary group names the user is a member of.
1653 =item olduser - When modifying a user, the hash reference of it's old details.
1655 =item oldplainpass - When modifying a user, it's old un-encrypted password, if available.
1660 local ($user, $action, $plainpass, $secs, $olduser, $oldpass) = @_;
1662 $ENV{'USERADMIN_USER'} = $user->{'user'};
1663 $ENV{'USERADMIN_UID'} = $user->{'uid'};
1664 $ENV{'USERADMIN_REAL'} = $user->{'real'};
1665 $ENV{'USERADMIN_SHELL'} = $user->{'shell'};
1666 $ENV{'USERADMIN_HOME'} = $user->{'home'};
1667 $ENV{'USERADMIN_GID'} = $user->{'gid'};
1668 local $group = &my_getgrgid($user->{'gid'});
1670 $ENV{'USERADMIN_GROUP'} = $group;
1672 $ENV{'USERADMIN_PASS'} = $plainpass if (defined($plainpass));
1673 $ENV{'USERADMIN_SECONDARY'} = join(",", @{$secs}) if (defined($secs));
1674 $ENV{'USERADMIN_ACTION'} = $action;
1675 $ENV{'USERADMIN_SOURCE'} = $main::module_name;
1677 $ENV{'USERADMIN_OLD_USER'} = $olduser->{'user'};
1678 $ENV{'USERADMIN_OLD_UID'} = $olduser->{'uid'};
1679 $ENV{'USERADMIN_OLD_REAL'} = $olduser->{'real'};
1680 $ENV{'USERADMIN_OLD_SHELL'} = $olduser->{'shell'};
1681 $ENV{'USERADMIN_OLD_HOME'} = $olduser->{'home'};
1682 $ENV{'USERADMIN_OLD_GID'} = $olduser->{'gid'};
1683 $ENV{'USERADMIN_OLD_PASS'} = $oldpass if (defined($oldpass));
1687 =head2 set_group_envs(&group, action, [&oldgroup])
1689 Sets up the USERADMIN_ environment variables for a group update of some kind,
1690 prior to calling making_changes or made_changes. The parameters are :
1692 =item group - Group details hash reference, in the same format as returned by list_groups.
1694 =item action - Must be one of CREATE_GROUP, MODIFY_GROUP or DELETE_GROUP.
1696 =item oldgroup - When modifying a group, the hash reference of it's old details.
1701 local ($group, $action, $oldgroup) = @_;
1703 $ENV{'USERADMIN_GROUP'} = $group->{'group'};
1704 $ENV{'USERADMIN_GID'} = $group->{'gid'};
1705 $ENV{'USERADMIN_MEMBERS'} = $group->{'members'};
1706 $ENV{'USERADMIN_ACTION'} = $action;
1707 $ENV{'USERADMIN_SOURCE'} = $main::module_name;
1709 $ENV{'USERADMIN_OLD_GROUP'} = $oldgroup->{'group'};
1710 $ENV{'USERADMIN_OLD_GID'} = $oldgroup->{'gid'};
1711 $ENV{'USERADMIN_OLD_MEMBERS'} = $oldgroup->{'members'};
1717 Removes all variables set by set_user_envs and set_group_envs.
1723 foreach $e (keys %ENV) {
1724 delete($ENV{$e}) if ($e =~ /^USERADMIN_/);
1728 =head2 encrypt_password(password, [salt])
1730 Encrypts a password using the encryption format configured for this system.
1731 If the salt parameter is given, it will be used for hashing the password -
1732 this is typically an already encrypted password, that you want to compare with
1733 the result of this function to check that passwords match. If missing, a salt
1734 will be randomly generated.
1737 sub encrypt_password
1739 local ($pass, $salt) = @_;
1741 if ($gconfig{'os_type'} eq 'macos' && &passfiles_type() == 7) {
1742 # New OSX directory service uses SHA1 for passwords!
1743 $salt ||= chr(int(rand(26))+65).chr(int(rand(26))+65).
1744 chr(int(rand(26))+65).chr(int(rand(26))+65);
1745 if (&check_sha1()) {
1746 # Use Digest::SHA1 perl module
1747 return &encrypt_sha1_hash($pass, $salt);
1749 elsif (&has_command("openssl")) {
1750 # Use openssl command
1751 local $temp = &transname();
1752 &open_execute_command(OPENSSL, "openssl dgst -sha1 >$temp", 0);
1753 print OPENSSL $salt,$pass;
1755 local $rv = &read_file_contents($temp);
1756 &unlink_file($temp);
1761 &error("Either the Digest::SHA1 Perl module or openssl command is needed to hash passwords");
1764 elsif ($config{'md5'} == 2) {
1768 elsif ($config{'md5'} == 3) {
1769 # Always use blowfish
1772 elsif ($config{'md5'} == 4) {
1776 elsif ($config{'md5'} == 1 && !$config{'skip_md5'}) {
1778 $format = &use_md5() if (defined(&use_md5));
1781 if ($no_encrypt_password) {
1782 # Some operating systems don't do any encryption!
1785 elsif ($format == 1) {
1786 # MD5 encryption is selected .. use it if possible
1787 local $err = &check_md5();
1789 &error(&text('usave_edigestmd5',
1790 "/config.cgi?$module_name",
1791 "/cpan/download.cgi?source=3&cpan=$err", $err));
1793 return &encrypt_md5($pass, $salt);
1795 elsif ($format == 2) {
1796 # Blowfish is selected .. use it if possible
1797 local $err = &check_blowfish();
1799 &error(&text('usave_edigestblowfish',
1800 "/config.cgi?$module_name",
1801 "/cpan/download.cgi?source=3&cpan=$err", $err));
1803 return &encrypt_blowfish($pass, $salt);
1805 elsif ($format == 3) {
1806 # SHA512 is selected .. use it
1807 local $err = &check_sha512();
1809 &error($text{'usave_edigestsha512'});
1811 return &encrypt_sha512($pass, $salt);
1814 # Just do old-style crypt() DES encryption
1815 if ($salt !~ /^[a-z0-9]{2}/i) {
1816 # Un-usable non-DES salt
1819 $salt ||= chr(int(rand(26))+65) . chr(int(rand(26))+65);
1820 return &unix_crypt($pass, $salt);
1824 =head2 build_user_used([&uid-hash], [&shell-list], [&username-hash])
1826 Fills in hashes with used UIDs, shells and usernames, based on existing users.
1827 Useful for allocating a new UID, with code like :
1830 useradmin::build_user_used(\%used);
1831 $newuid = useradmin::allocate_uid(\%used);
1838 while(@u = &my_getpwent()) {
1839 $_[0]->{$u[2]}++ if ($_[0]);
1840 push(@{$_[1]}, $u[8]) if ($_[1] && $u[8]);
1841 $_[2]->{$u[0]}++ if ($_[2]);
1845 foreach $u (&list_users()) {
1846 $_[0]->{$u->{'uid'}}++ if ($_[0]);
1847 push(@{$_[1]}, $u->{'shell'}) if ($_[1] && $u->{'shell'});
1848 $_[2]->{$u->{'user'}}++ if ($_[2]);
1852 =head2 build_group_used([&gid-hash], [&groupname-hash])
1854 Fills in hashes with used GIDs and group names, based on existing groups.
1855 Useful for allocating a new GID, with code like :
1858 useradmin::build_group_used(\%used);
1859 $newgid = useradmin::allocate_gid(\%used);
1862 sub build_group_used
1866 while(@g = &my_getgrent()) {
1867 $_[0]->{$g[2]}++ if ($_[0]);
1868 $_[1]->{$g[0]}++ if ($_[1]);
1872 foreach $g (&list_groups()) {
1873 $_[0]->{$g->{'gid'}}++ if ($_[0]);
1874 $_[1]->{$g->{'group'}}++ if ($_[1]);
1878 =head2 allocate_uid(&uids-used)
1880 Given a hash reference whose keys are UIDs already in use, returns a free UID
1881 suitable for a new user.
1886 local $rv = int($config{'base_uid'} > $access{'lowuid'} ?
1887 $config{'base_uid'} : $access{'lowuid'});
1888 while($_[0]->{$rv}) {
1894 =head2 allocate_gid(&gids-used)
1896 Given a hash reference whose keys are GIDs already in use, returns a free GID
1897 suitable for a new group.
1902 local $rv = int($config{'base_gid'} > $access{'lowgid'} ?
1903 $config{'base_gid'} : $access{'lowgid'});
1904 while($_[0]->{$rv}) {
1910 =head2 list_allowed_users(&access, &allusers)
1912 Returns a list of users to whom access is allowed. The parameters are :
1914 =item access - A hash reference of Webmin user permissions, such as returned by get_module_acl.
1916 =item allusers - List of all users to filter down.
1919 sub list_allowed_users
1921 local %access = %{$_[0]};
1922 local @ulist = @{$_[1]};
1923 if ($access{'uedit_mode'} == 1) {
1926 elsif ($access{'uedit_mode'} == 2) {
1928 map { $canu{$_}++ } &split_quoted_string($access{'uedit'});
1929 @ulist = grep { $canu{$_->{'user'}} } @ulist;
1931 elsif ($access{'uedit_mode'} == 3) {
1933 map { $cannotu{$_}++ } &split_quoted_string($access{'uedit'});
1934 @ulist = grep { !$cannotu{$_->{'user'}} } @ulist;
1936 elsif ($access{'uedit_mode'} == 4) {
1938 (!$access{'uedit'} || $_->{'uid'} >= $access{'uedit'}) &&
1939 (!$access{'uedit2'} || $_->{'uid'} <= $access{'uedit2'})
1942 elsif ($access{'uedit_mode'} == 5) {
1944 map { $cangid{$_}++ } &split_quoted_string($access{'uedit'});
1945 if ($access{'uedit_sec'}) {
1946 # Match secondary groups too
1947 local @glist = &list_groups();
1949 foreach $g (@glist) {
1950 push(@ucan, split(/,/, $g->{'members'}))
1951 if ($cangid{$g->{'gid'}});
1953 @ulist = grep { $cangid{$_->{'gid'}} ||
1954 &indexof($_->{'user'}, @ucan) >= 0 } @ulist;
1957 @ulist = grep { $cangid{$_->{'gid'}} } @ulist;
1960 elsif ($access{'uedit_mode'} == 6) {
1961 @ulist = grep { $_->{'user'} eq $remote_user } @ulist;
1963 elsif ($access{'uedit_mode'} == 7) {
1964 @ulist = grep { $_->{'user'} =~ /$access{'uedit_re'}/ } @ulist;
1966 elsif ($access{'uedit_mode'} == 8) {
1968 (!$access{'uedit'} || $_->{'gid'} >= $access{'uedit'}) &&
1969 (!$access{'uedit2'} || $_->{'gid'} <= $access{'uedit2'})
1972 if ($access{'view'}) {
1973 # Include non-editable users in results
1974 local @rv = @{$_[1]};
1977 if (&indexof($u, @ulist) < 0) {
1988 =head2 list_allowed_groups(&access, &allgroups)
1990 Returns a list of groups to whom access is allowed. The parameters are :
1992 =item access - A hash reference of Webmin user permissions, such as returned by get_module_acl.
1994 =item allgroups - List of all Unix groups to filter down.
1997 sub list_allowed_groups
1999 local %access = %{$_[0]};
2000 local @glist = @{$_[1]};
2001 if ($access{'gedit_mode'} == 1) {
2004 elsif ($access{'gedit_mode'} == 2) {
2006 map { $cang{$_}++ } &split_quoted_string($access{'gedit'});
2007 @glist = grep { $cang{$_->{'group'}} } @glist;
2009 elsif ($access{'gedit_mode'} == 3) {
2011 map { $cannotg{$_}++ } &split_quoted_string($access{'gedit'});
2012 @glist = grep { !$cannotg{$_->{'group'}} } @glist;
2014 elsif ($access{'gedit_mode'} == 4) {
2016 (!$access{'gedit'} || $_->{'gid'} >= $access{'gedit'}) &&
2017 (!$access{'gedit2'} || $_->{'gid'} <= $access{'gedit2'})
2020 if ($access{'view'}) {
2021 # Include non-editable groups in results
2022 local @rv = @{$_[1]};
2025 if (&indexof($g, @glist) < 0) {
2038 Tells the create/modify/delete functions to only update files in memory,
2049 Flushes any user file changes
2055 &flush_file_lines();
2059 #################################################################
2063 #################################################################
2067 #### This subroutine assumes the usernames are standardized
2068 #### using the format of 7 characters with 3 letters followed
2069 #### by 4 digits, or 4 letters followed by 3 digits. If
2070 #### uppercase letters are used in the username, they will be
2071 #### converted to lowercase and this subroutine will generate
2072 #### a UID number identical to the usernames lowercase
2075 #### 3 letters, 4 digits Lowest possible UID (aaa0000) = 1,000,000
2076 #### 3 letters, 4 digits Hightest possible UID (zzz9999) = 176,759,999
2078 #### 4 letters, 3 digits Lowest possible UID (aaaa000) = 176,760,000
2079 #### 4 letters, 3 digits Hightest possible UID (zzzz999) = 633,735,999
2081 #################################################################
2083 foreach (split(//,$_[0])) {
2084 ++${num_let} if ( m/[a-z]/i );
2086 if ( length($_[0]) ne 7 ) {
2087 print "ERROR: Number of characters in username $_[0] is not equal to 7\n";
2090 if ( ${num_let} ne 3 && ${num_let} ne 4 ) {
2091 print "ERROR: Number of letters in username $_[0] is not equal to 3 or 4\n";
2094 my ${mkuid_type} = 10 ** ( 7 - ${num_let} );
2095 my ${lowlimit} = 1000000;
2099 ${lowuid} = ( 26 ** ( ${num_let} - 1 ) * ${lowlimit}/100 ) + ${lowlimit};
2100 ${lowuid} = ${lowlimit} if ( ${num_let} eq 3 );
2103 #################################################################
2105 #### Establish an associative array containing all the
2106 #### letters of the alphabet and assign a numeric value
2107 #### to each letter from 1 - 26.
2109 #################################################################
2110 $letters{'a'} = ++${icnt};
2111 $letters{'b'} = ++${icnt};
2112 $letters{'c'} = ++${icnt};
2113 $letters{'d'} = ++${icnt};
2114 $letters{'e'} = ++${icnt};
2115 $letters{'f'} = ++${icnt};
2116 $letters{'g'} = ++${icnt};
2117 $letters{'h'} = ++${icnt};
2118 $letters{'i'} = ++${icnt};
2119 $letters{'j'} = ++${icnt};
2120 $letters{'k'} = ++${icnt};
2121 $letters{'l'} = ++${icnt};
2122 $letters{'m'} = ++${icnt};
2123 $letters{'n'} = ++${icnt};
2124 $letters{'o'} = ++${icnt};
2125 $letters{'p'} = ++${icnt};
2126 $letters{'q'} = ++${icnt};
2127 $letters{'r'} = ++${icnt};
2128 $letters{'s'} = ++${icnt};
2129 $letters{'t'} = ++${icnt};
2130 $letters{'u'} = ++${icnt};
2131 $letters{'v'} = ++${icnt};
2132 $letters{'w'} = ++${icnt};
2133 $letters{'x'} = ++${icnt};
2134 $letters{'y'} = ++${icnt};
2135 $letters{'z'} = ++${icnt};
2137 #################################################################
2139 #### Initialize variables to be use while calculating the UID
2140 #### number associated with the login name.
2142 #### nvalue is used to store numeric characters that occurs
2143 #### in the login name
2144 #### ecnt is used to keep track of the base 26 exponent for
2145 #### each letter character that occurs in the login name
2146 #### subtot is the sum of the calculated value for each
2147 #### character position in the login name
2148 #### mult is the total of the 26 ** ecnt at each iteration of
2151 #################################################################
2159 #################################################################
2161 #### each character position of the login name is split out
2162 #### and used as an iteration of the foreach loop
2164 #################################################################
2166 foreach (split(//,$_[0])) {
2168 #################################################################
2170 #### If the current character of the login name is a letter,
2171 #### convert it to lower case, and obtain it's numeric value
2172 #### from the associative array of letters, otherwise, if the
2173 #### current character is a number, append the number to the
2174 #### end of a buffer and save it for later processing.
2176 #################################################################
2178 $kstring = "\L${_}";
2179 ${lvalue} = ${letters{${kstring}}};
2182 ${nvalue} = "${nvalue}${_}";
2184 #################################################################
2185 #### Calculate the multiplier for a base 26 calculation using
2186 #### each iteration through the foreach loop as an increment
2187 #### of the exponent. The base 26 exponent starting at 0.
2188 #################################################################
2190 ${mult} = ${base} ** ${ecnt};
2192 #################################################################
2194 #### Multiply the numeric value of the current character by
2195 #### the multiplier and add this result to a running subtotal
2196 #### of all characters of the login name.
2198 #################################################################
2199 ${subtot} = ${subtot} + ( ${lvalue} * ${mult} );
2201 #################################################################
2203 #### Increment the base 26 exponent by one before iterating for
2204 #### the next character of the login name.
2206 #################################################################
2210 #################################################################
2212 #### After all characters of the login name have be processed,
2213 #### multiply the result by 1,000. This is done because the
2214 #### username standard is 3 letters followed by 4 digits. So
2215 #### each 3 letter combination can have 1,000 possible combinations.
2216 #### Then add the numeric values saved and any value to use
2217 #### as the lowest UID number allowed through this calculated method.
2219 #################################################################
2221 ${tot} = ( ${subtot} * ${mkuid_type} ) + int(${nvalue}) + ${lowuid};
2223 #################################################################
2225 #### Return the calculated UID number as the result of this
2228 #################################################################
2232 ################################################################
2233 sub berkeley_cksum {
2234 my($crc) = my($len) = 0;
2236 my($buflen) = 4096; # buffer is "4k", you can up it if you want...
2239 $num = length($buf);
2242 foreach ( unpack("C*", $buf) ) {
2243 $crc |= 0x10000 if ( $crc & 1 ); # get ready for rotating the 1 below
2244 $crc = (($crc>>1)+$_) & 0xffff; # keep to 16-bit
2246 return sprintf("%lu",${crc});;
2249 =head2 users_table(&users, [form], [no-last], [no-boxes], [&otherlinks], [&rightlinks])
2251 Prints a table listing full user details, with checkboxes and buttons to
2252 delete or disable multiple at once.
2257 local ($users, $formno, $nolast, $noboxes, $links, $rightlinks) = @_;
2259 local (@ginfo, %gidgrp);
2261 while(@ginfo = &my_getgrent()) {
2262 $gidgrp{$ginfo[2]} = $ginfo[0];
2266 # Work out if any users can be edited
2268 foreach my $u (@$users) {
2269 if (!$u->{'noedit'}) {
2274 $anyedit = 0 if ($noboxes);
2275 local $lshow = !$nolast && $config{'last_show'};
2278 $buttons .= &ui_submit($text{'index_mass'}, "delete") if ($access{'udelete'});
2279 $buttons .= &ui_submit($text{'index_mass2'}, "disable");
2280 $buttons .= &ui_submit($text{'index_mass3'}, "enable");
2281 $buttons .= "<br>" if ($buttons);
2284 print &ui_form_start("mass_delete_user.cgi", "post");
2285 push(@linksrow, &select_all_link("d", $_[1]),
2286 &select_invert_link("d", $_[1]));
2288 push(@linksrow, @$links);
2289 local @grid = ( &ui_links_row(\@linksrow), &ui_links_row($rightlinks) );
2290 print &ui_grid_table(\@grid, 2, 100, [ "align=left", "align=right" ]);
2292 local @tds = $anyedit ? ( "width=5" ) : ( );
2293 push(@tds, "width=15%", "width=10%");
2294 print &ui_columns_start([
2295 $anyedit ? ( "" ) : ( ),
2302 $lshow ? ( $text{'lastlogin'} ) : ( )
2306 $llogin = &get_recent_logins();
2307 if (&foreign_check("mailboxes")) {
2308 &foreign_require("mailboxes");
2312 foreach $u (@$users) {
2313 $u->{'real'} =~ s/,.*$// if ($config{'extra_real'} ||
2314 $u->{'real'} =~ /,$/);
2316 push(@cols, "") if ($anyedit && $u->{'noedit'});
2317 push(@cols, &user_link($u));
2318 push(@cols, $u->{'uid'});
2319 push(@cols, &html_escape($gidgrp{$u->{'gid'}} || $u->{'gid'}));
2320 push(@cols, &html_escape($u->{'real'}));
2321 push(@cols, &html_escape($u->{'home'}));
2322 push(@cols, &html_escape($u->{'shell'}));
2324 # Show last login, in local format after Unix time conversion
2325 my $ll = $llogin->{$u->{'user'}};
2326 if (defined(&mailboxes::parse_mail_date)) {
2327 my $tm = &mailboxes::parse_mail_date($ll);
2329 $ll = &make_date($tm);
2332 push(@cols, &html_escape($ll));
2334 if ($u->{'noedit'}) {
2335 print &ui_columns_row(\@cols, \@tds);
2338 print &ui_checked_columns_row(\@cols, \@tds, "d", $u->{'user'});
2341 print &ui_columns_end();
2342 print &ui_links_row(\@linksrow);
2345 print &ui_form_end();
2349 =head2 groups_table(&groups, [form], [no-buttons], [&otherlinks], [&rightlinks])
2351 Prints a table of groups, possibly with checkboxes and a delete button
2356 local ($groups, $formno, $noboxes, $links, $rightlinks) = @_;
2358 # Work out if any groups can be edited or have descriptions
2361 foreach my $g (@$groups) {
2362 if (!$g->{'noedit'}) {
2369 $anyedit = 0 if ($noboxes);
2372 if ($anyedit && $access{'gdelete'}) {
2373 print &ui_form_start("mass_delete_group.cgi", "post");
2374 push(@linksrow, &select_all_link("gd", $formno),
2375 &select_invert_link("gd", $formno) );
2377 push(@linksrow, @$links);
2378 local @grid = ( &ui_links_row(\@linksrow), &ui_links_row($rightlinks) );
2379 print &ui_grid_table(\@grid, 2, 100, [ "align=left", "align=right" ]);
2381 local @tds = $anyedit ? ( "width=5" ) : ( );
2382 push(@tds, "width=15%", "width=10%");
2383 print &ui_columns_start([
2384 $anyedit ? ( "" ) : ( ),
2385 $text{'gedit_group'},
2387 $anydesc ? ( $text{'gedit_desc'} ) : ( ),
2388 $text{'gedit_members'} ], 100, 0, \@tds);
2390 foreach $g (@$groups) {
2391 local $members = join(" ", split(/,/, $g->{'members'}));
2393 if ($anyedit && ($g->{'noedit'} || !$access{'gdelete'})) {
2394 # Need an explicity blank first column
2397 push(@cols, &group_link($g));
2398 push(@cols, $g->{'gid'});
2400 push(@cols, &html_escape($g->{'desc'}));
2402 push(@cols, &html_escape($members));
2403 if ($g->{'noedit'} || !$access{'gdelete'}) {
2404 print &ui_columns_row(\@cols, \@tds);
2407 print &ui_checked_columns_row(\@cols, \@tds, "gd",
2411 print &ui_columns_end();
2412 print &ui_links_row(\@linksrow);
2413 if ($anyedit && $access{'gdelete'}) {
2414 print &ui_submit($text{'index_gmass'}, "delete"),"<br>\n";
2415 print &ui_form_end();
2419 =head2 date_input(day, month, year, prefix)
2421 Returns HTML for selecting a date
2426 local ($d, $m, $y, $prefix) = @_;
2428 $rv .= &ui_textbox($prefix."d", $d, 3)."/";
2429 $rv .= &ui_select($prefix."m", $m,
2430 [ map { [ $_, $text{"smonth_".$_} ] } (1..12) ])."/";
2431 $rv .= &ui_textbox($prefix."y", $y, 5);
2432 $rv .= &date_chooser_button($prefix."d", $prefix."m", $prefix."y");
2436 =head2 list_last_logins([user], [max])
2438 Returns a list of array references, each containing the details of a login.
2441 sub list_last_logins
2444 &open_last_command(LAST, $_[0]);
2445 while(@last = &read_last_line(LAST)) {
2446 push(@rv, [ @last ]);
2447 if ($_[1] && scalar(@rv) >= $_[1]) {
2455 =head2 get_recent_logins()
2457 Returns a hash ref from username to most recent login time/date
2460 sub get_recent_logins
2462 if (defined(&os_most_recent_logins)) {
2463 return &os_most_recent_logins();
2467 foreach my $l (&list_last_logins()) {
2468 $rv{$l->[0]} ||= $l->[3];
2474 =head2 user_link(&user)
2476 Returns a link to a user editing form. Mainly for internal use.
2481 if ($_[0]->{'pass'} =~ /^\Q$disable_string\E/) {
2482 $dis = "<i>".&html_escape($_[0]->{'user'})."</i>";
2485 $dis = &html_escape($_[0]->{'user'});
2487 if ($_[0]->{'noedit'}) {
2490 elsif ($_[0]->{'dn'}) {
2491 return "<a href='edit_user.cgi?dn=".&urlize($_[0]->{'dn'})."'>".
2495 return "<a href='edit_user.cgi?user=".&urlize($_[0]->{'user'})."'>".
2500 =head2 group_link(&group)
2502 Returns a link to a group editing form. Mainly for internal use.
2507 if ($_[0]->{'noedit'}) {
2508 return &html_escape($_[0]->{'group'});
2510 elsif ($_[0]->{'dn'}) {
2511 return "<a href='edit_group.cgi?dn=".&urlize($_[0]->{'dn'})."'>".
2512 &html_escape($_[0]->{'group'})."</a>";
2515 return "<a href='edit_group.cgi?group=".&urlize($_[0]->{'group'})."'>".
2516 &html_escape($_[0]->{'group'})."</a>";
2520 =head2 sort_users(&users, mode)
2522 Sorts a list of users according to the user's preference for this module,
2523 and returns the results.
2528 local ($users, $mode) = @_;
2529 local @ulist = @$users;
2531 @ulist = sort { $a->{'user'} cmp $b->{'user'} } @ulist;
2533 elsif ($mode == 2) {
2534 @ulist = sort { lc($a->{'real'}) cmp lc($b->{'real'}) } @ulist;
2536 elsif ($mode == 3) {
2537 @ulist = sort { @wa = split(/\s+/, $a->{'real'});
2538 @wb = split(/\s+/, $b->{'real'});
2539 lc($wa[@wa-1]) cmp lc($wb[@wb-1]) } @ulist;
2541 elsif ($mode == 4) {
2542 @ulist = sort { $a->{'shell'} cmp $b->{'shell'} } @ulist;
2544 elsif ($mode == 5) {
2545 @ulist = sort { $a->{'uid'} <=> $b->{'uid'} } @ulist;
2547 elsif ($mode == 6) {
2548 @ulist = sort { $a->{'home'} cmp $b->{'home'} } @ulist;
2553 =head2 sort_groups(&groups, mode)
2555 Sorts a list of groups according to the user's preference for this module,
2556 and returns the results.
2561 local ($groups, $mode) = @_;
2562 local @glist = @$groups;
2564 @glist = sort { $a->{'gid'} <=> $b->{'gid'} } @glist;
2566 elsif ($mode == 1) {
2567 @glist = sort { $a->{'group'} cmp $b->{'group'} } @glist;
2572 =head2 create_home_directory(&user, [real-dir])
2574 Creates and chmod's the home directory for a user, or calls error on failure.
2577 sub create_home_directory
2579 local ($user, $home) = @_;
2580 $home ||= $user->{'home'};
2582 &make_dir($home, oct($config{'homedir_perms'}), 1) ||
2583 &error(&text('usave_emkdir', $!));
2584 &set_ownership_permissions($user->{'uid'}, $user->{'gid'},
2585 oct($config{'homedir_perms'}), $home) ||
2586 &error(&text('usave_echmod', $!));
2587 if ($config{'selinux_con'} && &is_selinux_enabled() && &has_command("chcon")) {
2588 &system_logged("chcon ".quotemeta($config{'selinux_con'}).
2589 " ".quotemeta($home)." >/dev/null 2>&1");
2591 &unlock_file($home);
2594 =head2 delete_home_directory(&user)
2596 Deletes some users home directory.
2599 sub delete_home_directory
2602 if ($user->{'home'} && -d $user->{'home'}) {
2603 local $realhome = &resolve_links($user->{'home'});
2604 local $qhome = quotemeta($realhome);
2605 if ($config{'delete_only'}) {
2606 &system_logged("find $qhome ! -type d -user $user->{'uid'} | xargs rm -f >/dev/null 2>&1");
2607 &system_logged("find $qhome -type d -user $user->{'uid'} | xargs rmdir >/dev/null 2>&1");
2608 &unlink_file($realhome);
2611 &system_logged("rm -rf $qhome >/dev/null 2>&1");
2613 unlink($user->{'home'}); # in case of links
2617 =head2 supports_temporary_disable
2619 Returns 1 if temporary locking of passwords (with an ! at the start of the
2620 hash) is supported on this OS.
2623 sub supports_temporary_disable
2625 return &passfiles_type() != 7; # Not on OSX, which has a fixed-size hash
2628 =head2 change_all_home_groups(old-gid, new-gid, &members)
2630 Change the GID on all files in the home directories of users whose GID is the
2634 sub change_all_home_groups
2636 local ($oldgid, $gid, $mems) = @_;
2638 while(my @uinfo = &my_getpwent()) {
2639 if ($uinfo[3] == $oldgid || &indexof($uinfo[0], @$mems) >= 0) {
2640 &recursive_change($uinfo[7], -1, $oldgid, -1, $gid);
2646 =head2 generate_random_password()
2648 Returns a randomly generated 15 character password
2651 sub generate_random_password
2656 $rv .= $random_password_chars[rand(scalar(@random_password_chars))];