summaryrefslogtreecommitdiff
path: root/perl/lib/Wallet/ACL.pm
diff options
context:
space:
mode:
Diffstat (limited to 'perl/lib/Wallet/ACL.pm')
-rw-r--r--perl/lib/Wallet/ACL.pm667
1 files changed, 667 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