Commit 03c2107c authored by Leigh Stoller's avatar Leigh Stoller

Changes our ssh key/account handling in RedeemTicket() and

CreateSliver(), to handle multiple accounts.  This somewhat reflects
the Geni AM API for keys, which allows the client to specify multiple
users, each with a set of ssh keys.

The keys argument to the CM now looks like the following (note that
the old format is still accepted and will be for a while).

[{'urn'   => 'urn:blabla'
  'login' => 'dopey',
  'keys'  => [ list of keys like before ]},
 {'login' => "leebee",
  'keys'  => [ list of keys ... ]}];

Key Points:

1. You can supply a urn or a login or both. Typically, it is going to
   be the result of getkeys() at the PG SA, and so it will include
   both.

2. If a login is provided, use that. Otherwise use the id from the urn.

3. No matter what, verify that the token is valid for Emulab an uid
   (standard 8 char unix login that is good on just about any unix
   variant), and transform it if not.

4. For now, getkeys() at the SA will continue to return the old format
   (unless you supply version=2 argument) since we do not want to
   default to a keylist that most CMs will barf on.

5. I have modified the AM code to transform the Geni AM version of the
   "users" argument into the above structure. Bottom line here, is
   that users of the AM interface will not actually need to do
   anything, although now multiple users are actually supported
   instead of ignored.

