Commit 9c216565 authored by Leigh Stoller's avatar Leigh Stoller

Extend Powder license code to support "usage" licenses; licenses tied to

specific node, node types, or aggregates. Before we submit the
experiment to the target clusters, process the rspec looking for
anything that needs an accepted license, break the flow by throwing up a
modal that tells the user they need to request permission to use the
resources. This sends email to operations.

Later ... after email discussion with user, need to run a CLI command on
the boss that requires the user to accept those licenses.

	boss> wap manage_licenses require <license name> <uid>

Next page load by the user will throw them into the license accept
page.

Now we just need some licenses ....
parent ccc44c9f
......@@ -48,12 +48,15 @@ use emutil;
use WebTask;
use emdb;
use APT_Dataset;
use APT_Aggregate;
use GeniXML;
use GeniHRN;
use libtestbed;
use Project;
use Lease;
use Image;
use Node;
use NodeType;
use English;
use Data::Dumper;
use File::Basename;
......@@ -1448,6 +1451,186 @@ sub NeedStitcher($$)
return 0;
}
#
# Check licenses. Runs after SetSites().
#
sub CheckLicenses($$$$$)
{
my ($rspecstr, $user, $project, $plicenses, $perrmsg) = @_;
my $uid_idx = $user->uid_idx();
my %licenses = ();
my $rspec = GeniXML::Parse($rspecstr);
if (! defined($rspec)) {
$$perrmsg = "Could not parse rspec\n";
return -1;
}
my $isAccepted = sub ($) {
my ($license_idx) = @_;
my $query_result =
DBQueryWarn("select * from user_licenses ".
"where uid_idx='$uid_idx' and ".
" license_idx='$license_idx'");
if (!$query_result) {
$$perrmsg = "Internal DB Error";
return -1;
}
if ($query_result->numrows) {
my $row = $query_result->fetchrow_hashref();
#
# We should not be here if the license is not accepted,
# since the Web UI would have forced the user to accept it.
#
if (!$row->{"accepted"}) {
$$perrmsg = "License $license_idx was not accepted";
return -1;
}
# Okay to proceed
return 1;
}
# License is not accepted.
return 0;
};
foreach my $ref (GeniXML::FindNodes("n:node", $rspec)->get_nodelist()) {
my $client_id = GetVirtualId($ref);
my $manager_urn = GetManagerId($ref);
my $aptagg = APT_Aggregate->Lookup($manager_urn);
print STDERR "CheckLicenses: $client_id, $manager_urn\n";
#
# Check for aggregate restriction,
#
if ($aptagg && $aptagg->required_license()) {
my $license_idx = $aptagg->required_license();
my $accepted = &$isAccepted($license_idx);
if ($accepted < 0) {
return -1;
}
if (!$accepted) {
#
# Not allowed to proceed, tell Web UI to throw up dialog
# asking user if they want to request access.
#
if (!exists($licenses{"$license_idx"})) {
$licenses{"$license_idx"} = {
"type" => "aggregate",
"target" => $aptagg->urn(),
"license" => $aptagg->required_license(),
};
}
}
}
#
# Check for node/type restrictions when its our own URN.
#
if ($manager_urn eq $MYURN) {
my $hardtype = undef;
my $component_id = GeniXML::GetNodeId($ref);
if ($component_id && $component_id ne "*") {
print STDERR "CheckLicenses: $client_id, $component_id\n";
my $hrn = GeniHRN->new($component_id);
if (! ($hrn && $hrn->IsNode())) {
$$perrmsg = "Not a valid component ID: $component_id";
return 1;
}
my $node = Node->Lookup($hrn->id());
if ($node) {
my $idx;
if ($node->NodeAttribute("required_license",
\$idx) == 0 && $idx) {
my $accepted = &$isAccepted($idx);
if ($accepted < 0) {
return -1;
}
if (!$accepted) {
#
# Not allowed to proceed, tell Web UI to
# throw up dialog asking user if they want
# to request access.
#
if (!exists($licenses{"$idx"})) {
$licenses{"$idx"} = {
"type" => "node",
"target" => $hrn->id(),
"license" => $idx,
};
}
}
}
else {
# Set this so we check for a type restriction below,
# since user probably did not set a hardware type.
$hardtype = $node->type();
}
}
}
if (!$hardtype) {
$hardtype = GeniXML::FindFirst("n:hardware_type", $ref);
if ($hardtype) {
$hardtype = GeniXML::GetText("name", $hardtype);
}
}
if ($hardtype) {
print STDERR "CheckLicenses: $client_id, $hardtype\n";
my $nodetype = NodeType->Lookup($hardtype);
if (! $nodetype) {
$$perrmsg = "Not a valid hardware type: $hardtype";
return 1;
}
if ($nodetype->required_license()) {
my $idx = $nodetype->required_license();
my $accepted = &$isAccepted($idx);
if ($accepted < 0) {
return -1;
}
if (!$accepted) {
#
# Not allowed to proceed, tell Web UI to throw
# up dialog asking user if they want to
# request access.
#
if (!exists($licenses{"$idx"})) {
$licenses{"$idx"} = {
"type" => "type",
"target" => $hardtype,
"license" => $idx,
};
}
}
}
}
}
}
#
# Lookup additonal details to return to the Web UI.
#
foreach my $idx (keys(%licenses)) {
my $details = $licenses{"$idx"};
my $query_result =
DBQueryWarn("select * from licenses where license_idx='$idx'");
if (!$query_result || !$query_result->numrows) {
$$perrmsg = "Could not look up details for license $idx";
return -1;
}
my $row = $query_result->fetchrow_hashref();
$details->{"license_name"} = $row->{"license_name"};
$details->{"description_text"} = $row->{"description_text"};
$details->{"description_type"} = $row->{"description_type"};
}
my @licenses = values(%licenses);
$$plicenses = \@licenses;
return scalar(@licenses);
}
#
# Set the repository for the rspec. This is a top level element. At
# some point we can think about per-node repos.
......
......@@ -695,6 +695,27 @@ if (defined($profile->repourl())) {
}
}
#
# Check for nodes/types/aggregates that require a License.
#
my $licenses;
$tmp = APT_Profile::CheckLicenses($rspecstr, $geniuser->emulab_user(), $project,
\$licenses, \$errmsg);
if ($tmp) {
if ($tmp < 0) {
fatal("Could not determine license requirements: $errmsg");
}
# Special handling.
if ($licenses) {
if (defined($webtask)) {
$webtask->required_licenses($licenses);
}
UserError("Licenses are required");
}
UserError($errmsg);
}
#
# Now we know where to send to logs.
#
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
# Copyright (c) 2000-2019 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -33,7 +33,7 @@ use Date::Parse;
sub usage()
{
print STDERR "Usage: manage_licenses add [-h] ...\n";
print STDERR "Usage: manage_licenses modify [-h] ...\n";
print STDERR " manage_licenses modify [-h] ...\n";
print STDERR " manage_licenses delete ...\n";
print STDERR " manage_licenses require ...\n";
print STDERR " manage_licenses norequire ...\n";
......@@ -153,14 +153,18 @@ exit(0);
#
sub AddLicense()
{
my $optlist = "hu";
my ($license_type, $description_type);
my $optlist = "hus:";
my $license_level = "project";
my $license_target = "usage";
my ($license_type, $description_type, $prompt);
my $usage = sub {
print STDERR "Usage: add <name> [-u] <form prompt> <license.[md,txt,html]> ";
print STDERR "Usage: add [-u] [-s <form prompt>] <name> <license.[md,txt,html]> ";
print STDERR " [description.[md,txt,html]]\n";
print STDERR " -u - Users must agree; default ".
"is project leader only\n";
print STDERR " -s prompt - Create a signup license instead of ".
"a usage license\n";
print STDERR " form prompt - Query for the form ".
"('Do you need MathLab')\n";
print STDERR " name - A descriptive token\n";
......@@ -178,25 +182,26 @@ sub AddLicense()
&$usage();
}
if (defined($options{"u"})) {
fatal("The -u option is not implemented yet");
$license_level = "user";
}
if (defined($options{"s"})) {
$license_target = "signup";
$prompt = $options{"s"};
}
&$usage()
if (@ARGV < 3);
if (@ARGV < 2);
my $token = shift(@ARGV);
my $prompt = shift(@ARGV);
my $lfile = shift(@ARGV);
my $dfile = shift(@ARGV) if (@ARGV);
if ($token !~ /^\w+$/) {
fatal("Invalid characters in name, alphanumeric only please");
}
if (! TBcheck_dbslot($prompt, "default",
"tinytext", TBDB_CHECKDBSLOT_ERROR)) {
fatal("Invalid prompt: " . TBFieldErrorString());
}
if ($lfile =~ /\.((md|txt|html))$/) {
$license_type = $1;
$license_type = "text"
if ($license_type eq "txt");
}
else {
fatal("License file extension must be one of .md, .txt, or .html");
......@@ -207,6 +212,8 @@ sub AddLicense()
if (defined($dfile)) {
if ($dfile =~ /\.((md|txt|html))$/) {
$description_type = $1;
$description_type = "text"
if ($description_type eq "txt");
}
else {
fatal("Description file extension must be one of ".
......@@ -225,8 +232,13 @@ sub AddLicense()
or fatal("Could not open $dfile: $!");
$safe_description= DBQuoteSpecial($description);
}
$safe_prompt = DBQuoteSpecial($prompt);
if ($license_target eq "signup") {
if (! TBcheck_dbslot($prompt, "default",
"tinytext", TBDB_CHECKDBSLOT_ERROR)) {
fatal("Invalid prompt: " . TBFieldErrorString());
}
$safe_prompt = DBQuoteSpecial($prompt);
}
my $query_result =
DBQueryFatal("select license_idx from licenses ".
"where license_name='$token'");
......@@ -237,8 +249,11 @@ sub AddLicense()
$query_result =
DBQueryFatal("insert into licenses set created=now(), ".
" license_level='$license_level', ".
" license_target='$license_target', ".
" license_name='$token', license_text=$safe_license, ".
" license_type='$license_type',form_text=$safe_prompt " .
" license_type='$license_type' " .
(defined($prompt) ? ", form_text=$safe_prompt" : "").
(defined($dfile) ?
", description_text=$safe_description, ".
" description_type='$description_type'" : ""));
......@@ -274,17 +289,19 @@ sub ListLicenses()
DBQueryFatal("select * from licenses order by license_idx");
if ($query_result->numrows) {
printf("%-5s %-12s %s\n", "Index", "Name", "Level");
printf("------------------------\n");
printf("%-5s %-12s %-10s %s\n", "Index", "Name", "Level", "Target");
printf("-----------------------------------\n");
while (my $row = $query_result->fetchrow_hashref()) {
my $license_idx = $row->{'license_idx'};
my $license_name = $row->{'license_name'};
my $license_level = $row->{'license_level'};
my $license_target= $row->{'license_target'};
my $license_text = $row->{'license_text'};
printf("%-5d %-12s %-10s %s\n", $license_idx, $license_name,
$license_level, substr($license_text, 0, 50));
printf("%-5d %-12s %-10s %-10s %s\n", $license_idx, $license_name,
$license_level, $license_target,
substr($license_text, 0, 40));
}
}
exit(0);
......@@ -325,8 +342,10 @@ sub ShowLicense()
print "IDX: " . $row->{'license_idx'} . "\n";
print "Name: " . $row->{'license_name'} . "\n";
print "Level: " . $row->{'license_level'} . "\n";
print "Target: " . $row->{'license_target'} . "\n";
print "Created: " . $row->{'created'} . "\n";
print "Prompt: " . $row->{'form_text'} . "\n";
print "Prompt: " . $row->{'form_text'} . "\n"
if ($row->{'license_target'} eq "signup");
if ($row->{'description_text'}) {
print "Description:\n";
print " Text Type: " . $row->{'description_type'} . "\n";
......@@ -443,7 +462,28 @@ sub RequireLicense($)
}
}
else {
fatal("User level licenses are not supported yet")
my $user = User->Lookup($token);
if (!defined($user)) {
fatal("No such user");
}
my $uid = $user->uid();
my $uid_idx = $user->uid_idx();
if ($op eq "norequire") {
DBQueryFatal("delete from user_licenses ".
"where license_idx='$license_idx' and ".
" uid_idx='$uid_idx'");
}
elsif ($op eq "accept") {
DBQueryFatal("update user_licenses set accepted=now() ".
"where license_idx='$license_idx' and ".
" uid_idx='$uid_idx'");
}
else {
DBQueryFatal("insert into user_licenses set ".
" license_idx='$license_idx', uid='$uid', ".
" uid_idx='$uid_idx'");
}
}
if (defined($webtask)) {
$webtask->Exited(0);
......@@ -534,7 +574,33 @@ sub OutstandingLicenses($)
}
}
elsif (defined($options{"u"})) {
fatal("The -u option is not implemented yet");
my $user = User->Lookup($token);
if (!defined($user)) {
fatal("No such user");
}
my $uid = $user->uid();
my $uid_idx = $user->uid_idx();
my $query_result =
DBQueryFatal("select ul.* from user_licenses as ul ".
"where ul.uid_idx='$uid_idx' and ".
($op eq "accepted" ?
" ul.accepted is not null" : " ul.accepted is null"));
if ($query_result->numrows) {
printf("%-5s %s\n", "Index", "Name");
printf("------------------------\n");
while (my $row = $query_result->fetchrow_hashref()) {
my $license_idx = $row->{'license_idx'};
my $lrow = LookupLicense($license_idx);
my $license_name = $lrow->{'license_name'};
my $license_text = $lrow->{'license_text'};
printf("%-5d %-12s %s\n", $license_idx, $license_name,
substr($license_text, 0, 60));
}
}
}
exit(0);
}
......
......@@ -447,6 +447,9 @@ sub isfakenode($;$) {
sub isblackbox($;$) {
return GetAttribute($_[0], "blackbox", $_[1]);
}
sub required_license($;$) {
return GetAttribute($_[0], "required_license", $_[1]);
}
sub control_interface($;$) { return control_iface($_[0], $_[1]); }
#
......
......@@ -144,6 +144,7 @@ CREATE TABLE `apt_aggregates` (
`panicpoweroff` tinyint(1) NOT NULL default '0',
`portals` set('emulab','aptlab','cloudlab','phantomnet','powder') default NULL,
`canuse_feature` varchar(64) default NULL,
`required_license int(11) default NULL,
`jsondata` text,
PRIMARY KEY (`urn`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
......@@ -3027,6 +3028,7 @@ CREATE TABLE `licenses` (
`license_idx` int(11) NOT NULL auto_increment,
`license_name` varchar(48) NOT NULL default '',
`license_level` enum('project','user') NOT NULL default 'project',
`license_target` enum('signup','usage') NOT NULL default 'signup',
`created` datetime default NULL,
`validfor` int(11) NOT NULL default '0',
`form_text` tinytext,
......
use strict;
use libdb;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if (!DBSlotExists("apt_aggregates", "required_license")) {
DBQueryFatal("alter table apt_aggregates add ".
" `required_license` int(11) default NULL ".
" after canuse_feature");
}
if (!DBSlotExists("licenses", "license_target")) {
DBQueryFatal("alter table licenses add ".
" `license_target` enum('signup','usage') NOT NULL ".
" default 'signup' after license_level");
}
return 0;
}
1;
# Local Variables:
# mode:perl
# End:
......@@ -372,6 +372,20 @@ class Aggregate
}
return $aggregate;
}
#
# List of types available at this aggregate. For now we just want
# the type names.
#
function TypeList()
{
$result = array();
foreach ($this->typeinfo as $type => $info) {
$result[$type] = $type;
}
return $result;
}
}
#
......
......@@ -1239,7 +1239,8 @@ function CalculateAggregateStatus(&$amlist, &$fedlist, &$status,
if ($extended) {
$amlist[$urn] = array("urn" => $urn,
"name" => $am,
"nickname" => $aggregate->nickname());
"nickname" => $aggregate->nickname(),
"typelist" => $aggregate->TypeList());
}
else {
$amlist[$urn] = $am;
......@@ -1262,7 +1263,8 @@ function CalculateAggregateStatus(&$amlist, &$fedlist, &$status,
if ($extended) {
$amlist[$urn] = array("urn" => $urn,
"name" => $am,
"nickname" => $aggregate->nickname());
"nickname" => $aggregate->nickname(),
"typelist" => $aggregate->TypeList());
}
else {
$amlist[$urn] = $am;
......
......@@ -442,8 +442,7 @@ function Do_GetImageInfo()
}
#
# Allow for checking at each step, although at the moment we
# do notreally do this.
# Allow for checking at each step.
#
function Do_CheckForm()
{
......@@ -1124,11 +1123,18 @@ function Do_Submit()
Instance::Instantiate($uuid, $this_user, $options, $args, $webtask);
if (!$instance) {
# Create error is handled differently.
SPITAJAX_ERROR(3, $webtask->output());
if (isset($keyname)) {
unlink($keyname);
}
# Create error is handled differently.
$licenses = $webtask->TaskValue("required_licenses");
if ($licenses) {
SPITAJAX_ERROR(3, $licenses);
$_SESSION["licenses"] = $licenses;
}
else {
SPITAJAX_ERROR(1, $webtask->output());
}
$webtask->Delete();
return;
}
......@@ -1163,6 +1169,53 @@ function Do_Submit()
return;
}
#
# Send license email.
#
function Do_RequestLicenses()
{
global $this_user;
global $ajax_args;
session_start();
$email = $this_user->email();
$name = $this_user->name();
$uid = $this_user->uid();
$adminemail = $this_user->adminEmail();
$licenses = json_decode(json_encode($_SESSION["licenses"]), true);
$body = "";
foreach ($licenses as $idx => $details) {
$lname = $details["license_name"];
$type = $details["type"];
$target = $details["target"];
$body .= "License ${lname}: ";
if ($type == "node") {
$body .= "Node $target";
}
elseif ($type == "type") {
$body .= "Node type $target";
}
elseif ($type == "aggregate") {
$body .= "Location $target";
}
$body .= "\n\n";
}
TBMAIL($adminemail,
"$name ($uid) is requesting access to restricted resources",
"$name ($uid) is requesting access to restricted resources:\n\n".
$body .
"Operations staff will contact you if more information is needed.\n".
"You will receive email when permission to use these resources\n".
"has been granted.\n",
"From: $adminemail\n".
"CC: $name <$email>");
SPITAJAX_RESPONSE(1);
}
#
# Mark (or clear) a profile as a favorite.
#
......
......@@ -950,17 +950,20 @@ $(function ()
return;
}
$("#waitwait-modal").modal('show');
SubmitForm(0, 2, function(json) {
$("#waitwait-modal").modal('hide');
SubmitForm(0, 3, function(json) {
if (json.code) {
console.info(json);
if (json.code == 2) {
ShowFormErrors(json.value);
if (json.code == 3) {
submitted = false;
sup.HideWaitWait(function () {
HandleLicenseRequirements(json.value);
})
return;
}
sup.SpitOops("oops", json.value);
submitted = false;
sup.HideWaitWait(function () {
sup.SpitOops("oops", json.value);
});
return;
}
/*
......@@ -2594,5 +2597,48 @@ $(function ()
}
}
}
/*
* Handle License requirements.
*/
function HandleLicenseRequirements(licenses)
{
var html = "";
_.each(licenses, function (details) {
var dt = null;
if (details.type == "node") {
dt = "Node " + details.target;
}
else if (details.type == "type") {
dt = "Node Type " + details.target;
}
else if (details.type == "aggregate") {
dt = "Resource " + details.target;
}
html = html +
"<dt>" + dt + "</dt>" +
"<dd><pre>" + details.description_text + "</pre></dd>";
});
$('#request-licenses-modal dl').html(html);
$('#request-license-button').click(function (event) {
sup.HideModal('#request-licenses-modal');
sup.CallServerMethod(null, "instantiate", "RequestLicenses", null,
function (json) {
if (json.code) {
alert("Could not request resource " +
"access: " + json.value);
return;
}
window.location
.replace("licenses-pending.php");
});
});
sup.ShowModal('#request-licenses-modal', function () {
$('#request-license-button').off("click");
});
}
$(document).ready(initialize);
});
......@@ -69,7 +69,8 @@ $(function ()
html = license.description_text;
}
else if (license.description_type == "text") {
html = "<pre>" + license.description_text + "</pre>";
html = "<textarea style='width: 100%;' rows=8>" +
license.description_text + "</textarea>";
}
$('#description-panel .license')
.html(html)
......@@ -84,7 +85,8 @@ $(function ()
html = license.license_text;
}
else if (license.license_type == "text") {
html = "<pre>" + license.license_text + "</pre>";
html = "<textarea style='width: 100%;' rows=20>" +
license.license_text + "</textarea>";
}
$('#license-panel .panel-body .license-text').html(html);
}
......
<?php
#
# Copyright (c) 2000-2019 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