#!/usr/bin/perl -wT # # EMULAB-COPYRIGHT # Copyright (c) 2008-2009 University of Utah and the Flux Group. # All rights reserved. # package GeniTicket; # # Some simple ticket stuff. # use strict; use Exporter; use vars qw(@ISA @EXPORT); @ISA = "Exporter"; @EXPORT = qw (TICKET_PURGED TICKET_EXPIRED TICKET_REDEEMED TICKET_RELEASED); use GeniDB; use GeniCredential; use GeniCertificate; use emutil qw(TBGetUniqueIndex); use GeniUtil; use English; use XML::Simple; use XML::LibXML; use Data::Dumper; use Date::Parse; use POSIX qw(strftime); use Time::Local; use File::Temp qw(tempfile); use overload ('""' => 'Stringify'); # Configure variables my $TB = "@prefix@"; my $TBOPS = "@TBOPSEMAIL@"; my $TBAPPROVAL = "@TBAPPROVALEMAIL@"; my $TBAUDIT = "@TBAUDITEMAIL@"; my $BOSSNODE = "@BOSSNODE@"; my $OURDOMAIN = "@OURDOMAIN@"; my $SIGNCRED = "$TB/sbin/signgenicred"; my $VERIFYCRED = "$TB/sbin/verifygenicred"; my $NFREE = "$TB/bin/nfree"; my $CMCERT = "$TB/etc/genicm.pem"; # Ticket release flags sub TICKET_PURGED() { return 1; } sub TICKET_REDEEMED() { return 2; } sub TICKET_EXPIRED() { return 3; } sub TICKET_RELEASED() { return 4; } # Cache of tickets. my %tickets = (); BEGIN { use GeniUtil; GeniUtil::AddCache(\%tickets); } # Do not load this for the Clearinghouse XML server. BEGIN { if (! defined($main::GENI_ISCLRHOUSE)) { require Experiment; } } # # Lookup by local idx. # sub Lookup($$) { my ($class, $idx) = @_; return undef if (! ($idx =~ /^\d*$/)); return $tickets{"$idx"} if (exists($tickets{"$idx"})); my $query_result = DBQueryWarn("select * from geni_tickets where idx='$idx'"); return undef if (!defined($query_result) || !$query_result->numrows); my $row = $query_result->fetchrow_hashref(); # Map the component my $component; if ($row->{'component_uuid'}) { $component = GeniComponent->Lookup($row->{'component_uuid'}); return undef if (!defined($component)); } my $ticket = GeniTicket->CreateFromSignedTicket($row->{'ticket_string'}, $component, 1); return undef if (!defined($ticket)); # We ignore this in the ticket. In fact, we need to change how we bring # tickets back into the system. $ticket->{'redeem_before'} = $row->{'redeem_before'}; # Mark as coming from the DB. $ticket->{'idx'} = $idx; $ticket->{'stored'} = 1; # Cache. $tickets{"$idx"} = $ticket; return $ticket; } # # Lookup a ticket for a slice. This assumes only a single slice ticket. # sub LookupForSlice($$) { my ($class, $slice) = @_; return undef if (! ref($slice)); my $slice_uuid = $slice->uuid(); my $query_result = DBQueryWarn("select idx from geni_tickets ". "where slice_uuid='$slice_uuid'"); return undef if (!$query_result || !$query_result->numrows); if ($query_result->numrows != 1) { print STDERR "Too many tickets stored for $slice\n"; return undef; } my ($idx) = $query_result->fetchrow_array(); return GeniTicket->Lookup($idx); } # # Create an unsigned ticket object, to be populated and signed and returned. # sub Create($$$$) { my ($class, $slice, $owner, $rspec) = @_; # Every Ticket gets a new unique index (sequence number). my $seqno = TBGetUniqueIndex('next_ticket', 1); my $self = {}; $self->{'rspec'} = $rspec; $self->{'ticket_uuid'} = undef; $self->{'slice_uuid'} = $slice->uuid(); $self->{'owner_uuid'} = $owner->uuid(); $self->{'slice_hrn'} = $slice->hrn(); $self->{'owner_hrn'} = $owner->hrn(); $self->{'slice_cert'} = $slice->GetCertificate(); $self->{'owner_cert'} = $owner->GetCertificate(); $self->{'seqno'} = $seqno; $self->{'ticket_string'} = undef; $self->{'component'} = undef; $self->{'stored'} = 0; # Stored to the DB. # # For now, all tickets expire very quickly ... # $self->{'redeem_before'} = POSIX::strftime("20%y-%m-%dT%H:%M:%S", localtime(time() + (5*60))); # # Locally generated tickets need a local DB index, which can be the # same as the sequence number. A ticket from a remote component will # have it own seqno, and so we will generate a locally valid idx for # those when when(if) we store them in the DB. # $self->{'idx'} = $seqno; bless($self, $class); return $self; } # accessors sub field($$) { return ($_[0]->{$_[1]}); } sub idx($) { return field($_[0], "idx"); } sub seqno($) { return field($_[0], "seqno"); } sub rspec($) { return field($_[0], "rspec"); } sub slice_uuid($) { return field($_[0], "slice_uuid"); } sub target_uuid($) { return field($_[0], "slice_uuid"); } sub owner_uuid($) { return field($_[0], "owner_uuid"); } sub slice_hrn($) { return field($_[0], "slice_hrn"); } sub target_hrn($) { return field($_[0], "slice_hrn"); } sub owner_hrn($) { return field($_[0], "owner_hrn"); } sub slice_cert($) { return field($_[0], "slice_cert"); } sub owner_cert($) { return field($_[0], "owner_cert"); } sub uuid($) { return field($_[0], "ticket_uuid"); } sub ticket_uuid($) { return field($_[0], "ticket_uuid"); } sub ticket($) { return field($_[0], "ticket"); } sub asString($) { return field($_[0], "ticket_string"); } sub ticket_string($) { return field($_[0], "ticket_string"); } sub redeem_before($) { return field($_[0], "redeem_before"); } sub component_uuid($) { return field($_[0], "component_uuid"); } sub component($) { return field($_[0], "component"); } sub stored($) { return field($_[0], "stored"); } # # Stringify for output. # sub Stringify($) { my ($self) = @_; my $idx = $self->idx(); if (!defined($idx)) { my $seqno = $self->seqno(); $idx = "S$seqno"; } my $slice_uuid = $self->slice_uuid(); return "[GeniTicket: $idx, slice:$slice_uuid]"; } # # Flush from our little cache, as for the expire daemon. # sub Flush($) { my ($self) = @_; delete($tickets{$self->idx()}); } # # Create a ticket object from a signed ticket string. # sub CreateFromSignedTicket($$;$$) { my ($class, $ticket_string, $component, $nosig) = @_; # # This flag is used to avoid verifying the signature since I do not # really care if the component gives me a bad ticket; I am not using # it locally, just passing it back to the component at some point. # $nosig = 0 if (!defined($nosig)); if (! $nosig) { my ($fh, $filename) = tempfile(UNLINK => 0); return undef if (!defined($fh)); print $fh $ticket_string; close($fh); system("$VERIFYCRED $filename"); if ($?) { print STDERR "Ticket in $filename did not verify\n"; return undef; } unlink($filename); } # Use XML::Simple to convert to something we can mess with. my $parser = XML::LibXML->new; my $doc; eval { $doc = $parser->parse_string($ticket_string); }; if ($@) { print STDERR "Failed to parse ticket string: $@\n"; return undef; } # Dig out the rspec. my ($rspec_node) = $doc->getElementsByTagName("rspec"); return undef if (!defined($rspec_node)); my $rspec = XMLin($rspec_node->toString(), ForceArray => ["node", "link"]); # Dig out the ticket uuid. my ($uuid_node) = $doc->getElementsByTagName("uuid"); return undef if (!defined($uuid_node)); my $ticket_uuid = $uuid_node->to_literal(); if (! ($ticket_uuid =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) { print STDERR "Invalid uuid in ticket\n"; return undef; } # Dig out the target certificate. my ($cert_node) = $doc->getElementsByTagName("target_gid"); return undef if (!defined($cert_node)); my $target_certificate = GeniCertificate->LoadFromString($cert_node->to_literal()); return undef if (!defined($target_certificate)); if (!($target_certificate->uuid() =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) { print STDERR "Invalid target_uuid in credential\n"; return undef; } if (!($target_certificate->hrn() =~ /^[\w\.]+$/)) { print STDERR "Invalid hrn in credential\n"; return undef; } # Dig out the owner certificate. ($cert_node) = $doc->getElementsByTagName("owner_gid"); return undef if (!defined($cert_node)); my $owner_certificate = GeniCertificate->LoadFromString($cert_node->to_literal()); return undef if (!defined($owner_certificate)); if (!($owner_certificate->uuid() =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) { print STDERR "Invalid target_uuid in credential\n"; return undef; } if (!($owner_certificate->hrn() =~ /^[\w\.]+$/)) { print STDERR "Invalid hrn in credential\n"; return undef; } # Sequence number my ($seqno_node) = $doc->getElementsByTagName("serial"); return undef if (!defined($seqno_node)); my $seqno = $seqno_node->to_literal(); if (! ($seqno =~ /^\w+$/)) { print STDERR "Invalid sequence number in ticket\n"; return undef; } my $self = {}; $self->{'idx'} = undef; $self->{'rspec'} = $rspec; $self->{'ticket_uuid'} = $ticket_uuid; $self->{'slice_uuid'} = $target_certificate->uuid(); $self->{'owner_uuid'} = $owner_certificate->uuid(); $self->{'slice_hrn'} = $target_certificate->hrn(); $self->{'owner_hrn'} = $owner_certificate->hrn(); $self->{'slice_cert'} = $target_certificate; $self->{'owner_cert'} = $owner_certificate; $self->{'ticket_string'} = $ticket_string; $self->{'xmlref'} = $doc; $self->{'component'} = $component; $self->{'seqno'} = $seqno; $self->{'stored'} = 0; # # We save copies of the tickets we hand out; lets find that record # in the DB, just to verify. # if (! $nosig) { my $query_result = DBQueryWarn("select * from geni_tickets where idx='$seqno'"); if (!$query_result || !$query_result->numrows) { print STDERR "Could not find the ticket $seqno in the DB\n"; return undef; } my $row = $query_result->fetchrow_hashref(); $self->{'redeem_before'} = $row->{'redeem_before'}; $self->{'idx'} = $seqno; $self->{'stored'} = 1; } bless($self, $class); return $self; } # # Might have to delete this from the DB, as with an error handing out # a ticket. # sub Delete($$) { my ($self, $flag) = @_; return -1 if (! ref($self)); if ($self->stored()) { my $idx = $self->idx(); my $uuid = $self->ticket_uuid(); DBQueryWarn("delete from geni_tickets where idx='$idx'") or return -1; if ($flag == TICKET_PURGED) { GeniUsage->DeleteTicket($self) == 0 or print STDERR "GeniTicket::Delete: ". "GeniUsage->DeleteTicket($self) failed\n"; } elsif ($flag == TICKET_REDEEMED) { GeniUsage->RedeemTicket($self) == 0 or print STDERR "GeniTicket::Delete: ". "GeniUsage->RedeemTicket($self) failed\n"; } elsif ($flag == TICKET_RELEASED) { GeniUsage->ReleaseTicket($self) == 0 or print STDERR "GeniTicket::Delete: ". "GeniUsage->ReleaseTicket($self) failed\n"; } elsif ($flag == TICKET_EXPIRED) { GeniUsage->ExpireTicket($self) == 0 or print STDERR "GeniTicket::Delete: ". "GeniUsage->ExpireTicket($self) failed\n"; } delete($tickets{"$idx"}); } return 0; } # # Return the rspec in XML for the ticket. # sub rspecXML($) { my ($self) = @_; return undef if (! ref($self)); return undef if (!defined($self->rspec())); my $rspec_xml = XMLout($self->rspec(), "NoAttr" => 1); $rspec_xml =~ s/opt\>/rspec\>/g; return $rspec_xml; } # # Populate the ticket with some stuff, which right now is just the # number of node we are willing to grant. # sub Grant($$) { my ($self, $count) = @_; return -1 if (! ref($self)); $self->{'count'} = $count; return 0; } # # Store the given ticket in the DB. We only do this for signed tickets, # so we have a record of them. We store them on the server and the client # side. # sub Store($) { my ($self) = @_; my @insert_data = (); my $idx = $self->idx(); my $seqno = $self->seqno(); my $slice_uuid = $self->slice_uuid(); my $owner_uuid = $self->owner_uuid(); my $ticket_uuid= $self->ticket_uuid(); my $expires = $self->redeem_before(); # # For a locally created/signed ticket, seqno=idx. For a ticket from # another component, we have to generate a locally unique idx for # the DB insertion. # if (!defined($idx)) { $idx = TBGetUniqueIndex('next_ticket', 1); $self->{'idx'} = $idx; } # A locally generated ticket will not have a component. Might change that. push(@insert_data, "component_uuid='" . $self->component()->uuid() . "'") if (defined($self->component())); # Now tack on other stuff we need. push(@insert_data, "created=now()"); push(@insert_data, "idx='$idx'"); push(@insert_data, "seqno='$seqno'"); push(@insert_data, "ticket_uuid='$ticket_uuid'"); push(@insert_data, "slice_uuid='$slice_uuid'"); push(@insert_data, "owner_uuid='$owner_uuid'"); push(@insert_data, "redeem_before='$expires'"); my $safe_ticket = DBQuoteSpecial($self->ticket_string()); push(@insert_data, "ticket_string=$safe_ticket"); # Insert into DB. DBQueryWarn("insert into geni_tickets set " . join(",", @insert_data)) or return -1; if (GeniUsage->NewTicket($self)) { print STDERR "GeniTicket::Store: GeniUsage->NewTicket($self) failed\n"; } $tickets{"$idx"} = $self; $self->{'stored'} = 1; return 0; } # # Sign the ticket before returning it. We capture the output, which is # in XML. # sub Sign($) { my ($self) = @_; return -1 if (!ref($self)); my $idx = $self->seqno(); my $expires = $self->redeem_before(); my $slice_cert = $self->slice_cert()->cert(); my $owner_cert = $self->owner_cert()->cert(); my $rspec_xml = XMLout($self->rspec(), "NoAttr" => 1); $rspec_xml =~ s/opt\>/rspec\>/g; # # Every ticket/credential gets its own uuid. # my $ticket_uuid = NewUUID(); $self->{'ticket_uuid'} = $ticket_uuid; # Convert to GMT. $expires = POSIX::strftime("20%y-%m-%dT%H:%M:%S", gmtime(str2time($expires))); # # Create a template xml file to sign. # my $template = "\n". "\n". " ticket\n". " $idx\n". " $owner_cert\n". " $slice_cert\n". " $ticket_uuid\n". " $expires\n". " \n". " 1\n". " $expires\n". " $rspec_xml\n". " \n". "\n"; my ($fh, $filename) = tempfile(UNLINK => 0); return -1 if (!defined($fh)); print $fh $template; close($fh); # # Fire up the signer and capture the output. This is the signed ticket # that is returned. # if (! open(SIGNER, "$SIGNCRED -c $CMCERT $filename |")) { print STDERR "Could not sign $filename\n"; return -1; } my $ticket = ""; while () { $ticket .= $_; } if (!close(SIGNER)) { print STDERR "Could not sign $filename\n"; return -1; } $self->{'ticket_string'} = $ticket; $self->Store() == 0 or return -1; unlink($filename); return 0; } # # Get the experiment for the slice this sliver belongs to. # sub GetExperiment($) { my ($self) = @_; return undef if (! ref($self)); return Experiment->Lookup($self->slice_uuid()); } # # Look up a list of tickets for a locally instantiated slice. # Used by the CM. # sub SliceTickets($$$) { my ($class, $slice, $pref) = @_; my $slice_uuid = $slice->uuid(); my @result = (); my $query_result = DBQueryWarn("select idx from geni_tickets ". "where slice_uuid='$slice_uuid'"); return -1 if (!$query_result); while (my ($idx) = $query_result->fetchrow_array()) { my $ticket = GeniTicket->Lookup($idx); return -1 if (!defined($ticket)); push(@result, $ticket); } @$pref = @result; return 0; } # # Release a ticket. Need to release the nodes ... # Used by the CM. # sub Release($$) { my ($self, $flag) = @_; return -1 if (! ref($self)); my $experiment = Experiment->Lookup($self->slice_uuid()); if (!defined($experiment)) { # # Cannot be any nodes if no experiment. # return $self->Delete($flag); } my $pid = $experiment->pid(); my $eid = $experiment->eid(); my @nodeids = (); my @nodes = (); foreach my $ref (@{$self->rspec()->{'node'}}) { my $resource_uuid = $ref->{'uuid'}; my $node = Node->Lookup($resource_uuid); next if (!defined($node)); my $reservation = $node->Reservation(); next if (!defined($reservation)); if ($reservation->SameExperiment($experiment)) { push(@nodeids, $node->node_id()); push(@nodes, $node); } } if (@nodeids) { system("export NORELOAD=1; $NFREE -x -q $pid $eid @nodeids"); } foreach my $node (@nodes) { $node->Refresh(); } $self->Delete($flag); return 0; } # # Equality test for two tickets. # sub SameTicket($$) { my ($self, $other) = @_; # Must be a real reference. return 0 if (! (ref($self) && ref($other))); return $self->idx() == $other->idx(); } # # Check if ticket has expired. Use the DB directly. # sub Expired($) { my ($self) = @_; my $idx = $self->idx(); my $query_result = DBQueryWarn("select idx from geni_tickets ". "where idx='$idx' and ". " (UNIX_TIMESTAMP(now()) > ". " UNIX_TIMESTAMP(redeem_before))"); return $query_result->numrows; } # _Always_ make sure that this 1 is at the end of the file... 1;