summaryrefslogtreecommitdiff
path: root/perl/Wallet/Object
diff options
context:
space:
mode:
Diffstat (limited to 'perl/Wallet/Object')
-rw-r--r--perl/Wallet/Object/Base.pm378
-rw-r--r--perl/Wallet/Object/File.pm5
-rw-r--r--perl/Wallet/Object/Keytab.pm137
-rw-r--r--perl/Wallet/Object/WAKeyring.pm370
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