All new accounts created on Gitlab now require administrator approval. If you invite any collaborators, please let Flux staff know so they can approve the accounts.

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)) {