diff --git a/backend/GNUmakefile.in b/backend/GNUmakefile.in
index 3697ae7c36d1f9724281333218b7140ef8f4ab56..4aac3824ddea13b6ca0ce72ebe436abfbefe3750 100644
--- a/backend/GNUmakefile.in
+++ b/backend/GNUmakefile.in
@@ -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)
 
diff --git a/backend/newgroup.in b/backend/newgroup.in
new file mode 100644
index 0000000000000000000000000000000000000000..0668c3da0856c16ec189778cd90ead390ce820da
--- /dev/null
+++ b/backend/newgroup.in
@@ -0,0 +1,375 @@
+#!/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;
+}
diff --git a/configure b/configure
index e061fdb31c5a7a452d90f8eca70140295959b61d..3e044219b566b96727fe6061aa65bfb5f38cd9ba 100755
--- a/configure
+++ b/configure
@@ -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 \
diff --git a/configure.in b/configure.in
index ef103b351745c46794ea7d1e1a77980571f30550..2fac1dc383c25d065f0b9c98f7a46eec58d88074 100755
--- a/configure.in
+++ b/configure.in
@@ -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 \
diff --git a/db/Group.pm.in b/db/Group.pm.in
index f76fddbbaf2fc2c6fcd9fbab971328fc943ff14e..2969835aabe73b8eb4e174676807ed99bb55619b 100644
--- a/db/Group.pm.in
+++ b/db/Group.pm.in
@@ -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) = @_;
diff --git a/sql/database-fill.sql b/sql/database-fill.sql
index fa21f4e6ef30fc5ab28074303399dc858de02b10..020f361184102ef5e31d20d6bcdd357acbd6c19f 100644
--- a/sql/database-fill.sql
+++ b/sql/database-fill.sql
@@ -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);
diff --git a/www/group_defs.php b/www/group_defs.php
index afff99800592c062cbdd7c0546c35572583f0157..6053a0320135cdc9ef4ae288de5ea3c3de7ceb6c 100644
--- a/www/group_defs.php
+++ b/www/group_defs.php
@@ -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.
     #
diff --git a/www/newgroup.php3 b/www/newgroup.php3
index fa98826f364a41b29f76fe95bce1081f89b9f044..35b1f9b99e12fb28d4586f47a6855b1cb7ebc5af 100644
--- a/www/newgroup.php3
+++ b/www/newgroup.php3
@@ -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";
+	}
+
+	echo "       </select>";
+	echo "    </td>
+	      </tr>\n";
+    }
 
-# Need the user object for creating the group.
-if (! ($leader = User::Lookup($group_leader))) {
-    USERERROR("User '$group_leader' is an unknown user!", 1);
+    echo "<tr>
+	      <td>*Group Name (no blanks, lowercase):</td>
+	      <td class=left>
+		  <input type=text 
+			 name=\"formfields[group_id]\" 
+			 value=\"" . $formfields["group_id"] . "\"
+			 size=$TBDB_GIDLEN
+			 maxlength=$TBDB_GIDLEN>
+	      </td>
+	  </tr>\n";
+
+    echo "<tr>
+	      <td>*Group Description:</td>
+	      <td class=left>
+		  <input type=text size=50
+			 name=\"formfields[group_description]\"
+			 value=\"" . $formfields["group_description"] . "\">
+	      </td>
+	  </tr>\n";
+
+    echo "<tr>
+	      <td>*Group Leader (Emulab userid):</td>
+	      <td class=left>
+		  <input type=text
+			 name=\"formfields[group_leader]\"
+			 value=\"" . $formfields["group_leader"] . "\"
+			 size=$TBDB_UIDLEN maxlength=$TBDB_UIDLEN>
+	      </td>
+	  </tr>\n";
+
+    echo "<tr>
+              <td align=center colspan=2>
+                  <b><input type=submit name=submit value=Submit></b>
+              </td>
+          </tr>\n";
+
+    echo "</form>
+          </table>\n";
+
+    echo "<br><center>
+	     Important <a href=docwrapper.php3?docname=groups.html#SECURITY'>
+	     security issues</a> are discussed in the
+	     <a href='docwrapper.php3?docname=groups.html'>Groups Tutorial</a>.
+          </center>\n";
 }
 
 #
