Commit 16aaa101 authored by Leigh Stoller's avatar Leigh Stoller

Move the bulk (or guts) of newuser and newproject from the web

interface to the backend. There are new scripts that can be called
from the command line:

	newuser xmlfile
	newproj xmlfile

They both run from small xmlfiles that are generated by the web
interface from the form data. I also moved user verification to the
backend so that we do not have duplicated email functions, but that
was a small change.

Upon error, the xmlfile is saved and sent to tbops so that we can
rerun the command by hand, rather then force user to fill out form
again. I also do a better job of putting the form back up intact when
there are internal errors.

If the user provides an initial public key, that is put into the xml
file as well and addpubkey is called from newuser instead of the web
interface. A more general change to addpukey is that it is now
*always* called as "nobody". This script was a morass of confusion
cause of having to call it as nobody before the user actually
exists. In fact, another of my ongoing projects is to reduce the
number of scripts called as a particular user, but thats a story for
another day. Anyway, the script is always called as nobody, but we
pass along the implied user in the environment so that it can do
permission checks.
parent a1cfdc36
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
# Copyright (c) 2000-2007 University of Utah and the Flux Group.
# All rights reserved.
#
# Most of the configure-substitution magic is done here.
......@@ -37,6 +37,7 @@ TBAUDITEMAIL = @TBAUDITEMAIL@
TBACTIVEARCHIVE = @TBACTIVEARCHIVE@
TBUSERSARCHIVE = @TBUSERSARCHIVE@
TBERRORSEMAIL = @TBERRORSEMAIL@
TBAPPROVALEMAIL = @TBAPPROVALEMAIL@
BOSSNODE = @BOSSNODE@
USERNODE = @USERNODE@
FSNODE = @FSNODE@
......
......@@ -12,8 +12,10 @@ UNIFIED = @UNIFIED_BOSS_AND_OPS@
include $(OBJDIR)/Makeconf
SBIN_STUFF = tbacct addsfskey addpubkey mkusercert quotamail genpubkeys
LIBEXEC_STUFF = webtbacct webaddsfskey webaddpubkey webmkusercert
SBIN_STUFF = tbacct addsfskey addpubkey mkusercert quotamail genpubkeys \
newuser newproj
LIBEXEC_STUFF = webtbacct webaddsfskey webaddpubkey webmkusercert \
webnewuser webnewproj
# These scripts installed setuid, with sudo.
SETUID_BIN_SCRIPTS =
......
......@@ -171,22 +171,25 @@ if (defined($user)) {
$user_email = $target_user->email();
$user_dbid = $target_user->dbid();
$user_uid = $target_user->uid();
$USERUID = $user_uid;
}
#
# If invoked as "nobody" its for a user with no actual account.
# If invoked as "nobody" we came from the web interface. We have to have
# a credential in the environment, unless its just a key verification
# operation, which anyone can do.
#
if (getpwuid($UID) eq "nobody") {
if ($initmode || $genmode) {
fatal("Bad usage as 'nobody'");
$this_user = User->ImpliedUser();
if (($initmode || $genmode || !$verify) && !defined($this_user)) {
fatal("Bad usage from web interface");
}
$nobody = 1;
}
else {
$USERUID = getpwnam($user);
# Map invoking user to object.
$this_user = User->LookupByUnixId($UID);
# From the command line; map invoking user to object.
$this_user = User->ThisUser();
if (! defined($this_user)) {
fatal("You ($UID) do not exist!");
......@@ -194,16 +197,17 @@ else {
}
#
# Initmode or genmode, do it and exit.
# Initmode or genmode, do it and exit. Eventually get rid of the switch
# to the target user.
#
if ($initmode) {
# Drop root privs, switch to target user.
$EUID = $USERUID;
$EUID = $UID = $USERUID;
exit InitUser();
}
if ($genmode) {
# Drop root privs, switch to target user.
$EUID = $USERUID;
$EUID = $UID = $USERUID;
exit GenerateKeyFile();
}
......@@ -235,10 +239,12 @@ else {
#
if (!$verify) {
# If its the user himself, then we can generate a new authkeys file.
if (!$nobody && !TBAdmin() && !$target_user->SameUser($this_user)) {
if (!$target_user->SameUser($this_user) && !TBAdmin()) {
fatal("You are not allowed to set pubkeys for $target_user\n");
}
if (-d "$HOMEDIR/$user_uid/.ssh") {
# Drop root privs, switch to target user.
$EUID = $UID = $USERUID;
$genmode = 1;
}
......@@ -250,11 +256,6 @@ if (!$verify) {
AuditStart(0);
}
# Drop root privs, switching to user.
if (!$nobody) {
$EUID = $USERUID;
}
#
# Grab the first line of the file. Parse it to see if its in the
# format we like (openssh), either protocol 1 or 2.
......
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2007 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
use strict;
use Getopt::Std;
use XML::Simple;
use Data::Dumper;
#
# Create a new user from a XML description.
#
sub usage()
{
print("Usage: newproj <xmlfile>\n");
print(" newproj -m <pid_idx>\n");
exit(-1);
}
my $optlist = "dm:";
my $debug = 1;
my $resend;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $TBAPPROVAL = "@TBAPPROVALEMAIL@";
my $TBAUDIT = "@TBAUDITEMAIL@";
my $TBBASE = "@TBBASE@";
my $TBWWW = "@TBWWW@";
#
# This script is setuid, so please do not run it as root. Hard to track
# what has happened.
#
if ($EUID == 0) {
die("*** $0:\n".
" Please do not run this as root!\n");
}
#
# Untaint the path
#
$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
#
# Turn off line buffering on output
#
$| = 1;
#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use libdb;
use libtestbed;
use Project;
use User;
# Protos
sub fatal($);
sub UserError($);
# Locals
my $SAVEUID = $UID;
my $xmlfile;
#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"m"})) {
$resend = $options{"m"};
if ($resend =~ /^(\d*)$/) {
$resend = $1;
}
else {
fatal("Bad characters in -m option: $resend");
}
}
else {
usage()
if (@ARGV != 1);
$xmlfile = shift(@ARGV);
}
#
# Map invoking user to object.
# If invoked as "nobody" we are coming from the web interface and there
# is no current user context.
#
my $this_user;
if (getpwuid($UID) ne "nobody") {
$this_user = User->ThisUser();
if (! defined($this_user)) {
fatal("You ($UID) do not exist!");
}
fatal("You must have admin privledges to create new projects!")
if (!TBAdmin());
}
#
# Resend email message and exit.
#
if (defined($resend)) {
my $project = Project->Lookup($resend);
fatal("Could not map project $resend to object!")
if (!defined($project));
exit($project->SendNewProjectEmail());
}
#
# Check the filename when invoked from the web interface; must be a
# file in /tmp.
#
if (! defined($this_user)) {
if ($xmlfile =~ /^([-\w\.\/]+)$/) {
$xmlfile = $1;
}
else {
fatal("Bad data in pathname: $xmlfile");
}
# Use realpath to resolve any symlinks.
my $translated = `realpath $xmlfile`;
if ($translated =~ /^(\/tmp\/[-\w\.\/]+)$/) {
$xmlfile = $1;
}
else {
fatal("Bad data in translated pathname: $xmlfile");
}
}
#
# These are the fields that we allow to come in from the XMLfile.
#
my %required = ("name" => "pid",
"leader" => "head_uid",
"short description" => "name",
"URL" => "URL",
"funders" => "funders",
"long description" => "why",
"public" => "public",
"num_pcs" => "num_pcs",
"linkedtous" => "linked_to_us");
my %optional = ("members" => "num_members",
"ron" => "num_ron",
"plab" => "num_pcplab",
"whynotpublic" => "public_whynot",
"user_interface" => "default_user_interface");
#
# This script is not audited cause we want the output to be sent back
# to the web interface. Thats okay since we send email from the script
# anyway.
#
#
# Must wrap the parser in eval since it exits on error.
#
my $xmlparse = eval { XMLin($xmlfile,
VarAttr => 'name',
ContentKey => '-content',
SuppressEmpty => undef); };
fatal($@)
if ($@);
#
# Make sure all the required arguments were provided.
#
foreach my $key (keys(%required)) {
fatal("Missing required attribute '$key'")
if (! exists($xmlparse->{'attribute'}->{"$key"}));
}
#
# We build up an array of arguments to pass to User->Create() as we check
# the attributes.
#
my %newproj_args = ();
foreach my $key (keys(%{ $xmlparse->{'attribute'} })) {
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
if ($debug) {
print STDERR "Project attribute: '$key' -> '$value'\n";
}
my $dbslot;
# Must be in the allowed lists above, with exceptions handled below
if (exists($required{$key})) {
$dbslot = $required{$key};
next
if (!defined($dbslot));
fatal("Null value for required field $key")
if (!defined($value));
}
elsif (exists($optional{$key})) {
$dbslot = $optional{$key};
next
if (!defined($dbslot) || !defined($value));
}
else {
fatal("Invalid attribute in XML: '$key' -> '$value'\n");
}
# Now check that the value is legal.
if (! TBcheck_dbslot($value, "projects", $dbslot,
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
UserError("Illegal data: $key - $value");
}
#
# Do a taint check to avoid warnings, since the values are abviously okay.
#
if ($value =~ /^(.*)$/) {
$value = $1;
}
$newproj_args{$dbslot} = $value;
}
#
# Now do special checks.
#
UserError("Project already exists; pick another name!")
if (Project->Lookup($newproj_args{'pid'}));
my $leader = User->Lookup($newproj_args{'head_uid'});
UserError("Project leader does not exist!")
if (!defined($leader));
#
# Now safe to create the project. Move the pid out of the argument array
# since its actually an argument to the Create() routine. Ditto for the
# project leader.
#
my $new_pid = $newproj_args{'pid'};
delete($newproj_args{'pid'});
delete($newproj_args{'head_uid'});
my $newproj = Project->Create($new_pid, $leader, \%newproj_args);
if (!defined($newproj)) {
fatal("Could not create new project!");
}
my $new_idx = $newproj->pid_idx();
#
# See if we are in an initial Emulab setup. If so, no email sent.
#
my $firstinitstate;
TBGetSiteVar("general/firstinit/state", \$firstinitstate);
#
# Send the email notification.
#
$newproj->SendNewProjectEmail($firstinitstate eq "createproject");
# The web interface requires this line to be printed!
print "User $new_pid/$new_idx has been created\n";
exit(0);
sub fatal($) {
my($mesg) = $_[0];
print STDERR "*** $0:\n".
" $mesg\n";
exit(-1);
}
sub UserError($) {
my($mesg) = $_[0];
print $mesg;
exit(1);
}
sub escapeshellarg($)
{
my ($str) = @_;
$str =~ s/(')/'\\''/g;
return $str;
}
This diff is collapsed.
......@@ -24,7 +24,8 @@ use Getopt::Std;
sub usage()
{
print("Usage: tbacct [-f] [-b] ".
"<add|del|mod|passwd|wpasswd|email|freeze|thaw> <user> [args]\n");
"<add|del|mod|passwd|wpasswd|email|freeze|thaw|verify> ".
"<user> [args]\n");
exit(-1);
}
my $optlist = "fb";
......@@ -135,6 +136,7 @@ sub UpdateWindowsPassword();
sub UpdateUser(;$);
sub FreezeUser();
sub ThawUser();
sub VerifyUser();
sub UpdateEmail();
sub CheckDotFiles();
sub GenerateSFSKey();
......@@ -181,7 +183,7 @@ if ($user =~ /^([-\w]+)$/i) {
else {
die("Tainted argument: $user\n");
}
if ($cmd =~ /^(add|del|mod|freeze|passwd|wpasswd|thaw|email)$/) {
if ($cmd =~ /^(add|del|mod|freeze|passwd|wpasswd|thaw|email|verify)$/) {
$cmd = $1;
}
else {
......@@ -211,7 +213,7 @@ if (getpwuid($UID) eq "nobody") {
$this_user = $target_user;
}
else {
$this_user = User->LookupByUnixId($UID);
$this_user = User->ThisUser();
if (! defined($this_user)) {
fatal("You ($UID) do not exist!");
......@@ -302,6 +304,10 @@ SWITCH: for ($cmd) {
ThawUser();
last SWITCH;
};
/^verify$/ && do {
VerifyUser();
last SWITCH;
};
}
# Always do this!
......@@ -867,6 +873,34 @@ sub ThawUser()
return UpdateUser(0);
}
#
# Verify a user. Converts status and sends email
#
sub VerifyUser()
{
#
# Only admin people can do this unless its the user himself.
#
if (! $target_user->SameUser($this_user) && ! TBAdmin()) {
fatal("You do not have permission to verify user $user.");
}
if ($target_user->status() ne USERSTATUS_NEWUSER) {
fatal("$user is not a newuser! Cannot verify the account!");
}
my $newstatus = ($target_user->wikionly() ?
USERSTATUS_ACTIVE() : USERSTATUS_UNAPPROVED());
$target_user->SetStatus($newstatus) == 0 or
fatal("Could not set user status to '$newstatus' for $target_user");
$target_user->SendVerifiedEmail() == 0 or
fatal("Could not send verified email for $target_user");
return 0;
}
#
# Check dot files. We do this over and over ...
#
......
......@@ -618,9 +618,9 @@ sub SetPassword($$$)
}
#
# User approved; find users groups and send email.
# User verified; find users groups and send email.
#
sub SendApprovalEmail($)
sub SendVerifiedEmail($)
{
my ($self) = @_;
......
......@@ -573,7 +573,15 @@ REPLACE INTO table_regex VALUES ('nseconfigs','vname','text','redirect','virt_no
REPLACE INTO table_regex VALUES ('nseconfigs','nseconfig','text','regex','^[\\040-\\176\\012\\011\\015]*$',0,16777215,NULL);
REPLACE INTO table_regex VALUES ('os_info','osname','text','regex','^[-\\w\\.+]+$',2,20,NULL);
REPLACE INTO table_regex VALUES ('projects','newpid','text','regex','^[a-zA-Z][-a-zA-Z0-9]+$',2,12,NULL);
REPLACE INTO table_regex VALUES ('projects','head_uid','text','redirect','users:uid',0,0,NULL);
REPLACE INTO table_regex VALUES ('projects','name','text','redirect','default:tinytext',0,256,NULL);
REPLACE INTO table_regex VALUES ('projects','funders','text','redirect','default:tinytext',0,256,NULL);
REPLACE INTO table_regex VALUES ('projects','public','int','redirect','default:tinyint',0,1,NULL);
REPLACE INTO table_regex VALUES ('projects','linked_to_us','int','redirect','default:tinyint',0,1,NULL);
REPLACE INTO table_regex VALUES ('projects','public_whynot','text','redirect','default:tinytext',0,256,NULL);
REPLACE INTO table_regex VALUES ('projects','default_user_interface','text','regex','^(emulab|plab)$',2,12,NULL);
REPLACE INTO table_regex VALUES ('projects','pid','text','regex','^[-\\w]+$',2,12,NULL);
REPLACE INTO table_regex VALUES ('projects','URL','text','redirect','default:tinytext',0,0,NULL);
REPLACE INTO table_regex VALUES ('reserved','vname','text','redirect','virt_nodes:vname',1,32,NULL);
REPLACE INTO table_regex VALUES ('users','uid','text','regex','^[a-zA-Z][\\w]+$',2,8,NULL);
REPLACE INTO table_regex VALUES ('users','usr_phone','text','regex','^[-\\d\\(\\)\\+\\.x ]+$',7,64,NULL);
......
......@@ -340,20 +340,16 @@ function SUEXEC($uid, $gid, $cmdandargs, $action) {
return $suexec_retval;
}
function ADDPUBKEY($uid, $cmdandargs) {
#
# We invoke addpubkey as user nobody all the time. The implied user is passed
# along in an HTTP_ variable (see tbauth). This avoids a bunch of confusion
# that results from new users who do not have a context yet.
#
function ADDPUBKEY($cmdandargs) {
global $TBSUEXEC_PATH;
#
# Complication. User might not have an actual account if setting or
# changing his own pubkeys. webonly, unapproved, and unverified users
# can still muck with their personal info. So, just invoke as user
# nobody. We will get audit email in case we need to track what has
# happened.
#
if (!$uid || !HASREALACCOUNT($uid)) {
$uid = "nobody";
}
return SUEXEC($uid, "nobody", $cmdandargs, 0);
return SUEXEC("nobody", "nobody", "webaddpubkey $cmdandargs",
SUEXEC_ACTION_CONTINUE);
}
#
......
<?php
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2003, 2006 University of Utah and the Flux Group.
# Copyright (c) 2000-2003, 2006, 2007 University of Utah and the Flux Group.
# All rights reserved.
#
include("defs.php3");
......@@ -139,7 +139,7 @@ DBQueryFatal("delete from user_pubkeys ".
# will complain and die!
#
if (HASREALACCOUNT($target_uid)) {
ADDPUBKEY($uid, "webaddpubkey -w $target_uid");
ADDPUBKEY("-w $target_uid");
}
header("Location: " . CreateURL("showpubkeys", $target_user));
......
<?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");
......@@ -676,73 +676,6 @@ if (count($errors)) {
return;
}
# Okay, do pubkey checks.
if (!$returning && !$forwikionly) {
#
# Pub key provided in form (paste in).
#
if (isset($formfields[usr_key]) &&
strcmp($formfields[usr_key], "")) {
#
# This is passed off to the shell, so taint check it.
#
if (! preg_match("/^[-\w\s\.\@\+\/\=]*$/", $formfields[usr_key])) {
$errors["PubKey"] = "Invalid characters";
}
else {
#
# Replace any embedded newlines first.
#
$formfields[usr_key] =
ereg_replace("[\n]", "", $formfields[usr_key]);
$usr_key = $formfields[usr_key];
#
# Verify key format.
#
if (ADDPUBKEY(null, "webaddpubkey -n -k '$usr_key' ")) {
$errors["Pubkey Format"] =
"Could not be parsed. Is it a public key?";
}
else {
$addpubkeyargs = "-k '$usr_key' ";
}
}
}
#
# If usr provided a file for the key, it overrides the paste in text.
#
if (isset($_FILES['usr_keyfile']) &&
$_FILES['usr_keyfile']['name'] != "" &&
$_FILES['usr_keyfile']['name'] != "none") {
$localfile = $_FILES['usr_keyfile']['tmp_name'];
if (! stat($localfile)) {
$errors["PubKey File"] = "No such file";
}
# Taint check shell arguments always!
elseif (! preg_match("/^[-\w\.\/]*$/", $localfile)) {
$errors["PubKey File"] = "Invalid characters";
}
else {
chmod($localfile, 0644);
#
# Verify key format.
#
if (ADDPUBKEY(null, "webaddpubkey -n $localfile ")) {
$errors["Pubkey Format"] =
"Could not be parsed. Is it a public key?";
}
else {
$addpubkeyargs = "$localfile";
}
}
}
}
#
# Need the user, project and group objects for the rest of this.
#
......@@ -769,84 +702,57 @@ if (count($errors)) {
}
#
# Create a new user.
# Create a new user. We do this by creating a little XML file to pass to
# the newuser script.
#
if (! $returning) {
#
# Certain of these values must be escaped or otherwise sanitized.
#
$joining_uid = ($USERSELECTUIDS ? $formfields[joining_uid] : null);
$usr_name = addslashes($formfields[usr_name]);
$usr_email = $formfields[usr_email];
$password1 = $formfields[password1];
$password2 = $formfields[password2];
$wikiname = ($WIKISUPPORT ? $formfields[wikiname] : "");
if (!$forwikionly) {
$usr_affil = addslashes($formfields[usr_affil]);
$usr_title = addslashes($formfields[usr_title]);
$usr_addr = addslashes($formfields[usr_addr]);
$usr_city = addslashes($formfields[usr_city]);
$usr_state = addslashes($formfields[usr_state]);
$usr_zip = addslashes($formfields[usr_zip]);
$usr_country = addslashes($formfields[usr_country]);
$usr_phone = $formfields[usr_phone];
$args = array();
$args["name"] = $formfields[usr_name];
$args["email"] = $formfields[usr_email];
$args["address"] = $formfields[usr_addr];
$args["address2"] = $formfields[usr_addr2];
$args["city"] = $formfields[usr_city];
$args["state"] = $formfields[usr_state];
$args["zip"] = $formfields[usr_zip];
$args["country"] = $formfields[usr_country];
$args["phone"] = $formfields[usr_phone];
$args["shell"] = 'tcsh';
$args["title"] = $formfields[usr_title];
$args["affiliation"] = $formfields[usr_affil];
$args["password"] = $formfields[password1];
$args["wikiname"] = ($WIKISUPPORT ? $formfields[wikiname] : "");
if (isset($formfields[usr_URL]) &&
$formfields[usr_URL] != $HTTPTAG && $formfields[usr_URL] != "") {
$args["URL"] = $formfields[usr_URL];
}
else {
$usr_affil = "";
$usr_title = "";
$usr_addr = "";
$usr_city = "";
$usr_state = "";
$usr_zip = "";
$usr_country = "";
$usr_phone = "";
if ($USERSELECTUIDS) {
$args["login"] = $formfields[joining_uid];
}
if (! isset($formfields[usr_URL]) ||
strcmp($formfields[usr_URL], "") == 0 ||
strcmp($formfields[usr_URL], $HTTPTAG) == 0) {
$usr_URL = "";
}
else {
$usr_URL = addslashes($formfields[usr_URL]);
}
# Backend verifies pubkey and returns error.
if (!$forwikionly) {
if (isset($_FILES['usr_keyfile']) &&
$_FILES['usr_keyfile']['name'] != "" &&
$_FILES['usr_keyfile']['name'] != "none") {
if (! isset($formfields[usr_addr2])) {
$usr_addr2 = "";
}
else {
$usr_addr2 = addslashes($formfields[usr_addr2