diff --git a/account/tbacct.in b/account/tbacct.in index c6a9101ce9f6ca8cf6099e4227e4269b9003355e..0617316df3a3d328944fa836183e89b89f46af6d 100644 --- a/account/tbacct.in +++ b/account/tbacct.in @@ -123,6 +123,7 @@ use libaudit; use libdb; use libtestbed; use User; +use Project; # # Function prototypes @@ -141,6 +142,16 @@ sub fatal($); my $HOMEDIR = USERROOT(); +# +# Rewrite audit version of ARGV to prevent password in mail logs. +# +if ($ARGV[0] eq "passwd" && scalar(@ARGV) == 3) { + my @NEWARGV = @ARGV; + + $NEWARGV[scalar(@NEWARGV) - 1] = "**********"; + AuditSetARGV(@NEWARGV); +} + # # Parse command arguments. Once we return from getopts, all that should be # left are the required arguments. @@ -188,10 +199,23 @@ if (! defined($target_user)) { fatal("$user does not exist!"); } -# Map invoking user to object. -my $this_user = User->Lookup($UID); -if (! defined($this_user)) { - fatal("You ($UID) do not exist!"); +# +# Map invoking user to object. +# If invoked as "nobody" its for a user with no actual account, and so +# just set the current user to that user. If we make any callouts it will +# fail (verbosely of course). +# +my $this_user; + +if (getpwuid($UID) eq "nobody") { + $this_user = $target_user; +} +else { + $this_user = User->Lookup($UID); + + if (! defined($this_user)) { + fatal("You ($UID) do not exist!"); + } } # @@ -207,6 +231,7 @@ if (AuditStart(0)) { # # Get the user info (the user being operated on). # +my $dbid = $target_user->dbid(); my $pswd = $target_user->pswd(); my $user_number = $target_user->unix_uid(); my $fullname = $target_user->name(); @@ -223,19 +248,17 @@ my $wikionly = $target_user->wikionly(); # for the case that the account is being (re)created. We convert that to # the unix info. # +my $firstproject; my $default_groupname; my $default_groupgid; -$query_result = - DBQueryFatal("select m.pid from group_membership as m ". - "where m.uid='$user' and m.pid=m.gid and m.trust!='none' ". - "order by date_approved asc limit 1"); +if ($target_user->FirstApprovedProject(\$firstproject) < 0) { + fatal("Could not determine first approved project for $target_user"); +} -if (my ($defpid) = $query_result->fetchrow_array) { - if (! TBGroupUnixInfo($defpid, $defpid, - \$default_groupgid, \$default_groupname)) { - fatal("No info for default project $defpid!"); - } +if (defined($firstproject)) { + $default_groupname = $firstproject->unix_name(); + $default_groupgid = $firstproject->unix_gid(); } else { print "No group membership for $user; using the guest group!\n"; @@ -359,6 +382,16 @@ sub AddUser() fatal("Could not add user $user ($user_number) to $CONTROL."); } } + + # shell escape. + $pswd =~ s/\$/\\\$/g; + $pswd =~ s/\*/\\\*/g; + + print "Initializing user $user password on $CONTROL.\n"; + + if (system("$SSH -host $CONTROL $CHPASS -p '$pswd' $user")) { + fatal("Could not initialize password for user $user on $CONTROL!"); + } } $UID = $SAVEUID; @@ -415,8 +448,7 @@ sub AddUser() if ($CONTROL ne $BOSSNODE) { GenerateSFSKey(); } - - return UpdatePassword(); + return 0; } # @@ -493,43 +525,110 @@ sub DelUser() # sub UpdatePassword() { - # shell escape. - $pswd =~ s/\$/\\\$/g; - $pswd =~ s/\*/\\\*/g; - # - # Check status. Ignore if user is not active. + # New password (encrypted) comes in on the command line. # - if ($status ne USERSTATUS_ACTIVE) { - print("$user is not active! Not updating the password!\n"); + usage() + if (! @ARGV); + + my $new_pswd = shift(@ARGV); + + # Lets not do this if no changes. + if ($new_pswd eq $target_user->pswd()) { + print "Password has not changed ...\n"; return 0; } + # Lets prevent any odd characters. + if ($new_pswd =~ /[\'\\\"\&]+/) { + fatal("Invalid characters in new password encryption string!"); + } + + # + # Insert into database. When changing password for someone else, + # always set the expiration to right now so that the target user + # is "forced" to change it. + # + my $expires; + + if (! $target_user->SameUser($this_user)) { + $expires = "now()"; + } + else { + $expires = "date_add(now(), interval 1 year)"; + } + + if ($target_user->SetPassword($new_pswd, $expires)) { + fatal("Could not update password encryption string for $target_user"); + } + + # Send auditing email before next step in case of failure. + SENDMAIL("$fullname <$user_email>", + "Password for '$user' has been changed", + "\n". + "Emulab password for '$user' has been changed by " . + $this_user->uid() ."\n". + "\n". + "Name: " . $target_user->name() . "\n". + "IDX: " . $target_user->uid_idx() . "\n". + "\n". + "If this is unexpected, please contact Testbed Operations\n". + "($TBOPS) immediately!\n". + "\n", + "$TBOPS", + "Bcc: $TBAUDIT"); + + # Go no further if a webonly user. + return 0 + if ($webonly); + + # + # Go no further if user is not active or frozen. + # + return 0 + if (! ($status eq USERSTATUS_ACTIVE || $status eq USERSTATUS_FROZEN)); + + # + # Change on ops only if there is a real account there. + # if (! $wikionly) { + # + # Grab from the DB to avoid taint checking sillyness. + # + my $safe_pswd = $target_user->pswd(); + # shell escape. + $safe_pswd =~ s/\$/\\\$/g; + $safe_pswd =~ s/\*/\\\*/g; + $UID = 0; if ($CONTROL ne $BOSSNODE) { print "Updating user $user password on $CONTROL.\n"; - if (system("$SSH -host $CONTROL $CHPASS -p '$pswd' $user")) { + if (system("$SSH -host $CONTROL $CHPASS -p '$safe_pswd' $user")) { fatal("Could not change password for user $user on $CONTROL!"); } } $UID = $SAVEUID; } + + # + # Ick. If invoked as "nobody" then the user was either frozen or + # inactive. Lets skip the rest of this for now. Needs more thought + # and cleanup in the web interface to this, since we cannot call + # out to these scripts as "nobody" (yet). + # + return 0 + if (getpwuid($UID) eq "nobody"); $EUID = $UID; - # And to the wiki if enabled. + # And the wiki if enabled. system("$ADDWIKIUSER -u $user") - if ($WIKISUPPORT && $user ne $PROTOUSER); + if ($WIKISUPPORT && $user ne $PROTOUSER && !$webonly); # And to the bugdb if enabled. system("$ADDBUGDBUSER -m $user") - if ($BUGDBSUPPORT && $user ne $PROTOUSER); + if ($BUGDBSUPPORT && $user ne $PROTOUSER && ! ($wikionly || $webonly)); - # And to the OPS db if enabled. - system("$OPSDBCONTROL adduser $user") - if ($OPSDBSUPPORT && $user ne $PROTOUSER); - $EUID = 0; return 0; diff --git a/db/User.pm.in b/db/User.pm.in index 8cee2bbf4e1ba2c9e0016acab8496f089dc572e6..c87011c27b7deceb1eaec670e68df21f2f9e9186 100644 --- a/db/User.pm.in +++ b/db/User.pm.in @@ -1,7 +1,7 @@ #!/usr/bin/perl -wT # # EMULAB-COPYRIGHT -# Copyright (c) 2005, 2006 University of Utah and the Flux Group. +# Copyright (c) 2005, 2006, 2007 University of Utah and the Flux Group. # All rights reserved. # package User; @@ -21,6 +21,7 @@ use English; use Data::Dumper; use File::Basename; use overload ('""' => 'Stringify'); +use Project; # Configure variables my $TB = "@prefix@"; @@ -46,15 +47,29 @@ sub mysystem($) # sub Lookup($$) { - my ($class, $uid_idx) = @_; + my ($class, $token) = @_; + my $query_result; # Look in cache first - return $users{"$uid_idx"} - if (exists($users{"$uid_idx"})); + return $users{"$token"} + if (exists($users{"$token"})); + + # + # For backwards compatability, look to see if the token is numeric + # or alphanumeric. If numeric, assumes its an idx, otherwise a name. + # + if ($token =~ /^\d*$/) { + $query_result = + DBQueryWarn("select * from users where uid_idx='$token'"); + } + elsif ($token =~ /^\w*$/) { + $query_result = + DBQueryWarn("select * from users where uid='$token'"); + } + else { + return undef; + } - my $query_result = - DBQueryWarn("select * from users where uid_idx=$uid_idx"); - return undef if (!$query_result || !$query_result->numrows); @@ -64,13 +79,14 @@ sub Lookup($$) bless($self, $class); # Add to cache. - $users{"$uid_idx"} = $self; + $users{$self->{'USER'}->{'uid_idx'}} = $self; return $self; } # accessors sub field($$) { return ((! ref($_[0])) ? -1 : $_[0]->{'USER'}->{$_[1]}); } sub uid_idx($) { return field($_[0], "uid_idx"); } +sub dbid($) { return field($_[0], "uid_idx"); } sub uid($) { return field($_[0], "uid"); } sub created($) { return field($_[0], "usr_created"); } sub expires($) { return field($_[0], "usr_expires"); } @@ -124,15 +140,7 @@ sub LookupByUid($$) { my ($class, $uid) = @_; - my $query_result = - DBQueryWarn("select uid_idx from users where uid='$uid'"); - - return undef - if (! $query_result || !$query_result->numrows); - - my ($uid_idx) = $query_result->fetchrow_array(); - - return User->Lookup($uid_idx); + return User->Lookup($uid); } # @@ -223,5 +231,78 @@ sub Update($$) return Refresh($self); } +# +# Equality test for two users. Not strictly necessary in perl, but good form. +# +sub SameUser($$) +{ + my ($self, $other) = @_; + + # Must be a real reference. + return -1 + if (! (ref($self) && ref($other))); + + return $self->uid_idx() == $other->uid_idx(); +} + +# +# First approved project. +# +sub FirstApprovedProject($$) +{ + my ($self, $pptr) = @_; + + # Must be a real reference. + return -1 + if (! ref($self)); + + my $uid_idx = $self->uid_idx(); + + my $query_result = + DBQueryWarn("select pid_idx from group_membership ". + "where uid_idx='$uid_idx' and pid_idx=gid_idx and ". + " trust!='none' ". + "order by date_approved asc limit 1"); + + if (! $query_result || !$query_result->numrows) { + $pptr = undef; + return 0; + } + + my ($pid_idx) = $query_result->fetchrow_array(); + my $project = Project->Lookup($pid_idx); + + if (! defined($project)) { + warn("*** User::FirstApprovedProject: ". + "Could not load project $pid_idx!"); + return -1; + } + $$pptr = $project; + return 0; +} + +# +# Set password for user. +# +sub SetPassword($$$) +{ + my ($self, $encoding, $expires) = @_; + + # Must be a real reference. + return -1 + if (! ref($self)); + + my $uid_idx = $self->uid_idx(); + + # Clear the chpasswd stuff anytime passwd is set. + return -1 + if (! DBQueryWarn("update users set ". + " usr_pswd='$encoding', pswd_expires=$expires, ". + " chpasswd_key=NULL,chpasswd_expires=0 ". + "where uid_idx='$uid_idx'")); + + return Refresh($self); +} + # _Always_ make sure that this 1 is at the end of the file... 1; diff --git a/www/chpasswd.php3 b/www/chpasswd.php3 index 4cc42a964f8307a940e018cadbc386eea0570261..3caaab37040adef5ec2f20b8dc6d32ae359a1dde 100644 --- a/www/chpasswd.php3 +++ b/www/chpasswd.php3 @@ -1,7 +1,7 @@ <?php # # EMULAB-COPYRIGHT -# Copyright (c) 2000-2003, 2005, 2006 University of Utah and the Flux Group. +# Copyright (c) 2000-2003, 2005, 2006, 2007 University of Utah and the Flux Group. # All rights reserved. # include("defs.php3"); @@ -191,33 +191,25 @@ setcookie($TBAUTHCOOKIE, "", time() - 1000000, "/", $TBAUTHDOMAIN, 0); PAGEHEADER("Reset Your Password", $view); $encoding = crypt("$password1"); -$expires = "date_add(now(), interval 1 year)"; +$safe_encoding = escapeshellarg($encoding); -$target_user->SetPassword($encoding, $expires); +STARTBUSY("Resetting your password"); -if (HASREALACCOUNT($target_uid)) { - STARTBUSY("Resetting your password"); - - SUEXEC($target_uid, "nobody", "webtbacct passwd $target_uid", +# +# Invoke backend to deal with this. +# +if (!HASREALACCOUNT($target_uid)) { + SUEXEC("nobody", "nobody", + "webtbacct passwd $target_uid $safe_encoding", SUEXEC_ACTION_DIE); - - CLEARBUSY(); -} - -TBMAIL("$usr_name <$usr_email>", - "Password Reset for '$target_uid'", - "\n". - "The password for '$target_uid' has been reset via the web interface.\n". - "If this message is unexpected, please contact Testbed Operations\n". - "($TBMAILADDR_OPS) immediately!\n". - "\n". - "The change originated from IP: " . $_SERVER['REMOTE_ADDR'] . "\n". - "\n". - "Thanks,\n". - "Testbed Operations\n", - "From: $TBMAIL_OPS\n". - "Bcc: $TBMAIL_AUDIT\n". - "Errors-To: $TBMAIL_WWW"); +} +else { + SUEXEC($target_uid, "nobody", + "webtbacct passwd $target_uid $safe_encoding", + SUEXEC_ACTION_DIE); +} + +CLEARBUSY(); echo "<br> Your password has been changed.\n"; diff --git a/www/moduserinfo.php3 b/www/moduserinfo.php3 index 197437bfa8007b27e0bfe083083b2151df177d1c..0ec0fc798f48034da76e9f9e2e208fd130e92785 100644 --- a/www/moduserinfo.php3 +++ b/www/moduserinfo.php3 @@ -245,6 +245,7 @@ function SPITFORM($formfields, $errors) <td class=left> <input type=password name=\"formfields[password1]\" + value=\"" . $formfields[password1] . "\" size=8></td> </tr>\n"; @@ -253,6 +254,7 @@ function SPITFORM($formfields, $errors) <td class=left> <input type=password name=\"formfields[password2]\" + value=\"" . $formfields[password2] . "\" size=8></td> </tr>\n"; @@ -656,28 +658,20 @@ if ((isset($formfields["password1"]) && $formfields["password1"] != "") && # Do it again. This ensures we use the current algorithm, not whatever # it was encoded with last time. # - $new_encoding = crypt($formfields["password1"]); + $new_encoding = crypt($formfields["password1"]); + $safe_encoding = escapeshellarg($new_encoding); # - # Insert into database. When changing password for someone else, - # always set the expiration to right now so that the target user - # is "forced" to change it. + # Invoke backend to deal with this. # - if (! $target_user->SameUser($this_user)) - $expires = "now()"; - else - $expires = "date_add(now(), interval 1 year)"; - - $target_user->SetPassword($new_encoding, $expires); - - if ($wikionly) { - if ($CHECKLOGIN_STATUS & CHECKLOGIN_ACTIVE) { - SUEXEC("nobody", "nobody", "webtbacct passwd $target_uid", - SUEXEC_ACTION_DIE); - } + if (!HASREALACCOUNT($uid)) { + SUEXEC("nobody", "nobody", + "webtbacct passwd $target_uid $safe_encoding", + SUEXEC_ACTION_DIE); } - elseif (HASREALACCOUNT($uid) && HASREALACCOUNT($target_uid)) { - SUEXEC($uid, "nobody", "webtbacct passwd $target_uid", + else { + SUEXEC($uid, "nobody", + "webtbacct passwd $target_uid $safe_encoding", SUEXEC_ACTION_DIE); } }