Commit 3164ca14 authored by Russ Fish's avatar Russ Fish

Move newgroup page form logic to a backend Perl script and methods.

     www/newgroup.php3 - The reworked PHP page.
     www/newgroup_form.php3 - Removed, form merged into newgroup.php3 .
     www/group_defs.php - Add a Group::Create method bridging to the script via XML.
     www/showproject.php3 - Link to newgroup.php3, rather than newgroup_form.php3 .
     configure configure.in  - Add the newgroup backend script.
     backend/{newgroup,GNUmakefile}.in - Add the Perl script.
     db/Group.pm.in - Update the AccessCheck method to allow TB_PROJECT_LEADGROUP.
     sql/database-fill.sql - Add table_regex 'groups' checking patterns.
parent 75705f75
......@@ -12,8 +12,8 @@ UNIFIED = @UNIFIED_BOSS_AND_OPS@
include $(OBJDIR)/Makeconf
BIN_SCRIPTS = moduserinfo
WEB_BIN_SCRIPTS = webmoduserinfo
BIN_SCRIPTS = moduserinfo newgroup
WEB_BIN_SCRIPTS = webmoduserinfo webnewgroup
WEB_SBIN_SCRIPTS=
LIBEXEC_SCRIPTS = $(WEB_BIN_SCRIPTS) $(WEB_SBIN_SCRIPTS)
......
#!/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;
#
# Back-end script to create a Project Group.
#
sub usage()
{
print("Usage: newgroup [-v] <xmlfile>\n");
exit(-1);
}
my $optlist = "dv";
my $debug = 0;
my $verify = 0; # Check data and return status only.
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $TBAUDIT = "@TBAUDITEMAIL@";
my $MKGROUP = "$TB/sbin/mkgroup";
my $MODGROUPS = "$TB/sbin/modgroups";
#
# 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 User;
use Project;
use Group;
# Protos
sub fatal($);
sub UserError(;$);
sub escapeshellarg($);
#
# 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{"v"})) {
$verify = 1;
}
if (@ARGV != 1) {
usage();
}
my $xmlfile = shift(@ARGV);
#
# Map invoking user to object.
# If invoked as "nobody" we are coming from the web interface and the
# current user context is "implied" (see tbauth.php3).
#
my $this_user;
if (getpwuid($UID) ne "nobody") {
$this_user = User->ThisUser();
if (! defined($this_user)) {
fatal("You ($UID) do not exist!");
}
# You don't need admin privileges to create a Project Group.
}
else {
#
# Check the filename when invoked from the web interface; must be a
# file in /tmp.
#
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");
}
# The web interface (and in the future the xmlrpc interface) sets this.
$this_user = User->ImpliedUser();
if (! defined($this_user)) {
fatal("Cannot determine implied user!");
}
}
#
# These are the fields that we allow to come in from the XMLfile.
#
my $SLOT_OPTIONAL = 0x1; # The field is not required.
my $SLOT_REQUIRED = 0x2; # The field is required and must be non-null.
my $SLOT_ADMINONLY = 0x4; # Only admins can set this field.
#
# XXX We should encode all of this in the DB so that we can generate the
# forms on the fly, as well as this checking code.
#
my %xmlfields =
# XML Field Name DB slot name Flags Default
("project" => ["project", $SLOT_REQUIRED],
"group_id" => ["group_id", $SLOT_REQUIRED],
"group_leader" => ["group_leader", $SLOT_REQUIRED],
"group_description"=> ["group_description",$SLOT_OPTIONAL, ""]);
#
# Must wrap the parser in eval since it exits on error.
#
my $xmlparse = eval { XMLin($xmlfile,
VarAttr => 'name',
ContentKey => '-content',
SuppressEmpty => undef); };
fatal($@)
if ($@);
#
# Process and dump the errors (formatted for the web interface).
# We should probably XML format the errors instead but not sure I want
# to go there yet.
#
my %errors = ();
#
# Make sure all the required arguments were provided.
#
foreach my $key (keys(%xmlfields)) {
my (undef, $required, undef) = @{$xmlfields{$key}};
$errors{$key} = "Required value not provided"
if ($required & $SLOT_REQUIRED &&
! exists($xmlparse->{'attribute'}->{"$key"}));
}
UserError()
if (keys(%errors));
#
# We build up an array of arguments to pass to Group->Create() as we check
# the attributes.
#
my %newgroup_args = ();
foreach my $key (keys(%{ $xmlparse->{'attribute'} })) {
my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
if ($debug) {
print STDERR "User attribute: '$key' -> '$value'\n";
}
$errors{$key} = "Unknown attribute"
if (!exists($xmlfields{$key}));
my ($dbslot, $required, $default) = @{$xmlfields{$key}};
if ($required & $SLOT_REQUIRED) {
# A slot that must be provided, so do not allow a null value.
if (!defined($value)) {
$errors{$key} = "Must provide a non-null value";
next;
}
}
if ($required & $SLOT_OPTIONAL) {
# Optional slot. If value is null skip it. Might not be the correct
# thing to do all the time?
if (!defined($value)) {
next
if (!defined($default));
$value = $default;
}
}
if ($required & $SLOT_ADMINONLY) {
# Admin implies optional, but thats probably not correct approach.
$errors{$key} = "Administrators only"
if (! $this_user->IsAdmin());
}
# Now check that the value is legal.
if (! TBcheck_dbslot($value, "groups", $dbslot, TBDB_CHECKDBSLOT_ERROR)) {
$errors{$key} = TBFieldErrorString();
next;
}
$newgroup_args{$dbslot} = $value;
}
UserError()
if (keys(%errors));
#
# Now do special checks.
#
my $project = Project->Lookup($newgroup_args{"project"});
if (!defined($project)) {
UserError("Project: No such project");
}
if (!$project->AccessCheck($this_user, TB_PROJECT_MAKEOSID())) {
UserError("Project: Not enough permission");
}
# Need these below
my $group_id = $newgroup_args{"group_id"};
my $group_leader = $newgroup_args{"group_leader"};
my $descr = $newgroup_args{"group_description"};
my $group_pid = $project->pid();
#
# Certain of these values must be escaped or otherwise sanitized.
#
$descr = escapeshellarg($descr);
#
# Verify permission.
#
if (!$project->AccessCheck($this_user, TB_PROJECT_MAKEGROUP())) {
UserError("Access: Not group_root in project $group_pid");
}
# Need the user object for creating the group.
my $leader = User->Lookup($group_leader);
if (! $leader) {
UserError("GroupLeader: User '$group_leader' is an unknown user");
}
#
# Verify leader. Any user can lead a group, but they must be a member of
# the project, and we have to avoid an ISADMIN() check in AccessCheck().
#
my $proj_leader = $project->GetLeader();
if (!$leader->SameUser($proj_leader) ||
$leader->status() eq USERSTATUS_UNAPPROVED() ||
!$project->AccessCheck($leader, TB_PROJECT_LEADGROUP())) {
UserError("GroupLeader: $group_leader does not have enough permission ".
"to lead a group in project $group_pid!");
}
#
# Make sure the GID is not already there.
#
my $oldgroup = Group->LookupByPidGid($group_pid, $group_id);
if ($oldgroup) {
UserError("GroupId: The group $group_id already exists! ".
"Please select another.");
}
#
# The unix group name must be globally unique. Form a name and check it.
# Subgroup names have a project-name prefix, and a numeric suffix if needed.
#
my $unix_gname = substr($group_pid, 0, 3) . "-" . $group_id;
my $maxtries = 99;
my $count = 0;
my $TBDB_UNIXGLEN = 16; # XXX Where should this be?
while ($count < $maxtries) {
if (length($unix_gname) > $TBDB_UNIXGLEN) {
UserError("GroupId: Unix group name $unix_gname is too long!");
}
my $query_result =
DBQueryFatal("select gid from groups where unix_name='$unix_gname'");
if (!$query_result->numrows) {
last;
}
$count++;
$unix_gname = substr($group_pid, 0, 3) . "-" .
substr($group_id, 0, length($group_id) - 2) . "$count";
}
if ($count == $maxtries) {
UserError("GroupId: Could not form a unique Unix group name!");
}
exit(0)
if ($verify);
#
# Now safe to create a Project Group.
#
# Put it in the DB. (This is used by Project->Create too.)
my $new_group = Group->Create($project, $group_id,
$leader, $descr, $unix_gname);
fatal("Could not create new Group!")
if (!defined($new_group));
my $group_idx = $new_group->gid_idx();
#
# Run the script to make the group directory, set the perms, etc.
#
my $cmd = "mkgroup $group_idx";
##print $cmd;
my $cmd_out = `$cmd`;
UserError("Error: " . $cmd_out)
if ($?);
#
# Now add the group leader to the group.
#
my $safe_id = escapeshellarg($group_id);
my $cmd = "webmodgroups -a $group_pid:$safe_id:group_root $group_leader",
##print $cmd;
$cmd_out = `$cmd`;
UserError("Error: " . $cmd_out)
if ($?);
# The web interface requires this line to be printed.
print "GROUP $group_id/$group_idx has been created\n";
exit(0);
sub fatal($)
{
my ($mesg) = @_;
print STDERR "*** $0:\n".
" $mesg\n";
# Exit with negative status so web interface treats it as system error.
exit(-1);
}
sub UserError(;$)
{
my ($mesg) = @_;
if (keys(%errors)) {
foreach my $key (keys(%errors)) {
my $val = $errors{$key};
print "${key}: $val\n";
}
}
print "$mesg\n"
if (defined($mesg));
# Exit with positive status so web interface treats it as user error.
exit(1);
}
sub escapeshellarg($)
{
my ($str) = @_;
$str =~ s/[^[:alnum:]]/\\$&/g;
return $str;
}
......@@ -2427,7 +2427,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
account/GNUmakefile account/tbacct \
account/addpubkey account/addsfskey account/genpubkeys \
account/quotamail account/mkusercert account/newproj account/newuser \
backend/GNUmakefile backend/moduserinfo \
backend/GNUmakefile backend/moduserinfo backend/newgroup \
tbsetup/GNUmakefile tbsetup/console_setup tbsetup/spewlogfile \
tbsetup/spewrpmtar tbsetup/gentopofile tbsetup/power_sgmote.pm \
tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \
......
......@@ -807,7 +807,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
account/GNUmakefile account/tbacct \
account/addpubkey account/addsfskey account/genpubkeys \
account/quotamail account/mkusercert account/newproj account/newuser \
backend/GNUmakefile backend/moduserinfo \
backend/GNUmakefile backend/moduserinfo backend/newgroup \
tbsetup/GNUmakefile tbsetup/console_setup tbsetup/spewlogfile \
tbsetup/spewrpmtar tbsetup/gentopofile tbsetup/power_sgmote.pm \
tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \
......
......@@ -436,6 +436,10 @@ sub AccessCheck($$$)
return 0
if (! ref($self));
my $pid = $self->pid();
my $gid = $self->gid();
my $uid = $user->uid();
if ($access_type < TB_PROJECT_MIN ||
$access_type > TB_PROJECT_MAX) {
print "*** Invalid access type: $access_type!\n";
......@@ -448,17 +452,83 @@ sub AccessCheck($$$)
if ($access_type == TB_PROJECT_READINFO) {
$mintrust = PROJMEMBERTRUST_USER;
}
elsif ($access_type == TB_PROJECT_CREATEEXPT ||
$access_type == TB_PROJECT_MAKEOSID) {
elsif ($access_type == TB_PROJECT_MAKEGROUP ||
$access_type == TB_PROJECT_DELGROUP) {
$mintrust = PROJMEMBERTRUST_GROUPROOT;
}
elsif ($access_type == TB_PROJECT_LEADGROUP) {
#
# Allow mere user (in default group) to lead a subgroup.
#
$mintrust = PROJMEMBERTRUST_USER;
}
elsif ($access_type == TB_PROJECT_MAKEOSID ||
$access_type == TB_PROJECT_MAKEIMAGEID ||
$access_type == TB_PROJECT_CREATEEXPT) {
$mintrust = PROJMEMBERTRUST_LOCALROOT;
}
elsif ($access_type == TB_PROJECT_DELUSER) {
$mintrust = PROJMEMBERTRUST_PROJROOT;
elsif ($access_type == TB_PROJECT_ADDUSER ||
$access_type == TB_PROJECT_EDITGROUP) {
#
# If user is project_root or group_root in default group,
# allow them to add/edit/remove users in any group.
#
if (TBMinTrust(TBGrpTrust($uid, $pid, $pid),
PROJMEMBERTRUST_GROUPROOT)) {
return 1;
}
#
# Otherwise, editing a group requires group_root
# in that group.
#
$mintrust = PROJMEMBERTRUST_GROUPROOT;
}
elsif ($access_type == TB_PROJECT_MAKEGROUP ||
$access_type == TB_PROJECT_DELGROUP) {
elsif ($access_type == TB_PROJECT_BESTOWGROUPROOT) {
#
# If user is project_root,
# allow them to bestow group_root in any group.
#
if (TBMinTrust(TBGrpTrust($uid, $pid, $pid),
PROJMEMBERTRUST_PROJROOT)) {
return 1;
}
if ($gid == $pid) {
#
# Only project_root can bestow group_root in default group,
# and we already established that they are not project_root,
# so fail.
#
return 0;
}
else {
#
# Non-default group.
# group_root in default group may bestow group_root.
#
if (TBMinTrust(TBGrpTrust($uid, $pid, $pid),
PROJMEMBERTRUST_GROUPROOT)) {
return 1;
}
#
# group_root in the group in question may also bestow
# group_root.
#
$mintrust = PROJMEMBERTRUST_GROUPROOT;
}
}
elsif ($access_type == TB_PROJECT_GROUPGRABUSERS) {
#
# Only project_root or group_root in default group
# may grab (involuntarily add) users into groups.
#
$gid = $pid;
$mintrust = PROJMEMBERTRUST_GROUPROOT;
}
elsif ($access_type == TB_PROJECT_DELUSER) {
$mintrust = PROJMEMBERTRUST_PROJROOT;
}
else {
print "*** Invalid access type: $access_type!\n";
return 0;
......@@ -785,7 +855,6 @@ sub LeaderMailList($)
#
# Return list of members in this group, by specific trust.
#
sub MemberList($$;$$)
{
my ($self, $prval, $flags, $desired_trust) = @_;
......
......@@ -565,7 +565,13 @@ REPLACE INTO table_regex VALUES ('experiments','wa_plr_solverweight','float','re
REPLACE INTO table_regex VALUES ('experiments','cpu_usage','int','redirect','default:tinyint',0,5,NULL);
REPLACE INTO table_regex VALUES ('experiments','mem_usage','int','redirect','default:tinyint',0,5,NULL);
REPLACE INTO table_regex VALUES ('experiments','sync_server','text','redirect','virt_nodes:vname',0,0,NULL);
REPLACE INTO table_regex VALUES ('groups','project','text','redirect','projects:pid',0,0,NULL);
REPLACE INTO table_regex VALUES ('groups','gid','text','regex','^[a-zA-Z][-\\w]+$',2,12,NULL);
REPLACE INTO table_regex VALUES ('groups','group_id','text','redirect','groups:gid',2,12,NULL);
REPLACE INTO table_regex VALUES ('groups','group_leader','text','redirect','users:uid',2,8,NULL);
REPLACE INTO table_regex VALUES ('groups','group_description','text','redirect','default:tinytext',0,256,NULL);
REPLACE INTO table_regex VALUES ('nodes','node_id','text','regex','^[-\\w]+$',1,12,NULL);
REPLACE INTO table_regex VALUES ('nseconfigs','pid','text','redirect','projects:pid',0,0,NULL);
REPLACE INTO table_regex VALUES ('nseconfigs','eid','text','redirect','experiments:eid',0,0,NULL);
......
......@@ -93,6 +93,94 @@ class Group
return 0;
}
#
# Class function to create a new Project Group.
#
function Create($project, $uid, $args, &$errors) {
global $suexec_output, $suexec_output_array;
#
# Generate a temporary file and write in the XML goo.
#
$xmlname = tempnam("/tmp", "newgroup");
if (! $xmlname) {
TBERROR("Could not create temporary filename", 0);
$errors[] = "Transient error; please try again later.";
return null;
}
if (! ($fp = fopen($xmlname, "w"))) {
TBERROR("Could not open temp file $xmlname", 0);
$errors[] = "Transient error; please try again later.";
return null;
}
# Add these. Maybe caller should do this?
$args["project"] = $project->pid();
fwrite($fp, "<group>\n");
foreach ($args as $name => $value) {
fwrite($fp, "<attribute name=\"$name\">");
fwrite($fp, " <value>" . htmlspecialchars($value) . "</value>");
fwrite($fp, "</attribute>\n");
}
fwrite($fp, "</group>\n");
fclose($fp);
chmod($xmlname, 0666);
# Note: running as the user for mkgroup and modgroups underneath.
$unix_gid = $project->unix_gid();
$retval = SUEXEC($uid, $unix_gid, "webnewgroup $xmlname",
SUEXEC_ACTION_IGNORE);
if ($retval) {
if ($retval < 0) {
$errors[] = "Transient error; please try again later.";
SUEXECERROR(SUEXEC_ACTION_CONTINUE);
}
else {
# unlink($xmlname);
if (count($suexec_output_array)) {
for ($i = 0; $i < count($suexec_output_array); $i++) {
$line = $suexec_output_array[$i];
if (preg_match("/^([-\w]+):\s*(.*)$/",
$line, $matches)) {
$errors[$matches[1]] = $matches[2];
}
else
$errors[] = $line;
}
}
else
$errors[] = "Transient error; please try again later.";
}
return null;
}
#
# Parse the last line of output. Ick.
#
unset($matches);
if (!preg_match("/^GROUP\s+([^\/]+)\/(\d+)\s+/",
$suexec_output_array[count($suexec_output_array)-1],
$matches)) {
$errors[] = "Transient error; please try again later.";
SUEXECERROR(SUEXEC_ACTION_CONTINUE);
return null;
}
$group = $matches[2];
$newgroup = Group::Lookup($group);
if (! $newgroup) {
$errors[] = "Transient error; please try again later.";
TBERROR("Could not lookup new group $group", 0);
return null;
}
# Unlink this here, so that the file is left behind in case of error.
# We can then create the group by hand from the xmlfile, if desired.
unlink($xmlname);
return $newgroup;
}
#
# Load the project for a group lazily.
#
......
......@@ -19,125 +19,209 @@ $uid = $this_user->uid();
$isadmin = ISADMIN();
#
# Verify page arguments
#
$reqargs = RequiredPageArguments("project", PAGEARG_PROJECT,
"group_id", PAGEARG_STRING,
"group_description", PAGEARG_ANYTHING,
"group_leader", PAGEARG_STRING);
# Need these below
$group_pid = $project->pid();
$unix_gid = $project->unix_gid();
$safe_id = escapeshellarg($group_id);
#
# Check ID for sillyness.
#
if ($group_id == "") {
USERERROR("Must provide a group name!", 1);
}
elseif (! TBvalid_gid($group_id)) {
USERERROR("Invalid group name: " . TBFieldErrorString(), 1);
# Verify page arguments.
#
$optargs = OptionalPageArguments("project", PAGEARG_PROJECT,
"submit", PAGEARG_STRING,
"formfields", PAGEARG_ARRAY);
if (!isset($project)) {
#
# See what projects the uid can do this in.
#
$projlist = $this_user->ProjectAccessList($TB_PROJECT_MAKEGROUP);
if (! count($projlist)) {
USERERROR("You do not appear to be a member of any Projects in which ".
"you have permission to create new groups.", 1);
}
}
if ($group_leader == "") {
USERERROR("Must provide a group leader!", 1);
else {
#
# Verify permission for specific project.
#
$pid = $project->pid();
if (!$project->AccessCheck($this_user, $TB_PROJECT_MAKEGROUP)) {
USERERROR("You do not have permission to create groups in ".
"project $pid!", 1);
}
}
#
# Certain of these values must be escaped or otherwise sanitized.
# Spit the form out using the array of data.
#
$group_description = addslashes($group_description);
function SPITFORM($formfields, $errors)
{
global $project, $pid, $projlist;
global $TBDB_GIDLEN, $TBDB_UIDLEN;
if ($errors) {
echo "<table class=nogrid
align=center border=0 cellpadding=6 cellspacing=0>
<tr>
<th align=center colspan=2>
<font size=+1 color=red>
&nbsp;Oops, please fix the following errors!&nbsp;
</font>
</td>
</tr>\n";
while (list ($name, $message) = each ($errors)) {
echo "<tr>
<td align=right>
<font color=red>$name:&nbsp;</font></td>
<td align=left>
<font color=red>$message</font></td>
</tr>\n";
}
echo "</table><br>\n";
}
#
# Verify permission.
#
if (!$project->AccessCheck($this_user, $TB_PROJECT_MAKEGROUP)) {
USERERROR("You do not have permission to create groups in project ".
"$group_pid!", 1);
}
echo "<br>
<table align=center border=1>
<tr>
<td align=center colspan=2>
<em>(Fields marked with * are required)</em>
</td>
</tr>\n";
if (isset($project)) {
$url = CreateURL("newgroup", $project);
echo "<form action='$url' method=post>
<tr>
<td>* Project:</td>
<td class=left>
<input name=project type=readonly value='$pid'>
</td>
</tr>\n";
}
else {
$url = CreateURL("newgroup");
echo "<form action='$url' method=post>
<tr>
<td>*Select Project:</td>";
echo " <td><select name=project>";
while (list($proj) = each($projlist)) {
echo "<option value='$proj'>$proj </option>\n";