-# 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().
+# Accumulate error reports for the user, e.g.
+#    $errors["Key"] = "Msg";
+# Required page args may need to be checked early.
+$errors  = array();
+
+#
+# On first load, display a virgin form and exit.
 #
-if (!$project->IsMember($leader, $isapproved) ||
-    !$project->AccessCheck($leader, $TB_PROJECT_LEADGROUP)) {
-    USERERROR("$group_leader does not have enough permission to lead a group ".
-	      "in project $group_pid!", 1);
+if (!isset($submit)) {
+    $defaults = array();
+    $defaults["group_id"]	   = "";
+    $defaults["group_description"] = "";
+    $defaults["group_leader"]	   = $uid;
+
+    SPITFORM($defaults, $errors);
+    PAGEFOOTER();
+    return;
 }
 
 #
-# Make sure the GID is not already there.
-#
-if (($oldgroup = Group::LookupByPidGid($group_pid, $group_id))) {
-    USERERROR("The group $group_id already exists! Please select another.", 1);
+# If any errors, respit the form with the current values and the
+# error messages displayed. Iterate until happy.
+# 
+if (count($errors)) {
+    SPITFORM($formfields, $errors);
+    PAGEFOOTER();
+    return;
 }
 
 #
-# The unix group name must be globally unique. Form a name and check it.
+# Build up argument array to pass along.
 #
-$unix_gname = substr($group_pid, 0, 3) . "-" . $group_id;
-$maxtries   = 99;
-$count      = 0;
-
-while ($count < $maxtries) {
-    if (strlen($unix_gname) > $TBDB_UNIXGLEN) {
-	TBERROR("Unix group name $unix_gname is too long!", 1);
-    }
-    
-    $query_result =
-	DBQueryFatal("select gid from groups where unix_name='$unix_gname'");
-
-    if (mysql_num_rows($query_result) == 0) {
-	break;
-    }
-    $count++;
+$args = array();
 
-    $unix_gname = substr($group_pid, 0, 3) . "-" .
-	substr($group_id,  0, strlen($group_id) - 2) . "$count";
+if (isset($formfields["project"]) &&
+    $formfields["project"] != "none" && $formfields["project"] != "") {
+    $args["project"]		= $formfields["project"];
 }
-if ($count == $maxtries) {
-    TBERROR("Could not form a unique Unix group name!", 1);
+if (isset($formfields["group_id"]) && $formfields["group_id"] != "") {
+    $args["group_id"]	= $formfields["group_id"];
 }
-
-#
-# Create the new group and set up the initial membership for the leader.
-#
-# Note, if the project leader wants to be in the subgroup, he/she has to
-# add themself via the edit page. 
-#
-if (! ($newgroup = Group::NewGroup($project, $group_id, $leader,
-				   $group_description, $unix_gname))) {
-    TBERROR("Could not create new group $group_pid/$group_id!", 1);
+if (isset($formfields["group_description"]) && 
+    $formfields["group_description"] != "") {
+    $args["group_description"]	= $formfields["group_description"];
+}
+if (isset($formfields["group_leader"]) && $formfields["group_leader"] != "") {
+    $args["group_leader"]	= $formfields["group_leader"];
 }
-$gid_idx = $newgroup->gid_idx();
 
-STARTBUSY("Creating project group $group_id.");
+$group_id = $formfields["group_id"];
+###STARTBUSY("Creating project group $group_id.");
+echo "<br>Creating project group $group_id.<br>\n";
 
-#
-# Run the script. This will make the group directory, set the perms, etc.
-#
-SUEXEC($uid, $unix_gid,
-       "webmkgroup $gid_idx", SUEXEC_ACTION_DIE);
-
-#
-# Now add the group leader to the group.
-# 
-SUEXEC($uid, $unix_gid,
-       "webmodgroups -a $group_pid:$safe_id:group_root $group_leader",
-       SUEXEC_ACTION_DIE);
+if (! ($newgroup = Group::Create($project, $uid, $args, $errors))) {
+    # Always respit the form so that the form fields are not lost.
+    # I just hate it when that happens so lets not be guilty of it ourselves.
+    SPITFORM($formfields, $errors);
+    PAGEFOOTER();
+    return;
+}
 
-STOPBUSY();
+###STOPBUSY();
 
-#
-# Redirect back to project page.
-#
+echo "<center><h3>Done!</h3></center>\n";
 PAGEREPLACE(CreateURL("showgroup", $newgroup));
 
 #
diff --git a/www/newgroup_form.php3 b/www/newgroup_form.php3
deleted file mode 100644
index 88e7087ba48fcf54c54d888c735fcefe39006730..0000000000000000000000000000000000000000
--- a/www/newgroup_form.php3
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-#
-# EMULAB-COPYRIGHT
-# Copyright (c) 2000-2007 University of Utah and the Flux Group.
-# All rights reserved.
-#
-include("defs.php3");
-
-#
-# Standard Testbed Header
-#
-PAGEHEADER("Create a Project Group");
-
-#
-# Only known and logged in users.
-#
-$this_user = CheckLoginOrDie();
-$uid       = $this_user->uid();
-$isadmin   = ISADMIN();
-
-#
-# Verify page arguments.
-#
-$optargs = OptionalPageArguments("project", PAGEARG_PROJECT);
-
-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);
-    }
-}
-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);
-    }
-}
-
-
-echo "<form action=newgroup.php3 method=post>
-      <table align=center border=1> 
-      <tr>
-        <td align=center colspan=2>
-           <em>(Fields marked with * are required)</em>
-        </td>
-      </tr>\n";
-
-if (isset($project)) {
-    echo "<tr>
-              <td>* Project:</td>
-              <td class=left>
-                  <input name=project type=readonly value='$pid'>
-              </td>
-          </tr>\n";
-}
-else {
-    echo "<tr>
-              <td>*Select Project:</td>";
-    echo "    <td><select name=project>";
-
-    while (list($project) = each($projlist)) {
-	echo "<option value='$project'>$project </option>\n";
-    }
-
-    echo "       </select>";
-    echo "    </td>
-          </tr>\n";
-}
-
-echo "<tr>
-          <td>*Group Name (no blanks, lowercase):</td>
-          <td class=left>
-              <input name=group_id type=text size=$TBDB_GIDLEN
-                     maxlength=$TBDB_GIDLEN>
-          </td>
-      </tr>\n";
-
-echo "<tr>
-          <td>*Group Description:</td>
-          <td class=left>
-              <input name=group_description type=text size=50>
-          </td>
-      </tr>\n";
-
-echo "<tr>
-          <td>*Group Leader (Emulab userid):</td>
-          <td class=left>
-              <input name=group_leader type=text value='$uid'
-	             size=$TBDB_UIDLEN maxlength=$TBDB_UIDLEN>
-          </td>
-      </tr>\n";
-
-echo "<tr>
-         <td align=center colspan=2>
-            <b><input type=submit value=Submit></b></td>
-      </tr>
-      </form>
-      </table>\n";
-
-echo "<br><center>
-       Important <a href=docwrapper.php3?docname=groups.html#SECURITY'>
-       security issues</a> are discussed in the
-       <a href='docwrapper.php3?docname=groups.html'>Groups Tutorial</a>.
-      </center>\n";
-
-#
-# Standard Testbed Footer
-# 
-PAGEFOOTER();
-?>
diff --git a/www/showproject.php3 b/www/showproject.php3
index 0186e5f6a7f794508d3b947184154da4394c4f6f..a9b3d7258041876a26da9d9317d517355aa5f85d 100644
--- a/www/showproject.php3
+++ b/www/showproject.php3
@@ -45,7 +45,7 @@ if (! $project->AccessCheck($this_user, $TB_PROJECT_READINFO)) {
 SUBPAGESTART();
 SUBMENUSTART("Project Options");
 WRITESUBMENUBUTTON("Create Subgroup",
-		   "newgroup_form.php3?pid=$pid");
+		   "newgroup.php3?pid=$pid");
 WRITESUBMENUBUTTON("Edit User Privs",
 		   "editgroup_form.php3?pid=$pid&gid=$pid");
 WRITESUBMENUBUTTON("Remove Users",