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);
     }
 }