Allow external password change command
[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 require './ui-lib.pl';
9 &ReadParse();
10 &get_miniserv_config(\%miniserv);
11 $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";
12
13 # Validate inputs
14 $in{'new1'} ne '' || &pass_error($text{'password_enew1'});
15 $in{'new1'} eq $in{'new2'} || &pass_error($text{'password_enew2'});
16
17 # Is this a Webmin user?
18 if (&foreign_check("acl")) {
19         &foreign_require("acl", "acl-lib.pl");
20         ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users();
21         if ($wuser->{'pass'} eq 'x') {
22                 # A Webmin user, but using Unix authentication
23                 $wuser = undef;
24                 }
25         elsif ($wuser->{'pass'} eq '*LK*' ||
26                $wuser->{'pass'} =~ /^\!/) {
27                 &pass_error("Webmin users with locked accounts cannot change ".
28                             "their passwords!");
29                 }
30         }
31 if (!$in{'pam'} && !$wuser) {
32         $miniserv{'passwd_cindex'} ne '' && $miniserv{'passwd_mindex'} ne '' || 
33                 die "Missing password file configuration";
34         }
35
36 if ($wuser) {
37         # Update Webmin user's password
38         $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
39         $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});
40         $perr = &acl::check_password_restrictions($in{'user'}, $in{'new1'});
41         $perr && &pass_error(&text('password_enewpass', $perr));
42         $wuser->{'pass'} = &acl::encrypt_password($in{'new1'});
43         $wuser->{'temppass'} = 0;
44         &acl::modify_user($wuser->{'name'}, $wuser);
45         &reload_miniserv();
46         }
47 elsif ($gconfig{'passwd_cmd'}) {
48         # Use some configured command
49         $passwd_cmd = &has_command($gconfig{'passwd_cmd'});
50         $passwd_cmd || &pass_error("The password change command <tt>$gconfig{'passwd_cmd'}</tt> was not found");
51
52         &foreign_require("proc", "proc-lib.pl");
53         &clean_environment();
54         $ENV{'REMOTE_USER'} = $in{'user'};      # some programs need this
55         $passwd_cmd .= " ".quotemeta($in{'user'});
56         ($fh, $fpid) = &proc::pty_process_exec($passwd_cmd, 0, 0);
57         &reset_environment();
58         while(1) {
59                 local $rv = &wait_for($fh,
60                            '(new|re-enter).*:',
61                            '(old|current|login).*:',
62                            'pick a password',
63                            'too\s+many\s+failures',
64                            'attributes\s+changed\s+on|successfully\s+changed',
65                            'pick your passwords');
66                 $out .= $wait_for_input;
67                 sleep(1);
68                 if ($rv == 0) {
69                         # Prompt for the new password
70                         syswrite($fh, $in{'new1'}."\n", length($in{'new1'})+1);
71                         }
72                 elsif ($rv == 1) {
73                         # Prompt for the old password
74                         syswrite($fh, $in{'old'}."\n", length($in{'old'})+1);
75                         }
76                 elsif ($rv == 2) {
77                         # Request for a menu option (SCO?)
78                         syswrite($fh, "1\n", 2);
79                         }
80                 elsif ($rv == 3) {
81                         # Failed too many times
82                         last;
83                         }
84                 elsif ($rv == 4) {
85                         # All done
86                         last;
87                         }
88                 elsif ($rv == 5) {
89                         # Request for a menu option (HP/UX)
90                         syswrite($fh, "p\n", 2);
91                         }
92                 else {
93                         last;
94                         }
95                 last if (++$count > 10);
96                 }
97         $crv = close($fh);
98         sleep(1);
99         waitpid($fpid, 1);
100         if ($? || $count > 10 ||
101             $out =~ /error|failed/i || $out =~ /bad\s+password/i) {
102                 &pass_error("<tt>".&html_escape($out)."</tt>");
103                 }
104         }
105 elsif ($in{'pam'}) {
106         # Use PAM to make the change..
107         eval "use Authen::PAM;";
108         if ($@) {
109                 &pass_error(&text('password_emodpam', $@));
110                 }
111
112         # Check if the old password is correct
113         $service = $miniserv{'pam'} ? $miniserv{'pam'} : "webmin";
114         $pamh = new Authen::PAM($service, $in{'user'}, \&pam_check_func);
115         $rv = $pamh->pam_authenticate();
116         $rv == PAM_SUCCESS() ||
117                 &pass_error($text{'password_eold'});
118         $pamh = undef;
119
120         # Change the password with PAM, in a sub-process. This is needed because
121         # the UID must be changed to properly signal to the PAM libraries that
122         # the password change is not being done by the root user.
123         $temp = &transname();
124         $pid = fork();
125         @uinfo = getpwnam($in{'user'});
126         if (!$pid) {
127                 ($>, $<) = (0, $uinfo[2]);
128                 $pamh = new Authen::PAM("passwd", $in{'user'}, \&pam_change_func);
129                 $rv = $pamh->pam_chauthtok();
130                 open(TEMP, ">$temp");
131                 print TEMP "$rv\n";
132                 print TEMP ($messages || $pamh->pam_strerror($rv)),"\n";
133                 close(TEMP);
134                 exit(0);
135                 }
136         waitpid($pid, 0);
137         open(TEMP, $temp);
138         chop($rv = <TEMP>);
139         chop($messages = <TEMP>);
140         close(TEMP);
141         unlink($temp);
142         $rv == PAM_SUCCESS || &pass_error(&text('password_epam', $messages));
143         $pamh = undef;
144         }
145 else {
146         # Directly update password file
147
148         # Read shadow file and find user
149         &lock_file($miniserv{'passwd_file'});
150         $lref = &read_file_lines($miniserv{'passwd_file'});
151         for($i=0; $i<@$lref; $i++) {
152                 @line = split(/:/, $lref->[$i], -1);
153                 local $u = $line[$miniserv{'passwd_uindex'}];
154                 if ($u eq $in{'user'}) {
155                         $idx = $i;
156                         last;
157                         }
158                 }
159         defined($idx) || &pass_error($text{'password_euser'});
160
161         # Validate old password
162         &unix_crypt($in{'old'}, $line[$miniserv{'passwd_pindex'}]) eq
163                 $line[$miniserv{'passwd_pindex'}] ||
164                         &pass_error($text{'password_eold'});
165
166         # Make sure new password meets restrictions
167         if (&foreign_check("changepass")) {
168                 &foreign_require("changepass", "changepass-lib.pl");
169                 $err = &changepass::check_password($in{'new1'}, $in{'user'});
170                 &pass_error($err) if ($err);
171                 }
172         elsif (&foreign_check("useradmin")) {
173                 &foreign_require("useradmin", "user-lib.pl");
174                 $err = &useradmin::check_password_restrictions(
175                                 $in{'new1'}, $in{'user'});
176                 &pass_error($err) if ($err);
177                 }
178
179         # Set new password and save file
180         $salt = chr(int(rand(26))+65) . chr(int(rand(26))+65);
181         $line[$miniserv{'passwd_pindex'}] = &unix_crypt($in{'new1'}, $salt);
182         $days = int(time()/(24*60*60));
183         $line[$miniserv{'passwd_cindex'}] = $days;
184         $lref->[$idx] = join(":", @line);
185         &flush_file_lines();
186         &unlock_file($miniserv{'passwd_file'});
187         }
188
189 # Change password in Usermin too
190 if (&get_product_name() eq 'usermin' &&
191     &foreign_check("changepass")) {
192         # XXX remote user??
193         &foreign_require("changepass", "changepass-lib.pl");
194         &changepass::change_mailbox_passwords(
195                 $in{'user'}, $in{'old'}, $in{'new1'});
196         }
197
198 # Show ok page
199 &header(undef, undef, undef, undef, 1, 1);
200
201 print "<center><h3>",&text('password_done', "/"),"</h3></center>\n";
202
203 &footer();
204
205 sub pass_error
206 {
207 &header(undef, undef, undef, undef, 1, 1);
208 print &ui_hr();
209
210 print "<center><h3>",$text{'password_err'}," : ",@_,"</h3></center>\n";
211
212 print &ui_hr();
213 &footer();
214 exit;
215 }
216
217 sub pam_check_func
218 {
219 my @res;
220 while ( @_ ) {
221         my $code = shift;
222         my $msg = shift;
223         my $ans = "";
224
225         $ans = $in{'user'} if ($code == PAM_PROMPT_ECHO_ON());
226         $ans = $in{'old'} if ($code == PAM_PROMPT_ECHO_OFF());
227
228         push @res, PAM_SUCCESS();
229         push @res, $ans;
230         }
231 push @res, PAM_SUCCESS();
232 return @res;
233 }
234
235 sub pam_change_func
236 {
237 my @res;
238 while ( @_ ) {
239         my $code = shift;
240         my $msg = shift;
241         my $ans = "";
242         $messages = $msg;
243
244         if ($code == PAM_PROMPT_ECHO_ON()) {
245                 # Assume asking for username
246                 push @res, PAM_SUCCESS();
247                 push @res, $in{'user'};
248                 }
249         elsif ($code == PAM_PROMPT_ECHO_OFF()) {
250                 # Assume asking for a password (old first, then new)
251                 push @res, PAM_SUCCESS();
252                 if ($msg =~ /old|current/i) {
253                         push @res, $in{'old'};
254                         }
255                 else {
256                         push @res, $in{'new1'};
257                         }
258                 }
259         else {
260                 # Some message .. ignore it
261                 push @res, PAM_SUCCESS();
262                 push @res, undef;
263                 }
264         }
265 push @res, PAM_SUCCESS();
266 return @res;
267 }
268