Still to be done are the changes to the login services structure in
the manifest. We have yet to settle on what these changes will look
like, but since people generally supply valid login ids, you probably
will not need this, since no transformation will take place.
parent 940ecc7b
......@@ -4280,10 +4280,92 @@ sub UnBindNonLocalUsers($)
my $idx = $self->idx();
DBQueryWarn("delete from nonlocal_user_bindings ".
"where exptidx='$idx'")
#
# Need to delete the pubkeys, so need a list of current bindings.
#
my $query_result =
DBQueryWarn("select uid,uid_idx from nonlocal_user_accounts ".
"where exptidx='$idx'");
return -1
if (!$query_result);
while (my ($uid, $uid_idx) = $query_result->fetchrow_array()) {
DBQueryWarn("delete from nonlocal_user_pubkeys ".
"where uid_idx='$uid_idx'")
or return -1;
DBQueryWarn("delete from nonlocal_user_accounts ".
"where uid_idx='$uid_idx'")
or return -1;
}
return 0;
}
#
# Bind nonlocal user to experiment (slice, in Geni).
#
sub BindNonLocalUser($$$$$$)
{
my ($self, $keys, $uid, $urn, $name, $email) = @_;
return -1
if (! ref($self));
my $exptidx = $self->idx();
my $safe_urn = DBQuoteSpecial($urn)
if (defined($urn));
my $safe_uid = DBQuoteSpecial($uid);
my $safe_name = DBQuoteSpecial($name);
my $safe_email = DBQuoteSpecial($email);
my $uid_idx;
#
# User may already exist, as for updating keys.
#
my $query_result =
DBQueryWarn("select uid_idx from nonlocal_user_accounts ".
"where uid=$safe_uid and exptidx='$exptidx'");
return -1
if (!$query_result);
if ($query_result->numrows) {
($uid_idx) = $query_result->fetchrow_array();
}
else {
my @insert_data = ();
$uid_idx = User->NextIDX();
push(@insert_data, "created=now()");
push(@insert_data, "uid_idx='$uid_idx'");
push(@insert_data, "unix_uid=NULL");
push(@insert_data, "exptidx='$exptidx'");
push(@insert_data, "urn=$safe_urn")
if (defined($urn));
push(@insert_data, "uid=$safe_uid");
push(@insert_data, "name=$safe_name");
push(@insert_data, "email=$safe_email");
push(@insert_data, "uid_uuid=uuid()");
# Insert into DB.
my $insert_result =
DBQueryWarn("insert into nonlocal_user_accounts set " .
join(",", @insert_data));
}
#
# Always replace the entire key set; easier to manage.
#
DBQueryWarn("delete from nonlocal_user_pubkeys ".
"where uid_idx='$uid_idx'")
or return -1;
foreach my $key (@{ $keys }) {
my $safe_key = DBQuoteSpecial($key);
DBQueryWarn("insert into nonlocal_user_pubkeys set ".
" uid=$safe_uid, uid_idx='$uid_idx', ".
" idx=NULL, stamp=now(), pubkey=$safe_key")
or return -1;
}
return 0;
}
......
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
# Copyright (c) 2005-2010 University of Utah and the Flux Group.
# Copyright (c) 2005-2011 University of Utah and the Flux Group.
# All rights reserved.
#
package User;
......@@ -542,7 +542,7 @@ sub Delete($)
or return -1;
DBQueryWarn("delete from users where uid_idx='$uid_idx'")
or return -1;
return 0;
}
......@@ -1631,243 +1631,6 @@ sub escapeshellarg($)
return $1;
}
#############################################################################
# Non-local users, as for federation/geni.
#
package User::NonLocal;
use emdb;
use User;
use libtestbed;
use English;
use overload ('""' => 'Stringify');
# Cache of instances to avoid regenerating them.
my %nonlocal_users = ();
#
# Lookup by idx.
#
sub Lookup($$)
{
my ($class, $token) = @_;
my $idx;
my $query_result;
if ($token =~ /^\d+$/) {
$idx = $token;
}
elsif ($token =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) {
$query_result =
DBQueryWarn("select uid_idx from nonlocal_users ".
"where uid_uuid='$token'");
return undef
if (! $query_result || !$query_result->numrows);
($idx) = $query_result->fetchrow_array();
}
else {
return undef;
}
# Look in cache first
return $nonlocal_users{"$idx"}
if (exists($nonlocal_users{"$idx"}));
$query_result =
DBQueryWarn("select * from nonlocal_users where uid_idx='$idx'");
return undef
if (!$query_result || !$query_result->numrows);
my $self = {};
$self->{'USER'} = $query_result->fetchrow_hashref();
# Want to know if this is a shadow of a local user.
$self->{'SHADOW'} = User->Lookup($idx);
bless($self, $class);
# Add to cache.
$nonlocal_users{$self->{'USER'}->{'uid_idx'}} = $self;
return $self;
}
# accessors
sub field($$) { return ((! ref($_[0])) ? -1 : $_[0]->{'USER'}->{$_[1]}); }
sub uid_idx($) { return field($_[0], "uid_idx"); }
sub idx($) { return field($_[0], "uid_idx"); }
sub uid($) { return field($_[0], "uid"); }
sub uid_uuid($) { return field($_[0], "uid_uuid"); }
sub created($) { return field($_[0], "created"); }
sub name($) { return field($_[0], "name"); }
sub email($) { return field($_[0], "email"); }
sub shadow($) { return $_[0]->{'SHADOW'}; }
#
# Stringify for output.
#
sub Stringify($)
{
my ($self) = @_;
my $uid = $self->uid();
my $idx = $self->idx();
return "[NonLocalUser: $uid, IDX: $idx]";
}
#
# Class function to create a new nonlocal user and return object.
#
sub Create($$$$$$;$)
{
my ($class, $idx, $uid, $uuid, $name, $email, $sshkeys) = @_;
my @insert_data = ();
my $islocal = 0;
# Every user gets a new unique index.
if (!defined($idx) || $idx == 0) {
$idx = User->NextIDX();
}
else {
# Sanity check.
my $existing = User->Lookup($idx);
if (defined($existing)) {
if ($uuid eq $existing->uuid()) {
#
# Shadow a local user, strictly for tmcd.
#
$islocal = 1;
}
else {
print STDERR "NonLocal user with $idx exists: $existing\n";
return undef;
}
}
}
# Now tack on other stuff we need.
push(@insert_data, "created=now()");
push(@insert_data, "uid_idx='$idx'");
my $safe_uid = DBQuoteSpecial($uid);
my $safe_name = DBQuoteSpecial($name);
my $safe_email = DBQuoteSpecial($email);
my $safe_uuid = DBQuoteSpecial($uuid);
push(@insert_data, "uid=$safe_uid");
push(@insert_data, "name=$safe_name");
push(@insert_data, "email=$safe_email");
push(@insert_data, "uid_uuid=$safe_uuid");
if (defined($sshkeys)) {
foreach my $sshkey (@{ $sshkeys }) {
my $safe_sshkey = DBQuoteSpecial($sshkey);
DBQueryWarn("insert into nonlocal_user_pubkeys set ".
" uid=$safe_uid, uid_idx='$idx', ".
" idx=NULL, stamp=now(), pubkey=$safe_sshkey")
or return undef;
}
}
# Insert into DB.
if (!DBQueryWarn("insert into nonlocal_users set " .
join(",", @insert_data))) {
DBQueryWarn("delete from nonlocal_user_pubkeys where uid_idx='$idx'")
if (!$islocal);
return undef;
}
return User::NonLocal->Lookup($idx);
}
#
# Modify the keys.
#
sub ModifyKeys($$)
{
my ($self, $sshkeys) = @_;
my $idx = $self->uid_idx();
my $uid = $self->uid();
DBQueryWarn("delete from nonlocal_user_pubkeys where uid_idx='$idx'")
or return -1;
if (defined($sshkeys)) {
foreach my $sshkey (@{ $sshkeys }) {
my $safe_sshkey = DBQuoteSpecial($sshkey);
DBQueryWarn("insert into nonlocal_user_pubkeys set ".
" uid='$uid', uid_idx='$idx', ".
" idx=NULL, stamp=now(), pubkey=$safe_sshkey")
or return -1;
}
}
return 0;
}
#
# Delete the user, as for registration errors.
#
sub Delete($)
{
my ($self) = @_;
return 0
if (! ref($self));
my $idx = $self->idx();
DBQueryWarn("delete from nonlocal_user_pubkeys where uid_idx='$idx'")
or return -1;
DBQueryWarn("delete from nonlocal_user_bindings where uid_idx='$idx'")
or return -1;
DBQueryWarn("delete from nonlocal_users where uid_idx='$idx'")
or return -1;
return 0;
}
#
# Bind nonlocal user to experiment (slice, in Geni).
#
sub BindToExperiment($$)
{
my ($self, $experiment) = @_;
return -1
if (! ref($self));
my $uid = $self->uid();
my $idx = $self->idx();
my $exptidx = $experiment->idx();
# Insert into DB.
DBQueryWarn("replace into nonlocal_user_bindings set " .
" uid='$uid', uid_idx='$idx', exptidx='$exptidx'")
or return -1;
return 0;
}
sub UnBindFromExperiment($$)
{
my ($self, $experiment) = @_;
return -1
if (! ref($self));
my $uid = $self->uid();
my $idx = $self->idx();
my $exptidx = $experiment->idx();
# Insert into DB.
DBQueryWarn("delete from nonlocal_user_bindings " .
"where uid_idx='$idx' and exptidx='$exptidx'")
or return -1;
return 0;
}
# _Always_ make sure that this 1 is at the end of the file...
1;
#
# Add perl Digest-SHA1 port.
#
use strict;
use libinstall;
use emdbi;
sub InstallUpdate($$)
{
my ($version, $phase) = @_;
if ($phase eq "pre") {
Phase "p5-Digest-SHA1", "Checking for port p5-Digest-SHA1", sub {
DoneIfPackageInstalled("p5-Digest-SHA1");
ExecQuietFatal("cd $PORTSDIR/security/p5-Digest-SHA1; ".
"make MASTER_SITE_FREEBSD=1 -DBATCH install");
};
}
return 0;
}
1;
......@@ -28,6 +28,7 @@ use emutil;
use Compress::Zlib;
use MIME::Base64;
use XML::LibXML;
use Data::Dumper;
# Disable UUID checks in GeniCredential.
$GeniCredential::CHECK_UUID = 0;
......@@ -240,35 +241,25 @@ sub CreateSliver()
auto_add_sa($cred);
}
# circulate through the users and bind them to the slice
my $caller_urn = $ENV{'GENIURN'};
my $caller_keys = undef;
foreach my $user (@$users) {
my $user_urn = $user->{'urn'};
# Skip if it is not the caller
next if ($user_urn ne $caller_urn);
$caller_keys = $user->{keys};
}
# Package the caller_keys in a list of hashes the way the CM wants
# it. Each hash has two keys ('type' and 'key'). 'type' is always
# 'ssh' for us, and 'key' is the key.
my $sliver_keys = undef;
if (defined($caller_keys)) {
$sliver_keys = [];
foreach my $key (@$caller_keys) {
# The CMV2 does not like newlines at the end of the keys.
chomp($key);
push(@$sliver_keys, {'type' => 'ssh', 'key' => $key});
}
if (@$users) {
$sliver_keys = [];
foreach my $user (@$users) {
my $user_urn = $user->{'urn'};
my @user_keys = ();
foreach my $key (@{ $user->{keys} }) {
# The CMV2 does not like newlines at the end of the keys.
chomp($key);
push(@user_keys, {'type' => 'ssh', 'key' => $key});
}
push(@{$sliver_keys}, {'urn' => $user_urn,
'keys' => \@user_keys});
}
}
# This only gets us part of the way there. For users that are not
# *this* user, we need to somehow add their keys, which also
# requires adding them as a user *without* their certificate. Not
# sure we'll be able to do that.
# Invoke CreateSliver
my $create_args = {
'slice_urn' => $slice_urn,
......@@ -276,7 +267,6 @@ sub CreateSliver()
'credentials' => $credentials,
'keys' => $sliver_keys
};
my $response = GeniCMV2::CreateSliver($create_args);
if (!ref($response)) {
# This is cause GeniCMV2::CreateSliver does a fork, and the child
......
......@@ -442,7 +442,6 @@ sub Register($)
if ($type eq "user") {
my $name = $info->{'name'};
my $email = $info->{'email'};
my $keys = undef;
if (! TBcheck_dbslot($name, "users", "usr_name",
TBDB_CHECKDBSLOT_ERROR)) {
......@@ -474,9 +473,9 @@ sub Register($)
"Not allowed to change hrn");
}
#
# Update operation, but only name, email, and keys for now.
# Update operation, but only name, email
#
if ($existing->Modify($name, $email, $keys) != 0) {
if ($existing->Modify($name, $email) != 0) {
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Could not update user");
}
......@@ -502,8 +501,7 @@ sub Register($)
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"user already registered");
}
my $newuser = GeniUser->Create($certificate, $slice_authority,
$info, $keys);
my $newuser = GeniUser->Create($certificate, $slice_authority, $info);
if (!defined($newuser)) {
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Could not be registered");
......
......@@ -40,6 +40,7 @@ use emutil;
use EmulabConstants;
use libEmulab;
use Lan;
use Experiment;
use English;
use Data::Dumper;
use XML::Simple;
......@@ -49,6 +50,7 @@ use Time::Local;
use Compress::Zlib;
use File::Temp qw(tempfile);
use MIME::Base64;
use Digest::SHA1 qw(sha1_hex);
# Configure variables
my $TB = "@prefix@";
......@@ -1945,21 +1947,6 @@ sub SliverWorkAux($$$$$$$)
"Slice has expired");
}
#
# Create the user.
#
my $owner = CreateUserFromCertificate($owner_cert);
return $owner
if (GeniResponse::IsResponse($owner));
if (defined($keys)) {
$response = CheckKeys($keys);
return $response
if (GeniResponse::IsResponse($response));
$owner->Modify(undef, undef, $keys);
}
my $experiment = GeniExperiment($slice);
if (!defined($experiment)) {
$slice->UnLock();
......@@ -1971,6 +1958,23 @@ sub SliverWorkAux($$$$$$$)
my $pid = $experiment->pid();
my $eid = $experiment->eid();
#
# Create the user.
#
my $owner = CreateUserFromCertificate($owner_cert);
return $owner
if (GeniResponse::IsResponse($owner));
if (defined($keys)) {
$response = AddKeys($slice, $owner, $keys);
if (GeniResponse::IsResponse($response)) {
$slice->UnLock();
$ticket->UnLock()
if (defined($ticket));
return $response;
}
}
#
# Figure out what nodes to allocate or free.
#
......@@ -2184,16 +2188,6 @@ sub SliverWorkAux($$$$$$$)
}
}
#
# Create an emulab nonlocal user for tmcd.
#
if ($owner->BindToSlice($slice)) {
$message = "Error binding user to slice";
print STDERR "$message\n";
goto bad;
}
#
# We are actually an Aggregate, so return an aggregate of slivers,
# even if there is just one node. This makes sliceupdate easier.
......@@ -3564,13 +3558,7 @@ sub BindToSlice($)
my $user = CreateUserFromCertificate($credential->owner_cert());
return $user
if (GeniResponse::IsResponse($user));
if (defined($keys)) {
my $response = CheckKeys($keys);
return $response
if (GeniResponse::IsResponse($response));
$user->Modify(undef, undef, $keys);
}
if ($slice->Lock() != 0) {
return GeniResponse->BusyResponse();
}
......@@ -3580,11 +3568,12 @@ sub BindToSlice($)
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Error binding slice to user");
}
# Bind for existing slivers.
if ($user->BindToSlice($slice) != 0) {
$slice->UnLock();
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Error binding user to slice");
if (defined($keys)) {
my $response = AddKeys($slice, $user, $keys);
if (GeniResponse::IsResponse($response)) {
$slice->UnLock();
return $response;
}
}
$slice->UnLock();
return GeniResponse->Create(GENIRESPONSE_SUCCESS);
......@@ -4229,7 +4218,7 @@ sub CreateUserFromCertificate($)
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Could not create user from your certificate")
if (!defined($user));
return $user;
}
......@@ -4562,7 +4551,6 @@ sub GeniExperiment($)
my $gid = $pid;
my $hrn = $slice->hrn();
my $urn = $slice->urn();
require Experiment;
my $experiment = Experiment->Lookup($uuid);
return $experiment
......@@ -4799,27 +4787,120 @@ sub CheckTicket($)
return $ticket;
}
sub CheckKeys($)
sub AddKeys($$$)
{
my ($keys) = @_;
my ($slice, $owner, $keys) = @_;
goto bad
if (!ref($keys));
foreach my $keyref (@{ $keys }) {
goto bad
if (!ref($keyref));
my $key = $keyref->{'key'};
my $type = $keyref->{'type'};
if (! (ref($keys) && ref($keys) eq "ARRAY"));
goto bad
if (! (defined($key) && defined($type) &&
my $slice_experiment = $slice->GetExperiment();
if (!$slice_experiment) {
print STDERR "AddKeys: No experiment for $slice\n";
return GeniResponse->Create(GENIRESPONSE_ERROR);
}
my $keychecker = sub {
my ($arg) = @_;
return -1
if (! (ref($arg) && ref($arg) eq "HASH"));
my $key = $arg->{'key'};
my $type = $arg->{'type'};
return -1
if (! (defined($arg) && defined($arg) &&
$key ne "" && $type ne ""));
}
return 0;
};
#
# The old format (which we still want to support) was
# a single list of keys for the user redeeming a ticket.
#
# The new format is a list of users, each with sshkeys.
# The hash key is the urn of the user.
#
if (exists($keys->[0]->{'urn'}) || exists($keys->[0]->{'login'})) {
foreach my $ref (@{ $keys }) {
goto bad
if (! (exists($ref->{'urn'}) || exists($ref->{'login'})));
my @keylist = ();
foreach my $keyref (@{ $ref->{'keys'} }) {
goto bad
if (&$keychecker($keyref) != 0);
push(@keylist, $keyref->{'key'});
}
my $urn = $ref->{'urn'} if (exists($ref->{'urn'}));
# Allow user to override urn token.
my $uid = $ref->{'login'} if (exists($ref->{'login'}));
my $name;
my $email;
# The slice owner is easy.
if (defined($urn) && $urn eq $owner->urn()) {
$name = $owner->name();
$email = $owner->email();
$uid = $owner->uid()
if (!defined($uid));
}
else {
#
# Trickier, since we have no email and no name, and
# we might have to derive the login uid from the urn, which
# might not even be a valid uid for Emulab.
#
if (!defined($uid)) {
my (undef,$type,$id) = GeniHRN::Parse($urn);
if (!defined($id) || $type ne "user") {
goto bad;
}
$uid = $id;
}
$name = $uid;
$email = "root\@localhost";
}
if (! GeniUser->ValidUserID($uid)) {
my $digest = sha1_hex($uid);
$digest = substr($digest, 0, 7);
$uid = lc("u${digest}");
if (GeniUser->ValidUserID($uid)) {
print STDERR "Cannot form a uid for $name\n";
goto bad;
}
}
$slice_experiment->BindNonLocalUser(\@keylist,
$uid, $urn,
$name, $email)
== 0 or goto error;
}
}
else {
my @keylist = ();
foreach my $keyref (@{ $keys }) {
goto bad
if (&$keychecker($keyref) != 0);
push(@keylist, $keyref->{'key'});
}
$slice_experiment->BindNonLocalUser(\@keylist,
$owner->uid(),
$owner->urn(),
$owner->name(),
$owner->email()) == 0
or goto error;
}
return 0;
bad:
return GeniResponse->Create(GENIRESPONSE_BADARGS, undef, "Malformed keys")
return GeniResponse->Create(GENIRESPONSE_BADARGS, undef, "Malformed keys");
error:
return GeniResponse->Create(GENIRESPONSE_ERROR, undef, "Internal Error");
}
# _Always_ make sure that this 1 is at the end of the file...
......
......@@ -1427,9 +1427,6 @@ sub BindToSlice($)
return $user
if (GeniResponse::IsResponse($user));
if (!$user->IsLocal() && defined($keys)) {
$user->Modify(undef, undef, $keys);
}
if ($slice->Lock() != 0) {
return GeniResponse->BusyResponse();
}
......@@ -1439,11 +1436,12 @@ sub BindToSlice($)
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Error binding slice to user");
}
# Bind for existing slivers.
if ($user->BindToSlice($slice) != 0) {
$slice->UnLock();
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Error binding user to slice");
if (defined($keys)) {
my $response = GeniCM::AddKeys($slice, $user, $keys);
if (GeniResponse::IsResponse($response)) {
$slice->UnLock();
return $response;
}
}