#!/usr/bin/perl use Apache::PSWD; ############################################################## # User interface: # Called with the name of an existing (normal) user, allows # the user to set his password. # The user must already be authenticated and in the password file # in order for this to work. # When called with the ID of a user in the special "administrator" # group, presents an interface which allows adding, deleting, and # modifying passwords of other users, as well as adding users to # particular groups. # Copyright 1997, Lincoln D. Stein. All rights reserved. # See the accompanying HTML file for usage and distribution # information. The master version can be found at: # http://www.genome.wi.mit.edu/ftp/pub/software/WWW/passwd/ ############################################################## # >>>>>>>>>>>>>>>>>> SITE-SPECIFIC GLOBALS <<<<<<<<<<<<<<<<< # >>>>>>>> THESE MUST BE MODIFIED TO SUIT YOUR SITE <<<<<<<< # Path to our configuration file. Change as appropriate for # your site. $CONFIG_FILE = '/home/sydney/dixonj/auth/change_passwd.conf'; # Name of the administrators' group. When members of this group # call up this script, they will be able to create and edit other # users. Set to an empty string to disable this feature. $ADMIN_GROUP = 'administrators'; #$ADMIN_GROUP = ''; # Set this to the default group for new users, or an empty string # if you don't want there to be any. $DEFAULT_GROUP = 'users'; # Set this to "1" to require the script to be under # access control $REQUIRE_ACCESS_CONTROL = 0; # By default, the password and group files are set to be world-readable, # owner writable (-rw-r--r--). You may wish to change this to group-writable # if you wish to make this script set-gid. # e.g. $CREATE_MODE = 0664; $CREATE_MODE = 0664; # -rw-r--r-- # If you are using this script from the command line, you # may need to change $STTY to point to the position of the # 'stty' program on your system (it's used to turn off line echo # when entering passwords.) $STTY = '/bin/stty'; ########################################################################### # ------------------- NO USER SERVICEABLE PARTS BELOW --------------------- $ENV{PATH} = '/bin:/usr/bin'; $ENV{IFS} = ''; $MAX_SCROLL = 8; $Apache::PSWD::CREATE_MODE = $CREATE_MODE; BEGIN { if ($ENV{REQUEST_METHOD}) { require CGI; CGI->import(qw(:standard :html3 font)); require "CGI/Carp.pm"; CGI::Carp->import(); } } ($DEFAULT_DOMAIN,%DATABASES) = read_config(); if (!$ENV{REQUEST_METHOD}) { &dbm_manage; exit 0; } import_names('Q'); $Q::domain = $DEFAULT_DOMAIN unless $Q::domain; $referer = '' || $Q::referer || referer(); $url = '' || url(); # print the HTTP header. print header(), start_html('Change Password'); if (defined($Q::action) && $Q::action eq 'about') { about(); exit 0; } # Unless the user has authenticated himself, object. $user = remote_user(); if ($REQUIRE_ACCESS_CONTROL and !$user) { error_msg('No Authorization', 'This script can only be accessed by users who have authenticated themselves. ', 'Please place this script under authentication restrictions (both GET and POST) and try again.'); exit 0; } # Check the configuration and object if not defined. unless ($DATABASES{$Q::domain}) { error_msg('Invalid Domain', "The provided password/group configuration, $Q::domain, is undefined. ", 'Please define the configuration and try again.'); exit 0; } # Attempt to open the database. $entry = $DATABASES{$Q::domain}; $db = "Apache::PSWD::$entry->{type}"->new(-passwd => $entry->{passwd}, -group => $entry->{group} ); unless ($db) { error_msg('Invalid File', "Domain ",strong($Q::domain)," could not be opened: ", em($Apache::PSWD::error)); exit 0; } # If no user is defined by access control, then prompt for it. $user = get_user_from_params($db) unless $user; unless ($user) { &print_tail; exit 0; } # Make sure that the user is in the database. unless ($db->passwd($user)) { error_msg('Invalid User', "The user named \"$user\" is not found within the $Q::domain password file. ", "Permission denied."); exit 0; } # See if this user is in the magic group. if ($ADMIN_GROUP && $db->match_group(-name => $user, -group => $ADMIN_GROUP)) { do_administration($db); exit 0; } # At this point everything seems to be copascetic, so we can present the # password changing screen. if (defined($Q::password1) && defined($Q::password2) && $Q::password1 && $Q::password2) { &change_password ($db,$user,$Q::password1,$Q::password2); } else { &print_password_prompt; } &print_tail; sub print_password_prompt { print h1("Change password for $user"), 'Type your new password into both text fields and press "Change"', p(), start_form(), table( Tr( th("New Password"), td(password_field('password1')) ), Tr( th("Type it again"), td(password_field('password2')), td(submit(-name=>'action',-value=>'Change')) ) ); print hidden(-name=>'referer',-value=>$referer) if $referer; print hidden(-name=>'domain',-value=>defined($Q::domain) ? $Q::domain : 'default'); print hidden(-name=>'user',-default=>$user); print hidden(-name=>'passwd',-default=>''); print end_form(); } sub change_password { my ($db,$user,$password1,$password2) = @_; unless ($password1 eq $password2) { error_msg('Password Mismatch', "The two passwords don't match. ", "Please retype them."); print hr(); return; } # If we get here then it's OK to change the password. if ($db->set_passwd(-name=>$user,-passwd=>$password1)) { print h2('Password changed'), "Password for $user has been changed.", hr(); } else { print h2('Error changing password'), "An error occurred while changing your password. ", "Please try again.", hr(); } } sub print_tail { my $url = url(); print a({href=>$referer},"Exit the password changing pages") if $referer; print hr(), a({href=>"$url?action=about"},"About this script"), end_html(); } sub get_user_from_params { my $user = $Q::admin || $Q::user; my $passwd = $Q::passwd || $Q::passwd1; if ($user && $passwd) { return $user if $db->match_passwd(-name=>$user,-passwd=>$passwd); error_msg('Authentication Error', 'The user name and/or password you entered was incorrect. ', 'Please try again.'); print hr(); } print h1('Enter Current Password'), 'Enter your current user name and password, then press ',em("Submit"), start_form(), table( Tr( th('Name'), td(textfield(-name=>'user', -default=>user_name())) ), Tr( th('Password'), td(password_field(-name=>'passwd')), td(submit(-name=>'action',-value=>'Submit')) ) ); print hidden(-name=>'referer',-value=>$referer) if $referer; print hidden(-name=>'domain',-value=>defined($Q::domain) ? $Q::domain : 'default'); print end_form(); return undef; } sub about { $url=~s/action=about//; print h1('About change_passwd'), "This script was written by ",a({href=>'http://www.genome.wi.mit.edu/~lstein/'},"Lincoln D. Stein"),'. ', "You are free to modify and redistribute it, so long as this notice remains intact. ", "© Copyright 1997, Lincoln D. Stein. All rights reserved.", hr(), a({href=>$url},"Change password page."); } sub error_msg { my ($head,@rest) = @_; print h1(font({color=>'#FF0000'},$head)),@rest; } # --------------- Administration screens are defined here -------------- sub do_administration { my $db = shift; $_ = ''; $_ = $Q::action if defined($Q::action); # Because of the funny way that fields are set up, we take the # last member of the @user array if it is non-null. Otherwise, # the first. my $user = $Q::user[$#Q::user] || $Q::user; # do different things depending on the value of the # "action" variable. SWITCH: { /edit\/add/i and $db->passwd($user) && generate_user_list($db), generate_user_page($db,$user), last SWITCH; /delete/i and delete_user($db,$user), generate_user_list($db), last SWITCH; /set values/i and set_user($db,$user,$Q::password1,$Q::password2,@Q::groups) && generate_user_list($db), generate_user_page($db,$user), last SWITCH; # default generate_user_list($db); } &print_tail; } sub delete_user { my ($db,$user) = @_; if ($db->delete_user($user)) { print h1('User Deleted'), "The entry for user ",em($user)," was successfully deleted.", hr(); return 1; } else { error_msg('Error Deleting User', "An error occurred while deleting user $user: ", em("$Apache::PSWD::error."), " Please fix the error and try again. "); print hr(); return undef; } } sub set_user { my($db,$user,$password1,$password2,@groups) = @_; # The two passwords have to match. unless ($password1 eq $password2) { error_msg('Password Mismatch', "The two typed passwords don't match. ", 'Please try again.'), print hr(); return undef; } # The two passwords have to be non-null. unless ($password1) { error_msg('Invalid Password', 'The password has to be non-empty. ', 'Please type and confirm the new password.'); print hr(); return undef; } # If the passwords are different from the current entry for the user, then # we need to set it. my $current = $db->passwd($user); if ( !$current or ( ($current ne $password1) and !$db->match_passwd(-name=>$user,-passwd=>$password1)) ) { my $success = $db->set_passwd(-name=>$user,-passwd=>$password1); unless ($success) { error_msg('Error Setting Password', "An error occurred while setting the password: ", em("$Apache::PSWD::error."), " Please fix the error and try again. "); print hr(); return undef; } } # If the groups are different from the current entry, then we # need to set it. my @current_groups = $db->group($user); @groups = sort grep($_,@groups); # get rid of nonnull entries and sort if ("@current_groups" ne "@groups") { my $success = $db->set_group(-name=>$user,'-group'=>\@groups); unless ($success) { error_msg('Error Setting Groups', "An error occurred while setting the groups: ", em("$Apache::PSWD::error."), " Please fix the error and try again."); print hr(); return undef; } } # If we get here, then all is well. print h1('Edit successful'), "The entry for user ",em($user)," was successfully updated.", hr(); 1; } sub generate_user_list { my $db = shift; print h1("User List for Domain",em($Q::domain)); my @users = $db->users(); print start_form(), hidden(-name=>'referer',-value=>$referer), hidden(-name=>'domain',-value=>$Q::domain), $REQUIRE_ACCESS_CONTROL ? '' : ( hidden(-name=>'admin',-value=>$Q::user), hidden(-name=>'passwd',-value=>'') ), table( Tr( th({valign=>'TOP',align=>'RIGHT'},"Existing Users"), td({valign=>'TOP',align=>'LEFT',rowspan=>2}, @users > $MAX_SCROLL ? scrolling_list(-name=>'user','-values'=>\@users,-size=>$MAX_SCROLL) : popup_menu(-name=>'user','-values'=>\@users, -default=>$Q::user[$#user]||$Q::user, -override=>1) ), th({valign=>'MIDDLE',align=>'RIGHT'},"New User"), td({valign=>'MIDDLE',align=>'LEFT'},textfield(-name=>'user',-default=>'',-override=>1,-width=>16), ) ), Tr( th(''), td(''), td(submit(-name=>'action',-value=>'Edit/Add'), submit(-name=>'action',-value=>'Delete') ), ) ), end_form(), hr(); } sub generate_user_page { my $db = shift; my $user = shift; my $current_passwd = $db->passwd($user); my @groups = $db->group($user); my @all_groups = $db->groups(); @groups = ($DEFAULT_GROUP) if !@groups && $DEFAULT_GROUP; @all_groups = ($DEFAULT_GROUP) if !@all_groups && $DEFAULT_GROUP; print h1($current_passwd ? "Edit User \"$user\"" : "New User \"$user\""); print start_form(), hidden(-name=>'referer',-value=>$referer), hidden(-name=>'domain',-value=>$Q::domain), hidden(-name=>'user',-value=>$user), $REQUIRE_ACCESS_CONTROL ? '' : ( hidden(-name=>'admin',-value=>$Q::user), hidden(-name=>'passwd',-value=>'') ), table( Tr( th({align=>CENTER,colspan=>2},'Groups'), th({align=>CENTER,colspan=>2},'Password') ), Tr( th({valign=>TOP},'Defined:'), (@all_groups <= 5) ? td(checkbox_group(-name=>'groups','-values'=>\@all_groups, '-defaults'=>\@groups,-linebreak=>1)) : td(scrolling_list(-name=>'groups','-values'=>\@all_groups, '-defaults'=>\@groups,-multiple=>1)), td( table( Tr( th('Password:'), td(password_field(-name=>'password1',-default=>$current_passwd,-size=>12)) ), Tr( th('Confirm:'), td(password_field(-name=>'password2',-default=>$current_passwd,-size=>12)) ) ) ) ), Tr( th('New Group:'), td(textfield(-name=>'groups',-default=>'',-override=>1,-size=>12)), ), Tr( td(''), td(&reset(-value=>'Reset Values')), td(submit(-name=>'action',-value=>'Set Values')) ) ), end_form(); if (0) { # dead code? my $back = self_url; $back=~s/action=[^=&]*&?//g; $back=~s/password[0-9]?=[^=&]*&?//g; $back=~s/groups=[^=&]*&?//g; $back=~s/user=[^=&]*&?//g; $back.="user=$user"; print a({href=>$back},"List of Users"); } print hr(); } # --------------------- command line functions -------------------- # Usage: change_passwd.cgi ... # # commands: adduser deleteuser setgroup view # sub dbm_manage { # allow the domain to be optional unshift @ARGV,$DEFAULT_DOMAIN if $ARGV[0]=~/^(add|delete|domains|edit|group|view)$/i; my ($domain,$command,@rest) = @ARGV; @ARGV = (); my $usage = < ... Manage Apache databases from the command line. Arguments: domain Security domain [$DEFAULT_DOMAIN] command One of "add" "delete" "edit" "group" "view" "domains" Commands: Name Arguments Description ---- ---------- ----------- add Add/edit a user's password, optionally set groups delete Delete a user edit Same as "add" domains (none) List domains group Assign user to named group(s) view Get information about user view (none) Dump out entire domain USAGE ; die $usage if !$domain; die $usage if $domain=~/^-h/i; die $usage if $domain=~/^--help/i; my($entry) = $DATABASES{$domain}; die "Unknown database domain \"$domain\".\n",$usage unless $entry; my $db = "Apache::PSWD::$entry->{type}"->new(-passwd => $entry->{passwd}, -group => $entry->{group} ); die "Problem opening database ($Apache::PSWD::error).\n" unless $db; # Unless the current *UNIX* user is listed as belonging to the # administrators' group, undo any sgid and suid permissions that # might be in effect. Otherwise any user can mess with the files! # You can still change the files if you have the appropriate UNIX # permissions. my ($unix_name) = getpwuid($<); # get the real user id my (%groups); grep($groups{$_}++,$db->group($unix_name)); ($>,$)) = ($<,$() unless $groups{$ADMIN_GROUP}; $_ = $command; SWITCH: { /add/i and do_add($db,@rest),last SWITCH; /delete/i and do_delete($db,@rest),last SWITCH; /edit/i and do_add($db,@rest),last SWITCH; /domain/i and do_domain(),last SWITCH; /group/i and do_group($db,@rest),last SWITCH; /view/i and do_view($db,@rest),last SWITCH; die $usage; } } sub do_add { my($db,$user,$password,@groups) = @_; $user = $user || prompt('User name: '); $password = $password || password_prompt(); my $current = $db->passwd($user); print "Password successfully changed for $user.\n" if $db->set_passwd(-name=>$user,-passwd=>$password); @groups = map { split('\s*,\s*') } @groups; @groups = $DEFAULT_GROUP unless $current || @groups; @groups = () if $groups[0]=~/^(-|''|"")$/; print "Group set to @groups.\n" if @groups && $db->set_group(-name=>$user,-group=>\@groups); } sub do_delete { my($db,@user) = @_; @user = prompt('User name: ') unless @user; my $user; foreach $user (@user) { unless ($db->passwd($user)) { print "$user is not in users database.\n" ; next; } unless ($db->delete_user($user)) { print "$user: delete unsuccessful.\n"; next; } print "$user deleted.\n"; } } sub do_group { my($db,$user,@group) = @_; $user = $user || prompt('User name: '); die "$user is not in users database.\n" unless $db->passwd($user); @group = prompt("Enter comma-separated list of groups for $user: ") unless @group; die "No groups given.\n" unless @group; @group = map { split('\s*,\s*') } @group; @group = () if $group[0]=~/^(-|''|"")$/; die "Attempt to set groups failed.\n" unless $db->set_group(-name=>$user,-group=>\@group); print "Groups set for $user.\n"; } sub do_view { my($db,@user) = @_; my (@list); if (@user) { @list = @user; } else { @list = $db->users; } foreach (@list) { local($user,$passwd,@groups)=($_,$db->passwd($_),$db->group($_)); $passwd = "** unknown **" unless $passwd; local($group) = join(",",@groups); write; $- = 100; } } sub do_domain { $~='DOMAIN'; $^='DOMAIN_TOP'; foreach (sort keys %DATABASES) { local($name,$type,$password,$group) = (($_ eq $DEFAULT_DOMAIN ? "*$_" : $_), $DATABASES{$_}->{type},$ DATABASES{$_}->{passwd}, $DATABASES{$_}->{group}); write; $-=100; } } sub prompt { my $prompt = shift; my $line; do { print $prompt; chomp($line = <>); } until $line; return $line; } sub password_prompt { my $line; my ($pw1,$pw2); system "$STTY cbreak -echo>/dev/tty" and die "$STTY: $!"; # turn off echo do { $pw1 = prompt("New password: "); $pw2 = prompt("\nRe-type new password: "); print "\n"; print "The two passwords don't match. Try again.\n" unless $pw1 eq $pw2; } until $pw1 eq $pw2; system "$STTY -cbreak echo >/dev/tty"; # turn on echo return $pw1; } ################################### # Parse our configuration file... ################################### sub read_config { my %DATABASE; local(*CONF); my($domain,$directive,$value,$default_domain); open(CONF,$CONFIG_FILE) || die $!; while () { chomp; s/\#.*$//; # get rid of all comments if (//i) { die "Syntax error in $CONFIG_FILE, line $.: Missing directive.\n" if $domain; die "Syntax error in $CONFIG_FILE, line $.: directive without domain name.\n" unless $1; $domain = $1; $default_domain = $domain unless $default_domain; next; } if (/<\/Domain\s*>/i) { die "Syntax error in $CONFIG_FILE, line $.: seen without preceding directive.\n" unless $domain; die "Incomplete definition for domain $domain. Need UserFile and Type directives at line $.\n" unless $DATABASE{$domain}->{'passwd'} && $DATABASE{$domain}->{'type'}; undef $domain; next; } next unless ($directive,$value) = /(\w+)\s+(\S+)/; die "Syntax error in $CONFIG_FILE, line $.: $directive directive without preceding tag.\n" unless $domain; $directive=~tr/A-Z/a-z/; if ($directive eq 'userfile') { $DATABASE{$domain}->{passwd}=$value; next; } if ($directive eq 'groupfile') { $DATABASE{$domain}->{group}=$value; next; } if ($directive eq 'type') { $DATABASE{$domain}->{type}=$value; next; } die "Unknown directive $directive at line $.\n"; } close CONF; return ($default_domain,%DATABASE); } # These useless lines avoid "possible typo" warnings $foo = scalar(@Q::groups); $foo = $foo && $Q::referer; $foo = $Q::admin; format STDOUT_TOP= Name Password Groups ---- -------- ------ . format STDOUT= ^<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ $user,$passwd,$group . format DOMAIN_TOP= Name Type ---- ---- . format DOMAIN= @<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<< $name,$type .