Commit 664108af authored by Leigh Stoller's avatar Leigh Stoller

Subgroups! What more needs to be said?

parent 1a348241
......@@ -89,7 +89,7 @@ my $UPDATEGENIUSER= "$TB/sbin/protogeni/updategeniuser";
my $STITCHER = "$TB/gcf/src/stitcher.py";
my $OPENSSL = "/usr/bin/openssl";
my $MANAGEINSTANCE= "$TB/bin/manage_instance";
my $DEFAULT_URN = "urn:publicid:IDN+${OURDOMAIN}+authority+cm";
my $DEFAULT_URN = "urn:publicid:IDN+$OURDOMAIN+authority+cm";
my $GUEST_URN = "urn:publicid:IDN+apt.emulab.net+authority+cm";
my $default_aggregate_urn = $DEFAULT_URN;
......@@ -100,7 +100,7 @@ delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
#
# Turn off line buffering on output
#
$| = 1;
$| = 1;
# Load the Testbed support stuff.
use lib "@prefix@/lib";
......@@ -258,7 +258,7 @@ foreach my $key ("username", "email", "profile", "portal") {
# Gather up args and sanity check.
#
my ($value, $user_urn, $user_uid, $user_hrn, $user_email, $project, $pid,
$sshkey, $profile, $profileid, $version, $rspecstr, $errmsg,
$gid, $group, $sshkey, $profile, $profileid, $version, $rspecstr, $errmsg,
$userslice_id, $portal);
# This is used internally to determine which portal was used.
......@@ -561,6 +561,21 @@ if ($localuser) {
}
$pid = $project->pid();
# Option subgroup.
if (exists($xmlparse->{'attribute'}->{"gid"}) &&
$xmlparse->{'attribute'}->{"gid"}->{"value"} ne "" &&
$xmlparse->{'attribute'}->{"gid"}->{"value"} ne $pid) {
my $val = $xmlparse->{'attribute'}->{"gid"}->{"value"};
$group = $project->LookupGroup($val);
if (!defined($group)) {
fatal("Group $val does not exist in project $pid");
}
}
else {
$group = $project->GetProjectGroup();
}
$gid = $group->gid();
# Use of the Image Tracker is a Portal directive at the moment.
$usetracker = 1
if (GetSiteVar("protogeni/use_imagetracker") &&
......@@ -579,6 +594,7 @@ elsif (!$localuser) {
# Guest users get a holding project.
$pid = $APT_HOLDINGPROJECT;
$project = Project->Lookup($pid);
$group = $project->GetProjectGroup();
if (!defined($project)) {
fatal("Project $pid does not exist");
}
......@@ -613,7 +629,9 @@ if (defined($profile)) {
my $safe_uid = $user_uid; $safe_uid =~ s/_/-/;
my $slice_id = (defined($userslice_id) ? $userslice_id :
$safe_uid . "-QV" . TBGetUniqueIndex('next_quickvm', 1));
my $slice_urn = GeniHRN::Generate("${OURDOMAIN}:${pid}", "slice", $slice_id);
my $slice_auth = ($pid eq $gid ? $pid : "${pid}:${gid}");
my $slice_urn = GeniHRN::Generate("${OURDOMAIN}:${slice_auth}",
"slice", $slice_id);
my $slice_hrn = "${PGENIDOMAIN}.${pid}.${slice_id}";
my $SERVER_NAME = (exists($ENV{"SERVER_NAME"}) ? $ENV{"SERVER_NAME"} : "");
......@@ -630,7 +648,7 @@ if (GeniSlice->Lookup($slice_hrn) || GeniSlice->Lookup($slice_urn)) {
fatal("Could not form a unique slice name");
}
}
#
# Generate a certificate for this new slice.
#
......@@ -667,7 +685,7 @@ my $slice_uuid = $slice->uuid();
# nodes, hence the alternate CA, and the XMLRPC server will not allow
# this certificate to do anything, except at the portal RPC server.
#
my $alt_urn = GeniHRN::Generate("aptlab.net:${pid}", "slice", $slice_id);
my $alt_urn = GeniHRN::Generate("aptlab.net:${slice_auth}", "slice", $slice_id);
my $alt_hrn = "aptlab.${pid}.${slice_id}";
my $alt_url = "$PROTOGENI_URL/portal";
......@@ -740,6 +758,8 @@ my $blob = {'uuid' => $quickvm_uuid,
if (defined($project)) {
$blob->{"pid"} = $project->pid();
$blob->{"pid_idx"} = $project->pid_idx();
$blob->{"gid"} = $group->gid();
$blob->{"gid_idx"} = $group->gid_idx();
}
$errmsg = undef;
$instance = APT_Instance->Create($blob, \$errmsg);
......
......@@ -39,6 +39,11 @@ function ClassicExperimentList($which, $target, $state = "active")
$target_idx = $target->uid_idx();
$whereclause = "where e.swapper_idx='$target_idx'";
}
elseif ($which == "group") {
$target_pid = $target->pid();
$target_gid = $target->gid();
$whereclause = "where e.pid='$target_pid' and e.gid='$target_gid'";
}
else {
$target_pid = $target->pid();
$whereclause = "where e.pid='$target_pid'";
......@@ -103,6 +108,11 @@ function ExperimentList($which, $target)
$target_uuid = $target->uuid();
$whereclause = "where a.creator_uuid='$target_uuid'";
}
elseif ($which == "group") {
$target_pid = $target->pid();
$target_gid = $target->gid();
$whereclause = "where a.pid='$target_pid' and a.gid='$target_gid'";
}
else {
$target_pid = $target->pid();
$whereclause = "where a.pid='$target_pid'";
......@@ -218,6 +228,11 @@ function ProfileList($which, $target)
$target_idx = $target->uid_idx();
$whereclause = "where v.creator_idx='$target_idx'";
}
elseif ($which == "group") {
$target_pid = $target->pid();
$target_gid = $target->gid();
$whereclause = "where v.pid='$target_pid' and v.gid='$target_gid'";
}
else {
$target_idx = $target->pid_idx();
$whereclause = "where v.pid_idx='$target_idx'";
......
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
chdir("..");
include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
# Must be after quickvm_sup.php since it changes the auth domain.
$page_title = "Create Group";
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLoginOrRedirect();
$this_idx = $this_user->uid_idx();
$this_uid = $this_user->uid();
$isadmin = (ISADMIN() ? 1 : 0);
#
# Verify page arguments.
#
$reqargs = RequiredPageArguments("project", PAGEARG_PROJECT);
$optargs = OptionalPageArguments("leader", PAGEARG_USER);
if (!$project->AccessCheck($this_user, $TB_PROJECT_MAKEGROUP)) {
SPITUSERERROR("You do not have permission to create groups in ".
"project " . $project->pid());
}
if (isset($leader)) {
$isapproved = 0;
if (! ($project->IsMember($leader, $isapproved) && $isapproved)) {
SPITUSERERROR($leader->uid(). " is not a member of project " .
$project->uid());
}
}
SPITHEADER(1);
# Place to hang the toplevel template.
echo "<div id='main-body'></div>\n";
# Initial form contents.
$formfields = array();
$formfields["project"] = $project->pid();
$formfields["group_id"] = "";
$formfields["group_leader"] = (isset($leader) ? $leader->uid() : $this_uid);
$formfields["group_description"] = "";
echo "<script type='text/plain' id='form-json'>\n";
echo htmlentities(json_encode($formfields)) . "\n";
echo "</script>\n";
echo "<script type='text/javascript'>\n";
echo " window.ISADMIN = $isadmin;\n";
echo "</script>\n";
SPITREQUIRE("create-group");
SPITFOOTER();
?>
This diff is collapsed.
......@@ -110,6 +110,8 @@ class Instance
function paniced() { return $this->field('paniced'); }
function pid() { return $this->field('pid'); }
function pid_idx() { return $this->field('pid_idx'); }
function gid() { return $this->field('gid'); }
function gid_idx() { return $this->field('gid_idx'); }
function public_url() { return $this->field('public_url'); }
function logfileid() { return $this->field('logfileid'); }
function manifest() { return $this->field('manifest'); }
......@@ -266,11 +268,14 @@ class Instance
#
# With a real user, run as that user.
#
$uid = ($creator ? $creator->uid() : "nobody");
$pid = "nobody";
if ($creator && $creator->FirstApprovedProject()) {
$pid = $creator->FirstApprovedProject()->pid();
}
if ($creator) {
$uid = $creator->uid();
$pid = $args["pid"];
}
else {
$uid = "nobody";
$pid = "nobody";
}
if (isset($_SERVER['REMOTE_ADDR'])) {
putenv("REMOTE_ADDR=" . $_SERVER['REMOTE_ADDR']);
}
......
......@@ -546,6 +546,7 @@ function CheckStep2()
global $this_user;
global $ajax_args;
global $ISAPT, $ISPNET, $ISCLOUD, $ISEMULAB;
global $TB_PROJECT_CREATEEXPT;
if (!isset($ajax_args["formfields"])) {
SPITAJAX_ERROR(-1, "Missing formfields");
......@@ -609,8 +610,10 @@ function CheckStep2()
#
# Project has to exist.
#
$project = Project::LookupByPid($formfields["pid"]);
if (!$project) {
if (!isset($formfields["pid"])) {
$errors["pid"] = "Must select a project";
}
elseif (! ($project = Project::LookupByPid($formfields["pid"]))) {
$errors["pid"] = "No such project";
}
# User better be a member.
......@@ -619,6 +622,23 @@ function CheckStep2()
!$isapproved)) {
$errors["pid"] = "Illegal project";
}
elseif ($formfields["pid"] != $formfields["gid"]) {
$group = $project->LookupSubgroupByName($formfields["gid"]);
if (!$group) {
$errors["gid"] = "No such group in selected project";
}
elseif (!$group->AccessCheck($this_user, $TB_PROJECT_CREATEEXPT)) {
$errors["gid"] = "No permission to create experiments in ".
"selected group";
}
}
else {
$group = $project->DefaultGroup();
if (!$group->AccessCheck($this_user, $TB_PROJECT_CREATEEXPT)) {
$errors["pid"] = "No permission to create experiments in ".
"selected project";
}
}
# Experiment name is optional, we generate one later.
if (isset($formfields["name"]) && $formfields["name"] != "") {
......@@ -787,6 +807,7 @@ function Do_Submit()
}
# Required for real users.
$args["pid"] = $formfields["pid"];
$args["gid"] = $formfields["gid"];
# Experiment name is optional, we generate one later.
if (isset($formfields["name"]) && $formfields["name"] != "") {
......
......@@ -388,12 +388,8 @@ function SPITFORM($formfields, $newuser, $errors)
# Spit out a project selection list if a real user.
#
if ($this_user && !$this_user->webonly()) {
$plist = array();
while (list($project) = each($projlist)) {
$plist[] = $project;
}
echo "<script type='text/plain' id='projects-json'>\n";
echo htmlentities(json_encode($plist));
echo htmlentities(json_encode($projlist));
echo "</script>\n";
}
#
......@@ -459,10 +455,12 @@ if (!isset($create)) {
if ($this_user && count($projlist)) {
list($project, $grouplist) = each($projlist);
$defaults["pid"] = $project;
$defaults["gid"] = $project;
reset($projlist);
}
else {
$defaults["pid"] = "";
$defaults["gid"] = "";
}
#
......
......@@ -224,7 +224,7 @@ define(['underscore', 'js/quickvm_sup'],
/*
* Submit form.
*/
function SubmitForm(form, route, method, callback) {
function SubmitForm(form, route, method, callback, message) {
/*
* Convert form data into formfields array, like all our
* form handler pages expect.
......@@ -241,7 +241,7 @@ define(['underscore', 'js/quickvm_sup'],
DisableUnsavedWarning(form);
callback(json);
};
sup.ShowModal("#waitwait-modal");
sup.ShowWaitWait(message);
var xmlthing =
sup.CallServerMethod(null, route, method,
{"formfields" : formfields,
......
require(window.APT_OPTIONS.configObject,
['underscore', 'js/quickvm_sup', 'moment', 'js/aptforms',
'js/lib/text!template/create-group.html',
'js/lib/text!template/oops-modal.html',
'js/lib/text!template/waitwait-modal.html'],
function (_, sup, moment, aptforms,
mainString, oopsString, waitwaitString)
{
'use strict';
var mainTemplate = _.template(mainString);
var fields = null;
var isadmin = false;
function initialize()
{
window.APT_OPTIONS.initialize(sup);
isadmin = window.ISADMIN;
fields = JSON.parse(_.unescape($('#form-json')[0].textContent));
GeneratePageBody(fields);
// Now we can do this.
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
}
//
// Moved into a separate function since we want to regen the form
// after each submit, which happens via ajax on this page.
//
function GeneratePageBody(formfields)
{
// Generate the template.
var html = mainTemplate({
formfields: formfields,
isadmin: isadmin,
});
html = aptforms.FormatFormFieldsHorizontal(html);
$('#main-body').html(html);
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
aptforms.EnableUnsavedWarning('#create_dataset_form');
// Handler for submit button.
$('#create-group-button').click(function (event) {
event.preventDefault();
SubmitForm();
});
}
//
// Submit the form.
//
function SubmitForm()
{
var submit_callback = function(json) {
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
window.location.replace(json.value);
};
var checkonly_callback = function(json) {
if (json.code) {
if (json.code != 2) {
sup.SpitOops("oops", json.value);
}
return;
}
aptforms.SubmitForm('#create-group-form', "groups", "Create",
submit_callback,
"Creating your group, this will take a " +
"minute or two ... patience please");
};
aptforms.CheckForm('#create-group-form', "groups", "Create",
checkonly_callback);
}
$(document).ready(initialize);
});
......@@ -130,7 +130,7 @@ function (_, Constraints, sup, ppstart, JacksEditor, wt,
admin: isadmin,
});
$('#main-body').html(html);
UpdateGroupSelector();
// TEMPORARY BUTTON FOR CLASSIC PICKER
// To be removed when the new picker becomes default
......@@ -247,7 +247,8 @@ function (_, Constraints, sup, ppstart, JacksEditor, wt,
* is changed.
*/
$('#profile_pid').change(function (event) {
console.log('profile-pid change');
console.log('profile-pid change');
UpdateGroupSelector();
UpdateImageConstraints();
return true;
});
......@@ -432,7 +433,7 @@ function (_, Constraints, sup, ppstart, JacksEditor, wt,
if (isSystem) {
result.sysproj[key] = obj;
}
if (_.contains(projlist, obj.project)) {
if (_.has(projlist, obj.project)) {
if (!result.inproj[obj.project]) {
result.inproj[obj.project] = {};
}
......@@ -1624,5 +1625,39 @@ function (_, Constraints, sup, ppstart, JacksEditor, wt,
});
}
// When the project is changed, look to see if the new project includes
// multiple subgroups. If only one subgroup, hide the group selector.
// Otherwise build/show a group selector.
function UpdateGroupSelector()
{
var pid = $('#project_selector #profile_pid').val();
var glist = projlist[pid];
console.info(pid, glist);
if (glist.length == 1) {
var gid = glist[0];
// No need to show it.
$('#group_selector').addClass("hidden");
// But need to add an option so we can select it for submit.
var html = "<option selected value=" + gid + ">" + gid + "</option>";
$('#group_selector #profile_gid').html(html);
$('#group_selector #profile_gid').val(gid);
return;
}
var html = "";
_.each(glist, function(gid) {
var selected = "";
// Select the project group by default.
if (gid == pid) {
selected = "selected";
}
html = html +
"<option " + selected + " value=" + gid + ">" + gid + "</option>";
});
$('#group_selector #profile_gid').html(html);
$('#group_selector').removeClass("hidden");
}
$(document).ready(initialize);
});
......@@ -15,8 +15,6 @@ define(['underscore', 'js/quickvm_sup', 'moment'],
var counter = {
"network.create" : 0,
"network.delete" : 0,
"port.create" : 0,
"port.delete" : 0,
"router.create" : 0,
"router.delete" : 0,
"subnet.create" : 0,
......
This diff is collapsed.
......@@ -6,10 +6,13 @@ require(window.APT_OPTIONS.configObject,
'js/lib/text!template/member-list.html',
'js/lib/text!template/project-profile.html',
'js/lib/text!template/classic-explist.html',
'js/lib/text!template/group-list.html',
'js/lib/text!template/waitwait-modal.html',
'js/lib/text!template/oops-modal.html',
],
function (_, sup, moment, mainString,
experimentString, profileString, memberString, detailsString,
classicString)
classicString, groupsString, waitString, oopsString)
{
'use strict';
var mainTemplate = _.template(mainString);
......@@ -25,6 +28,8 @@ function (_, sup, moment, mainString,
target_project : window.TARGET_PROJECT,
});
$('#main-body').html(html);
$('#waitwait_div').html(waitString);
$('#oops_div').html(oopsString);
// Javascript to enable link to tab
var hash = document.location.hash;
......@@ -50,6 +55,7 @@ function (_, sup, moment, mainString,
LoadProfileTab();
LoadClassicProfiles();
LoadMembersTab();
LoadGroupsTab();
LoadProjectTab();
}
......@@ -126,7 +132,7 @@ function (_, sup, moment, mainString,
});
var table = $('#experiments_table')
.tablesorter({
theme : 'green',
theme : 'blue',
});
}
var xmlthing = sup.CallServerMethod(null,
......@@ -144,6 +150,8 @@ function (_, sup, moment, mainString,
console.info(json.value);
return;
}
if (json.value.length == 0)
return;
var template = _.template(classicString);
$('#classic_experiments_content')
......@@ -161,7 +169,7 @@ function (_, sup, moment, mainString,
});
var table = $('#classic_experiments_content .tablesorter')
.tablesorter({
theme : 'green',
theme : 'blue',
});
};
var xmlthing = sup.CallServerMethod(null,
......@@ -205,7 +213,7 @@ function (_, sup, moment, mainString,
var table = $('#profiles_table')
.tablesorter({
theme : 'green',
theme : 'blue',
widgets: ["filter"],
widgetOptions: {
// include child row content while filtering, if true
......@@ -258,7 +266,7 @@ function (_, sup, moment, mainString,
});
var table = $('#classic_profiles_content .tablesorter')
.tablesorter({
theme : 'green',
theme : 'blue',
});
};
var xmlthing = sup.CallServerMethod(null,
......@@ -305,7 +313,10 @@ function (_, sup, moment, mainString,
$('#members_content')
.html(template({"members" : json.value,
"nonmembers" : {},
"pid" : window.TARGET_PROJECT,
"gid" : window.TARGET_PROJECT,
"canedit" : window.CANAPPROVE,
"canapprove" : window.CANAPPROVE}));
// Format dates with moment before display.
......@@ -315,10 +326,64 @@ function (_, sup, moment, mainString,
$(this).html(moment($(this).html()).format("ll"));
}
});
// Bind approve/deny buttons.
$('#members_table .approveuser')
.click(function () {
DoApproval($(this).data("uid"), "approve");
});
$('#members_table .denyuser')
.click(function () {
DoApproval($(this).data("uid"), "deny");
});
// Bind edit privs selection
$('#members_table .editprivs')
.on('focusin', function() {
// Remember trust before change.
$(this).data('val', $(this).val());
})
.change(function () {
if ($(this).val() == "user" && !WarnedAboutUserPrivs) {
sup.ShowModal('#confirm-user-privs-modal');
WarnedAboutUserPrivs = true;
var which = $(this);
$('#cancel-user-privs').click(function () {
// Restore old trust we saved above.
$(which).val($(which).data('val'));
});
$('#confirm-user-privs').click(function () {
DoEditPrivs($(which).data("uid"), $(which).val());
});
return;
}
DoEditPrivs($(this).data("uid"), $(this).val());
});
var table = $('#members_table')
.tablesorter({
theme : 'green',
theme : 'blue',
});
// Do this after converting table.
$('[data-toggle="tooltip"]').tooltip({
trigger: 'hover',
placement: 'auto',
});
// Do this after converting table.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
placement: 'auto',
});
// Enable the remove button when users are selected.
$('#members_table .subgroup-checkbox').change(function () {
$('#subgroup-delete-button').removeAttr("disabled");
});
// Handler for the remove button.
$('#confirm-remove-users').click(function () {
sup.HideModal('#confirm-remove-users-modal');
DoRemoveUsers();
});
}
var xmlthing = sup.CallServerMethod(null,
"show-project", "MemberList",
......@@ -326,6 +391,119 @@ function (_, sup, moment, mainString,
xmlthing.done(callback);
}
// Approve or Deny.
function DoApproval(uid, action)
{
console.info(uid, action);
var callback = function(json) {
sup.HideWaitWait();
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
LoadMembersTab();
}
sup.ShowWaitWait("We are approving (or denying) ... patience please");
var xmlthing =
sup.CallServerMethod(null, "approveuser", action,
{"user_uid" : uid,
"pid" : window.TARGET_PROJECT});
xmlthing.done(callback);
}
// Edit privs
function DoEditPrivs(uid, priv)
{
console.info(uid, priv);
var callback = function(json) {
sup.HideWaitWait();
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
LoadMembersTab();
}
sup.ShowWaitWait("We are modifying privs ... patience please");
var xmlthing =
sup.CallServerMethod(null, "groups", "EditPrivs",
{"user_uid" : uid,
"priv" : priv,
"pid" : window.TARGET_PROJECT,
"gid" : window.TARGET_PROJECT});
xmlthing.done(callback);
}
// Remove users.
function DoRemoveUsers()
{
// Find list of selected users.
var selected_users = {};
$('.remove-checkbox').each(function () {
if ($(this).is(":checked")) {
var uid = $(this).data("uid");
selected_users[uid] = uid;
}
});
if (! Object.keys(selected_users).length) {
return;
}