diff options
| author | Russ Allbery <rra@stanford.edu> | 2013-03-27 15:19:54 -0700 | 
|---|---|---|
| committer | Russ Allbery <rra@stanford.edu> | 2013-03-27 15:19:54 -0700 | 
| commit | 5df16adc5024c56e3d733741919954308b4d498a (patch) | |
| tree | 5f042adaaa988478ca271f41f9b272ef5a1b45b5 /perl/Wallet/Object | |
| parent | 431c3b56a52b9fe3135ab4339bada13ed49bda92 (diff) | |
| parent | 6871bae8e26beadaff5035de56b4f70a78961dc9 (diff) | |
Merge tag 'upstream/1.0' into debian
Upstream version 1.0
Diffstat (limited to 'perl/Wallet/Object')
| -rw-r--r-- | perl/Wallet/Object/Base.pm | 378 | ||||
| -rw-r--r-- | perl/Wallet/Object/File.pm | 5 | ||||
| -rw-r--r-- | perl/Wallet/Object/Keytab.pm | 137 | ||||
| -rw-r--r-- | perl/Wallet/Object/WAKeyring.pm | 370 | 
4 files changed, 665 insertions, 225 deletions
| diff --git a/perl/Wallet/Object/Base.pm b/perl/Wallet/Object/Base.pm index 5097729..dd128cc 100644 --- a/perl/Wallet/Object/Base.pm +++ b/perl/Wallet/Object/Base.pm @@ -1,7 +1,8 @@  # Wallet::Object::Base -- Parent class for any object stored in the wallet.  #  # Written by Russ Allbery <rra@stanford.edu> -# Copyright 2007, 2008, 2010 Board of Trustees, Leland Stanford Jr. University +# Copyright 2007, 2008, 2010, 2011 +#     The Board of Trustees of the Leland Stanford Junior University  #  # See LICENSE for licensing terms. @@ -17,12 +18,13 @@ use vars qw($VERSION);  use DBI;  use POSIX qw(strftime); +use Text::Wrap qw(wrap);  use Wallet::ACL;  # This version should be increased on any code change to this module.  Always  # use two digits for the minor version with a leading zero if necessary so  # that it will sort properly. -$VERSION = '0.05'; +$VERSION = '0.06';  ##############################################################################  # Constructors @@ -34,15 +36,16 @@ $VERSION = '0.05';  # type in the object.  If the object doesn't exist, returns undef.  This will  # probably be usable as-is by most object types.  sub new { -    my ($class, $type, $name, $dbh) = @_; -    my $sql = 'select ob_name from objects where ob_type = ? and ob_name = ?'; -    my $data = $dbh->selectrow_array ($sql, undef, $type, $name); -    $dbh->commit; -    die "cannot find ${type}:${name}\n" unless ($data and $data eq $name); +    my ($class, $type, $name, $schema) = @_; +    my %search = (ob_type => $type, +                  ob_name => $name); +    my $object = $schema->resultset('Object')->find (\%search); +    die "cannot find ${type}:${name}\n" +        unless ($object and $object->ob_name eq $name);      my $self = { -        dbh  => $dbh, -        name => $name, -        type => $type, +        schema => $schema, +        name   => $name, +        type   => $type,      };      bless ($self, $class);      return $self; @@ -53,28 +56,37 @@ sub new {  # specified class.  Stores the database handle to use, the name, and the type  # in the object.  Subclasses may need to override this to do additional setup.  sub create { -    my ($class, $type, $name, $dbh, $user, $host, $time) = @_; +    my ($class, $type, $name, $schema, $user, $host, $time) = @_;      $time ||= time;      die "invalid object type\n" unless $type;      die "invalid object name\n" unless $name; +    my $guard = $schema->txn_scope_guard;      eval { -        my $date = strftime ('%Y-%m-%d %T', localtime $time); -        my $sql = 'insert into objects (ob_type, ob_name, ob_created_by, -            ob_created_from, ob_created_on) values (?, ?, ?, ?, ?)'; -        $dbh->do ($sql, undef, $type, $name, $user, $host, $date); -        $sql = "insert into object_history (oh_type, oh_name, oh_action, -            oh_by, oh_from, oh_on) values (?, ?, 'create', ?, ?, ?)"; -        $dbh->do ($sql, undef, $type, $name, $user, $host, $date); -        $dbh->commit; +        my %record = (ob_type         => $type, +                      ob_name         => $name, +                      ob_created_by   => $user, +                      ob_created_from => $host, +                      ob_created_on   => strftime ('%Y-%m-%d %T', +                                                   localtime $time)); +        $schema->resultset('Object')->create (\%record); + +        %record = (oh_type   => $type, +                   oh_name   => $name, +                   oh_action => 'create', +                   oh_by     => $user, +                   oh_from   => $host, +                   oh_on     => strftime ('%Y-%m-%d %T', localtime $time)); +        $schema->resultset('ObjectHistory')->create (\%record); + +        $guard->commit;      };      if ($@) { -        $dbh->rollback;          die "cannot create object ${type}:${name}: $@\n";      }      my $self = { -        dbh  => $dbh, -        name => $name, -        type => $type, +        schema => $schema, +        name   => $name, +        type   => $type,      };      bless ($self, $class);      return $self; @@ -124,30 +136,36 @@ sub log_action {      # We have two traces to record, one in the object_history table and one in      # the object record itself.  Commit both changes as a transaction.  We      # assume that AutoCommit is turned off. +    my $guard = $self->{schema}->txn_scope_guard;      eval { -        my $date = strftime ('%Y-%m-%d %T', localtime $time); -        my $sql = 'insert into object_history (oh_type, oh_name, oh_action, -            oh_by, oh_from, oh_on) values (?, ?, ?, ?, ?, ?)'; -        $self->{dbh}->do ($sql, undef, $self->{type}, $self->{name}, $action, -                          $user, $host, $date); +        my %record = (oh_type   => $self->{type}, +                      oh_name   => $self->{name}, +                      oh_action => $action, +                      oh_by     => $user, +                      oh_from   => $host, +                      oh_on     => strftime ('%Y-%m-%d %T', localtime $time)); +        $self->{schema}->resultset('ObjectHistory')->create (\%record); + +        my %search = (ob_type   => $self->{type}, +                      ob_name   => $self->{name}); +        my $object = $self->{schema}->resultset('Object')->find (\%search);          if ($action eq 'get') { -            $sql = 'update objects set ob_downloaded_by = ?, -                ob_downloaded_from = ?, ob_downloaded_on = ? where -                ob_type = ? and ob_name = ?'; -            $self->{dbh}->do ($sql, undef, $user, $host, $date, $self->{type}, -                              $self->{name}); +            $object->ob_downloaded_by   ($user); +            $object->ob_downloaded_from ($host); +            $object->ob_downloaded_on   (strftime ('%Y-%m-%d %T', +                                                   localtime $time));          } elsif ($action eq 'store') { -            $sql = 'update objects set ob_stored_by = ?, ob_stored_from = ?, -                ob_stored_on = ? where ob_type = ? and ob_name = ?'; -            $self->{dbh}->do ($sql, undef, $user, $host, $date, $self->{type}, -                              $self->{name}); +            $object->ob_stored_by   ($user); +            $object->ob_stored_from ($host); +            $object->ob_stored_on   (strftime ('%Y-%m-%d %T', +                                               localtime $time));          } -        $self->{dbh}->commit; +        $object->update; +        $guard->commit;      };      if ($@) {          my $id = $self->{type} . ':' . $self->{name};          $self->error ("cannot update history for $id: $@"); -        $self->{dbh}->rollback;          return;      }      return 1; @@ -169,16 +187,22 @@ sub log_set {      }      my %fields = map { $_ => 1 }          qw(owner acl_get acl_store acl_show acl_destroy acl_flags expires -           flags type_data); +           comment flags type_data);      unless ($fields{$field}) {          die "invalid history field $field";      } -    my $date = strftime ('%Y-%m-%d %T', localtime $time); -    my $sql = "insert into object_history (oh_type, oh_name, oh_action, -        oh_field, oh_type_field, oh_old, oh_new, oh_by, oh_from, oh_on) -        values (?, ?, 'set', ?, ?, ?, ?, ?, ?, ?)"; -    $self->{dbh}->do ($sql, undef, $self->{type}, $self->{name}, $field, -                      $type_field, $old, $new, $user, $host, $date); + +    my %record = (oh_type       => $self->{type}, +                  oh_name       => $self->{name}, +                  oh_action     => 'set', +                  oh_field      => $field, +                  oh_type_field => $type_field, +                  oh_old        => $old, +                  oh_new        => $new, +                  oh_by         => $user, +                  oh_from       => $host, +                  oh_on         => strftime ('%Y-%m-%d %T', localtime $time)); +    $self->{schema}->resultset('ObjectHistory')->create (\%record);  }  ############################################################################## @@ -200,20 +224,21 @@ sub _set_internal {          $self->error ("cannot modify ${type}:${name}: object is locked");          return;      } + +    my $guard = $self->{schema}->txn_scope_guard;      eval { -        my $sql = "select ob_$attr from objects where ob_type = ? and -            ob_name = ?"; -        my $old = $self->{dbh}->selectrow_array ($sql, undef, $type, $name); -        $sql = "update objects set ob_$attr = ? where ob_type = ? and -            ob_name = ?"; -        $self->{dbh}->do ($sql, undef, $value, $type, $name); +        my %search = (ob_type => $type, +                      ob_name => $name); +        my $object = $self->{schema}->resultset('Object')->find (\%search); +        my $old = $object->get_column ("ob_$attr"); + +        $object->update ({ "ob_$attr" => $value });          $self->log_set ($attr, $old, $value, $user, $host, $time); -        $self->{dbh}->commit; +        $guard->commit;      };      if ($@) {          my $id = $self->{type} . ':' . $self->{name};          $self->error ("cannot set $attr on $id: $@"); -        $self->{dbh}->rollback;          return;      }      return 1; @@ -234,14 +259,13 @@ sub _get_internal {      my $type = $self->{type};      my $value;      eval { -        my $sql = "select $attr from objects where ob_type = ? and -            ob_name = ?"; -        $value = $self->{dbh}->selectrow_array ($sql, undef, $type, $name); -        $self->{dbh}->commit; +        my %search = (ob_type => $type, +                      ob_name => $name); +        my $object = $self->{schema}->resultset('Object')->find (\%search); +        $value = $object->get_column ($attr);      };      if ($@) {          $self->error ($@); -        $self->{dbh}->rollback;          return;      }      return $value; @@ -258,7 +282,7 @@ sub acl {      my $attr = "acl_$type";      if ($id) {          my $acl; -        eval { $acl = Wallet::ACL->new ($id, $self->{dbh}) }; +        eval { $acl = Wallet::ACL->new ($id, $self->{schema}) };          if ($@) {              $self->error ($@);              return; @@ -291,6 +315,19 @@ sub attr_show {      return '';  } +# Get or set the comment value of an object.  If setting it, trace information +# must also be provided. +sub comment { +    my ($self, $comment, $user, $host, $time) = @_; +    if ($comment) { +        return $self->_set_internal ('comment', $comment, $user, $host, $time); +    } elsif (defined $comment) { +        return $self->_set_internal ('comment', undef, $user, $host, $time); +    } else { +        return $self->_get_internal ('comment'); +    } +} +  # Get or set the expires value of an object.  Expects an expiration time in  # seconds since epoch.  If setting the expiration, trace information must also  # be provided. @@ -315,7 +352,7 @@ sub owner {      my ($self, $owner, $user, $host, $time) = @_;      if ($owner) {          my $acl; -        eval { $acl = Wallet::ACL->new ($owner, $self->{dbh}) }; +        eval { $acl = Wallet::ACL->new ($owner, $self->{schema}) };          if ($@) {              $self->error ($@);              return; @@ -338,17 +375,21 @@ sub flag_check {      my ($self, $flag) = @_;      my $name = $self->{name};      my $type = $self->{type}; -    my $dbh = $self->{dbh}; +    my $schema = $self->{schema};      my $value;      eval { -        my $sql = 'select fl_flag from flags where fl_type = ? and fl_name = ? -            and fl_flag = ?'; -        $value = $dbh->selectrow_array ($sql, undef, $type, $name, $flag); -        $dbh->commit; +        my %search = (fl_type => $type, +                      fl_name => $name, +                      fl_flag => $flag); +        my $flag = $schema->resultset('Flag')->find (\%search); +        if (not defined $flag) { +            $value = 0; +        } else { +            $value = $flag->fl_flag; +        }      };      if ($@) {          $self->error ("cannot check flag $flag for ${type}:${name}: $@"); -        $dbh->rollback;          return;      } else {          return ($value) ? 1 : 0; @@ -362,23 +403,22 @@ sub flag_clear {      $time ||= time;      my $name = $self->{name};      my $type = $self->{type}; -    my $dbh = $self->{dbh}; +    my $schema = $self->{schema}; +    my $guard = $schema->txn_scope_guard;      eval { -        my $sql = 'select * from flags where fl_type = ? and fl_name = ? and -            fl_flag = ?'; -        my ($data) = $dbh->selectrow_array ($sql, undef, $type, $name, $flag); -        unless (defined $data) { +        my %search = (fl_type => $type, +                      fl_name => $name, +                      fl_flag => $flag); +        my $flag = $schema->resultset('Flag')->find (\%search); +        unless (defined $flag) {              die "flag not set\n";          } -        $sql = 'delete from flags where fl_type = ? and fl_name = ? and -            fl_flag = ?'; -        $dbh->do ($sql, undef, $type, $name, $flag); -        $self->log_set ('flags', $flag, undef, $user, $host, $time); -        $dbh->commit; +        $flag->delete; +        $self->log_set ('flags', $flag->fl_flag, undef, $user, $host, $time); +        $guard->commit;      };      if ($@) {          $self->error ("cannot clear flag $flag on ${type}:${name}: $@"); -        $dbh->rollback;          return;      }      return 1; @@ -392,20 +432,18 @@ sub flag_list {      undef $self->{error};      my @flags;      eval { -        my $sql = 'select fl_flag from flags where fl_type = ? and -            fl_name = ? order by fl_flag'; -        my $sth = $self->{dbh}->prepare ($sql); -        $sth->execute ($self->{type}, $self->{name}); -        my $flag; -        while (defined ($flag = $sth->fetchrow_array)) { -            push (@flags, $flag); +        my %search = (fl_type => $self->{type}, +                      fl_name => $self->{name}); +        my %attrs  = (order_by => 'fl_flag'); +        my @flags_rs = $self->{schema}->resultset('Flag')->search (\%search, +                                                                   \%attrs); +        for my $flag (@flags_rs) { +            push (@flags, $flag->fl_flag);          } -        $self->{dbh}->commit;      };      if ($@) {          my $id = $self->{type} . ':' . $self->{name};          $self->error ("cannot retrieve flags for $id: $@"); -        $self->{dbh}->rollback;          return;      } else {          return @flags; @@ -419,23 +457,22 @@ sub flag_set {      $time ||= time;      my $name = $self->{name};      my $type = $self->{type}; -    my $dbh = $self->{dbh}; +    my $schema = $self->{schema}; +    my $guard = $schema->txn_scope_guard;      eval { -        my $sql = 'select * from flags where fl_type = ? and fl_name = ? and -            fl_flag = ?'; -        my ($data) = $dbh->selectrow_array ($sql, undef, $type, $name, $flag); -        if (defined $data) { +        my %search = (fl_type => $type, +                      fl_name => $name, +                      fl_flag => $flag); +        my $flag = $schema->resultset('Flag')->find (\%search); +        if (defined $flag) {              die "flag already set\n";          } -        $sql = 'insert into flags (fl_type, fl_name, fl_flag) -            values (?, ?, ?)'; -        $dbh->do ($sql, undef, $type, $name, $flag); -        $self->log_set ('flags', undef, $flag, $user, $host, $time); -        $dbh->commit; +        $flag = $schema->resultset('Flag')->create (\%search); +        $self->log_set ('flags', undef, $flag->fl_flag, $user, $host, $time); +        $guard->commit;      };      if ($@) {          $self->error ("cannot set flag $flag on ${type}:${name}: $@"); -        $dbh->rollback;          return;      }      return 1; @@ -451,11 +488,10 @@ sub format_acl_id {      my ($self, $id) = @_;      my $name = $id; -    my $sql = 'select ac_name from acls where ac_id = ?'; -    my $sth = $self->{dbh}->prepare ($sql); -    $sth->execute ($id); -    if (my @ref = $sth->fetchrow_array) { -        $name = $ref[0] . " ($id)"; +    my %search = (ac_id => $id); +    my $acl_rs = $self->{schema}->resultset('Acl')->find (\%search); +    if (defined $acl_rs) { +        $name = $acl_rs->ac_name . " ($id)";      }      return $name; @@ -468,23 +504,29 @@ sub history {      my ($self) = @_;      my $output = '';      eval { -        my $sql = 'select oh_action, oh_field, oh_type_field, oh_old, oh_new, -            oh_by, oh_from, oh_on from object_history where oh_type = ? and -            oh_name = ? order by oh_on'; -        my $sth = $self->{dbh}->prepare ($sql); -        $sth->execute ($self->{type}, $self->{name}); -        my @data; -        while (@data = $sth->fetchrow_array) { -            $output .= "$data[7]  "; -            my ($old, $new) = @data[3..4]; -            if ($data[0] eq 'set' and $data[1] eq 'flags') { -                if (defined ($data[4])) { -                    $output .= "set flag $data[4]"; -                } elsif (defined ($data[3])) { -                    $output .= "clear flag $data[3]"; +        my %search = (oh_type => $self->{type}, +                      oh_name => $self->{name}); +        my %attrs = (order_by => 'oh_on'); +        my @history = $self->{schema}->resultset('ObjectHistory') +            ->search (\%search, \%attrs); + +        for my $history_rs (@history) { +            $output .= sprintf ("%s %s  ", $history_rs->oh_on->ymd, +                               $history_rs->oh_on->hms); + +            my $old    = $history_rs->oh_old; +            my $new    = $history_rs->oh_new; +            my $action = $history_rs->oh_action; +            my $field  = $history_rs->oh_field; + +            if ($action eq 'set' and $field eq 'flags') { +                if (defined ($new)) { +                    $output .= "set flag $new"; +                } elsif (defined ($old)) { +                    $output .= "clear flag $old";                  } -            } elsif ($data[0] eq 'set' and $data[1] eq 'type_data') { -                my $attr = $data[2]; +            } elsif ($action eq 'set' and $field eq 'type_data') { +                my $attr = $history_rs->oh_type_field;                  if (defined ($old) and defined ($new)) {                      $output .= "set attribute $attr to $new (was $old)";                  } elsif (defined ($old)) { @@ -492,9 +534,8 @@ sub history {                  } elsif (defined ($new)) {                      $output .= "add $new to attribute $attr";                  } -            } elsif ($data[0] eq 'set' -                     and ($data[1] eq 'owner' or $data[1] =~ /^acl_/)) { -                my $field = $data[1]; +            } elsif ($action eq 'set' +                     and ($field eq 'owner' or $field =~ /^acl_/)) {                  $old = $self->format_acl_id ($old) if defined ($old);                  $new = $self->format_acl_id ($new) if defined ($new);                  if (defined ($old) and defined ($new)) { @@ -504,8 +545,7 @@ sub history {                  } elsif (defined ($old)) {                      $output .= "unset $field (was $old)";                  } -            } elsif ($data[0] eq 'set') { -                my $field = $data[1]; +            } elsif ($action eq 'set') {                  if (defined ($old) and defined ($new)) {                      $output .= "set $field to $new (was $old)";                  } elsif (defined ($new)) { @@ -514,16 +554,15 @@ sub history {                      $output .= "unset $field (was $old)";                  }              } else { -                $output .= $data[0]; +                $output .= $action;              } -            $output .= "\n    by $data[5] from $data[6]\n"; +            $output .= sprintf ("\n    by %s from %s\n", $history_rs->oh_by, +                               $history_rs->oh_from);          } -        $self->{dbh}->commit;      };      if ($@) {          my $id = $self->{type} . ':' . $self->{name};          $self->error ("cannot read history for $id: $@"); -        $self->{dbh}->rollback;          return;      }      return $output; @@ -565,6 +604,7 @@ sub show {                   [ ob_acl_destroy     => 'Destroy ACL'     ],                   [ ob_acl_flags       => 'Flags ACL'       ],                   [ ob_expires         => 'Expires'         ], +                 [ ob_comment         => 'Comment'         ],                   [ ob_created_by      => 'Created by'      ],                   [ ob_created_from    => 'Created from'    ],                   [ ob_created_on      => 'Created on'      ], @@ -576,15 +616,14 @@ sub show {                   [ ob_downloaded_on   => 'Downloaded on'   ]);      my $fields = join (', ', map { $_->[0] } @attrs);      my @data; +    my $object_rs;      eval { -        my $sql = "select $fields from objects where ob_type = ? and -            ob_name = ?"; -        @data = $self->{dbh}->selectrow_array ($sql, undef, $type, $name); -        $self->{dbh}->commit; +        my %search = (ob_type => $type, +                      ob_name => $name); +        $object_rs = $self->{schema}->resultset('Object')->find (\%search);      };      if ($@) {          $self->error ("cannot retrieve data for ${type}:${name}: $@"); -        $self->{dbh}->rollback;          return;      }      my $output = ''; @@ -592,8 +631,19 @@ sub show {      # Format the results.  We use a hack to insert the flags before the first      # trace field since they're not a field in the object in their own right. -    for my $i (0 .. $#data) { -        if ($attrs[$i][0] eq 'ob_created_by') { +    # The comment should be word-wrapped at 80 columns. +    for my $i (0 .. $#attrs) { +        my $field = $attrs[$i][0]; +        my $fieldtext = $attrs[$i][1]; +        next unless my $value = $object_rs->get_column ($field); + +        if ($field eq 'ob_comment' && length ($value) > 79 - 17) { +            local $Text::Wrap::columns = 80; +            local $Text::Wrap::unexpand = 0; +            $value = wrap (' ' x 17, ' ' x 17, $value); +            $value =~ s/^ {17}//; +        } +        if ($field eq 'ob_created_by') {              my @flags = $self->flag_list;              if (not @flags and $self->error) {                  return; @@ -607,15 +657,14 @@ sub show {              }              $output .= $attr_output;          } -        next unless defined $data[$i]; -        if ($attrs[$i][0] =~ /^ob_(owner|acl_)/) { -            my $acl = eval { Wallet::ACL->new ($data[$i], $self->{dbh}) }; +        if ($field =~ /^ob_(owner|acl_)/) { +            my $acl = eval { Wallet::ACL->new ($value, $self->{schema}) };              if ($acl and not $@) { -                $data[$i] = $acl->name || $data[$i]; -                push (@acls, [ $acl, $data[$i] ]); +                $value = $acl->name || $value; +                push (@acls, [ $acl, $value ]);              }          } -        $output .= sprintf ("%15s: %s\n", $attrs[$i][1], $data[$i]); +        $output .= sprintf ("%15s: %s\n", $fieldtext, $value);      }      if (@acls) {          my %seen; @@ -639,20 +688,31 @@ sub destroy {          $self->error ("cannot destroy ${type}:${name}: object is locked");          return;      } +    my $guard = $self->{schema}->txn_scope_guard;      eval { -        my $date = strftime ('%Y-%m-%d %T', localtime $time); -        my $sql = 'delete from flags where fl_type = ? and fl_name = ?'; -        $self->{dbh}->do ($sql, undef, $type, $name); -        $sql = 'delete from objects where ob_type = ? and ob_name = ?'; -        $self->{dbh}->do ($sql, undef, $type, $name); -        $sql = "insert into object_history (oh_type, oh_name, oh_action, -            oh_by, oh_from, oh_on) values (?, ?, 'destroy', ?, ?, ?)"; -        $self->{dbh}->do ($sql, undef, $type, $name, $user, $host, $date); -        $self->{dbh}->commit; + +        # Remove any flags that may exist for the record. +        my %search = (fl_type => $type, +                      fl_name => $name); +        $self->{schema}->resultset('Flag')->search (\%search)->delete; + +        # Remove any object records +        %search = (ob_type => $type, +                   ob_name => $name); +        $self->{schema}->resultset('Object')->search (\%search)->delete; + +        # And create a new history object for the destroy action. +        my %record = (oh_type => $type, +                      oh_name => $name, +                      oh_action => 'destroy', +                      oh_by     => $user, +                      oh_from   => $host, +                      oh_on     => strftime ('%Y-%m-%d %T', localtime $time)); +        $self->{schema}->resultset('ObjectHistory')->create (\%record); +        $guard->commit;      };      if ($@) {          $self->error ("cannot destroy ${type}:${name}: $@"); -        $self->{dbh}->rollback;          return;      }      return 1; @@ -671,7 +731,7 @@ Wallet::Object::Base - Generic parent class for wallet objects  =for stopwords  DBH HOSTNAME DATETIME ACL backend metadata timestamp Allbery wallet-backend -backend-specific +backend-specific subclasses  =head1 SYNOPSIS @@ -709,7 +769,7 @@ such object exits, throws an exception.  Otherwise, returns an object  blessed into the class used for the new() call (so subclasses can leave  this method alone and not override it). -Takes a Wallet::Database object, which is stored in the object and used +Takes a Wallet::Schema object, which is stored in the object and used  for any further operations.  =item create(TYPE, NAME, DBH, PRINCIPAL, HOSTNAME [, DATETIME]) @@ -778,6 +838,18 @@ attributes set, this method should return that metadata, formatted as key:  value pairs with the keys right-aligned in the first 15 characters,  followed by a space, a colon, and the value. +=item comment([COMMENT, PRINCIPAL, HOSTNAME [, DATETIME]]) + +Sets or retrieves the comment associated with an object.  If no arguments +are given, returns the current comment or undef if no comment is set.  If +arguments are given, change the comment to COMMENT and return true on +success and false on failure.  Pass in the empty string for COMMENT to +clear the comment. + +The other arguments are used for logging and history and should indicate +the user and host from which the change is made and the time of the +change. +  =item destroy(PRINCIPAL, HOSTNAME [, DATETIME])  Destroys the object by removing all record of it from the database.  The diff --git a/perl/Wallet/Object/File.pm b/perl/Wallet/Object/File.pm index 47c8ac2..49589f1 100644 --- a/perl/Wallet/Object/File.pm +++ b/perl/Wallet/Object/File.pm @@ -1,7 +1,8 @@  # Wallet::Object::File -- File object implementation for the wallet.  #  # Written by Russ Allbery <rra@stanford.edu> -# Copyright 2008, 2010 Board of Trustees, Leland Stanford Jr. University +# Copyright 2008, 2010 +#     The Board of Trustees of the Leland Stanford Junior University  #  # See LICENSE for licensing terms. @@ -143,7 +144,7 @@ API HOSTNAME DATETIME keytab remctld backend nul Allbery wallet-backend      my @name = qw(file mysql-lsdb)      my @trace = ($user, $host, time); -    my $object = Wallet::Object::Keytab->create (@name, $dbh, @trace); +    my $object = Wallet::Object::Keytab->create (@name, $schema, @trace);      unless ($object->store ("the-password\n")) {          die $object->error, "\n";      } diff --git a/perl/Wallet/Object/Keytab.pm b/perl/Wallet/Object/Keytab.pm index b7c2805..e00747b 100644 --- a/perl/Wallet/Object/Keytab.pm +++ b/perl/Wallet/Object/Keytab.pm @@ -1,8 +1,8 @@  # Wallet::Object::Keytab -- Keytab object implementation for the wallet.  #  # Written by Russ Allbery <rra@stanford.edu> -# Copyright 2007, 2008, 2009, 2010 -#     Board of Trustees, Leland Stanford Jr. University +# Copyright 2007, 2008, 2009, 2010, 2013 +#     The Board of Trustees of the Leland Stanford Junior University  #  # See LICENSE for licensing terms. @@ -40,21 +40,29 @@ sub enctypes_set {      my @trace = ($user, $host, $time);      my $name = $self->{name};      my %enctypes = map { $_ => 1 } @$enctypes; +    my $guard = $self->{schema}->txn_scope_guard;      eval { -        my $sql = 'select ke_enctype from keytab_enctypes where ke_name = ?'; -        my $sth = $self->{dbh}->prepare ($sql); -        $sth->execute ($name); -        my (@current, $entry); -        while (defined ($entry = $sth->fetchrow_arrayref)) { -            push (@current, @$entry); + +        # Find all enctypes for the given keytab. +        my %search = (ke_name => $name); +        my @enctypes = $self->{schema}->resultset('KeytabEnctype') +            ->search (\%search); +        my (@current); +        for my $enctype_rs (@enctypes) { +            push (@current, $enctype_rs->ke_enctype);          } + +        # Use the existing enctypes and the enctypes we should have to match +        # against ones that need to be removed, and note those that already +        # exist.          for my $enctype (@current) {              if ($enctypes{$enctype}) {                  delete $enctypes{$enctype};              } else { -                $sql = 'delete from keytab_enctypes where ke_name = ? and -                    ke_enctype = ?'; -                $self->{dbh}->do ($sql, undef, $name, $enctype); +                %search = (ke_name    => $name, +                           ke_enctype => $enctype); +                $self->{schema}->resultset('KeytabEnctype')->find (\%search) +                    ->delete;                  $self->log_set ('type_data enctypes', $enctype, undef, @trace);              }          } @@ -64,21 +72,20 @@ sub enctypes_set {          # doesn't enforce integrity constraints.  We do this in sorted order          # to make it easier to test.          for my $enctype (sort keys %enctypes) { -            $sql = 'select en_name from enctypes where en_name = ?'; -            my $status = $self->{dbh}->selectrow_array ($sql, undef, $enctype); -            unless ($status) { +            my %search = (en_name => $enctype); +            my $enctype_rs = $self->{schema}->('Enctype')->find (\%search); +            unless (defined $enctype_rs) {                  die "unknown encryption type $enctype\n";              } -            $sql = 'insert into keytab_enctypes (ke_name, ke_enctype) values -                (?, ?)'; -            $self->{dbh}->do ($sql, undef, $name, $enctype); +            my %record = (ke_name    => $name, +                          ke_enctype => $enctype); +            $self->{schema}->resultset('Enctype')->create (\%record);              $self->log_set ('type_data enctypes', undef, $enctype, @trace);          } -        $self->{dbh}->commit; +        $guard->commit;      };      if ($@) {          $self->error ($@); -        $self->{dbh}->rollback;          return;      }      return 1; @@ -92,19 +99,16 @@ sub enctypes_list {      my ($self) = @_;      my @enctypes;      eval { -        my $sql = 'select ke_enctype from keytab_enctypes where ke_name = ? -            order by ke_enctype'; -        my $sth = $self->{dbh}->prepare ($sql); -        $sth->execute ($self->{name}); -        my $entry; -        while (defined ($entry = $sth->fetchrow_arrayref)) { -            push (@enctypes, @$entry); +        my %search = (ke_name => $self->{name}); +        my %attrs = (order_by => 'ke_enctype'); +        my @enctypes_rs = $self->{schema}->resultset('KeytabEnctype') +            ->search (\%search, \%attrs); +        for my $enctype_rs (@enctypes_rs) { +            push (@enctypes, $enctype_rs->ke_enctype);          } -        $self->{dbh}->commit;      };      if ($@) {          $self->error ($@); -        $self->{dbh}->rollback;          return;      }      return @enctypes; @@ -132,21 +136,21 @@ sub sync_set {          $self->error ("unsupported synchronization target $target");          return;      } else { +        my $guard = $self->{schema}->txn_scope_guard;          eval { -            my $sql = 'select ks_target from keytab_sync where ks_name = ?'; -            my $dbh = $self->{dbh};              my $name = $self->{name}; -            my ($result) = $dbh->selectrow_array ($sql, undef, $name); -            if ($result) { -                my $sql = 'delete from keytab_sync where ks_name = ?'; -                $self->{dbh}->do ($sql, undef, $name); -                $self->log_set ('type_data sync', $result, undef, @trace); +            my %search = (ks_name => $name); +            my $sync_rs = $self->{schema}->resultset('KeytabSync') +                ->find (\%search); +            if (defined $sync_rs) { +                my $target = $sync_rs->ks_target; +                $sync_rs->delete; +                $self->log_set ('type_data sync', $target, undef, @trace);              } -            $self->{dbh}->commit; +            $guard->commit;          };          if ($@) {              $self->error ($@); -            $self->{dbh}->rollback;              return;          }      } @@ -161,19 +165,16 @@ sub sync_list {      my ($self) = @_;      my @targets;      eval { -        my $sql = 'select ks_target from keytab_sync where ks_name = ? -            order by ks_target'; -        my $sth = $self->{dbh}->prepare ($sql); -        $sth->execute ($self->{name}); -        my $target; -        while (defined ($target = $sth->fetchrow_array)) { -            push (@targets, $target); +        my %search = (ks_name => $self->{name}); +        my %attrs = (order_by => 'ks_target'); +        my @syncs = $self->{schema}->resultset('KeytabSync')->search (\%search, +                                                                      \%attrs); +        for my $sync_rs (@syncs) { +            push (@targets, $sync_rs->ks_target);          } -        $self->{dbh}->commit;      };      if ($@) {          $self->error ($@); -        $self->{dbh}->rollback;          return;      }      return @targets; @@ -238,21 +239,16 @@ sub attr_show {  # Override new to start by creating a handle for the kadmin module we're  # using.  sub new { -    my ($class, $type, $name, $dbh) = @_; +    my ($class, $type, $name, $schema) = @_;       my $self = { -        dbh    => $dbh, +        schema => $schema,          kadmin => undef,      };      bless $self, $class;      my $kadmin = Wallet::Kadmin->new ();      $self->{kadmin} = $kadmin; -    # Set a callback for things to do after a fork, specifically for the MIT -    # kadmin module which forks to kadmin. -    my $callback = sub { $self->{dbh}->{InactiveDestroy} = 1 }; -    $kadmin->fork_callback ($callback); - -    $self = $class->SUPER::new ($type, $name, $dbh); +    $self = $class->SUPER::new ($type, $name, $schema);      $self->{kadmin} = $kadmin;      return $self;  } @@ -262,24 +258,20 @@ sub new {  # great here since we don't have a way to communicate the error back to the  # caller.  sub create { -    my ($class, $type, $name, $dbh, $creator, $host, $time) = @_; +    my ($class, $type, $name, $schema, $creator, $host, $time) = @_;      my $self = { -        dbh    => $dbh, +        schema => $schema,          kadmin => undef,      };      bless $self, $class;      my $kadmin = Wallet::Kadmin->new ();      $self->{kadmin} = $kadmin; -    # Set a callback for things to do after a fork, specifically for the MIT -    # kadmin module which forks to kadmin. -    my $callback = sub { $self->{dbh}->{InactiveDestroy} = 1 }; -    $kadmin->fork_callback ($callback); -      if (not $kadmin->create ($name)) {          die $kadmin->error, "\n";      } -    $self = $class->SUPER::create ($type, $name, $dbh, $creator, $host, $time); +    $self = $class->SUPER::create ($type, $name, $schema, $creator, $host, +                                   $time);      $self->{kadmin} = $kadmin;      return $self;  } @@ -292,16 +284,21 @@ sub destroy {          $self->error ("cannot destroy $id: object is locked");          return;      } +    my $schema = $self->{schema}; +    my $guard = $schema->txn_scope_guard;      eval { -        my $sql = 'delete from keytab_sync where ks_name = ?'; -        $self->{dbh}->do ($sql, undef, $self->{name}); -        $sql = 'delete from keytab_enctypes where ke_name = ?'; -        $self->{dbh}->do ($sql, undef, $self->{name}); -        $self->{dbh}->commit; +        my %search = (ks_name => $self->{name}); +        my $sync_rs = $schema->resultset('KeytabSync')->search (\%search); +        $sync_rs->delete_all if defined $sync_rs; + +        %search = (ke_name => $self->{name}); +        my $enctype_rs = $schema->resultset('KeytabEnctype')->search (\%search); +        $enctype_rs->delete_all if defined $enctype_rs; + +        $guard->commit;      };      if ($@) {          $self->error ($@); -        $self->{dbh}->rollback;          return;      }      my $kadmin = $self->{kadmin}; @@ -347,7 +344,7 @@ __END__  =for stopwords  keytab API KDC keytabs HOSTNAME DATETIME enctypes enctype DBH metadata -unmanaged kadmin Allbery +unmanaged kadmin Allbery unlinked  =head1 NAME @@ -357,7 +354,7 @@ Wallet::Object::Keytab - Keytab object implementation for wallet      my @name = qw(keytab host/shell.example.com);      my @trace = ($user, $host, time); -    my $object = Wallet::Object::Keytab->create (@name, $dbh, @trace); +    my $object = Wallet::Object::Keytab->create (@name, $schema, @trace);      my $keytab = $object->get (@trace);      $object->destroy (@trace); diff --git a/perl/Wallet/Object/WAKeyring.pm b/perl/Wallet/Object/WAKeyring.pm new file mode 100644 index 0000000..f33497c --- /dev/null +++ b/perl/Wallet/Object/WAKeyring.pm @@ -0,0 +1,370 @@ +# Wallet::Object::WAKeyring -- WebAuth keyring object implementation. +# +# Written by Russ Allbery <rra@stanford.edu> +# Copyright 2012, 2013 +#     The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +############################################################################## +# Modules and declarations +############################################################################## + +package Wallet::Object::WAKeyring; +require 5.006; + +use strict; +use vars qw(@ISA $VERSION); + +use Digest::MD5 qw(md5_hex); +use Fcntl qw(LOCK_EX); +use Wallet::Config (); +use Wallet::Object::Base; +use WebAuth 3.06 qw(WA_KEY_AES WA_AES_128); + +@ISA = qw(Wallet::Object::Base); + +# This version should be increased on any code change to this module.  Always +# use two digits for the minor version with a leading zero if necessary so +# that it will sort properly. +$VERSION = '0.01'; + +############################################################################## +# File naming +############################################################################## + +# Returns the path into which that keyring object will be stored or undef on +# error.  On error, sets the internal error. +sub file_path { +    my ($self) = @_; +    my $name = $self->{name}; +    unless ($Wallet::Config::WAKEYRING_BUCKET) { +        $self->error ('WebAuth keyring support not configured'); +        return; +    } +    unless ($name) { +        $self->error ('WebAuth keyring objects may not have empty names'); +        return; +    } +    my $hash = substr (md5_hex ($name), 0, 2); +    $name =~ s/([^\w-])/sprintf ('%%%02X', ord ($1))/ge; +    my $parent = "$Wallet::Config::WAKEYRING_BUCKET/$hash"; +    unless (-d $parent || mkdir ($parent, 0700)) { +        $self->error ("cannot create keyring bucket $hash: $!"); +        return; +    } +    return "$Wallet::Config::WAKEYRING_BUCKET/$hash/$name"; +} + +############################################################################## +# Core methods +############################################################################## + +# Override destroy to delete the file as well. +sub destroy { +    my ($self, $user, $host, $time) = @_; +    my $id = $self->{type} . ':' . $self->{name}; +    my $path = $self->file_path; +    if (defined ($path) && -f $path && !unlink ($path)) { +        $self->error ("cannot delete $id: $!"); +        return; +    } +    return $self->SUPER::destroy ($user, $host, $time); +} + +# Update the keyring if needed, and then return the contents of the current +# keyring. +sub get { +    my ($self, $user, $host, $time) = @_; +    $time ||= time; +    my $id = $self->{type} . ':' . $self->{name}; +    if ($self->flag_check ('locked')) { +        $self->error ("cannot get $id: object is locked"); +        return; +    } +    my $path = $self->file_path; +    return unless defined $path; + +    # Create a WebAuth context and ensure we can load the relevant modules. +    my $wa = eval { WebAuth->new }; +    if ($@) { +        $self->error ("cannot initialize WebAuth: $@"); +        return; +    } + +    # Check if the keyring already exists.  If not, create a new one with a +    # single key that's immediately valid and two more that will become valid +    # in the future. +    # +    # If the keyring does already exist, get a lock on the file.  At the end +    # of this process, we'll do an atomic update and then drop our lock. +    # +    # FIXME: There are probably better ways to do this.  There are some race +    # conditions here, particularly with new keyrings. +    unless (open (FILE, '+<', $path)) { +        my $data; +        eval { +            my $key = $wa->key_create (WA_KEY_AES, WA_AES_128); +            my $ring = $wa->keyring_new ($key); +            $key = $wa->key_create (WA_KEY_AES, WA_AES_128); +            my $valid = time + $Wallet::Config::WAKEYRING_REKEY_INTERVAL; +            $ring->add (time, $valid, $key); +            $key = $wa->key_create (WA_KEY_AES, WA_AES_128); +            $valid += $Wallet::Config::WAKEYRING_REKEY_INTERVAL; +            $ring->add (time, $valid, $key); +            $data = $ring->encode; +            $ring->write ($path); +        }; +        if ($@) { +            $self->error ("cannot create new keyring"); +            return; +        }; +        $self->log_action ('get', $user, $host, $time); +        return $data; +    } +    unless (flock (FILE, LOCK_EX)) { +        $self->error ("cannot get lock on keyring: $!"); +        return; +    } + +    # Read the keyring. +    my $ring = eval { WebAuth::Keyring->read ($wa, $path) }; +    if ($@) { +        $self->error ("cannot read keyring: $@"); +        return; +    } + +    # If the most recent key has a valid-after older than now + +    # WAKEYRING_REKEY_INTERVAL, we generate a new key with a valid_after of +    # now + 2 * WAKEYRING_REKEY_INTERVAL. +    my ($count, $newest) = (0, 0); +    for my $entry ($ring->entries) { +        $count++; +        if ($entry->valid_after > $newest) { +            $newest = $entry->valid_after; +        } +    } +    eval { +        if ($newest <= time + $Wallet::Config::WAKEYRING_REKEY_INTERVAL) { +            my $valid = time + 2 * $Wallet::Config::WAKEYRING_REKEY_INTERVAL; +            my $key = $wa->key_create (WA_KEY_AES, WA_AES_128); +            $ring->add (time, $valid, $key); +        } +    }; +    if ($@) { +        $self->error ("cannot add new key: $@"); +        return; +    } + +    # If there are any keys older than the purge interval, remove them, but +    # only do so if we have more than three keys (the one that's currently +    # active, the one that's going to come active in the rekey interval, and +    # the one that's going to come active after that. +    # +    # FIXME: Be sure that we don't remove the last currently-valid key. +    my $cutoff = time - $Wallet::Config::WAKEYRING_PURGE_INTERVAL; +    my $i = 0; +    my @purge; +    if ($count > 3) { +        for my $entry ($ring->entries) { +            if ($entry->creation < $cutoff) { +                push (@purge, $i); +            } +            $i++; +        } +    } +    if (@purge && $count - @purge >= 3) { +        eval { +            for my $key (reverse @purge) { +                $ring->remove ($key); +            } +        }; +        if ($@) { +            $self->error ("cannot remove old keys: $@"); +            return; +        } +    } + +    # Encode the key. +    my $data = eval { $ring->encode }; +    if ($@) { +        $self->error ("cannot encode keyring: $@"); +        return; +    } + +    # Write the new keyring to the path. +    eval { $ring->write ($path) }; +    if ($@) { +        $self->error ("cannot store new keyring: $@"); +        return; +    } +    close FILE; +    $self->log_action ('get', $user, $host, $time); +    return $data; +} + +# Store the file on the wallet server. +# +# FIXME: Check the provided keyring for validity. +sub store { +    my ($self, $data, $user, $host, $time) = @_; +    $time ||= time; +    my $id = $self->{type} . ':' . $self->{name}; +    if ($self->flag_check ('locked')) { +        $self->error ("cannot store $id: object is locked"); +        return; +    } +    if ($Wallet::Config::FILE_MAX_SIZE) { +        my $max = $Wallet::Config::FILE_MAX_SIZE; +        if (length ($data) > $max) { +            $self->error ("data exceeds maximum of $max bytes"); +            return; +        } +    } +    my $path = $self->file_path; +    return unless $path; +    unless (open (FILE, '>', $path)) { +        $self->error ("cannot store $id: $!"); +        return; +    } +    unless (print FILE ($data) and close FILE) { +        $self->error ("cannot store $id: $!"); +        close FILE; +        return; +    } +    $self->log_action ('store', $user, $host, $time); +    return 1; +} + +1; +__END__ + +############################################################################## +# Documentation +############################################################################## + +=for stopwords +WebAuth keyring keyrings API HOSTNAME DATETIME keytab AES rekey Allbery + +=head1 NAME + +Wallet::Object::WAKeyring - WebAuth keyring object implementation for wallet + +=head1 SYNOPSIS + +    my ($user, $host, $time); +    my @name = qw(wa-keyring www.stanford.edu); +    my @trace = ($user, $host, $time); +    my $object = Wallet::Object::WAKeyring->create (@name, $schema, $trace); +    my $keyring = $object->get (@trace); +    unless ($object->store ($keyring)) { +        die $object->error, "\n"; +    } +    $object->destroy (@trace); + +=head1 DESCRIPTION + +Wallet::Object::WAKeyring is a representation of a WebAuth keyring in the +wallet.  It implements the wallet object API and provides the necessary +glue to store a keyring on the wallet server, retrieve it, update the +keyring with new keys automatically as needed, purge old keys +automatically, and delete the keyring when the object is deleted. + +WebAuth keyrings hold one or more keys.  Each key has a creation time and +a validity time.  The key cannot be used until its validity time has been +reached.  This permits safe key rotation: a new key is added with a +validity time in the future, and then the keyring is updated everywhere it +needs to be before that validity time is reached.  This wallet object +automatically handles key rotation by adding keys with validity dates in +the future and removing keys with creation dates substantially in the +past. + +To use this object, various configuration options specifying where to +store the keyrings and how to handle key rotation must be set.  See +Wallet::Config for details on these configuration parameters and +information about how to set wallet configuration. + +=head1 METHODS + +This object mostly inherits from Wallet::Object::Base.  See the +documentation for that class for all generic methods.  Below are only +those methods that are overridden or behave specially for this +implementation. + +=over 4 + +=item destroy(PRINCIPAL, HOSTNAME [, DATETIME]) + +Destroys a WebAuth keyring object by removing it from the database and +deleting the corresponding file on the wallet server.  Returns true on +success and false on failure.  The caller should call error() to get the +error message after a failure.  PRINCIPAL, HOSTNAME, and DATETIME are +stored as history information.  PRINCIPAL should be the user who is +destroying the object.  If DATETIME isn't given, the current time is used. + +=item get(PRINCIPAL, HOSTNAME [, DATETIME]) + +Either creates a new WebAuth keyring (if this object has not bee stored or +retrieved before) or does any necessary periodic maintenance on the +keyring and then returns its data.  The caller should call error() to get +the error message if get() returns undef.  PRINCIPAL, HOSTNAME, and +DATETIME are stored as history information.  PRINCIPAL should be the user +who is downloading the keytab.  If DATETIME isn't given, the current time +is used. + +If this object has never been stored or retrieved before, a new keyring +will be created with three 128-bit AES keys: one that is immediately +valid, one that will become valid after the rekey interval, and one that +will become valid after twice the rekey interval. + +If keyring data for this object already exists, the creation and validity +dates for each key in the keyring will be examined.  If the key with the +validity date the farthest into the future has a date that's less than or +equal to the current time plus the rekey interval, a new 128-bit AES key +will be added to the keyring with a validity time of twice the rekey +interval in the future.  Finally, all keys with a creation date older than +the configured purge interval will be removed provided that the keyring +has at least three keys + +=item store(DATA, PRINCIPAL, HOSTNAME [, DATETIME]) + +Store DATA as the current contents of the WebAuth keyring object.  Note +that this is not checked for validity, just assumed to be a valid keyring. +Any existing data will be overwritten.  Returns true on success and false +on failure.  The caller should call error() to get the error message after +a failure.  PRINCIPAL, HOSTNAME, and DATETIME are stored as history +information.  PRINCIPAL should be the user who is destroying the object. +If DATETIME isn't given, the current time is used. + +If FILE_MAX_SIZE is set in the wallet configuration, a store() of DATA +larger than that configuration setting will be rejected. + +=back + +=head1 FILES + +=over 4 + +=item WAKEYRING_BUCKET/<hash>/<file> + +WebAuth keyrings are stored on the wallet server under the directory +WAKEYRING_BUCKET as set in the wallet configuration.  <hash> is the first +two characters of the hex-encoded MD5 hash of the wallet file object name, +used to not put too many files in the same directory.  <file> is the name +of the file object with all characters other than alphanumerics, +underscores, and dashes replaced by "%" and the hex code of the character. + +=back + +=head1 SEE ALSO + +Wallet::Config(3), Wallet::Object::Base(3), wallet-backend(8), WebAuth(3) + +This module is part of the wallet system. The current version is available +from <http://www.eyrie.org/~eagle/software/wallet/>. + +=head1 AUTHOR + +Russ Allbery <rra@stanford.edu> + +=cut | 
