Completed password change lockout function
[webmin.git] / password_change.cgi
1 #!/usr/local/bin/perl
2 # password_change.cgi
3 # Actually update a user's password by directly modifying /etc/shadow
4
5 $ENV{'MINISERV_INTERNAL'} || die "Can only be called by miniserv.pl";
6 require './web-lib.pl';
7 &init_config();
8 &ReadParse();
9 &get_miniserv_config(\%miniserv);
10 $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";
11
12 # Validate inputs
13 $in{'new1'} ne '' || &pass_error($text{'password_enew1'});
14 $in{'new1'} eq $in{'new2'} || &pass_error($text{'password_enew2'});
15
16 # Is this a Webmin user?
17 if (&foreign_check("acl")) {
18         &foreign_require("acl", "acl-lib.pl");
19         ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users();
20         if ($wuser->{'pass'} eq 'x') {
21                 # A Webmin user, but using Unix authentication
22                 $wuser = undef;
23                 }
24         elsif ($wuser->{'pass'} eq '*LK*' ||
25                $wuser->{'pass'} =~ /^\!/) {
26                 &pass_error("Webmin users with locked accounts cannot change ".
27                             "their passwords!");
28                 }
29         }
30 if (!$in{'pam'} && !$wuser) {
31         $miniserv{'passwd_cindex'} ne '' && $miniserv{'passwd_mindex'} ne '' || 
32                 die "Missing password file configuration";
33         }
34
35 if ($wuser) {
36         # Update Webmin user's password
37         $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
38         $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});
39         $perr = &acl::check_password_restrictions($in{'user'}, $in{'new1'});
40         $perr && &pass_error(&text('password_enewpass', $perr));
41         $wuser->{'pass'} = &acl::encrypt_password($in{'new1'});
42         &acl::modify_user($wuser->{'name'}, $wuser);
43         &reload_miniserv();
44         }
45 elsif ($in{'pam'}) {
46         # Use PAM to make the change..
47         eval "use Authen::PAM;";
48         if ($@) {
49                 &pass_error(&text('password_emodpam', $@));
50                 }
51
52         # Check if the old password is correct
53         $service = $miniserv{'pam'} ? $miniserv{'pam'} : "webmin";
54         $pamh = new Authen::PAM($service, $in{'user'}, \&pam_check_func);
55         $rv = $pamh->pam_authenticate();
56         $rv == PAM_SUCCESS() ||
57                 &pass_error($text{'password_eold'});
58         $pamh = undef;
59
60         # Change the password with PAM, in a sub-process. This is needed because
61         # the UID must be changed to properly signal to the PAM libraries that
62         # the password change is not being done by the root user.
63         $temp = &transname();
64         $pid = fork();
65         @uinfo = getpwnam($in{'user'});
66         if (!$pid) {
67                 ($>, $<) = (0, $uinfo[2]);
68                 $pamh = new Authen::PAM("passwd", $in{'user'}, \&pam_change_func);
69                 $rv = $pamh->pam_chauthtok();
70                 open(TEMP, ">$temp");
71                 print TEMP "$rv\n";
72                 print TEMP ($messages || $pamh->pam_strerror($rv)),"\n";
73                 close(TEMP);
74                 exit(0);
75                 }
76         waitpid($pid, 0);
77         open(TEMP, $temp);
78         chop($rv = <TEMP>);
79         chop($messages = <TEMP>);
80         close(TEMP);
81         unlink($temp);
82         $rv == PAM_SUCCESS || &pass_error(&text('password_epam', $messages));
83         $pamh = undef;
84         }
85 else {
86         # Directly update password file
87
88         # Read shadow file and find user
89         &lock_file($miniserv{'passwd_file'});
90         $lref = &read_file_lines($miniserv{'passwd_file'});
91         for($i=0; $i<@$lref; $i++) {
92                 @line = split(/:/, $lref->[$i], -1);
93                 local $u = $line[$miniserv{'passwd_uindex'}];
94                 if ($u eq $in{'user'}) {
95                         $idx = $i;
96                         last;
97                         }
98                 }
99         defined($idx) || &pass_error($text{'password_euser'});
100
101         # Validate old password
102         &unix_crypt($in{'old'}, $line[$miniserv{'passwd_pindex'}]) eq
103                 $line[$miniserv{'passwd_pindex'}] ||
104                         &pass_error($text{'password_eold'});
105
106         # Make sure new password meets restrictions
107         if (&foreign_check("changepass")) {
108                 &foreign_require("changepass", "changepass-lib.pl");
109                 $err = &changepass::check_password($in{'new1'}, $in{'user'});
110                 &pass_error($err) if ($err);
111                 }
112         elsif (&foreign_check("useradmin")) {
113                 &foreign_require("useradmin", "user-lib.pl");
114                 $err = &useradmin::check_password_restrictions(
115                                 $in{'new1'}, $in{'user'});
116                 &pass_error($err) if ($err);
117                 }
118
119         # Set new password and save file
120         $salt = chr(int(rand(26))+65) . chr(int(rand(26))+65);
121         $line[$miniserv{'passwd_pindex'}] = &unix_crypt($in{'new1'}, $salt);
122         $days = int(time()/(24*60*60));
123         $line[$miniserv{'passwd_cindex'}] = $days;
124         $lref->[$idx] = join(":", @line);
125         &flush_file_lines();
126         &unlock_file($miniserv{'passwd_file'});
127         }
128
129 # Show ok page
130 &header(undef, undef, undef, undef, 1, 1);
131
132 print "<center><h3>",&text('password_done', "/"),"</h3></center>\n";
133
134 &footer();
135
136 sub pass_error
137 {
138 &header(undef, undef, undef, undef, 1, 1);
139 print "<hr>\n";
140
141 print "<center><h3>",$text{'password_err'}," : ",@_,"</h3></center>\n";
142
143 print "<hr>\n";
144 &footer();
145 exit;
146 }
147
148 sub pam_check_func
149 {
150 my @res;
151 while ( @_ ) {
152         my $code = shift;
153         my $msg = shift;
154         my $ans = "";
155
156         $ans = $in{'user'} if ($code == PAM_PROMPT_ECHO_ON());
157         $ans = $in{'old'} if ($code == PAM_PROMPT_ECHO_OFF());
158
159         push @res, PAM_SUCCESS();
160         push @res, $ans;
161         }
162 push @res, PAM_SUCCESS();
163 return @res;
164 }
165
166 sub pam_change_func
167 {
168 my @res;
169 while ( @_ ) {
170         my $code = shift;
171         my $msg = shift;
172         my $ans = "";
173         $messages = $msg;
174
175         if ($code == PAM_PROMPT_ECHO_ON()) {
176                 # Assume asking for username
177                 push @res, PAM_SUCCESS();
178                 push @res, $in{'user'};
179                 }
180         elsif ($code == PAM_PROMPT_ECHO_OFF()) {
181                 # Assume asking for a password (old first, then new)
182                 push @res, PAM_SUCCESS();
183                 if ($msg =~ /old|current/i) {
184                         push @res, $in{'old'};
185                         }
186                 else {
187                         push @res, $in{'new1'};
188                         }
189                 }
190         else {
191                 # Some message .. ignore it
192                 push @res, PAM_SUCCESS();
193                 push @res, undef;
194                 }
195         }
196 push @res, PAM_SUCCESS();
197 return @res;
198 }
199