3 Library for editing webmin users, passwords and access rights.
5 foreign_require("acl", "acl-lib.pl");
6 @users = acl::list_users();
7 $newguy = { 'name' => 'newguy',
8 'pass' => acl::encrypt_password('smeg'),
9 'modules' => [ 'useradmin' ] };
10 acl::create_user($newguy);
14 BEGIN { push(@INC, ".."); };
18 %access = &get_module_acl();
19 $access{'switch'} = 0 if (&is_readonly_mode());
21 =head2 list_users([&only-users])
23 Returns a list of hashes containing Webmin user details. Useful keys include :
25 =item name - Login name
27 =item pass - Encrypted password
29 =item modules - Array references of modules
31 =item theme - Custom theme, if any
33 If the only-users parameter is given, limit the list to just users with
40 my (%miniserv, @rv, %acl, %logout);
42 &read_acl(undef, \%acl);
43 &get_miniserv_config(\%miniserv);
44 foreach my $a (split(/\s+/, $miniserv{'logouttimes'})) {
45 if ($a =~ /^([^=]+)=(\S+)$/) {
49 open(PWFILE, $miniserv{'userfile'});
52 local @user = split(/:/, $_);
55 next if ($only && &indexof($user[0], @$only) < 0);
56 $user{'name'} = $user[0];
57 $user{'pass'} = $user[1];
58 $user{'sync'} = $user[2];
59 $user{'cert'} = $user[3];
60 if ($user[4] =~ /^(allow|deny)\s+(.*)/) {
63 if ($user[5] =~ /days\s+(\S+)/) {
66 if ($user[5] =~ /hours\s+(\d+\.\d+)-(\d+\.\d+)/) {
67 $user{'hoursfrom'} = $1;
68 $user{'hoursto'} = $2;
70 $user{'lastchange'} = $user[6];
71 $user{'olds'} = [ split(/\s+/, $user[7]) ];
72 $user{'minsize'} = $user[8];
73 $user{'nochange'} = int($user[9]);
74 $user{'temppass'} = int($user[10]);
75 $user{'modules'} = $acl{$user[0]};
76 $user{'lang'} = $gconfig{"lang_$user[0]"};
77 $user{'notabs'} = $gconfig{"notabs_$user[0]"};
78 $user{'skill'} = $gconfig{"skill_$user[0]"};
79 $user{'risk'} = $gconfig{"risk_$user[0]"};
80 $user{'rbacdeny'} = $gconfig{"rbacdeny_$user[0]"};
81 if ($gconfig{"theme_$user[0]"}) {
82 ($user{'theme'}, $user{'overlay'}) =
83 split(/\s+/, $gconfig{"theme_$user[0]"});
85 elsif (defined($gconfig{"theme_$user[0]"})) {
88 $user{'readonly'} = $gconfig{"readonly_$user[0]"};
89 $user{'ownmods'} = [ split(/\s+/,
90 $gconfig{"ownmods_$user[0]"}) ];
91 $user{'logouttime'} = $logout{$user[0]};
92 $user{'real'} = $gconfig{"realname_$user[0]"};
98 # If a user DB is enabled, get users from it too
99 if ($miniserv{'userdb'}) {
100 my ($dbh, $proto, $prefix, $args) =&connect_userdb($miniserv{'userdb'});
101 &error("Failed to connect to user database : $dbh") if (!ref($dbh));
102 if ($proto eq "mysql" || $proto eq "postgresql") {
103 # Fetch users with SQL
105 my $cmd = $dbh->prepare(
106 "select id,name,pass from webmin_user".
107 ($only ? " where name in (".
108 join(",", map { "'$_'" } @$only).")" : ""));
109 $cmd && $cmd->execute() ||
110 &error("Failed to query users : ".$dbh->errstr);
111 while(my ($id, $name, $pass) = $cmd->fetchrow()) {
112 my $u = { 'name' => $name,
121 # Add user attributes
122 my $cmd = $dbh->prepare(
123 "select id,attr,value from webmin_user_attr ".
125 " where id in (".join(",", keys %userid).")" : ""));
126 $cmd && $cmd->execute() ||
127 &error("Failed to query user attrs : ".$dbh->errstr);
128 while(my ($id, $attr, $value) = $cmd->fetchrow()) {
129 if ($attr eq "olds" || $attr eq "modules" ||
130 $attr eq "ownmods") {
131 $value = [ split(/\s+/, $value) ];
133 $userid{$id}->{$attr} = $value;
137 elsif ($proto eq "ldap") {
138 # Find users with LDAP query
139 my $filter = '(objectClass='.$args->{'userclass'}.')';
142 "(|".join("", map { "(cn=$_)" } @$only).")";
143 $filter = "(&".$filter.$ufilter.")";
145 my $rv = $dbh->search(
149 if (!$rv || $rv->code) {
150 &error("Failed to search users : ".
151 ($rv ? $rv->error : "Unknown error"));
153 foreach my $l ($rv->all_entries) {
154 my $u = { 'name' => $l->get_value('cn'),
155 'pass' => $l->get_value('webminPass'),
158 foreach my $la ($l->get_value('webminAttr')) {
159 my ($attr, $value) = split(/=/, $la, 2);
160 if ($attr eq "olds" || $attr eq "ownmods") {
161 $value = [ split(/\s+/, $value) ];
163 $u->{$attr} = $value;
165 $u->{'modules'} = [ $l->get_value('webminModule') ];
169 &disconnect_userdb($miniserv{'userdb'}, $dbh);
175 =head2 get_user(username)
177 Returns a hash ref of details of the user with some name, in the same format
178 as list_users. Returns undef if not found
184 my @rv = &list_users([ $username ]);
185 my ($user) = grep { $_->{'name'} eq $username } @rv;
189 =head2 list_groups([&only-groups])
191 Returns a list of hashes, one per Webmin group. Group membership is stored in
192 /etc/webmin/webmin.groups, and other attributes in the config file. Useful
195 =item name - Group name
197 =item members - Array reference of member users
199 =item modules - Modules to grant to members
201 If the only-groups parameter is given, limit the list to just groups with
210 &get_miniserv_config(\%miniserv);
212 # Add groups from local files
213 open(GROUPS, "$config_directory/webmin.groups");
216 local @g = split(/:/, $_);
218 next if ($only && &indexof($g[0], @$only) < 0);
219 local $group = { 'name' => $g[0],
220 'members' => [ split(/\s+/, $g[1]) ],
221 'modules' => [ split(/\s+/, $g[2]) ],
223 'ownmods' => [ split(/\s+/, $g[4]) ] };
229 # If a user DB is enabled, get groups from it too
230 if ($miniserv{'userdb'}) {
231 my ($dbh, $proto, $prefix, $args) =&connect_userdb($miniserv{'userdb'});
232 &error("Failed to connect to group database : $dbh") if (!ref($dbh));
233 if ($proto eq "mysql" || $proto eq "postgresql") {
234 # Fetch groups with SQL
236 my $cmd = $dbh->prepare(
237 "select id,name,description from webmin_group ".
238 ($only ? " where name in (".
239 join(",", map { "'$_'" } @$only).")" : ""));
240 $cmd && $cmd->execute() ||
241 &error("Failed to query groups : ".$dbh->errstr);
242 while(my ($id, $name, $desc) = $cmd->fetchrow()) {
243 my $g = { 'name' => $name,
252 # Add group attributes
253 my $cmd = $dbh->prepare(
254 "select id,attr,value from webmin_group_attr ".
256 " where id in (".join(",", keys %userid).")" : ""));
257 $cmd && $cmd->execute() ||
258 &error("Failed to query group attrs : ".$dbh->errstr);
259 while(my ($id, $attr, $value) = $cmd->fetchrow()) {
260 if ($attr eq "members" || $attr eq "modules" ||
261 $attr eq "ownmods") {
262 $value = [ split(/\s+/, $value) ];
264 $groupid{$id}->{$attr} = $value;
268 elsif ($proto eq "ldap") {
269 # Find groups with LDAP query
270 my $filter = '(objectClass='.$args->{'groupclass'}.')';
273 "(|".join("", map { "(cn=$_)" } @$only).")";
274 $filter = "(&".$filter.$gfilter.")";
276 my $rv = $dbh->search(
280 if (!$rv || $rv->code) {
281 &error("Failed to search groups : ".
282 ($rv ? $rv->error : "Unknown error"));
284 foreach my $l ($rv->all_entries) {
285 my $g = { 'name' => $l->get_value('cn'),
286 'desc' => $l->get_value('webminDesc'),
289 foreach my $la ($l->get_value('webminAttr')) {
290 my ($attr, $value) = split(/=/, $la, 2);
291 if ($attr eq "members" || $attr eq "ownmods") {
292 $value = [ split(/\s+/, $value) ];
294 $g->{$attr} = $value;
296 $g->{'modules'} = [ $l->get_value('webminModule') ];
300 &disconnect_userdb($miniserv{'userdb'}, $dbh);
306 =head2 get_group(groupname)
308 Returns a hash ref of details of the group with some name, in the same format
309 as list_groups. Returns undef if not found
314 my ($groupname) = @_;
315 my @rv = &list_groups([ $groupname ]);
316 my ($group) = grep { $_->{'name'} eq $groupname } @rv;
322 Returns a list of the dirs of all modules available on this system.
327 return map { $_->{'dir'} } &list_module_infos();
330 =head2 list_module_infos
332 Returns a list of the details of all modules that can be used on this system,
333 each of which is a hash reference in the same format as their module.info files.
336 sub list_module_infos
338 my @mods = grep { &check_os_support($_) } &get_all_module_infos();
339 return sort { $a->{'desc'} cmp $b->{'desc'} } @mods;
342 =head2 create_user(&details, [clone])
344 Creates a new Webmin user, based on the hash reference in the details parameter.
345 This must be in the same format as those returned by list_users. If the clone
346 parameter is given, it must be a username to copy detailed access control
347 settings from for this new user.
355 my @mods = &list_modules();
357 &get_miniserv_config(\%miniserv);
359 if ($miniserv{'userdb'} && !$miniserv{'userdb_addto'}) {
360 # Adding to user database
361 my ($dbh, $proto, $prefix, $args) =&connect_userdb($miniserv{'userdb'});
362 &error("Failed to connect to user database : $dbh") if (!ref($dbh));
363 if ($proto eq "mysql" || $proto eq "postgresql") {
365 my $cmd = $dbh->prepare("insert into webmin_user (name,pass) values (?, ?)");
366 $cmd && $cmd->execute($user{'name'}, $user{'pass'}) ||
367 &error("Failed to add user : ".$dbh->errstr);
369 my $cmd = $dbh->prepare("select max(id) from webmin_user");
371 my ($id) = $cmd->fetchrow();
374 # Add other attributes
375 my $cmd = $dbh->prepare("insert into webmin_user_attr (id,attr,value) values (?, ?, ?)");
376 foreach my $attr (keys %user) {
377 next if ($attr eq "name" || $attr eq "pass");
378 my $value = $user{$attr};
379 if ($attr eq "olds" || $attr eq "modules" ||
380 $attr eq "ownmods") {
381 $value = join(" ", @$value);
383 $cmd->execute($id, $attr, $value) ||
384 &error("Failed to add user attribute : ".
389 $_[0]->{'proto'} = $proto;
391 elsif ($proto eq "ldap") {
393 my $dn = "cn=".$user{'name'}.",".$prefix;
394 my @attrs = ( "objectClass", $args->{'userclass'},
396 "webminPass", $user{'pass'} );
398 foreach my $attr (keys %user) {
399 next if ($attr eq "name" || $attr eq "pass" ||
401 my $value = $user{$attr};
402 if ($attr eq "olds" || $attr eq "ownmods") {
403 $value = join(" ", @$value);
406 defined($value) ? $attr."=".$value : $attr);
409 push(@attrs, "webminAttr", \@webminattrs);
411 if (@{$user{'modules'}}) {
412 push(@attrs, "webminModule", $user{'modules'});
414 my $rv = $dbh->add($dn, attr => \@attrs);
415 if (!$rv || $rv->code) {
416 &error("Failed to add user to LDAP : ".
417 ($rv ? $rv->error : "Unknown error"));
420 $_[0]->{'proto'} = 'ldap';
422 &disconnect_userdb($miniserv{'userdb'}, $dbh);
423 $user{'proto'} = $proto;
426 # Adding to local files
427 &lock_file($ENV{'MINISERV_CONFIG'});
428 if ($user{'theme'}) {
429 $miniserv{"preroot_".$user{'name'}} =
430 $user{'theme'}.($user{'overlay'} ? " ".$user{'overlay'} : "");
432 elsif (defined($user{'theme'})) {
433 $miniserv{"preroot_".$user{'name'}} = "";
435 if (defined($user{'logouttime'})) {
436 my @logout = split(/\s+/, $miniserv{'logouttimes'});
437 push(@logout, "$user{'name'}=$user{'logouttime'}");
438 $miniserv{'logouttimes'} = join(" ", @logout);
440 &put_miniserv_config(\%miniserv);
441 &unlock_file($ENV{'MINISERV_CONFIG'});
444 push(@times, "days", $user{'days'}) if ($user{'days'} ne '');
445 push(@times, "hours", $user{'hoursfrom'}."-".$user{'hoursto'})
446 if ($user{'hoursfrom'});
447 &lock_file($miniserv{'userfile'});
448 &open_tempfile(PWFILE, ">>$miniserv{'userfile'}");
449 &print_tempfile(PWFILE,
450 "$user{'name'}:$user{'pass'}:$user{'sync'}:$user{'cert'}:",
451 ($user{'allow'} ? "allow $user{'allow'}" :
452 $user{'deny'} ? "deny $user{'deny'}" : ""),":",
453 join(" ", @times),":",
454 $user{'lastchange'},":",
455 join(" ", @{$user{'olds'}}),":",
456 $user{'minsize'},":",
457 $user{'nochange'},":",
460 &close_tempfile(PWFILE);
461 &unlock_file($miniserv{'userfile'});
463 &lock_file(&acl_filename());
464 &open_tempfile(ACL, ">>".&acl_filename());
465 &print_tempfile(ACL, &acl_line(\%user, \@mods));
466 &close_tempfile(ACL);
467 &unlock_file(&acl_filename());
469 delete($gconfig{"lang_".$user{'name'}});
470 $gconfig{"lang_".$user{'name'}} = $user{'lang'} if ($user{'lang'});
471 delete($gconfig{"notabs_".$user{'name'}});
472 $gconfig{"notabs_".$user{'name'}} = $user{'notabs'} if ($user{'notabs'});
473 delete($gconfig{"skill_".$user{'name'}});
474 $gconfig{"skill_".$user{'name'}} = $user{'skill'} if ($user{'skill'});
475 delete($gconfig{"risk_".$user{'name'}});
476 $gconfig{"risk_".$user{'name'}} = $user{'risk'} if ($user{'risk'});
477 delete($gconfig{"rbacdeny_".$user{'name'}});
478 $gconfig{"rbacdeny_".$user{'name'}} = $user{'rbacdeny'} if ($user{'rbacdeny'});
479 delete($gconfig{"ownmods_".$user{'name'}});
480 $gconfig{"ownmods_".$user{'name'}} = join(" ", @{$user{'ownmods'}})
481 if (@{$user{'ownmods'}});
482 delete($gconfig{"theme_".$user{'name'}});
483 if ($user{'theme'}) {
484 $gconfig{"theme_".$user{'name'}} =
485 $user{'theme'}.($user{'overlay'} ? " ".$user{'overlay'} : "");
487 elsif (defined($user{'theme'})) {
488 $gconfig{"theme_".$user{'name'}} = '';
490 $gconfig{"readonly_".$user{'name'}} = $user{'readonly'}
491 if (defined($user{'readonly'}));
492 $gconfig{"realname_".$user{'name'}} = $user{'real'}
493 if (defined($user{'real'}));
494 &write_file("$config_directory/config", \%gconfig);
497 # Copy ACLs from user being cloned
499 ©_acl_files($clone, $user{'name'}, [ "", @mods ]);
503 =head2 modify_user(old-name, &details)
505 Updates an existing Webmin user, identified by the old-name paramter. The
506 details hash must be in the same format as returned by list_users or passed
512 my $username = $_[0];
514 my (%miniserv, @pwfile, @acl, @mods, $m);
516 &get_miniserv_config(\%miniserv);
518 if ($user{'proto'}) {
519 # In users and groups DB
520 my ($dbh, $proto) = &connect_userdb($miniserv{'userdb'});
521 &error("Failed to connect to user database : $dbh") if (!ref($dbh));
522 if ($proto eq "mysql" || $proto eq "postgresql") {
523 # Get old password, for change detection
524 my $cmd = $dbh->prepare(
525 "select pass from webmin_user where id = ?");
526 $cmd && $cmd->execute($user{'id'}) ||
527 &error("Failed to get old password : ".$dbh->errstr);
528 my ($oldpass) = $cmd->fetchrow();
530 &add_old_password(\%user, $oldpass, \%miniserv);
532 # Update primary details
533 my $cmd = $dbh->prepare("update webmin_user set name = ?, ".
534 "pass = ? where id = ?");
535 $cmd && $cmd->execute($user{'name'}, $user{'pass'},
537 &error("Failed to update user : ".$dbh->errstr);
541 my $cmd = $dbh->prepare("delete from webmin_user_attr ".
543 $cmd && $cmd->execute($user{'id'}) ||
544 &error("Failed to delete attrs : ".$dbh->errstr);
545 my $cmd = $dbh->prepare("insert into webmin_user_attr ".
546 "(id,attr,value) values (?, ?, ?)");
547 foreach my $attr (keys %user) {
548 next if ($attr eq "name" || $attr eq "pass");
549 my $value = $user{$attr};
550 if ($attr eq "olds" || $attr eq "modules" ||
551 $attr eq "ownmods") {
552 $value = join(" ", @$value);
554 $cmd->execute($user{'id'}, $attr, $value) ||
555 &error("Failed to add user attribute : ".
560 elsif ($proto eq "ldap") {
561 # Rename in LDAP if needed
562 if ($user{'name'} ne $username) {
563 my $newdn = $user{'id'};
564 $newdn =~ s/^cn=\Q$username\E,/cn=$user{'name'},/;
565 my $rv = $dbh->moddn($user{'id'},
566 newrdn => "cn=$user{'name'}");
567 if (!$rv || $rv->code) {
568 &error("Failed to rename user : ".
569 ($rv ? $rv->error : "Unknown error"));
571 $user{'id'} = $newdn;
574 # Re-save all the attributes
575 my @attrs = ( "cn", $user{'name'},
576 "webminPass", $user{'pass'} );
578 foreach my $attr (keys %user) {
579 next if ($attr eq "name" || $attr eq "desc" ||
581 my $value = $user{$attr};
582 if ($attr eq "olds" || $attr eq "ownmods") {
583 $value = join(" ", @$value);
586 defined($value) ? $attr."=".$value : $attr);
588 push(@attrs, "webminAttr", \@webminattrs);
589 push(@attrs, "webminModule", $user{'modules'});
590 my $rv = $dbh->modify($user{'id'}, replace => { @attrs });
591 if (!$rv || $rv->code) {
592 &error("Failed to modify user : ".
593 ($rv ? $rv->error : "Unknown error"));
596 &disconnect_userdb($miniserv{'userdb'}, $dbh);
600 &lock_file($ENV{'MINISERV_CONFIG'});
601 delete($miniserv{"preroot_".$username});
602 if ($user{'theme'}) {
603 $miniserv{"preroot_".$user{'name'}} =
604 $user{'theme'}.($user{'overlay'} ? " ".$user{'overlay'} : "");
606 elsif (defined($user{'theme'})) {
607 $miniserv{"preroot_".$user{'name'}} = "";
609 local @logout = split(/\s+/, $miniserv{'logouttimes'});
610 @logout = grep { $_ !~ /^$username=/ } @logout;
611 if (defined($user{'logouttime'})) {
612 push(@logout, "$user{'name'}=$user{'logouttime'}");
614 $miniserv{'logouttimes'} = join(" ", @logout);
615 &put_miniserv_config(\%miniserv);
616 &unlock_file($ENV{'MINISERV_CONFIG'});
619 push(@times, "days", $user{'days'}) if ($user{'days'} ne '');
620 push(@times, "hours", $user{'hoursfrom'}."-".$user{'hoursto'})
621 if ($user{'hoursfrom'});
622 &lock_file($miniserv{'userfile'});
623 open(PWFILE, $miniserv{'userfile'});
626 &open_tempfile(PWFILE, ">$miniserv{'userfile'}");
628 if (/^([^:]+):([^:]*)/ && $1 eq $username) {
629 &add_old_password(\%user, "$2", \%miniserv);
630 &print_tempfile(PWFILE,
631 "$user{'name'}:$user{'pass'}:",
632 "$user{'sync'}:$user{'cert'}:",
633 ($user{'allow'} ? "allow $user{'allow'}" :
634 $user{'deny'} ? "deny $user{'deny'}" : ""),":",
635 join(" ", @times),":",
636 $user{'lastchange'},":",
637 join(" ", @{$user{'olds'}}),":",
638 $user{'minsize'},":",
639 $user{'nochange'},":",
644 &print_tempfile(PWFILE, $_);
647 &close_tempfile(PWFILE);
648 &unlock_file($miniserv{'userfile'});
650 &lock_file(&acl_filename());
651 @mods = &list_modules();
652 open(ACL, &acl_filename());
655 &open_tempfile(ACL, ">".&acl_filename());
657 if (/^(\S+):/ && $1 eq $username) {
658 &print_tempfile(ACL, &acl_line($_[1], \@mods));
661 &print_tempfile(ACL, $_);
664 &close_tempfile(ACL);
665 &unlock_file(&acl_filename());
667 delete($gconfig{"lang_".$username});
668 $gconfig{"lang_".$user{'name'}} = $user{'lang'} if ($user{'lang'});
669 delete($gconfig{"notabs_".$username});
670 $gconfig{"notabs_".$user{'name'}} = $user{'notabs'}
671 if ($user{'notabs'});
672 delete($gconfig{"skill_".$username});
673 $gconfig{"skill_".$user{'name'}} = $user{'skill'} if ($user{'skill'});
674 delete($gconfig{"risk_".$username});
675 $gconfig{"risk_".$user{'name'}} = $user{'risk'} if ($user{'risk'});
676 delete($gconfig{"rbacdeny_".$username});
677 $gconfig{"rbacdeny_".$user{'name'}} = $user{'rbacdeny'}
678 if ($user{'rbacdeny'});
679 delete($gconfig{"ownmods_".$username});
680 $gconfig{"ownmods_".$user{'name'}} = join(" ", @{$user{'ownmods'}})
681 if (@{$user{'ownmods'}});
682 delete($gconfig{"theme_".$username});
683 if ($user{'theme'}) {
684 $gconfig{"theme_".$user{'name'}} =
685 $user{'theme'}.($user{'overlay'} ? " ".$user{'overlay'} : "");
687 elsif (defined($user{'theme'})) {
688 $gconfig{"theme_".$user{'name'}} = '';
690 delete($gconfig{"readonly_".$username});
691 $gconfig{"readonly_".$user{'name'}} = $user{'readonly'}
692 if (defined($user{'readonly'}));
693 delete($gconfig{"realname_".$username});
694 $gconfig{"realname_".$user{'name'}} = $user{'real'}
695 if (defined($user{'real'}));
696 &write_file("$config_directory/config", \%gconfig);
699 if ($username ne $user{'name'} && !$user{'proto'}) {
700 # Rename all .acl files if user renamed
701 foreach $m (@mods, "") {
702 local $file = "$config_directory/$m/$username.acl";
705 "$config_directory/$m/$user{'name'}.acl");
708 local $file = "$config_directory/$username.acl";
710 &rename_file($file, "$config_directory/$user{'name'}.acl");
714 if ($miniserv{'session'} && $username ne $user{'name'}) {
715 # Modify all sessions for the renamed user
716 &rename_session_user(\&miniserv, $username, $user{'name'});
720 =head2 add_old_password(&user, oldpass, &miniserv)
722 Internal function to update the olds list of old passwords for a user
727 my ($user, $oldpass, $miniserv) = @_;
728 if ($oldpass ne $user->{'pass'} &&
729 "!".$oldpass ne $user->{'pass'} &&
730 $oldpass ne "!".$user->{'pass'} &&
731 $user->{'pass'} ne 'x' &&
732 $user->{'pass'} ne 'e' &&
733 $user->{'pass'} ne '*LK*') {
734 # Password change detected .. update change time
735 # and save the old one
736 local $nolock = $oldpass;
738 unshift(@{$user->{'olds'}}, $nolock);
739 if ($miniserv->{'pass_oldblock'}) {
740 while(scalar(@{$user->{'olds'}}) >
741 $miniserv->{'pass_oldblock'}) {
742 pop(@{$user->{'olds'}});
745 $user->{'lastchange'} = time();
749 =head2 delete_user(name)
751 Deletes the named user, including all .acl files for detailed module access
758 my (@pwfile, @acl, %miniserv);
761 &lock_file($ENV{'MINISERV_CONFIG'});
762 &get_miniserv_config(\%miniserv);
763 delete($miniserv{"preroot_".$username});
764 my @logout = split(/\s+/, $miniserv{'logouttimes'});
765 @logout = grep { $_ !~ /^$username=/ } @logout;
766 $miniserv{'logouttimes'} = join(" ", @logout);
767 &put_miniserv_config(\%miniserv);
768 &unlock_file($ENV{'MINISERV_CONFIG'});
770 &lock_file($miniserv{'userfile'});
771 open(PWFILE, $miniserv{'userfile'});
774 &open_tempfile(PWFILE, ">$miniserv{'userfile'}");
776 if (!/^([^:]+):/ || $1 ne $username) {
777 &print_tempfile(PWFILE, $_);
780 &close_tempfile(PWFILE);
781 &unlock_file($miniserv{'userfile'});
783 &lock_file(&acl_filename());
784 open(ACL, &acl_filename());
787 &open_tempfile(ACL, ">".&acl_filename());
789 if (!/^([^:]+):/ || $1 ne $username) {
790 &print_tempfile(ACL, $_);
793 &close_tempfile(ACL);
794 &unlock_file(&acl_filename());
796 delete($gconfig{"lang_".$username});
797 delete($gconfig{"notabs_".$username});
798 delete($gconfig{"skill_".$username});
799 delete($gconfig{"risk_".$username});
800 delete($gconfig{"ownmods_".$username});
801 delete($gconfig{"theme_".$username});
802 delete($gconfig{"readonly_".$username});
803 &write_file("$config_directory/config", \%gconfig);
805 # Delete all module .acl files
806 &unlink_file(map { "$config_directory/$_/$username.acl" } &list_modules());
807 &unlink_file("$config_directory/$username.acl");
809 if ($miniserv{'session'}) {
810 # Delete all sessions for the deleted user
811 &delete_session_user(\%miniserv, $username);
814 if ($miniserv{'userdb'}) {
815 # Also delete from user database
816 my ($dbh, $proto, $prefix, $args) =&connect_userdb($miniserv{'userdb'});
817 &error("Failed to connect to user database : $dbh") if (!ref($dbh));
818 if ($proto eq "mysql" || $proto eq "postgresql") {
819 # Find the user with SQL query
820 my $cmd = $dbh->prepare(
821 "select id from webmin_user where name = ?");
822 $cmd && $cmd->execute($username) ||
823 &error("Failed to find user : ".$dbh->errstr);
824 my ($id) = $cmd->fetchrow();
829 my $cmd = $dbh->prepare(
830 "delete from webmin_user where id = ?");
831 $cmd && $cmd->execute($id) ||
832 &error("Failed to delete user : ".$dbh->errstr);
836 my $cmd = $dbh->prepare(
837 "delete from webmin_user_attr where id = ?");
838 $cmd && $cmd->execute($id) ||
839 &error("Failed to delete user attrs : ".
844 my $cmd = $dbh->prepare(
845 "delete from webmin_user_acl where id = ?");
846 $cmd && $cmd->execute($id) ||
847 &error("Failed to delete user acls : ".
852 elsif ($proto eq "ldap") {
853 # Find user with LDAP query
854 my $rv = $dbh->search(
856 filter => '(&(cn='.$username.')(objectClass='.
857 $args->{'userclass'}.'))',
859 if (!$rv || $rv->code) {
860 &error("Failed to find user : ".
861 ($rv ? $rv->error : "Unknown error"));
863 my ($user) = $rv->all_entries;
867 my $rv = $dbh->search(
869 filter => '(objectClass=*)',
871 if (!$rv || $rv->code) {
872 &error("Failed to delete LDAP user : ".
873 ($rv ? $rv->error : "Unknown error"));
875 foreach my $so ($rv->all_entries) {
876 next if ($so->dn() eq $user->dn());
877 my $drv = $dbh->delete($so->dn());
879 &error("Failed to delete LDAP ".
880 "sub-object : ".$drv->error);
884 # Delete the user from LDAP
885 my $rv = $dbh->delete($user->dn());
886 if (!$rv || $rv->code) {
887 &error("Failed to delete LDAP user : ".
888 ($rv ? $rv->error : "Unknown error"));
892 &disconnect_userdb($miniserv{'userdb'}, $dbh);
896 =head2 create_group(&group, [clone])
898 Add a new webmin group, based on the details in the group hash. The required
901 =item name - Unique name of the group
903 =item modules - An array reference of module names
905 =item members - An array reference of group member names. Sub-groups must have their names prefixed with an @.
910 my %group = %{$_[0]};
913 &get_miniserv_config(\%miniserv);
915 if ($miniserv{'userdb'} && !$miniserv{'userdb_addto'}) {
916 # Adding to group database
917 my ($dbh, $proto, $prefix, $args) =&connect_userdb($miniserv{'userdb'});
918 &error("Failed to connect to group database : $dbh") if (!ref($dbh));
919 if ($proto eq "mysql" || $proto eq "postgresql") {
921 my $cmd = $dbh->prepare("insert into webmin_group (name,description) values (?, ?)");
922 $cmd && $cmd->execute($group{'name'}, $group{'desc'}) ||
923 &error("Failed to add group : ".$dbh->errstr);
925 my $cmd = $dbh->prepare("select max(id) from webmin_group");
927 my ($id) = $cmd->fetchrow();
930 # Add other attributes
931 my $cmd = $dbh->prepare("insert into webmin_group_attr (id,attr,value) values (?, ?, ?)");
932 foreach my $attr (keys %group) {
933 next if ($attr eq "name" || $attr eq "desc");
934 my $value = $group{$attr};
935 if ($attr eq "members" || $attr eq "modules" ||
936 $attr eq "ownmods") {
937 $value = join(" ", @$value);
939 $cmd->execute($id, $attr, $value) ||
940 &error("Failed to add group attribute : ".
945 $_[0]->{'proto'} = $proto;
947 elsif ($proto eq "ldap") {
949 my $dn = "cn=".$group{'name'}.",".$prefix;
950 my @attrs = ( "objectClass", $args->{'groupclass'},
951 "cn", $group{'name'},
952 "webminDesc", $group{'desc'} );
954 foreach my $attr (keys %group) {
955 next if ($attr eq "name" || $attr eq "desc" ||
957 my $value = $group{$attr};
958 if ($attr eq "members" || $attr eq "ownmods") {
959 $value = join(" ", @$value);
961 push(@webminattrs, $attr."=".$value);
964 push(@attrs, "webminAttr", \@webminattrs);
966 if (@{$group{'modules'}}) {
967 push(@attrs, "webminModule", $group{'modules'});
969 my $rv = $dbh->add($dn, attr => \@attrs);
970 if (!$rv || $rv->code) {
971 &error("Failed to add group to LDAP : ".
972 ($rv ? $rv->error : "Unknown error"));
975 $_[0]->{'proto'} = 'ldap';
977 &disconnect_userdb($miniserv{'userdb'}, $dbh);
978 $group{'proto'} = $proto;
981 # Adding to local files
982 &lock_file("$config_directory/webmin.groups");
983 open(GROUP, ">>$config_directory/webmin.groups");
984 print GROUP &group_line(\%group),"\n";
986 &unlock_file("$config_directory/webmin.groups");
990 # Clone ACLs from original group
991 ©_acl_files($clone, $group{'name'}, [ "", &list_modules() ],
996 =head2 modify_group(old-name, &group)
998 Update a webmin group, identified by the name parameter. The group's new
999 details are in the group hash ref, which must be in the same format as
1000 returned by list_groups.
1005 my $groupname = $_[0];
1006 my %group = %{$_[1]};
1008 &get_miniserv_config(\%miniserv);
1010 if ($group{'proto'}) {
1011 # In users and groups DB
1012 my ($dbh, $proto) = &connect_userdb($miniserv{'userdb'});
1013 &error("Failed to connect to group database : $dbh") if (!ref($dbh));
1014 if ($proto eq "mysql" || $proto eq "postgresql") {
1015 # Update primary details
1016 my $cmd = $dbh->prepare("update webmin_group set name = ?, ".
1017 "description = ? where id = ?");
1018 $cmd && $cmd->execute($group{'name'}, $group{'desc'},
1020 &error("Failed to update group : ".$dbh->errstr);
1023 # Re-save attributes
1024 my $cmd = $dbh->prepare("delete from webmin_group_attr ".
1026 $cmd && $cmd->execute($group{'id'}) ||
1027 &error("Failed to delete attrs : ".$dbh->errstr);
1028 my $cmd = $dbh->prepare("insert into webmin_group_attr ".
1029 "(id,attr,value) values (?, ?, ?)");
1030 foreach my $attr (keys %group) {
1031 next if ($attr eq "name" || $attr eq "desc");
1032 my $value = $group{$attr};
1033 if ($attr eq "members" || $attr eq "modules" ||
1034 $attr eq "ownmods") {
1035 $value = join(" ", @$value);
1037 $cmd->execute($group{'id'}, $attr, $value) ||
1038 &error("Failed to add group attribute : ".
1043 elsif ($proto eq "ldap") {
1044 # Rename in LDAP if needed
1045 if ($group{'name'} ne $groupname) {
1046 my $newdn = $group{'id'};
1047 $newdn =~ s/^cn=\Q$groupname\E,/cn=$group{'name'},/;
1048 my $rv = $dbh->moddn($group{'id'},
1049 newrdn => "cn=$group{'name'}");
1050 if (!$rv || $rv->code) {
1051 &error("Failed to rename group : ".
1052 ($rv ? $rv->error : "Unknown error"));
1054 $group{'id'} = $newdn;
1057 # Re-save all the attributes
1058 my @attrs = ( "cn", $group{'name'},
1059 "webminDesc", $group{'desc'} );
1061 foreach my $attr (keys %group) {
1062 next if ($attr eq "name" || $attr eq "desc" ||
1063 $attr eq "modules");
1064 my $value = $group{$attr};
1065 if ($attr eq "members" || $attr eq "ownmods") {
1066 $value = join(" ", @$value);
1068 push(@webminattrs, $attr."=".$value);
1070 push(@attrs, "webminAttr", \@webminattrs);
1071 push(@attrs, "webminModule", $group{'modules'});
1072 my $rv = $dbh->modify($group{'id'}, replace => { @attrs });
1073 if (!$rv || $rv->code) {
1074 &error("Failed to modify group : ".
1075 ($rv ? $rv->error : "Unknown error"));
1078 &disconnect_userdb($miniserv{'userdb'}, $dbh);
1082 &lock_file("$config_directory/webmin.groups");
1083 local $lref = &read_file_lines("$config_directory/webmin.groups");
1084 foreach $l (@$lref) {
1085 if ($l =~ /^([^:]+):/ && $1 eq $groupname) {
1086 $l = &group_line(\%group);
1089 &flush_file_lines("$config_directory/webmin.groups");
1090 &unlock_file("$config_directory/webmin.groups");
1093 if ($groupname ne $group{'name'} && !$group{'proto'}) {
1094 # Rename all .gacl files if group renamed
1095 foreach my $m (@{$group{'modules'}}, "") {
1096 local $file = "$config_directory/$m/$groupname.gacl";
1099 "$config_directory/$m/$group{'name'}.gacl");
1105 =head2 delete_group(name)
1107 Delete a webmin group, identified by the name parameter.
1112 my ($groupname) = @_;
1114 &get_miniserv_config(\%miniserv);
1116 # Delete from local files
1117 &lock_file("$config_directory/webmin.groups");
1118 my $lref = &read_file_lines("$config_directory/webmin.groups");
1119 @$lref = grep { !/^([^:]+):/ || $1 ne $groupname } @$lref;
1120 &flush_file_lines();
1121 &unlock_file("$config_directory/webmin.groups");
1122 &unlink_file(map { "$config_directory/$_/$groupname.gacl" } &list_modules());
1124 if ($miniserv{'userdb'}) {
1125 # Also delete from group database
1126 my ($dbh, $proto, $prefix, $args) =&connect_userdb($miniserv{'userdb'});
1127 &error("Failed to connect to group database : $dbh") if (!ref($dbh));
1128 if ($proto eq "mysql" || $proto eq "postgresql") {
1129 # Find the group with SQL query
1130 my $cmd = $dbh->prepare(
1131 "select id from webmin_group where name = ?");
1132 $cmd && $cmd->execute($groupname) ||
1133 &error("Failed to find group : ".$dbh->errstr);
1134 my ($id) = $cmd->fetchrow();
1139 my $cmd = $dbh->prepare(
1140 "delete from webmin_group where id = ?");
1141 $cmd && $cmd->execute($id) ||
1142 &error("Failed to delete group : ".$dbh->errstr);
1146 my $cmd = $dbh->prepare(
1147 "delete from webmin_group_attr where id = ?");
1148 $cmd && $cmd->execute($id) ||
1149 &error("Failed to delete group attrs : ".
1154 my $cmd = $dbh->prepare(
1155 "delete from webmin_group_acl where id = ?");
1156 $cmd && $cmd->execute($id) ||
1157 &error("Failed to delete group acls : ".
1162 elsif ($proto eq "ldap") {
1163 # Find group with LDAP query
1164 my $rv = $dbh->search(
1166 filter => '(&(cn='.$groupname.')(objectClass='.
1167 $args->{'groupclass'}.'))',
1169 if (!$rv || $rv->code) {
1170 &error("Failed to find group : ".
1171 ($rv ? $rv->error : "Unknown error"));
1173 my ($group) = $rv->all_entries;
1176 # Delete sub-objects
1177 my $rv = $dbh->search(
1178 base => $group->dn(),
1179 filter => '(objectClass=*)',
1181 if (!$rv || $rv->code) {
1182 &error("Failed to delete LDAP group : ".
1183 ($rv ? $rv->error : "Unknown error"));
1185 foreach my $so ($rv->all_entries) {
1186 next if ($so->dn() eq $group->dn());
1187 my $drv = $dbh->delete($so->dn());
1189 &error("Failed to delete LDAP ".
1190 "sub-object : ".$drv->error);
1194 # Delete the group from LDAP
1195 my $rv = $dbh->delete($group->dn());
1196 if (!$rv || $rv->code) {
1197 &error("Failed to delete LDAP group : ".
1198 ($rv ? $rv->error : "Unknown error"));
1202 &disconnect_userdb($miniserv{'userdb'}, $dbh);
1207 =head2 group_line(&group)
1209 Internal function to generate a group file line
1214 return join(":", $_[0]->{'name'},
1215 join(" ", @{$_[0]->{'members'}}),
1216 join(" ", @{$_[0]->{'modules'}}),
1218 join(" ", @{$_[0]->{'ownmods'}}) );
1221 =head2 acl_line(&user, &allmodules)
1223 Internal function to generate an ACL file line.
1228 my %user = %{$_[0]};
1229 return "$user{'name'}: ".join(' ', @{$user{'modules'}})."\n";
1232 =head2 can_edit_user(user, [&groups])
1234 Returns 1 if the current Webmin user can edit some other user.
1239 return 1 if ($access{'users'} eq '*');
1240 if ($access{'users'} eq '~') {
1241 return $base_remote_user eq $_[0];
1243 my $glist = $_[1] ? $_[1] : [ &list_groups() ];
1244 foreach my $u (split(/\s+/, $access{'users'})) {
1245 if ($u =~ /^_(\S+)$/) {
1246 foreach my $g (@$glist) {
1247 return 1 if ($g->{'name'} eq $1 &&
1248 &indexof($_[0], @{$g->{'members'}}) >= 0);
1252 return 1 if ($u eq $_[0]);
1258 =head2 open_session_db(\%miniserv)
1260 Opens the session database, and ties it to the sessiondb hash. Parameters are :
1262 =item miniserv - The Webmin miniserv.conf file as a hash ref, as supplied by get_miniserv_config
1267 my $sfile = $_[0]->{'sessiondb'} ? $_[0]->{'sessiondb'} :
1268 $_[0]->{'pidfile'} =~ /^(.*)\/[^\/]+$/ ? "$1/sessiondb"
1270 eval "use SDBM_File";
1271 dbmopen(%sessiondb, $sfile, 0700);
1272 eval { $sessiondb{'1111111111'} = 'foo bar' };
1274 dbmclose(%sessiondb);
1275 eval "use NDBM_File";
1276 dbmopen(%sessiondb, $sfile, 0700);
1279 delete($sessiondb{'1111111111'});
1283 =head2 delete_session_id(\%miniserv, id)
1285 Deletes one session from the database. Parameters are :
1287 =item miniserv - The Webmin miniserv.conf file as a hash ref, as supplied by get_miniserv_config.
1289 =item user - ID of the session to remove.
1292 sub delete_session_id
1294 return 1 if (&is_readonly_mode());
1295 &open_session_db($_[0]);
1296 my $ex = exists($sessiondb{$_[1]});
1297 delete($sessiondb{$_[1]});
1298 dbmclose(%sessiondb);
1302 =head2 delete_session_user(\%miniserv, user)
1304 Deletes all sessions for some user. Parameters are :
1306 =item miniserv - The Webmin miniserv.conf file as a hash ref, as supplied by get_miniserv_config.
1308 =item user - Name of the user whose sessions get removed.
1311 sub delete_session_user
1313 return 1 if (&is_readonly_mode());
1314 &open_session_db($_[0]);
1315 foreach my $s (keys %sessiondb) {
1316 local ($u,$t) = split(/\s+/, $sessiondb{$s});
1318 delete($sessiondb{$s});
1321 dbmclose(%sessiondb);
1324 =head2 rename_session_user(\%miniserv, olduser, newuser)
1326 Changes the username in all sessions for some user. Parameters are :
1328 =item miniserv - The Webmin miniserv.conf file as a hash ref, as supplied by get_miniserv_config.
1330 =item olduser - The original username.
1332 =item newuser - The new username.
1335 sub rename_session_user
1337 return 1 if (&is_readonly_mode());
1338 &open_session_db(\%miniserv);
1339 foreach my $s (keys %sessiondb) {
1340 local ($u,$t) = split(/\s+/, $sessiondb{$s});
1342 $sessiondb{$s} = "$_[2] $t";
1345 dbmclose(%sessiondb);
1348 =head2 update_members(&allusers, &allgroups, &modules, &members)
1350 Update the modules for members users and groups of some group. The parameters
1353 =item allusers - An array ref of all Webmin users, as returned by list_users.
1355 =item allgroups - An array ref of all Webmin groups.
1357 =item modules - Modules to assign to members.
1359 =item members - An array ref of member user and group names.
1364 foreach my $m (@{$_[3]}) {
1365 if ($m !~ /^\@(.*)$/) {
1367 my ($u) = grep { $_->{'name'} eq $m } @{$_[0]};
1369 $u->{'modules'} = [ @{$_[2]}, @{$u->{'ownmods'}} ];
1370 &modify_user($u->{'name'}, $u);
1375 my $gname = substr($m, 1);
1376 my ($g) = grep { $_->{'name'} eq $gname } @{$_[1]};
1378 $g->{'modules'} = [ @{$_[2]}, @{$g->{'ownmods'}} ];
1379 &modify_group($g->{'name'}, $g);
1380 &update_members($_[0], $_[1], $g->{'modules'},
1387 =head2 copy_acl_files(from, to, &modules, [from-type], [to-type])
1389 Copy all .acl files from some user to another user in a list of modules.
1390 The parameters are :
1392 =item from - Source user or group name.
1394 =item to - Destination user or group name.
1396 =item modules - Array ref of module names.
1398 =item from-type - Either "user" or "group", defaults to "user"
1400 =item to-type - Either "user" or "group", defaults to "user"
1405 my ($from, $to, $mods, $fromtype, $totype) = @_;
1406 $fromtype ||= "user";
1408 my ($dbh, $proto, $fromid, $toid);
1410 # Check if the source user/group is in a DB
1411 my $userdb = &get_userdb_string();
1413 ($dbh, $proto, $prefix, $args) = &connect_userdb($userdb);
1414 &error($dbh) if (!ref($dbh));
1415 if ($proto eq "mysql" || $proto eq "postgresql") {
1417 my $cmd = $dbh->prepare(
1418 "select id from webmin_${fromtype} where name = ?");
1419 $cmd && $cmd->execute($from) || &error($dbh->errstr);
1420 ($fromid) = $cmd->fetchrow();
1422 my $cmd = $dbh->prepare(
1423 "select id from webmin_${totype} where name = ?");
1424 $cmd && $cmd->execute($to) || &error($dbh->errstr);
1425 ($toid) = $cmd->fetchrow();
1428 elsif ($proto eq "ldap") {
1430 my $fromclass = $fromtype eq "user" ? "userclass"
1432 my $rv = $dbh->search(
1434 filter => '(&(cn='.$from.')(objectClass='.
1437 $rv->code && &error($rv->error);
1438 my ($fromobj) = $rv->all_entries;
1439 $fromid = $fromobj ? $fromobj->dn() : undef;
1440 my $toclass = $totype eq "user" ? "userclass"
1442 my $rv = $dbh->search(
1444 filter => '(&(cn='.$to.')(objectClass='.
1447 $rv->code && &error($rv->error);
1448 my ($toobj) = $rv->all_entries;
1449 $toid = $toobj ? $toobj->dn() : undef;
1453 if (defined($fromid) && defined($toid) &&
1454 ($proto eq "mysql" || $proto eq "postgresql")) {
1455 # Copy from database to database
1456 my $delcmd = $dbh->prepare("delete from webmin_${totype}_acl where id = ? and module = ?");
1457 my $cmd = $dbh->prepare("insert into webmin_${totype}_acl select ?,module,attr,value from webmin_${fromtype}_acl where id = ? and module = ?");
1458 foreach my $m (@$mods) {
1459 $delcmd && $delcmd->execute($toid, $m) ||
1460 &error("Failed to clear ACLs : ".$dbh->errstr);
1462 $cmd && $cmd->execute($toid, $fromid, $m) ||
1463 &error("Failed to copy ACLs : ".$dbh->errstr);
1467 elsif (!defined($fromid) && !defined($toid)) {
1469 my $fromsuffix = $fromtype eq "user" ? "acl" : "gacl";
1470 my $tosuffix = $totype eq "user" ? "acl" : "gacl";
1471 foreach my $m (@$mods) {
1472 &unlink_file("$config_directory/$m/$to.$tosuffix");
1474 if (&read_file("$config_directory/$m/$from.$fromsuffix",
1476 &write_file("$config_directory/$m/$to.$tosuffix",
1482 # Source and dest use different storage types
1483 foreach my $m (@$mods) {
1485 if ($fromtype eq "user") {
1486 %caccess = &get_module_acl($from, $m, 1, 1);
1489 %caccess = &get_group_module_acl($from, $m, 1);
1492 if ($totype eq "user") {
1493 &save_module_acl(\%caccess, $to, $m, 1);
1496 &save_group_module_acl(\%caccess, $to, $m, 1);
1502 &disconnect_userdb($userdb, $dbh);
1506 =head2 copy_group_acl_files(from, to, &modules)
1508 Copy all .gacl files from some group to another in a list of modules. Parameters
1511 =item from - Source group name.
1513 =item to - Destination group name.
1515 =item modules - Array ref of module names.
1518 sub copy_group_acl_files
1520 my ($from, $to, $mods) = @_;
1521 ©_acl_files($from, $to, $mods, "group", "group");
1524 =head2 copy_group_user_acl_files(from, to, &modules)
1526 Copy all .acl files from some group to a user in a list of modules. Parameters
1529 =item from - Source group name.
1531 =item to - Destination user name.
1533 =item modules - Array ref of module names.
1536 sub copy_group_user_acl_files
1538 my ($from, $to, $mods) = @_;
1539 ©_acl_files($from, $to, $mods, "group", "user");
1542 =head2 set_acl_files(&allusers, &allgroups, module, &members, &access)
1544 Recursively update the ACL for all sub-users and groups of a group, by copying
1545 detailed access control settings from the group down to users. Parameters are :
1547 =item allusers - An array ref of Webmin users, as returned by list_users.
1549 =item allgroups - An array ref of Webmin groups.
1551 =item module - Name of the module to update ACL for.
1553 =item members - Names of group members.
1555 =item access - The module ACL hash ref to copy to users.
1560 my ($allusers, $allgroups, $mod, $members, $access) = @_;
1561 foreach my $m (@$members) {
1562 if ($m !~ /^\@(.*)$/) {
1564 local ($u) = grep { $_->{'name'} eq $m } @$allusers;
1567 "$config_directory/$mod/$u->{'name'}.acl";
1568 &lock_file($aclfile);
1569 &save_module_acl($access, $u->{'name'}, $mod, 1);
1570 chmod(0640, $aclfile) if (-r $aclfile);
1571 &unlock_file($aclfile);
1576 local $gname = substr($m, 1);
1577 local ($g) = grep { $_->{'name'} eq $gname } @$allgroups;
1580 "$config_directory/$mod/$g->{'name'}.gacl";
1581 &lock_file($aclfile);
1582 &save_group_module_acl($access, $g->{'name'}, $mod, 1);
1583 chmod(0640, $aclfile) if (-r $aclfile);
1584 &unlock_file($aclfile);
1585 &set_acl_files($allusers, $allgroups, $mod,
1586 $g->{'members'}, $access);
1594 Returns the path to the openssl command (or equivalent) on this system.
1599 if (&has_command($config{'ssleay'})) {
1600 return &has_command($config{'ssleay'});
1602 elsif (&has_command("openssl")) {
1603 return &has_command("openssl");
1605 elsif (&has_command("ssleay")) {
1606 return &has_command("ssleay");
1613 =head2 encrypt_password(password, [salt])
1615 Encrypts and returns a Webmin user password. If the optional salt parameter
1616 is not given, a salt will be selected randomly.
1619 sub encrypt_password
1621 my ($pass, $salt) = @_;
1622 if ($gconfig{'md5pass'}) {
1623 # Use MD5 encryption
1624 $salt ||= '$1$'.substr(time(), -8).'$xxxxxxxxxxxxxxxxxxxxxx';
1625 return &encrypt_md5($pass, $salt);
1630 $salt ||= chr(int(rand(26))+65).chr(int(rand(26))+65);
1631 return &unix_crypt($pass, $salt);
1635 =head2 get_unixauth(\%miniserv)
1637 Returns a list of Unix users/groups/all and the Webmin user that they
1638 authenticate as, as array references.
1644 my @ua = split(/\s+/, $_[0]->{'unixauth'});
1645 foreach my $ua (@ua) {
1646 if ($ua =~ /^(\S+)=(\S+)$/) {
1647 push(@rv, [ $1, $2 ]);
1650 push(@rv, [ "*", $ua ]);
1656 =head2 save_unixauth(\%miniserv, &authlist)
1658 Updates %miniserv with the given Unix auth list, which must be in the format
1659 returned by get_unixauth.
1665 foreach my $ua (@{$_[1]}) {
1666 if ($ua->[0] ne "*") {
1667 push(@ua, "$ua->[0]=$ua->[1]");
1670 push(@ua, $ua->[1]);
1673 $_[0]->{'unixauth'} = join(" ", @ua);
1676 =head2 delete_from_groups(user|@group)
1678 Removes the specified user from all groups.
1681 sub delete_from_groups
1684 foreach my $g (&list_groups()) {
1685 my @mems = @{$g->{'members'}};
1686 my $i = &indexof($user, @mems);
1688 splice(@mems, $i, 1);
1689 $g->{'members'} = \@mems;
1690 &modify_group($g->{'name'}, $g);
1695 =head2 check_password_restrictions(username, password)
1697 Checks if some new password is valid for a user, and if not returns
1701 sub check_password_restrictions
1703 my ($name, $pass) = @_;
1705 &get_miniserv_config(\%miniserv);
1706 my ($user) = grep { $_->{'name'} eq $name } &list_users();
1707 my $minsize = $user ? $user->{'minsize'} : undef;
1708 $minsize ||= $miniserv{'pass_minsize'};
1709 if (length($pass) < $minsize) {
1710 return &text('cpass_minsize', $minsize);
1712 foreach my $re (split(/\t+/, $miniserv{'pass_regexps'})) {
1713 if ($re =~ /^\!(.*)$/) {
1715 $pass !~ /$re/ || return ($miniserv{'pass_regdesc'} ||
1716 $text{'cpass_notre'});
1719 $pass =~ /$re/ || return ($miniserv{'pass_regdesc'} ||
1723 if ($miniserv{'pass_nouser'}) {
1724 $pass =~ /\Q$name\E/i && return $text{'cpass_name'};
1726 if ($miniserv{'pass_nodict'}) {
1727 local $temp = &transname();
1728 &open_tempfile(TEMP, ">$temp", 0, 1);
1729 &print_tempfile(TEMP, $pass,"\n");
1730 &close_tempfile(TEMP);
1732 if (&has_command("ispell")) {
1733 open(SPELL, "ispell -a <$temp |");
1741 elsif (&has_command("spell")) {
1742 open(SPELL, "spell <$temp |");
1743 local $line = <SPELL>;
1744 $unknown++ if ($line);
1748 return &text('cpass_spellcmd', "<tt>ispell</tt>",
1751 $unknown || return $text{'cpass_dict'};
1753 if ($miniserv{'pass_oldblock'} && $user) {
1755 foreach my $o (@{$user->{'olds'}}) {
1756 local $enc = &encrypt_password($pass, $o);
1757 $enc eq $o && return $text{'cpass_old'};
1758 last if ($c++ > $miniserv{'pass_oldblock'});
1764 =head2 hash_session_id(sid)
1766 Returns an MD5 or Unix-crypted session ID.
1772 my $use_md5 = &md5_perl_module();
1773 if (!$hash_session_id_cache{$sid}) {
1776 $hash_session_id_cache{$sid} = &hash_md5_session($sid);
1780 $hash_session_id_cache{$sid} = &unix_crypt($sid, "XX");
1783 return $hash_session_id_cache{$sid};
1786 =head2 hash_md5_session(string)
1788 Returns a string encrypted in MD5 format.
1791 sub hash_md5_session
1794 my $use_md5 = &md5_perl_module();
1797 my $ctx = eval "new $use_md5";
1800 # Add some more stuff from the hash of the password and salt
1801 my $ctx1 = eval "new $use_md5";
1802 $ctx1->add($passwd);
1803 $ctx1->add($passwd);
1804 my $final = $ctx1->digest();
1805 for(my $pl=length($passwd); $pl>0; $pl-=16) {
1806 $ctx->add($pl > 16 ? $final : substr($final, 0, $pl));
1809 # This piece of code seems rather pointless, but it's in the C code that
1810 # does MD5 in PAM so it has to go in!
1813 for(my $i=length($passwd); $i; $i >>= 1) {
1818 $ctx->add(substr($passwd, $j, 1));
1821 $final = $ctx->digest();
1823 # Convert the 16-byte final string into a readable form
1825 my @final = map { ord($_) } split(//, $final);
1826 $l = ($final[ 0]<<16) + ($final[ 6]<<8) + $final[12];
1827 $rv .= &to64($l, 4);
1828 $l = ($final[ 1]<<16) + ($final[ 7]<<8) + $final[13];
1829 $rv .= &to64($l, 4);
1830 $l = ($final[ 2]<<16) + ($final[ 8]<<8) + $final[14];
1831 $rv .= &to64($l, 4);
1832 $l = ($final[ 3]<<16) + ($final[ 9]<<8) + $final[15];
1833 $rv .= &to64($l, 4);
1834 $l = ($final[ 4]<<16) + ($final[10]<<8) + $final[ 5];
1835 $rv .= &to64($l, 4);
1837 $rv .= &to64($l, 2);
1842 =head2 md5_perl_module
1844 Returns a Perl module for MD5 hashing, or undef if none.
1854 eval "use Digest::MD5";
1856 $use_md5 = "Digest::MD5";
1861 =head2 session_db_key(sid)
1863 Returns the session DB key for some session ID. Assumes that open_session_db
1864 has already been called.
1870 my $hash = &hash_session_id($sid);
1871 return $sessiondb{$hash} ? $hash : $sid;
1874 =head2 setup_anonymous_access(path, module)
1876 Grants anonymous access to some path. By default, the user for other anonymous
1877 access will be used, or if there is none, a user named 'anonymous' will be
1878 created and granted access to the module.
1881 sub setup_anonymous_access
1883 my ($path, $mod) = @_;
1885 # Find out what users and paths we grant access to currently
1887 &get_miniserv_config(\%miniserv);
1888 my @anon = split(/\s+/, $miniserv{'anonymous'});
1891 foreach my $a (@anon) {
1892 my ($p, $u) = split(/=/, $a);
1893 $found++ if ($p eq $path);
1896 return 1 if ($found); # Already setup
1899 # Create a user if need be
1900 $user = "anonymous";
1901 local $uinfo = { 'name' => $user,
1903 'modules' => [ $mod ],
1905 &create_user($uinfo);
1908 # Make sure the user has the module
1909 local ($uinfo) = grep { $_->{'name'} eq $user } &list_users();
1910 if ($uinfo && &indexof($mod, @{$uinfo->{'modules'}}) < 0) {
1911 push(@{$uinfo->{'modules'}}, $mod);
1912 &modify_user($uinfo->{'name'}, $uinfo);
1915 print STDERR "Anonymous access is granted to user $user, but he doesn't exist!\n";
1919 # Grant access to the user and path
1920 push(@anon, "$path=$user");
1921 $miniserv{'anonymous'} = join(" ", @anon);
1922 &put_miniserv_config(\%miniserv);
1926 =head2 join_userdb_string(proto, user, pass, host, prefix, &args)
1928 Creates a string in the format accepted by split_userdb_string
1931 sub join_userdb_string
1933 my ($proto, $user, $pass, $host, $prefix, $args) = @_;
1934 return "" if (!$proto);
1937 $argstr = "?".join("&", map { $_."=".$args->{$_} } (keys %$args));
1939 return $proto."://".$user.":".$pass."\@".$host."/".$prefix.$argstr;
1942 =head2 validate_userdb(string, [no-table-check])
1944 Checks if some user database is usable, and if not returns an error message
1949 my ($str, $notablecheck) = @_;
1950 my ($proto, $user, $pass, $host, $prefix, $args) = &split_userdb_string($str);
1951 if ($proto eq "mysql" || $proto eq "postgresql") {
1954 return &text('sql_emod', 'DBI') if ($@);
1955 if ($proto eq "mysql") {
1956 eval 'use DBD::mysql;';
1957 return &text('sql_emod', 'DBD::mysql') if ($@);
1958 my $drh = DBI->install_driver("mysql");
1959 return $text{'sql_emysqldriver'} if (!$drh);
1962 eval 'use DBD::Pg;';
1963 return &text('sql_emod', 'DBD::Pg') if ($@);
1964 my $drh = DBI->install_driver("Pg");
1965 return $text{'sql_epostgresqldriver'} if (!$drh);
1968 # Connect to the database
1969 my $dbh = &connect_userdb($str);
1970 ref($dbh) || return $dbh;
1972 # Validate critical tables
1973 if (!$notablecheck) {
1975 ( "webmin_user" => [ "id", "name", "pass" ],
1976 "webmin_group" => [ "id", "name", "description" ],
1977 "webmin_user_attr" => [ "id", "attr", "value" ],
1978 "webmin_group_attr" => [ "id", "attr", "value" ],
1979 "webmin_user_acl" => [ "id", "module", "attr", "value" ],
1980 "webmin_group_acl" => [ "id", "module", "attr", "value"],
1982 foreach my $t (keys %tables) {
1983 my @cols = @{$tables{$t}};
1984 my $sql = "select ".join(",", @cols)." from $t limit 1";
1985 my $cmd = $dbh->prepare($sql);
1986 if (!$cmd || !$cmd->execute()) {
1987 return &text('sql_etable', $t,
1988 &html_escape($dbh->errstr));
1993 &disconnect_userdb($str, $dbh);
1996 elsif ($proto eq "ldap") {
1998 eval 'use Net::LDAP;';
1999 return &text('sql_emod', 'Net::LDAP') if ($@);
2002 my $dbh = &connect_userdb($str);
2003 ref($dbh) || return $dbh;
2005 # Check for Webmin object classes
2006 my $schema = $dbh->schema();
2007 my @allocs = map { $_->{'name'} }
2008 $schema->all_objectclasses();
2009 &indexof($args->{'userclass'}, @allocs) >= 0 ||
2010 return &text('sql_eclass', $args->{'userclass'});
2011 &indexof($args->{'groupclass'}, @allocs) >= 0 ||
2012 return &text('sql_eclass', $args->{'groupclass'});
2014 # Check that base DN exists
2015 if (!$notablecheck) {
2016 my $superprefix = $prefix;
2017 $superprefix =~ s/^[^,]+,//; # Make parent DN
2018 my $rv = $dbh->search(base => $superprefix,
2019 filter => '(objectClass=*)',
2021 my $niceprefix = lc($prefix);
2022 $niceprefix =~ s/\s//g;
2024 foreach my $d ($rv->all_entries) {
2025 my $niced = lc($d->dn());
2027 $found++ if ($niced eq $niceprefix);
2029 $found || return &text('sql_eldapdn', $prefix);
2031 &disconnect_userdb($str, $dbh);
2035 return "Unknown user database type $proto";
2039 =head2 userdb_table_sql(string)
2041 Returns SQL statements needed to create all required tables. Mainly for
2045 sub userdb_table_sql
2048 my ($key, $auto, $idattrkey);
2049 if ($str =~ /^mysql:/) {
2050 return ( "create table webmin_user ".
2051 "(id int(20) not null primary key auto_increment, ".
2052 "name varchar(255) not null, pass varchar(255))",
2053 "create table webmin_group ".
2054 "(id int(20) not null primary key auto_increment, ".
2055 "name varchar(255) not null, ".
2056 "description varchar(255))",
2057 "create table webmin_user_attr ".
2058 "(id int(20) not null, ".
2059 "attr varchar(255) not null, ".
2060 "value varchar(4096), ".
2061 "primary key(id, attr))",
2062 "create table webmin_group_attr ".
2063 "(id int(20) not null, ".
2064 "attr varchar(255) not null, ".
2065 "value varchar(4096), ".
2066 "primary key(id, attr))",
2067 "create table webmin_user_acl ".
2068 "(id int(20) not null, ".
2069 "module varchar(32) not null, ".
2070 "attr varchar(32) not null, ".
2071 "value varchar(255), ".
2072 "primary key(id, module, attr))",
2073 "create table webmin_group_acl ".
2074 "(id int(20) not null, ".
2075 "module varchar(32) not null, ".
2076 "attr varchar(32) not null, ".
2077 "value varchar(255), ".
2078 "primary key(id, module, attr))",
2081 elsif ($str =~ /^postgresql:/) {
2082 return ( "create table webmin_user ".
2083 "(id serial not null primary key, ".
2084 "name varchar(255), ".
2085 "pass varchar(255))",
2086 "create table webmin_group ".
2087 "(id serial not null primary key, ".
2088 "name varchar(255), ".
2089 "description varchar(255))",
2090 "create table webmin_user_attr ".
2091 "(id int8 not null, ".
2092 "attr varchar(255) not null, ".
2093 "value varchar(4096), ".
2094 "primary key(id, attr))",
2095 "create table webmin_group_attr ".
2096 "(id int8 not null, ".
2097 "attr varchar(255) not null, ".
2098 "value varchar(4096), ".
2099 "primary key(id, attr))",
2100 "create table webmin_user_acl ".
2101 "(id int8 not null, ".
2102 "module varchar(255) not null, ".
2103 "attr varchar(255) not null, ".
2104 "value varchar(255), ".
2105 "primary key(id, module, attr))",
2106 "create table webmin_group_acl ".
2107 "(id int8 not null, ".
2108 "module varchar(255) not null, ".
2109 "attr varchar(255) not null, ".
2110 "value varchar(255), ".
2111 "primary key(id, module, attr))",