#!/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
.