summaryrefslogtreecommitdiff
path: root/perl/lib
diff options
context:
space:
mode:
authorRuss Allbery <eagle@eyrie.org>2014-07-16 13:43:17 -0700
committerRuss Allbery <eagle@eyrie.org>2014-07-16 13:43:17 -0700
commit6409733ee3b7b1910dc1c166a392cc628834146c (patch)
treee9460f8f2ca0f3676afeed2a9dcf549acfc39b53 /perl/lib
parent334ed844cbb5c8f7ea82a94c701a3016dd6950b9 (diff)
parentf8963ceb19cd2b503b981f43a3f8c0f45649989f (diff)
Imported Upstream version 1.1
Diffstat (limited to 'perl/lib')
-rw-r--r--perl/lib/Wallet/ACL.pm667
-rw-r--r--perl/lib/Wallet/ACL/Base.pm126
-rw-r--r--perl/lib/Wallet/ACL/Krb5.pm126
-rw-r--r--perl/lib/Wallet/ACL/Krb5/Regex.pm134
-rw-r--r--perl/lib/Wallet/ACL/LDAP/Attribute.pm264
-rw-r--r--perl/lib/Wallet/ACL/NetDB.pm268
-rw-r--r--perl/lib/Wallet/ACL/NetDB/Root.pm129
-rw-r--r--perl/lib/Wallet/Admin.pm389
-rw-r--r--perl/lib/Wallet/Config.pm827
-rw-r--r--perl/lib/Wallet/Database.pm124
-rw-r--r--perl/lib/Wallet/Kadmin.pm241
-rw-r--r--perl/lib/Wallet/Kadmin/Heimdal.pm315
-rw-r--r--perl/lib/Wallet/Kadmin/MIT.pm324
-rw-r--r--perl/lib/Wallet/Object/Base.pm1052
-rw-r--r--perl/lib/Wallet/Object/Duo.pm332
-rw-r--r--perl/lib/Wallet/Object/File.pm243
-rw-r--r--perl/lib/Wallet/Object/Keytab.pm514
-rw-r--r--perl/lib/Wallet/Object/WAKeyring.pm371
-rw-r--r--perl/lib/Wallet/Policy/Stanford.pm422
-rw-r--r--perl/lib/Wallet/Report.pm681
-rw-r--r--perl/lib/Wallet/Schema.pm354
-rw-r--r--perl/lib/Wallet/Schema/Result/Acl.pm110
-rw-r--r--perl/lib/Wallet/Schema/Result/AclEntry.pm74
-rw-r--r--perl/lib/Wallet/Schema/Result/AclHistory.pm123
-rw-r--r--perl/lib/Wallet/Schema/Result/AclScheme.pm84
-rw-r--r--perl/lib/Wallet/Schema/Result/Duo.pm53
-rw-r--r--perl/lib/Wallet/Schema/Result/Enctype.pm45
-rw-r--r--perl/lib/Wallet/Schema/Result/Flag.pm62
-rw-r--r--perl/lib/Wallet/Schema/Result/KeytabEnctype.pm53
-rw-r--r--perl/lib/Wallet/Schema/Result/KeytabSync.pm53
-rw-r--r--perl/lib/Wallet/Schema/Result/Object.pm266
-rw-r--r--perl/lib/Wallet/Schema/Result/ObjectHistory.pm135
-rw-r--r--perl/lib/Wallet/Schema/Result/SyncTarget.pm48
-rw-r--r--perl/lib/Wallet/Schema/Result/Type.pm75
-rw-r--r--perl/lib/Wallet/Server.pm1096
35 files changed, 10180 insertions, 0 deletions
diff --git a/perl/lib/Wallet/ACL.pm b/perl/lib/Wallet/ACL.pm
new file mode 100644
index 0000000..a3b0146
--- /dev/null
+++ b/perl/lib/Wallet/ACL.pm
@@ -0,0 +1,667 @@
+# Wallet::ACL -- Implementation of ACLs in the wallet system.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2008, 2010, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($VERSION);
+
+use DateTime;
+use DBI;
+
+# 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.08';
+
+##############################################################################
+# Constructors
+##############################################################################
+
+# Initialize a new ACL from the database. Verify that the ACL already exists
+# in the database and, if so, return a new blessed object. Stores the ACL ID
+# and the database handle to use for future operations. If the object
+# doesn't exist, throws an exception.
+sub new {
+ my ($class, $id, $schema) = @_;
+ my (%search, $data, $name);
+ if ($id =~ /^\d+\z/) {
+ $search{ac_id} = $id;
+ } else {
+ $search{ac_name} = $id;
+ }
+ eval {
+ $data = $schema->resultset('Acl')->find (\%search);
+ };
+ if ($@) {
+ die "cannot search for ACL $id: $@\n";
+ } elsif (not defined $data) {
+ die "ACL $id not found\n";
+ }
+ my $self = {
+ schema => $schema,
+ id => $data->ac_id,
+ name => $data->ac_name,
+ };
+ bless ($self, $class);
+ return $self;
+}
+
+# Create a new ACL in the database with the given name and return a new
+# blessed ACL object for it. Stores the database handle to use and the ID of
+# the newly created ACL in the object. On failure, throws an exception.
+sub create {
+ my ($class, $name, $schema, $user, $host, $time) = @_;
+ if ($name =~ /^\d+\z/) {
+ die "ACL name may not be all numbers\n";
+ }
+ $time ||= time;
+ my $id;
+ eval {
+ my $guard = $schema->txn_scope_guard;
+
+ # Create the new record.
+ my %record = (ac_name => $name);
+ my $acl = $schema->resultset('Acl')->create (\%record);
+ $id = $acl->ac_id;
+ die "unable to retrieve new ACL ID" unless defined $id;
+
+ # Add to the history table.
+ my $date = DateTime->from_epoch (epoch => $time);
+ %record = (ah_acl => $id,
+ ah_name => $name,
+ ah_action => 'create',
+ ah_by => $user,
+ ah_from => $host,
+ ah_on => $date);
+ my $history = $schema->resultset('AclHistory')->create (\%record);
+ die "unable to create new history entry" unless defined $history;
+ $guard->commit;
+ };
+ if ($@) {
+ die "cannot create ACL $name: $@\n";
+ }
+ my $self = {
+ schema => $schema,
+ id => $id,
+ name => $name,
+ };
+ bless ($self, $class);
+ return $self;
+}
+
+##############################################################################
+# Utility functions
+##############################################################################
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+# Returns the ID of an ACL.
+sub id {
+ my ($self) = @_;
+ return $self->{id};
+}
+
+# Returns the name of the ACL.
+sub name {
+ my ($self)= @_;
+ return $self->{name};
+}
+
+# Given an ACL scheme, return the mapping to a class by querying the
+# database, or undef if no mapping exists. Also load the relevant module.
+sub scheme_mapping {
+ my ($self, $scheme) = @_;
+ my $class;
+ eval {
+ my %search = (as_name => $scheme);
+ my $scheme_rec = $self->{schema}->resultset('AclScheme')
+ ->find (\%search);
+ $class = $scheme_rec->as_class;
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ if (defined $class) {
+ eval "require $class";
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ }
+ return $class;
+}
+
+# Record a change to an ACL. Takes the type of change, the scheme and
+# identifier of the entry, and the trace information (user, host, and time).
+# This function does not commit and does not catch exceptions. It should
+# normally be called as part of a larger transaction that implements the
+# change and should be committed with that change.
+sub log_acl {
+ my ($self, $action, $scheme, $identifier, $user, $host, $time) = @_;
+ unless ($action =~ /^(add|remove|rename)\z/) {
+ die "invalid history action $action";
+ }
+ my $date = DateTime->from_epoch (epoch => $time);
+ my %record = (ah_acl => $self->{id},
+ ah_name => $self->{name},
+ ah_action => $action,
+ ah_scheme => $scheme,
+ ah_identifier => $identifier,
+ ah_by => $user,
+ ah_from => $host,
+ ah_on => $date);
+ $self->{schema}->resultset('AclHistory')->create (\%record);
+}
+
+##############################################################################
+# ACL manipulation
+##############################################################################
+
+# Changes the human-readable name of the ACL. Note that this operation is not
+# logged since it isn't a change to any of the data stored in the wallet.
+# Returns true on success, false on failure.
+sub rename {
+ my ($self, $name, $user, $host, $time) = @_;
+ $time ||= time;
+ if ($name =~ /^\d+\z/) {
+ $self->error ("ACL name may not be all numbers");
+ return;
+ }
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %search = (ac_id => $self->{id});
+ my $acls = $self->{schema}->resultset('Acl')->find (\%search);
+ $acls->ac_name ($name);
+ $acls->update;
+ $self->log_acl ('rename', undef, undef, $user, $host, $time);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot rename ACL $self->{id} to $name: $@");
+ return;
+ }
+ $self->{name} = $name;
+ return 1;
+}
+
+# Destroy the ACL, deleting it out of the database. Returns true on success,
+# false on failure.
+#
+# Checks to ensure that the ACL is not referenced anywhere in the database,
+# since we may not have referential integrity enforcement. It's not clear
+# that this is the right place to do this; it's a bit of an abstraction
+# violation, since it's a query against the object table.
+sub destroy {
+ my ($self, $user, $host, $time) = @_;
+ $time ||= time;
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+
+ # Make certain no one is using the ACL.
+ my @search = ({ ob_owner => $self->{id} },
+ { ob_acl_get => $self->{id} },
+ { ob_acl_store => $self->{id} },
+ { ob_acl_show => $self->{id} },
+ { ob_acl_destroy => $self->{id} },
+ { ob_acl_flags => $self->{id} });
+ my @entries = $self->{schema}->resultset('Object')->search (\@search);
+ if (@entries) {
+ my ($entry) = @entries;
+ die "ACL in use by ".$entry->ob_type.":".$entry->ob_name;
+ }
+
+ # Delete any entries (there may or may not be any).
+ my %search = (ae_id => $self->{id});
+ @entries = $self->{schema}->resultset('AclEntry')->search(\%search);
+ for my $entry (@entries) {
+ $entry->delete;
+ }
+
+ # There should definitely be an ACL record to delete.
+ %search = (ac_id => $self->{id});
+ my $entry = $self->{schema}->resultset('Acl')->find(\%search);
+ $entry->delete if defined $entry;
+
+ # Create new history line for the deletion.
+ my $date = DateTime->from_epoch (epoch => $time);
+ my %record = (ah_acl => $self->{id},
+ ah_name => $self->{name},
+ ah_action => 'destroy',
+ ah_by => $user,
+ ah_from => $host,
+ ah_on => $date);
+ $self->{schema}->resultset('AclHistory')->create (\%record);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot destroy ACL $self->{id}: $@");
+ return;
+ }
+ return 1;
+}
+
+##############################################################################
+# ACL entry manipulation
+##############################################################################
+
+# Add an ACL entry to this ACL. Returns true on success and false on failure.
+sub add {
+ my ($self, $scheme, $identifier, $user, $host, $time) = @_;
+ $time ||= time;
+ unless ($self->scheme_mapping ($scheme)) {
+ $self->error ("unknown ACL scheme $scheme");
+ return;
+ }
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %record = (ae_id => $self->{id},
+ ae_scheme => $scheme,
+ ae_identifier => $identifier);
+ my $entry = $self->{schema}->resultset('AclEntry')->create (\%record);
+ $self->log_acl ('add', $scheme, $identifier, $user, $host, $time);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot add $scheme:$identifier to $self->{id}: $@");
+ return;
+ }
+ return 1;
+}
+
+# Remove an ACL entry to this ACL. Returns true on success and false on
+# failure. Detect the case where no such row exists before doing the delete
+# so that we can provide a good error message.
+sub remove {
+ my ($self, $scheme, $identifier, $user, $host, $time) = @_;
+ $time ||= time;
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %search = (ae_id => $self->{id},
+ ae_scheme => $scheme,
+ ae_identifier => $identifier);
+ my $entry = $self->{schema}->resultset('AclEntry')->find (\%search);
+ unless (defined $entry) {
+ die "entry not found in ACL\n";
+ }
+ $entry->delete;
+ $self->log_acl ('remove', $scheme, $identifier, $user, $host, $time);
+ $guard->commit;
+ };
+ if ($@) {
+ my $entry = "$scheme:$identifier";
+ $self->error ("cannot remove $entry from $self->{id}: $@");
+ return;
+ }
+ return 1;
+}
+
+##############################################################################
+# ACL checking
+##############################################################################
+
+# List all of the entries in an ACL. Returns an array of tuples, each of
+# which contains a scheme and identifier, or an array containing undef on
+# error. Sets the internal error string on error.
+sub list {
+ my ($self) = @_;
+ undef $self->{error};
+ my @entries;
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %search = (ae_id => $self->{id});
+ my @entry_recs = $self->{schema}->resultset('AclEntry')
+ ->search (\%search);
+ for my $entry (@entry_recs) {
+ push (@entries, [ $entry->ae_scheme, $entry->ae_identifier ]);
+ }
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot retrieve ACL $self->{id}: $@");
+ return;
+ } else {
+ return @entries;
+ }
+}
+
+# Return as a string a human-readable description of an ACL, including its
+# membership. This method is only for human-readable output; use the list()
+# method if you are using the results in other code. Returns undef on
+# failure.
+sub show {
+ my ($self) = @_;
+ my @entries = $self->list;
+ if (not @entries and $self->error) {
+ return;
+ }
+ my $name = $self->name;
+ my $id = $self->id;
+ my $output = "Members of ACL $name (id: $id) are:\n";
+ for my $entry (sort { $$a[0] cmp $$b[0] or $$a[1] cmp $$b[1] } @entries) {
+ my ($scheme, $identifier) = @$entry;
+ $output .= " $scheme $identifier\n";
+ }
+ return $output;
+}
+
+# Return as a string the history of an ACL. Returns undef on failure.
+sub history {
+ my ($self) = @_;
+ my $output = '';
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %search = (ah_acl => $self->{id});
+ my %options = (order_by => 'ah_on');
+ my @data = $self->{schema}->resultset('AclHistory')
+ ->search (\%search, \%options);
+ for my $data (@data) {
+ my $date = $data->ah_on;
+ $date->set_time_zone ('local');
+ $output .= sprintf ("%s %s ", $date->ymd, $date->hms);
+ if ($data->ah_action eq 'add' || $data->ah_action eq 'remove') {
+ $output .= sprintf ("%s %s %s", $data->ah_action,
+ $data->ah_scheme, $data->ah_identifier);
+ } elsif ($data->ah_action eq 'rename') {
+ $output .= 'rename from ' . $data->ah_name;
+ } else {
+ $output .= $data->ah_action;
+ }
+ $output .= sprintf ("\n by %s from %s\n", $data->ah_by,
+ $data->ah_from);
+ }
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot read history for $self->{id}: $@");
+ return;
+ }
+ return $output;
+}
+
+# Given a principal, a scheme, and an identifier, check whether that ACL
+# scheme and identifier grant access to that principal. Return 1 if access
+# was granted, 0 if access was deined, and undef on some error. On error, the
+# error message is also added to the check_errors variable. This method is
+# internal to the class.
+#
+# Maintain ACL verifiers for all schemes we've seen in the local %verifier
+# hash so that we can optimize repeated ACL checks.
+{
+ my %verifier;
+ sub check_line {
+ my ($self, $principal, $scheme, $identifier) = @_;
+ unless ($verifier{$scheme}) {
+ my $class = $self->scheme_mapping ($scheme);
+ unless ($class) {
+ push (@{ $self->{check_errors} }, "unknown scheme $scheme");
+ return;
+ }
+ $verifier{$scheme} = $class->new;
+ unless (defined $verifier{$scheme}) {
+ push (@{ $self->{check_errors} }, "cannot verify $scheme");
+ return;
+ }
+ }
+ my $result = ($verifier{$scheme})->check ($principal, $identifier);
+ if (not defined $result) {
+ push (@{ $self->{check_errors} }, ($verifier{$scheme})->error);
+ return;
+ } else {
+ return $result;
+ }
+ }
+}
+
+# Given a principal, check whether it should be granted access according to
+# this ACL. Returns 1 if access was granted, 0 if access was denied, and
+# undef on some error. Errors from ACL verifiers do not cause an error
+# return, but are instead accumulated in the check_errors variable returned by
+# the check_errors() method.
+sub check {
+ my ($self, $principal) = @_;
+ unless ($principal) {
+ $self->error ('no principal specified');
+ return;
+ }
+ my @entries = $self->list;
+ return if (not @entries and $self->error);
+ my %verifier;
+ $self->{check_errors} = [];
+ for my $entry (@entries) {
+ my ($scheme, $identifier) = @$entry;
+ my $result = $self->check_line ($principal, $scheme, $identifier);
+ return 1 if $result;
+ }
+ return 0;
+}
+
+# Returns the errors from the last ACL verification as an array in array
+# context or as a string with newlines after each error in a scalar context.
+sub check_errors {
+ my ($self) = @_;
+ my @errors;
+ if ($self->{check_errors}) {
+ @errors = @{ $self->{check_errors} };
+ }
+ return wantarray ? @errors : join ("\n", @errors, '');
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=head1 NAME
+
+Wallet::ACL - Implementation of ACLs in the wallet system
+
+=for stopwords
+ACL DBH metadata HOSTNAME DATETIME timestamp Allbery verifier verifiers
+
+=head1 SYNOPSIS
+
+ my $acl = Wallet::ACL->create ('group:sysadmin');
+ $acl->rename ('group:unix');
+ $acl->add ('krb5', 'alice@EXAMPLE.COM', $admin, $host);
+ $acl->add ('krb5', 'bob@EXAMPLE.COM', $admin, $host);
+ if ($acl->check ($user)) {
+ print "Permission granted\n";
+ warn scalar ($acl->check_errors) if $acl->check_errors;
+ }
+ $acl->remove ('krb5', 'bob@EXAMPLE.COM', $admin, $host);
+ my @entries = $acl->list;
+ my $summary = $acl->show;
+ my $history = $acl->history;
+ $acl->destroy ($admin, $host);
+
+=head1 DESCRIPTION
+
+Wallet::ACL implements the ACL system for the wallet: the methods to
+create, find, rename, and destroy ACLs; the methods to add and remove
+entries from an ACL; and the methods to list the contents of an ACL and
+check a principal against it.
+
+An ACL is a list of zero or more ACL entries, each of which consists of a
+scheme and an identifier. Each scheme is associated with a verifier
+module that checks Kerberos principals against identifiers for that scheme
+and returns whether the principal should be permitted access by that
+identifier. The interpretation of the identifier is entirely left to the
+scheme. This module maintains the ACLs and dispatches check operations to
+the appropriate verifier module.
+
+Each ACL is identified by a human-readable name and a persistent unique
+numeric identifier. The numeric identifier (ID) should be used to refer
+to the ACL so that it can be renamed as needed without breaking external
+references.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new(ACL, SCHEMA)
+
+Instantiate a new ACL object with the given ACL ID or name. Takes the
+Wallet::Schema object to use for retrieving metadata from the wallet
+database. Returns a new ACL object if the ACL was found and throws an
+exception if it wasn't or on any other error.
+
+=item create(NAME, SCHEMA, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Similar to new() in that it instantiates a new ACL object, but instead of
+finding an existing one, creates a new ACL record in the database with the
+given NAME. NAME must not be all-numeric, since that would conflict with
+the automatically assigned IDs. Returns the new object on success and
+throws an exception on failure. PRINCIPAL, HOSTNAME, and DATETIME are
+stored as history information. PRINCIPAL should be the user who is
+creating the ACL. If DATETIME isn't given, the current time is used.
+
+=back
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item add(SCHEME, INSTANCE, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Add the given ACL entry (given by SCHEME and INSTANCE) to this ACL.
+Returns true on success and false on failure. On failure, the caller
+should call error() to get the error message. PRINCIPAL, HOSTNAME, and
+DATETIME are stored as history information. PRINCIPAL should be the user
+who is adding the ACL entry. If DATETIME isn't given, the current time is
+used.
+
+=item check(PRINCIPAL)
+
+Checks whether the given PRINCIPAL should be allowed access given ACL.
+Returns 1 if access was granted, 0 if access is declined, and undef on
+error. On error, the caller should call error() to get the error text.
+Any errors found by the individual ACL verifiers can be retrieved by
+calling check_errors(). Errors from individual ACL verifiers will not
+result in an error return from check(); instead, the check will continue
+with the next entry in the ACL.
+
+check() returns success as soon as an entry in the ACL grants access to
+PRINCIPAL. There is no provision for negative ACLs or exceptions.
+
+=item check_errors()
+
+Return (as a list in array context and a string with newlines between
+errors and at the end of the last error in scalar context) the errors, if
+any, returned by ACL verifiers for the last check operation. If there
+were no errors from the last check() operation, returns the empty list in
+array context and undef in scalar context.
+
+=item destroy(PRINCIPAL, HOSTNAME [, DATETIME])
+
+Destroys this ACL from the database. Note that this will fail if the ACL
+is still referenced by any object; the ACL must be removed from all
+objects first. Returns true on success and false on failure. On failure,
+the caller should call error() to get the error message. PRINCIPAL,
+HOSTNAME, and DATETIME are stored as history information. PRINCIPAL
+should be the user who is destroying the ACL. If DATETIME isn't given,
+the current time is used.
+
+=item error()
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+=item history()
+
+Returns the human-readable history of this ACL. Each action that changes
+the ACL (not including changes to the name of the ACL) will be represented
+by two lines. The first line will have a timestamp of the change followed
+by a description of the change, and the second line will give the user who
+made the change and the host from which the change was made. On failure,
+returns undef, and the caller should call error() to get the error
+message.
+
+=item id()
+
+Returns the numeric system-generated ID of this ACL.
+
+=item list()
+
+Returns all the entries of this ACL. The return value will be a list of
+references to pairs of scheme and identifier. For example, for an ACL
+containing two entries, both of scheme C<krb5> and with values
+C<alice@EXAMPLE.COM> and C<bob@EXAMPLE.COM>, list() would return:
+
+ ([ 'krb5', 'alice@EXAMPLE.COM' ], [ 'krb5', 'bob@EXAMPLE.COM' ])
+
+Returns the empty list on failure. To distinguish between this and the
+ACL containing no entries, the caller should call error(). error() is
+guaranteed to return the error message if there was an error and undef if
+there was no error.
+
+=item name()
+
+Returns the human-readable name of this ACL.
+
+=item remove(SCHEME, INSTANCE, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Remove the given ACL line (given by SCHEME and INSTANCE) from this ACL.
+Returns true on success and false on failure. On failure, the caller
+should call error() to get the error message. PRINCIPAL, HOSTNAME, and
+DATETIME are stored as history information. PRINCIPAL should be the user
+who is removing the ACL entry. If DATETIME isn't given, the current time
+is used.
+
+=item rename(NAME)
+
+Rename this ACL. This changes the name used for human convenience but not
+the system-generated ACL ID that is used to reference this ACL. The new
+NAME must not be all-numeric, since that would conflict with
+system-generated ACL IDs. Returns true on success and false on failure.
+On failure, the caller should call error() to get the error message.
+
+Note that rename() operations are not logged in the ACL history.
+
+=item show()
+
+Returns a human-readable description of this ACL, including its
+membership. This method should only be used for display of the ACL to
+humans. Use the list(), name(), and id() methods instead to get ACL
+information for use in other code. On failure, returns undef, and the
+caller should call error() to get the error message.
+
+=back
+
+=head1 SEE ALSO
+
+Wallet::ACL::Base(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/ACL/Base.pm b/perl/lib/Wallet/ACL/Base.pm
new file mode 100644
index 0000000..a2b07cc
--- /dev/null
+++ b/perl/lib/Wallet/ACL/Base.pm
@@ -0,0 +1,126 @@
+# Wallet::ACL::Base -- Parent class for wallet ACL verifiers.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL::Base;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($VERSION);
+
+# 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.02';
+
+##############################################################################
+# Interface
+##############################################################################
+
+# Creates a new persistant verifier, taking a database handle. This parent
+# class just creates an empty object and ignores the handle. Child classes
+# should override if there are necessary initialization tasks or if the handle
+# will be used by the verifier.
+sub new {
+ my $type = shift;
+ my $self = {};
+ bless ($self, $type);
+ return $self;
+}
+
+# The default check method denies all access.
+sub check {
+ return 0;
+}
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+ACL Allbery verifier verifiers
+
+=head1 NAME
+
+Wallet::ACL::Base - Generic parent class for wallet ACL verifiers
+
+=head1 SYNOPSIS
+
+ package Wallet::ACL::Simple
+ @ISA = qw(Wallet::ACL::Base);
+ sub check {
+ my ($self, $principal, $acl) = @_;
+ return ($principal eq $acl) ? 1 : 0;
+ }
+
+=head1 DESCRIPTION
+
+Wallet::ACL::Base is the generic parent class for wallet ACL verifiers.
+It provides default functions and behavior and all ACL verifiers should
+inherit from it. It is not used directly.
+
+=head1 METHODS
+
+=over 4
+
+=item new()
+
+Creates a new ACL verifier. The generic function provided here just
+creates and blesses an object.
+
+=item check(PRINCIPAL, ACL)
+
+This method should always be overridden by child classes. The default
+implementation just declines all access.
+
+=item error([ERROR ...])
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+For the convenience of child classes, this method can also be called with
+one or more error strings. If so, those strings are concatenated
+together, trailing newlines are removed, any text of the form S<C< at \S+
+line \d+\.?>> at the end of the message is stripped off, and the result is
+stored as the error. Only child classes should call this method with an
+error string.
+
+=back
+
+=head1 SEE ALSO
+
+Wallet::ACL(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/ACL/Krb5.pm b/perl/lib/Wallet/ACL/Krb5.pm
new file mode 100644
index 0000000..80d32bd
--- /dev/null
+++ b/perl/lib/Wallet/ACL/Krb5.pm
@@ -0,0 +1,126 @@
+# Wallet::ACL::Krb5 -- Wallet Kerberos v5 principal ACL verifier.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL::Krb5;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::ACL::Base;
+
+@ISA = qw(Wallet::ACL::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.02';
+
+##############################################################################
+# Interface
+##############################################################################
+
+# The most trivial ACL verifier. Returns true if the provided principal
+# matches the ACL.
+sub check {
+ my ($self, $principal, $acl) = @_;
+ unless ($principal) {
+ $self->error ('no principal specified');
+ return;
+ }
+ unless ($acl) {
+ $self->error ('malformed krb5 ACL');
+ return;
+ }
+ return ($principal eq $acl) ? 1 : 0;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+ACL krb5 Allbery verifier
+
+=head1 NAME
+
+Wallet::ACL::Krb5 - Simple wallet ACL verifier for Kerberos principals
+
+=head1 SYNOPSIS
+
+ my $verifier = Wallet::ACL::Krb5->new;
+ my $status = $verifier->check ($principal, $acl);
+ if (not defined $status) {
+ die "Something failed: ", $verifier->error, "\n";
+ } elsif ($status) {
+ print "Access granted\n";
+ } else {
+ print "Access denied\n";
+ }
+
+=head1 DESCRIPTION
+
+Wallet::ACL::Krb5 is the simplest wallet ACL verifier, used to verify ACL
+lines of type C<krb5>. The value of such an ACL is a simple Kerberos
+principal in its text display form, and the ACL grants access to a given
+principal if and only if the principal exactly matches the ACL.
+
+=head1 METHODS
+
+=over 4
+
+=item new()
+
+Creates a new ACL verifier. For this verifier, there is no setup work.
+
+=item check(PRINCIPAL, ACL)
+
+Returns true if PRINCIPAL matches ACL, false if not, and undef on an error
+(see L<"DIAGNOSTICS"> below).
+
+=item error()
+
+Returns the error if check() returned undef.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=over 4
+
+=item malformed krb5 ACL
+
+The ACL parameter to check() was malformed. Currently, this error is only
+given if ACL is undefined or the empty string.
+
+=item no principal specified
+
+The PRINCIPAL parameter to check() was undefined or the empty string.
+
+=back
+
+=head1 SEE ALSO
+
+Wallet::ACL(3), Wallet::ACL::Base(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/ACL/Krb5/Regex.pm b/perl/lib/Wallet/ACL/Krb5/Regex.pm
new file mode 100644
index 0000000..4934cfc
--- /dev/null
+++ b/perl/lib/Wallet/ACL/Krb5/Regex.pm
@@ -0,0 +1,134 @@
+# Wallet::ACL::Krb5::Regex -- Wallet Kerberos v5 principal regex ACL verifier
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL::Krb5::Regex;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::ACL::Krb5;
+
+@ISA = qw(Wallet::ACL::Krb5);
+
+# 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';
+
+##############################################################################
+# Interface
+##############################################################################
+
+# Returns true if the Perl regular expression specified by the ACL matches
+# the provided Kerberos principal.
+sub check {
+ my ($self, $principal, $acl) = @_;
+ unless ($principal) {
+ $self->error ('no principal specified');
+ return;
+ }
+ unless ($acl) {
+ $self->error ('no ACL specified');
+ return;
+ }
+ my $regex = eval { qr/$acl/ };
+ if ($@) {
+ $self->error ('malformed krb5-regex ACL');
+ return;
+ }
+ return ($principal =~ m/$regex/) ? 1 : 0;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+ACL krb5-regex Durkacz Allbery verifier
+
+=head1 NAME
+
+Wallet::ACL::Krb5::Regex - Regex wallet ACL verifier for Kerberos principals
+
+=head1 SYNOPSIS
+
+ my $verifier = Wallet::ACL::Krb5::Regex->new;
+ my $status = $verifier->check ($principal, $acl);
+ if (not defined $status) {
+ die "Something failed: ", $verifier->error, "\n";
+ } elsif ($status) {
+ print "Access granted\n";
+ } else {
+ print "Access denied\n";
+ }
+
+=head1 DESCRIPTION
+
+Wallet::ACL::Krb5::Regex is the wallet ACL verifier used to verify ACL
+lines of type C<krb5-regex>. The value of such an ACL is a Perl regular
+expression, and the ACL grants access to a given Kerberos principal if and
+only if the regular expression matches that principal.
+
+=head1 METHODS
+
+=over 4
+
+=item new()
+
+Creates a new ACL verifier. For this verifier, there is no setup work.
+
+=item check(PRINCIPAL, ACL)
+
+Returns true if the Perl regular expression specified by the ACL matches the
+PRINCIPAL, false if not, and undef on an error (see L<"DIAGNOSTICS"> below).
+
+=item error()
+
+Returns the error if check() returned undef.
+
+=back
+
+=head1 DIAGNOSTICS
+
+=over 4
+
+=item malformed krb5-regex ACL
+
+The ACL parameter to check() was a malformed Perl regular expression.
+
+=item no principal specified
+
+The PRINCIPAL parameter to check() was undefined or the empty string.
+
+=item no ACL specified
+
+The ACL parameter to check() was undefined or the empty string.
+
+=back
+
+=head1 SEE ALSO
+
+Wallet::ACL(3), Wallet::ACL::Base(3), Wallet::ACL::Krb5(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Ian Durkacz
+
+=cut
diff --git a/perl/lib/Wallet/ACL/LDAP/Attribute.pm b/perl/lib/Wallet/ACL/LDAP/Attribute.pm
new file mode 100644
index 0000000..c27729e
--- /dev/null
+++ b/perl/lib/Wallet/ACL/LDAP/Attribute.pm
@@ -0,0 +1,264 @@
+# Wallet::ACL::LDAP::Attribute -- Wallet LDAP attribute ACL verifier.
+#
+# Written by Russ Allbery
+# Copyright 2012, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL::LDAP::Attribute;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Authen::SASL ();
+use Net::LDAP qw(LDAP_COMPARE_TRUE);
+use Wallet::ACL::Base;
+use Wallet::Config;
+
+@ISA = qw(Wallet::ACL::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';
+
+##############################################################################
+# Interface
+##############################################################################
+
+# Create a new persistant verifier. Load the Net::LDAP module and open a
+# persistant LDAP server connection that we'll use for later calls.
+sub new {
+ my $type = shift;
+ my $host = $Wallet::Config::LDAP_HOST;
+ my $base = $Wallet::Config::LDAP_BASE;
+ unless ($host and defined ($base) and $Wallet::Config::LDAP_CACHE) {
+ die "LDAP attribute ACL support not configured\n";
+ }
+
+ # Ensure the required Perl modules are available and bind to the directory
+ # server. Catch any errors with a try/catch block.
+ my $ldap;
+ eval {
+ local $ENV{KRB5CCNAME} = $Wallet::Config::LDAP_CACHE;
+ my $sasl = Authen::SASL->new (mechanism => 'GSSAPI');
+ $ldap = Net::LDAP->new ($host, onerror => 'die');
+ my $mesg = eval { $ldap->bind (undef, sasl => $sasl) };
+ };
+ if ($@) {
+ my $error = $@;
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ die "LDAP attribute ACL support not available: $error\n";
+ }
+
+ # We successfully bound, so create our object and return it.
+ my $self = { ldap => $ldap };
+ bless ($self, $type);
+ return $self;
+}
+
+# Check whether a given principal has the required LDAP attribute. We first
+# map the principal to a DN by doing a search for that principal (and bailing
+# if we get more than one entry). Then, we do a compare to see if that DN has
+# the desired attribute and value.
+#
+# If the ldap_map_principal sub is defined in Wallet::Config, call it on the
+# principal first to map it to the value for which we'll search.
+#
+# The connection is configured to die on any error, so we do all the work in a
+# try/catch block to report errors.
+sub check {
+ my ($self, $principal, $acl) = @_;
+ undef $self->{error};
+ unless ($principal) {
+ $self->error ('no principal specified');
+ return;
+ }
+ my ($attr, $value);
+ if ($acl) {
+ ($attr, $value) = split ('=', $acl, 2);
+ }
+ unless (defined ($attr) and defined ($value)) {
+ $self->error ('malformed ldap-attr ACL');
+ return;
+ }
+ my $ldap = $self->{ldap};
+
+ # Map the principal name to an attribute value for our search if we're
+ # doing a custom mapping.
+ if (defined &Wallet::Config::ldap_map_principal) {
+ eval { $principal = Wallet::Config::ldap_map_principal ($principal) };
+ if ($@) {
+ $self->error ("mapping principal to LDAP failed: $@");
+ return;
+ }
+ }
+
+ # Now, map the user to a DN by doing a search.
+ my $entry;
+ eval {
+ my $fattr = $Wallet::Config::LDAP_FILTER_ATTR || 'krb5PrincipalName';
+ my $filter = "($fattr=$principal)";
+ my $base = $Wallet::Config::LDAP_BASE;
+ my @options = (base => $base, filter => $filter, attrs => [ 'dn' ]);
+ my $search = $ldap->search (@options);
+ if ($search->count == 1) {
+ $entry = $search->pop_entry;
+ } elsif ($search->count > 1) {
+ die $search->count . " LDAP entries found for $principal";
+ }
+ };
+ if ($@) {
+ $self->error ("cannot search for $principal in LDAP: $@");
+ return;
+ }
+ return 0 unless $entry;
+
+ # We have a user entry. We can now check whether that user has the
+ # desired attribute and value.
+ my $result;
+ eval {
+ my $mesg = $ldap->compare ($entry, attr => $attr, value => $value);
+ $result = $mesg->code;
+ };
+ if ($@) {
+ $self->error ("cannot check LDAP attribute $attr for $principal: $@");
+ return;
+ }
+ return ($result == LDAP_COMPARE_TRUE) ? 1 : 0;
+}
+
+1;
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+ACL Allbery verifier LDAP PRINCIPAL's DN ldap-attr
+
+=head1 NAME
+
+Wallet::ACL::LDAP::Attribute - Wallet ACL verifier for LDAP attribute compares
+
+=head1 SYNOPSIS
+
+ my $verifier = Wallet::ACL::LDAP::Attribute->new;
+ my $status = $verifier->check ($principal, "$attr=$value");
+ if (not defined $status) {
+ die "Something failed: ", $verifier->error, "\n";
+ } elsif ($status) {
+ print "Access granted\n";
+ } else {
+ print "Access denied\n";
+ }
+
+=head1 DESCRIPTION
+
+Wallet::ACL::LDAP::Attribute checks whether the LDAP record for the entry
+corresponding to a principal contains an attribute with a particular
+value. It is used to verify ACL lines of type C<ldap-attr>. The value of
+such an ACL is an attribute followed by an equal sign and a value, and the
+ACL grants access to a given principal if and only if the LDAP entry for
+that principal has that attribute set to that value.
+
+To use this object, several configuration parameters must be set. See
+L<Wallet::Config> for details on those configuration parameters and
+information about how to set wallet configuration.
+
+=head1 METHODS
+
+=over 4
+
+=item new()
+
+Creates a new ACL verifier. Opens and binds the connection to the LDAP
+server.
+
+=item check(PRINCIPAL, ACL)
+
+Returns true if PRINCIPAL is granted access according to ACL, false if
+not, and undef on an error (see L<"DIAGNOSTICS"> below). ACL must be an
+attribute name and a value, separated by an equal sign (with no
+whitespace). PRINCIPAL will be granted access if its LDAP entry contains
+that attribute with that value.
+
+=item error()
+
+Returns the error if check() returned undef.
+
+=back
+
+=head1 DIAGNOSTICS
+
+The new() method may fail with one of the following exceptions:
+
+=over 4
+
+=item LDAP attribute ACL support not available: %s
+
+Attempting to connect or bind to the LDAP server failed.
+
+=item LDAP attribute ACL support not configured
+
+The required configuration parameters were not set. See Wallet::Config(3)
+for the required configuration parameters and how to set them.
+
+=back
+
+Verifying an LDAP attribute ACL may fail with the following errors
+(returned by the error() method):
+
+=over 4
+
+=item cannot check LDAP attribute %s for %s: %s
+
+The LDAP compare to check for the required attribute failed. The
+attribute may have been misspelled, or there may be LDAP directory
+permission issues. This error indicates that PRINCIPAL's entry was
+located in LDAP, but the check failed during the compare to verify the
+attribute value.
+
+=item cannot search for %s in LDAP: %s
+
+Searching for PRINCIPAL (possibly after ldap_map_principal() mapping)
+failed. This is often due to LDAP directory permissions issues. This
+indicates a failure during the mapping of PRINCIPAL to an LDAP DN.
+
+=item malformed ldap-attr ACL
+
+The ACL parameter to check() was malformed. Usually this means that
+either the attribute or the value were empty or the required C<=> sign
+separating them was missing.
+
+=item mapping principal to LDAP failed: %s
+
+There was an ldap_map_principal() function defined in the wallet
+configuration, but calling it for the PRINCIPAL argument failed.
+
+=item no principal specified
+
+The PRINCIPAL parameter to check() was undefined or the empty string.
+
+=back
+
+=head1 SEE ALSO
+
+Wallet::ACL(3), Wallet::ACL::Base(3), Wallet::Config(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/ACL/NetDB.pm b/perl/lib/Wallet/ACL/NetDB.pm
new file mode 100644
index 0000000..ad2164b
--- /dev/null
+++ b/perl/lib/Wallet/ACL/NetDB.pm
@@ -0,0 +1,268 @@
+# Wallet::ACL::NetDB -- Wallet NetDB role ACL verifier.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL::NetDB;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::ACL::Base;
+use Wallet::Config;
+
+@ISA = qw(Wallet::ACL::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.05';
+
+##############################################################################
+# Interface
+##############################################################################
+
+# Creates a new persistant verifier. Load the Net::Remctl module and open a
+# persistant remctl connection that we'll use for later calls.
+sub new {
+ my $type = shift;
+ my $host = $Wallet::Config::NETDB_REMCTL_HOST;
+ unless ($host and $Wallet::Config::NETDB_REMCTL_CACHE) {
+ die "NetDB ACL support not configured\n";
+ }
+ eval { require Net::Remctl };
+ if ($@) {
+ my $error = $@;
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ die "NetDB ACL support not available: $error\n";
+ }
+ local $ENV{KRB5CCNAME} = $Wallet::Config::NETDB_REMCTL_CACHE;
+ my $remctl = Net::Remctl->new;
+
+ # Net::Remctl 2.12 and later will support passing in an empty string for
+ # the principal. Until then, be careful not to pass principal unless it
+ # was specified.
+ my $port = $Wallet::Config::NETDB_REMCTL_PORT || 0;
+ my $principal = $Wallet::Config::NETDB_REMCTL_PRINCIPAL;
+ my $status;
+ if (defined $principal) {
+ $status = $remctl->open ($host, $port, $principal);
+ } else {
+ $status = $remctl->open ($host, $port);
+ }
+ unless ($status) {
+ die "cannot connect to NetDB remctl interface: ", $remctl->error, "\n";
+ }
+ my $self = { remctl => $remctl };
+ bless ($self, $type);
+ return $self;
+}
+
+# Check whether the given principal has one of the user, administrator, or
+# admin team roles in NetDB for the given host. Returns 1 if it does, 0 if it
+# doesn't, and undef, setting the error, if there's some failure in making the
+# remctl call.
+sub check {
+ my ($self, $principal, $acl) = @_;
+ unless ($principal) {
+ $self->error ('no principal specified');
+ return;
+ }
+ unless ($acl) {
+ $self->error ('malformed netdb ACL');
+ return;
+ }
+ my $remctl = $self->{remctl};
+ if ($Wallet::Config::NETDB_REALM) {
+ $principal =~ s/\@\Q$Wallet::Config::NETDB_REALM\E\z//;
+ }
+ unless ($remctl->command ('netdb', 'node-roles', $principal, $acl)) {
+ $self->error ('cannot check NetDB ACL: ' . $remctl->error);
+ return;
+ }
+ my ($roles, $output, $status, $error);
+ do {
+ $output = $remctl->output;
+ if ($output->type eq 'output') {
+ if ($output->stream == 1) {
+ $roles .= $output->data;
+ } else {
+ $error .= $output->data;
+ }
+ } elsif ($output->type eq 'error') {
+ $self->error ('cannot check NetDB ACL: ' . $output->data);
+ return;
+ } elsif ($output->type eq 'status') {
+ $status = $output->status;
+ } else {
+ $self->error ('malformed NetDB remctl token: ' . $output->type);
+ return;
+ }
+ } while ($output->type eq 'output');
+ if ($status == 0) {
+ $roles ||= '';
+ my @roles = split (' ', $roles);
+ for my $role (@roles) {
+ return 1 if $role eq 'admin';
+ return 1 if $role eq 'team';
+ return 1 if $role eq 'user';
+ }
+ return 0;
+ } else {
+ if ($error) {
+ chomp $error;
+ $error =~ s/\n/ /g;
+ $self->error ("error checking NetDB ACL: $error");
+ } else {
+ $self->error ("error checking NetDB ACL");
+ }
+ return;
+ }
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+ACL NetDB remctl DNS DHCP Allbery netdb verifier
+
+=head1 NAME
+
+Wallet::ACL::NetDB - Wallet ACL verifier for NetDB roles
+
+=head1 SYNOPSIS
+
+ my $verifier = Wallet::ACL::NetDB->new;
+ my $status = $verifier->check ($principal, $node);
+ if (not defined $status) {
+ die "Something failed: ", $verifier->error, "\n";
+ } elsif ($status) {
+ print "Access granted\n";
+ } else {
+ print "Access denied\n";
+ }
+
+=head1 DESCRIPTION
+
+Wallet::ACL::NetDB checks a principal against the NetDB roles for a given
+host. It is used to verify ACL lines of type C<netdb>. The value of such
+an ACL is a node, and the ACL grants access to a given principal if and
+only if that principal has one of the roles user, admin, or team for that
+node.
+
+To use this object, several configuration parameters must be set. See
+L<Wallet::Config> for details on those configuration parameters and
+information about how to set wallet configuration.
+
+=head1 METHODS
+
+=over 4
+
+=item new()
+
+Creates a new ACL verifier. Opens the remctl connection to the NetDB
+server and authenticates.
+
+=item check(PRINCIPAL, ACL)
+
+Returns true if PRINCIPAL is granted access according to ACL, false if
+not, and undef on an error (see L<"DIAGNOSTICS"> below). ACL is a node,
+and PRINCIPAL will be granted access if it (with the realm stripped off if
+configured) has the user, admin, or team role for that node.
+
+=item error()
+
+Returns the error if check() returned undef.
+
+=back
+
+=head1 DIAGNOSTICS
+
+The new() method may fail with one of the following exceptions:
+
+=over 4
+
+=item NetDB ACL support not available: %s
+
+The Net::Remctl Perl module, required for NetDB ACL support, could not be
+loaded.
+
+=item NetDB ACL support not configured
+
+The required configuration parameters were not set. See Wallet::Config(3)
+for the required configuration parameters and how to set them.
+
+=item cannot connect to NetDB remctl interface: %s
+
+Connecting to the NetDB remctl interface failed with the given error
+message.
+
+=back
+
+Verifying a NetDB ACL may fail with the following errors (returned by the
+error() method):
+
+=over 4
+
+=item cannot check NetDB ACL: %s
+
+Issuing the remctl command to get the roles for the given principal failed
+or returned an error.
+
+=item error checking NetDB ACL: %s
+
+The NetDB remctl interface that returns the roles for a user returned an
+error message or otherwise returned failure.
+
+=item malformed netdb ACL
+
+The ACL parameter to check() was malformed. Currently, this error is only
+given if ACL is undefined or the empty string.
+
+=item malformed NetDB remctl token: %s
+
+The Net::Remctl Perl library returned a malformed token. This should
+never happen and indicates a bug in Net::Remctl.
+
+=item no principal specified
+
+The PRINCIPAL parameter to check() was undefined or the empty string.
+
+=back
+
+=head1 CAVEATS
+
+The list of possible NetDB roles that should be considered sufficient to
+grant access is not currently configurable.
+
+=head1 SEE ALSO
+
+Net::Remctl(3), Wallet::ACL(3), Wallet::ACL::Base(3), Wallet::Config(3),
+wallet-backend(8)
+
+NetDB is a free software system for managing DNS, DHCP, and related
+machine information for large organizations. For more information on
+NetDB, see L<http://www.stanford.edu/group/networking/netdb/>.
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/ACL/NetDB/Root.pm b/perl/lib/Wallet/ACL/NetDB/Root.pm
new file mode 100644
index 0000000..34163e7
--- /dev/null
+++ b/perl/lib/Wallet/ACL/NetDB/Root.pm
@@ -0,0 +1,129 @@
+# Wallet::ACL::NetDB::Root -- Wallet NetDB role ACL verifier (root instances).
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::ACL::NetDB::Root;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::ACL::NetDB;
+use Wallet::Config;
+
+@ISA = qw(Wallet::ACL::NetDB);
+
+# 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.02';
+
+##############################################################################
+# Interface
+##############################################################################
+
+# Override the check method of Wallet::ACL::NetDB to require that the
+# principal be a root instance and to strip /root out of the principal name
+# before checking roles.
+sub check {
+ my ($self, $principal, $acl) = @_;
+ unless ($principal) {
+ $self->error ('no principal specified');
+ return;
+ }
+ unless ($principal =~ s%^([^/\@]+)/root(\@|\z)%$1$2%) {
+ return 0;
+ }
+ return $self->SUPER::check ($principal, $acl);
+}
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+ACL NetDB DNS DHCP Allbery verifier
+
+=head1 NAME
+
+Wallet::ACL::NetDB::Root - Wallet ACL verifier for NetDB roles (root instances)
+
+=head1 SYNOPSIS
+
+ my $verifier = Wallet::ACL::NetDB::Root->new;
+ my $status = $verifier->check ($principal, $node);
+ if (not defined $status) {
+ die "Something failed: ", $verifier->error, "\n";
+ } elsif ($status) {
+ print "Access granted\n";
+ } else {
+ print "Access denied\n";
+ }
+
+=head1 DESCRIPTION
+
+Wallet::ACL::NetDB::Root works identically to Wallet::ACL::NetDB except
+that it requires the principal to be a root instance (in other words, to
+be in the form <principal>/root@<realm>) and strips the C</root> portion
+from the principal before checking against NetDB roles. As with the base
+NetDB ACL verifier, the value of a C<netdb-root> ACL is a node, and the
+ACL grants access to a given principal if and only if the that principal
+(with C</root> stripped) has one of the roles user, admin, or team for
+that node.
+
+To use this object, the same configuration parameters must be set as for
+Wallet::ACL::NetDB. See Wallet::Config(3) for details on those
+configuration parameters and information about how to set wallet
+configuration.
+
+=head1 METHODS
+
+=over 4
+
+=item check(PRINCIPAL, ACL)
+
+Returns true if PRINCIPAL is granted access according to ACL, false if
+not, and undef on an error (see L<"DIAGNOSTICS"> below). ACL is a node,
+and PRINCIPAL will be granted access if it has an instance of C<root> and
+if (with C</root> stripped off and the realm stripped off if configured)
+has the user, admin, or team role for that node.
+
+=back
+
+=head1 DIAGNOSTICS
+
+Same as for Wallet::ACL::NetDB.
+
+=head1 CAVEATS
+
+The instance to strip is not currently configurable.
+
+The list of possible NetDB roles that should be considered sufficient to
+grant access is not currently configurable.
+
+=head1 SEE ALSO
+
+Net::Remctl(3), Wallet::ACL(3), Wallet::ACL::Base(3),
+Wallet::ACL::NetDB(3), Wallet::Config(3), wallet-backend(8)
+
+NetDB is a free software system for managing DNS, DHCP, and related
+machine information for large organizations. For more information on
+NetDB, see L<http://www.stanford.edu/group/networking/netdb/>.
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm
new file mode 100644
index 0000000..8481979
--- /dev/null
+++ b/perl/lib/Wallet/Admin.pm
@@ -0,0 +1,389 @@
+# Wallet::Admin -- Wallet system administrative interface.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2008, 2009, 2010, 2011, 2012, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Admin;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($VERSION);
+
+use Wallet::ACL;
+use Wallet::Schema;
+
+# 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.07';
+
+# The last non-DBIx::Class version of Wallet::Schema. If a database has no
+# DBIx::Class versioning, we artificially install this version number before
+# starting the upgrade process so that the automated DBIx::Class upgrade will
+# work properly.
+our $BASE_VERSION = '0.07';
+
+##############################################################################
+# Constructor, destructor, and accessors
+##############################################################################
+
+# Create a new wallet administrator object. Opens a connection to the
+# database that will be used for all of the wallet configuration information.
+# Throw an exception if anything goes wrong.
+sub new {
+ my ($class) = @_;
+ my $schema = Wallet::Schema->connect;
+ my $self = { schema => $schema };
+ bless ($self, $class);
+ return $self;
+}
+
+# Returns the database handle (used mostly for testing).
+sub dbh {
+ my ($self) = @_;
+ return $self->{schema}->storage->dbh;
+}
+
+# Returns the DBIx::Class-based database schema object.
+sub schema {
+ my ($self) = @_;
+ return $self->{schema};
+}
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+# Disconnect the database handle on object destruction to avoid warnings.
+sub DESTROY {
+ my ($self) = @_;
+ $self->{schema}->storage->dbh->disconnect;
+}
+
+##############################################################################
+# Database initialization
+##############################################################################
+
+# Initializes the database by populating it with our schema and then creates
+# and returns a new wallet server object. This is used only for initial
+# database creation. Takes the Kerberos principal who will be the default
+# administrator so that we can create an initial administrator ACL. Returns
+# true on success and false on failure, setting the object error.
+sub initialize {
+ my ($self, $user) = @_;
+
+ # Deploy the database schema from DDL files, if they exist. If not then
+ # we automatically get the database from the Schema modules.
+ $self->{schema}->deploy ({}, $Wallet::Config::DB_DDL_DIRECTORY);
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ $self->default_data;
+
+ # Create a default admin ACL.
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ $self->{schema}->resultset ('Acl')->populate ([
+ [ qw/ac_id ac_name/ ],
+ [ 1, 'ADMIN' ],
+ ]);
+ $self->{schema}->resultset ('AclEntry')->populate ([
+ [ qw/ae_id ae_scheme ae_identifier/ ],
+ [ 1, 'krb5', $user ],
+ ]);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot add ADMIN ACL: $@");
+ return;
+ }
+ return 1;
+}
+
+# Load default data into various tables. We'd like to do this more directly
+# in the schema definitions, but not yet seeing a good way to do that.
+sub default_data {
+ my ($self) = @_;
+
+ # acl_schemes default rows.
+ my ($r1) = $self->{schema}->resultset('AclScheme')->populate ([
+ [ qw/as_name as_class/ ],
+ [ 'krb5', 'Wallet::ACL::Krb5' ],
+ [ 'krb5-regex', 'Wallet::ACL::Krb5::Regex' ],
+ [ 'ldap-attr', 'Wallet::ACL::LDAP::Attribute' ],
+ [ 'netdb', 'Wallet::ACL::NetDB' ],
+ [ 'netdb-root', 'Wallet::ACL::NetDB::Root' ],
+ ]);
+ warn "default AclScheme not installed" unless defined $r1;
+
+ # types default rows.
+ my @record = ([ qw/ty_name ty_class/ ],
+ [ 'file', 'Wallet::Object::File' ],
+ [ 'keytab', 'Wallet::Object::Keytab' ],
+ [ 'wa-keyring', 'Wallet::Object::WAKeyring' ]);
+ ($r1) = $self->{schema}->resultset('Type')->populate (\@record);
+ warn "default Type not installed" unless defined $r1;
+
+ # enctypes default rows.
+ @record = ([ qw/en_name/ ],
+ [ 'aes128-cts-hmac-sha1-96' ],
+ [ 'aes256-cts-hmac-sha1-96' ],
+ [ 'arcfour-hmac-md5' ],
+ [ 'des-cbc-crc' ],
+ [ 'des3-cbc-sha1' ]);
+ ($r1) = $self->{schema}->resultset('Enctype')->populate (\@record);
+ warn "default Enctype not installed" unless defined $r1;
+
+ return 1;
+}
+
+# The same as initialize, but also drops any existing tables first before
+# creating the schema. Takes the same arguments. Returns true on success and
+# false on failure.
+sub reinitialize {
+ my ($self, $user) = @_;
+ return unless $self->destroy;
+ return $self->initialize ($user);
+}
+
+# Drop the database, including all of its data. Returns true on success and
+# false on failure.
+sub destroy {
+ my ($self) = @_;
+
+ # Get an actual DBI handle and use it to delete all tables.
+ my $dbh = $self->dbh;
+ my @tables = qw/acl_entries object_history objects acls acl_history
+ acl_schemes enctypes flags keytab_enctypes keytab_sync sync_targets
+ duo types dbix_class_schema_versions/;
+ for my $table (@tables) {
+ my $sql = "DROP TABLE IF EXISTS $table";
+ $dbh->do ($sql);
+ }
+
+ return 1;
+}
+
+# Save a DDL of the database in every supported database server. Returns
+# true on success and false on failure.
+sub backup {
+ my ($self, $oldversion) = @_;
+
+ my @dbs = qw/MySQL SQLite PostgreSQL/;
+ my $version = $Wallet::Schema::VERSION;
+ $self->{schema}->create_ddl_dir (\@dbs, $version,
+ $Wallet::Config::DB_DDL_DIRECTORY,
+ $oldversion);
+
+ return 1;
+}
+
+# Upgrade the database to the latest schema version. Returns true on success
+# and false on failure.
+sub upgrade {
+ my ($self) = @_;
+
+ # Check to see if the database is versioned. If not, install the
+ # versioning table and default version.
+ if (!$self->{schema}->get_db_version) {
+ $self->{schema}->install ($BASE_VERSION);
+ }
+
+ # Suppress warnings that actually are just informational messages.
+ local $SIG{__WARN__} = sub {
+ my ($warn) = @_;
+ return if $warn =~ m{Upgrade not necessary};
+ return if $warn =~ m{Attempting upgrade};
+ warn $warn;
+ };
+
+ # Perform the actual upgrade.
+ if ($self->{schema}->get_db_version) {
+ $self->{schema}->upgrade_directory ($Wallet::Config::DB_DDL_DIRECTORY);
+ eval { $self->{schema}->upgrade; };
+ }
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+
+ return 1;
+}
+
+##############################################################################
+# Object registration
+##############################################################################
+
+# Given an object type and class name, add a new class mapping to that
+# database for the given object type. This is used to register new object
+# types. Returns true on success, false on failure, and sets the internal
+# error on failure.
+sub register_object {
+ my ($self, $type, $class) = @_;
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %record = (ty_name => $type,
+ ty_class => $class);
+ $self->{schema}->resultset('Type')->create (\%record);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot register $class for $type: $@");
+ return;
+ }
+ return 1;
+}
+
+# Given an ACL verifier scheme and class name, add a new class mapping to that
+# database for the given ACL verifier scheme. This is used to register new
+# ACL schemes. Returns true on success, false on failure, and sets the
+# internal error on failure.
+sub register_verifier {
+ my ($self, $scheme, $class) = @_;
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %record = (as_name => $scheme,
+ as_class => $class);
+ $self->{schema}->resultset('AclScheme')->create (\%record);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot register $class for $scheme: $@");
+ return;
+ }
+ return 1;
+}
+
+1;
+__DATA__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=head1 NAME
+
+Wallet::Admin - Wallet system administrative interface
+
+=for stopwords
+ACL hostname Allbery verifier
+
+=head1 SYNOPSIS
+
+ use Wallet::Admin;
+ my $admin = Wallet::Admin->new;
+ unless ($admin->initialize ('user/admin@EXAMPLE.COM')) {
+ die $admin->error;
+ }
+
+=head1 DESCRIPTION
+
+Wallet::Admin implements the administrative interface to the wallet server
+and database. It is normally instantiated and used by B<wallet-admin>, a
+thin wrapper around this object that provides a command-line interface to
+its actions.
+
+To use this object, several configuration variables must be set (at least
+the database configuration). For information on those variables and how
+to set them, see L<Wallet::Config>. For more information on the normal
+user interface to the wallet server, see L<Wallet::Server>.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new()
+
+Creates a new wallet administrative object and connects to the database.
+On any error, this method throws an exception.
+
+=back
+
+=head1 INSTANCE METHODS
+
+For all methods that can fail, the caller should call error() after a
+failure to get the error message.
+
+=over 4
+
+=item destroy ()
+
+Destroys the database, deleting all of its data and all of the tables used
+by the wallet server. Returns true on success and false on failure.
+
+=item error ()
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+=item initialize(PRINCIPAL)
+
+Initializes the database as configured in Wallet::Config and loads the
+wallet database schema. Then, creates an ACL with the name ADMIN and adds
+an ACL entry of scheme C<krb5> and instance PRINCIPAL to that ACL. This
+bootstraps the authorization system and lets that Kerberos identity make
+further changes to the ADMIN ACL and the rest of the wallet database.
+Returns true on success and false on failure.
+
+initialize() uses C<localhost> as the hostname and PRINCIPAL as the user
+when logging the history of the ADMIN ACL creation and for any subsequent
+actions on the object it returns.
+
+=item register_object (TYPE, CLASS)
+
+Register in the database a mapping from the object type TYPE to the class
+CLASS. Returns true on success and false on failure (including when the
+verifier is already registered).
+
+=item register_verifier (SCHEME, CLASS)
+
+Register in the database a mapping from the ACL scheme SCHEME to the class
+CLASS. Returns true on success and false on failure (including when the
+verifier is already registered).
+
+=item reinitialize (PRINCIPAL)
+
+Performs the same actions as initialize(), but first drops any existing
+wallet database tables from the database, allowing this function to be
+called on a prior wallet database. All data stored in the database will
+be deleted and a fresh set of wallet database tables will be created.
+This method is equivalent to calling destroy() followed by initialize().
+Returns true on success and false on failure.
+
+=item upgrade ()
+
+Upgrades the database to the latest schema version, preserving data as
+much as possible. Returns true on success and false on failure.
+
+=back
+
+=head1 SEE ALSO
+
+wallet-admin(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Config.pm b/perl/lib/Wallet/Config.pm
new file mode 100644
index 0000000..527658c
--- /dev/null
+++ b/perl/lib/Wallet/Config.pm
@@ -0,0 +1,827 @@
+# Wallet::Config -- Configuration handling for the wallet server.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2008, 2010, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Config;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($PATH $VERSION);
+
+# 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';
+
+# Path to the config file to load.
+$PATH = $ENV{WALLET_CONFIG} || '/etc/wallet/wallet.conf';
+
+=head1 NAME
+
+Wallet::Config - Configuration handling for the wallet server
+
+=for stopwords
+DBI DSN SQLite subdirectories KEYTAB keytab kadmind KDC add-ons kadmin DNS
+SRV kadmin keytabs remctl backend lowercased NETDB ACL NetDB unscoped
+usernames rekey hostnames Allbery wallet-backend keytab-backend Heimdal
+rekeys WebAuth WEBAUTH keyring LDAP DN GSS-API integrations
+
+=head1 SYNOPSIS
+
+ use Wallet::Config;
+ my $driver = $Wallet::Config::DB_DRIVER;
+ my $info;
+ if (defined $Wallet::Config::DB_INFO) {
+ $info = $Wallet::Config::DB_INFO;
+ } else {
+ $info = "database=$Wallet::Config::DB_NAME";
+ $info .= ";host=$Wallet::Config::DB_HOST"
+ if $Wallet::Config::DB_HOST;
+ $info .= ";port=$Wallet::Config::DB_PORT"
+ if $Wallet::Config::DB_PORT;
+ }
+ my $dsn = "dbi:$driver:$info";
+ my $user = $Wallet::Config::DB_USER;
+ my $password = $Wallet::Config::DB_PASSWORD;
+ my $dbh = DBI->connect ($dsn, $user, $password);
+
+=head1 DESCRIPTION
+
+Wallet::Config encapsulates all of the site-specific configuration for the
+wallet server. It is implemented as a Perl class that declares and sets
+the defaults for various configuration variables and then, if it exists,
+loads the file specified by the WALLET_CONFIG environment variable or
+F</etc/wallet/wallet.conf> if that environment variable isn't set. That
+file should contain any site-specific overrides to the defaults, and at
+least some parameters must be set.
+
+This file must be valid Perl. To set a variable, use the syntax:
+
+ $VARIABLE = <value>;
+
+where VARIABLE is the variable name (always in all-capital letters) and
+<value> is the value. If setting a variable to a string and not a number,
+you should normally enclose <value> in C<''>. For example, to set the
+variable DB_DRIVER to C<MySQL>, use:
+
+ $DB_DRIVER = 'MySQL';
+
+Always remember the initial dollar sign (C<$>) and ending semicolon
+(C<;>). Those familiar with Perl syntax can of course use the full range
+of Perl expressions.
+
+This configuration file should end with the line:
+
+ 1;
+
+This ensures that Perl doesn't think there is an error when loading the
+file.
+
+=head1 DATABASE CONFIGURATION
+
+=over 4
+
+=item DB_DDL_DIRECTORY
+
+Specifies the directory used to dump the database schema in formats for
+each possible database server. This also includes diffs between schema
+versions, for upgrades. The default value is F</usr/local/share/wallet>,
+which matches the default installation location.
+
+=cut
+
+our $DB_DDL_DIRECTORY = '/usr/local/share/wallet';
+
+=item DB_DRIVER
+
+Sets the Perl database driver to use for the wallet database. Common
+values would be C<SQLite> or C<MySQL>. Less common values would be
+C<Oracle>, C<Sybase>, or C<ODBC>. The appropriate DBD::* Perl module for
+the chosen driver must be installed and will be dynamically loaded by the
+wallet. For more information, see L<DBI>.
+
+This variable must be set.
+
+=cut
+
+our $DB_DRIVER;
+
+=item DB_INFO
+
+Sets the remaining contents for the DBI DSN (everything after the driver).
+Using this variable provides full control over the connect string passed
+to DBI. When using SQLite, set this variable to the path to the SQLite
+database. If this variable is set, DB_NAME, DB_HOST, and DB_PORT are
+ignored. For more information, see L<DBI> and the documentation for the
+database driver you're using.
+
+Either DB_INFO or DB_NAME must be set. If you don't need to pass any
+additional information to DBI, set DB_INFO to the empty string (C<''>).
+
+=cut
+
+our $DB_INFO;
+
+=item DB_NAME
+
+If DB_INFO is not set, specifies the database name. The third part of the
+DBI connect string will be set to C<database=DB_NAME>, possibly with a
+host and port appended if DB_HOST and DB_PORT are set. For more
+information, see L<DBI> and the documentation for the database driver
+you're using.
+
+Either DB_INFO or DB_NAME must be set.
+
+=cut
+
+our $DB_NAME;
+
+=item DB_HOST
+
+If DB_INFO is not set, specifies the database host. C<;host=DB_HOST> will
+be appended to the DBI connect string. For more information, see L<DBI>
+and the documentation for the database driver you're using.
+
+=cut
+
+our $DB_HOST;
+
+=item DB_PORT
+
+If DB_PORT is not set, specifies the database port. C<;port=DB_PORT> will
+be appended to the DBI connect string. If this variable is set, DB_HOST
+should also be set. For more information, see L<DBI> and the
+documentation for the database driver you're using.
+
+=cut
+
+our $DB_PORT;
+
+=item DB_USER
+
+Specifies the user for database authentication. Some database backends,
+particularly SQLite, do not need this.
+
+=cut
+
+our $DB_USER;
+
+=item DB_PASSWORD
+
+Specifies the password for database authentication. Some database
+backends, particularly SQLite, do not need this.
+
+=cut
+
+our $DB_PASSWORD;
+
+=back
+
+=head1 DUO OBJECT CONFIGURATION
+
+These configuration variables only need to be set if you intend to use the
+C<duo> object type (the Wallet::Object::Duo class).
+
+=over 4
+
+=item DUO_AGENT
+
+If this configuration variable is set, its value should be an object that
+is call-compatible with LWP::UserAgent. This object will be used instead
+of LWP::UserAgent to make API calls to Duo. This is primarily useful for
+testing, allowing replacement of the user agent with a mock implementation
+so that a test can run without needing a Duo account.
+
+=cut
+
+our $DUO_AGENT;
+
+=item DUO_KEY_FILE
+
+The path to a file in JSON format that contains the key and hostname data
+for the Duo Admin API integration used to manage integrations via wallet.
+This file should be in the format expected by the C<key_file> parameter
+to the Net::Duo::Admin constructor. See L<Net::Duo::Admin> for more
+information.
+
+DUO_KEY_FILE must be set to use Duo objects.
+
+=cut
+
+our $DUO_KEY_FILE;
+
+=item DUO_TYPE
+
+The type of integration to create. Currently, only one type of integration
+can be created by one wallet configuration. This restriction may be relaxed
+in the future. The default value is C<unix> to create UNIX integrations.
+
+=cut
+
+our $DUO_TYPE = 'unix';
+
+=back
+
+=head1 FILE OBJECT CONFIGURATION
+
+These configuration variables only need to be set if you intend to use the
+C<file> object type (the Wallet::Object::File class).
+
+=over 4
+
+=item FILE_BUCKET
+
+The directory into which to store file objects. File objects will be
+stored in subdirectories of this directory. See L<Wallet::Object::File>
+for the full details of the naming scheme. This directory must be
+writable by the wallet server and the wallet server must be able to create
+subdirectories of it.
+
+FILE_BUCKET must be set to use file objects.
+
+=cut
+
+our $FILE_BUCKET;
+
+=item FILE_MAX_SIZE
+
+The maximum size of data that can be stored in a file object in bytes. If
+this configuration variable is set, an attempt to store data larger than
+this limit will be rejected.
+
+=cut
+
+our $FILE_MAX_SIZE;
+
+=back
+
+=head1 KEYTAB OBJECT CONFIGURATION
+
+These configuration variables only need to be set if you intend to use the
+C<keytab> object type (the Wallet::Object::Keytab class).
+
+=over 4
+
+=item KEYTAB_FILE
+
+Specifies the keytab to use to authenticate to B<kadmind>. The principal
+whose key is stored in this keytab must have the ability to create,
+modify, inspect, and delete any principals that should be managed by the
+wallet. (In MIT Kerberos F<kadm5.acl> parlance, this is C<admci>
+privileges.)
+
+KEYTAB_FILE must be set to use keytab objects.
+
+=cut
+
+our $KEYTAB_FILE;
+
+=item KEYTAB_FLAGS
+
+These flags, if any, are passed to the C<addprinc> command when creating a
+new principal in the Kerberos KDC. To not pass any flags, set
+KEYTAB_FLAGS to the empty string. The default value is C<-clearpolicy>,
+which clears any password strength policy from principals created by the
+wallet. (Since the wallet randomizes the keys, password strength checking
+is generally pointless and may interact poorly with the way C<addprinc
+-randkey> works when third-party add-ons for password strength checking
+are used.)
+
+=cut
+
+our $KEYTAB_FLAGS = '-clearpolicy';
+
+=item KEYTAB_HOST
+
+Specifies the host on which the kadmin service is running. This setting
+overrides the C<admin_server> setting in the [realms] section of
+F<krb5.conf> and any DNS SRV records and allows the wallet to run on a
+system that doesn't have a Kerberos configuration for the wallet's realm.
+
+=cut
+
+our $KEYTAB_HOST;
+
+=item KEYTAB_KADMIN
+
+The path to the B<kadmin> command-line client. The default value is
+C<kadmin>, which will cause the wallet to search for B<kadmin> on its
+default PATH.
+
+=cut
+
+our $KEYTAB_KADMIN = 'kadmin';
+
+=item KEYTAB_KRBTYPE
+
+The Kerberos KDC implementation type, either C<Heimdal> or C<MIT>
+(case-insensitive). KEYTAB_KRBTYPE must be set to use keytab objects.
+
+=cut
+
+our $KEYTAB_KRBTYPE;
+
+=item KEYTAB_PRINCIPAL
+
+The principal whose key is stored in KEYTAB_FILE. The wallet will
+authenticate as this principal to the kadmin service.
+
+KEYTAB_PRINCIPAL must be set to use keytab objects, at least until
+B<kadmin> is smart enough to use the first principal found in the keytab
+it's using for authentication.
+
+=cut
+
+our $KEYTAB_PRINCIPAL;
+
+=item KEYTAB_REALM
+
+Specifies the realm in which to create Kerberos principals. The keytab
+object implementation can only work in a single realm for a given wallet
+installation and the keytab object names are stored without realm.
+KEYTAB_REALM is added when talking to the KDC via B<kadmin>.
+
+KEYTAB_REALM must be set to use keytab objects. C<ktadd> doesn't always
+default to the local realm.
+
+=cut
+
+our $KEYTAB_REALM;
+
+=item KEYTAB_TMP
+
+A directory into which the wallet can write keytabs temporarily while
+processing C<get> commands from clients. The keytabs are written into
+this directory with predictable names, so this should not be a system
+temporary directory such as F</tmp> or F</var/tmp>. It's best to create a
+directory solely for this purpose that's owned by the user the wallet
+server will run as.
+
+KEYTAB_TMP must be set to use keytab objects.
+
+=cut
+
+our $KEYTAB_TMP;
+
+=back
+
+=head2 Retrieving Existing Keytabs
+
+Heimdal provides the choice, over the network protocol, of either
+downloading the existing keys for a principal or generating new random
+keys. MIT Kerberos does not; downloading a keytab over the kadmin
+protocol always rekeys the principal.
+
+For MIT Kerberos, the keytab object backend therefore optionally supports
+retrieving existing keys, and hence keytabs, for Kerberos principals by
+contacting the KDC via remctl and talking to B<keytab-backend>. This is
+enabled by setting the C<unchanging> flag on keytab objects. To configure
+that support, set the following variables.
+
+This is not required for Heimdal; for Heimdal, setting the C<unchanging>
+flag is all that's needed.
+
+=over 4
+
+=item KEYTAB_REMCTL_CACHE
+
+Specifies the ticket cache to use when retrieving existing keytabs from
+the KDC. This is only used to implement support for the C<unchanging>
+flag. The ticket cache must be for a principal with access to run
+C<keytab retrieve> via remctl on KEYTAB_REMCTL_HOST.
+
+=cut
+
+our $KEYTAB_REMCTL_CACHE;
+
+=item KEYTAB_REMCTL_HOST
+
+The host to which to connect with remctl to retrieve existing keytabs.
+This is only used to implement support for the C<unchanging> flag. This
+host must provide the C<keytab retrieve> command and KEYTAB_REMCTL_CACHE
+must also be set to a ticket cache for a principal with access to run that
+command.
+
+=cut
+
+our $KEYTAB_REMCTL_HOST;
+
+=item KEYTAB_REMCTL_PRINCIPAL
+
+The service principal to which to authenticate when retrieving existing
+keytabs. This is only used to implement support for the C<unchanging>
+flag. If this variable is not set, the default is formed by prepending
+C<host/> to KEYTAB_REMCTL_HOST. (Note that KEYTAB_REMCTL_HOST is not
+lowercased first.)
+
+=cut
+
+our $KEYTAB_REMCTL_PRINCIPAL;
+
+=item KEYTAB_REMCTL_PORT
+
+The port on KEYTAB_REMCTL_HOST to which to connect with remctl to retrieve
+existing keytabs. This is only used to implement support for the
+C<unchanging> flag. If this variable is not set, the default remctl port
+will be used.
+
+=cut
+
+our $KEYTAB_REMCTL_PORT;
+
+=back
+
+=head1 WEBAUTH KEYRING OBJECT CONFIGURATION
+
+These configuration variables only need to be set if you intend to use the
+C<wakeyring> object type (the Wallet::Object::WAKeyring class).
+
+=over 4
+
+=item WAKEYRING_BUCKET
+
+The directory into which to store WebAuth keyring objects. WebAuth
+keyring objects will be stored in subdirectories of this directory. See
+L<Wallet::Object::WAKeyring> for the full details of the naming scheme.
+This directory must be writable by the wallet server and the wallet server
+must be able to create subdirectories of it.
+
+WAKEYRING_BUCKET must be set to use WebAuth keyring objects.
+
+=cut
+
+our $WAKEYRING_BUCKET;
+
+=item WAKEYRING_REKEY_INTERVAL
+
+The interval, in seconds, at which new keys are generated in a keyring.
+The object implementation will try to arrange for there to be keys added
+to the keyring separated by this interval.
+
+It's useful to provide some interval to install the keyring everywhere
+that it's used before the key becomes inactive. Every keyring will
+therefore normally have at least three keys: one that's currently active,
+one that becomes valid in the future but less than
+WAKEYRING_REKEY_INTERVAL from now, and one that becomes valid between one
+and two of those intervals into the future. This means that one has twice
+this interval to distribute the keyring everywhere it is used.
+
+Internally, this is implemented by adding a new key that becomes valid in
+twice this interval from the current time if the newest key becomes valid
+at or less than this interval in the future.
+
+The default value is 60 * 60 * 24 (one day).
+
+=cut
+
+our $WAKEYRING_REKEY_INTERVAL = 60 * 60 * 24;
+
+=item WAKEYRING_PURGE_INTERVAL
+
+The interval, in seconds, from the key creation date after which keys are
+removed from the keyring. This is used to clean up old keys and finish
+key rotation. Keys won't be removed unless there are more than three keys
+in the keyring to try to keep a misconfiguration from removing all valid
+keys.
+
+The default value is 60 * 60 * 24 * 90 (90 days).
+
+=cut
+
+our $WAKEYRING_PURGE_INTERVAL = 60 * 60 * 24 * 90;
+
+=back
+
+=head1 LDAP ACL CONFIGURATION
+
+These configuration variables are only needed if you intend to use the
+C<ldap-attr> ACL type (the Wallet::ACL::LDAP::Attribute class). They
+specify the LDAP server and additional connection and data model
+information required for the wallet to check for the existence of
+attributes.
+
+=over 4
+
+=item LDAP_HOST
+
+The LDAP server name to use to verify LDAP ACLs. This variable must be
+set to use LDAP ACLs.
+
+=cut
+
+our $LDAP_HOST;
+
+=item LDAP_BASE
+
+The base DN under which to search for the entry corresponding to a
+principal. Currently, the wallet always does a full subtree search under
+this base DN. This variable must be set to use LDAP ACLs.
+
+=cut
+
+our $LDAP_BASE;
+
+=item LDAP_FILTER_ATTR
+
+The attribute used to find the entry corresponding to a principal. The
+LDAP entry containing this attribute with a value equal to the principal
+will be found and checked for the required attribute and value. If this
+variable is not set, the default is C<krb5PrincipalName>.
+
+=cut
+
+our $LDAP_FILTER_ATTR;
+
+=item LDAP_CACHE
+
+Specifies the Kerberos ticket cache to use when connecting to the LDAP
+server. GSS-API authentication is always used; there is currently no
+support for any other type of bind. The ticket cache must be for a
+principal with access to verify the values of attributes that will be used
+with this ACL type. This variable must be set to use LDAP ACLs.
+
+=cut
+
+our $LDAP_CACHE;
+
+=back
+
+Finally, depending on the structure of the LDAP directory being queried,
+there may not be any attribute in the directory whose value exactly
+matches the Kerberos principal. The attribute designated by
+LDAP_FILTER_ATTR may instead hold a transformation of the principal name
+(such as the principal with the local realm stripped off, or rewritten
+into an LDAP DN form). If this is the case, define a Perl function named
+ldap_map_principal. This function will be called whenever an LDAP
+attribute ACL is being verified. It will take one argument, the
+principal, and is expected to return the value to search for in the LDAP
+directory server.
+
+For example, if the principal name without the local realm is stored in
+the C<uid> attribute in the directory, set LDAP_FILTER_ATTR to C<uid> and
+then define ldap_map_attribute as follows:
+
+ sub ldap_map_principal {
+ my ($principal) = @_;
+ $principal =~ s/\@EXAMPLE\.COM$//;
+ return $principal;
+ }
+
+Note that this example only removes the local realm (here, EXAMPLE.COM).
+Any principal from some other realm will be left fully qualified, and then
+presumably will not be found in the directory.
+
+=head1 NETDB ACL CONFIGURATION
+
+These configuration variables are only needed if you intend to use the
+C<netdb> ACL type (the Wallet::ACL::NetDB class). They specify the remctl
+connection information for retrieving user roles from NetDB and the local
+realm to remove from principals (since NetDB normally expects unscoped
+local usernames).
+
+=over 4
+
+=item NETDB_REALM
+
+The wallet uses fully-qualified principal names (including the realm), but
+NetDB normally expects local usernames without the realm. If this
+variable is set, the given realm will be stripped from any principal names
+before passing them to NetDB. Principals in other realms will be passed
+to NetDB without modification.
+
+=cut
+
+our $NETDB_REALM;
+
+=item NETDB_REMCTL_CACHE
+
+Specifies the ticket cache to use when querying the NetDB remctl interface
+for user roles. The ticket cache must be for a principal with access to
+run C<netdb node-roles> via remctl on KEYTAB_REMCTL_HOST. This variable
+must be set to use NetDB ACLs.
+
+=cut
+
+our $NETDB_REMCTL_CACHE;
+
+=item NETDB_REMCTL_HOST
+
+The host to which to connect with remctl to query NetDB for user roles.
+This host must provide the C<netdb node-roles> command and
+NETDB_REMCTL_CACHE must also be set to a ticket cache for a principal with
+access to run that command. This variable must be set to use NetDB ACLs.
+
+=cut
+
+our $NETDB_REMCTL_HOST;
+
+=item NETDB_REMCTL_PRINCIPAL
+
+The service principal to which to authenticate when querying NetDB for
+user roles. If this variable is not set, the default is formed by
+prepending C<host/> to NETDB_REMCTL_HOST. (Note that NETDB_REMCTL_HOST is
+not lowercased first.)
+
+=cut
+
+our $NETDB_REMCTL_PRINCIPAL;
+
+=item NETDB_REMCTL_PORT
+
+The port on NETDB_REMCTL_HOST to which to connect with remctl to query
+NetDB for user roles. If this variable is not set, the default remctl
+port will be used.
+
+=cut
+
+our $NETDB_REMCTL_PORT;
+
+=back
+
+=head1 DEFAULT OWNERS
+
+By default, only users in the ADMIN ACL can create new objects in the
+wallet. To allow other users to create new objects, define a Perl
+function named default_owner. This function will be called whenever a
+non-ADMIN user tries to create a new object and will be passed the type
+and name of the object. It should return undef if there is no default
+owner for that object. If there is, it should return a list containing
+the name to use for the ACL and then zero or more anonymous arrays of two
+elements each giving the type and identifier for each ACL entry.
+
+For example, the following simple function says to use a default owner
+named C<default> with one entry of type C<krb5> and identifier
+C<rra@example.com> for the object with type C<keytab> and name
+C<host/example.com>:
+
+ sub default_owner {
+ my ($type, $name) = @_;
+ if ($type eq 'keytab' and $name eq 'host/example.com') {
+ return ('default', [ 'krb5', 'rra@example.com' ]);
+ } else {
+ return;
+ }
+ }
+
+Of course, normally this function is used for more complex mappings. Here
+is a more complete example. For objects of type keytab corresponding to
+various types of per-machine principals, return a default owner that sets
+as owner anyone with a NetDB role for that system and the system's host
+principal. This permits authorization management using NetDB while also
+allowing the system to bootstrap itself once the host principal has been
+downloaded and rekey itself using the old host principal.
+
+ sub default_owner {
+ my ($type, $name) = @_;
+ my %allowed = map { $_ => 1 }
+ qw(HTTP cifs host imap ldap nfs pop sieve smtp webauth);
+ my $realm = 'example.com';
+ return unless $type eq 'keytab';
+ return unless $name =~ m%/%;
+ my ($service, $instance) = split ('/', $name, 2);
+ return unless $allowed{$service};
+ my $acl_name = "host/$instance";
+ my @acl = ([ 'netdb', $instance ],
+ [ 'krb5', "host/$instance\@$realm" ]);
+ return ($acl_name, @acl);
+ }
+
+The auto-created ACL used for the owner of the new object will, in the
+above example, be named C<host/I<system>> where I<system> is the
+fully-qualified name of the system as derived from the keytab being
+requested.
+
+If the name of the ACL returned by the default_owner function matches an
+ACL that already exists in the wallet database, the existing ACL will be
+compared to the default ACL returned by the default_owner function. If
+the existing ACL has the same entries as the one returned by
+default_owner, creation continues if the user is authorized by that ACL.
+If they don't match, creation of the object is rejected, since the
+presence of an existing ACL may indicate that something different is being
+done with this object.
+
+=head1 NAMING ENFORCEMENT
+
+By default, wallet permits administrators to create objects of any name
+(unless the object backend rejects the name). However, naming standards
+for objects can be enforced, even for administrators, by defining a Perl
+function in the configuration file named verify_name. If such a function
+exists, it will be called for any object creation and will be passed the
+type of object, the object name, and the identity of the person doing the
+creation. If it returns undef or the empty string, object creation will
+be allowed. If it returns anything else, object creation is rejected and
+the return value is used as the error message.
+
+This function is also called for naming audits done via Wallet::Report
+to find any existing objects that violate a (possibly updated) naming
+policy. In this case, the third argument (the identity of the person
+creating the object) will be undef. As a general rule, if the third
+argument is undef, the function should apply the most liberal accepted
+naming policy so that the audit returns only objects that violate all
+naming policies, but some sites may wish different results for their audit
+reports.
+
+Please note that this return status is backwards from what one would
+normally expect. A false value is success; a true value is failure with
+an error message.
+
+For example, the following verify_name function would ensure that any
+keytab objects for particular principals have fully-qualified hostnames:
+
+ sub verify_name {
+ my ($type, $name, $user) = @_;
+ my %host_based = map { $_ => 1 }
+ qw(HTTP cifs host imap ldap nfs pop sieve smtp webauth);
+ return unless $type eq 'keytab';
+ return unless $name =~ m%/%;
+ my ($service, $instance) = split ('/', $name, 2);
+ return unless $host_based{$service};
+ return "host name $instance must be fully qualified"
+ unless $instance =~ /\./;
+ return;
+ }
+
+Objects that aren't of type C<keytab> or which aren't for a host-based key
+have no naming requirements enforced by this example.
+
+=head1 ACL NAMING ENFORCEMENT
+
+Similar to object names, by default wallet permits administrators to
+create ACLs with any name. However, naming standards for ACLs can be
+enforced by defining a Perl function in the configuration file named
+verify_acl_name. If such a function exists, it will be called for any ACL
+creation or rename and will be passed given the new ACL name and the
+identity of the person doing the creation. If it returns undef or the
+empty string, object creation will be allowed. If it returns anything
+else, object creation is rejected and the return value is used as the
+error message.
+
+This function is also called for naming audits done via Wallet::Report to
+find any existing objects that violate a (possibly updated) naming policy.
+In this case, the second argument (the identity of the person creating the
+ACL) will be undef. As a general rule, if the second argument is undef,
+the function should apply the most liberal accepted naming policy so that
+the audit returns only ACLs that violate all naming policies, but some
+sites may wish different results for their audit reports.
+
+Please note that this return status is backwards from what one would
+normally expect. A false value is success; a true value is failure with
+an error message.
+
+For example, the following verify_acl_name function would ensure that any
+ACLs created contain a slash and the part before the slash be one of
+C<host>, C<group>, C<user>, or C<service>.
+
+ sub verify_acl_name {
+ my ($name, $user) = @_;
+ return 'ACL names must contain a slash' unless $name =~ m,/,;
+ my ($first, $rest) = split ('/', $name, 2);
+ my %types = map { $_ => 1 } qw(host group user service);
+ unless ($types{$first}) {
+ return "unknown ACL type $first";
+ }
+ return;
+ }
+
+Obvious improvements could be made, such as checking that the part after
+the slash for a C<host/> ACL looked like a host name and the part after a
+slash for a C<user/> ACL look like a user name.
+
+=head1 ENVIRONMENT
+
+=over 4
+
+=item WALLET_CONFIG
+
+If this environment variable is set, it is taken to be the path to the
+wallet configuration file to load instead of F</etc/wallet/wallet.conf>.
+
+=back
+
+=cut
+
+# Now, load the configuration file so that it can override the defaults.
+if (-r $PATH) {
+ do $PATH or die (($@ || $!) . "\n");
+}
+
+1;
+__END__
+
+=head1 SEE ALSO
+
+DBI(3), Wallet::Object::Keytab(3), Wallet::Server(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Database.pm b/perl/lib/Wallet/Database.pm
new file mode 100644
index 0000000..3a4e130
--- /dev/null
+++ b/perl/lib/Wallet/Database.pm
@@ -0,0 +1,124 @@
+# Wallet::Database -- Wallet system database connection management.
+#
+# This module is a thin wrapper around DBIx::Class to handle determination
+# of the database configuration settings automatically on connect. The
+# intention is that Wallet::Database objects can be treated in all respects
+# like DBIx::Class objects in the rest of the code.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2008, 2009, 2010, 2012, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Database;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::Schema;
+use Wallet::Config;
+
+@ISA = qw(Wallet::Schema);
+
+# 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.04';
+
+##############################################################################
+# Core overrides
+##############################################################################
+
+# Override DBI::connect to supply our own connect string, username, and
+# password and to set some standard options. Takes no arguments other than
+# the implicit class argument.
+sub connect {
+ my ($class) = @_;
+ unless ($Wallet::Config::DB_DRIVER
+ and (defined ($Wallet::Config::DB_INFO)
+ or defined ($Wallet::Config::DB_NAME))) {
+ die "database connection information not configured\n";
+ }
+ my $dsn = "DBI:$Wallet::Config::DB_DRIVER:";
+ if (defined $Wallet::Config::DB_INFO) {
+ $dsn .= $Wallet::Config::DB_INFO;
+ } else {
+ $dsn .= "database=$Wallet::Config::DB_NAME";
+ $dsn .= ";host=$Wallet::Config::DB_HOST" if $Wallet::Config::DB_HOST;
+ $dsn .= ";port=$Wallet::Config::DB_PORT" if $Wallet::Config::DB_PORT;
+ }
+ my $user = $Wallet::Config::DB_USER;
+ my $pass = $Wallet::Config::DB_PASSWORD;
+ my %attrs = (PrintError => 0, RaiseError => 1);
+ my $dbh = eval { $class->SUPER::connect ($dsn, $user, $pass, \%attrs) };
+ if ($@) {
+ die "cannot connect to database: $@\n";
+ }
+ return $dbh;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=head1 NAME
+
+Wallet::Dabase - Wrapper module for wallet database connections
+
+=for stopwords
+DBI RaiseError PrintError AutoCommit Allbery
+
+=head1 SYNOPSIS
+
+ use Wallet::Database;
+ my $dbh = Wallet::Database->connect;
+
+=head1 DESCRIPTION
+
+Wallet::Database is a thin wrapper module around DBI that takes care of
+building a connect string and setting database options based on wallet
+configuration. The only overridden method is connect(). All other
+methods should work the same as in DBI and Wallet::Database objects should
+be usable exactly as if they were DBI objects.
+
+connect() will obtain the database connection information from the wallet
+configuration; see L<Wallet::Config> for more details. It will also
+automatically set the RaiseError attribute to true and the PrintError and
+AutoCommit attributes to false, matching the assumptions made by the
+wallet database code.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item connect()
+
+Opens a new database connection and returns the database object. On any
+failure, throws an exception. Unlike the DBI method, connect() takes no
+arguments; all database connection information is derived from the wallet
+configuration.
+
+=back
+
+=head1 SEE ALSO
+
+DBI(3), Wallet::Config(3)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Kadmin.pm b/perl/lib/Wallet/Kadmin.pm
new file mode 100644
index 0000000..65a5700
--- /dev/null
+++ b/perl/lib/Wallet/Kadmin.pm
@@ -0,0 +1,241 @@
+# Wallet::Kadmin -- Kerberos administration API for wallet keytab backend.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2009, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Kadmin;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($VERSION);
+
+use Wallet::Config ();
+
+# 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.03';
+
+##############################################################################
+# Utility functions for child classes
+##############################################################################
+
+# Read the entirety of a possibly binary file and return the contents,
+# deleting the file after reading it. If reading the file fails, set the
+# error message and return undef.
+sub read_keytab {
+ my ($self, $file) = @_;
+ local *TMPFILE;
+ unless (open (TMPFILE, '<', $file)) {
+ $self->error ("cannot open temporary file $file: $!");
+ return;
+ }
+ local $/;
+ undef $!;
+ my $data = <TMPFILE>;
+ if ($!) {
+ $self->error ("cannot read temporary file $file: $!");
+ unlink $file;
+ return;
+ }
+ close TMPFILE;
+ unlink $file;
+ return $data;
+}
+
+##############################################################################
+# Public methods
+##############################################################################
+
+# Create a new kadmin object, by finding the type requested in the wallet
+# config and passing off to the proper module. Returns the object directly
+# from the specific Wallet::Kadmin::* module.
+sub new {
+ my ($class) = @_;
+ my $kadmin;
+ if (not $Wallet::Config::KEYTAB_KRBTYPE) {
+ die "keytab object implementation not configured\n";
+ } elsif (lc ($Wallet::Config::KEYTAB_KRBTYPE) eq 'mit') {
+ require Wallet::Kadmin::MIT;
+ $kadmin = Wallet::Kadmin::MIT->new;
+ } elsif (lc ($Wallet::Config::KEYTAB_KRBTYPE) eq 'heimdal') {
+ require Wallet::Kadmin::Heimdal;
+ $kadmin = Wallet::Kadmin::Heimdal->new;
+ } else {
+ my $type = $Wallet::Config::KEYTAB_KRBTYPE;
+ die "unknown KEYTAB_KRBTYPE setting: $type\n";
+ }
+
+ return $kadmin;
+}
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+# Set a callback to be called for forked kadmin processes. This does nothing
+# by default but may be overridden by subclasses that need special behavior
+# (such as the current Wallet::Kadmin::MIT module).
+sub fork_callback { }
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+backend Kadmin keytabs keytab Heimdal API kadmind kadmin KDC ENCTYPE
+enctypes enctype Allbery
+
+=head1 NAME
+
+Wallet::Kadmin - Kerberos administration API for wallet keytab backend
+
+=head1 SYNOPSIS
+
+ my $kadmin = Wallet::Kadmin->new;
+ $kadmin->create ('host/foo.example.com');
+ my $data = $kadmin->keytab_rekey ('host/foo.example.com',
+ 'aes256-cts-hmac-sha1-96');
+ $data = $kadmin->keytab ('host/foo.example.com');
+ my $exists = $kadmin->exists ('host/oldshell.example.com');
+ $kadmin->destroy ('host/oldshell.example.com') if $exists;
+
+=head1 DESCRIPTION
+
+Wallet::Kadmin is a wrapper and base class for modules that provide an
+interface for wallet to do Kerberos administration, specifically create
+and delete principals and create keytabs for a principal. Each subclass
+administers a specific type of Kerberos implementation, such as MIT
+Kerberos or Heimdal, providing a standard set of API calls used to
+interact with that implementation's kadmin interface.
+
+The class uses Wallet::Config to find which type of kadmin interface is in
+use and then returns an object to use for interacting with that interface.
+See L<Wallet::Config/"KEYTAB OBJECT CONFIGURATION"> for details on how to
+configure this module.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new()
+
+Finds the proper Kerberos implementation and calls the new() constructor
+for that implementation's module, returning the resulting object. If the
+implementation is not recognized or set, die with an error message.
+
+=back
+
+=head1 INSTANCE METHODS
+
+These methods are provided by any object returned by new(), regardless of
+the underlying kadmin interface. They are implemented by the child class
+appropriate for the configured Kerberos implementation.
+
+=over 4
+
+=item create(PRINCIPAL)
+
+Adds a new principal with a given name. The principal is created with a
+random password, and any other flags set by Wallet::Config. Returns true
+on success and false on failure. If the principal already exists, return
+true as we are bringing our expectations in line with reality.
+
+=item destroy(PRINCIPAL)
+
+Removes a principal with the given name. Returns true on success or false
+on failure. If the principal does not exist, return true as we are
+bringing our expectations in line with reality.
+
+=item error([ERROR ...])
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+For the convenience of child classes, this method can also be called with
+one or more error strings. If so, those strings are concatenated
+together, trailing newlines are removed, any text of the form S<C< at \S+
+line \d+\.?>> at the end of the message is stripped off, and the result is
+stored as the error. Only child classes should call this method with an
+error string.
+
+=item exists(PRINCIPAL)
+
+Returns true if the given principal exists in the KDC and C<0> if it
+doesn't. If an error is encountered in checking whether the principal
+exists, exists() returns undef.
+
+=item fork_callback(CALLBACK)
+
+If the module has to fork an external process for some reason, such as a
+kadmin command-line client, the sub CALLBACK will be called in the child
+process before running the program. This can be used to, for example,
+properly clean up shared database handles.
+
+=item keytab(PRINCIPAL)
+
+keytab() creates a keytab for the given principal, storing it in the given
+file. A keytab is an on-disk store for the key or keys for a Kerberos
+principal. Keytabs are used by services to verify incoming authentication
+from clients or by automated processes that need to authenticate to
+Kerberos. To create a keytab, the principal has to have previously been
+created in the Kerberos KDC. Returns the keytab as binary data on success
+and undef on failure.
+
+=item keytab_rekey(PRINCIPAL [, ENCTYPE ...])
+
+Like keytab(), but randomizes the key for the principal before generating
+the keytab and writes it to the given file. This will invalidate any
+existing keytabs for that principal. This method can also limit the
+encryption types of the keys for that principal via the optional ENCTYPE
+arguments. The enctype values must be enctype strings recognized by the
+Kerberos implementation (strings like C<aes256-cts-hmac-sha1-96> or
+C<des-cbc-crc>). If none are given, the KDC defaults will be used.
+Returns the keytab as binary data on success and undef on failure.
+
+=back
+
+The following methods are utility methods to aid with child class
+implementation and should only be called by child classes.
+
+=over 4
+
+=item read_keytab(FILE)
+
+Reads the contents of the keytab stored in FILE into memory and returns it
+as binary data. On failure, returns undef and sets the object error.
+
+=back
+
+=head1 SEE ALSO
+
+kadmin(8), Wallet::Config(3), Wallet::Object::Keytab(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHORS
+
+Jon Robertson <jonrober@stanford.edu> and Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Kadmin/Heimdal.pm b/perl/lib/Wallet/Kadmin/Heimdal.pm
new file mode 100644
index 0000000..1208801
--- /dev/null
+++ b/perl/lib/Wallet/Kadmin/Heimdal.pm
@@ -0,0 +1,315 @@
+# Wallet::Kadmin::Heimdal -- Wallet Kerberos administration API for Heimdal.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2009, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Kadmin::Heimdal;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Heimdal::Kadm5 qw(KRB5_KDB_DISALLOW_ALL_TIX);
+use Wallet::Config ();
+use Wallet::Kadmin ();
+
+@ISA = qw(Wallet::Kadmin);
+
+# 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.04';
+
+##############################################################################
+# Utility functions
+##############################################################################
+
+# Add the realm to the end of the principal if no realm is currently present.
+sub canonicalize_principal {
+ my ($self, $principal) = @_;
+ if ($Wallet::Config::KEYTAB_REALM && $principal !~ /\@/) {
+ $principal .= '@' . $Wallet::Config::KEYTAB_REALM;
+ }
+ return $principal;
+}
+
+# Generate a long random password.
+#
+# Please note: This is not a cryptographically secure password! It's used
+# only because the Heimdal kadmin interface requires a password on create.
+# The keys will be set before the principal is ever set active, so it will
+# never be possible to use the password. It just needs to be random in case
+# password quality checks are applied to it.
+#
+# Make the password reasonably long and include a variety of character classes
+# so that it should pass any password strength checking.
+sub insecure_random_password {
+ my ($self) = @_;
+ my @classes = (
+ 'abcdefghijklmnopqrstuvwxyz',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ '0123456789',
+ '~`!@#$%^&*()-_+={[}]|:;<,>.?/'
+ );
+ my $password = q{};
+ for my $i (1..20) {
+ my $class = $i % scalar (@classes);
+ my $alphabet = $classes[$class];
+ my $letter = substr ($alphabet, int (rand (length $alphabet)), 1);
+ $password .= $letter;
+ }
+ return $password;
+}
+
+##############################################################################
+# Public interfaces
+##############################################################################
+
+# Check whether a given principal already exists in Kerberos. Returns true if
+# so, false otherwise.
+sub exists {
+ my ($self, $principal) = @_;
+ $principal = $self->canonicalize_principal ($principal);
+ my $kadmin = $self->{client};
+ my $princdata = eval { $kadmin->getPrincipal ($principal) };
+ if ($@) {
+ $self->error ("error getting principal: $@");
+ return;
+ }
+ return $princdata ? 1 : 0;
+}
+
+# Create a principal in Kerberos. If there is an error, return undef and set
+# the error. Return 1 on success or the principal already existing.
+sub create {
+ my ($self, $principal) = @_;
+ $principal = $self->canonicalize_principal ($principal);
+ my $exists = eval { $self->exists ($principal) };
+ if ($@) {
+ $self->error ("error adding principal $principal: $@");
+ return;
+ }
+ return 1 if $exists;
+
+ # The way Heimdal::Kadm5 works, we create a principal object, create the
+ # actual principal set inactive, then randomize it and activate it. We
+ # have to set a password, even though we're about to replace it with
+ # random keys, but since the principal is created inactive, it doesn't
+ # have to be a very good one.
+ my $kadmin = $self->{client};
+ eval {
+ my $princdata = $kadmin->makePrincipal ($principal);
+ my $attrs = $princdata->getAttributes;
+ $attrs |= KRB5_KDB_DISALLOW_ALL_TIX;
+ $princdata->setAttributes ($attrs);
+ my $password = $self->insecure_random_password;
+ $kadmin->createPrincipal ($princdata, $password, 0);
+ $kadmin->randKeyPrincipal ($principal);
+ $kadmin->enablePrincipal ($principal);
+ };
+ if ($@) {
+ $self->error ("error adding principal $principal: $@");
+ return;
+ }
+ return 1;
+}
+
+# Create a keytab for a principal. Returns the keytab as binary data or undef
+# on failure, setting the error.
+sub keytab {
+ my ($self, $principal) = @_;
+ $principal = $self->canonicalize_principal ($principal);
+ my $kadmin = $self->{client};
+ my $file = $Wallet::Config::KEYTAB_TMP . "/keytab.$$";
+ unlink $file;
+ my $princdata = eval { $kadmin->getPrincipal ($principal) };
+ if ($@) {
+ $self->error ("error creating keytab for $principal: $@");
+ return;
+ } elsif (!$princdata) {
+ $self->error ("error creating keytab for $principal: principal does"
+ . " not exist");
+ return;
+ }
+ eval { $kadmin->extractKeytab ($princdata, $file) };
+ if ($@) {
+ $self->error ("error creating keytab for principal: $@");
+ return;
+ }
+ return $self->read_keytab ($file);
+}
+
+# Create a keytab for a principal, randomizing the keys for that principal at
+# the same time. Takes the principal and an optional list of encryption types
+# to which to limit the keytab. Return the keytab data on success and undef
+# on failure. If the keytab creation fails, sets the error.
+sub keytab_rekey {
+ my ($self, $principal, @enctypes) = @_;
+ $principal = $self->canonicalize_principal ($principal);
+
+ # The way Heimdal works, you can only remove enctypes from a principal,
+ # not add them back in. So we need to run randkeyPrincipal first each
+ # time to restore all possible enctypes and then whittle them back down
+ # to those we have been asked for this time.
+ my $kadmin = $self->{client};
+ eval { $kadmin->randKeyPrincipal ($principal) };
+ if ($@) {
+ $self->error ("error creating keytab for $principal: could not"
+ . " reinit enctypes: $@");
+ return;
+ }
+ my $princdata = eval { $kadmin->getPrincipal ($principal) };
+ if ($@) {
+ $self->error ("error creating keytab for $principal: $@");
+ return;
+ } elsif (!$princdata) {
+ $self->error ("error creating keytab for $principal: principal does"
+ . " not exist");
+ return;
+ }
+
+ # Now actually remove any non-requested enctypes, if we requested any.
+ if (@enctypes) {
+ my $alltypes = $princdata->getKeytypes;
+ my %wanted = map { $_ => 1 } @enctypes;
+ for my $key (@{ $alltypes }) {
+ my $keytype = $key->[0];
+ next if exists $wanted{$keytype};
+ eval { $princdata->delKeytypes ($keytype) };
+ if ($@) {
+ $self->error ("error removing keytype $keytype from the"
+ . " keytab: $@");
+ return;
+ }
+ }
+ eval { $kadmin->modifyPrincipal ($princdata) };
+ if ($@) {
+ $self->error ("error saving principal modifications: $@");
+ return;
+ }
+ }
+
+ # Create the keytab.
+ my $file = $Wallet::Config::KEYTAB_TMP . "/keytab.$$";
+ unlink $file;
+ eval { $kadmin->extractKeytab ($princdata, $file) };
+ if ($@) {
+ $self->error ("error creating keytab for principal: $@");
+ return;
+ }
+ return $self->read_keytab ($file);
+}
+
+# Delete a principal from Kerberos. Return true if successful, false
+# otherwise. If the deletion fails, sets the error. If the principal doesn't
+# exist, return success; we're bringing reality in line with our expectations.
+sub destroy {
+ my ($self, $principal) = @_;
+ $principal = $self->canonicalize_principal ($principal);
+ my $exists = eval { $self->exists ($principal) };
+ if ($@) {
+ $self->error ("error checking principal existance: $@");
+ return;
+ } elsif (not $exists) {
+ return 1;
+ }
+ my $kadmin = $self->{client};
+ my $retval = eval { $kadmin->deletePrincipal ($principal) };
+ if ($@) {
+ $self->error ("error deleting $principal: $@");
+ return;
+ }
+ return 1;
+}
+
+# Create a new Wallet::Kadmin::Heimdal object and its underlying
+# Heimdal::Kadm5 object.
+sub new {
+ my ($class) = @_;
+ unless (defined ($Wallet::Config::KEYTAB_PRINCIPAL)
+ and defined ($Wallet::Config::KEYTAB_FILE)
+ and defined ($Wallet::Config::KEYTAB_REALM)) {
+ die "keytab object implementation not configured\n";
+ }
+ unless (defined ($Wallet::Config::KEYTAB_TMP)) {
+ die "KEYTAB_TMP configuration variable not set\n";
+ }
+ my @options = (RaiseError => 1,
+ Principal => $Wallet::Config::KEYTAB_PRINCIPAL,
+ Realm => $Wallet::Config::KEYTAB_REALM,
+ Keytab => $Wallet::Config::KEYTAB_FILE);
+ if ($Wallet::Config::KEYTAB_HOST) {
+ push (@options, Server => $Wallet::Config::KEYTAB_HOST);
+ }
+ my $client = Heimdal::Kadm5::Client->new (@options);
+ my $self = { client => $client };
+ bless ($self, $class);
+ return $self;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+keytabs keytab kadmin KDC API Allbery Heimdal unlinked
+
+=head1 NAME
+
+Wallet::Kadmin::Heimdal - Wallet Kerberos administration API for Heimdal
+
+=head1 SYNOPSIS
+
+ my $kadmin = Wallet::Kadmin::Heimdal->new;
+ $kadmin->create ('host/foo.example.com');
+ $kadmin->keytab_rekey ('host/foo.example.com', 'keytab',
+ 'aes256-cts-hmac-sha1-96');
+ my $data = $kadmin->keytab ('host/foo.example.com');
+ my $exists = $kadmin->exists ('host/oldshell.example.com');
+ $kadmin->destroy ('host/oldshell.example.com') if $exists;
+
+=head1 DESCRIPTION
+
+Wallet::Kadmin::Heimdal implements the Wallet::Kadmin API for Heimdal,
+providing an interface to create and delete principals and create keytabs.
+It provides the API documented in L<Wallet::Kadmin> for a Heimdal KDC.
+
+To use this class, several configuration parameters must be set. See
+L<Wallet::Config/"KEYTAB OBJECT CONFIGURATION"> for details.
+
+=head1 FILES
+
+=over 4
+
+=item KEYTAB_TMP/keytab.<pid>
+
+The keytab is created in this file and then read into memory. KEYTAB_TMP
+is set in the wallet configuration, and <pid> is the process ID of the
+current process. The file is unlinked after being read.
+
+=back
+
+=head1 SEE ALSO
+
+kadmin(8), Wallet::Config(3), Wallet::Kadmin(3),
+Wallet::Object::Keytab(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHORS
+
+Russ Allbery <eagle@eyrie.org> and Jon Robertson <jonrober@stanford.edu>.
+
+=cut
diff --git a/perl/lib/Wallet/Kadmin/MIT.pm b/perl/lib/Wallet/Kadmin/MIT.pm
new file mode 100644
index 0000000..ac45265
--- /dev/null
+++ b/perl/lib/Wallet/Kadmin/MIT.pm
@@ -0,0 +1,324 @@
+# Wallet::Kadmin::MIT -- Wallet Kerberos administration API for MIT.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Pulled into a module by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2007, 2008, 2009, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Kadmin::MIT;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::Config ();
+use Wallet::Kadmin ();
+
+@ISA = qw(Wallet::Kadmin);
+
+# 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.03';
+
+##############################################################################
+# kadmin Interaction
+##############################################################################
+
+# Make sure that principals are well-formed and don't contain characters that
+# will cause us problems when talking to kadmin. Takes a principal and
+# returns true if it's okay, false otherwise. Note that we do not permit
+# realm information here.
+sub valid_principal {
+ my ($self, $principal) = @_;
+ return scalar ($principal =~ m,^[\w-]+(/[\w_.-]+)?\z,);
+}
+
+# Run a kadmin command and capture the output. Returns the output, either as
+# a list of lines or, in scalar context, as one string. The exit status of
+# kadmin is often worthless.
+sub kadmin {
+ my ($self, $command) = @_;
+ unless (defined ($Wallet::Config::KEYTAB_PRINCIPAL)
+ and defined ($Wallet::Config::KEYTAB_FILE)
+ and defined ($Wallet::Config::KEYTAB_REALM)) {
+ die "keytab object implementation not configured\n";
+ }
+ my @args = ('-p', $Wallet::Config::KEYTAB_PRINCIPAL, '-k', '-t',
+ $Wallet::Config::KEYTAB_FILE, '-q', $command);
+ push (@args, '-s', $Wallet::Config::KEYTAB_HOST)
+ if $Wallet::Config::KEYTAB_HOST;
+ push (@args, '-r', $Wallet::Config::KEYTAB_REALM)
+ if $Wallet::Config::KEYTAB_REALM;
+ my $pid = open (KADMIN, '-|');
+ if (not defined $pid) {
+ $self->error ("cannot fork: $!");
+ return;
+ } elsif ($pid == 0) {
+ $self->{fork_callback} () if $self->{fork_callback};
+ unless (open (STDERR, '>&STDOUT')) {
+ warn "wallet: cannot dup stdout: $!\n";
+ exit 1;
+ }
+ unless (exec ($Wallet::Config::KEYTAB_KADMIN, @args)) {
+ warn "wallet: cannot run $Wallet::Config::KEYTAB_KADMIN: $!\n";
+ exit 1;
+ }
+ }
+ local $_;
+ my @output;
+ while (<KADMIN>) {
+ if (/^wallet: cannot /) {
+ s/^wallet: //;
+ $self->error ($_);
+ return;
+ }
+ push (@output, $_) unless /Authenticating as principal/;
+ }
+ close KADMIN;
+ return wantarray ? @output : join ('', @output);
+}
+
+##############################################################################
+# Public interfaces
+##############################################################################
+
+# Set a callback to be called for forked kadmin processes.
+sub fork_callback {
+ my ($self, $callback) = @_;
+ $self->{fork_callback} = $callback;
+}
+
+# Check whether a given principal already exists in Kerberos. Returns true if
+# so, false otherwise. Returns undef if kadmin fails, with the error already
+# set by kadmin.
+sub exists {
+ my ($self, $principal) = @_;
+ return unless $self->valid_principal ($principal);
+ if ($Wallet::Config::KEYTAB_REALM) {
+ $principal .= '@' . $Wallet::Config::KEYTAB_REALM;
+ }
+ my $output = $self->kadmin ("getprinc $principal");
+ if (!defined $output) {
+ return;
+ } elsif ($output =~ /^get_principal: /) {
+ return 0;
+ } else {
+ return 1;
+ }
+}
+
+# Create a principal in Kerberos. Sets the error and returns undef on failure,
+# and returns 1 on either success or the principal already existing.
+sub create {
+ my ($self, $principal) = @_;
+ unless ($self->valid_principal ($principal)) {
+ $self->error ("invalid principal name $principal");
+ return;
+ }
+ return 1 if $self->exists ($principal);
+ if ($Wallet::Config::KEYTAB_REALM) {
+ $principal .= '@' . $Wallet::Config::KEYTAB_REALM;
+ }
+ my $flags = $Wallet::Config::KEYTAB_FLAGS || '';
+ my $output = $self->kadmin ("addprinc -randkey $flags $principal");
+ if (!defined $output) {
+ return;
+ } elsif ($output =~ /^add_principal: (.*)/m) {
+ $self->error ("error adding principal $principal: $1");
+ return;
+ }
+ return 1;
+}
+
+# Retrieve an existing keytab from the KDC via a remctl call. The KDC needs
+# to be running the keytab-backend script and support the keytab retrieve
+# remctl command. In addition, the user must have configured us with the path
+# to a ticket cache and the host to which to connect with remctl. Returns the
+# keytab on success and undef on failure.
+sub keytab {
+ my ($self, $principal) = @_;
+ my $host = $Wallet::Config::KEYTAB_REMCTL_HOST;
+ unless ($host and $Wallet::Config::KEYTAB_REMCTL_CACHE) {
+ $self->error ('keytab unchanging support not configured');
+ return;
+ }
+ eval { require Net::Remctl };
+ if ($@) {
+ $self->error ("keytab unchanging support not available: $@");
+ return;
+ }
+ if ($principal !~ /\@/ && $Wallet::Config::KEYTAB_REALM) {
+ $principal .= '@' . $Wallet::Config::KEYTAB_REALM;
+ }
+ local $ENV{KRB5CCNAME} = $Wallet::Config::KEYTAB_REMCTL_CACHE;
+ my $port = $Wallet::Config::KEYTAB_REMCTL_PORT || 0;
+ my $remctl_princ = $Wallet::Config::KEYTAB_REMCTL_PRINCIPAL || '';
+ my @command = ('keytab', 'retrieve', $principal);
+ my $result = Net::Remctl::remctl ($host, $port, $remctl_princ, @command);
+ if ($result->error) {
+ $self->error ("cannot retrieve keytab for $principal: ",
+ $result->error);
+ return;
+ } elsif ($result->status != 0) {
+ my $error = $result->stderr;
+ $error =~ s/\s+$//;
+ $error =~ s/\n/ /g;
+ $self->error ("cannot retrieve keytab for $principal: $error");
+ return;
+ } else {
+ return $result->stdout;
+ }
+}
+
+# Create a keytab for a principal, randomizing the keys for that principal
+# in the process. Takes the principal and an optional list of encryption
+# types to which to limit the keytab. Return the keytab data on success
+# and undef otherwise. If the keytab creation fails, sets the error.
+sub keytab_rekey {
+ my ($self, $principal, @enctypes) = @_;
+ unless ($self->valid_principal ($principal)) {
+ $self->error ("invalid principal name: $principal");
+ return;
+ }
+ if ($Wallet::Config::KEYTAB_REALM) {
+ $principal .= '@' . $Wallet::Config::KEYTAB_REALM;
+ }
+ my $file = $Wallet::Config::KEYTAB_TMP . "/keytab.$$";
+ unlink $file;
+ my $command = "ktadd -q -k $file";
+ if (@enctypes) {
+ @enctypes = map { /:/ ? $_ : "$_:normal" } @enctypes;
+ $command .= ' -e "' . join (' ', @enctypes) . '"';
+ }
+ my $output = $self->kadmin ("$command $principal");
+ if (!defined $output) {
+ return;
+ } elsif ($output =~ /^(?:kadmin|ktadd): (.*)/m) {
+ $self->error ("error creating keytab for $principal: $1");
+ return;
+ }
+ return $self->read_keytab ($file);
+}
+
+# Delete a principal from Kerberos. Return true if successful, false
+# otherwise. If the deletion fails, sets the error. If the principal doesn't
+# exist, return success; we're bringing reality in line with our expectations.
+sub destroy {
+ my ($self, $principal) = @_;
+ unless ($self->valid_principal ($principal)) {
+ $self->error ("invalid principal name: $principal");
+ }
+ my $exists = $self->exists ($principal);
+ if (!defined $exists) {
+ return;
+ } elsif (not $exists) {
+ return 1;
+ }
+ if ($Wallet::Config::KEYTAB_REALM) {
+ $principal .= '@' . $Wallet::Config::KEYTAB_REALM;
+ }
+ my $output = $self->kadmin ("delprinc -force $principal");
+ if (!defined $output) {
+ return;
+ } elsif ($output =~ /^delete_principal: (.*)/m) {
+ $self->error ("error deleting $principal: $1");
+ return;
+ }
+ return 1;
+}
+
+# Create a new MIT kadmin object. Very empty for the moment, but later it
+# will probably fill out if we go to using a module rather than calling
+# kadmin directly.
+sub new {
+ my ($class) = @_;
+ unless (defined ($Wallet::Config::KEYTAB_TMP)) {
+ die "KEYTAB_TMP configuration variable not set\n";
+ }
+ my $self = {};
+ bless ($self, $class);
+ return $self;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+rekeying rekeys remctl backend keytabs keytab kadmin KDC API Allbery
+unlinked
+
+=head1 NAME
+
+Wallet::Kadmin::MIT - Wallet Kerberos administration API for MIT
+
+=head1 SYNOPSIS
+
+ my $kadmin = Wallet::Kadmin::MIT->new;
+ $kadmin->create ('host/foo.example.com');
+ my $data = $kadmin->keytab_rekey ('host/foo.example.com',
+ 'aes256-cts-hmac-sha1-96');
+ $data = $kadmin->keytab ('host/foo.example.com');
+ my $exists = $kadmin->exists ('host/oldshell.example.com');
+ $kadmin->destroy ('host/oldshell.example.com') if $exists;
+
+=head1 DESCRIPTION
+
+Wallet::Kadmin::MIT implements the Wallet::Kadmin API for MIT Kerberos,
+providing an interface to create and delete principals and create keytabs.
+It provides the API documented in L<Wallet::Kadmin> for an MIT Kerberos
+KDC.
+
+MIT Kerberos does not provide any method via the kadmin network protocol
+to retrieve a keytab for a principal without rekeying it, so the keytab()
+method (as opposed to keytab_rekey(), which rekeys the principal) is
+implemented using a remctl backend. For that method (used for unchanging
+keytab objects) to work, the necessary wallet configuration and remctl
+interface on the KDC must be set up.
+
+To use this class, several configuration parameters must be set. See
+L<Wallet::Config/"KEYTAB OBJECT CONFIGURATION"> for details.
+
+=head1 FILES
+
+=over 4
+
+=item KEYTAB_TMP/keytab.<pid>
+
+The keytab is created in this file and then read into memory. KEYTAB_TMP
+is set in the wallet configuration, and <pid> is the process ID of the
+current process. The file is unlinked after being read.
+
+=back
+
+=head1 LIMITATIONS
+
+Currently, this implementation calls an external B<kadmin> program rather
+than using a native Perl module and therefore requires B<kadmin> be
+installed and parses its output. It may miss some error conditions if the
+output of B<kadmin> ever changes.
+
+=head1 SEE ALSO
+
+kadmin(8), Wallet::Config(3), Wallet::Kadmin(3),
+Wallet::Object::Keytab(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHORS
+
+Russ Allbery <eagle@eyrie.org> and Jon Robertson <jonrober@stanford.edu>.
+
+=cut
diff --git a/perl/lib/Wallet/Object/Base.pm b/perl/lib/Wallet/Object/Base.pm
new file mode 100644
index 0000000..a6a78bf
--- /dev/null
+++ b/perl/lib/Wallet/Object/Base.pm
@@ -0,0 +1,1052 @@
+# Wallet::Object::Base -- Parent class for any object stored in the wallet.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2008, 2010, 2011, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Object::Base;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($VERSION);
+
+use DateTime;
+use Date::Parse qw(str2time);
+use DBI;
+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.08';
+
+##############################################################################
+# Constructors
+##############################################################################
+
+# Initialize an object from the database. Verifies that the object already
+# exists with the given type, and if it does, returns a new blessed object of
+# the specified class. Stores the database handle to use, the name, and the
+# 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, $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 = {
+ schema => $schema,
+ name => $name,
+ type => $type,
+ };
+ bless ($self, $class);
+ return $self;
+}
+
+# Create a new object in the database of the specified name and type, setting
+# the ob_created_* fields accordingly, and returns a new blessed object of the
+# 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, $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 = DateTime->from_epoch (epoch => $time);
+ my %record = (ob_type => $type,
+ ob_name => $name,
+ ob_created_by => $user,
+ ob_created_from => $host,
+ ob_created_on => $date);
+ $schema->resultset('Object')->create (\%record);
+ %record = (oh_type => $type,
+ oh_name => $name,
+ oh_action => 'create',
+ oh_by => $user,
+ oh_from => $host,
+ oh_on => $date);
+ $schema->resultset('ObjectHistory')->create (\%record);
+ $guard->commit;
+ };
+ if ($@) {
+ die "cannot create object ${type}:${name}: $@\n";
+ }
+ my $self = {
+ schema => $schema,
+ name => $name,
+ type => $type,
+ };
+ bless ($self, $class);
+ return $self;
+}
+
+##############################################################################
+# Utility functions
+##############################################################################
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+# Returns the type of the object.
+sub type {
+ my ($self) = @_;
+ return $self->{type};
+}
+
+# Returns the name of the object.
+sub name {
+ my ($self) = @_;
+ return $self->{name};
+}
+
+# Record a global object action for this object. Takes the action (which must
+# be one of get or store), and the trace information: user, host, and time.
+# Returns true on success and false on failure, setting error appropriately.
+#
+# This function commits its transaction when complete and should not be called
+# inside another transaction.
+sub log_action {
+ my ($self, $action, $user, $host, $time) = @_;
+ unless ($action =~ /^(get|store)\z/) {
+ $self->error ("invalid history action $action");
+ return;
+ }
+
+ # 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 = DateTime->from_epoch (epoch => $time);
+ my %record = (oh_type => $self->{type},
+ oh_name => $self->{name},
+ oh_action => $action,
+ oh_by => $user,
+ oh_from => $host,
+ oh_on => $date);
+ $self->{schema}->resultset('ObjectHistory')->create (\%record);
+
+ # Add in more timestamps based on the action type.
+ my %search = (ob_type => $self->{type},
+ ob_name => $self->{name});
+ my $object = $self->{schema}->resultset('Object')->find (\%search);
+ if ($action eq 'get') {
+ $object->ob_downloaded_by ($user);
+ $object->ob_downloaded_from ($host);
+ $object->ob_downloaded_on ($date);
+ } elsif ($action eq 'store') {
+ $object->ob_stored_by ($user);
+ $object->ob_stored_from ($host);
+ $object->ob_stored_on ($date);
+ }
+ $object->update;
+ $guard->commit;
+ };
+ if ($@) {
+ my $id = $self->{type} . ':' . $self->{name};
+ $self->error ("cannot update history for $id: $@");
+ return;
+ }
+ return 1;
+}
+
+# Record a setting change for this object. Takes the field, the old value,
+# the new value, and the trace information (user, host, and time). The field
+# may have the special value "type_data <field>" in which case the value after
+# the whitespace is used as the type_field value.
+#
+# This function does not commit and does not catch exceptions. It should
+# normally be called as part of a larger transaction that implements the
+# setting change and should be committed with that change.
+sub log_set {
+ my ($self, $field, $old, $new, $user, $host, $time) = @_;
+ my $type_field;
+ if ($field =~ /^type_data\s+/) {
+ ($field, $type_field) = split (' ', $field, 2);
+ }
+ my %fields = map { $_ => 1 }
+ qw(owner acl_get acl_store acl_show acl_destroy acl_flags expires
+ comment flags type_data);
+ unless ($fields{$field}) {
+ die "invalid history field $field";
+ }
+
+ my $date = DateTime->from_epoch (epoch => $time);
+ 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 => $date);
+ $self->{schema}->resultset('ObjectHistory')->create (\%record);
+}
+
+##############################################################################
+# Get/set values
+##############################################################################
+
+# Set a particular attribute. Takes the attribute to set and its new value.
+# Returns undef on failure and true on success.
+sub _set_internal {
+ my ($self, $attr, $value, $user, $host, $time) = @_;
+ if ($attr !~ /^[a-z_]+\z/) {
+ $self->error ("invalid attribute $attr");
+ return;
+ }
+ $time ||= time;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ if ($self->flag_check ('locked')) {
+ $self->error ("cannot modify ${type}:${name}: object is locked");
+ return;
+ }
+
+ my $guard = $self->{schema}->txn_scope_guard;
+ eval {
+ my %search = (ob_type => $type,
+ ob_name => $name);
+ my $object = $self->{schema}->resultset('Object')->find (\%search);
+ my $column = "ob_$attr";
+ my $old = $object->$column;
+ my $new = $value;
+ $object->update ({ $column => $value });
+
+ if (ref ($old) && $old->isa ('DateTime')) {
+ $old->set_time_zone ('local');
+ $old = $old->ymd . q{ } . $old->hms;
+ }
+ if (ref ($new) && $new->isa ('DateTime')) {
+ $new->set_time_zone ('local');
+ $new = $new->ymd . q{ } . $new->hms;
+ }
+ $self->log_set ($attr, $old, $new, $user, $host, $time);
+ $guard->commit;
+ };
+ if ($@) {
+ my $id = $self->{type} . ':' . $self->{name};
+ $self->error ("cannot set $attr on $id: $@");
+ return;
+ }
+ return 1;
+}
+
+# Get a particular attribute. Returns the attribute value or undef if the
+# value isn't set or on a database error. The two cases can be distinguished
+# by whether $self->{error} is set.
+sub _get_internal {
+ my ($self, $attr) = @_;
+ undef $self->{error};
+ if ($attr !~ /^[a-z_]+\z/) {
+ $self->error ("invalid attribute $attr");
+ return;
+ }
+ $attr = 'ob_' . $attr;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ my $value;
+ eval {
+ my %search = (ob_type => $type,
+ ob_name => $name);
+ my $object = $self->{schema}->resultset('Object')->find (\%search);
+ $value = $object->$attr;
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $value;
+}
+
+# Get or set an ACL on an object. Takes the type of ACL and, if setting, the
+# new ACL identifier. If setting it, trace information must also be provided.
+sub acl {
+ my ($self, $type, $id, $user, $host, $time) = @_;
+ if ($type !~ /^(get|store|show|destroy|flags)\z/) {
+ $self->error ("invalid ACL type $type");
+ return;
+ }
+ my $attr = "acl_$type";
+ if ($id) {
+ my $acl;
+ eval { $acl = Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $self->_set_internal ($attr, $acl->id, $user, $host, $time);
+ } elsif (defined $id) {
+ return $self->_set_internal ($attr, undef, $user, $host, $time);
+ } else {
+ my $id = $self->_get_internal ($attr);
+ return unless defined $id;
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $acl->name;
+ }
+}
+
+# Get or set an attribute on an object. Takes the name of the attribute and,
+# if setting, the values and trace information. The values must be provided
+# as a reference to an array, even if there is only one value.
+#
+# Attributes are used by backends for backend-specific information (such as
+# enctypes for a keytab). The default implementation rejects all attribute
+# names as unknown.
+sub attr {
+ my ($self, $attr, $values, $user, $host, $time) = @_;
+ $self->error ("unknown attribute $attr");
+ return;
+}
+
+# Format the object attributes for inclusion in show(). The default
+# implementation just returns the empty string.
+sub attr_show {
+ my ($self) = @_;
+ 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.
+sub expires {
+ my ($self, $expires, $user, $host, $time) = @_;
+ if ($expires) {
+ my $seconds = str2time ($expires);
+ unless (defined $seconds) {
+ $self->error ("malformed expiration time $expires");
+ return;
+ }
+ my $date = DateTime->from_epoch (epoch => $seconds);
+ return $self->_set_internal ('expires', $date, $user, $host, $time);
+ } elsif (defined $expires) {
+ return $self->_set_internal ('expires', undef, $user, $host, $time);
+ } else {
+ my $date = $self->_get_internal ('expires');
+ if (defined $date) {
+ $date->set_time_zone ('local');
+ return $date->ymd . q{ } . $date->hms;
+ } else {
+ return;
+ }
+ }
+}
+
+# Get or set the owner of an object. If setting it, trace information must
+# also be provided.
+sub owner {
+ my ($self, $owner, $user, $host, $time) = @_;
+ if ($owner) {
+ my $acl;
+ eval { $acl = Wallet::ACL->new ($owner, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $self->_set_internal ('owner', $acl->id, $user, $host, $time);
+ } elsif (defined $owner) {
+ return $self->_set_internal ('owner', undef, $user, $host, $time);
+ } else {
+ my $id = $self->_get_internal ('owner');
+ return unless defined $id;
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $acl->name;
+ }
+}
+
+##############################################################################
+# Flags
+##############################################################################
+
+# Check whether a flag is set on the object. Returns true if set, 0 if not
+# set, and undef on error.
+sub flag_check {
+ my ($self, $flag) = @_;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ my $schema = $self->{schema};
+ my $value;
+ eval {
+ 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}: $@");
+ return;
+ } else {
+ return ($value) ? 1 : 0;
+ }
+}
+
+# Clear a flag on an object. Takes the flag and trace information. Returns
+# true on success and undef on failure.
+sub flag_clear {
+ my ($self, $flag, $user, $host, $time) = @_;
+ $time ||= time;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ my $schema = $self->{schema};
+ my $guard = $schema->txn_scope_guard;
+ eval {
+ 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";
+ }
+ $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}: $@");
+ return;
+ }
+ return 1;
+}
+
+# List the flags on an object. Returns a list of flag names, which may be
+# empty. On error, returns the empty list. The caller should call error() in
+# this case to determine if an error occurred.
+sub flag_list {
+ my ($self) = @_;
+ undef $self->{error};
+ my @flags;
+ eval {
+ 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);
+ }
+ };
+ if ($@) {
+ my $id = $self->{type} . ':' . $self->{name};
+ $self->error ("cannot retrieve flags for $id: $@");
+ return;
+ } else {
+ return @flags;
+ }
+}
+
+# Set a flag on an object. Takes the flag and trace information. Returns
+# true on success and undef on failure.
+sub flag_set {
+ my ($self, $flag, $user, $host, $time) = @_;
+ $time ||= time;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ my $schema = $self->{schema};
+ my $guard = $schema->txn_scope_guard;
+ eval {
+ 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";
+ }
+ $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}: $@");
+ return;
+ }
+ return 1;
+}
+
+##############################################################################
+# History
+##############################################################################
+
+# Expand a given ACL id to add its name, for readability. Returns the
+# original id alone if there was a problem finding the name.
+sub format_acl_id {
+ my ($self, $id) = @_;
+ my $name = $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;
+}
+
+# Return the formatted history for a given object or undef on error.
+# Currently always returns the complete history, but eventually will need to
+# provide some way of showing only recent entries.
+sub history {
+ my ($self) = @_;
+ my $output = '';
+ eval {
+ my %search = (oh_type => $self->{type},
+ oh_name => $self->{name});
+ my %attrs = (order_by => 'oh_id');
+ my @history = $self->{schema}->resultset('ObjectHistory')
+ ->search (\%search, \%attrs);
+
+ for my $history_rs (@history) {
+ my $date = $history_rs->oh_on;
+ $date->set_time_zone ('local');
+ $output .= sprintf ("%s %s ", $date->ymd, $date->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 ($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)) {
+ $output .= "remove $old from attribute $attr";
+ } elsif (defined ($new)) {
+ $output .= "add $new to attribute $attr";
+ }
+ } 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)) {
+ $output .= "set $field to $new (was $old)";
+ } elsif (defined ($new)) {
+ $output .= "set $field to $new";
+ } elsif (defined ($old)) {
+ $output .= "unset $field (was $old)";
+ }
+ } elsif ($action eq 'set') {
+ if (defined ($old) and defined ($new)) {
+ $output .= "set $field to $new (was $old)";
+ } elsif (defined ($new)) {
+ $output .= "set $field to $new";
+ } elsif (defined ($old)) {
+ $output .= "unset $field (was $old)";
+ }
+ } else {
+ $output .= $action;
+ }
+ $output .= sprintf ("\n by %s from %s\n", $history_rs->oh_by,
+ $history_rs->oh_from);
+ }
+ };
+ if ($@) {
+ my $id = $self->{type} . ':' . $self->{name};
+ $self->error ("cannot read history for $id: $@");
+ return;
+ }
+ return $output;
+}
+
+##############################################################################
+# Object manipulation
+##############################################################################
+
+# The get methods must always be overridden by the subclass.
+sub get { die "Do not instantiate Wallet::Object::Base directly\n"; }
+
+# Provide a default store implementation that returns an immutable object
+# error so that auto-generated types don't have to provide their own.
+sub store {
+ my ($self, $data, $user, $host, $time) = @_;
+ my $id = $self->{type} . ':' . $self->{name};
+ if ($self->flag_check ('locked')) {
+ $self->error ("cannot store $id: object is locked");
+ return;
+ }
+ $self->error ("cannot store $id: object type is immutable");
+ return;
+}
+
+# The default show function. This may be adequate for many types; types that
+# have additional data should call this method, grab the results, and then add
+# their data on to the end.
+sub show {
+ my ($self) = @_;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ my @attrs = ([ ob_type => 'Type' ],
+ [ ob_name => 'Name' ],
+ [ ob_owner => 'Owner' ],
+ [ ob_acl_get => 'Get ACL' ],
+ [ ob_acl_store => 'Store ACL' ],
+ [ ob_acl_show => 'Show ACL' ],
+ [ 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' ],
+ [ ob_stored_by => 'Stored by' ],
+ [ ob_stored_from => 'Stored from' ],
+ [ ob_stored_on => 'Stored on' ],
+ [ ob_downloaded_by => 'Downloaded by' ],
+ [ ob_downloaded_from => 'Downloaded from' ],
+ [ ob_downloaded_on => 'Downloaded on' ]);
+ my $fields = join (', ', map { $_->[0] } @attrs);
+ my @data;
+ my $object_rs;
+ eval {
+ 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}: $@");
+ return;
+ }
+ my $output = '';
+ my @acls;
+
+ # 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.
+ # The comment should be word-wrapped at 80 columns.
+ for my $i (0 .. $#attrs) {
+ my $field = $attrs[$i][0];
+ my $fieldtext = $attrs[$i][1];
+ my $value = $object_rs->$field;
+ next unless defined($value);
+
+ 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}//;
+ } elsif ($field eq 'ob_created_by') {
+ my @flags = $self->flag_list;
+ if (not @flags and $self->error) {
+ return;
+ }
+ if (@flags) {
+ $output .= sprintf ("%15s: %s\n", 'Flags', "@flags");
+ }
+ my $attr_output = $self->attr_show;
+ if (not defined $attr_output) {
+ return;
+ }
+ $output .= $attr_output;
+ } elsif (ref ($value) && $value->isa ('DateTime')) {
+ $value->set_time_zone ('local');
+ $value = sprintf ("%s %s", $value->ymd, $value->hms);
+ } elsif ($field =~ /^ob_(owner|acl_)/) {
+ my $acl = eval { Wallet::ACL->new ($value, $self->{schema}) };
+ if ($acl and not $@) {
+ $value = $acl->name || $value;
+ push (@acls, [ $acl, $value ]);
+ }
+ }
+ $output .= sprintf ("%15s: %s\n", $fieldtext, $value);
+ }
+ if (@acls) {
+ my %seen;
+ @acls = grep { !$seen{$_->[1]}++ } @acls;
+ for my $acl (@acls) {
+ $output .= "\n" . $acl->[0]->show;
+ }
+ }
+ return $output;
+}
+
+# The default destroy function only destroys the database metadata. Generally
+# subclasses need to override this to destroy whatever additional information
+# is stored about this object.
+sub destroy {
+ my ($self, $user, $host, $time) = @_;
+ $time ||= time;
+ my $name = $self->{name};
+ my $type = $self->{type};
+ if ($self->flag_check ('locked')) {
+ $self->error ("cannot destroy ${type}:${name}: object is locked");
+ return;
+ }
+ my $guard = $self->{schema}->txn_scope_guard;
+ eval {
+
+ # 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 $date = DateTime->from_epoch (epoch => $time);
+ my %record = (oh_type => $type,
+ oh_name => $name,
+ oh_action => 'destroy',
+ oh_by => $user,
+ oh_from => $host,
+ oh_on => $date);
+ $self->{schema}->resultset('ObjectHistory')->create (\%record);
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ("cannot destroy ${type}:${name}: $@");
+ return;
+ }
+ return 1;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=head1 NAME
+
+Wallet::Object::Base - Generic parent class for wallet objects
+
+=for stopwords
+DBH HOSTNAME DATETIME ACL backend metadata timestamp Allbery wallet-backend
+backend-specific subclasses
+
+=head1 SYNOPSIS
+
+ package Wallet::Object::Simple;
+ @ISA = qw(Wallet::Object::Base);
+ sub get {
+ my ($self, $user, $host, $time) = @_;
+ $self->log_action ('get', $user, $host, $time) or return;
+ return "Some secure data";
+ }
+
+=head1 DESCRIPTION
+
+Wallet::Object::Base is the generic parent class for wallet objects (data
+types that can be stored in the wallet system). It provides default
+functions and behavior, including handling generic object settings. All
+handlers for objects stored in the wallet should inherit from it. It is
+not used directly.
+
+=head1 PUBLIC CLASS METHODS
+
+The following methods are called by the rest of the wallet system and
+should be implemented by all objects stored in the wallet. They should be
+called with the desired wallet object class as the first argument
+(generally using the Wallet::Object::Type->new syntax).
+
+=over 4
+
+=item new(TYPE, NAME, DBH)
+
+Creates a new object with the given object type and name, based on data
+already in the database. This method will only succeed if an object of
+the given TYPE and NAME is already present in the wallet database. If no
+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::Schema object, which is stored in the object and used
+for any further operations.
+
+=item create(TYPE, NAME, DBH, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Similar to new() but instead creates a new entry in the database. This
+method will throw an exception if an entry for that type and name already
+exists in the database or if creating the database record fails.
+Otherwise, a new database entry will be created with that type and name,
+no owner, no ACLs, no expiration, no flags, and with created by, from, and
+on set to the PRINCIPAL, HOSTNAME, and DATETIME parameters. If DATETIME
+isn't given, the current time is used. The database handle is treated as
+with new().
+
+=back
+
+=head1 PUBLIC INSTANCE METHODS
+
+The following methods may be called on instantiated wallet objects.
+Normally, the only methods that a subclass will need to override are
+get(), store(), show(), and destroy().
+
+If the locked flag is set on an object, no actions may be performed on
+that object except for the flag methods and show(). All other actions
+will be rejected with an error saying the object is locked.
+
+=over 4
+
+=item acl(TYPE [, ACL, PRINCIPAL, HOSTNAME [, DATETIME]])
+
+Sets or retrieves a given object ACL as a numeric ACL ID. TYPE must be
+one of C<get>, C<store>, C<show>, C<destroy>, or C<flags>, corresponding
+to the ACLs kept on an object. If no other arguments are given, returns
+the current ACL setting as an ACL ID or undef if that ACL isn't set. If
+other arguments are given, change that ACL to ACL and return true on
+success and false on failure. Pass in the empty string for ACL to clear
+the ACL. 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 attr(ATTRIBUTE [, VALUES, PRINCIPAL, HOSTNAME [, DATETIME]])
+
+Sets or retrieves a given object attribute. Attributes are used to store
+backend-specific information for a particular object type and ATTRIBUTE
+must be an attribute type known to the underlying object implementation.
+The default implementation of this method rejects all attributes as
+unknown.
+
+If no other arguments besides ATTRIBUTE are given, returns the values of
+that attribute, if any, as a list. On error, returns the empty list. To
+distinguish between an error and an empty return, call error() afterward.
+It is guaranteed to return undef unless there was an error.
+
+If other arguments are given, sets the given ATTRIBUTE values to VALUES,
+which must be a reference to an array (even if only one value is being
+set). Pass a reference to an empty array to clear the attribute values.
+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. Returns true on success and false on failure.
+
+=item attr_show()
+
+Returns a formatted text description of the type-specific attributes of
+the object, or undef on error. The default implementation of this method
+always returns the empty string. If there are any type-specific
+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
+Wallet::Object::Base implementation handles the generic database work, but
+any subclass should override this method to do any deletion of files or
+entries in external databases and any other database entries and then call
+the parent method to handle the generic database cleanup. Returns true on
+success and false on failure. The 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 error([ERROR ...])
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+For the convenience of child classes, this method can also be called with
+one or more error strings. If so, those strings are concatenated
+together, trailing newlines are removed, any text of the form S<C< at \S+
+line \d+\.?>> at the end of the message is stripped off, and the result is
+stored as the error. Only child classes should call this method with an
+error string.
+
+=item expires([EXPIRES, PRINCIPAL, HOSTNAME [, DATETIME]])
+
+Sets or retrieves the expiration date of an object. If no arguments are
+given, returns the current expiration or undef if no expiration is set.
+If arguments are given, change the expiration to EXPIRES and return true
+on success and false on failure. EXPIRES must be in the format
+C<YYYY-MM-DD HH:MM:SS>, although the time portion may be omitted. Pass in
+the empty string for EXPIRES to clear the expiration date.
+
+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 flag_check(FLAG)
+
+Check whether the given flag is set on an object. Returns true if set,
+C<0> if not set, and undef on error.
+
+=item flag_clear(FLAG, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Clears FLAG on an object. Returns true on success and false on failure.
+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 flag_list()
+
+List the flags set on an object. If no flags are set, returns the empty
+list. On failure, returns an empty list. To distinguish between the
+empty response and an error, the caller should call error() after an empty
+return. It is guaranteed to return undef if there was no error.
+
+=item flag_set(FLAG, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Sets FLAG on an object. Returns true on success and false on failure.
+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 get(PRINCIPAL, HOSTNAME [, DATETIME])
+
+An object implementation must override this method with one that returns
+either the data of the object or undef on some error, using the provided
+arguments to update history information. The Wallet::Object::Base
+implementation just throws an exception.
+
+=item history()
+
+Returns the formatted history for the object. There will be two lines for
+each action on the object. The first line has the timestamp of the action
+and the action, and the second line gives the user who performed the
+action and the host from which they performed it (based on the trace
+information passed into the other object methods).
+
+=item name()
+
+Returns the object's name.
+
+=item owner([OWNER, PRINCIPAL, HOSTNAME [, DATETIME]])
+
+Sets or retrieves the owner of an object as a numeric ACL ID. If no
+arguments are given, returns the current owner ACL ID or undef if none is
+set. If arguments are given, change the owner to OWNER and return true on
+success and false on failure. Pass in the empty string for OWNER to clear
+the owner. 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 show()
+
+Returns a formatted text description of the object suitable for human
+display, or undef on error. All of the base metadata about the object,
+formatted as key: value pairs with the keys aligned in the first 15
+characters followed by a space, a colon, and the value. The attr_show()
+method of the object is also called and any formatted output it returns
+will be included. If any ACLs or an owner are set, after this data there
+is a blank line and then the information for each unique ACL, separated by
+blank lines.
+
+=item store(DATA, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Store user-supplied data into the given object. This may not be supported
+by all backends (for instance, backends that automatically generate the
+data will not support this). The default implementation rejects all
+store() calls with an error message saying that the object is immutable.
+
+=item type()
+
+Returns the object's type.
+
+=back
+
+=head1 UTILITY METHODS
+
+The following instance methods should not be called externally but are
+provided for subclasses to call to implement some generic actions.
+
+=over 4
+
+=item log_action (ACTION, PRINCIPAL, HOSTNAME, DATETIME)
+
+Updates the history tables and trace information appropriately for ACTION,
+which should be either C<get> or C<store>. No other changes are made to
+the database, just updates of the history table and trace fields with the
+provided data about who performed the action and when.
+
+This function commits its transaction when complete and therefore should
+not be called inside another transaction. Normally it's called as a
+separate transaction after the data is successfully stored or retrieved.
+
+=item log_set (FIELD, OLD, NEW, PRINCIPAL, HOSTNAME, DATETIME)
+
+Updates the history tables for the change in a setting value for an
+object. FIELD should be one of C<owner>, C<acl_get>, C<acl_store>,
+C<acl_show>, C<acl_destroy>, C<acl_flags>, C<expires>, C<flags>, or a
+value starting with C<type_data> followed by a space and a type-specific
+field name. The last form is the most common form used by a subclass.
+OLD is the previous value of the field or undef if the field was unset,
+and NEW is the new value of the field or undef if the field should be
+unset.
+
+This function does not commit and does not catch database exceptions. It
+should normally be called as part of a larger transaction that implements
+the change in the setting.
+
+=back
+
+=head1 SEE ALSO
+
+wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Object/Duo.pm b/perl/lib/Wallet/Object/Duo.pm
new file mode 100644
index 0000000..e3fe2da
--- /dev/null
+++ b/perl/lib/Wallet/Object/Duo.pm
@@ -0,0 +1,332 @@
+# Wallet::Object::Duo -- Duo integration object implementation for the wallet.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Object::Duo;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use JSON;
+use Net::Duo::Admin;
+use Net::Duo::Admin::Integration;
+use Perl6::Slurp qw(slurp);
+use Wallet::Config ();
+use Wallet::Object::Base;
+
+@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';
+
+##############################################################################
+# Core methods
+##############################################################################
+
+# Override attr_show to display the Duo integration key attribute.
+sub attr_show {
+ my ($self) = @_;
+ my $output = '';
+ my $key;
+ eval {
+ my %search = (du_name => $self->{name});
+ my $row = $self->{schema}->resultset ('Duo')->find (\%search);
+ $key = $row->get_column ('du_key');
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return sprintf ("%15s: %s\n", 'Duo key', $key);
+}
+
+# Override new to start by creating a Net::Duo::Admin object for subsequent
+# calls.
+sub new {
+ my ($class, $type, $name, $schema) = @_;
+
+ # We have to have a Duo integration key file set.
+ if (not $Wallet::Config::DUO_KEY_FILE) {
+ die "duo object implementation not configured\n";
+ }
+ my $key_file = $Wallet::Config::DUO_KEY_FILE;
+ my $agent = $Wallet::Config::DUO_AGENT;
+
+ # Construct the Net::Duo::Admin object.
+ require Net::Duo::Admin;
+ my $duo = Net::Duo::Admin->new (
+ {
+ key_file => $key_file,
+ user_agent => $agent,
+ }
+ );
+
+ # Construct the object.
+ my $self = $class->SUPER::new ($type, $name, $schema);
+ $self->{duo} = $duo;
+ return $self;
+}
+
+# Override create to start by creating a new integration in Duo, and only
+# create the entry in the database if that succeeds. Error handling isn't
+# great here since we don't have a way to communicate the error back to the
+# caller.
+sub create {
+ my ($class, $type, $name, $schema, $creator, $host, $time) = @_;
+
+ # We have to have a Duo integration key file set.
+ if (not $Wallet::Config::DUO_KEY_FILE) {
+ die "duo object implementation not configured\n";
+ }
+ my $key_file = $Wallet::Config::DUO_KEY_FILE;
+ my $agent = $Wallet::Config::DUO_AGENT;
+
+ # Construct the Net::Duo::Admin object.
+ require Net::Duo::Admin;
+ my $duo = Net::Duo::Admin->new (
+ {
+ key_file => $key_file,
+ user_agent => $agent,
+ }
+ );
+
+ # Create the object in Duo.
+ require Net::Duo::Admin::Integration;
+ my %data = (
+ name => $name,
+ notes => 'Managed by wallet',
+ type => $Wallet::Config::DUO_TYPE,
+ );
+ my $integration = Net::Duo::Admin::Integration->create ($duo, \%data);
+
+ # Create the object in wallet.
+ my @trace = ($creator, $host, $time);
+ my $self = $class->SUPER::create ($type, $name, $schema, @trace);
+ $self->{duo} = $duo;
+
+ # Add the integration key to the object metadata.
+ my $guard = $self->{schema}->txn_scope_guard;
+ eval {
+ my %record = (
+ du_name => $name,
+ du_key => $integration->integration_key,
+ );
+ $self->{schema}->resultset ('Duo')->create (\%record);
+ $guard->commit;
+ };
+ if ($@) {
+ my $id = $self->{type} . ':' . $self->{name};
+ $self->error ("cannot set Duo key for $id: $@");
+ return;
+ }
+
+ # Done. Return the object.
+ return $self;
+}
+
+# Override destroy to delete the integration out of Duo as well.
+sub destroy {
+ my ($self, $user, $host, $time) = @_;
+ my $id = $self->{type} . ':' . $self->{name};
+ if ($self->flag_check ('locked')) {
+ $self->error ("cannot destroy $id: object is locked");
+ return;
+ }
+ my $schema = $self->{schema};
+ my $guard = $schema->txn_scope_guard;
+ eval {
+ my %search = (du_name => $self->{name});
+ my $row = $schema->resultset ('Duo')->find (\%search);
+ my $key = $row->get_column ('du_key');
+ my $int = Net::Duo::Admin::Integration->new ($self->{duo}, $key);
+ $int->delete;
+ $row->delete;
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $self->SUPER::destroy ($user, $host, $time);
+}
+
+# Our get implementation. Retrieve the integration information from Duo and
+# construct the configuration file expected by the Duo PAM module.
+sub get {
+ my ($self, $user, $host, $time) = @_;
+ $time ||= time;
+
+ # Check that the object isn't locked.
+ my $id = $self->{type} . ':' . $self->{name};
+ if ($self->flag_check ('locked')) {
+ $self->error ("cannot get $id: object is locked");
+ return;
+ }
+
+ # Retrieve the integration from Duo.
+ my $key;
+ eval {
+ my %search = (du_name => $self->{name});
+ my $row = $self->{schema}->resultset ('Duo')->find (\%search);
+ $key = $row->get_column ('du_key');
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ my $integration = Net::Duo::Admin::Integration->new ($self->{duo}, $key);
+
+ # We also need the admin server name, which we can get from the Duo object
+ # configuration with a bit of JSON decoding.
+ my $json = JSON->new->utf8 (1);
+ my $config = $json->decode (scalar slurp $Wallet::Config::DUO_KEY_FILE);
+
+ # Construct the returned file.
+ my $output = "[duo]\n";
+ $output .= "ikey = $key\n";
+ $output .= 'skey = ' . $integration->secret_key . "\n";
+ $output .= "host = $config->{api_hostname}\n";
+
+ # Log the action and return.
+ $self->log_action ('get', $user, $host, $time);
+ return $output;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+Allbery Duo integration DBH keytab
+
+=head1 NAME
+
+Wallet::Object::Duo - Duo integration object implementation for wallet
+
+=head1 SYNOPSIS
+
+ my @name = qw(duo host.example.com);
+ my @trace = ($user, $host, time);
+ my $object = Wallet::Object::Duo->create (@name, $schema, @trace);
+ my $config = $object->get (@trace);
+ $object->destroy (@trace);
+
+=head1 DESCRIPTION
+
+Wallet::Object::Duo is a representation of Duo integrations the wallet.
+It implements the wallet object API and provides the necessary glue to
+create a Duo integration, return a configuration file containing the key
+and API information for that integration, and delete the integration from
+Duo when the wallet object is destroyed.
+
+Currently, only one configured integration type can be managed by the
+wallet, and the integration information is always returned in the
+configuration file format expected by the Duo UNIX integration. The
+results of retrieving this object will be text, suitable for putting in
+the UNIX integration configuration file, containing the integration key,
+secret key, and admin hostname for that integration.
+
+This object can be retrieved repeatedly without changing the secret key,
+matching Duo's native behavior with integrations. To change the keys of
+the integration, delete it and recreate it.
+
+To use this object, at least one configuration parameter must be set. See
+L<Wallet::Config> for details on supported 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 create(TYPE, NAME, DBH, PRINCIPAL, HOSTNAME [, DATETIME])
+
+This is a class method and should be called on the Wallet::Object::Duo
+class. It creates a new object with the given TYPE and NAME (TYPE is
+normally C<duo> and must be for the rest of the wallet system to use the
+right class, but this module doesn't check for ease of subclassing), using
+DBH as the handle to the wallet metadata database. PRINCIPAL, HOSTNAME,
+and DATETIME are stored as history information. PRINCIPAL should be the
+user who is creating the object. If DATETIME isn't given, the current
+time is used.
+
+When a new Duo integration object is created, a new integration will be
+created in the configured Duo account and the integration key will be
+stored in the wallet object. If the integration already exists, create()
+will fail. The new integration's type is controlled by the DUO_TYPE
+configuration variable, which defaults to C<unix>. See L<Wallet::Config>
+for more information.
+
+If create() fails, it throws an exception.
+
+=item destroy(PRINCIPAL, HOSTNAME [, DATETIME])
+
+Destroys a Duo integration object by removing it from the database and
+deleting the integration from Duo. If deleting the Duo integration fails,
+destroy() fails. 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])
+
+Retrieves the configuration information for the Duo integration and
+returns that information in the format expected by the configuration file
+for the Duo UNIX integration. Returns undef on failure. The caller
+should call error() to get the error message if get() returns undef.
+
+The returned configuration look look like:
+
+ [duo]
+ ikey = <integration-key>
+ skey = <secret-key>
+ host = <api-hostname>
+
+The C<host> parameter will be taken from the configuration file pointed
+to by the DUO_KEY_FILE configuration variable.
+
+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.
+
+=back
+
+=head1 LIMITATIONS
+
+Only one Duo account is supported for a given wallet implementation.
+Currently, only one Duo integration type is supported as well. Further
+development should expand the available integration types, possibly as
+additional wallet object types.
+
+=head1 SEE ALSO
+
+Net::Duo(3), Wallet::Config(3), Wallet::Object::Base(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Object/File.pm b/perl/lib/Wallet/Object/File.pm
new file mode 100644
index 0000000..1ff1288
--- /dev/null
+++ b/perl/lib/Wallet/Object/File.pm
@@ -0,0 +1,243 @@
+# Wallet::Object::File -- File object implementation for the wallet.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2008, 2010, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Object::File;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Digest::MD5 qw(md5_hex);
+use Wallet::Config ();
+use Wallet::Object::Base;
+
+@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.03';
+
+##############################################################################
+# File naming
+##############################################################################
+
+# Returns the path into which that file 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::FILE_BUCKET) {
+ $self->error ('file support not configured');
+ return;
+ }
+ unless ($name) {
+ $self->error ('file 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::FILE_BUCKET/$hash";
+ unless (-d $parent || mkdir ($parent, 0700)) {
+ $self->error ("cannot create file bucket $hash: $!");
+ return;
+ }
+ return "$Wallet::Config::FILE_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);
+}
+
+# Return the contents of the file.
+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 $path;
+ unless (open (FILE, '<', $path)) {
+ $self->error ("cannot get $id: object has not been stored");
+ return;
+ }
+ local $/;
+ my $data = <FILE>;
+ unless (close FILE) {
+ $self->error ("cannot get $id: $!");
+ return;
+ }
+ $self->log_action ('get', $user, $host, $time);
+ return $data;
+}
+
+# Store the file on the wallet server.
+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
+##############################################################################
+
+=head1 NAME
+
+Wallet::Object::File - File object implementation for wallet
+
+=for stopwords
+API HOSTNAME DATETIME keytab remctld backend nul Allbery wallet-backend
+
+=head1 SYNOPSIS
+
+ my @name = qw(file mysql-lsdb)
+ my @trace = ($user, $host, time);
+ my $object = Wallet::Object::Keytab->create (@name, $schema, @trace);
+ unless ($object->store ("the-password\n")) {
+ die $object->error, "\n";
+ }
+ my $password = $object->get (@trace);
+ $object->destroy (@trace);
+
+=head1 DESCRIPTION
+
+Wallet::Object::File is a representation of simple file objects in the
+wallet. It implements the wallet object API and provides the necessary
+glue to store a file on the wallet server, retrieve it later, and delete
+it when the file object is deleted. A file object must be stored before
+it can be retrieved with get.
+
+To use this object, the configuration option specifying where on the
+wallet server to store file objects must be set. See L<Wallet::Config>
+for details on this configuration parameter 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 file 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])
+
+Retrieves the current contents of the file object or undef on error.
+store() must be called before get() will be successful. 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.
+
+=item store(DATA, PRINCIPAL, HOSTNAME [, DATETIME])
+
+Store DATA as the current contents of the file object. 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 FILE_BUCKET/<hash>/<file>
+
+Files are stored on the wallet server under the directory FILE_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 C<%> and the hex code of the character.
+
+=back
+
+=head1 LIMITATIONS
+
+The wallet implementation itself can handle arbitrary file object names.
+However, due to limitations in the B<remctld> server usually used to run
+B<wallet-backend>, file object names containing nul characters (ASCII 0)
+may not be permitted. The file system used for storing file objects may
+impose a length limitation on the file object name.
+
+=head1 SEE ALSO
+
+remctld(8), Wallet::Config(3), Wallet::Object::Base(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Object/Keytab.pm b/perl/lib/Wallet/Object/Keytab.pm
new file mode 100644
index 0000000..975179b
--- /dev/null
+++ b/perl/lib/Wallet/Object/Keytab.pm
@@ -0,0 +1,514 @@
+# Wallet::Object::Keytab -- Keytab object implementation for the wallet.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2008, 2009, 2010, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Object::Keytab;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(@ISA $VERSION);
+
+use Wallet::Config ();
+use Wallet::Object::Base;
+use Wallet::Kadmin;
+
+@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.09';
+
+##############################################################################
+# Enctype restriction
+##############################################################################
+
+# Set the enctype restrictions for a keytab. Called by attr() and takes a
+# reference to the encryption types to set. Returns true on success and false
+# on failure, setting the object error if it fails.
+sub enctypes_set {
+ my ($self, $enctypes, $user, $host, $time) = @_;
+ $time ||= time;
+ my @trace = ($user, $host, $time);
+ my $name = $self->{name};
+ my %enctypes = map { $_ => 1 } @$enctypes;
+ my $guard = $self->{schema}->txn_scope_guard;
+ eval {
+ # 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 {
+ %search = (ke_name => $name,
+ ke_enctype => $enctype);
+ $self->{schema}->resultset('KeytabEnctype')->find (\%search)
+ ->delete;
+ $self->log_set ('type_data enctypes', $enctype, undef, @trace);
+ }
+ }
+
+ # When inserting new enctypes, we unfortunately have to do the
+ # consistency check against the enctypes table ourselves, since SQLite
+ # doesn't enforce integrity constraints. We do this in sorted order
+ # to make it easier to test.
+ for my $enctype (sort keys %enctypes) {
+ my %search = (en_name => $enctype);
+ my $enctype_rs = $self->{schema}->resultset('Enctype')
+ ->find (\%search);
+ unless (defined $enctype_rs) {
+ die "unknown encryption type $enctype\n";
+ }
+ my %record = (ke_name => $name,
+ ke_enctype => $enctype);
+ $self->{schema}->resultset('KeytabEnctype')->create (\%record);
+ $self->log_set ('type_data enctypes', undef, $enctype, @trace);
+ }
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return 1;
+}
+
+# Return a list of the encryption types current set for a keytab. Called by
+# attr() or get(). Returns the empty list on failure or on an empty list of
+# enctype restrictions, but sets the object error on failure so the caller
+# should use that to determine success.
+sub enctypes_list {
+ my ($self) = @_;
+ my @enctypes;
+ eval {
+ 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);
+ }
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return @enctypes;
+}
+
+##############################################################################
+# Synchronization
+##############################################################################
+
+# Set a synchronization target or clear the targets if $targets is an
+# empty list. Returns true on success and false on failure.
+#
+# Currently, no synchronization targets are supported, but we preserve the
+# ability to clear synchronization and the basic structure of the code so
+# that they can be added later.
+sub sync_set {
+ my ($self, $targets, $user, $host, $time) = @_;
+ $time ||= time;
+ my @trace = ($user, $host, $time);
+ if (@$targets > 1) {
+ $self->error ('only one synchronization target supported');
+ return;
+ } elsif (@$targets) {
+ my $target = $targets->[0];
+ $self->error ("unsupported synchronization target $target");
+ return;
+ } else {
+ my $guard = $self->{schema}->txn_scope_guard;
+ eval {
+ my $name = $self->{name};
+ 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);
+ }
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ }
+ return 1;
+}
+
+# Return a list of the current synchronization targets. Returns the empty
+# list on failure or on an empty list of enctype restrictions, but sets
+# the object error on failure so the caller should use that to determine
+# success.
+sub sync_list {
+ my ($self) = @_;
+ my @targets;
+ eval {
+ 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);
+ }
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return @targets;
+}
+
+##############################################################################
+# Core methods
+##############################################################################
+
+# Override attr to support setting the enctypes and sync attributes. Note
+# that the sync attribute has no supported targets at present and hence will
+# always return an error, but the code is still here so that it doesn't have
+# to be rewritten once a new sync target is added.
+sub attr {
+ my ($self, $attribute, $values, $user, $host, $time) = @_;
+ $time ||= time;
+ my @trace = ($user, $host, $time);
+ my %known = map { $_ => 1 } qw(enctypes sync);
+ undef $self->{error};
+ unless ($known{$attribute}) {
+ $self->error ("unknown attribute $attribute");
+ return;
+ }
+ if ($values) {
+ if ($attribute eq 'enctypes') {
+ return $self->enctypes_set ($values, $user, $host, $time);
+ } elsif ($attribute eq 'sync') {
+ return $self->sync_set ($values, $user, $host, $time);
+ }
+ } else {
+ if ($attribute eq 'enctypes') {
+ return $self->enctypes_list;
+ } elsif ($attribute eq 'sync') {
+ return $self->sync_list;
+ }
+ }
+}
+
+# Override attr_show to display the enctypes and sync attributes.
+sub attr_show {
+ my ($self) = @_;
+ my $output = '';
+ my @targets = $self->attr ('sync');
+ if (not @targets and $self->error) {
+ return;
+ } elsif (@targets) {
+ $output .= sprintf ("%15s: %s\n", 'Synced with', "@targets");
+ }
+ my @enctypes = $self->attr ('enctypes');
+ if (not @enctypes and $self->error) {
+ return;
+ } elsif (@enctypes) {
+ $output .= sprintf ("%15s: %s\n", 'Enctypes', $enctypes[0]);
+ shift @enctypes;
+ for my $enctype (@enctypes) {
+ $output .= (' ' x 17) . $enctype . "\n";
+ }
+ }
+ return $output;
+}
+
+# Override new to start by creating a handle for the kadmin module we're
+# using.
+sub new {
+ my ($class, $type, $name, $schema) = @_;
+ my $self = {
+ schema => $schema,
+ kadmin => undef,
+ };
+ bless $self, $class;
+ my $kadmin = Wallet::Kadmin->new ();
+ $self->{kadmin} = $kadmin;
+
+ $self = $class->SUPER::new ($type, $name, $schema);
+ $self->{kadmin} = $kadmin;
+ return $self;
+}
+
+# Override create to start by creating the principal in Kerberos and only
+# create the entry in the database if that succeeds. Error handling isn't
+# great here since we don't have a way to communicate the error back to the
+# caller.
+sub create {
+ my ($class, $type, $name, $schema, $creator, $host, $time) = @_;
+ my $self = {
+ schema => $schema,
+ kadmin => undef,
+ };
+ bless $self, $class;
+ my $kadmin = Wallet::Kadmin->new ();
+ $self->{kadmin} = $kadmin;
+
+ if (not $kadmin->create ($name)) {
+ die $kadmin->error, "\n";
+ }
+ $self = $class->SUPER::create ($type, $name, $schema, $creator, $host,
+ $time);
+ $self->{kadmin} = $kadmin;
+ return $self;
+}
+
+# Override destroy to delete the principal out of Kerberos as well.
+sub destroy {
+ my ($self, $user, $host, $time) = @_;
+ my $id = $self->{type} . ':' . $self->{name};
+ if ($self->flag_check ('locked')) {
+ $self->error ("cannot destroy $id: object is locked");
+ return;
+ }
+ my $schema = $self->{schema};
+ my $guard = $schema->txn_scope_guard;
+ eval {
+ 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 ($@);
+ return;
+ }
+ my $kadmin = $self->{kadmin};
+ if (not $kadmin->destroy ($self->{name})) {
+ $self->error ($kadmin->error);
+ return;
+ }
+ return $self->SUPER::destroy ($user, $host, $time);
+}
+
+# Our get implementation. Generate a keytab into a temporary file and then
+# return that as the return value.
+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 $kadmin = $self->{kadmin};
+ my $result;
+ if ($self->flag_check ('unchanging')) {
+ $result = $kadmin->keytab ($self->{name});
+ } else {
+ my @enctypes = $self->attr ('enctypes');
+ $result = $kadmin->keytab_rekey ($self->{name}, @enctypes);
+ }
+ if (defined $result) {
+ $self->log_action ('get', $user, $host, $time);
+ } else {
+ $self->error ($kadmin->error);
+ }
+ return $result;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+keytab API KDC keytabs HOSTNAME DATETIME enctypes enctype DBH metadata
+unmanaged kadmin Allbery unlinked
+
+=head1 NAME
+
+Wallet::Object::Keytab - Keytab object implementation for wallet
+
+=head1 SYNOPSIS
+
+ my @name = qw(keytab host/shell.example.com);
+ my @trace = ($user, $host, time);
+ my $object = Wallet::Object::Keytab->create (@name, $schema, @trace);
+ my $keytab = $object->get (@trace);
+ $object->destroy (@trace);
+
+=head1 DESCRIPTION
+
+Wallet::Object::Keytab is a representation of Kerberos keytab objects in
+the wallet. It implements the wallet object API and provides the
+necessary glue to create principals in a Kerberos KDC, create and return
+keytabs for those principals, and delete them out of Kerberos when the
+wallet object is destroyed.
+
+A keytab is an on-disk store for the key or keys for a Kerberos principal.
+Keytabs are used by services to verify incoming authentication from
+clients or by automated processes that need to authenticate to Kerberos.
+To create a keytab, the principal has to be created in Kerberos and then a
+keytab is generated and stored in a file on disk.
+
+This implementation generates a new random key (and hence invalidates all
+existing keytabs) each time the keytab is retrieved with the get() method.
+
+To use this object, several configuration parameters must be set. See
+L<Wallet::Config> for details on those 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 attr(ATTRIBUTE [, VALUES, PRINCIPAL, HOSTNAME [, DATETIME]])
+
+Sets or retrieves a given object attribute. The following attribute is
+supported:
+
+=over 4
+
+=item enctypes
+
+Restricts the generated keytab to a specific set of encryption types. The
+values of this attribute must be enctype strings recognized by Kerberos
+(strings like C<aes256-cts-hmac-sha1-96> or C<des-cbc-crc>). Encryption
+types must also be present in the list of supported enctypes stored in the
+database database or the attr() method will reject them. Note that the
+salt should not be included; since the salt is irrelevant for keytab keys,
+it will always be set to the default by the wallet.
+
+If this attribute is set, the principal will be restricted to that
+specific enctype list when get() is called for that keytab. If it is not
+set, the default set in the KDC will be used.
+
+This attribute is ignored if the C<unchanging> flag is set on a keytab.
+Keytabs retrieved with C<unchanging> set will contain all keys present in
+the KDC for that Kerberos principal and therefore may contain different
+enctypes than those requested by this attribute.
+
+=item sync
+
+This attribute is intended to set a list of external systems with which
+data about this keytab is synchronized, but there are no supported targets
+currently. However, there is support for clearing this attribute or
+returning its current value.
+
+=back
+
+If no other arguments besides ATTRIBUTE are given, returns the values of
+that attribute, if any, as a list. On error, returns the empty list. To
+distinguish between an error and an empty return, call error() afterward.
+It is guaranteed to return undef unless there was an error.
+
+If other arguments are given, sets the given ATTRIBUTE values to VALUES,
+which must be a reference to an array (even if only one value is being
+set). Pass a reference to an empty array to clear the attribute values.
+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 create(TYPE, NAME, DBH, PRINCIPAL, HOSTNAME [, DATETIME])
+
+This is a class method and should be called on the Wallet::Object::Keytab
+class. It creates a new object with the given TYPE and NAME (TYPE is
+normally C<keytab> and must be for the rest of the wallet system to use
+the right class, but this module doesn't check for ease of subclassing),
+using DBH as the handle to the wallet metadata database. PRINCIPAL,
+HOSTNAME, and DATETIME are stored as history information. PRINCIPAL
+should be the user who is creating the object. If DATETIME isn't given,
+the current time is used.
+
+When a new keytab object is created, the Kerberos principal designated by
+NAME is also created in the Kerberos realm determined from the wallet
+configuration. If the principal already exists, create() still succeeds
+(so that a previously unmanaged principal can be imported into the
+wallet). Otherwise, if the Kerberos principal could not be created,
+create() fails. The principal is created with the randomized keys. NAME
+must not contain the realm; instead, the KEYTAB_REALM configuration
+variable should be set. See L<Wallet::Config> for more information.
+
+If create() fails, it throws an exception.
+
+=item destroy(PRINCIPAL, HOSTNAME [, DATETIME])
+
+Destroys a keytab object by removing it from the database and deleting the
+principal out of Kerberos. If deleting the principal fails, destroy()
+fails, but destroy() succeeds if the principal didn't exist when it was
+called (so that it can be used to clean up stranded entries). 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])
+
+Retrieves a keytab for this object and returns the keytab data or undef on
+error. The caller should call error() to get the error message if get()
+returns undef. The keytab is created with new randomized keys,
+invalidating any existing keytabs for that principal, unless the
+unchanging flag is set on the object. 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.
+
+=back
+
+=head1 FILES
+
+=over 4
+
+=item KEYTAB_TMP/keytab.<pid>
+
+The keytab is created in this file and then read into memory. KEYTAB_TMP
+is set in the wallet configuration, and <pid> is the process ID of the
+current process. The file is unlinked after being read.
+
+=back
+
+=head1 LIMITATIONS
+
+Only one Kerberos realm is supported for a given wallet implementation and
+all keytab objects stored must be in that realm. Keytab names in the
+wallet database do not have realm information.
+
+=head1 SEE ALSO
+
+kadmin(8), Wallet::Config(3), Wallet::Object::Base(3), wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Object/WAKeyring.pm b/perl/lib/Wallet/Object/WAKeyring.pm
new file mode 100644
index 0000000..3e80300
--- /dev/null
+++ b/perl/lib/Wallet/Object/WAKeyring.pm
@@ -0,0 +1,371 @@
+# Wallet::Object::WAKeyring -- WebAuth keyring object implementation.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2012, 2013, 2014
+# 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 warnings;
+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 <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm
new file mode 100644
index 0000000..5ac29e0
--- /dev/null
+++ b/perl/lib/Wallet/Policy/Stanford.pm
@@ -0,0 +1,422 @@
+# Wallet::Policy::Stanford -- Stanford's wallet naming and ownership policy.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Policy::Stanford;
+
+use 5.008;
+use strict;
+use warnings;
+
+use base qw(Exporter);
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ $VERSION = '1.00';
+ @EXPORT_OK = qw(default_owner verify_name);
+}
+
+##############################################################################
+# Configuration
+##############################################################################
+
+# These variables are all declared as globals so that they can be overridden
+# from wallet.conf if desirable.
+
+# The domain to append to hostnames to fully-qualify them.
+our $DOMAIN = 'stanford.edu';
+
+# Groups for file object naming, each mapped to the ACL to use for
+# non-host-based objects owned by that group. This default is entirely
+# Stanford-specific, even more so than the rest of this file.
+our %ACL_FOR_GROUP = (
+ 'its-apps' => 'group/its-app-support',
+ 'its-crc-sg' => 'group/crcsg',
+ 'its-idg' => 'group/its-idg',
+ 'its-rc' => 'group/its-rc',
+ 'its-sa-core' => 'group/its-sa-core',
+);
+
+# Legacy group names for older file objects.
+our @GROUPS_LEGACY = qw(apps crcsg gsb idg sysadmin sulair vast);
+
+# File object types. Each type can have one or more parameters: whether it is
+# host-based (host), whether it takes a qualifier after the host or service
+# (extra), and whether that qualifier is mandatory (need_extra).
+our %FILE_TYPE = (
+ config => { extra => 1, need_extra => 1 },
+ db => { extra => 1, need_extra => 1 },
+ 'gpg-key' => { },
+ htpasswd => { host => 1, extra => 1, need_extra => 1 },
+ password => { extra => 1, need_extra => 1 },
+ 'password-ipmi' => { host => 1 },
+ 'password-root' => { host => 1 },
+ 'password-tivoli' => { host => 1 },
+ properties => { extra => 1 },
+ 'ssh-dsa' => { host => 1 },
+ 'ssh-rsa' => { host => 1 },
+ 'ssl-key' => { host => 1, extra => 1 },
+ 'ssl-keypair' => { host => 1, extra => 1 },
+ 'ssl-keystore' => { extra => 1 },
+ 'ssl-pkcs12' => { extra => 1 },
+ 'tivoli-key' => { host => 1 },
+);
+
+# Host-based file object types for the legacy file object naming scheme.
+our @FILE_HOST_LEGACY = qw(htpasswd ssh-rsa ssh-dsa ssl-key tivoli-key);
+
+# File object types for the legacy file object naming scheme.
+our @FILE_TYPES_LEGACY = qw(config db gpg-key htpasswd password properties
+ ssh-rsa ssh-dsa ssl-key ssl-keystore ssl-pkcs12 tivoli-key);
+
+# Host-based Kerberos principal prefixes.
+our @KEYTAB_HOST = qw(HTTP afpserver cifs ftp host imap ipp ldap lpr nfs pop
+ postgres sieve smtp webauth xmpp);
+
+# The Kerberos realm, used when forming principals for krb5 ACLs.
+our $REALM = 'stanford.edu';
+
+# A file listing principal names that should be required to use a root
+# instance to autocreate any objects.
+our $ROOT_REQUIRED = '/etc/remctl/acl/its-idg';
+
+##############################################################################
+# Implementation
+##############################################################################
+
+# Retrieve an existing ACL and return its members as a list.
+#
+# $name - Name of the ACL to retrieve
+#
+# Returns: Members of the ACL as a list of pairs
+# The empty list on any failure to retrieve the ACL
+sub _acl_members {
+ my ($name) = @_;
+ my $schema = eval { Wallet::Schema->connect };
+ return if (!$schema || $@);
+ my $acl = eval { Wallet::ACL->new ($name, $schema) };
+ return if (!$acl || $@);
+ return $acl->list;
+}
+
+# Retrieve an existing ACL and check whether it contains a netdb-root member.
+# This is used to check if a default ACL is already present with a netdb-root
+# member so that we can return a default owner that matches. We only ever
+# increase the ACL from netdb to netdb-root, never degrade it, so this doesn't
+# pose a security problem.
+#
+# On any failure, just return an empty ACL to use the default.
+sub _acl_has_netdb_root {
+ my ($name) = @_;
+ for my $line (_acl_members($name)) {
+ return 1 if $line->[0] eq 'netdb-root';
+ }
+ return;
+}
+
+# Map a file object name to a hostname for the legacy file object naming
+# scheme and return it. Returns undef if this file object name doesn't map to
+# a hostname.
+sub _host_for_file_legacy {
+ my ($name) = @_;
+ my %allowed = map { $_ => 1 } @FILE_HOST_LEGACY;
+ my $allowed_regex = '(?:' . join ('|', sort keys %allowed) . ')';
+ if ($name !~ /^[^-]+-(.*)-$allowed_regex(?:-.*)?$/) {
+ return;
+ }
+ my $host = $1;
+ if ($host !~ /\./) {
+ $host .= q{.} . $DOMAIN;
+ }
+ return $host;
+}
+
+# Map a file object name to a hostname. Returns undef if this file object
+# name doesn't map to a hostname.
+sub _host_for_file {
+ my ($name) = @_;
+
+ # If $name doesn't contain /, defer to the legacy naming scheme.
+ if ($name !~ m{ / }xms) {
+ return _host_for_file_legacy($name);
+ }
+
+ # Parse the name and check whether this is a host-based object.
+ my ($type, $host) = split('/', $name);
+ return if !$FILE_TYPE{$type}{host};
+ return $host;
+}
+
+# Map a keytab object name to a hostname and return it. Returns undef if this
+# keytab principal name doesn't map to a hostname.
+sub _host_for_keytab {
+ my ($name) = @_;
+ my %allowed = map { $_ => 1 } @KEYTAB_HOST;
+ return unless $name =~ m,/,;
+ my ($service, $host) = split ('/', $name, 2);
+ return unless $allowed{$service};
+ if ($host !~ /\./) {
+ $host .= q{.} . $DOMAIN;
+ }
+ return $host;
+}
+
+# The default owner of host-based objects should be the host keytab and the
+# NetDB ACL for that host, with one twist. If the creator of a new node is
+# using a root instance, we want to require everyone managing that node be
+# using root instances by default.
+sub default_owner {
+ my ($type, $name) = @_;
+
+ # How to determine the host for host-based objects.
+ my %host_for = (
+ keytab => \&_host_for_keytab,
+ file => \&_host_for_file,
+ );
+
+ # If we have a possible host mapping, see if we can use that.
+ if (defined($host_for{$type})) {
+ my $host = $host_for{$type}->($name);
+ if ($host) {
+ my $acl_name = "host/$host";
+ my @acl;
+ if ($ENV{REMOTE_USER} =~ m,/root,
+ || _acl_has_netdb_root ($acl_name)) {
+ @acl = ([ 'netdb-root', $host ],
+ [ 'krb5', "host/$host\@$REALM" ]);
+ } else {
+ @acl = ([ 'netdb', $host ],
+ [ 'krb5', "host/$host\@$REALM" ]);
+ }
+ return ($acl_name, @acl);
+ }
+ }
+
+ # We have no open if this is not a file object.
+ return if $type ne 'file';
+
+ # Parse the name of the file object only far enough to get type and group
+ # (if there is a group).
+ my ($file_type, $group) = split('/', $name);
+
+ # Host-based file objects should be caught by the above. We certainly
+ # can't do anything about them here.
+ return if $FILE_TYPE{$file_type}{host};
+
+ # If we have a mapping for this group, retrieve the ACL contents. We
+ # would like to just return the ACL name, but wallet currently requires we
+ # return the whole ACL.
+ my $acl = $ACL_FOR_GROUP{$group};
+ return if !defined($acl);
+ my @members = _acl_members($acl);
+ return if @members == 0;
+ return ($acl, @members);
+}
+
+# Enforce a naming policy. Host-based keytabs must have fully-qualified
+# hostnames, limit the acceptable characters for service/* keytabs, and
+# enforce our naming constraints on */cgi principals.
+#
+# Also use this function to require that IDG staff always do implicit object
+# creation using a */root instance.
+sub verify_name {
+ my ($type, $name, $user) = @_;
+ my %staff;
+ if (open (STAFF, '<', $ROOT_REQUIRED)) {
+ local $_;
+ while (<STAFF>) {
+ s/^\s+//;
+ s/\s+$//;
+ next if m,/root\@,;
+ $staff{$_} = 1;
+ }
+ close STAFF;
+ }
+
+ # Check for a staff member not using their root instance.
+ if (defined ($user) && $staff{$user}) {
+ return 'use a */root instance for wallet object creation';
+ }
+
+ # Check keytab naming conventions.
+ if ($type eq 'keytab') {
+ my %host = map { $_ => 1 } @KEYTAB_HOST;
+ if ($name !~ m,^[a-zA-Z0-9_-]+/[a-z0-9.-]+$,) {
+ return "invalid principal name $name";
+ }
+ my ($principal, $instance)
+ = ($name =~ m,^([a-zA-Z0-9_-]+)/([a-z0-9.-]+)$,);
+ unless (defined ($principal) && defined ($instance)) {
+ return "invalid principal name $name";
+ }
+ if ($host{$principal} and $principal ne 'http') {
+ if ($instance !~ /^[a-z0-9-]+\.[a-z0-9.-]+$/) {
+ return "host name $instance is not fully qualified";
+ }
+ } elsif ($principal eq 'afs') {
+ if ($instance !~ /^[a-z0-9-]+\.[a-z0-9.-]+$/) {
+ return "AFS cell name $instance is not fully qualified";
+ }
+ } elsif ($principal eq 'service') {
+ if ($instance !~ /^[a-z0-9-]+$/) {
+ return "invalid service principal name $name";
+ }
+ } elsif ($instance eq 'cgi') {
+ if ($principal !~ /^[a-z][a-z0-9]{1,7}$/
+ and $principal !~ /^(class|dept|group)-[a-z0-9_-]+$/) {
+ return "invalid CGI principal name $name";
+ }
+ } elsif ($instance eq 'cron') {
+ if ($principal !~ /^[a-z][a-z0-9]{1,7}$/
+ and $principal !~ /^(class|dept|group)-[a-z0-9_-]+$/) {
+ return "invalid cron principal name $name";
+ }
+ } else {
+ return "unknown principal type $principal";
+ }
+ }
+
+ # Check file object naming conventions.
+ if ($type eq 'file') {
+ if ($name =~ m{ / }xms) {
+ my @name = split('/', $name);
+
+ # Names have between two and four components and all must be
+ # non-empty.
+ if (@name > 4) {
+ return "too many components in $name";
+ }
+ if (@name < 2) {
+ return "too few components in $name";
+ }
+ if (grep { $_ eq q{} } @name) {
+ return "empty component in $name";
+ }
+
+ # All objects start with the type. First check if this is a
+ # host-based type.
+ my $type = shift @name;
+ if ($FILE_TYPE{$type} && $FILE_TYPE{$type}{host}) {
+ my ($host, $extra) = @name;
+ if ($host !~ m{ [.] }xms) {
+ return "host name $host is not fully qualified";
+ }
+ if (defined($extra) && !$FILE_TYPE{$type}{extra}) {
+ return "extraneous component at end of $name";
+ }
+ if (!defined($extra) && $FILE_TYPE{$type}{need_extra}) {
+ return "missing component in $name";
+ }
+ return;
+ }
+
+ # Otherwise, the name is group-based. There be at least two
+ # remaining components.
+ if (@name < 2) {
+ return "too few components in $name";
+ }
+ my ($group, $service, $extra) = @name;
+
+ # Check the group.
+ if (!$ACL_FOR_GROUP{$group}) {
+ return "unknown group $group";
+ }
+
+ # Check the type. Be sure it's not host-based.
+ if (!$FILE_TYPE{$type}) {
+ return "unknown type $type";
+ }
+ if ($FILE_TYPE{$type}{host}) {
+ return "bad name for host-based file type $type";
+ }
+
+ # Check the extra data.
+ if (defined($extra) && !$FILE_TYPE{$type}{extra}) {
+ return "extraneous component at end of $name";
+ }
+ if (!defined($extra) && $FILE_TYPE{$type}{need_extra}) {
+ return "missing component in $name";
+ }
+ return;
+ } else {
+ # Legacy naming scheme.
+ my %groups = map { $_ => 1 } @GROUPS_LEGACY;
+ my %types = map { $_ => 1 } @FILE_TYPES_LEGACY;
+ if ($name !~ m,^[a-zA-Z0-9_.-]+$,) {
+ return "invalid file object $name";
+ }
+ my $group_regex = '(?:' . join ('|', sort keys %groups) . ')';
+ my $type_regex = '(?:' . join ('|', sort keys %types) . ')';
+ if ($name !~ /^$group_regex-/) {
+ return "no recognized owning group in $name";
+ } elsif ($name !~ /^$group_regex-.*-$type_regex(-.*)?$/) {
+ return "invalid file object name $name";
+ }
+ }
+ }
+
+ # Success.
+ return;
+}
+
+1;
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+Allbery
+
+=head1 NAME
+
+Wallet::Policy::Stanford - Stanford's wallet naming and ownership policy
+
+=head1 SYNOPSIS
+
+ use Wallet::Policy::Stanford;
+ my ($type, $name, $user) = @_;
+
+ my $error = valid_name($type, $name, $user);
+ my ($name, @acl) = default_owner($type, $name);
+
+=head1 DESCRIPTION
+
+Wallet::Policy::Stanford implements Stanford's wallet naming and ownership
+policy as described in F<docs/stanford-naming> in the wallet distribution.
+It is primarily intended as an example for other sites, but it is used at
+Stanford to implement that policy.
+
+This module provides the default_owner() and verify_name() functions that
+are part of the wallet configuration interface (as documented in
+L<Wallet::Config>). They can be imported directly into a wallet
+configuration file from this module or wrapped to apply additional rules.
+
+=head1 SEE ALSO
+
+Wallet::Config(3)
+
+The L<Stanford policy|http://www.eyrie.org/~eagle/software/wallet/naming.html>
+implemented by this module.
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Report.pm b/perl/lib/Wallet/Report.pm
new file mode 100644
index 0000000..bf48308
--- /dev/null
+++ b/perl/lib/Wallet/Report.pm
@@ -0,0 +1,681 @@
+# Wallet::Report -- Wallet system reporting interface.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2008, 2009, 2010, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Report;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw($VERSION);
+
+use Wallet::ACL;
+use Wallet::Schema;
+
+# 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.04';
+
+##############################################################################
+# Constructor, destructor, and accessors
+##############################################################################
+
+# Create a new wallet report object. Opens a connection to the database that
+# will be used for all of the wallet configuration information. Throw an
+# exception if anything goes wrong.
+sub new {
+ my ($class) = @_;
+ my $schema = Wallet::Schema->connect;
+ my $self = { schema => $schema };
+ bless ($self, $class);
+ return $self;
+}
+
+# Returns the database handle (used mostly for testing).
+sub dbh {
+ my ($self) = @_;
+ return $self->{schema}->storage->dbh;
+}
+
+# Returns the DBIx::Class-based database schema object.
+sub schema {
+ my ($self) = @_;
+ return $self->{schema};
+}
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+# Disconnect the database handle on object destruction to avoid warnings.
+sub DESTROY {
+ my ($self) = @_;
+ $self->{schema}->storage->dbh->disconnect;
+}
+
+##############################################################################
+# Object reports
+##############################################################################
+
+# Return the SQL statement to find every object in the database.
+sub objects_all {
+ my ($self) = @_;
+ my @objects;
+
+ my %search = ();
+ my %options = (order_by => [ qw/ob_type ob_name/ ],
+ select => [ qw/ob_type ob_name/ ]);
+
+ return (\%search, \%options);
+}
+
+# Return the SQL statement and the search field required to find all objects
+# matching a specific type.
+sub objects_type {
+ my ($self, $type) = @_;
+ my @objects;
+
+ my %search = (ob_type => $type);
+ my %options = (order_by => [ qw/ob_type ob_name/ ],
+ select => [ qw/ob_type ob_name/ ]);
+
+ return (\%search, \%options);
+}
+
+# Return the SQL statement and search field required to find all objects owned
+# by a given ACL. If the requested owner is null, we ignore this and do a
+# different search for IS NULL. If the requested owner does not actually
+# match any ACLs, set an error and return undef.
+sub objects_owner {
+ my ($self, $owner) = @_;
+ my @objects;
+
+ my %search;
+ my %options = (order_by => [ qw/ob_type ob_name/ ],
+ select => [ qw/ob_type ob_name/ ]);
+
+ if (lc ($owner) eq 'null') {
+ %search = (ob_owner => undef);
+ } else {
+ my $acl = eval { Wallet::ACL->new ($owner, $self->{schema}) };
+ return unless $acl;
+ %search = (ob_owner => $acl->id);
+ }
+
+ return (\%search, \%options);
+}
+
+# Return the SQL statement and search field required to find all objects that
+# have a specific flag set.
+sub objects_flag {
+ my ($self, $flag) = @_;
+ my @objects;
+
+ my %search = ('flags.fl_flag' => $flag);
+ my %options = (join => 'flags',
+ prefetch => 'flags',
+ order_by => [ qw/ob_type ob_name/ ],
+ select => [ qw/ob_type ob_name/ ]);
+
+ return (\%search, \%options);
+}
+
+# Return the SQL statement and search field required to find all objects that
+# a given ACL has any permissions on. This expands from objects_owner in that
+# it will also match any records that have the ACL set for get, store, show,
+# destroy, or flags. If the requested owner does not actually match any ACLs,
+# set an error and return the empty string.
+sub objects_acl {
+ my ($self, $search) = @_;
+ my @objects;
+
+ my $schema = $self->{schema};
+ my $acl = eval { Wallet::ACL->new ($search, $schema) };
+ return unless $acl;
+
+ my @search = ({ ob_owner => $acl->id },
+ { ob_acl_get => $acl->id },
+ { ob_acl_store => $acl->id },
+ { ob_acl_show => $acl->id },
+ { ob_acl_destroy => $acl->id },
+ { ob_acl_flags => $acl->id });
+ my %options = (order_by => [ qw/ob_type ob_name/ ],
+ select => [ qw/ob_type ob_name/ ]);
+
+ return (\@search, \%options);
+}
+
+# Return the SQL statement to find all objects that have been created but
+# have never been retrieved (via get).
+sub objects_unused {
+ my ($self) = @_;
+ my @objects;
+
+ my %search = (ob_downloaded_on => undef);
+ my %options = (order_by => [ qw/ob_type ob_name/ ],
+ select => [ qw/ob_type ob_name/ ]);
+
+ return (\%search, \%options);
+}
+
+# Returns a list of all objects stored in the wallet database in the form of
+# type and name pairs. On error and for an empty database, the empty list
+# will be returned. To distinguish between an empty list and an error, call
+# error(), which will return undef if there was no error. Farms out specific
+# statement to another subroutine for specific search types, but each case
+# should return ob_type and ob_name in that order.
+sub objects {
+ my ($self, $type, @args) = @_;
+ undef $self->{error};
+
+ # Get the search and options array refs from specific functions.
+ my ($search_ref, $options_ref);
+ if (!defined $type || $type eq '') {
+ ($search_ref, $options_ref) = $self->objects_all;
+ } else {
+ if ($type ne 'unused' && @args != 1) {
+ $self->error ("object searches require one argument to search");
+ } elsif ($type eq 'type') {
+ ($search_ref, $options_ref) = $self->objects_type (@args);
+ } elsif ($type eq 'owner') {
+ ($search_ref, $options_ref) = $self->objects_owner (@args);
+ } elsif ($type eq 'flag') {
+ ($search_ref, $options_ref) = $self->objects_flag (@args);
+ } elsif ($type eq 'acl') {
+ ($search_ref, $options_ref) = $self->objects_acl (@args);
+ } elsif ($type eq 'unused') {
+ ($search_ref, $options_ref) = $self->objects_unused (@args);
+ } else {
+ $self->error ("do not know search type: $type");
+ }
+ return unless $search_ref;
+ }
+
+ # Perform the search and return on any errors.
+ my @objects;
+ my $schema = $self->{schema};
+ eval {
+ my @objects_rs = $schema->resultset('Object')->search ($search_ref,
+ $options_ref);
+ for my $object_rs (@objects_rs) {
+ push (@objects, [ $object_rs->ob_type, $object_rs->ob_name ]);
+ }
+ };
+ if ($@) {
+ $self->error ("cannot list objects: $@");
+ return;
+ }
+
+ return @objects;
+}
+
+##############################################################################
+# ACL reports
+##############################################################################
+
+# Returns the SQL statement required to find and return all ACLs in the
+# database.
+sub acls_all {
+ my ($self) = @_;
+ my @acls;
+
+ my $schema = $self->{schema};
+ my %search = ();
+ my %options = (order_by => [ qw/ac_id/ ],
+ select => [ qw/ac_id ac_name/ ]);
+
+ eval {
+ my @acls_rs = $schema->resultset('Acl')->search (\%search, \%options);
+ for my $acl_rs (@acls_rs) {
+ push (@acls, [ $acl_rs->ac_id, $acl_rs->ac_name ]);
+ }
+ };
+
+ if ($@) {
+ $self->error ("cannot list ACLs: $@");
+ return;
+ }
+ return (@acls);
+}
+
+# Returns the SQL statement required to find all empty ACLs in the database.
+sub acls_empty {
+ my ($self) = @_;
+ my @acls;
+
+ my $schema = $self->{schema};
+ my %search = (ae_id => undef);
+ my %options = (join => 'acl_entries',
+ prefetch => 'acl_entries',
+ order_by => [ qw/ac_id/ ],
+ select => [ qw/ac_id ac_name/ ]);
+
+ eval {
+ my @acls_rs = $schema->resultset('Acl')->search (\%search, \%options);
+ for my $acl_rs (@acls_rs) {
+ push (@acls, [ $acl_rs->ac_id, $acl_rs->ac_name ]);
+ }
+ };
+
+ if ($@) {
+ $self->error ("cannot list ACLs: $@");
+ return;
+ }
+ return (@acls);
+}
+
+# Returns the SQL statement and the field required to find ACLs containing the
+# specified entry. The identifier is automatically surrounded by wildcards to
+# do a substring search.
+sub acls_entry {
+ my ($self, $type, $identifier) = @_;
+ my @acls;
+
+ my $schema = $self->{schema};
+ my %search = (ae_scheme => $type,
+ ae_identifier => { like => '%'.$identifier.'%' });
+ my %options = (join => 'acl_entries',
+ prefetch => 'acl_entries',
+ order_by => [ qw/ac_id/ ],
+ select => [ qw/ac_id ac_name/ ],
+ distinct => 1);
+
+ eval {
+ my @acls_rs = $schema->resultset('Acl')->search (\%search, \%options);
+ for my $acl_rs (@acls_rs) {
+ push (@acls, [ $acl_rs->ac_id, $acl_rs->ac_name ]);
+ }
+ };
+
+ if ($@) {
+ $self->error ("cannot list ACLs: $@");
+ return;
+ }
+ return (@acls);
+}
+
+# Returns the SQL statement required to find unused ACLs.
+sub acls_unused {
+ my ($self) = @_;
+ my @acls;
+
+ my $schema = $self->{schema};
+ my %search = (
+ #'acls_owner.ob_owner' => undef,
+ #'acls_get.ob_owner' => undef,
+ #'acls_store.ob_owner' => undef,
+ #'acls_show.ob_owner' => undef,
+ #'acls_destroy.ob_owner' => undef,
+ #'acls_flags.ob_owner' => undef,
+ );
+ my %options = (#join => [ qw/acls_owner acls_get acls_store acls_show acls_destroy acls_flags/ ],
+ order_by => [ qw/ac_id/ ],
+ select => [ qw/ac_id ac_name/ ]);
+
+ eval {
+ my @acls_rs = $schema->resultset('Acl')->search (\%search, \%options);
+
+ # FIXME: Almost certainly a way of doing this with the search itself.
+ for my $acl_rs (@acls_rs) {
+ next if $acl_rs->acls_owner->first;
+ next if $acl_rs->acls_get->first;
+ next if $acl_rs->acls_store->first;
+ next if $acl_rs->acls_show->first;
+ next if $acl_rs->acls_destroy->first;
+ next if $acl_rs->acls_flags->first;
+ push (@acls, [ $acl_rs->ac_id, $acl_rs->ac_name ]);
+ }
+ };
+
+ if ($@) {
+ $self->error ("cannot list ACLs: $@");
+ return;
+ }
+ return (@acls);
+}
+
+# Obtain a textual representation of the membership of an ACL, returning undef
+# on error and setting the internal error.
+sub acl_membership {
+ my ($self, $id) = @_;
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ my @members = map { "$_->[0] $_->[1]" } $acl->list;
+ if (!@members && $acl->error) {
+ $self->error ($acl->error);
+ return;
+ }
+ return join ("\n", @members);
+}
+
+# Duplicate ACL detection unfortunately needs to do something more complex
+# than just return a SQL statement, so it's handled differently than other
+# reports. All the work is done here and the results returned as a list of
+# sets of duplicates.
+sub acls_duplicate {
+ my ($self) = @_;
+ my @acls = sort map { $_->[1] } $self->acls;
+ return if (!@acls && $self->{error});
+ return if @acls < 2;
+ my %result;
+ for my $i (0 .. ($#acls - 1)) {
+ my $members = $self->acl_membership ($acls[$i]);
+ return unless defined $members;
+ for my $j (($i + 1) .. $#acls) {
+ my $check = $self->acl_membership ($acls[$j]);
+ return unless defined $check;
+ if ($check eq $members) {
+ $result{$acls[$i]} ||= [];
+ push (@{ $result{$acls[$i]} }, $acls[$j]);
+ }
+ }
+ }
+ my @result;
+ for my $acl (sort keys %result) {
+ push (@result, [ $acl, sort @{ $result{$acl} } ]);
+ }
+ return @result;
+}
+
+# Returns a list of all ACLs stored in the wallet database as a list of pairs
+# of ACL IDs and ACL names, possibly limited by some criteria. On error and
+# for an empty database, the empty list will be returned. To distinguish
+# between an empty list and an error, call error(), which will return undef if
+# there was no error.
+sub acls {
+ my ($self, $type, @args) = @_;
+ undef $self->{error};
+
+ # Find the ACLs for any given search.
+ my @acls;
+ if (!defined $type || $type eq '') {
+ @acls = $self->acls_all;
+ } else {
+ if ($type eq 'duplicate') {
+ return $self->acls_duplicate;
+ } elsif ($type eq 'entry') {
+ if (@args == 0) {
+ $self->error ('ACL searches require an argument to search');
+ return;
+ } else {
+ @acls = $self->acls_entry (@args);
+ }
+ } elsif ($type eq 'empty') {
+ @acls = $self->acls_empty;
+ } elsif ($type eq 'unused') {
+ @acls = $self->acls_unused;
+ } else {
+ $self->error ("unknown search type: $type");
+ return;
+ }
+ }
+ return @acls;
+}
+
+# Returns all ACL entries contained in owner ACLs for matching objects.
+# Objects are specified by type and name, which may be SQL wildcard
+# expressions. Each list member will be a pair of ACL scheme and ACL
+# identifier, with duplicates removed. On error and for no matching entries,
+# the empty list will be returned. To distinguish between an empty return and
+# an error, call error(), which will return undef if there was no error.
+sub owners {
+ my ($self, $type, $name) = @_;
+ undef $self->{error};
+ my $schema = $self->{schema};
+
+ my @owners;
+ eval {
+ my %search = (
+ 'acls_owner.ob_type' => { like => $type },
+ 'acls_owner.ob_name' => { like => $name });
+ my %options = (
+ join => { 'acls' => 'acls_owner' },
+ order_by => [ qw/ae_scheme ae_identifier/ ],
+ distinct => 1,
+ );
+
+ my @acls_rs = $schema->resultset('AclEntry')->search (\%search,
+ \%options);
+ for my $acl_rs (@acls_rs) {
+ my $scheme = $acl_rs->ae_scheme;
+ my $identifier = $acl_rs->ae_identifier;
+ push (@owners, [ $scheme, $identifier ]);
+ }
+ };
+ if ($@) {
+ $self->error ("cannot report on owners: $@");
+ return;
+ }
+ return @owners;
+}
+
+##############################################################################
+# Auditing
+##############################################################################
+
+# Audit the database for violations of local policy. Returns a list of
+# objects (as type and name pairs) or a list of ACLs (as ID and name pairs).
+# On error and for no matching entries, the empty list will be returned. To
+# distinguish between an empty return and an error, call error(), which will
+# return undef if there was no error.
+sub audit {
+ my ($self, $type, $audit) = @_;
+ undef $self->{error};
+ unless (defined ($type) and defined ($audit)) {
+ $self->error ("type and audit not specified");
+ return;
+ }
+ if ($type eq 'objects') {
+ if ($audit eq 'name') {
+ return unless defined &Wallet::Config::verify_name;
+ my @objects = $self->objects;
+ my @results;
+ for my $object (@objects) {
+ my ($type, $name) = @$object;
+ my $error = Wallet::Config::verify_name ($type, $name);
+ push (@results, $object) if $error;
+ }
+ return @results;
+ } else {
+ $self->error ("unknown object audit: $audit");
+ return;
+ }
+ } elsif ($type eq 'acls') {
+ if ($audit eq 'name') {
+ return unless defined &Wallet::Config::verify_acl_name;
+ my @acls = $self->acls;
+ my @results;
+ for my $acl (@acls) {
+ my $error = Wallet::Config::verify_acl_name ($acl->[1]);
+ push (@results, $acl) if $error;
+ }
+ return @results;
+ } else {
+ $self->error ("unknown acl audit: $audit");
+ return;
+ }
+ } else {
+ $self->error ("unknown audit type: $type");
+ return;
+ }
+}
+
+1;
+__DATA__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=head1 NAME
+
+Wallet::Report - Wallet system reporting interface
+
+=for stopwords
+ACL ACLs wildcard Allbery SQL tuples
+
+=head1 SYNOPSIS
+
+ use Wallet::Report;
+ my $report = Wallet::Report->new;
+ my @objects = $report->objects ('type', 'keytab');
+ for my $object (@objects) {
+ print "@$object\n";
+ }
+ @objects = $report->audit ('objects', 'name');
+
+=head1 DESCRIPTION
+
+Wallet::Report provides a mechanism to generate lists and reports on the
+contents of the wallet database. The format of the results returned
+depend on the type of search, but will generally be returned as a list of
+tuples identifying objects, ACLs, or ACL entries.
+
+To use this object, several configuration variables must be set (at least
+the database configuration). For information on those variables and how
+to set them, see L<Wallet::Config>. For more information on the normal
+user interface to the wallet server, see L<Wallet::Server>.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new()
+
+Creates a new wallet report object and connects to the database. On any
+error, this method throws an exception.
+
+=back
+
+=head1 INSTANCE METHODS
+
+For all methods that can fail, the caller should call error() after a
+failure to get the error message. For all methods that return lists, if
+they return an empty list, the caller should call error() to distinguish
+between an empty report and an error.
+
+=over 4
+
+=item acls([ TYPE [, SEARCH ... ]])
+
+Returns a list of all ACLs matching a search type and string in the
+database, or all ACLs if no search information is given. There are
+currently four search types. C<duplicate> returns sets of duplicate ACLs
+(ones with exactly the same entries). C<empty> takes no arguments and
+will return only those ACLs that have no entries within them. C<entry>
+takes two arguments, an entry scheme and a (possibly partial) entry
+identifier, and will return any ACLs containing an entry with that scheme
+and with an identifier containing that value. C<unused> returns all ACLs
+that are not referenced by any object.
+
+The return value for everything except C<duplicate> is a list of
+references to pairs of ACL ID and name. For example, if there are two
+ACLs in the database, one with name C<ADMIN> and ID 1 and one with name
+C<group/admins> and ID 3, acls() with no arguments would return:
+
+ ([ 1, 'ADMIN' ], [ 3, 'group/admins' ])
+
+The return value for the C<duplicate> search is sets of ACL names that are
+duplicates (have the same entries). For example, if C<d1>, C<d2>, and
+C<d3> are all duplicates, and C<o1> and C<o2> are also duplicates, the
+result would be:
+
+ ([ 'd1', 'd2', 'd3' ], [ 'o1', 'o2' ])
+
+Returns the empty list on failure. An error can be distinguished from
+empty search results by calling error(). error() is guaranteed to return
+the error message if there was an error and undef if there was no error.
+
+=item audit(TYPE, AUDIT)
+
+Audits the wallet database for violations of local policy. TYPE is the
+general class of thing to audit, and AUDIT is the specific audit to
+perform. TYPE may be either C<objects> or C<acls>. Currently, the only
+implemented audit is C<name>. This returns a list of all objects, as
+references to pairs of type and name, or ACLs, as references to pairs of
+ID and name, that are not accepted by the verify_name() or
+verify_acl_name() function defined in the wallet configuration. See
+L<Wallet::Config> for more information.
+
+Returns the empty list on failure. An error can be distinguished from
+empty search results by calling error(). error() is guaranteed to return
+the error message if there was an error and undef if there was no error.
+
+=item error()
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+=item objects([ TYPE [, SEARCH ... ]])
+
+Returns a list of all objects matching a search type and string in the
+database, or all objects in the database if no search information is
+given.
+
+There are five types of searches currently. C<type>, with a given type,
+will return only those entries where the type matches the given type.
+C<owner>, with a given owner, will only return those objects owned by the
+given ACL name or ID. C<flag>, with a given flag name, will only return
+those items with a flag set to the given value. C<acl> operates like
+C<owner>, but will return only those objects that have the given ACL name
+or ID on any of the possible ACL settings, not just owner. C<unused> will
+return all entries for which a get command has never been issued.
+
+The return value is a list of references to pairs of type and name. For
+example, if two objects existed in the database, both of type C<keytab>
+and with values C<host/example.com> and C<foo>, objects() with no
+arguments would return:
+
+ ([ 'keytab', 'host/example.com' ], [ 'keytab', 'foo' ])
+
+Returns the empty list on failure. To distinguish between this and an
+empty search result, the caller should call error(). error() is
+guaranteed to return the error message if there was an error and undef if
+there was no error.
+
+=item owners(TYPE, NAME)
+
+Returns a list of all ACL lines contained in owner ACLs for objects
+matching TYPE and NAME, which are interpreted as SQL patterns using C<%>
+as a wildcard. The return value is a list of references to pairs of
+schema and identifier, with duplicates removed.
+
+Returns the empty list on failure. To distinguish between this and no
+matches, the caller should call error(). error() is guaranteed to return
+the error message if there was an error and undef if there was no error.
+
+=back
+
+=head1 SEE ALSO
+
+Wallet::Config(3), Wallet::Server(3)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org> and Jon Robertson <jonrober@stanford.edu>.
+
+=cut
diff --git a/perl/lib/Wallet/Schema.pm b/perl/lib/Wallet/Schema.pm
new file mode 100644
index 0000000..cb4c93e
--- /dev/null
+++ b/perl/lib/Wallet/Schema.pm
@@ -0,0 +1,354 @@
+# Database schema and connector for the wallet system.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema;
+
+use strict;
+use warnings;
+
+use Wallet::Config;
+
+use base 'DBIx::Class::Schema';
+
+# 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.
+our $VERSION = '0.09';
+
+__PACKAGE__->load_namespaces;
+__PACKAGE__->load_components (qw/Schema::Versioned/);
+
+##############################################################################
+# Core overrides
+##############################################################################
+
+# Override DBI::connect to supply our own connect string, username, and
+# password and to set some standard options. Takes no arguments other than
+# the implicit class argument.
+sub connect {
+ my ($class) = @_;
+ unless ($Wallet::Config::DB_DRIVER
+ and (defined ($Wallet::Config::DB_INFO)
+ or defined ($Wallet::Config::DB_NAME))) {
+ die "database connection information not configured\n";
+ }
+ my $dsn = "DBI:$Wallet::Config::DB_DRIVER:";
+ if (defined $Wallet::Config::DB_INFO) {
+ $dsn .= $Wallet::Config::DB_INFO;
+ } else {
+ $dsn .= "database=$Wallet::Config::DB_NAME";
+ $dsn .= ";host=$Wallet::Config::DB_HOST" if $Wallet::Config::DB_HOST;
+ $dsn .= ";port=$Wallet::Config::DB_PORT" if $Wallet::Config::DB_PORT;
+ }
+ my $user = $Wallet::Config::DB_USER;
+ my $pass = $Wallet::Config::DB_PASSWORD;
+ my %attrs = (PrintError => 0, RaiseError => 1);
+ my $schema = eval { $class->SUPER::connect ($dsn, $user, $pass, \%attrs) };
+ if ($@) {
+ die "cannot connect to database: $@\n";
+ }
+ return $schema;
+}
+
+1;
+
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=for stopwords
+RaiseError PrintError AutoCommit ACL verifier API APIs enums keytab backend
+enctypes DBI Allbery
+
+=head1 NAME
+
+Wallet::Schema - Database schema and connector for the wallet system
+
+=head1 SYNOPSIS
+
+ use Wallet::Schema;
+ my $schema = Wallet::Schema->connect;
+
+=head1 DESCRIPTION
+
+This class encapsulates the database schema for the wallet system. The
+documentation you're reading explains and comments the schema. The
+class runs using the DBIx::Class module.
+
+connect() will obtain the database connection information from the wallet
+configuration; see L<Wallet::Config> for more details. It will also
+automatically set the RaiseError attribute to true and the PrintError and
+AutoCommit attributes to false, matching the assumptions made by the
+wallet database code.
+
+=head1 SCHEMA
+
+=head2 Normalization Tables
+
+Holds the supported object types and their corresponding Perl classes:
+
+ create table types
+ (ty_name varchar(16) primary key,
+ ty_class varchar(64));
+ insert into types (ty_name, ty_class)
+ values ('file', 'Wallet::Object::File');
+ insert into types (ty_name, ty_class)
+ values ('keytab', 'Wallet::Object::Keytab');
+
+Holds the supported ACL schemes and their corresponding Perl classes:
+
+ create table acl_schemes
+ (as_name varchar(32) primary key,
+ as_class varchar(64));
+ insert into acl_schemes (as_name, as_class)
+ values ('krb5', 'Wallet::ACL::Krb5');
+ insert into acl_schemes (as_name, as_class)
+ values ('krb5-regex', 'Wallet::ACL::Krb5::Regex');
+ insert into acl_schemes (as_name, as_class)
+ values ('ldap-attr', 'Wallet::ACL::LDAP::Attribute');
+ insert into acl_schemes (as_name, as_class)
+ values ('netdb', 'Wallet::ACL::NetDB');
+ insert into acl_schemes (as_name, as_class)
+ values ('netdb-root', 'Wallet::ACL::NetDB::Root');
+
+If you have extended the wallet to support additional object types or
+additional ACL schemes, you will want to add additional rows to these
+tables mapping those types or schemes to Perl classes that implement the
+object or ACL verifier APIs.
+
+=head2 ACL Tables
+
+A wallet ACL consists of zero or more ACL entries, each of which is a
+scheme and an identifier. The scheme identifies the check that should be
+performed and the identifier is additional scheme-specific information.
+Each ACL references entries in the following table:
+
+ create table acls
+ (ac_id integer auto_increment primary key,
+ ac_name varchar(255) not null,
+ unique (ac_name));
+
+This just keeps track of unique ACL identifiers. The data is then stored
+in:
+
+ create table acl_entries
+ (ae_id integer not null references acls(ac_id),
+ ae_scheme varchar(32)
+ not null references acl_schemes(as_name),
+ ae_identifier varchar(255) not null,
+ primary key (ae_id, ae_scheme, ae_identifier));
+ create index ae_id on acl_entries (ae_id);
+
+ACLs may be referred to in the API via either the numeric ID or the
+human-readable name, but internally ACLs are always referenced by numeric
+ID so that they can be renamed without requiring complex data
+modifications.
+
+Currently, the ACL named C<ADMIN> (case-sensitive) is special-cased in the
+Wallet::Server code and granted global access.
+
+Every change made to any ACL in the database will be recorded in this
+table.
+
+ create table acl_history
+ (ah_id integer auto_increment primary key,
+ ah_acl integer not null,
+ ah_name varchar(255) default null,
+ ah_action varchar(16) not null,
+ ah_scheme varchar(32) default null,
+ ah_identifier varchar(255) default null,
+ ah_by varchar(255) not null,
+ ah_from varchar(255) not null,
+ ah_on datetime not null);
+ create index ah_acl on acl_history (ah_acl);
+
+ah_action must be one of C<create>, C<destroy>, C<add>, C<remove>, or
+C<rename> (enums aren't used for compatibility with databases other than
+MySQL). For a change of type create, destroy, or rename, only the action,
+the ACL name (in the case of rename, the old ACL name prior to the
+rename), and the trace records (by, from, and on) are stored. For a
+change to the lines of an ACL, the scheme and identifier of the line that
+was added or removed are included.
+
+ah_by stores the authenticated identity that made the change, ah_from
+stores the host from which they made the change, and ah_on stores the time
+the change was made.
+
+=head2 Object Tables
+
+Each object stored in the wallet is represented by an entry in the objects
+table:
+
+ create table objects
+ (ob_type varchar(16)
+ not null references types(ty_name),
+ ob_name varchar(255) not null,
+ ob_owner integer default null references acls(ac_id),
+ ob_acl_get integer default null references acls(ac_id),
+ ob_acl_store integer default null references acls(ac_id),
+ ob_acl_show integer default null references acls(ac_id),
+ ob_acl_destroy integer default null references acls(ac_id),
+ ob_acl_flags integer default null references acls(ac_id),
+ ob_expires datetime default null,
+ ob_created_by varchar(255) not null,
+ ob_created_from varchar(255) not null,
+ ob_created_on datetime not null,
+ ob_stored_by varchar(255) default null,
+ ob_stored_from varchar(255) default null,
+ ob_stored_on datetime default null,
+ ob_downloaded_by varchar(255) default null,
+ ob_downloaded_from varchar(255) default null,
+ ob_downloaded_on datetime default null,
+ ob_comment varchar(255) default null,
+ primary key (ob_name, ob_type));
+ create index ob_owner on objects (ob_owner);
+ create index ob_expires on objects (ob_expires);
+
+Object names are not globally unique but only unique within their type, so
+the table has a joint primary key. Each object has an owner and then up
+to five more specific ACLs. The owner provides permission for get, store,
+and show operations if no more specific ACL is set. It does not provide
+permission for destroy or flags.
+
+The ob_acl_flags ACL controls who can set flags on this object. Each
+object may have zero or more flags associated with it:
+
+ create table flags
+ (fl_type varchar(16)
+ not null references objects(ob_type),
+ fl_name varchar(255)
+ not null references objects(ob_name),
+ fl_flag enum('locked', 'unchanging')
+ not null,
+ primary key (fl_type, fl_name, fl_flag));
+ create index fl_object on flags (fl_type, fl_name);
+
+Every change made to any object in the wallet database will be recorded in
+this table:
+
+ create table object_history
+ (oh_id integer auto_increment primary key,
+ oh_type varchar(16)
+ not null references objects(ob_type),
+ oh_name varchar(255)
+ not null references objects(ob_name),
+ oh_action varchar(16) not null,
+ oh_field varchar(16) default null,
+ oh_type_field varchar(255) default null,
+ oh_old varchar(255) default null,
+ oh_new varchar(255) default null,
+ oh_by varchar(255) not null,
+ oh_from varchar(255) not null,
+ oh_on datetime not null);
+ create index oh_object on object_history (oh_type, oh_name);
+
+oh_action must be one of C<create>, C<destroy>, C<get>, C<store>, or
+C<set>. oh_field must be one of C<owner>, C<acl_get>, C<acl_store>,
+C<acl_show>, C<acl_destroy>, C<acl_flags>, C<expires>, C<flags>, or
+C<type_data>. Enums aren't used for compatibility with databases other
+than MySQL.
+
+For a change of type create, get, store, or destroy, only the action and
+the trace records (by, from, and on) are stored. For changes to columns
+or to the flags table, oh_field takes what attribute is changed, oh_from
+takes the previous value converted to a string and oh_to takes the next
+value similarly converted to a string. The special field value
+"type_data" is used when type-specific data is changed, and in that case
+(and only that case) some type-specific name for the data being changed is
+stored in oh_type_field.
+
+When clearing a flag, oh_old will have the name of the flag and oh_new
+will be null. When setting a flag, oh_old will be null and oh_new will
+have the name of the flag.
+
+oh_by stores the authenticated identity that made the change, oh_from
+stores the host from which they made the change, and oh_on stores the time
+the change was made.
+
+=head2 Duo Backend Data
+
+Duo integration objects store some additional metadata about the
+integration to aid in synchronization with Duo.
+
+ create table duo
+ (du_name varchar(255)
+ not null references objects(ob_name),
+ du_key varchar(255) not null);
+ create index du_key on duo (du_key);
+
+du_key holds the Duo integration key, which is the unique name of the
+integration within Duo. Additional data may be added later to represent
+the other possible settings within Duo.
+
+=head2 Keytab Backend Data
+
+The keytab backend has stub support for synchronizing keys with an
+external system, although no external systems are currently supported.
+The permitted external systems are listed in a normalization table:
+
+ create table sync_targets
+ (st_name varchar(255) primary key);
+
+and then the synchronization targets for a given keytab are stored in this
+table:
+
+ create table keytab_sync
+ (ks_name varchar(255)
+ not null references objects(ob_name),
+ ks_target varchar(255)
+ not null references sync_targets(st_name),
+ primary key (ks_name, ks_target));
+ create index ks_name on keytab_sync (ks_name);
+
+The keytab backend supports restricting the allowable enctypes for a given
+keytab. The permitted enctypes are listed in a normalization table:
+
+ create table enctypes
+ (en_name varchar(255) primary key);
+
+and then the restrictions for a given keytab are stored in this table:
+
+ create table keytab_enctypes
+ (ke_name varchar(255)
+ not null references objects(ob_name),
+ ke_enctype varchar(255)
+ not null references enctypes(en_name),
+ primary key (ke_name, ke_enctype));
+ create index ke_name on keytab_enctypes (ke_name);
+
+To use this functionality, you will need to populate the enctypes table
+with the enctypes that a keytab may be restricted to. Currently, there is
+no automated mechanism to do this.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item connect()
+
+Opens a new database connection and returns the database object. On any
+failure, throws an exception. Unlike the DBI method, connect() takes no
+arguments; all database connection information is derived from the wallet
+configuration.
+
+=back
+
+=head1 SEE ALSO
+
+wallet-backend(8), Wallet::Config(3)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut
diff --git a/perl/lib/Wallet/Schema/Result/Acl.pm b/perl/lib/Wallet/Schema/Result/Acl.pm
new file mode 100644
index 0000000..226738a
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/Acl.pm
@@ -0,0 +1,110 @@
+# Wallet schema for an ACL.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::Acl;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+ACL
+
+=head1 NAME
+
+Wallet::Schema::Result::Acl - Wallet schema for an ACL
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("acls");
+
+=head1 ACCESSORS
+
+=head2 ac_id
+
+ data_type: 'integer'
+ is_auto_increment: 1
+ is_nullable: 0
+
+=head2 ac_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ac_id",
+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
+ "ac_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("ac_id");
+__PACKAGE__->add_unique_constraint("ac_name", ["ac_name"]);
+
+__PACKAGE__->has_one(
+ 'acl_entries',
+ 'Wallet::Schema::Result::AclEntry',
+ { 'foreign.ae_id' => 'self.ac_id' },
+ { cascade_copy => 0, cascade_delete => 0 },
+ );
+__PACKAGE__->has_many(
+ 'acl_history',
+ 'Wallet::Schema::Result::AclHistory',
+ { 'foreign.ah_id' => 'self.ac_id' },
+ { cascade_copy => 0, cascade_delete => 0 },
+ );
+
+# References for all of the various potential ACLs in owners.
+__PACKAGE__->has_many(
+ 'acls_owner',
+ 'Wallet::Schema::Result::Object',
+ { 'foreign.ob_owner' => 'self.ac_id' },
+ );
+__PACKAGE__->has_many(
+ 'acls_get',
+ 'Wallet::Schema::Result::Object',
+ { 'foreign.ob_acl_get' => 'self.ac_id' },
+ );
+__PACKAGE__->has_many(
+ 'acls_store',
+ 'Wallet::Schema::Result::Object',
+ { 'foreign.ob_acl_store' => 'self.ac_id' },
+ );
+__PACKAGE__->has_many(
+ 'acls_show',
+ 'Wallet::Schema::Result::Object',
+ { 'foreign.ob_acl_show' => 'self.ac_id' },
+ );
+__PACKAGE__->has_many(
+ 'acls_destroy',
+ 'Wallet::Schema::Result::Object',
+ { 'foreign.ob_acl_destroy' => 'self.ac_id' },
+ );
+__PACKAGE__->has_many(
+ 'acls_flags',
+ 'Wallet::Schema::Result::Object',
+ { 'foreign.ob_acl_flags' => 'self.ac_id' },
+ );
+
+# Override the insert method so that we can automatically create history
+# items.
+#sub insert {
+# my ($self, @args) = @_;
+# my $ret = $self->next::method (@args);
+# print "ID: ".$self->ac_id."\n";
+# use Data::Dumper; print Dumper (@args);
+
+# return $self;
+#}
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/AclEntry.pm b/perl/lib/Wallet/Schema/Result/AclEntry.pm
new file mode 100644
index 0000000..a33a98c
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/AclEntry.pm
@@ -0,0 +1,74 @@
+# Wallet schema for an entry in an ACL.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::AclEntry;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+ACL
+
+=head1 NAME
+
+Wallet::Schema::Result::AclEntry - Wallet schema for an entry in an ACL
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("acl_entries");
+
+=head1 ACCESSORS
+
+=head2 ae_id
+
+ data_type: 'integer'
+ is_nullable: 0
+
+=head2 ae_scheme
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 32
+
+=head2 ae_identifier
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ae_id",
+ { data_type => "integer", is_nullable => 0 },
+ "ae_scheme",
+ { data_type => "varchar", is_nullable => 0, size => 32 },
+ "ae_identifier",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("ae_id", "ae_scheme", "ae_identifier");
+
+__PACKAGE__->belongs_to(
+ 'acls',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ae_id' },
+ { is_deferrable => 1, on_delete => 'CASCADE',
+ on_update => 'CASCADE' },
+ );
+
+__PACKAGE__->has_one(
+ 'acl_scheme',
+ 'Wallet::Schema::Result::AclScheme',
+ { 'foreign.as_name' => 'self.ae_scheme' },
+ { cascade_delete => 0 },
+ );
+1;
diff --git a/perl/lib/Wallet/Schema/Result/AclHistory.pm b/perl/lib/Wallet/Schema/Result/AclHistory.pm
new file mode 100644
index 0000000..82e18a9
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/AclHistory.pm
@@ -0,0 +1,123 @@
+# Wallet schema for ACL history.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::AclHistory;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+__PACKAGE__->load_components("InflateColumn::DateTime");
+
+=for stopwords
+ACL
+
+=head1 NAME
+
+Wallet::Schema::Result::AclHistory - Wallet schema for ACL history
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("acl_history");
+
+=head1 ACCESSORS
+
+=head2 ah_id
+
+ data_type: 'integer'
+ is_auto_increment: 1
+ is_nullable: 0
+
+=head2 ah_acl
+
+ data_type: 'integer'
+ is_nullable: 0
+
+=head2 ah_name
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 ah_action
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 16
+
+=head2 ah_scheme
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 32
+
+=head2 ah_identifier
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 ah_by
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ah_from
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ah_on
+
+ data_type: 'datetime'
+ datetime_undef_if_invalid: 1
+ is_nullable: 0
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ah_id",
+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
+ "ah_acl",
+ { data_type => "integer", is_nullable => 0 },
+ "ah_name",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "ah_action",
+ { data_type => "varchar", is_nullable => 0, size => 16 },
+ "ah_scheme",
+ { data_type => "varchar", is_nullable => 1, size => 32 },
+ "ah_identifier",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "ah_by",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ah_from",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ah_on",
+ {
+ data_type => "datetime",
+ datetime_undef_if_invalid => 1,
+ is_nullable => 0,
+ },
+);
+__PACKAGE__->set_primary_key("ah_id");
+
+# Add an index on the ACL.
+sub sqlt_deploy_hook {
+ my ($self, $sqlt_table) = @_;
+ my $name = 'acl_history_idx_ah_acl';
+ $sqlt_table->add_index (name => $name, fields => [qw(ah_acl)]);
+ $name = 'acl_history_idx_ah_name';
+ $sqlt_table->add_index (name => $name, fields => [qw(ah_name)]);
+}
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/AclScheme.pm b/perl/lib/Wallet/Schema/Result/AclScheme.pm
new file mode 100644
index 0000000..91a58b2
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/AclScheme.pm
@@ -0,0 +1,84 @@
+# Wallet schema for ACL scheme.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::AclScheme;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components (qw//);
+
+=for stopwords
+ACL verifier APIs
+
+=head1 NAME
+
+Wallet::Schema::Result::AclScheme - Wallet schema for ACL scheme
+
+=head1 DESCRIPTION
+
+This is a normalization table used to constrain the values in other
+tables. It contains the types of ACL schemes that Wallet will
+recognize, and the modules that govern each of those schemes.
+
+By default it contains the following entries:
+
+ insert into acl_schemes (as_name, as_class)
+ values ('krb5', 'Wallet::ACL::Krb5');
+ insert into acl_schemes (as_name, as_class)
+ values ('krb5-regex', 'Wallet::ACL::Krb5::Regex');
+ insert into acl_schemes (as_name, as_class)
+ values ('ldap-attr', 'Wallet::ACL::LDAP::Attribute');
+ insert into acl_schemes (as_name, as_class)
+ values ('netdb', 'Wallet::ACL::NetDB');
+ insert into acl_schemes (as_name, as_class)
+ values ('netdb-root', 'Wallet::ACL::NetDB::Root');
+
+If you have extended the wallet to support additional ACL schemes, you
+will want to add additional rows to this table mapping those schemes
+to Perl classes that implement the ACL verifier APIs.
+
+=cut
+
+__PACKAGE__->table("acl_schemes");
+
+=head1 ACCESSORS
+
+=head2 as_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 32
+
+=head2 as_class
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 64
+
+=cut
+
+__PACKAGE__->add_columns(
+ "as_name",
+ { data_type => "varchar", is_nullable => 0, size => 32 },
+ "as_class",
+ { data_type => "varchar", is_nullable => 1, size => 64 },
+);
+__PACKAGE__->set_primary_key("as_name");
+
+#__PACKAGE__->resultset->populate ([
+# [ qw/as_name as_class/ ],
+# [ 'krb5', 'Wallet::ACL::Krb5' ],
+# [ 'krb5-regex', 'Wallet::ACL::Krb5::Regex' ],
+# [ 'ldap-attr', 'Wallet::ACL::LDAP::Attribute' ],
+# [ 'netdb', 'Wallet::ACL::NetDB' ],
+# [ 'netdb-root', 'Wallet::ACL::NetDB::Root' ],
+# ]);
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/Duo.pm b/perl/lib/Wallet/Schema/Result/Duo.pm
new file mode 100644
index 0000000..80a71dc
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/Duo.pm
@@ -0,0 +1,53 @@
+# Wallet schema for Duo metadata.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::Duo;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+keytab enctype
+
+=head1 NAME
+
+Wallet::Schema::Result::Duo - Wallet schema for Duo metadata
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("duo");
+
+=head1 ACCESSORS
+
+=head2 du_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 du_key
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "du_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "du_key",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("du_name");
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/Enctype.pm b/perl/lib/Wallet/Schema/Result/Enctype.pm
new file mode 100644
index 0000000..5733669
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/Enctype.pm
@@ -0,0 +1,45 @@
+# Wallet schema for Kerberos encryption type.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::Enctype;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+Kerberos
+
+=head1 NAME
+
+Wallet::Schema::Result::Enctype - Wallet schema for Kerberos encryption type
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("enctypes");
+
+=head1 ACCESSORS
+
+=head2 en_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "en_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("en_name");
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/Flag.pm b/perl/lib/Wallet/Schema/Result/Flag.pm
new file mode 100644
index 0000000..e223ff8
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/Flag.pm
@@ -0,0 +1,62 @@
+# Wallet schema for object flags.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::Flag;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 NAME
+
+Wallet::Schema::Result::Flag - Wallet schema for object flags
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("flags");
+
+=head1 ACCESSORS
+
+=head2 fl_type
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 16
+
+=head2 fl_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 fl_flag
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 32
+
+=cut
+
+__PACKAGE__->add_columns(
+ "fl_type" =>
+ { data_type => "varchar", is_nullable => 0, size => 16 },
+ "fl_name" =>
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "fl_flag" => {
+ data_type => 'enum',
+ is_enum => 1,
+ extra => { list => [qw/locked unchanging/] },
+ },
+);
+__PACKAGE__->set_primary_key("fl_type", "fl_name", "fl_flag");
+
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/KeytabEnctype.pm b/perl/lib/Wallet/Schema/Result/KeytabEnctype.pm
new file mode 100644
index 0000000..daea724
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/KeytabEnctype.pm
@@ -0,0 +1,53 @@
+# Wallet schema for keytab enctype.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::KeytabEnctype;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+keytab enctype
+
+=head1 NAME
+
+Wallet::Schema::Result::KeytabEnctype - Wallet schema for keytab enctype
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("keytab_enctypes");
+
+=head1 ACCESSORS
+
+=head2 ke_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ke_enctype
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ke_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ke_enctype",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("ke_name", "ke_enctype");
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/KeytabSync.pm b/perl/lib/Wallet/Schema/Result/KeytabSync.pm
new file mode 100644
index 0000000..ca84277
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/KeytabSync.pm
@@ -0,0 +1,53 @@
+# Wallet schema for keytab synchronization.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::KeytabSync;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+keytab
+
+=head1 NAME
+
+Wallet::Schema::Result::KeytabSync - Wallet schema for keytab synchronization
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("keytab_sync");
+
+=head1 ACCESSORS
+
+=head2 ks_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ks_target
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ks_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ks_target",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("ks_name", "ks_target");
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/Object.pm b/perl/lib/Wallet/Schema/Result/Object.pm
new file mode 100644
index 0000000..fd64e1b
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/Object.pm
@@ -0,0 +1,266 @@
+# Wallet schema for an object.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::Object;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+__PACKAGE__->load_components("InflateColumn::DateTime");
+
+=head1 NAME
+
+Wallet::Schema::Result::Object - Wallet schema for an object
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("objects");
+
+=head1 ACCESSORS
+
+=head2 ob_type
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 16
+
+=head2 ob_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ob_owner
+
+ data_type: 'integer'
+ is_nullable: 1
+
+=head2 ob_acl_get
+
+ data_type: 'integer'
+ is_nullable: 1
+
+=head2 ob_acl_store
+
+ data_type: 'integer'
+ is_nullable: 1
+
+=head2 ob_acl_show
+
+ data_type: 'integer'
+ is_nullable: 1
+
+=head2 ob_acl_destroy
+
+ data_type: 'integer'
+ is_nullable: 1
+
+=head2 ob_acl_flags
+
+ data_type: 'integer'
+ is_nullable: 1
+
+=head2 ob_expires
+
+ data_type: 'datetime'
+ datetime_undef_if_invalid: 1
+ is_nullable: 1
+
+=head2 ob_created_by
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ob_created_from
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 ob_created_on
+
+ data_type: 'datetime'
+ datetime_undef_if_invalid: 1
+ is_nullable: 0
+
+=head2 ob_stored_by
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 ob_stored_from
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 ob_stored_on
+
+ data_type: 'datetime'
+ datetime_undef_if_invalid: 1
+ is_nullable: 1
+
+=head2 ob_downloaded_by
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 ob_downloaded_from
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 ob_downloaded_on
+
+ data_type: 'datetime'
+ datetime_undef_if_invalid: 1
+ is_nullable: 1
+
+=head2 ob_comment
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ob_type",
+ { data_type => "varchar", is_nullable => 0, size => 16 },
+ "ob_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ob_owner",
+ { data_type => "integer", is_nullable => 1 },
+ "ob_acl_get",
+ { data_type => "integer", is_nullable => 1 },
+ "ob_acl_store",
+ { data_type => "integer", is_nullable => 1 },
+ "ob_acl_show",
+ { data_type => "integer", is_nullable => 1 },
+ "ob_acl_destroy",
+ { data_type => "integer", is_nullable => 1 },
+ "ob_acl_flags",
+ { data_type => "integer", is_nullable => 1 },
+ "ob_expires",
+ {
+ data_type => "datetime",
+ datetime_undef_if_invalid => 1,
+ is_nullable => 1,
+ },
+ "ob_created_by",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ob_created_from",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "ob_created_on",
+ {
+ data_type => "datetime",
+ datetime_undef_if_invalid => 1,
+ is_nullable => 0,
+ },
+ "ob_stored_by",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "ob_stored_from",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "ob_stored_on",
+ {
+ data_type => "datetime",
+ datetime_undef_if_invalid => 1,
+ is_nullable => 1,
+ },
+ "ob_downloaded_by",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "ob_downloaded_from",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "ob_downloaded_on",
+ {
+ data_type => "datetime",
+ datetime_undef_if_invalid => 1,
+ is_nullable => 1,
+ },
+ "ob_comment",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+);
+__PACKAGE__->set_primary_key("ob_name", "ob_type");
+
+__PACKAGE__->has_one(
+ 'types',
+ 'Wallet::Schema::Result::Type',
+ { 'foreign.ty_name' => 'self.ob_type' },
+ );
+
+__PACKAGE__->has_many(
+ 'flags',
+ 'Wallet::Schema::Result::Flag',
+ { 'foreign.fl_type' => 'self.ob_type',
+ 'foreign.fl_name' => 'self.ob_name' },
+ { cascade_copy => 0, cascade_delete => 0 },
+ );
+
+__PACKAGE__->has_many(
+ 'object_history',
+ 'Wallet::Schema::Result::ObjectHistory',
+ { 'foreign.oh_type' => 'self.ob_type',
+ 'foreign.oh_name' => 'self.ob_name' },
+ { cascade_copy => 0, cascade_delete => 0 },
+ );
+
+__PACKAGE__->has_many(
+ 'keytab_enctypes',
+ 'Wallet::Schema::Result::KeytabEnctype',
+ { 'foreign.ke_name' => 'self.ob_name' },
+ { cascade_copy => 0, cascade_delete => 0 },
+ );
+
+__PACKAGE__->has_many(
+ 'keytab_sync',
+ 'Wallet::Schema::Result::KeytabSync',
+ { 'foreign.ks_name' => 'self.ob_name' },
+ { cascade_copy => 0, cascade_delete => 0 },
+ );
+
+# References for all of the various potential ACLs.
+__PACKAGE__->belongs_to(
+ 'acls_owner',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ob_owner' },
+ );
+__PACKAGE__->belongs_to(
+ 'acls_get',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ob_acl_get' },
+ );
+__PACKAGE__->belongs_to(
+ 'acls_store',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ob_acl_store' },
+ );
+__PACKAGE__->belongs_to(
+ 'acls_show',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ob_acl_show' },
+ );
+__PACKAGE__->belongs_to(
+ 'acls_destroy',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ob_acl_destroy' },
+ );
+__PACKAGE__->belongs_to(
+ 'acls_flags',
+ 'Wallet::Schema::Result::Acl',
+ { 'foreign.ac_id' => 'self.ob_acl_flags' },
+ );
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/ObjectHistory.pm b/perl/lib/Wallet/Schema/Result/ObjectHistory.pm
new file mode 100644
index 0000000..5e9c8bd
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/ObjectHistory.pm
@@ -0,0 +1,135 @@
+# Wallet schema for object history.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::ObjectHistory;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+__PACKAGE__->load_components("InflateColumn::DateTime");
+
+=head1 NAME
+
+Wallet::Schema::Result::ObjectHistory - Wallet schema for object history
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("object_history");
+
+=head1 ACCESSORS
+
+=head2 oh_id
+
+ data_type: 'integer'
+ is_auto_increment: 1
+ is_nullable: 0
+
+=head2 oh_type
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 16
+
+=head2 oh_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 oh_action
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 16
+
+=head2 oh_field
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 16
+
+=head2 oh_type_field
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 oh_old
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 oh_new
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 255
+
+=head2 oh_by
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 oh_from
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=head2 oh_on
+
+ data_type: 'datetime'
+ datetime_undef_if_invalid: 1
+ is_nullable: 0
+
+=cut
+
+__PACKAGE__->add_columns(
+ "oh_id",
+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
+ "oh_type",
+ { data_type => "varchar", is_nullable => 0, size => 16 },
+ "oh_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "oh_action",
+ { data_type => "varchar", is_nullable => 0, size => 16 },
+ "oh_field",
+ { data_type => "varchar", is_nullable => 1, size => 16 },
+ "oh_type_field",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "oh_old",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "oh_new",
+ { data_type => "varchar", is_nullable => 1, size => 255 },
+ "oh_by",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "oh_from",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+ "oh_on",
+ {
+ data_type => "datetime",
+ datetime_undef_if_invalid => 1,
+ is_nullable => 0,
+ },
+);
+__PACKAGE__->set_primary_key("oh_id");
+
+# Add an index on object type and object name.
+sub sqlt_deploy_hook {
+ my ($self, $sqlt_table) = @_;
+ my $name = 'object_history_idx_oh_type_oh_name';
+ $sqlt_table->add_index (name => $name, fields => [qw(oh_type oh_name)]);
+}
+
+1;
diff --git a/perl/lib/Wallet/Schema/Result/SyncTarget.pm b/perl/lib/Wallet/Schema/Result/SyncTarget.pm
new file mode 100644
index 0000000..4300a54
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/SyncTarget.pm
@@ -0,0 +1,48 @@
+# Wallet schema for synchronization targets.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::SyncTarget;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 NAME
+
+Wallet::Schema::Result::SyncTarget - Wallet schema for synchronization targets
+
+=head1 DESCRIPTION
+
+=cut
+
+__PACKAGE__->table("sync_targets");
+
+=head1 ACCESSORS
+
+=head2 st_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 255
+
+=cut
+
+__PACKAGE__->add_columns(
+ "st_name",
+ { data_type => "varchar", is_nullable => 0, size => 255 },
+);
+__PACKAGE__->set_primary_key("st_name");
+
+#__PACKAGE__->has_many(
+# 'keytab_sync',
+# 'Wallet::Schema::Result::KeytabSync',
+# { 'foreign.ks_target' => 'self.st_name' },
+# { cascade_copy => 0, cascade_delete => 0 },
+# );
+1;
diff --git a/perl/lib/Wallet/Schema/Result/Type.pm b/perl/lib/Wallet/Schema/Result/Type.pm
new file mode 100644
index 0000000..748a8a8
--- /dev/null
+++ b/perl/lib/Wallet/Schema/Result/Type.pm
@@ -0,0 +1,75 @@
+# Wallet schema for object types.
+#
+# Written by Jon Robertson <jonrober@stanford.edu>
+# Copyright 2012, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+package Wallet::Schema::Result::Type;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=for stopwords
+APIs
+
+=head1 NAME
+
+Wallet::Schema::Result::Type - Wallet schema for object types
+
+=head1 DESCRIPTION
+
+This is a normalization table used to constrain the values in other
+tables. It contains the types of wallet objects that are considered
+valid, and the modules that govern each.
+
+By default it contains the following entries:
+
+ insert into types (ty_name, ty_class)
+ values ('file', 'Wallet::Object::File');
+ insert into types (ty_name, ty_class)
+ values ('keytab', 'Wallet::Object::Keytab');
+
+If you have extended the wallet to support additional object types ,
+you will want to add additional rows to this table mapping those types
+to Perl classes that implement the object APIs.
+
+=cut
+
+__PACKAGE__->table("types");
+
+=head1 ACCESSORS
+
+=head2 ty_name
+
+ data_type: 'varchar'
+ is_nullable: 0
+ size: 16
+
+=head2 ty_class
+
+ data_type: 'varchar'
+ is_nullable: 1
+ size: 64
+
+=cut
+
+__PACKAGE__->add_columns(
+ "ty_name",
+ { data_type => "varchar", is_nullable => 0, size => 16 },
+ "ty_class",
+ { data_type => "varchar", is_nullable => 1, size => 64 },
+);
+__PACKAGE__->set_primary_key("ty_name");
+
+#__PACKAGE__->has_many(
+# 'objects',
+# 'Wallet::Schema::Result::Object',
+# { 'foreign.ob_type' => 'self.ty_name' },
+# { cascade_copy => 0, cascade_delete => 0 },
+# );
+
+1;
diff --git a/perl/lib/Wallet/Server.pm b/perl/lib/Wallet/Server.pm
new file mode 100644
index 0000000..95fd4e6
--- /dev/null
+++ b/perl/lib/Wallet/Server.pm
@@ -0,0 +1,1096 @@
+# Wallet::Server -- Wallet system server implementation.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2007, 2008, 2010, 2011, 2013, 2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package Wallet::Server;
+require 5.006;
+
+use strict;
+use warnings;
+use vars qw(%MAPPING $VERSION);
+
+use Wallet::ACL;
+use Wallet::Config;
+use Wallet::Schema;
+
+# 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.11';
+
+##############################################################################
+# Utility methods
+##############################################################################
+
+# Create a new wallet server object. A new server should be created for each
+# user who is making changes to the wallet. Takes the principal and host who
+# are sending wallet requests. Opens a connection to the database that will
+# be used for all of the wallet metadata based on the wallet configuration
+# information. We also instantiate the administrative ACL, which we'll use
+# for various things. Throw an exception if anything goes wrong.
+sub new {
+ my ($class, $user, $host) = @_;
+ my $schema = Wallet::Schema->connect;
+ my $acl = Wallet::ACL->new ('ADMIN', $schema);
+ my $self = {
+ schema => $schema,
+ user => $user,
+ host => $host,
+ admin => $acl,
+ };
+ bless ($self, $class);
+ return $self;
+}
+
+# Returns the database handle (used mostly for testing).
+sub dbh {
+ my ($self) = @_;
+ return $self->{schema}->storage->dbh;
+}
+
+# Returns the DBIx::Class-based database schema object.
+sub schema {
+ my ($self) = @_;
+ return $self->{schema};
+}
+
+# Set or return the error stashed in the object.
+sub error {
+ my ($self, @error) = @_;
+ if (@error) {
+ my $error = join ('', @error);
+ chomp $error;
+ 1 while ($error =~ s/ at \S+ line \d+\.?\z//);
+ $self->{error} = $error;
+ }
+ return $self->{error};
+}
+
+# Disconnect the database handle on object destruction to avoid warnings.
+sub DESTROY {
+ my ($self) = @_;
+
+ if ($self->{schema}) {
+ $self->{schema}->storage->dbh->disconnect;
+ }
+}
+
+##############################################################################
+# Object methods
+##############################################################################
+
+# Given an object type, return the mapping to a class by querying the
+# database, or undef if no mapping exists. Also load the relevant module.
+sub type_mapping {
+ my ($self, $type) = @_;
+ my $class;
+ eval {
+ my $guard = $self->{schema}->txn_scope_guard;
+ my %search = (ty_name => $type);
+ my $type_rec = $self->{schema}->resultset('Type')->find (\%search);
+ $class = $type_rec->ty_class;
+ $guard->commit;
+ };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ if (defined $class) {
+ eval "require $class";
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ }
+ return $class;
+}
+
+# Given an object which doesn't currently exist, check whether a default_owner
+# function is defined and, if so, if it returns an ACL for that object. If
+# so, create the ACL and check if the current user is authorized by that ACL.
+# Returns true if so, false if not, setting the internal error as appropriate.
+#
+# This leaves those new ACLs in the database, which may not be the best
+# behavior, but it's the simplest given the current Wallet::ACL API. This
+# should probably be revisited later.
+sub create_check {
+ my ($self, $type, $name) = @_;
+ my $user = $self->{user};
+ my $host = $self->{host};
+ my $schema = $self->{schema};
+ unless (defined (&Wallet::Config::default_owner)) {
+ $self->error ("$user not authorized to create ${type}:${name}");
+ return;
+ }
+ my ($aname, @acl) = Wallet::Config::default_owner ($type, $name);
+ unless (defined $aname) {
+ $self->error ("$user not authorized to create ${type}:${name}");
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($aname, $schema) };
+ if ($@) {
+ $acl = eval { Wallet::ACL->create ($aname, $schema, $user, $host) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ for my $entry (@acl) {
+ unless ($acl->add ($entry->[0], $entry->[1], $user, $host)) {
+ $self->error ($acl->error);
+ return;
+ }
+ }
+ } else {
+ my @entries = $acl->list;
+ if (not @entries and $acl->error) {
+ $self->error ($acl->error);
+ return;
+ }
+ @entries = sort { $$a[0] cmp $$b[0] && $$a[1] cmp $$b[1] } @entries;
+ @acl = sort { $$a[0] cmp $$b[0] && $$a[1] cmp $$b[1] } @acl;
+ my $okay = 1;
+ if (@entries != @acl) {
+ $okay = 0;
+ } else {
+ for my $i (0 .. $#entries) {
+ $okay = 0 unless ($entries[$i][0] eq $acl[$i][0]);
+ $okay = 0 unless ($entries[$i][1] eq $acl[$i][1]);
+ }
+ }
+ unless ($okay) {
+ $self->error ("ACL $aname exists and doesn't match default");
+ return;
+ }
+ }
+ if ($acl->check ($user)) {
+ return $aname;
+ } else {
+ $self->error ("$user not authorized to create ${type}:${name}");
+ return;
+ }
+}
+
+# Create an object and returns it. This function is called by both create and
+# autocreate and assumes that permissions and names have already been checked.
+# On error, returns undef and sets the internal error.
+sub create_object {
+ my ($self, $type, $name) = @_;
+ my $class = $self->type_mapping ($type);
+ unless ($class) {
+ $self->error ("unknown object type $type");
+ return;
+ }
+ my $schema = $self->{schema};
+ my $user = $self->{user};
+ my $host = $self->{host};
+ my $object = eval { $class->create ($type, $name, $schema, $user, $host) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ return $object;
+}
+
+# Create a new object and returns that object. This method can only be called
+# by wallet administrators. autocreate should be used by regular users who
+# may benefit from default ACLs. On error, returns undef and sets the
+# internal error.
+sub create {
+ my ($self, $type, $name) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ my $id = $type . ':' . $name;
+ $self->error ("$self->{user} not authorized to create $id");
+ return;
+ }
+ if (defined (&Wallet::Config::verify_name)) {
+ my $error = Wallet::Config::verify_name ($type, $name, $self->{user});
+ if ($error) {
+ $self->error ("${type}:${name} rejected: $error");
+ return;
+ }
+ }
+ return unless $self->create_object ($type, $name);
+ return 1;
+}
+
+# Attempt to auto-create an object based on default ACLs. This method is
+# called by the wallet client when trying to get an object that doesn't
+# already exist. On error, returns undef and sets the internal error.
+sub autocreate {
+ my ($self, $type, $name) = @_;
+ if (defined (&Wallet::Config::verify_name)) {
+ my $error = Wallet::Config::verify_name ($type, $name, $self->{user});
+ if ($error) {
+ $self->error ("${type}:${name} rejected: $error");
+ return;
+ }
+ }
+ my $acl = $self->create_check ($type, $name);
+ return unless $acl;
+ my $object = $self->create_object ($type, $name);
+ return unless $object;
+ unless ($object->owner ($acl, $self->{user}, $self->{host})) {
+ $self->error ($object->error);
+ return;
+ }
+ return 1;
+}
+
+# Given the name and type of an object, returns a Perl object representing it
+# or returns undef and sets the internal error.
+sub retrieve {
+ my ($self, $type, $name) = @_;
+ my $class = $self->type_mapping ($type);
+ unless ($class) {
+ $self->error ("unknown object type $type");
+ return;
+ }
+ my $object = eval { $class->new ($type, $name, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ } else {
+ return $object;
+ }
+}
+
+# Sets the internal error variable to the correct message for permission
+# denied on an object.
+sub object_error {
+ my ($self, $object, $action) = @_;
+ my $user = $self->{user};
+ my $id = $object->type . ':' . $object->name;
+ if ($action eq 'getattr') {
+ $action = "get attributes for";
+ } elsif ($action eq 'setattr') {
+ $action = "set attributes for";
+ } elsif ($action !~ /^(create|get|store|show|destroy)\z/) {
+ $action = "set $action for";
+ }
+ $self->error ("$self->{user} not authorized to $action $id");
+}
+
+# Given an object and an action, checks if the current user has access to
+# perform that object. If so, returns true. If not, returns undef and sets
+# the internal error message. Note that we do not allow any special access to
+# admins for get and store; if they want to do that with objects, they need to
+# set the ACL accordingly.
+sub acl_verify {
+ my ($self, $object, $action) = @_;
+ my %actions = map { $_ => 1 }
+ qw(get store show destroy flags setattr getattr comment);
+ unless ($actions{$action}) {
+ $self->error ("unknown action $action");
+ return;
+ }
+ if ($action ne 'get' and $action ne 'store') {
+ return 1 if $self->{admin}->check ($self->{user});
+ }
+ my $id;
+ if ($action eq 'getattr') {
+ $id = $object->acl ('show');
+ } elsif ($action eq 'setattr') {
+ $id = $object->acl ('store');
+ } elsif ($action ne 'comment') {
+ $id = $object->acl ($action);
+ }
+ if (! defined ($id) and $action ne 'flags') {
+ $id = $object->owner;
+ }
+ unless (defined $id) {
+ $self->object_error ($object, $action);
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ my $status = $acl->check ($self->{user});
+ if ($status == 1) {
+ return 1;
+ } elsif (not defined $status) {
+ $self->error ($acl->error);
+ return;
+ } else {
+ $self->object_error ($object, $action);
+ return;
+ }
+}
+
+# Retrieves or sets an ACL on an object.
+sub acl {
+ my ($self, $type, $name, $acl, $id) = @_;
+ undef $self->{error};
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->object_error ($object, 'ACL');
+ return;
+ }
+ my $result;
+ if (defined $id) {
+ $result = $object->acl ($acl, $id, $self->{user}, $self->{host});
+ } else {
+ $result = $object->acl ($acl);
+ }
+ if (not defined ($result) and $object->error) {
+ $self->error ($object->error);
+ }
+ return $result;
+}
+
+# Retrieves or sets an attribute on an object.
+sub attr {
+ my ($self, $type, $name, $attr, @values) = @_;
+ undef $self->{error};
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ my $user = $self->{user};
+ my $host = $self->{host};
+ if (@values) {
+ return unless $self->acl_verify ($object, 'setattr');
+ if (@values == 1 and $values[0] eq '') {
+ @values = ();
+ }
+ my $result = $object->attr ($attr, [ @values ], $user, $host);
+ $self->error ($object->error) unless $result;
+ return $result;
+ } else {
+ return unless $self->acl_verify ($object, 'getattr');
+ my @result = $object->attr ($attr);
+ if (not @result and $object->error) {
+ $self->error ($object->error);
+ return;
+ } else {
+ return @result;
+ }
+ }
+}
+
+# Retrieves or sets the comment of an object.
+sub comment {
+ my ($self, $type, $name, $comment) = @_;
+ undef $self->{error};
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ my $result;
+ if (defined $comment) {
+ return unless $self->acl_verify ($object, 'comment');
+ $result = $object->comment ($comment, $self->{user}, $self->{host});
+ } else {
+ return unless $self->acl_verify ($object, 'show');
+ $result = $object->comment;
+ }
+ if (not defined ($result) and $object->error) {
+ $self->error ($object->error);
+ }
+ return $result;
+}
+
+# Retrieves or sets the expiration of an object.
+sub expires {
+ my ($self, $type, $name, $expires) = @_;
+ undef $self->{error};
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->object_error ($object, 'expires');
+ return;
+ }
+ my $result;
+ if (defined $expires) {
+ $result = $object->expires ($expires, $self->{user}, $self->{host});
+ } else {
+ $result = $object->expires;
+ }
+ if (not defined ($result) and $object->error) {
+ $self->error ($object->error);
+ }
+ return $result;
+}
+
+# Retrieves or sets the owner of an object.
+sub owner {
+ my ($self, $type, $name, $owner) = @_;
+ undef $self->{error};
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->object_error ($object, 'owner');
+ return;
+ }
+ my $result;
+ if (defined $owner) {
+ $result = $object->owner ($owner, $self->{user}, $self->{host});
+ } else {
+ $result = $object->owner;
+ }
+ if (not defined ($result) and $object->error) {
+ $self->error ($object->error);
+ }
+ return $result;
+}
+
+# Checks for the existence of an object. Returns 1 if it does, 0 if it
+# doesn't, and undef if there was an error in checking the existence of the
+# object.
+sub check {
+ my ($self, $type, $name) = @_;
+ my $object = $self->retrieve ($type, $name);
+ if (not defined $object) {
+ if ($self->error =~ /^cannot find/) {
+ return 0;
+ } else {
+ return;
+ }
+ }
+ return 1;
+}
+
+# Retrieve the information associated with an object, or returns undef and
+# sets the internal error if the retrieval fails or if the user isn't
+# authorized. If the object doesn't exist, attempts dynamic creation of the
+# object using the default ACL mappings (if any).
+sub get {
+ my ($self, $type, $name) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'get');
+ my $result = $object->get ($self->{user}, $self->{host});
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+# Store new data in an object, or returns undef and sets the internal error if
+# the object can't be found or if the user isn't authorized. Also don't
+# permit storing undef, although storing the empty string is fine. If the
+# object doesn't exist, attempts dynamic creation of the object using the
+# default ACL mappings (if any).
+sub store {
+ my ($self, $type, $name, $data) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'store');
+ if (not defined ($data)) {
+ $self->{error} = "no data supplied to store";
+ return;
+ }
+ my $result = $object->store ($data, $self->{user}, $self->{host});
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+# Return a human-readable description of the object's metadata, or returns
+# undef and sets the internal error if the object can't be found or if the
+# user isn't authorized.
+sub show {
+ my ($self, $type, $name) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'show');
+ my $result = $object->show;
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+# Return a human-readable description of the object history, or returns undef
+# and sets the internal error if the object can't be found or if the user
+# isn't authorized.
+sub history {
+ my ($self, $type, $name) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'show');
+ my $result = $object->history;
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+# Destroys the object, or returns undef and sets the internal error if the
+# object can't be found or if the user isn't authorized.
+sub destroy {
+ my ($self, $type, $name) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'destroy');
+ my $result = $object->destroy ($self->{user}, $self->{host});
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+##############################################################################
+# Object flag methods
+##############################################################################
+
+# Clear a flag on an object. Takes the object and the flag. Returns true on
+# success or undef and sets the internal error on failure.
+sub flag_clear {
+ my ($self, $type, $name, $flag) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'flags');
+ my $result = $object->flag_clear ($flag, $self->{user}, $self->{host});
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+# Set a flag on an object. Takes the object and the flag. Returns true on
+# success or undef and sets the internal error on failure.
+sub flag_set {
+ my ($self, $type, $name, $flag) = @_;
+ my $object = $self->retrieve ($type, $name);
+ return unless defined $object;
+ return unless $self->acl_verify ($object, 'flags');
+ my $result = $object->flag_set ($flag, $self->{user}, $self->{host});
+ $self->error ($object->error) unless defined $result;
+ return $result;
+}
+
+##############################################################################
+# ACL methods
+##############################################################################
+
+# Checks for the existence of an ACL. Returns 1 if it does, 0 if it doesn't,
+# and undef if there was an error in checking the existence of the object.
+sub acl_check {
+ my ($self, $id) = @_;
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ if ($@ =~ /^ACL .* not found/) {
+ return 0;
+ } else {
+ $self->error ($@);
+ return;
+ }
+ }
+ return 1;
+}
+
+# Create a new empty ACL in the database. Returns true on success and undef
+# on failure, setting the internal error.
+sub acl_create {
+ my ($self, $name) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->error ("$self->{user} not authorized to create ACL");
+ return;
+ }
+ my $user = $self->{user};
+ my $host = $self->{host};
+ if (defined (&Wallet::Config::verify_acl_name)) {
+ my $error = Wallet::Config::verify_acl_name ($name, $user);
+ if ($error) {
+ $self->error ("$name rejected: $error");
+ return;
+ }
+ }
+ my $schema = $self->{schema};
+ my $acl = eval { Wallet::ACL->create ($name, $schema, $user, $host) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ } else {
+ return 1;
+ }
+}
+
+# Sets the internal error variable to the correct message for permission
+# denied on an ACL.
+sub acl_error {
+ my ($self, $acl, $action) = @_;
+ my $user = $self->{user};
+ if ($action eq 'add') {
+ $action = 'add to';
+ } elsif ($action eq 'remove') {
+ $action = 'remove from';
+ } elsif ($action eq 'history') {
+ $action = 'see history of';
+ }
+ $self->error ("$self->{user} not authorized to $action ACL $acl");
+}
+
+# Display the history of an ACL or return undef and set the internal error.
+sub acl_history {
+ my ($self, $id) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->acl_error ($id, 'history');
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ my $result = $acl->history;
+ if (not defined $result) {
+ $self->error ($acl->error);
+ return;
+ }
+ return $result;
+}
+
+# Display the membership of an ACL or return undef and set the internal error.
+sub acl_show {
+ my ($self, $id) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->acl_error ($id, 'show');
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ my $result = $acl->show;
+ if (not defined $result) {
+ $self->error ($acl->error);
+ return;
+ }
+ return $result;
+}
+
+# Change the human-readable name of an ACL or return undef and set the
+# internal error.
+sub acl_rename {
+ my ($self, $id, $name) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->acl_error ($id, 'rename');
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ if ($acl->name eq 'ADMIN') {
+ $self->error ('cannot rename the ADMIN ACL');
+ return;
+ }
+ if (defined (&Wallet::Config::verify_acl_name)) {
+ my $error = Wallet::Config::verify_acl_name ($name, $self->{user});
+ if ($error) {
+ $self->error ("$name rejected: $error");
+ return;
+ }
+ }
+ unless ($acl->rename ($name, $self->{user}, $self->{host})) {
+ $self->error ($acl->error);
+ return;
+ }
+ return 1;
+}
+
+# Destroy an ACL, deleting it out of the database. Returns true on success.
+# On failure, returns undef, setting the internal error.
+sub acl_destroy {
+ my ($self, $id) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->acl_error ($id, 'destroy');
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ if ($acl->name eq 'ADMIN') {
+ $self->error ('cannot destroy the ADMIN ACL');
+ return;
+ }
+ unless ($acl->destroy ($self->{user}, $self->{host})) {
+ $self->error ($acl->error);
+ return;
+ }
+ return 1;
+}
+
+# Add an ACL entry to an ACL. Returns true on success. On failure, returns
+# undef, setting the internal error.
+sub acl_add {
+ my ($self, $id, $scheme, $identifier) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->acl_error ($id, 'add');
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ unless ($acl->add ($scheme, $identifier, $self->{user}, $self->{host})) {
+ $self->error ($acl->error);
+ return;
+ }
+ return 1;
+}
+
+# Remove an ACL entry to an ACL. Returns true on success. On failure,
+# returns undef, setting the internal error.
+sub acl_remove {
+ my ($self, $id, $scheme, $identifier) = @_;
+ unless ($self->{admin}->check ($self->{user})) {
+ $self->acl_error ($id, 'remove');
+ return;
+ }
+ my $acl = eval { Wallet::ACL->new ($id, $self->{schema}) };
+ if ($@) {
+ $self->error ($@);
+ return;
+ }
+ if ($acl->name eq 'ADMIN') {
+ my @e = $acl->list;
+ if (not @e and $acl->error) {
+ $self->error ($acl->error);
+ return;
+ } elsif (@e == 1 && $e[0][0] eq $scheme && $e[0][1] eq $identifier) {
+ $self->error ('cannot remove last ADMIN ACL entry');
+ return;
+ }
+ }
+ my $user = $self->{user};
+ my $host = $self->{host};
+ unless ($acl->remove ($scheme, $identifier, $user, $host)) {
+ $self->error ($acl->error);
+ return;
+ }
+ return 1;
+}
+
+1;
+__END__
+
+##############################################################################
+# Documentation
+##############################################################################
+
+=head1 NAME
+
+Wallet::Server - Wallet system server implementation
+
+=for stopwords
+keytabs metadata backend HOSTNAME ACL timestamp ACL's nul Allbery
+backend-specific wallet-backend verifier
+
+=head1 SYNOPSIS
+
+ use Wallet::Server;
+ my $server = Wallet::Server->new ($user, $host);
+ $server->create ('keytab', 'host/example.com@EXAMPLE.COM');
+
+=head1 DESCRIPTION
+
+Wallet::Server is the top-level class that implements the wallet server.
+The wallet is a system for storing, generating, and retrieving secure
+information such as Kerberos keytabs. The server maintains metadata about
+the objects, checks access against ACLs, and dispatches requests for
+objects to backend implementations for that object type.
+
+Wallet::Server is normally instantiated and used by B<wallet-backend>, a
+thin wrapper around this object that determines the authenticated remote
+user and gets user input and then calls the appropriate method of this
+object.
+
+To use this object, several configuration variables must be set (at least
+the database configuration). For information on those variables and how
+to set them, see L<Wallet::Config>.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new(PRINCIPAL, HOSTNAME)
+
+Creates a new wallet server object for actions from the user PRINCIPAL
+connecting from HOSTNAME. PRINCIPAL and HOSTNAME will be used for logging
+history information for all subsequent operations. new() opens the
+database, using the database configuration as set by Wallet::Config and
+ensures that the C<ADMIN> ACL exists. That ACL will be used to authorize
+privileged operations.
+
+On any error, this method throws an exception.
+
+=back
+
+=head1 INSTANCE METHODS
+
+For all methods that can fail, the caller should call error() after a
+failure to get the error message.
+
+=over 4
+
+=item acl(TYPE, NAME, ACL [, ID])
+
+Gets or sets the ACL type ACL to ID for the object identified by TYPE and
+NAME. ACL should be one of C<get>, C<store>, C<show>, C<destroy>, or
+C<flags>. If ID is not given, returns the current setting of that ACL as
+the name of the ACL or undef if that ACL isn't set or on failure. To
+distinguish between an ACL that isn't set and a failure to retrieve the
+ACL, the caller should call error() after an undef return. If error()
+also returns undef, that ACL wasn't set; otherwise, error() will return
+the error message.
+
+If ID is given, sets the specified ACL to ID, which can be either the name
+of an ACL or a numeric ACL ID. To clear the ACL, pass in an empty string
+as the ID. To set or clear an ACL, the current user must be authorized by
+the ADMIN ACL. Returns true for success and false for failure.
+
+ACL settings are checked before the owner and override the owner setting.
+
+=item acl_add(ID, SCHEME, IDENTIFIER)
+
+Adds an ACL entry with scheme SCHEME and identifier IDENTIFIER to the ACL
+identified by ID. ID may be either the ACL name or the numeric ACL ID.
+SCHEME must be a valid ACL scheme for which the wallet system has an ACL
+verifier implementation. To add an entry to an ACL, the current user must
+be authorized by the ADMIN ACL. Returns true for success and false for
+failure.
+
+=item acl_create(NAME)
+
+Create a new ACL with the specified NAME, which must not be all-numeric.
+The newly created ACL will be empty. To create an ACL, the current user
+must be authorized by the ADMIN ACL. Returns true on success and false on
+failure.
+
+=item acl_destroy(ID)
+
+Destroys the ACL identified by ID, which may be either the ACL name or its
+numeric ID. This call will fail if the ACL is still referenced by any
+object. The ADMIN ACL may not be destroyed. To destroy an ACL, the
+current user must be authorized by the ADMIN ACL. Returns true on success
+and false on failure.
+
+=item acl_history(ID)
+
+Returns the history of the ACL identified by ID, which may be either the
+ACL name or its numeric ID. To see the history of an ACL, the current
+user must be authorized by the ADMIN ACL. Each change that modifies the
+ACL (not counting changes in the name of the ACL) will be represented by
+two lines. The first line will have a timestamp of the change followed by
+a description of the change, and the second line will give the user who
+made the change and the host from which the change was made. Returns
+undef on failure.
+
+=item acl_remove(ID, SCHEME, IDENTIFIER)
+
+Removes from the ACL identified by ID the entry matching SCHEME and
+IDENTIFIER. ID may be either the name of the ACL or its numeric ID. The
+last entry in the ADMIN ACL cannot be removed. To remove an entry from an
+ACL, the current user must be authorized by the ADMIN ACL. Returns true
+on success and false on failure.
+
+=item acl_rename(OLD, NEW)
+
+Renames the ACL identified by OLD to NEW. This changes the human-readable
+name, not the underlying numeric ID, so the ACL's associations with
+objects will be unchanged. The ADMIN ACL may not be renamed. OLD may be
+either the current name or the numeric ID. NEW must not be all-numeric.
+To rename an ACL, the current user must be authorized by the ADMIN ACL.
+Returns true on success and false on failure.
+
+=item acl_show(ID)
+
+Returns a human-readable description, including membership, of the ACL
+identified by ID, which may be either the ACL name or its numeric ID. To
+show an ACL, the current user must be authorized by the ADMIN ACL
+(although be aware that anyone with show access to an object can see the
+membership of ACLs associated with that object through the show() method).
+Returns the human-readable description on success and undef on failure.
+
+=item attr(TYPE, NAME, ATTRIBUTE [, VALUE ...])
+
+Sets or retrieves a given object attribute. Attributes are used to store
+backend-specific information for a particular object type and ATTRIBUTE
+must be an attribute type known to the underlying object implementation.
+
+If VALUE is not given, returns the values of that attribute, if any, as a
+list. On error, returns the empty list. To distinguish between an error
+and an empty return, call error() afterward. It is guaranteed to return
+undef unless there was an error. To retrieve an attribute setting, the
+user must be authorized by the ADMIN ACL, the show ACL if set, or the
+owner ACL if the show ACL is not set.
+
+If VALUE is given, sets the given ATTRIBUTE values to VALUE, which is one
+or more attribute values. Pass the empty string as the only VALUE to
+clear the attribute values. Returns true on success and false on failure.
+To set an attribute value, the user must be authorized by the ADMIN ACL,
+the store ACL if set, or the owner ACL if the store ACL is not set.
+
+=item autocreate(TYPE, NAME)
+
+Creates a new object of type TYPE and name NAME. TYPE must be a
+recognized type for which the wallet system has a backend implementation.
+Returns true on success and false on failure.
+
+To create an object using this method, the current user must be authorized
+by the default owner as determined by the wallet configuration. For more
+information on how to map new objects to default owners, see
+Wallet::Config(3). Wallet administrators should use the create() method
+to create objects.
+
+=item check(TYPE, NAME)
+
+Check whether an object of type TYPE and name NAME exists. Returns 1 if
+it does, 0 if it doesn't, and undef if some error occurred while checking
+for the existence of the object.
+
+=item comment(TYPE, NAME, [COMMENT])
+
+Gets or sets the comment for the object identified by TYPE and NAME. If
+COMMENT is not given, returns the current comment or undef if no comment
+is set or on an error. To distinguish between an expiration that isn't
+set and a failure to retrieve the expiration, the caller should call
+error() after an undef return. If error() also returns undef, no comment
+was set; otherwise, error() will return the error message.
+
+If COMMENT is given, sets the comment to COMMENT. Pass in the empty
+string for COMMENT to clear the comment. To set a comment, the current
+user must be the object owner or be on the ADMIN ACL. Returns true for
+success and false for failure.
+
+=item create(TYPE, NAME)
+
+Creates a new object of type TYPE and name NAME. TYPE must be a
+recognized type for which the wallet system has a backend implementation.
+Returns true on success and false on failure.
+
+To create an object using this method, the current user must be authorized
+by the ADMIN ACL. Use autocreate() to create objects based on the default
+owner as determined by the wallet configuration.
+
+=item destroy(TYPE, NAME)
+
+Destroys the object identified by TYPE and NAME. This destroys any data
+that the wallet had saved about the object, may remove the underlying
+object from other external systems, and destroys the wallet database entry
+for the object. To destroy an object, the current user must be a member
+of the ADMIN ACL, authorized by the destroy ACL, or authorized by the
+owner ACL; however, if the destroy ACL is set, the owner ACL will not be
+checked. Returns true on success and false on failure.
+
+=item dbh()
+
+Returns the database handle of a Wallet::Server object. This is used
+mostly for testing; normally, clients should perform all actions through
+the Wallet::Server object to ensure that authorization and history logging
+is done properly.
+
+=item error()
+
+Returns the error of the last failing operation or undef if no operations
+have failed. Callers should call this function to get the error message
+after an undef return from any other instance method.
+
+=item expires(TYPE, NAME [, EXPIRES])
+
+Gets or sets the expiration for the object identified by TYPE and NAME.
+If EXPIRES is not given, returns the current expiration or undef if no
+expiration is set or on an error. To distinguish between an expiration
+that isn't set and a failure to retrieve the expiration, the caller should
+call error() after an undef return. If error() also returns undef, the
+expiration wasn't set; otherwise, error() will return the error message.
+
+If EXPIRES is given, sets the expiration to EXPIRES. EXPIRES must be in
+the format C<YYYY-MM-DD +HH:MM:SS>, although the time portion may be
+omitted. Pass in the empty string for EXPIRES to clear the expiration
+date. To set an expiration, the current user must be authorized by the
+ADMIN ACL. Returns true for success and false for failure.
+
+=item flag_clear(TYPE, NAME, FLAG)
+
+Clears the flag FLAG on the object identified by TYPE and NAME. To clear
+a flag, the current user must be authorized by the ADMIN ACL or the flags
+ACL on the object.
+
+=item flag_set(TYPE, NAME, FLAG)
+
+Sets the flag FLAG on the object identified by TYPE and NAME. To set a
+flag, the current user must be authorized by the ADMIN ACL or the flags
+ACL on the object.
+
+=item get(TYPE, NAME)
+
+Returns the data associated with the object identified by TYPE and NAME.
+Depending on the object TYPE, this may generate new data and invalidate
+any existing data or it may return data previously stored or generated.
+Note that this data may be binary and may contain nul characters. To get
+an object, the current user must either be authorized by the owner ACL or
+authorized by the get ACL; however, if the get ACL is set, the owner ACL
+will not be checked. Being a member of the ADMIN ACL does not provide any
+special privileges to get objects.
+
+Returns undef on failure. The caller should be careful to distinguish
+between undef and the empty string, which is valid object data.
+
+=item history(TYPE, NAME)
+
+Returns (as a string) the human-readable history of the object identified
+by TYPE and NAME, or undef on error. To see the object history, the
+current user must be a member of the ADMIN ACL, authorized by the show
+ACL, or authorized by the owner ACL; however, if the show ACL is set, the
+owner ACL will not be checked.
+
+=item owner(TYPE, NAME [, OWNER])
+
+Gets or sets the owner for the object identified by TYPE and NAME. If
+OWNER is not given, returns the current owner as the name of the ACL or
+undef if no owner is set or on an error. To distinguish between an owner
+that isn't set and a failure to retrieve the owner, the caller should call
+error() after an undef return. If error() also returns undef, that ACL
+wasn't set; otherwise, error() will return the error message.
+
+If OWNER is given, sets the owner to OWNER, which may be either the name
+of an ACL or a numeric ACL ID. To set an owner, the current user must be
+authorized by the ADMIN ACL. Returns true for success and false for
+failure.
+
+The owner of an object is permitted to get, store, and show that object,
+but cannot destroy or set flags on that object without being listed on
+those ACLs as well.
+
+=item schema()
+
+Returns the DBIx::Class schema object.
+
+=item show(TYPE, NAME)
+
+Returns (as a string) a human-readable representation of the metadata
+stored for the object identified by TYPE and NAME, or undef on error.
+Included is the metadata and entries of any ACLs associated with the
+object. To show an object, the current user must be a member of the ADMIN
+ACL, authorized by the show ACL, or authorized by the owner ACL; however,
+if the show ACL is set, the owner ACL will not be checked.
+
+=item store(TYPE, NAME, DATA)
+
+Stores DATA for the object identified with TYPE and NAME for later
+retrieval with get. Not all object types support this. Note that DATA
+may be binary and may contain nul characters. To store an object, the
+current user must either be authorized by the owner ACL or authorized by
+the store ACL; however, if the store ACL is set, the owner ACL is not
+checked. Being a member of the ADMIN ACL does not provide any special
+privileges to store objects. Returns true on success and false on
+failure.
+
+=back
+
+=head1 SEE ALSO
+
+wallet-backend(8)
+
+This module is part of the wallet system. The current version is
+available from L<http://www.eyrie.org/~eagle/software/wallet/>.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=cut