From e566f44d15a075e6fb4e06bdd14d94f10b4ec124 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Thu, 22 Jan 2015 13:54:09 -0800 Subject: Updated Stanford policy to add optional extra to ssh keys Change-Id: Ic575c22c741c29e814749d334e9ed40eb83014e5 --- perl/lib/Wallet/Policy/Stanford.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm index a392476..a707ea5 100644 --- a/perl/lib/Wallet/Policy/Stanford.pm +++ b/perl/lib/Wallet/Policy/Stanford.pm @@ -66,8 +66,8 @@ our %FILE_TYPE = ( 'password-root' => { host => 1 }, 'password-tivoli' => { host => 1 }, properties => { extra => 1 }, - 'ssh-dsa' => { host => 1 }, - 'ssh-rsa' => { host => 1 }, + 'ssh-dsa' => { host => 1, extra => 1 }, + 'ssh-rsa' => { host => 1, extra => 1 }, 'ssl-key' => { host => 1, extra => 1 }, 'ssl-keypair' => { host => 1, extra => 1 }, 'ssl-keystore' => { extra => 1 }, -- cgit v1.2.3 From aebae838e3aa327e94d796bd99b48c169ffe6683 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Fri, 6 Feb 2015 13:04:35 -0800 Subject: wallet-report: Added a report for unstored objects Report on all file objects that have never had data stored in them. Also clean up the text around the 'objects unused' report which said that it did this plus things that were never gotten, but in reality only reported on the objects that were never gotten. Change-Id: I30c9585ac6f3744fbea2f94b3d6874a64c0109ad --- perl/lib/Wallet/Report.pm | 37 ++++++++++++++++++++++++++++--------- server/wallet-report | 5 ++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/perl/lib/Wallet/Report.pm b/perl/lib/Wallet/Report.pm index bf48308..2382d87 100644 --- a/perl/lib/Wallet/Report.pm +++ b/perl/lib/Wallet/Report.pm @@ -175,6 +175,20 @@ sub objects_unused { return (\%search, \%options); } +# Return the SQL statement to find all fiel objects that have been created +# but have never had information stored (via store). +sub objects_unstored { + my ($self) = @_; + my @objects; + + my %search = (ob_stored_on => undef, + ob_type => 'file'); + 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 @@ -190,7 +204,7 @@ sub objects { if (!defined $type || $type eq '') { ($search_ref, $options_ref) = $self->objects_all; } else { - if ($type ne 'unused' && @args != 1) { + if ($type ne 'unused' && $type ne 'unstored' && @args != 1) { $self->error ("object searches require one argument to search"); } elsif ($type eq 'type') { ($search_ref, $options_ref) = $self->objects_type (@args); @@ -202,6 +216,8 @@ sub objects { ($search_ref, $options_ref) = $self->objects_acl (@args); } elsif ($type eq 'unused') { ($search_ref, $options_ref) = $self->objects_unused (@args); + } elsif ($type eq 'unstored') { + ($search_ref, $options_ref) = $self->objects_unstored (@args); } else { $self->error ("do not know search type: $type"); } @@ -633,14 +649,17 @@ 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, with a given type, -will return only those entries where the type matches the given type. -C, with a given owner, will only return those objects owned by the -given ACL name or ID. C, with a given flag name, will only return -those items with a flag set to the given value. C operates like -C, 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 will -return all entries for which a get command has never been issued. +There are several types of searches. C, with a given type, will +return only those entries where the type matches the given type. +C, with a given owner, will only return those objects owned by +the given ACL name or ID. C, with a given flag name, will only +return those items with a flag set to the given value. C operates +like C, 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 will return all entries for which a get command has never +been issued. C will return all entries for which a store +command has never been issued (limited to file type since storing isn't +needed for other types). 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 diff --git a/server/wallet-report b/server/wallet-report index b5a2247..1c8f914 100755 --- a/server/wallet-report +++ b/server/wallet-report @@ -25,7 +25,8 @@ Wallet reporting help: objects flag Objects with that flag set objects owner Objects owned by that owner objects type Objects of that type - objects unused Objects that have never been stored/gotten + objects unused Objects that have never been gotten + objects unstored Objects that have never been stored owners All ACL entries owning matching objects EOH @@ -220,6 +221,8 @@ Displays a summary of all available commands. =item objects unused +=item objects unstored + Returns a list of objects in the database. Objects will be listed in the form: -- cgit v1.2.3 From 0e16def8a9e12f9b2232b29da79cdacb6710b086 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Fri, 6 Feb 2015 23:43:50 -0800 Subject: Added acl replace command to wallet backend New command for replacing the ownership of anything owned by a specific ACL with another ACL. This differs from acl rename in that it's to be used when the destination ACL already exists and potentially already owns some objects. Change-Id: I765bebf499fe0f861abc2ffe1873990590beed36 --- client/wallet.pod | 10 ++++++++ perl/lib/Wallet/ACL.pm | 35 ++++++++++++++++++++++++++ perl/lib/Wallet/Server.pm | 38 ++++++++++++++++++++++++++++ perl/t/general/acl.t | 64 ++++++++++++++++++++++++++++++++++++++++++++--- server/wallet-backend | 22 ++++++++++++++++ 5 files changed, 166 insertions(+), 3 deletions(-) diff --git a/client/wallet.pod b/client/wallet.pod index 4b58bbf..20d1874 100644 --- a/client/wallet.pod +++ b/client/wallet.pod @@ -227,6 +227,16 @@ renamed. may be either the current name or the numeric ID. must not be all-numeric. To rename an ACL, the current user must be authorized by the C ACL. +=item acl replace + +Find any objects owned by , and then change their ownership to + instead. should already exist, and may already have +some objects owned by it. is not deleted afterwards, though in +most cases that is probably your next step. The C ACL may not be +replaced from. and may be either the current name or the +numeric ID. To replace an ACL, the current user must be authorized by +the C ACL. + =item acl show Display the name, numeric ID, and entries of the ACL . diff --git a/perl/lib/Wallet/ACL.pm b/perl/lib/Wallet/ACL.pm index a3b0146..370df8b 100644 --- a/perl/lib/Wallet/ACL.pm +++ b/perl/lib/Wallet/ACL.pm @@ -17,6 +17,7 @@ use strict; use warnings; use vars qw($VERSION); +use Wallet::Object::Base; use DateTime; use DBI; @@ -207,6 +208,32 @@ sub rename { return 1; } +# Moves everything owned by one ACL to instead be owned by another. You'll +# normally want to use rename, but this exists for cases where the replacing +# ACL already exists and has things assigned to it. Returns true on success, +# false on failure. +sub replace { + my ($self, $replace_id, $user, $host, $time) = @_; + $time ||= time; + + my %search = (ob_owner => $self->{id}); + my @objects = $self->{schema}->resultset('Object')->search (\%search); + if (@objects) { + for my $object (@objects) { + my $type = $object->ob_type; + my $name = $object->ob_name; + my $object = eval { + Wallet::Object::Base->new($type, $name, $self->{schema}); + }; + $object->owner ($replace_id, $user, $host, $time); + } + } else { + $self->error ("no objects found for ACL $self->{id}"); + return; + } + return 1; +} + # Destroy the ACL, deleting it out of the database. Returns true on success, # false on failure. # @@ -643,6 +670,14 @@ On failure, the caller should call error() to get the error message. Note that rename() operations are not logged in the ACL history. +=item replace(ID) + +Replace this ACL with another. This goes through each object owned by +the ACL and changes its ownership to the new ACL, leaving this acl owning +nothing (and probably then needing to be deleted). Returns true on +success and false on failure. On failure, the caller should call error() +to get the error message. + =item show() Returns a human-readable description of this ACL, including its diff --git a/perl/lib/Wallet/Server.pm b/perl/lib/Wallet/Server.pm index f6ea342..6af0570 100644 --- a/perl/lib/Wallet/Server.pm +++ b/perl/lib/Wallet/Server.pm @@ -734,6 +734,36 @@ sub acl_rename { return 1; } +# Move all ACLs owned by one ACL to another, or return undef and set the +# internal error. +sub acl_replace { + my ($self, $old_id, $replace_id) = @_; + unless ($self->{admin}->check ($self->{user})) { + $self->acl_error ($old_id, 'replace'); + return; + } + my $acl = eval { Wallet::ACL->new ($old_id, $self->{schema}) }; + if ($@) { + $self->error ($@); + return; + } + if ($acl->name eq 'ADMIN') { + $self->error ('cannot replace the ADMIN ACL'); + return; + } + my $replace_acl = eval { Wallet::ACL->new ($replace_id, $self->{schema}) }; + if ($@) { + $self->error ($@); + return; + } + + unless ($acl->replace ($replace_id, $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 { @@ -942,6 +972,14 @@ 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_replace(OLD, NEW) + +Moves any object owned by the ACL identified by OLD to be instead owned by +NEW. This goes through all objects owned by OLD and individually changes +the owner, along with history updates. OLD and NEW may be either the name +or the numeric ID. To replace 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 diff --git a/perl/t/general/acl.t b/perl/t/general/acl.t index 1dd5c53..ff8ddad 100755 --- a/perl/t/general/acl.t +++ b/perl/t/general/acl.t @@ -12,11 +12,11 @@ use strict; use warnings; use POSIX qw(strftime); -use Test::More tests => 101; +use Test::More tests => 109; use Wallet::ACL; use Wallet::Admin; -use Wallet::Server; +use Wallet::Object::Base; use lib 't/lib'; use Util; @@ -46,7 +46,7 @@ $acl = eval { Wallet::ACL->create (3, $schema, @trace) }; ok (!defined ($acl), 'Creating with a numeric name'); is ($@, "ACL name may not be all numbers\n", ' with the right error message'); $acl = eval { Wallet::ACL->create ('test', $schema, @trace) }; -ok (!defined ($acl), 'Creating a duplicate object'); +ok (!defined ($acl), 'Creating a duplicate acl'); like ($@, qr/^cannot create ACL test: /, ' with the right error message'); $acl = eval { Wallet::ACL->new ('test2', $schema) }; ok (!defined ($acl), 'Searching for a non-existent ACL'); @@ -231,6 +231,64 @@ is ($@, '', ' with no exceptions'); is ($acl->name, 'example', ' and the right name'); like ($acl->id, qr{\A[23]\z}, ' and an ID of 2 or 3'); +# Test replace. by creating three acls, then assigning two objects to the +# first, one to the second, and another to the third. Then replace the first +# acl with the second, so that we can verify that multiple objects are moved, +# that an object already belonging to the new acl is okay, and that the +# objects with unrelated ACL are unaffected. +my ($acl_old, $acl_new, $acl_other, $obj_old_one, $obj_old_two, $obj_new, + $obj_unrelated); +eval { + $acl_old = Wallet::ACL->create ('example-old', $schema, @trace); + $acl_new = Wallet::ACL->create ('example-new', $schema, @trace); + $acl_other = Wallet::ACL->create ('example-other', $schema, @trace); +}; +is ($@, '', 'ACLs needed for testing replace are created'); +eval { + $obj_old_one = Wallet::Object::Base->create ('keytab', + 'service/test1@EXAMPLE.COM', + $schema, @trace); + $obj_old_two = Wallet::Object::Base->create ('keytab', + 'service/test2@EXAMPLE.COM', + $schema, @trace); + $obj_new = Wallet::Object::Base->create ('keytab', + 'service/test3@EXAMPLE.COM', + $schema, @trace); + $obj_unrelated = Wallet::Object::Base->create ('keytab', + 'service/test4@EXAMPLE.COM', + $schema, @trace); +}; +is ($@, '', ' and so were needed objects'); +if ($obj_old_one->owner ('example-old', @trace) + && $obj_old_two->owner ('example-old', @trace) + && $obj_new->owner ('example-new', @trace) + && $obj_unrelated->owner ('example-other', @trace)) { + + ok (1, ' and setting initial ownership on the objects succeeds'); +} +is ($acl_old->replace('example-new', @trace), 1, + ' and replace ran successfully'); +eval { + $obj_old_one = Wallet::Object::Base->new ('keytab', + 'service/test1@EXAMPLE.COM', + $schema); + $obj_old_two = Wallet::Object::Base->new ('keytab', + 'service/test2@EXAMPLE.COM', + $schema); + $obj_new = Wallet::Object::Base->new ('keytab', + 'service/test3@EXAMPLE.COM', + $schema); + $obj_unrelated = Wallet::Object::Base->new ('keytab', + 'service/test4@EXAMPLE.COM', + $schema); +}; +is ($obj_old_one->owner, 'example-new', ' and first replace is correct'); +is ($obj_old_two->owner, 'example-new', ' and second replace is correct'); +is ($obj_new->owner, 'example-new', + ' and object already with new acl is correct'); +is ($obj_unrelated->owner, 'example-other', + ' and unrelated object ownership is correct'); + # Clean up. $setup->destroy; END { diff --git a/server/wallet-backend b/server/wallet-backend index 8dfc952..dcf2300 100755 --- a/server/wallet-backend +++ b/server/wallet-backend @@ -173,6 +173,9 @@ sub command { } elsif ($action eq 'rename') { check_args (2, 2, [], @args); $server->acl_rename (@args) or failure ($server->error, @_); + } elsif ($action eq 'replace') { + check_args (2, 2, [], @args); + $server->acl_replace (@args) or failure ($server->error, @_); } elsif ($action eq 'show') { check_args (1, 1, [], @args); my $output = $server->acl_show (@args); @@ -449,6 +452,25 @@ accidental lockout, but administrators can remove themselves from the C ACL and can leave only a non-functioning entry on the ACL. Use caution when removing entries from the C ACL. +=item acl rename + +Renames the ACL identified by to . This changes the +human-readable name, not the underlying numeric ID, so the ACL's +associations with objects will be unchanged. The C ACL may not be +renamed. may be either the current name or the numeric ID. +must not be all-numeric. To rename an ACL, the current user must be +authorized by the C ACL. + +=item acl replace + +Find any objects owned by , and then change their ownership to + instead. should already exist, and may already have +some objects owned by it. is not deleted afterwards, though in +most cases that is probably your next step. The C ACL may not be +replaced from. and may be either the current name or the +numeric ID. To replace an ACL, the current user must be authorized by +the C ACL. + =item acl show Display the name, numeric ID, and entries of the ACL . -- cgit v1.2.3 From 55875aa020f31751f295ae6c07547fe2949c5e82 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Sat, 7 Feb 2015 13:59:34 -0800 Subject: Added a new password object type The password type inherits almost everything from the file object, but if you try to get a password object that has never been stored, we generate a random string to put in the object rather than just erroring out. The maximum and minimum length of the string can be set in the wallet config. If a password object was stored earlier and then cleared out, we don't generate another random string. Change-Id: I17a65ca7dac9d4430e8a731f417297890ee612bb --- perl/lib/Wallet/Admin.pm | 1 + perl/lib/Wallet/Config.pm | 43 ++++++++ perl/lib/Wallet/Object/Password.pm | 210 +++++++++++++++++++++++++++++++++++++ perl/t/object/password.t | 118 +++++++++++++++++++++ 4 files changed, 372 insertions(+) create mode 100644 perl/lib/Wallet/Object/Password.pm create mode 100644 perl/t/object/password.t diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm index 8120e9c..a8b8368 100644 --- a/perl/lib/Wallet/Admin.pm +++ b/perl/lib/Wallet/Admin.pm @@ -131,6 +131,7 @@ sub default_data { [ 'duo-radius', 'Wallet::Object::Duo::RadiusProxy' ], [ 'duo-rdp', 'Wallet::Object::Duo::RDP' ], [ 'file', 'Wallet::Object::File' ], + [ 'password', 'Wallet::Object::Password' ], [ 'keytab', 'Wallet::Object::Keytab' ], [ 'wa-keyring', 'Wallet::Object::WAKeyring' ]); ($r1) = $self->{schema}->resultset('Type')->populate (\@record); diff --git a/perl/lib/Wallet/Config.pm b/perl/lib/Wallet/Config.pm index 2eb57f9..76c7ecd 100644 --- a/perl/lib/Wallet/Config.pm +++ b/perl/lib/Wallet/Config.pm @@ -260,6 +260,49 @@ our $FILE_MAX_SIZE; =back +=head1 PASSWORD OBJECT CONFIGURATION + +These configuration variables only need to be set if you intend to use the +C object type (the Wallet::Object::Password class). You will also +need to set the FILE_MAX_SIZE value from the file object configuration, as +that is inherited. + +=over 4 + +=item PWD_FILE_BUCKET + +The directory into which to store password objects. Password objects will +be stored in subdirectories of this directory. See +L 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. + +PWD_FILE_BUCKET must be set to use file objects. + +=cut + +our $PWD_FILE_BUCKET; + +=item PWD_LENGTH_MIN + +The minimum length for any auto-generated password objects created when get +is run before data is stored. + +=cut + +our $PWD_LENGTH_MIN = 20; + +=item PWD_LENGTH_MAX + +The maximum length for any auto-generated password objects created when get +is run before data is stored. + +=cut + +our $PWD_LENGTH_MAX = 21; + +=back + =head1 KEYTAB OBJECT CONFIGURATION These configuration variables only need to be set if you intend to use the diff --git a/perl/lib/Wallet/Object/Password.pm b/perl/lib/Wallet/Object/Password.pm new file mode 100644 index 0000000..d06c8a6 --- /dev/null +++ b/perl/lib/Wallet/Object/Password.pm @@ -0,0 +1,210 @@ +# Wallet::Object::Password -- Password object implementation for the wallet. +# +# Written by Jon Robertson +# Copyright 2015 +# The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +############################################################################## +# Modules and declarations +############################################################################## + +package Wallet::Object::Password; +require 5.006; + +use strict; +use warnings; +use vars qw(@ISA $VERSION); + +use Crypt::GeneratePassword qw(chars); +use Digest::MD5 qw(md5_hex); +use Wallet::Config (); +use Wallet::Object::File; + +@ISA = qw(Wallet::Object::File); + +# 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 password 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::PWD_FILE_BUCKET) { + $self->error ('password support not configured'); + return; + } + unless ($name) { + $self->error ('password 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::PWD_FILE_BUCKET/$hash"; + unless (-d $parent || mkdir ($parent, 0700)) { + $self->error ("cannot create password bucket $hash: $!"); + return; + } + return "$Wallet::Config::PWD_FILE_BUCKET/$hash/$name"; +} + +############################################################################## +# Core methods +############################################################################## + +# 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; + + # If nothing is yet stored, generate a random password and save it to + # the file. + my $schema = $self->{schema}; + my %search = (ob_type => $self->{type}, + ob_name => $self->{name}); + my $object = $schema->resultset('Object')->find (\%search); + unless ($object->ob_stored_on) { + unless (open (FILE, '>', $path)) { + $self->error ("cannot store initial settings for $id: $!\n"); + return; + } + my $pass = chars ($Wallet::Config::PWD_LENGTH_MIN, + $Wallet::Config::PWD_LENGTH_MAX); + print FILE $pass; + $self->log_action ('store', $user, $host, $time); + unless (close FILE) { + $self->error ("cannot get $id: $!"); + return; + } + } + + unless (open (FILE, '<', $path)) { + $self->error ("cannot get $id: object has not been stored"); + return; + } + local $/; + my $data = ; + unless (close FILE) { + $self->error ("cannot get $id: $!"); + return; + } + $self->log_action ('get', $user, $host, $time); + return $data; +} + +1; +__END__ + +############################################################################## +# Documentation +############################################################################## + +=head1 NAME + +Wallet::Object::Password - Password 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::Password->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::Password is an extension of Wallet::Object::File, +acting as a representation of simple file objects in the wallet. The +difference between the two is that if there is no data stored in a +password object when a user tries to get it for the first time, then a +random string suited for a password will be generated and put into the +object data. + +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 password object is deleted. + +To use this object, the configuration option specifying where on the +wallet server to store password objects must be set. See +L for details on this configuration parameter and +information about how to set wallet configuration. + +=head1 METHODS + +This object mostly inherits from Wallet::Object::File. 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 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. + +=back + +=head1 FILES + +=over 4 + +=item PWD_FILE_BUCKET// + +Password files are stored on the wallet server under the directory +PWD_FILE_BUCKET as set in the wallet configuration. is the +first two characters of the hex-encoded MD5 hash of the wallet password +object name, used to not put too many files in the same directory. + is the name of the password 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 password object +names. However, due to limitations in the B server usually +used to run B, password 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 +password object name. + +=head1 SEE ALSO + +remctld(8), Wallet::Config(3), Wallet::Object::File(3), +wallet-backend(8) + +This module is part of the wallet system. The current version is +available from L. + +=head1 AUTHOR + +Jon Robertson + +=cut diff --git a/perl/t/object/password.t b/perl/t/object/password.t new file mode 100644 index 0000000..c0f2fbc --- /dev/null +++ b/perl/t/object/password.t @@ -0,0 +1,118 @@ +#!/usr/bin/perl +# +# Tests for the password object implementation. Only includes tests that are +# basic or different from the file object implementation. +# +# Written by Jon Robertson +# Copyright 2015 +# The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +use strict; +use warnings; + +use POSIX qw(strftime); +use Test::More tests => 31; + +use Wallet::Admin; +use Wallet::Config; +use Wallet::Object::Password; + +use lib 't/lib'; +use Util; + +# Some global defaults to use. +my $user = 'admin@EXAMPLE.COM'; +my $host = 'localhost'; +my @trace = ($user, $host, time); + +# Flush all output immediately. +$| = 1; + +# Use Wallet::Admin to set up the database. +system ('rm -rf test-files') == 0 or die "cannot remove test-files\n"; +db_setup; +my $admin = eval { Wallet::Admin->new }; +is ($@, '', 'Database connection succeeded'); +is ($admin->reinitialize ($user), 1, 'Database initialization succeeded'); +my $schema = $admin->schema; + +# Use this to accumulate the history traces so that we can check history. +my $history = ''; +my $date = strftime ('%Y-%m-%d %H:%M:%S', localtime $trace[2]); + +$Wallet::Config::PWD_FILE_BUCKET = undef; + +# Test error handling in the absence of configuration. +my $object = eval { + Wallet::Object::Password->create ('password', 'test', $schema, @trace) + }; +ok (defined ($object), 'Creating a basic password object succeeds'); +ok ($object->isa ('Wallet::Object::Password'), ' and is the right class'); +is ($object->get (@trace), undef, ' and get fails'); +is ($object->error, 'password support not configured', + ' with the right error'); +is ($object->store (@trace), undef, ' and store fails'); +is ($object->error, 'password support not configured', + ' with the right error'); +is ($object->destroy (@trace), 1, ' but destroy succeeds'); + +# Set up our configuration. +mkdir 'test-files' or die "cannot create test-files: $!\n"; +$Wallet::Config::PWD_FILE_BUCKET = 'test-files'; +$Wallet::Config::PWD_LENGTH_MIN = 10; +$Wallet::Config::PWD_LENGTH_MAX = 10; + +# Okay, now we can test. First, the basic object without store. +$object = eval { + Wallet::Object::Password->create ('password', 'test', $schema, @trace) + }; +ok (defined ($object), 'Creating a basic password object succeeds'); +ok ($object->isa ('Wallet::Object::Password'), ' and is the right class'); +my $pwd = $object->get (@trace); +like ($pwd, qr{^.{$Wallet::Config::PWD_LENGTH_MIN}$}, + ' and get creates a random password string of the right length'); +ok (-d 'test-files/09', ' and the hash bucket was created'); +ok (-f 'test-files/09/test', ' and the file exists'); +is (contents ('test-files/09/test'), $pwd, ' with the right contents'); +my $pwd2 = $object->get (@trace); +is ($pwd, $pwd2, ' and getting again gives the same string'); +is ($object->destroy (@trace), 1, ' and destroying the object succeeds'); + +# Now check to see if the password length is adjusted. +$Wallet::Config::PWD_LENGTH_MIN = 20; +$Wallet::Config::PWD_LENGTH_MAX = 20; +$object = eval { + Wallet::Object::Password->create ('password', 'test', $schema, @trace) + }; +ok (defined ($object), 'Recreating the object succeeds'); +$pwd = $object->get (@trace); +like ($pwd, qr{^.{$Wallet::Config::PWD_LENGTH_MIN}$}, + ' and get creates a random password string of a longer length'); +is ($object->destroy (@trace), 1, ' and destroying the object succeeds'); + +# Now store something and be sure that we get something reasonable. +$object = eval { + Wallet::Object::Password->create ('password', 'test', $schema, @trace) + }; +ok (defined ($object), 'Recreating the object succeeds'); +is ($object->store ("foo\n", @trace), 1, ' and storing data in it succeeds'); +ok (-f 'test-files/09/test', ' and the file exists'); +is (contents ('test-files/09/test'), 'foo', ' with the right contents'); +is ($object->get (@trace), "foo\n", ' and get returns correctly'); +unlink 'test-files/09/test'; +is ($object->get (@trace), undef, + ' and get will not autocreate a password if there used to be data'); +is ($object->error, 'cannot get password:test: object has not been stored', + ' as if it had not been stored'); +is ($object->store ("bar\n\0baz\n", @trace), 1, ' but storing again works'); +ok (-f 'test-files/09/test', ' and the file exists'); +is (contents ('test-files/09/test'), 'bar', ' with the right contents'); +is ($object->get (@trace), "bar\n\0baz\n", ' and get returns correctly'); + +# Clean up. +$admin->destroy; +END { + unlink ('wallet-db'); +} -- cgit v1.2.3 From e7aed7182fe22c0f89754f96dfa6b2c6c2b665b0 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Sat, 7 Feb 2015 16:03:55 -0800 Subject: Added first pass of password objects to Stanford policy Change-Id: I6198f4247f589e94beced128504dd086194b1983 --- perl/lib/Wallet/Policy/Stanford.pm | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm index a707ea5..4a3c445 100644 --- a/perl/lib/Wallet/Policy/Stanford.pm +++ b/perl/lib/Wallet/Policy/Stanford.pm @@ -75,6 +75,16 @@ our %FILE_TYPE = ( 'tivoli-key' => { host => 1 }, ); +# Password object types. Most of these mimic file object types (which should +# be gradually phased out). +our %PASSWORD_TYPE = ( + 'ipmi' => { host => 1 }, + 'root' => { host => 1 }, + 'tivoli' => { host => 1 }, + 'system' => { host => 1, extra => 1, need_extra => 1 }, + 'app' => { extra => 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); @@ -144,6 +154,17 @@ sub _host_for_file_legacy { return $host; } +# Map a password object name to a hostname. Returns undef if this password +# object name doesn't map to a hostname. +sub _host_for_password { + my ($name) = @_; + + # Parse the name and check whether this is a host-based object. + my ($type, $host) = split('/', $name); + return if !$PASSWORD_TYPE{$type}{host}; + 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 { @@ -192,6 +213,7 @@ sub default_owner { my %host_for = ( 'keytab' => \&_host_for_keytab, 'file' => \&_host_for_file, + 'password' => \&_host_for_password, 'duo' => \&_host_for_duo, 'duo-pam' => \&_host_for_duo, 'duo-radius' => \&_host_for_duo, @@ -242,7 +264,7 @@ sub default_owner { # 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 +# Also use this function to require that ACS staff always do implicit object # creation using a */root instance. sub verify_name { my ($type, $name, $user) = @_; @@ -363,6 +385,8 @@ sub verify_name { return "missing component in $name"; } return; + + } else { # Legacy naming scheme. my %groups = map { $_ => 1 } @GROUPS_LEGACY; @@ -380,6 +404,71 @@ sub verify_name { } } + # Check password object naming conventions. + if ($type eq 'password') { + 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 ($PASSWORD_TYPE{$type} && $PASSWORD_TYPE{$type}{host}) { + my ($host, $extra) = @name; + if ($host !~ m{ [.] }xms) { + return "host name $host is not fully qualified"; + } + if (defined($extra) && !$PASSWORD_TYPE{$type}{extra}) { + return "extraneous component at end of $name"; + } + if (!defined($extra) && $PASSWORD_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 (!$PASSWORD_TYPE{$type}) { + return "unknown type $type"; + } + if ($PASSWORD_TYPE{$type}{host}) { + return "bad name for host-based file type $type"; + } + + # Check the extra data. + if (defined($extra) && !$PASSWORD_TYPE{$type}{extra}) { + return "extraneous component at end of $name"; + } + if (!defined($extra) && $PASSWORD_TYPE{$type}{need_extra}) { + return "missing component in $name"; + } + return; + } + } + # Check the naming conventions for all Duo object types. The object # should simply be the host name for now. if ($type =~ m{^duo(-\w+)?$}) { -- cgit v1.2.3 From f14bd8343010ad96104965029e36c5a65d231571 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Sat, 7 Feb 2015 16:09:12 -0800 Subject: Added an object history report to wallet-report Took code from Commerzbank AG and refactored to add to wallet-report. This does a complete dump of all object history for searching on. Change-Id: Id22c51d2938ad90e0c6a19aaa016501a1ba333b3 --- perl/lib/Wallet/Report.pm | 42 ++++++++++++++++++++++++++++++++++++++++++ server/wallet-report | 8 +++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/perl/lib/Wallet/Report.pm b/perl/lib/Wallet/Report.pm index 2382d87..912bc17 100644 --- a/perl/lib/Wallet/Report.pm +++ b/perl/lib/Wallet/Report.pm @@ -242,6 +242,48 @@ sub objects { return @objects; } +# Returns a list of all object_history records stored in the wallet database +# including all of their fields. 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_history { + my ($self, $type, @args) = @_; + undef $self->{error}; + + # All fields in the order we want to see them. + my @fields = ('oh_on', 'oh_by', 'oh_type', 'oh_name', 'oh_action', + 'oh_from'); + + # Get the search and options array refs from specific functions. + my %search = (); + my %options = (order_by => \@fields, + select => \@fields); + + # Perform the search and return on any errors. + my @objects; + my $schema = $self->{schema}; + eval { + my @objects_rs + = $schema->resultset('ObjectHistory')->search (\%search, + \%options); + for my $object_rs (@objects_rs) { + my @rec; + for my $field (@fields) { + push (@rec, $object_rs->get_column($field)); + } + push (@objects, \@rec); + } + }; + if ($@) { + $self->error ("cannot list objects: $@"); + return; + } + + return @objects; +} + ############################################################################## # ACL reports ############################################################################## diff --git a/server/wallet-report b/server/wallet-report index 1c8f914..bc499d4 100755 --- a/server/wallet-report +++ b/server/wallet-report @@ -23,6 +23,7 @@ Wallet reporting help: objects All objects objects acl Objects granting permissions to that ACL objects flag Objects with that flag set + objects history History of all objects objects owner Objects owned by that owner objects type Objects of that type objects unused Objects that have never been gotten @@ -75,7 +76,12 @@ sub command { print $HELP; } elsif ($command eq 'objects') { die "too many arguments to objects\n" if @args > 2; - my @objects = $report->objects (@args); + my @objects; + if (@args && $args[0] eq 'history') { + @objects = $report->objects_history (@args); + } else { + @objects = $report->objects (@args); + } if (!@objects and $report->error) { die $report->error, "\n"; } -- cgit v1.2.3 From ac97f9268b927cec5af229f496b9dd66332445e4 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 17 Feb 2015 12:27:04 -0800 Subject: Updated documentation for duo and password objects The documentation now includes information about the Duo file types, and the new password types. This is both the general information, and the Stanford-specific naming docs. Change-Id: Iae256224a063ce42f22cd933ef7bb3ab402e0e2d --- docs/objects-and-schemes | 24 +++++++++--- docs/stanford-naming | 97 +++++++++++++++++++++++++++++++----------------- 2 files changed, 81 insertions(+), 40 deletions(-) diff --git a/docs/objects-and-schemes b/docs/objects-and-schemes index 97e6289..763a24b 100644 --- a/docs/objects-and-schemes +++ b/docs/objects-and-schemes @@ -10,17 +10,21 @@ Introduction Object Types - duo + duo-ldap + duo-pam + duo-radius + duo-rdp Stores the configuration for a Duo Security integration. Duo is a cloud provider of multifactor authentication services. A Duo integration consists of some local configuration and a secret key that permits verification of a second factor using the Duo cloud service. - Currently, only UNIX integrations are supported. In the future, this - object type will likely be split into several object types - corresponding to the supported types of Duo integrations. + Each of these types is the same except for the output, which is + specialized towards giving information in the format suited for a + specific application. - Implemented via Wallet::Object::Duo. + Implemented via Wallet::Object::Duo::PAM, Wallet::Object::Duo::RDP, + Wallet::Object::Duo::LDAPProxy, Wallet::Object::Duo::RadiusProxy. file @@ -33,6 +37,16 @@ Object Types Implemented via Wallet::Object::File. + password + + Stores a file with single password in it and allows retrieval of that + file. This is built on the file object and is almost entirely + identical in function. It adds the ability to automatically generate + randomized content if you get the object before it's been stored, + letting you get autogenerated passwords. + + Implemented via Wallet::Object::Password. + keytab Stores a keytab representing private keys for a given Kerberos diff --git a/docs/stanford-naming b/docs/stanford-naming index c86c820..cb05a23 100644 --- a/docs/stanford-naming +++ b/docs/stanford-naming @@ -90,27 +90,6 @@ Object Naming (OLD: --htpasswd-) - password-ipmi/ - - Stores the password for remote IPMI/iLO/ILOM access to the - system. - - (OLD: --password-ipmi) - - password-root/ - - Stores the root password for a given server. - - (OLD: --password-root) - - password-tivoli/ - - Stores the Tivoli TSM backup password for a given server. See - also tivoli-key/, but depending on what one wants to do - with the password, this may be a better representation. - - (OLD: --password-tivoli) - ssh-/ Stores the SSH private key for . For shared private keys @@ -197,20 +176,6 @@ Object Naming (OLD: --gpg-key) - password/// - - A password for some account, service, keystore, or something - similar that is not covered by one of the more specific naming - conventions, such as a password used to connect to a remote ssh - service. is the service that uses this password and - is the thing the password is used for (such as the remote - account name). This may be a file containing only the password, - or a configuration file of some type that includes a field name - and the password. (However, use the db type described above for - database passwords.) - - (OLD: --password-) - properties//[/] The properties file for a Java application that contains some @@ -262,6 +227,68 @@ Object Naming --pam- --puppetconf --shibboleth + --password-ipmi + --password-root + --password-tivoli + --password- + + Replaced by password objects: + + password-ipmi/ + password-root/ + password-tivoli/ + + password/// should be replaced by the password + service/// object if a single password, or by + the file object db/* or config/* format if the object contains more + than just the bare password. + + Password + + Passwords are a recent type and so most password data is actually + in file objects. However, we'd like to move things there both for + the added features of password objects to self-set, and because it + helps clean up the file namespace a little more. + + Host-based: + + ipmi/ + + Stores the password for remote IPMI/iLO/ILOM access to the + system. + + tivoli/ + + Stores the Tivoli TSM backup password for a given server. See + also tivoli-key/ in the file section, but depending on + what one wants to do with the password, this may be a better + representation. + + root/ + + Stores the root password for a given server. + + system// + + Stores the password for a non-root system account, such as a user + required for file uploads. + + app// + + Stores an application password bound to a certain server. + + Service-based: + + service/// + + A password for some account, service, keystore, or something + similar that is not covered by one of the more specific naming + conventions, such as a password used to connect to a remote ssh + service. is the service that uses this password and + is the thing the password is used for (such as the remote + account name). This should only be for something including the + password and nothing else. See the file password/ object name + for something that includes more data. ACL Naming -- cgit v1.2.3 From 7181c4e75fbd3e0a4d16aad5f227fd4ecb94ccc0 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 17 Feb 2015 12:29:11 -0800 Subject: Added service type to Stanford policy for password Added to the password object type a new naming set for service/*, specifically for things that belong to a non-host-specific service. Change-Id: I1481d48319a5833f00eae940a6d2ca912874bb01 --- perl/lib/Wallet/Policy/Stanford.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm index 4a3c445..39a8174 100644 --- a/perl/lib/Wallet/Policy/Stanford.pm +++ b/perl/lib/Wallet/Policy/Stanford.pm @@ -82,7 +82,8 @@ our %PASSWORD_TYPE = ( 'root' => { host => 1 }, 'tivoli' => { host => 1 }, 'system' => { host => 1, extra => 1, need_extra => 1 }, - 'app' => { extra => 1 }, + 'app' => { host => 1, extra => 1, need_extra => 1 }, + 'service' => { extra => 1, need_extra => 1 }, ); # Host-based file object types for the legacy file object naming scheme. -- cgit v1.2.3 From ef46bf3e857043ce226ec9b9918605df57da87f9 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 17 Feb 2015 12:30:27 -0800 Subject: Added a contrib script for history actions Commerzbank offered a script for searching and editing the wallet history. The coding style is very different from our own, so I'm including this as a contrib script for now. Change-Id: I20516d63ad6f633ad0efc3977d990fa1e7a5ebd9 --- contrib/commerzbank/wallet-history | 475 +++++++++++++++++++++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100755 contrib/commerzbank/wallet-history diff --git a/contrib/commerzbank/wallet-history b/contrib/commerzbank/wallet-history new file mode 100755 index 0000000..9826057 --- /dev/null +++ b/contrib/commerzbank/wallet-history @@ -0,0 +1,475 @@ +#!/usr/bin/perl -w +# +# -*- perl -*- +# +#-------------------------------------------------------------------------------------------------------------- +# Program : wallet-history +# Function : Tool for listing and/or modifying Wallet's object history +# Author : Commerzbank AG +# History : 20.01.2014 - V0.1 - First version - maxcrc +# : 21.01.2014 - V0.2 - Fixed some minor bugs and code formatting - Gerhard Stahl +# Added some remarks for nessesary fixes +# : 29.01.2014 - V0.2 - Fixed issue related to variable comparision of undef values - maxcrc +# Reformatted code to use functions like debug, usage - maxcrc +# : 06.02.2014 - V0.3 - Added support for ACL history in addition to object history - maxcrc +# : 07.02.2014 - V0.4 - A lot of improvements, run with --help to see - maxcrc +# --acl and --obj argument names removed, please use --acls and --objs +# --from and --to argument names replaced with --start and --end +# Added new filtering args --from, --action, --acl, --by, --type and --name +# Added new --version action type +# Added new --columns action type +# : 08.02.2014 - V0.5 - Added support for ACL names via new ah_name column - maxcrc +# +#-------------------------------------------------------------------------------------------------------------- +# +# Usage: +# +# type ./wallet-history with no arguments to get the full help text (or with --help). +# +# In general, use a command of the form: +# +# perl wallet-history.pl ... (t.b.d.)... +# +#-------------------------------------------------------------------------------------------------------------- +# Version. + +my $VERSION = "0.5"; + +=head1 Needed Modules + +Here is the list of modules we need: + +=cut + +use strict; +use Getopt::Long; +use Wallet::Schema; +use DateTime; +use DateTime::Format::Strptime; +#use DBIx::ResultSet; +use DBI; +#use DateTime (); +#use Scalar::Util; + +my $debug_on = 0; + +=head1 Subroutines + +=head2 Utility subroutines + +=head3 usage - display a usage message + +=cut + +sub usage() { + print "wallet-history is tool for listing and/or modifying Wallet's history\n"; + print "usage:\n"; + print "wallet-history [mandatory action] [optional query type] [optional filtering arguments]\n"; + print " where mandatory action is one of:\n"; + print " --help - display this help information\n"; + print " --version - display app version\n"; + print " --columns - display column names (use with --objs or --acls)\n"; + print " --list - list ACL or object history with/without from/to date filters\n"; + print " --clear - clear ACL or object history with/without from/to date filters\n"; + print " optional query type is one of:\n"; + print " --acls - query is for ACL history (default is --objs)\n"; + print " --objs - query is for object history (this is default)\n"; + print " optional filtering data entries by (any [name] can be regular expression):\n"; + print " --start [date/time] - starting date\n"; + print " --end [date/time] - ending date\n"; + print " --from [name] - computer name\n"; + print " --action [name] - action\n"; + print " --acl [name] - acl (with --acls only)\n"; + print " --by [name] - by address (with --acls only)\n"; + print " --type [name] - type (with --objs only)\n"; + print " --name [name] - host name with --objs, acl name with --acls\n"; +} + + +=head3 usage - prints debug messages + +=cut + +sub debug { + my $msg = @_; + return unless ( $debug_on == 1); + print $msg . "\n"; +} + + +my $parser = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%dT%H:%M:%S', + on_error => 'croak', + ); + +my $opt_result = undef; +my $an_action_help = undef; +my $an_action_version = undef; +my $an_action_columns = undef; +my $an_action_list = undef; +my $an_action_clear = undef; +my $is_action_acls = 0; +my $is_action_objs = 1; +my $str_date_from = undef; +my $str_date_to = undef; +my $str_filter_computer_name = undef; +my $str_filter_action = undef; +my $str_filter_acl = undef; +my $str_filter_by = undef; +my $str_filter_type = undef; +my $str_filter_name = undef; + $opt_result = GetOptions ( + "help" => \$an_action_help + , "version" => \$an_action_version + , "columns" => \$an_action_columns + , "list" => \$an_action_list + , "clear" => \$an_action_clear + , "acls" => \$is_action_acls + , "objs" => \$is_action_objs + , "start=s" => \$str_date_from + , "end=s" => \$str_date_to + , "from=s" => \$str_filter_computer_name + , "action=s" => \$str_filter_action + , "acl=s" => \$str_filter_acl + , "by=s" => \$str_filter_by + , "type=s" => \$str_filter_type + , "name=s" => \$str_filter_name + ); + + if( ( defined $an_action_help ) or ( defined $an_action_version ) ) { + if( defined $an_action_version ) { + print "wallet-history version is $VERSION\n"; + } + if( defined $an_action_help ) { + usage; + } + exit 0; # FIN # + } + +my $str_resultset_name = undef; + if( $is_action_acls ) { + $is_action_objs = 0; + $str_resultset_name = 'AclHistory'; + } else { + $is_action_objs = 1; + $str_resultset_name = 'ObjectHistory'; + } + + if( defined $an_action_columns ) { + if( $is_action_acls ) { + print "\"ah_on\" \"ah_acl\" \"ah_by\" \"ah_action\" \"ah_from\"\n"; + } else { if( $is_action_objs ) { + print "\"oh_on\" \"oh_by\" \"oh_type\" \"oh_name\" \"oh_action\" \"oh_from\"\n"; + } else { + die "Critical internal error - unknown dataset type\n"; + } } + exit 0; # FIN # + } + + +my $date_from = undef; + if ( defined $str_date_from ) { + $date_from = $parser->parse_datetime( $str_date_from ); + debug "Using minimal(from) date $date_from\n"; + } +my $date_to = undef; + if ( defined $str_date_to ) { + $date_to = $parser->parse_datetime( $str_date_to ); + debug "Using maximal(to) date $date_to\n"; + } + + if ( $an_action_list || $an_action_clear ) { + my $schema = Wallet::Schema->connect; + my @data_entries; + eval { + my @data_entries_rs = $schema->resultset( $str_resultset_name )->search(); # ( $search_ref, $options_ref ); + for my $entry_rs (@data_entries_rs) { + if( $is_action_acls ) { + # begin - working with ACLs history + my $is_filter_passed = 1; + my $str_date_on = "" . $entry_rs->ah_on; + my $date_on = $parser->parse_datetime( $str_date_on ); + debug "date on as string ", $str_date_on, "\n"; + debug "date on as date ", $date_on, "\n"; + if ( ( defined $date_from ) || ( defined $date_to ) ) { + if ( ( defined $date_from ) && ( $date_from > $date_on ) ) { + $is_filter_passed = 0; + } + if ( ( defined $date_to ) && ( $date_to < $date_on ) ) { + $is_filter_passed = 0; + } + } # if ( ( defined $date_from ) || ( defined $date_to ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_computer_name ) ) { + my $str_text_to_test = "" . $entry_rs->ah_from; + if( not ( $str_text_to_test =~ /$str_filter_computer_name/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_computer_name ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_action ) ) { + my $str_text_to_test = "" . $entry_rs->ah_action; + if( not ( $str_text_to_test =~ /$str_filter_action/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_action ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_acl ) ) { + my $str_text_to_test = "" . $entry_rs->ah_acl; + if( not ( $str_text_to_test =~ /$str_filter_acl/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_acl ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_by ) ) { + my $str_text_to_test = "" . $entry_rs->ah_by; + if( not ( $str_text_to_test =~ /$str_filter_by/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_by ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_name ) ) { + my $str_text_to_test = "" . $entry_rs->ah_name; + if( not ( $str_text_to_test =~ /$str_filter_name/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_name ) ) + if( $is_filter_passed != 0 ) { + push (@data_entries, [$entry_rs->ah_on, $entry_rs->ah_acl, $entry_rs->ah_by, $entry_rs->ah_action, $entry_rs->ah_from, $entry_rs->ah_name ]); + debug "Found: \"", $entry_rs->ah_on, "\" \"", $entry_rs->ah_acl, "\" \"", $entry_rs->ah_by, "\" \"", $entry_rs->ah_action, "\" \"", $entry_rs->ah_from, "\" \"", $entry_rs->ah_name, "\n"; + } # if( $is_filter_passed != 0 ) + debug "walked throgh dates \"", ref($date_from), "-", $date_from, "\" \"", ref($date_on), "-", $date_on, "\" \"", ref($date_to), "-", $date_to, "\" result is ", $is_filter_passed, "\n"; + # end - working with ACLs history + } else { if( $is_action_objs ) { + # begin - working with objects history + my $is_filter_passed = 1; + my $str_date_on = "" . $entry_rs->oh_on; + my $date_on = $parser->parse_datetime( $str_date_on ); + debug "date on as string ", $str_date_on, "\n"; + debug "date on as date ", $date_on, "\n"; + if ( ( defined $date_from ) || ( defined $date_to ) ) { + if ( ( defined $date_from ) && ( $date_from > $date_on ) ) { + $is_filter_passed = 0; + } + if ( ( defined $date_to ) && ( $date_to < $date_on ) ) { + $is_filter_passed = 0; + } + } # if ( ( defined $date_from ) || ( defined $date_to ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_computer_name ) ) { + my $str_text_to_test = "" . $entry_rs->oh_from; + if( not ( $str_text_to_test =~ /$str_filter_computer_name/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_computer_name ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_action ) ) { + my $str_text_to_test = "" . $entry_rs->oh_action; + if( not ( $str_text_to_test =~ /$str_filter_action/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_action ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_type ) ) { + my $str_text_to_test = "" . $entry_rs->oh_type; + if( not ( $str_text_to_test =~ /$str_filter_type/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_type ) ) + if( $is_filter_passed != 0 && ( defined $str_filter_name ) ) { + my $str_text_to_test = "" . $entry_rs->oh_name; + if( not ( $str_text_to_test =~ /$str_filter_name/ ) ) { + $is_filter_passed = 0; + } + } # if( $is_filter_passed != 0 && ( defined $str_filter_name ) ) + if( $is_filter_passed != 0 ) { + push (@data_entries, [$entry_rs->oh_on, $entry_rs->oh_by, $entry_rs->oh_type, $entry_rs->oh_name, $entry_rs->oh_action, $entry_rs->oh_from ]); + debug "Found: \"", $entry_rs->oh_on, "\" \"", $entry_rs->oh_by, "\" \"", $entry_rs->oh_type, "\" \"", $entry_rs->oh_name, "\" \"", $entry_rs->oh_action, "\" \"", $entry_rs->oh_from, "\"", "\n"; + } # if( $is_filter_passed != 0 ) + debug "walked throgh dates \"", ref($date_from), "-", $date_from, "\" \"", ref($date_on), "-", $date_on, "\" \"", ref($date_to), "-", $date_to, "\" result is ", $is_filter_passed, "\n"; + # end - working with objects history + } else { + die "Critical internal error - unknown dataset type\n"; + } } + } # for my $entry_rs (@data_entries_rs) + }; # eval + if ($@) { + print "cannot list data entries: $@"; + exit -1; + } + + if( $is_action_acls ) { + # begin - working with ACLs history + for my $group (@data_entries) { + if ( $an_action_clear ) { + my $is_deleted_ok = 0; + print "\"", join ("\" \"", @$group), "\" ... "; + my $sql_delete_error = undef; + eval { + my $str_ah_on = "" . $group->[0]; + my $str_ah_acl = "" . $group->[1]; + my $str_ah_by = "" . $group->[2]; + my $str_ah_action = "" . $group->[3]; + my $str_ah_from = "" . $group->[4]; + my $str_ah_name = "" . $group->[5]; + + my $string = $str_ah_on; # '01234567890'; + my $find = 'T'; # '0'; + my $replace = ' '; # 'a'; + my $pos = index($string, $find); + while ( $pos > -1 ) { + substr( $string, $pos, length( $find ), $replace ); + $pos = index( $string, $find, $pos + length( $replace )); + } + $str_ah_on = $string; + + debug "Searching \"$str_ah_on\" \"$str_ah_acl\" \"$str_ah_by\" \"$str_ah_action\" \"$str_ah_from\" \"$str_ah_name\" ... \n"; + + #my @data_entries_rs = + # $schema->resultset('AclHistory')->search( [ + # { ah_on => $str_ah_on } + # , { ah_acl => $str_ah_acl } + # , { ah_by => $str_ah_by } + # , { ah_action => $str_ah_action } + # , { ah_from => $str_ah_from } + # , { ah_name => $str_ah_name } + # ] ); # -> delete; + + #my @data_entries_rs = + # $schema->resultset('AclHistory')->search( [ + # { ah_on => { '=' , $str_ah_on } } + # , { ah_acl => { '=' , $str_ah_acl } } + # , { ah_by => { '=' , $str_ah_by } } + # , { ah_action => { '=' , $str_ah_action } } + # , { ah_from => { '=' , $str_ah_from } } + # , { ah_name => { '=' , $str_ah_name } } + # ] ); # -> delete; + + my $str_sql_del = "DELETE FROM acl_history WHERE ah_on = TO_DATE(\'" . $str_ah_on . "\', 'YYYY-MM-DD HH24:MI:SS') AND ah_acl = \'" . $str_ah_acl . "\' AND ah_by = \'" . $str_ah_by . "\' AND ah_action = \'" . $str_ah_action . "\' AND ah_from = \'" . $str_ah_from . "\' AND ah_name = \'" . $str_ah_name . "\'"; + + #$schema->storage->debug(1); + my @ret = $schema->storage->dbh_do( + sub { + my ($storage, $dbh, @args) = @_; + debug "Attempting to ", $str_sql_del, "\n"; + debug "dbh is ", $dbh, "\n"; + my $sth = $dbh->prepare( $str_sql_del ); + #$sth->{PrintError} = 1; + #$sth->{RaiseError} = 1; + debug "sth is ", $sth, "\n"; + my $ret = $sth->execute(); # or die "Can't execute SQL statement: $DBI::errstr\n"; + debug "ret is ", $ret, "\n"; + debug "error string is", $sth->errstr(), "\n"; + if ( $ret == 0 ) { + $sql_delete_error = "Internal error"; + } else { + $is_deleted_ok = 1; + } + }, + $str_sql_del + ); + + #$is_deleted_ok = 1; + + #for my $entry_rs (@data_entries_rs) { + # print "Will delete: \"", $entry_rs->ah_on, "\" \"", $entry_rs->ah_acl, "\" \"", $entry_rs->ah_by, "\" \"", $entry_rs->ah_action, "\" \"", $entry_rs->ah_from, "\"", "\n"; + #} # for my $entry_rs (@data_entries_rs) + + }; # eval + if ( $is_deleted_ok ) { + print "Deleted\n"; + } else { + print "Error ", $sql_delete_error, "\n"; + } + } else { # else from if ( $an_action_clear ) + print "\"", join ("\" \"", @$group), "\"\n"; + } # else from if ( $an_action_clear ) + } # for my $group (@data_entries) + # end - working with ACLs history + } else { if( $is_action_objs ) { + # begin - working with objects history + for my $group (@data_entries) { + if ( $an_action_clear ) { + my $is_deleted_ok = 0; + print "\"", join ("\" \"", @$group), "\" ... "; + my $sql_delete_error = undef; + eval { + my $str_oh_on = "" . $group->[0]; + my $str_oh_by = "" . $group->[1]; + my $str_oh_type = "" . $group->[2]; + my $str_oh_name = "" . $group->[3]; + my $str_oh_action = "" . $group->[4]; + my $str_oh_from = "" . $group->[5]; + + my $string = $str_oh_on; # '01234567890'; + my $find = 'T'; # '0'; + my $replace = ' '; # 'a'; + my $pos = index($string, $find); + while ( $pos > -1 ) { + substr( $string, $pos, length( $find ), $replace ); + $pos = index( $string, $find, $pos + length( $replace )); + } + $str_oh_on = $string; + + debug "Searching \"$str_oh_on\" \"$str_oh_by\" \"$str_oh_type\" \"$str_oh_name\" \"$str_oh_action\" \"$str_oh_from\" ... \n"; + + #my @data_entries_rs = + # $schema->resultset('ObjectHistory')->search( [ + # { oh_on => $str_oh_on } + # , { oh_type => $str_oh_type } + # , { oh_name => $str_oh_name } + # , { oh_action => $str_oh_action } + # , { oh_from => $str_oh_from } + # ] ); # -> delete; + + #my @data_entries_rs = + # $schema->resultset('ObjectHistory')->search( [ + # { oh_on => { '=' , $str_oh_on } } + # , { oh_type => { '=' , $str_oh_type } } + # , { oh_name => { '=' , $str_oh_name } } + # , { oh_action => { '=' , $str_oh_action } } + # , { oh_from => { '=' , $str_oh_from } } + # ] ); # -> delete; + + my $str_sql_del = "DELETE FROM object_history WHERE oh_on = TO_DATE(\'" . $str_oh_on . "\', 'YYYY-MM-DD HH24:MI:SS') AND oh_type = \'" . $str_oh_type . "\' AND oh_name = \'" . $str_oh_name . "\' AND oh_action = \'" . $str_oh_action . "\' AND oh_from = \'" . $str_oh_from . "\'"; + + #$schema->storage->debug(1); + my @ret = $schema->storage->dbh_do( + sub { + my ($storage, $dbh, @args) = @_; + debug "Attempting to ", $str_sql_del, "\n"; + debug "dbh is ", $dbh, "\n"; + my $sth = $dbh->prepare( $str_sql_del ); + #$sth->{PrintError} = 1; + #$sth->{RaiseError} = 1; + debug "sth is ", $sth, "\n"; + my $ret = $sth->execute(); # or die "Can't execute SQL statement: $DBI::errstr\n"; + debug "ret is ", $ret, "\n"; + debug "error string is", $sth->errstr(), "\n"; + if ( $ret == 0 ) { + $sql_delete_error = "Internal error"; + } else { + $is_deleted_ok = 1; + } + }, + $str_sql_del + ); + + #$is_deleted_ok = 1; + + #for my $entry_rs (@data_entries_rs) { + # print "Will delete: \"", $entry_rs->oh_on, "\" \"", $entry_rs->oh_type, "\" \"", $entry_rs->oh_name, "\" \"", $entry_rs->oh_action, "\" \"", $entry_rs->oh_from, "\"", "\n"; + #} # for my $entry_rs (@data_entries_rs) + + }; # eval + if ( $is_deleted_ok ) { + print "Deleted\n"; + } else { + print "Error ", $sql_delete_error, "\n"; + } + } else { # else from if ( $an_action_clear ) + print "\"", join ("\" \"", @$group), "\"\n"; + } # else from if ( $an_action_clear ) + } # for my $group (@data_entries) + # end - working with objects history + } else { + die "Critical internal error - unknown dataset type\n"; + } } + + exit 0; # FIN # + } # if ( $an_action_list || $an_action_clear ) + + usage; + exit 0; # FIN # -- cgit v1.2.3 From 956cc93882ebf1fa331a75abe8a85cdbcad64486 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 17 Feb 2015 12:40:39 -0800 Subject: Updated NEWS with changes so far Change-Id: Icb894b4b52e6b5c07a7c12251b1f4c79025c7bc6 --- NEWS | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/NEWS b/NEWS index 272b109..64c0fbc 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,24 @@ User-Visible wallet Changes +wallet 1.3 (xxxx-xx-xx) + + A new object type, password (Wallet::Object::Password), is now + supported. This is a subclass of the file object that will randomly + generate content for the object if you do a get before storing any + content inside it. + + Added an acl replace command, to change all objects owned by one ACL + to be owned by another. + + Added a report for unstored objects to wallet-report, and cleaned up + the help for the existing unused report that implied it showed + unstored as well as unused. + + Took contributions from Commerzbank AG on the wallet history. Added + a command to dump all object history for searching on to + wallet-report, and added a new script for more detailed object + history operations to the contrib directory. + wallet 1.2 (2014-12-08) The duo object type has been split into several sub-types, each for a -- cgit v1.2.3 From ed628d6c89d63fdcbf038ce19776e3047c1105e2 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 17 Feb 2015 13:46:11 -0800 Subject: Added ssl-chain name prefix to Stanford policy Added for SSL files including the root cert as well, used in splunk. Change-Id: I1faaa840d309ae4370ae26da5b51c0cee84d7558 --- perl/lib/Wallet/Policy/Stanford.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm index 39a8174..362f098 100644 --- a/perl/lib/Wallet/Policy/Stanford.pm +++ b/perl/lib/Wallet/Policy/Stanford.pm @@ -68,6 +68,7 @@ our %FILE_TYPE = ( properties => { extra => 1 }, 'ssh-dsa' => { host => 1, extra => 1 }, 'ssh-rsa' => { host => 1, extra => 1 }, + 'ssl-chain' => { host => 1, extra => 1 }, 'ssl-key' => { host => 1, extra => 1 }, 'ssl-keypair' => { host => 1, extra => 1 }, 'ssl-keystore' => { extra => 1 }, -- cgit v1.2.3 From 1575e25c9e9edc8d577a0f1118732c98871984e0 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 17 Feb 2015 14:32:59 -0800 Subject: Added reports to list all types and acl schemes Two new reports, 'types' and 'schemes'. These will print out all configured types and acl schemes. Change-Id: Ib06d37755fe80c168a6f723c9a1e683fdf5dfcde --- perl/lib/Wallet/Report.pm | 38 ++++++++++++++++++++++++++++++++++++++ perl/t/general/report.t | 26 +++++++++++++++++++++++++- server/wallet-report | 16 ++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/perl/lib/Wallet/Report.pm b/perl/lib/Wallet/Report.pm index 912bc17..4d92d64 100644 --- a/perl/lib/Wallet/Report.pm +++ b/perl/lib/Wallet/Report.pm @@ -284,6 +284,27 @@ sub objects_history { return @objects; } +############################################################################## +# Type reports +############################################################################## + +# Return an alphabetical list of all valid types set up, along with the class +# that they belong to. +sub types { + my ($self) = @_; + + my (@types); + my @types_rs = $self->{schema}->resultset('Type')->all; + for my $type_rs (@types_rs) { + my $name = $type_rs->ty_name; + my $class = $type_rs->ty_class; + push(@types, [ $name, $class ]); + } + + @types = sort { $a->[0] cmp $b->[0] } @types; + return @types; +} + ############################################################################## # ACL reports ############################################################################## @@ -527,6 +548,23 @@ sub owners { return @owners; } +# Return an alphabetical list of all valid types set up, along with the class +# that they belong to. +sub acl_schemes { + my ($self) = @_; + + my (@schemes); + my @acls_rs = $self->{schema}->resultset('AclScheme')->all; + for my $acl_rs (@acls_rs) { + my $name = $acl_rs->as_name; + my $class = $acl_rs->as_class; + push(@schemes, [ $name, $class ]); + } + + @schemes = sort { $a->[0] cmp $b->[0] } @schemes; + return @schemes; +} + ############################################################################## # Auditing ############################################################################## diff --git a/perl/t/general/report.t b/perl/t/general/report.t index 8d348ed..a63ab79 100755 --- a/perl/t/general/report.t +++ b/perl/t/general/report.t @@ -11,7 +11,7 @@ use strict; use warnings; -use Test::More tests => 197; +use Test::More tests => 215; use Wallet::Admin; use Wallet::Report; @@ -41,6 +41,30 @@ is (scalar (@acls), 1, 'One ACL in the database'); is ($acls[0][0], 1, ' and that is ACL ID 1'); is ($acls[0][1], 'ADMIN', ' with the right name'); +# Check to see that we have all types that we expect. +my @types = $report->types; +is (scalar (@types), 10, 'There are ten types created'); +is ($types[0][0], 'base', ' and the first member is correct'); +is ($types[1][0], 'duo', ' and the second member is correct'); +is ($types[2][0], 'duo-ldap', ' and the third member is correct'); +is ($types[3][0], 'duo-pam', ' and the fourth member is correct'); +is ($types[4][0], 'duo-radius', ' and the fifth member is correct'); +is ($types[5][0], 'duo-rdp', ' and the sixth member is correct'); +is ($types[6][0], 'file', ' and the seventh member is correct'); +is ($types[7][0], 'keytab', ' and the eighth member is correct'); +is ($types[8][0], 'password', ' and the nineth member is correct'); +is ($types[9][0], 'wa-keyring', ' and the tenth member is correct'); + +# And that we have all schemes that we expect. +my @schemes = $report->acl_schemes; +is (scalar (@schemes), 6, 'There are six acl schemes created'); +is ($schemes[0][0], 'base', ' and the first member is correct'); +is ($schemes[1][0], 'krb5', ' and the second member is correct'); +is ($schemes[2][0], 'krb5-regex', ' and the third member is correct'); +is ($schemes[3][0], 'ldap-attr', ' and the fourth member is correct'); +is ($schemes[4][0], 'netdb', ' and the fifth member is correct'); +is ($schemes[5][0], 'netdb-root', ' and the sixth member is correct'); + # Create an object. my $server = eval { Wallet::Server->new ('admin@EXAMPLE.COM', 'localhost') }; is ($@, '', 'Creating a server instance did not die'); diff --git a/server/wallet-report b/server/wallet-report index bc499d4..6d1436c 100755 --- a/server/wallet-report +++ b/server/wallet-report @@ -29,6 +29,8 @@ Wallet reporting help: objects unused Objects that have never been gotten objects unstored Objects that have never been stored owners All ACL entries owning matching objects + schemes All configured ACL schemes + types All configured wallet types EOH ############################################################################## @@ -98,6 +100,20 @@ sub command { for my $entry (@entries) { print join (' ', @$entry), "\n"; } + } elsif ($command eq 'schemes') { + die "too many arguments to schemes\n" if @args > 0; + my @schemes = $report->acl_schemes; + for my $entry (@schemes) { + print join (' ', @$entry), "\n"; + } + + } elsif ($command eq 'types') { + die "too many arguments to types\n" if @args > 0; + my @types = $report->types; + for my $entry (@types) { + print join (' ', @$entry), "\n"; + } + } else { die "unknown command $command\n"; } -- cgit v1.2.3 From 000b338694fae87996220336678fe990a1c3e3e1 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Wed, 18 Feb 2015 15:17:51 -0800 Subject: Added new method for wallet-backend, update update will work generally like get, but only for objects that have a concept of updating content automatically, like keytabs and passwords. For these, the content will be updated before sending to the client. In a later release get for keytabs will be modified to never update the kvno before sending to the user, and so the unchanging flag will be phased out in lieu of explicitly using the method that does what you want. Change-Id: I96a84416c5e50278eb29fe07052dde6e063bc071 --- NEWS | 9 ++++++ client/wallet.pod | 13 +++++++++ perl/lib/Wallet/Object/Base.pm | 9 ++++++ perl/lib/Wallet/Object/Keytab.pm | 59 ++++++++++++++++++++++++++------------ perl/lib/Wallet/Object/Password.pm | 32 ++++++++++++++++----- perl/lib/Wallet/Server.pm | 15 ++++++++++ perl/t/object/base.t | 5 +++- perl/t/object/password.t | 8 +++++- server/wallet-backend | 16 +++++++++++ 9 files changed, 138 insertions(+), 28 deletions(-) diff --git a/NEWS b/NEWS index 64c0fbc..664da05 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,15 @@ wallet 1.3 (xxxx-xx-xx) generate content for the object if you do a get before storing any content inside it. + Added a new command to wallet-backend, update. This will update the + contents of an object before running a get on it, and is only valid + for objects that can automatically get new content, such as keytab + and password objects. A keytab will get a new kvno regardless of + the unchanging flag if called with update. In a future release get + will be changed to never update a keytab, and the unchanging flag + will be ignored. Please start moving to use get or update as the + situation warrants. + Added an acl replace command, to change all objects owned by one ACL to be owned by another. diff --git a/client/wallet.pod b/client/wallet.pod index 20d1874..672f0e4 100644 --- a/client/wallet.pod +++ b/client/wallet.pod @@ -385,6 +385,19 @@ If an object with type and name does not already exist when this command is issued (as checked with the check interface), B will attempt to automatically create it (using autocreate). +=item update + +Prints to standard output the data associated with the object identified +by and , or stores it in a file if the B<-f> option was +given. This will generate new data in the object, and only works for +objects that support generating new data automatically, such as keytabs or +passwords. Types that do not support generating new data will fail and +direct you to use get instead. + +If an object with type and name does not already exist when +this command is issued (as checked with the check interface), B +will attempt to automatically create it (using autocreate). + =back =head1 ATTRIBUTES diff --git a/perl/lib/Wallet/Object/Base.pm b/perl/lib/Wallet/Object/Base.pm index bdd61fb..97e6127 100644 --- a/perl/lib/Wallet/Object/Base.pm +++ b/perl/lib/Wallet/Object/Base.pm @@ -609,6 +609,15 @@ sub history { # The get methods must always be overridden by the subclass. sub get { die "Do not instantiate Wallet::Object::Base directly\n"; } +# The update method should only work if a subclass supports it as something +# different from get. That makes it explicit about whether the subclass has +# a meaningful update. +sub update { + my ($self) = @_; + $self->error ("update is not supported for this type, use get instead"); + return; +} + # 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 { diff --git a/perl/lib/Wallet/Object/Keytab.pm b/perl/lib/Wallet/Object/Keytab.pm index 975179b..c625766 100644 --- a/perl/lib/Wallet/Object/Keytab.pm +++ b/perl/lib/Wallet/Object/Keytab.pm @@ -28,6 +28,37 @@ use Wallet::Kadmin; # that it will sort properly. $VERSION = '0.09'; +############################################################################## +# Shared methods +############################################################################## + +# Generate a keytab into a temporary file and then return that as the return +# value. Used by both get and update, as the only difference is how we +# handle the unchanging flag. +sub retrieve { + my ($self, $operation, $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 ($operation eq 'get' && $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 ($operation, $user, $host, $time); + } else { + $self->error ($kadmin->error); + } + return $result; +} + ############################################################################## # Enctype restriction ############################################################################## @@ -314,25 +345,15 @@ sub destroy { # 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); - } + my $result = $self->retrieve ('get', $user, $host, $time); + return $result; +} + +# Our update implementation. Generate a new keytab regardless of the +# unchanging flag. +sub update { + my ($self, $user, $host, $time) = @_; + my $result = $self->retrieve ('update', $user, $host, $time); return $result; } diff --git a/perl/lib/Wallet/Object/Password.pm b/perl/lib/Wallet/Object/Password.pm index d06c8a6..3fd6ec8 100644 --- a/perl/lib/Wallet/Object/Password.pm +++ b/perl/lib/Wallet/Object/Password.pm @@ -57,12 +57,12 @@ sub file_path { } ############################################################################## -# Core methods +# Shared methods ############################################################################## # Return the contents of the file. -sub get { - my ($self, $user, $host, $time) = @_; +sub retrieve { + my ($self, $operation, $user, $host, $time) = @_; $time ||= time; my $id = $self->{type} . ':' . $self->{name}; if ($self->flag_check ('locked')) { @@ -72,13 +72,13 @@ sub get { my $path = $self->file_path; return unless $path; - # If nothing is yet stored, generate a random password and save it to - # the file. + # If nothing is yet stored, or we have requested an update, generate a + # random password and save it to the file. my $schema = $self->{schema}; my %search = (ob_type => $self->{type}, ob_name => $self->{name}); my $object = $schema->resultset('Object')->find (\%search); - unless ($object->ob_stored_on) { + if (!$object->ob_stored_on || $operation eq 'update') { unless (open (FILE, '>', $path)) { $self->error ("cannot store initial settings for $id: $!\n"); return; @@ -103,10 +103,28 @@ sub get { $self->error ("cannot get $id: $!"); return; } - $self->log_action ('get', $user, $host, $time); + $self->log_action ($operation, $user, $host, $time); return $data; } +############################################################################## +# Core methods +############################################################################## + +# Return the contents of the file. +sub get { + my ($self, $user, $host, $time) = @_; + my $result = $self->retrieve ('get', $user, $host, $time); + return $result; +} + +# Return the contents of the file after resetting them to a random string. +sub update { + my ($self, $user, $host, $time) = @_; + my $result = $self->retrieve ('update', $user, $host, $time); + return $result; +} + 1; __END__ diff --git a/perl/lib/Wallet/Server.pm b/perl/lib/Wallet/Server.pm index 6af0570..3ef5954 100644 --- a/perl/lib/Wallet/Server.pm +++ b/perl/lib/Wallet/Server.pm @@ -516,6 +516,21 @@ sub get { return $result; } +# Retrieve the information associated with an object, updating the current +# information if we are of a type that allows autogenerated information. +# 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 update { + my ($self, $type, $name) = @_; + my $object = $self->retrieve ($type, $name); + return unless defined $object; + return unless $self->acl_verify ($object, 'get'); + my $result = $object->update ($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 diff --git a/perl/t/object/base.t b/perl/t/object/base.t index ee9ff4b..8fedd64 100755 --- a/perl/t/object/base.t +++ b/perl/t/object/base.t @@ -12,7 +12,7 @@ use strict; use warnings; use POSIX qw(strftime); -use Test::More tests => 137; +use Test::More tests => 139; use Wallet::ACL; use Wallet::Admin; @@ -208,6 +208,9 @@ is ($object->flag_clear ('locked', @trace), 1, 'Clearing locked succeeds'); eval { $object->get (@trace) }; is ($@, "Do not instantiate Wallet::Object::Base directly\n", 'Get fails with the right error'); +ok (!$object->update (@trace), 'Update fails'); +is ($object->error, 'update is not supported for this type, use get instead', + ' with the right error'); ok (! $object->store ("Some data", @trace), 'Store fails'); is ($object->error, "cannot store keytab:$princ: object type is immutable", ' with the right error'); diff --git a/perl/t/object/password.t b/perl/t/object/password.t index c0f2fbc..4fe6b50 100644 --- a/perl/t/object/password.t +++ b/perl/t/object/password.t @@ -13,7 +13,7 @@ use strict; use warnings; use POSIX qw(strftime); -use Test::More tests => 31; +use Test::More tests => 33; use Wallet::Admin; use Wallet::Config; @@ -111,6 +111,12 @@ ok (-f 'test-files/09/test', ' and the file exists'); is (contents ('test-files/09/test'), 'bar', ' with the right contents'); is ($object->get (@trace), "bar\n\0baz\n", ' and get returns correctly'); +# And check to make sure update changes the contents. +$pwd = $object->update (@trace); +isnt ($pwd, "bar\n\0baz\n", 'Update changes the contents'); +like ($pwd, qr{^.{$Wallet::Config::PWD_LENGTH_MIN}$}, + ' to a random password string of the right length'); + # Clean up. $admin->destroy; END { diff --git a/server/wallet-backend b/server/wallet-backend index dcf2300..ea3e21e 100755 --- a/server/wallet-backend +++ b/server/wallet-backend @@ -315,6 +315,14 @@ sub command { } splice (@_, 3); $server->store (@args) or failure ($server->error, @_); + } elsif ($command eq 'update') { + check_args (2, 2, [], @args); + my $output = $server->update (@args); + if (defined $output) { + print $output; + } else { + failure ($server->error, @_); + } } else { error "unknown command $command"; } @@ -611,6 +619,14 @@ Stores for the object identified by and for later retrieval with C. Not all object types support this. If is not given as an argument, it will be read from standard input. +=item update + +Prints to standard output the data associated with the object identified +by and . If the object is one that can have changing +information, such as a keytab or password, then we generate new data for +that object regardless of whether there is current data or the unchanging +flag is set. + =back =head1 ATTRIBUTES -- cgit v1.2.3 From f3da929ff35f24163ab56dbd9615ee6a1c5336d8 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Wed, 18 Feb 2015 21:48:56 -0800 Subject: wallet-summary: Fixed wrong name in a function call Change-Id: I4bcc9c318ab3ec09add026e14204d929125302b7 --- contrib/wallet-summary | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/wallet-summary b/contrib/wallet-summary index 5cbf6e0..ba224d0 100755 --- a/contrib/wallet-summary +++ b/contrib/wallet-summary @@ -146,7 +146,7 @@ if ($mail) { } # Run the report. -my @principals = read_dump (); +my @principals = list_keytabs (); report_principals (@principals); # If -m was given, take the saved report and mail it as well. -- cgit v1.2.3 From 45a7c9d2896cf2e0d1548fd98b3b78f9f812744f Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Thu, 16 Apr 2015 14:58:58 -0700 Subject: wallet-report: Added report of all host-based objects for host "wallet-report objects host " reports on all objects that belong to the given host. This can be used to query things for retiring systems. Change-Id: Ib1c8e5978fed141d54ecc8504b56b43c037f9b17 --- perl/lib/Wallet/Config.pm | 28 +++++++++++++++ perl/lib/Wallet/Policy/Stanford.pm | 49 +++++++++++++++++--------- perl/lib/Wallet/Report.pm | 70 +++++++++++++++++++++++++++++++++++++- perl/t/general/report.t | 18 +++++++++- perl/t/policy/stanford.t | 28 +++++++++++++-- server/wallet-report | 3 ++ 6 files changed, 176 insertions(+), 20 deletions(-) diff --git a/perl/lib/Wallet/Config.pm b/perl/lib/Wallet/Config.pm index 76c7ecd..b3e1931 100644 --- a/perl/lib/Wallet/Config.pm +++ b/perl/lib/Wallet/Config.pm @@ -792,6 +792,34 @@ keytab objects for particular principals have fully-qualified hostnames: Objects that aren't of type C or which aren't for a host-based key have no naming requirements enforced by this example. +=head1 OBJECT HOST-BASED NAMES + +The above demonstrates having a host-based naming convention, where we +expect one part of an object name to be the name of the host that this +object is for. The most obvious examples are those keytab objects +above, where we want certain keytab names to be in the form of +/. It's then also useful to provide a Perl function +named is_for_host which then can be used to tell if a given object is a +host-based keytab for a specific host. This function is then called by +the objects_hostname in Wallet::Report to give a list of all host-based +objects for a given hostname. It should return true if the given object +is a host-based object for the hostname, otherwise false. + +An example that matches the same policy as the last verify_name example +would be: + + sub is_for_host { + my ($type, $name, $hostname) = @_; + my %host_based = map { $_ => 1 } + qw(HTTP cifs host imap ldap nfs pop sieve smtp webauth); + return 0 unless $type eq 'keytab'; + return 0 unless $name =~ m%/%; + my ($service, $instance) = split ('/', $name, 2); + return 0 unless $host_based{$service}; + return 1 if $hostname eq $instance; + return 0; + } + =head1 ACL NAMING ENFORCEMENT Similar to object names, by default wallet permits administrators to diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm index 362f098..86e204e 100644 --- a/perl/lib/Wallet/Policy/Stanford.pm +++ b/perl/lib/Wallet/Policy/Stanford.pm @@ -25,8 +25,8 @@ our (@EXPORT_OK, $VERSION); # 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); + $VERSION = '1.01'; + @EXPORT_OK = qw(default_owner verify_name is_for_host); } ############################################################################## @@ -87,6 +87,18 @@ our %PASSWORD_TYPE = ( 'service' => { extra => 1, need_extra => 1 }, ); +# Mappings that let us determine the host for a host-based object, if any. +our %HOST_FOR = ( + 'keytab' => \&_host_for_keytab, + 'file' => \&_host_for_file, + 'password' => \&_host_for_password, + 'duo' => \&_host_for_duo, + 'duo-pam' => \&_host_for_duo, + 'duo-radius' => \&_host_for_duo, + 'duo-ldap' => \&_host_for_duo, + 'duo-rdp' => \&_host_for_duo, +); + # 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); @@ -204,6 +216,23 @@ sub _host_for_duo { return $name; } +# Take a object type and name, along with a host name, and use these to +# decide if the given object is host-based and matches the given host. +sub is_for_host { + my ($type, $name, $host) = @_; + + # If we have a possible host mapping, get the host and see if it matches. + if (defined($HOST_FOR{$type})) { + my $object_host = $HOST_FOR{$type}->($name); + return 0 unless $object_host; + if ($host eq $object_host) { + return 1; + } + } + + return 0; +} + # 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 @@ -211,21 +240,9 @@ sub _host_for_duo { 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, - 'password' => \&_host_for_password, - 'duo' => \&_host_for_duo, - 'duo-pam' => \&_host_for_duo, - 'duo-radius' => \&_host_for_duo, - 'duo-ldap' => \&_host_for_duo, - 'duo-rdp' => \&_host_for_duo, - ); - # 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 (defined($HOST_FOR{$type})) { + my $host = $HOST_FOR{$type}->($name); if ($host) { my $acl_name = "host/$host"; my @acl; diff --git a/perl/lib/Wallet/Report.pm b/perl/lib/Wallet/Report.pm index 4d92d64..fc7bb4d 100644 --- a/perl/lib/Wallet/Report.pm +++ b/perl/lib/Wallet/Report.pm @@ -249,7 +249,7 @@ sub objects { # 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_history { - my ($self, $type, @args) = @_; + my ($self, $search_type, @args) = @_; undef $self->{error}; # All fields in the order we want to see them. @@ -284,6 +284,56 @@ sub objects_history { return @objects; } +# 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_hostname { + my ($self, $type, $hostname) = @_; + undef $self->{error}; + + # Make sure we have a given hostname. + if (!$hostname) { + $self->error ("object hosts requires one argument to search"); + return; + } + + # If we don't have a way to get host-based object lists, quit. + unless (defined &Wallet::Config::is_for_host) { + $self->error ('no host-based policy defined'); + return; + } + + # Search on all objects. + my %search = (); + my %options = (order_by => [ qw/ob_type ob_name/ ], + select => [ qw/ob_type ob_name/ ]); + + my @objects; + my $schema = $self->{schema}; + eval { + my @objects_rs = $schema->resultset('Object')->search (\%search, + \%options); + + # Check to see if an object is for the given host and add to list if + # so. + for my $object_rs (@objects_rs) { + my $type = $object_rs->ob_type; + my $name = $object_rs->ob_name; + next unless &Wallet::Config::is_for_host($type, $name, $hostname); + push (@objects, [ $type, $name ]); + } + }; + if ($@) { + $self->error ("cannot list objects: $@"); + return; + } + + return @objects; +} + ############################################################################## # Type reports ############################################################################## @@ -753,6 +803,24 @@ 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 objects_history(TYPE) + +Returns a dump of the entire object history table. The return value is +a list of references to each field in that table, in the following order: + + oh_on, oh_by, oh_type, oh_name, oh_action, oh_from + +=item objects_hostname(TYPE, HOSTNAME) + +Returns a list of all host-based objects for a given hostname. The +output is identical to the general objects command, but we need to +separate this out because the way it searches is very different. + +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 diff --git a/perl/t/general/report.t b/perl/t/general/report.t index a63ab79..170fe29 100755 --- a/perl/t/general/report.t +++ b/perl/t/general/report.t @@ -11,7 +11,7 @@ use strict; use warnings; -use Test::More tests => 215; +use Test::More tests => 218; use Wallet::Admin; use Wallet::Report; @@ -281,6 +281,22 @@ is (scalar (@lines), 1, 'Searching for ACL naming violations finds one'); is ($lines[0][0], 3, ' and the first has the right ID'); is ($lines[0][1], 'second', ' and the right name'); +# Set a host-based object matching script so that we can test the host report. +# The deactivation trick isn't needed here. +package Wallet::Config; +sub is_for_host { + my ($type, $name, $host) = @_; + my ($service, $principal) = split ('/', $name, 2); + return 0 unless $service && $principal; + return 1 if $host eq $principal; + return 0; +} +package main; +@lines = $report->objects_hostname ('host', 'admin'); +is (scalar (@lines), 1, 'Searching for host-based objects finds one'); +is ($lines[0][0], 'base', ' and the first has the right type'); +is ($lines[0][1], 'service/admin', ' and the right name'); + # Set up a file bucket so that we can create an object we can retrieve. system ('rm -rf test-files') == 0 or die "cannot remove test-files\n"; mkdir 'test-files' or die "cannot create test-files: $!\n"; diff --git a/perl/t/policy/stanford.t b/perl/t/policy/stanford.t index 555086c..9ed0fa6 100755 --- a/perl/t/policy/stanford.t +++ b/perl/t/policy/stanford.t @@ -16,7 +16,7 @@ use 5.008; use strict; use warnings; -use Test::More tests => 101; +use Test::More tests => 130; use lib 't/lib'; use Util; @@ -24,7 +24,8 @@ use Util; # Load the naming policy module. BEGIN { use_ok('Wallet::Admin'); - use_ok('Wallet::Policy::Stanford', qw(default_owner verify_name)); + use_ok('Wallet::Policy::Stanford', + qw(default_owner verify_name is_for_host)); use_ok('Wallet::Server'); } @@ -101,6 +102,29 @@ for my $name (@INVALID_FILES) { isnt(verify_name('file', $name), undef, "Invalid file $name"); } +# Now test a few cases for checking to see if a file is host-based. We don't +# test the legacy examples because they're more complicated and less obvious. +for my $name (@VALID_KEYTABS) { + my $hostname = 'example.stanford.edu'; + if ($name =~ m{\b$hostname\b}) { + is(is_for_host('keytab', $name, $hostname), 1, + "Keytab $name belongs to $hostname"); + } else { + is(is_for_host('keytab', $name, $hostname), 0, + "Keytab $name doesn't belong to $hostname"); + } +} +for my $name (@VALID_FILES) { + my $hostname = 'example.stanford.edu'; + if ($name =~ m{\b$hostname\b}) { + is(is_for_host('file', $name, $hostname), 1, + "File $name belongs to $hostname"); + } else { + is(is_for_host('file', $name, $hostname), 0, + "File $name doesn't belong to $hostname"); + } +} + # Now we need an actual database. Use Wallet::Admin to set it up. db_setup; my $setup = eval { Wallet::Admin->new }; diff --git a/server/wallet-report b/server/wallet-report index 6d1436c..77a2f8a 100755 --- a/server/wallet-report +++ b/server/wallet-report @@ -24,6 +24,7 @@ Wallet reporting help: objects acl Objects granting permissions to that ACL objects flag Objects with that flag set objects history History of all objects + objects host All host-based objects for a specific host objects owner Objects owned by that owner objects type Objects of that type objects unused Objects that have never been gotten @@ -81,6 +82,8 @@ sub command { my @objects; if (@args && $args[0] eq 'history') { @objects = $report->objects_history (@args); + } elsif (@args && $args[0] eq 'host') { + @objects = $report->objects_hostname (@args); } else { @objects = $report->objects (@args); } -- cgit v1.2.3 From 0f943b75d34623b6825a0acf34ee2cd965bc6799 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Fri, 17 Apr 2015 13:30:45 -0700 Subject: ACL.pm: Fixed capitalization of ACL in pod Change-Id: I9e4632f3ff81f916f9157ef8128b20915ecded08 --- perl/lib/Wallet/ACL.pm | 2 +- perl/lib/Wallet/Object/Duo/RDP.pm | 204 -------------------------------------- 2 files changed, 1 insertion(+), 205 deletions(-) delete mode 100644 perl/lib/Wallet/Object/Duo/RDP.pm diff --git a/perl/lib/Wallet/ACL.pm b/perl/lib/Wallet/ACL.pm index 370df8b..a090256 100644 --- a/perl/lib/Wallet/ACL.pm +++ b/perl/lib/Wallet/ACL.pm @@ -673,7 +673,7 @@ Note that rename() operations are not logged in the ACL history. =item replace(ID) Replace this ACL with another. This goes through each object owned by -the ACL and changes its ownership to the new ACL, leaving this acl owning +the ACL and changes its ownership to the new ACL, leaving this ACL owning nothing (and probably then needing to be deleted). Returns true on success and false on failure. On failure, the caller should call error() to get the error message. diff --git a/perl/lib/Wallet/Object/Duo/RDP.pm b/perl/lib/Wallet/Object/Duo/RDP.pm deleted file mode 100644 index c74661c..0000000 --- a/perl/lib/Wallet/Object/Duo/RDP.pm +++ /dev/null @@ -1,204 +0,0 @@ -# Wallet::Object::Duo::RDP -- Duo RDP int. object implementation for wallet -# -# Written by Russ Allbery -# Jon Robertson -# Copyright 2014 -# The Board of Trustees of the Leland Stanford Junior University -# -# See LICENSE for licensing terms. - -############################################################################## -# Modules and declarations -############################################################################## - -package Wallet::Object::Duo::RDP; -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::Duo; - -@ISA = qw(Wallet::Object::Duo); - -# 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 create to provide the specific Duo integration type that will be -# used in the remote Duo record. -sub create { - my ($class, $type, $name, $schema, $creator, $host, $time) = @_; - - $time ||= time; - my $self = $class->SUPER::create ($type, $name, $schema, $creator, $host, - $time, 'rdp'); - return $self; -} - -# Override get to output the data in a specific format used by Duo's RDP -# 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)->relaxed (1); - my $config = $json->decode (scalar slurp $Wallet::Config::DUO_KEY_FILE); - - # Construct the returned file. - my $output; - $output .= "Integration key: $key\n"; - $output .= 'Secret key: ' . $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 RDP - -=head1 NAME - -Wallet::Object::Duo::RDP - Duo RDP int. object implementation for wallet - -=head1 SYNOPSIS - - my @name = qw(duo-rdp host.example.com); - my @trace = ($user, $host, time); - my $object = Wallet::Object::Duo::RDP->create (@name, $schema, @trace); - my $config = $object->get (@trace); - $object->destroy (@trace); - -=head1 DESCRIPTION - -Wallet::Object::Duo::RDP is a representation of Duo integrations with -the wallet, specifically to output Duo integrations to set up an RDP -integration. This can be used to set up remote logins, or all Windows -logins period if so selected in Duo's software. 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. - -Because the Duo RDP software is configured by a GUI, the information -returned for a get operation is a simple set that's readable but not -useful for directly plugging into a config file. The values would need -to be cut and pasted into the GUI. - -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 for details on supported configuration parameters and -information about how to set wallet configuration. - -=head1 METHODS - -This object mostly inherits from Wallet::Object::Duo. 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 will override the Wallet::Object::Duo class with the information -needed to create a specific integration type in Duo. It creates a new -object with the given TYPE and NAME (TYPE is normally C 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. - -If create() fails, it throws an exception. - -=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: - - Integration key: - Secret key: - Host: - -The C 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. - -=head1 SEE ALSO - -Net::Duo(3), Wallet::Config(3), Wallet::Object::Duo(3), wallet-backend(8) - -This module is part of the wallet system. The current version is -available from L. - -=head1 AUTHORS - -Russ Allbery -Jon Robertson - -=cut -- cgit v1.2.3 From feacbd7d685b1790579f949b3e72a48412835d92 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Fri, 17 Apr 2015 13:41:52 -0700 Subject: Merged all Duo objects into one module To handle local proliferation of Duo integration type requests, all Duo types have been merged into one module that will pick up and decide integration specifics off of the object type. If you are using the Duo types locally already, you'll want to load perl/sql/wallet-1.3-update-duo.sql to your database to update the old object types to all use the Duo module. All existing Duo integrations have been added to the module for handling, but nothing new has been added to the wallet object types. Since there are a lot of Duo integrations, sites should only manually add the ones they're interested in to the wallet types table. Change-Id: If9c9a0a3e77923354f31d8f9c98a519c93df200b --- perl/lib/Wallet/Admin.pm | 8 +- perl/lib/Wallet/Object/Duo.pm | 121 ++++++++++++++++-- perl/lib/Wallet/Object/Duo/LDAPProxy.pm | 202 ----------------------------- perl/lib/Wallet/Object/Duo/PAM.pm | 205 ------------------------------ perl/lib/Wallet/Object/Duo/RadiusProxy.pm | 204 ----------------------------- perl/sql/wallet-1.3-update-duo.sql | 9 ++ perl/t/object/duo-ldap.t | 21 ++- perl/t/object/duo-pam.t | 20 ++- perl/t/object/duo-radius.t | 21 ++- perl/t/object/duo-rdp.t | 20 ++- 10 files changed, 162 insertions(+), 669 deletions(-) delete mode 100644 perl/lib/Wallet/Object/Duo/LDAPProxy.pm delete mode 100644 perl/lib/Wallet/Object/Duo/PAM.pm delete mode 100644 perl/lib/Wallet/Object/Duo/RadiusProxy.pm create mode 100644 perl/sql/wallet-1.3-update-duo.sql diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm index a8b8368..b38cc94 100644 --- a/perl/lib/Wallet/Admin.pm +++ b/perl/lib/Wallet/Admin.pm @@ -126,10 +126,10 @@ sub default_data { # types default rows. my @record = ([ qw/ty_name ty_class/ ], [ 'duo', 'Wallet::Object::Duo' ], - [ 'duo-ldap', 'Wallet::Object::Duo::LDAPProxy' ], - [ 'duo-pam', 'Wallet::Object::Duo::PAM' ], - [ 'duo-radius', 'Wallet::Object::Duo::RadiusProxy' ], - [ 'duo-rdp', 'Wallet::Object::Duo::RDP' ], + [ 'duo-ldap', 'Wallet::Object::Duo' ], + [ 'duo-pam', 'Wallet::Object::Duo' ], + [ 'duo-radius', 'Wallet::Object::Duo' ], + [ 'duo-rdp', 'Wallet::Object::Duo' ], [ 'file', 'Wallet::Object::File' ], [ 'password', 'Wallet::Object::Password' ], [ 'keytab', 'Wallet::Object::Keytab' ], diff --git a/perl/lib/Wallet/Object/Duo.pm b/perl/lib/Wallet/Object/Duo.pm index d08294b..d0901de 100644 --- a/perl/lib/Wallet/Object/Duo.pm +++ b/perl/lib/Wallet/Object/Duo.pm @@ -29,7 +29,100 @@ use 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.02'; +$VERSION = '0.03'; + +# Mappings from our types into what Duo calls the integration types. +our %DUO_TYPES = ( + 'duo' => { + integration => 'unix', + output => \&_output_generic, + }, + 'duo-ldap' => { + integration => 'ldapproxy', + output => \&_output_ldap, + }, + 'duo-pam' => { + integration => 'unix', + output => \&_output_pam, + }, + 'duo-radius' => { + integration => 'radius', + output => \&_output_radius, + }, + ); + +# Extra types to add. These are all just named as the Duo integration name +# with duo- before it and go to the generic output. Put them here to prevent +# pages of settings. These are also not all actually set as types in the +# types table to prevent overpopulation. You should manually create the +# entries in that table for any Duo integrations you want to add. +our @EXTRA_TYPES = ('accountsapi', 'adfs', 'adminapi', 'array', 'barracuda', + 'cisco', 'citrixcag', 'citrixns', 'confluence', 'drupal', + 'f5bigip', 'f5firepass', 'fortinet', 'jira', 'juniper', + 'juniperuac', 'lastpass', 'okta', 'onelogin', 'openvpn', + 'openvpnas', 'owa', 'paloalto', 'rdgateway', 'rdp', + 'rdweb', 'rest', 'rras', 'shibboleth', 'sonicwallsra', + 'splunk', 'tmg', 'uag', 'verify', 'vmwareview', 'websdk', + 'wordpress'); +for my $type (@EXTRA_TYPES) { + my $wallet_type = 'duo-'.$type; + $DUO_TYPES{$wallet_type}{integration} = $type; + $DUO_TYPES{$wallet_type}{output} = \&_output_generic; +}; + +############################################################################## +# Get output methods +############################################################################## + +# Output for any miscellaneous Duo integration, usually those that use a GUI +# to set information and so don't need a custom configuration file. +sub _output_generic { + my ($key, $secret, $hostname) = @_; + + my $output; + $output .= "Integration key: $key\n"; + $output .= "Secret key: $secret\n"; + $output .= "Host: $hostname\n"; + + return $output; +} + +# Output for the Duo unix integration, which hooks into the PAM stack. +sub _output_pam { + my ($key, $secret, $hostname) = @_; + + my $output = "[duo]\n"; + $output .= "ikey = $key\n"; + $output .= "skey = $secret\n"; + $output .= "host = $hostname\n"; + + return $output; +} + +# Output for the radius proxy, which can be plugged into the proxy config. +sub _output_radius { + my ($key, $secret, $hostname) = @_; + + my $output = "[radius_server_challenge]\n"; + $output .= "ikey = $key\n"; + $output .= "skey = $secret\n"; + $output .= "api_host = $hostname\n"; + $output .= "client = radius_client\n"; + + return $output; +} + +# Output for the LDAP proxy, which can be plugged into the proxy config. +sub _output_ldap { + my ($key, $secret, $hostname) = @_; + + my $output = "[ldap_server_challenge]\n"; + $output .= "ikey = $key\n"; + $output .= "skey = $secret\n"; + $output .= "api_host = $hostname\n"; + + return $output; +} ############################################################################## # Core methods @@ -86,7 +179,7 @@ sub new { # great here since we don't have a way to communicate the error back to the # caller. sub create { - my ($class, $type, $name, $schema, $creator, $host, $time, $duo_type) = @_; + 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) { @@ -95,6 +188,12 @@ sub create { my $key_file = $Wallet::Config::DUO_KEY_FILE; my $agent = $Wallet::Config::DUO_AGENT; + # Make sure this is actually a type we know about, since this handler + # can handle many types. + if (!exists $DUO_TYPES{$type}) { + die "$type is not a valid duo integration\n"; + } + # Construct the Net::Duo::Admin object. require Net::Duo::Admin; my $duo = Net::Duo::Admin->new ( @@ -106,7 +205,7 @@ sub create { # Create the object in Duo. require Net::Duo::Admin::Integration; - $duo_type ||= $Wallet::Config::DUO_TYPE; + my $duo_type = $DUO_TYPES{$type}{integration}; my %data = ( name => "$name ($duo_type)", notes => 'Managed by wallet', @@ -201,11 +300,17 @@ sub get { my $json = JSON->new->utf8 (1)->relaxed (1); my $config = $json->decode (scalar slurp $Wallet::Config::DUO_KEY_FILE); - # Construct the returned file. - my $output; - $output .= "Integration key: $key\n"; - $output .= 'Secret key: ' . $integration->secret_key . "\n"; - $output .= "Host: $config->{api_hostname}\n"; + # Construct the returned file. Assume the generic handler in case there + # is no valid handler, though that shouldn't happen. + my $output_sub; + my $type = $self->{type}; + if (exists $DUO_TYPES{$type}{output}) { + $output_sub = $DUO_TYPES{$type}{output}; + } else { + $output_sub = \&_output_generic; + } + my $output = $output_sub->($key, $integration->secret_key, + $config->{api_hostname}); # Log the action and return. $self->log_action ('get', $user, $host, $time); diff --git a/perl/lib/Wallet/Object/Duo/LDAPProxy.pm b/perl/lib/Wallet/Object/Duo/LDAPProxy.pm deleted file mode 100644 index 23894ac..0000000 --- a/perl/lib/Wallet/Object/Duo/LDAPProxy.pm +++ /dev/null @@ -1,202 +0,0 @@ -# Wallet::Object::Duo::LDAPProxy -- Duo auth proxy integration for LDAP -# -# Written by Jon Robertson -# Copyright 2014 -# The Board of Trustees of the Leland Stanford Junior University -# -# See LICENSE for licensing terms. - -############################################################################## -# Modules and declarations -############################################################################## - -package Wallet::Object::Duo::LDAPProxy; -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::Duo; - -@ISA = qw(Wallet::Object::Duo); - -# 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 create to provide the specific Duo integration type that will be -# used in the remote Duo record. -sub create { - my ($class, $type, $name, $schema, $creator, $host, $time) = @_; - - $time ||= time; - my $self = $class->SUPER::create ($type, $name, $schema, $creator, $host, - $time, 'ldapproxy'); - return $self; -} - -# Override get to output the data in a specific format used for Duo LDAP -# integration -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)->relaxed (1); - my $config = $json->decode (scalar slurp $Wallet::Config::DUO_KEY_FILE); - - # Construct the returned file. - my $output = "[ldap_server_challenge]\n"; - $output .= "ikey = $key\n"; - $output .= 'skey = ' . $integration->secret_key . "\n"; - $output .= "api_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 LDAP auth - -=head1 NAME - -Wallet::Object::Duo::LDAPProxy - Duo auth proxy integration for LDAP - -=head1 SYNOPSIS - - my @name = qw(duo-ldap host.example.com); - my @trace = ($user, $host, time); - my $object = Wallet::Object::Duo::LDAPProxy->create (@name, $schema, @trace); - my $config = $object->get (@trace); - $object->destroy (@trace); - -=head1 DESCRIPTION - -Wallet::Object::Duo::LDAPProxy is a representation of Duo -integrations with the wallet, specifically to output Duo integrations -in a format that can easily be pulled into configuring the Duo -Authentication Proxy for LDAP. 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. - -The integration information is always returned in the configuration file -format expected by the Authentication Proxy for Duo in configuring it -for LDAP. - -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 for details on supported configuration parameters and -information about how to set wallet configuration. - -=head1 METHODS - -This object mostly inherits from Wallet::Object::Duo. 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 will override the Wallet::Object::Duo class with the information -needed to create a specific integration type in Duo. It creates a new -object with the given TYPE and NAME (TYPE is normally C 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. - -If create() fails, it throws an exception. - -=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: - - [ldap_server_challenge] - ikey = - skey = - api_host = - -The C 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. - -=head1 SEE ALSO - -Net::Duo(3), Wallet::Config(3), Wallet::Object::Duo(3), wallet-backend(8) - -This module is part of the wallet system. The current version is -available from L. - -=head1 AUTHORS - -Jon Robertson - -=cut diff --git a/perl/lib/Wallet/Object/Duo/PAM.pm b/perl/lib/Wallet/Object/Duo/PAM.pm deleted file mode 100644 index d9d17f8..0000000 --- a/perl/lib/Wallet/Object/Duo/PAM.pm +++ /dev/null @@ -1,205 +0,0 @@ -# Wallet::Object::Duo::PAM -- Duo PAM int. object implementation for wallet -# -# Written by Russ Allbery -# Jon Robertson -# Copyright 2014 -# The Board of Trustees of the Leland Stanford Junior University -# -# See LICENSE for licensing terms. - -############################################################################## -# Modules and declarations -############################################################################## - -package Wallet::Object::Duo::PAM; -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::Duo; - -@ISA = qw(Wallet::Object::Duo); - -# 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 create to provide the specific Duo integration type that will be -# used in the remote Duo record. -sub create { - my ($class, $type, $name, $schema, $creator, $host, $time) = @_; - - $time ||= time; - my $self = $class->SUPER::create ($type, $name, $schema, $creator, $host, - $time, 'unix'); - return $self; -} - -# Override get to output the data in a specific format used by Duo's 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)->relaxed (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::PAM - Duo PAM int. object implementation for wallet - -=head1 SYNOPSIS - - my @name = qw(duo-pam host.example.com); - my @trace = ($user, $host, time); - my $object = Wallet::Object::Duo::PAM->create (@name, $schema, @trace); - my $config = $object->get (@trace); - $object->destroy (@trace); - -=head1 DESCRIPTION - -Wallet::Object::Duo::PAM is a representation of Duo integrations with -the wallet, specifically to output Duo integrations in a format that -can easily be pulled into configuring the Duo PAM interface. 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. - -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 for details on supported configuration parameters and -information about how to set wallet configuration. - -=head1 METHODS - -This object mostly inherits from Wallet::Object::Duo. 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 will override the Wallet::Object::Duo class with the information -needed to create a specific integration type in Duo. It creates a new -object with the given TYPE and NAME (TYPE is normally C 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. - -If create() fails, it throws an exception. - -=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 = - skey = - host = - -The C 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. - -=head1 SEE ALSO - -Net::Duo(3), Wallet::Config(3), Wallet::Object::Duo(3), wallet-backend(8) - -This module is part of the wallet system. The current version is -available from L. - -=head1 AUTHORS - -Russ Allbery -Jon Robertson - -=cut diff --git a/perl/lib/Wallet/Object/Duo/RadiusProxy.pm b/perl/lib/Wallet/Object/Duo/RadiusProxy.pm deleted file mode 100644 index a1f6e24..0000000 --- a/perl/lib/Wallet/Object/Duo/RadiusProxy.pm +++ /dev/null @@ -1,204 +0,0 @@ -# Wallet::Object::Duo::RadiusProxy -- Duo auth proxy integration for radius -# -# Written by Jon Robertson -# Copyright 2014 -# The Board of Trustees of the Leland Stanford Junior University -# -# See LICENSE for licensing terms. - -############################################################################## -# Modules and declarations -############################################################################## - -package Wallet::Object::Duo::RadiusProxy; -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::Duo; - -@ISA = qw(Wallet::Object::Duo); - -# 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 create to provide the specific Duo integration type that will be -# used in the remote Duo record. -sub create { - my ($class, $type, $name, $schema, $creator, $host, $time) = @_; - - $time ||= time; - my $self = $class->SUPER::create ($type, $name, $schema, $creator, $host, - $time, 'radius'); - return $self; -} - -# Override get to output the data in a specific format used for Duo radius -# integration -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)->relaxed (1); - my $config = $json->decode (scalar slurp $Wallet::Config::DUO_KEY_FILE); - - # Construct the returned file. - my $output = "[radius_server_challenge]\n"; - $output .= "ikey = $key\n"; - $output .= 'skey = ' . $integration->secret_key . "\n"; - $output .= "api_host = $config->{api_hostname}\n"; - $output .= "client = radius_client\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 auth - -=head1 NAME - -Wallet::Object::Duo::RadiusProxy - Duo auth proxy integration for RADIUS - -=head1 SYNOPSIS - - my @name = qw(duo-radius host.example.com); - my @trace = ($user, $host, time); - my $object = Wallet::Object::Duo::RadiusProxy->create (@name, $schema, @trace); - my $config = $object->get (@trace); - $object->destroy (@trace); - -=head1 DESCRIPTION - -Wallet::Object::Duo::RadiusProxy is a representation of Duo -integrations with the wallet, specifically to output Duo integrations -in a format that can easily be pulled into configuring the Duo -Authentication Proxy for Radius. 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. - -The integration information is always returned in the configuration file -format expected by the Authentication Proxy for Duo in configuring it -for Radius. - -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 for details on supported configuration parameters and -information about how to set wallet configuration. - -=head1 METHODS - -This object mostly inherits from Wallet::Object::Duo. 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 will override the Wallet::Object::Duo class with the information -needed to create a specific integration type in Duo. It creates a new -object with the given TYPE and NAME (TYPE is normally C 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. - -If create() fails, it throws an exception. - -=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: - - [radius_server_challenge] - ikey = - skey = - api_host = - client = radius_client - -The C 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. - -=head1 SEE ALSO - -Net::Duo(3), Wallet::Config(3), Wallet::Object::Duo(3), wallet-backend(8) - -This module is part of the wallet system. The current version is -available from L. - -=head1 AUTHORS - -Jon Robertson - -=cut diff --git a/perl/sql/wallet-1.3-update-duo.sql b/perl/sql/wallet-1.3-update-duo.sql new file mode 100644 index 0000000..affadcd --- /dev/null +++ b/perl/sql/wallet-1.3-update-duo.sql @@ -0,0 +1,9 @@ +-- +-- Run on installing wallet 1.3 in order to update what the Duo types +-- point to for modules. +-- + +UPDATE types set ty_class='Wallet::Object::Duo' where ty_name='duo-ldap'; +UPDATE types set ty_class='Wallet::Object::Duo' where ty_name='duo-pam'; +UPDATE types set ty_class='Wallet::Object::Duo' where ty_name='duo-radius'; +UPDATE types set ty_class='Wallet::Object::Duo' where ty_name='duo-rdp'; diff --git a/perl/t/object/duo-ldap.t b/perl/t/object/duo-ldap.t index 3648eba..8a00dbb 100644 --- a/perl/t/object/duo-ldap.t +++ b/perl/t/object/duo-ldap.t @@ -26,7 +26,7 @@ BEGIN { BEGIN { use_ok('Wallet::Admin'); use_ok('Wallet::Config'); - use_ok('Wallet::Object::Duo::LDAPProxy'); + use_ok('Wallet::Object::Duo'); } use lib 't/lib'; @@ -53,15 +53,14 @@ my $mock = Net::Duo::Mock::Agent->new ({ key_file => 't/data/duo/keys.json' }); # Test error handling in the absence of configuration. my $object = eval { - Wallet::Object::Duo::LDAPProxy->new ('duo-ldap', 'test', $schema); + Wallet::Object::Duo->new ('duo-ldap', 'test', $schema); }; -is ($object, undef, 'Wallet::Object::Duo::LDAPProxy new with no config failed'); +is ($object, undef, 'Wallet::Object::Duo new with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); $object = eval { - Wallet::Object::Duo::LDAPProxy->create ('duo-ldap', 'test', $schema, - @trace); + Wallet::Object::Duo->create ('duo-ldap', 'test', $schema, @trace); }; -is ($object, undef, 'Wallet::Object::Duo::LDAPProxy creation with no config failed'); +is ($object, undef, 'Wallet::Object::Duo creation with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); # Set up the Duo configuration. @@ -83,9 +82,8 @@ $mock->expect ( response_file => 't/data/duo/integration.json', } ); -$object = Wallet::Object::Duo::LDAPProxy->create ('duo-ldap', 'test', $schema, - @trace); -isa_ok ($object, 'Wallet::Object::Duo::LDAPProxy'); +$object = Wallet::Object::Duo->create ('duo-ldap', 'test', $schema, @trace); +isa_ok ($object, 'Wallet::Object::Duo'); # Check the metadata about the new wallet object. $expected = <<"EOO"; @@ -127,7 +125,7 @@ is ($object->flag_clear ('locked', @trace), 1, '...and clearing locked flag works'); # Create a new object by wallet type and name. -$object = Wallet::Object::Duo::LDAPProxy->new ('duo-ldap', 'test', $schema); +$object = Wallet::Object::Duo->new ('duo-ldap', 'test', $schema); # Test deleting an integration. We can't test this entirely properly because # currently Net::Duo::Mock::Agent doesn't support stacking multiple expected @@ -144,8 +142,7 @@ TODO: { local $TODO = 'Net::Duo::Mock::Agent not yet capable'; is ($object->destroy (@trace), 1, 'Duo object deletion succeeded'); - $object = eval { Wallet::Object::Duo::LDAPProxy->new ('duo-ldap', 'test', - $schema) }; + $object = eval { Wallet::Object::Duo->new ('duo-ldap', 'test', $schema) }; is ($object, undef, '...and now object cannot be retrieved'); is ($@, "cannot find duo:test\n", '...with correct error'); } diff --git a/perl/t/object/duo-pam.t b/perl/t/object/duo-pam.t index 7b88787..047343e 100644 --- a/perl/t/object/duo-pam.t +++ b/perl/t/object/duo-pam.t @@ -26,7 +26,7 @@ BEGIN { BEGIN { use_ok('Wallet::Admin'); use_ok('Wallet::Config'); - use_ok('Wallet::Object::Duo::PAM'); + use_ok('Wallet::Object::Duo'); } use lib 't/lib'; @@ -53,14 +53,14 @@ my $mock = Net::Duo::Mock::Agent->new ({ key_file => 't/data/duo/keys.json' }); # Test error handling in the absence of configuration. my $object = eval { - Wallet::Object::Duo::PAM->new ('duo-pam', 'test', $schema); + Wallet::Object::Duo->new ('duo-pam', 'test', $schema); }; -is ($object, undef, 'Wallet::Object::Duo::PAM new with no config failed'); +is ($object, undef, 'Wallet::Object::Duo new with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); $object = eval { - Wallet::Object::Duo::PAM->create ('duo-pam', 'test', $schema, @trace); + Wallet::Object::Duo->create ('duo-pam', 'test', $schema, @trace); }; -is ($object, undef, 'Wallet::Object::Duo::PAM creation with no config failed'); +is ($object, undef, 'Wallet::Object::Duo creation with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); # Set up the Duo configuration. @@ -82,9 +82,8 @@ $mock->expect ( response_file => 't/data/duo/integration.json', } ); -$object = Wallet::Object::Duo::PAM->create ('duo-pam', 'test', $schema, - @trace); -isa_ok ($object, 'Wallet::Object::Duo::PAM'); +$object = Wallet::Object::Duo->create ('duo-pam', 'test', $schema, @trace); +isa_ok ($object, 'Wallet::Object::Duo'); # Check the metadata about the new wallet object. $expected = <<"EOO"; @@ -126,7 +125,7 @@ is ($object->flag_clear ('locked', @trace), 1, '...and clearing locked flag works'); # Create a new object by wallet type and name. -$object = Wallet::Object::Duo::PAM->new ('duo-pam', 'test', $schema); +$object = Wallet::Object::Duo->new ('duo-pam', 'test', $schema); # Test deleting an integration. We can't test this entirely properly because # currently Net::Duo::Mock::Agent doesn't support stacking multiple expected @@ -143,8 +142,7 @@ TODO: { local $TODO = 'Net::Duo::Mock::Agent not yet capable'; is ($object->destroy (@trace), 1, 'Duo object deletion succeeded'); - $object = eval { Wallet::Object::Duo::PAM->new ('duo-pam', 'test', - $schema) }; + $object = eval { Wallet::Object::Duo->new ('duo-pam', 'test', $schema) }; is ($object, undef, '...and now object cannot be retrieved'); is ($@, "cannot find duo:test\n", '...with correct error'); } diff --git a/perl/t/object/duo-radius.t b/perl/t/object/duo-radius.t index f258518..55cbb9d 100644 --- a/perl/t/object/duo-radius.t +++ b/perl/t/object/duo-radius.t @@ -26,7 +26,7 @@ BEGIN { BEGIN { use_ok('Wallet::Admin'); use_ok('Wallet::Config'); - use_ok('Wallet::Object::Duo::RadiusProxy'); + use_ok('Wallet::Object::Duo'); } use lib 't/lib'; @@ -53,17 +53,16 @@ my $mock = Net::Duo::Mock::Agent->new ({ key_file => 't/data/duo/keys.json' }); # Test error handling in the absence of configuration. my $object = eval { - Wallet::Object::Duo::RadiusProxy->new ('duo-raduys', 'test', $schema); + Wallet::Object::Duo->new ('duo-radius', 'test', $schema); }; is ($object, undef, - 'Wallet::Object::Duo::RadiusProxy new with no config failed'); + 'Wallet::Object::Duo new with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); $object = eval { - Wallet::Object::Duo::RadiusProxy->create ('duo-radius', 'test', $schema, - @trace); + Wallet::Object::Duo->create ('duo-radius', 'test', $schema, @trace); }; is ($object, undef, - 'Wallet::Object::Duo::RadiusProxy creation with no config failed'); + 'Wallet::Object::Duo creation with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); # Set up the Duo configuration. @@ -85,9 +84,8 @@ $mock->expect ( response_file => 't/data/duo/integration-radius.json', } ); -$object = Wallet::Object::Duo::RadiusProxy->create ('duo-radius', 'test', - $schema, @trace); -isa_ok ($object, 'Wallet::Object::Duo::RadiusProxy'); +$object = Wallet::Object::Duo->create ('duo-radius', 'test', $schema, @trace); +isa_ok ($object, 'Wallet::Object::Duo'); # Check the metadata about the new wallet object. $expected = <<"EOO"; @@ -130,8 +128,7 @@ is ($object->flag_clear ('locked', @trace), 1, '...and clearing locked flag works'); # Create a new object by wallet type and name. -$object = Wallet::Object::Duo::RadiusProxy->new ('duo-radius', 'test', - $schema); +$object = Wallet::Object::Duo->new ('duo-radius', 'test', $schema); # Test deleting an integration. We can't test this entirely properly because # currently Net::Duo::Mock::Agent doesn't support stacking multiple expected @@ -149,7 +146,7 @@ TODO: { is ($object->destroy (@trace), 1, 'Duo object deletion succeeded'); $object = eval { - Wallet::Object::Duo::RadiusProxy->new ('duo-radius', 'test', $schema); + Wallet::Object::Duo->new ('duo-radius', 'test', $schema); }; is ($object, undef, '...and now object cannot be retrieved'); is ($@, "cannot find duo:test\n", '...with correct error'); diff --git a/perl/t/object/duo-rdp.t b/perl/t/object/duo-rdp.t index 9b2d566..25060ac 100644 --- a/perl/t/object/duo-rdp.t +++ b/perl/t/object/duo-rdp.t @@ -26,7 +26,7 @@ BEGIN { BEGIN { use_ok('Wallet::Admin'); use_ok('Wallet::Config'); - use_ok('Wallet::Object::Duo::RDP'); + use_ok('Wallet::Object::Duo'); } use lib 't/lib'; @@ -53,14 +53,14 @@ my $mock = Net::Duo::Mock::Agent->new ({ key_file => 't/data/duo/keys.json' }); # Test error handling in the absence of configuration. my $object = eval { - Wallet::Object::Duo::RDP->new ('duo-rdp', 'test', $schema); + Wallet::Object::Duo->new ('duo-rdp', 'test', $schema); }; -is ($object, undef, 'Wallet::Object::Duo::RDP new with no config failed'); +is ($object, undef, 'Wallet::Object::Duo new with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); $object = eval { - Wallet::Object::Duo::RDP->create ('duo-rdp', 'test', $schema, @trace); + Wallet::Object::Duo->create ('duo-rdp', 'test', $schema, @trace); }; -is ($object, undef, 'Wallet::Object::Duo::RDP creation with no config failed'); +is ($object, undef, 'Wallet::Object::Duo creation with no config failed'); is ($@, "duo object implementation not configured\n", '...with correct error'); # Set up the Duo configuration. @@ -82,9 +82,8 @@ $mock->expect ( response_file => 't/data/duo/integration-rdp.json', } ); -$object = Wallet::Object::Duo::RDP->create ('duo-rdp', 'test', $schema, - @trace); -isa_ok ($object, 'Wallet::Object::Duo::RDP'); +$object = Wallet::Object::Duo->create ('duo-rdp', 'test', $schema, @trace); +isa_ok ($object, 'Wallet::Object::Duo'); # Check the metadata about the new wallet object. $expected = <<"EOO"; @@ -125,7 +124,7 @@ is ($object->flag_clear ('locked', @trace), 1, '...and clearing locked flag works'); # Create a new object by wallet type and name. -$object = Wallet::Object::Duo::RDP->new ('duo-rdp', 'test', $schema); +$object = Wallet::Object::Duo->new ('duo-rdp', 'test', $schema); # Test deleting an integration. We can't test this entirely properly because # currently Net::Duo::Mock::Agent doesn't support stacking multiple expected @@ -142,8 +141,7 @@ TODO: { local $TODO = 'Net::Duo::Mock::Agent not yet capable'; is ($object->destroy (@trace), 1, 'Duo object deletion succeeded'); - $object = eval { Wallet::Object::Duo::RDP->new ('duo-rdp', 'test', - $schema) }; + $object = eval { Wallet::Object::Duo->new ('duo-rdp', 'test', $schema) }; is ($object, undef, '...and now object cannot be retrieved'); is ($@, "cannot find duo:test\n", '...with correct error'); } -- cgit v1.2.3 From 2e3c457d3b0ae813c00183334e80b9bd92ab6c90 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Fri, 8 May 2015 14:36:46 -0700 Subject: wallet-contacts: Added basic email sending to the contacts The email sending will only replace the To: field with the contacts and do no other template parsing, so it is currently limited. Change-Id: I4c653cf7bfe3ed2d9ca16299a4f937e015966554 --- contrib/wallet-contacts | 72 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/contrib/wallet-contacts b/contrib/wallet-contacts index 2799db3..ce16ab1 100755 --- a/contrib/wallet-contacts +++ b/contrib/wallet-contacts @@ -3,7 +3,7 @@ # wallet-contacts -- Report contact addresses for matching wallet objects. # # Written by Russ Allbery -# Copyright 2009 +# Copyright 2009, 2015 # The Board of Trustees of the Leland Stanford Junior University # # See LICENSE for licensing terms. @@ -12,16 +12,32 @@ # Modules and declarations ############################################################################## -require 5.006; - +use 5.010; +use autodie; use strict; +use warnings; use Getopt::Long qw(GetOptions); -use Wallet::Admin (); +use Perl6::Slurp; +use Wallet::Report (); # Used to cache lookups of e-mail addresses by identifiers. our %EMAIL; +############################################################################## +# Mail sending +############################################################################## + +# Given a message, mail it through sendmail. +sub mail { + my ($message) = @_; + + open (MAIL, '| /usr/sbin/sendmail -t -oi -oem') + or die "$0: cannot fork sendmail: $!\n"; + print MAIL $message; + close (MAIL); +} + ############################################################################## # whois lookups ############################################################################## @@ -79,9 +95,12 @@ sub whois_lookup { ############################################################################## # Read in command-line options. -my ($help); +my ($help, $mail, $dryrun); Getopt::Long::config ('no_ignore_case', 'bundling'); -GetOptions ('help|h' => \$help) or exit 1; +GetOptions ('help|h' => \$help, + 'mail=s' => \$mail, + 'dryrun' => \$dryrun, + ) or exit 1; if ($help) { print "Feeding myself to perldoc, please wait....\n"; exec ('perldoc', '-t', $0); @@ -95,10 +114,10 @@ if (@ARGV > 2 or not defined $name) { $0 =~ s%.*/%%; # Gather the list of ACL lines. -my $admin = Wallet::Admin->new; -my @lines = $admin->report_owners ($type, $name); -if (!@lines and $admin->error) { - die $admin->error, "\n"; +my $report = Wallet::Report->new; +my @lines = $report->owners ($type, $name); +if (!@lines and $report->error) { + die $report->error, "\n"; } # Now, for each line, turn it into an e-mail address. krb5 ACLs go as-is if @@ -127,10 +146,28 @@ for (@lines) { } # We now have a list of e-mail addresses. De-duplicate and then print them -# out. +# out or mail to them. my %seen; @email = grep { !$seen{$_}++ } sort @email; -print join ("\n", @email, ''); +if ($mail) { + if (!-e $mail) { + die "mail file $mail does not exist!\n"; + } + + # Load the message and set the To header. + my $message = slurp($mail); + my $mailto = join (', ', @email); + $message =~ s{^To:.*$}{To: $mailto}m; + + if ($dryrun) { + print $message; + } else { + mail ($message); + } + +} else { + print join ("\n", @email, ''); +} ############################################################################## # Documentation @@ -181,6 +218,17 @@ e-mail address for an administrator or user, it will warn but continue. Print out this documentation (which is done simply by feeding the script to C). +=item B<-mail>= + +Takes a given email message file, replaces the contents of the To: line +with the contacts found, and sends out that mail. This can be used for +simple notifications that have no template requirements. + +=item B<-dryrun> + +If --mail has been set, only print to the screen rather than actually +sending mail. Does nothing if --mail is not set. + =back =head1 CAVEATS -- cgit v1.2.3 From 626d3ee2b94384a4ffe95d5e8a907f359ff7cbfb Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Thu, 4 Jun 2015 10:56:30 -0700 Subject: ACL.pm: Error messages use name rather than ID All error messages should now use the ACL name rather than the ADL id, for readability. Change-Id: I2d1cfe806b459ef083293df4fa0b83cb4cef673b --- NEWS | 2 ++ perl/lib/Wallet/ACL.pm | 18 +++++++++--------- perl/t/general/acl.t | 6 +++--- perl/t/general/server.t | 10 +++++----- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/NEWS b/NEWS index 664da05..9e124e9 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,8 @@ wallet 1.3 (xxxx-xx-xx) Added an acl replace command, to change all objects owned by one ACL to be owned by another. + All ACL operations now refer to the ACL by name rather than ID. + Added a report for unstored objects to wallet-report, and cleaned up the help for the existing unused report that implied it showed unstored as well as unused. diff --git a/perl/lib/Wallet/ACL.pm b/perl/lib/Wallet/ACL.pm index a090256..260ff22 100644 --- a/perl/lib/Wallet/ACL.pm +++ b/perl/lib/Wallet/ACL.pm @@ -1,7 +1,7 @@ # Wallet::ACL -- Implementation of ACLs in the wallet system. # # Written by Russ Allbery -# Copyright 2007, 2008, 2010, 2013, 2014 +# Copyright 2007, 2008, 2010, 2013, 2014, 2015 # The Board of Trustees of the Leland Stanford Junior University # # See LICENSE for licensing terms. @@ -24,7 +24,7 @@ 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'; +$VERSION = '0.09'; ############################################################################## # Constructors @@ -201,7 +201,7 @@ sub rename { $guard->commit; }; if ($@) { - $self->error ("cannot rename ACL $self->{id} to $name: $@"); + $self->error ("cannot rename ACL $self->{name} to $name: $@"); return; } $self->{name} = $name; @@ -228,7 +228,7 @@ sub replace { $object->owner ($replace_id, $user, $host, $time); } } else { - $self->error ("no objects found for ACL $self->{id}"); + $self->error ("no objects found for ACL $self->{name}"); return; } return 1; @@ -284,7 +284,7 @@ sub destroy { $guard->commit; }; if ($@) { - $self->error ("cannot destroy ACL $self->{id}: $@"); + $self->error ("cannot destroy ACL $self->{name}: $@"); return; } return 1; @@ -312,7 +312,7 @@ sub add { $guard->commit; }; if ($@) { - $self->error ("cannot add $scheme:$identifier to $self->{id}: $@"); + $self->error ("cannot add $scheme:$identifier to $self->{name}: $@"); return; } return 1; @@ -339,7 +339,7 @@ sub remove { }; if ($@) { my $entry = "$scheme:$identifier"; - $self->error ("cannot remove $entry from $self->{id}: $@"); + $self->error ("cannot remove $entry from $self->{name}: $@"); return; } return 1; @@ -367,7 +367,7 @@ sub list { $guard->commit; }; if ($@) { - $self->error ("cannot retrieve ACL $self->{id}: $@"); + $self->error ("cannot retrieve ACL $self->{name}: $@"); return; } else { return @entries; @@ -422,7 +422,7 @@ sub history { $guard->commit; }; if ($@) { - $self->error ("cannot read history for $self->{id}: $@"); + $self->error ("cannot read history for $self->{name}: $@"); return; } return $output; diff --git a/perl/t/general/acl.t b/perl/t/general/acl.t index ff8ddad..80e8b3c 100755 --- a/perl/t/general/acl.t +++ b/perl/t/general/acl.t @@ -85,7 +85,7 @@ is ($acl->name, 'example', ' and the right name'); is ($acl->id, 2, ' and the right ID'); ok (! $acl->rename ('ADMIN', @trace), ' but renaming to an existing name fails'); -like ($acl->error, qr/^cannot rename ACL 2 to ADMIN: /, +like ($acl->error, qr/^cannot rename ACL example to ADMIN: /, ' with the right error'); # Test add, check, remove, list, and show. @@ -131,7 +131,7 @@ EOE is ($acl->show, $expected, ' and show returns correctly'); ok (! $acl->remove ('krb5', $admin, @trace), 'Removing a nonexistent entry fails'); -is ($acl->error, "cannot remove krb5:$admin from 2: entry not found in ACL", +is ($acl->error, "cannot remove krb5:$admin from example: entry not found in ACL", ' with the right error'); if ($acl->remove ('krb5', $user1, @trace)) { ok (1, ' but removing the first user works'); @@ -145,7 +145,7 @@ is (scalar (@entries), 1, ' and now there is one entry'); is ($entries[0][0], 'krb5', ' with the right scheme'); is ($entries[0][1], $user2, ' and the right identifier'); ok (! $acl->add ('krb5', $user2), 'Adding the same entry again fails'); -like ($acl->error, qr/^cannot add \Qkrb5:$user2\E to 2: /, +like ($acl->error, qr/^cannot add \Qkrb5:$user2\E to example: /, ' with the right error'); if ($acl->add ('krb5', '', @trace)) { ok (1, 'Adding a bad entry works'); diff --git a/perl/t/general/server.t b/perl/t/general/server.t index 0a527a5..8f4c16c 100755 --- a/perl/t/general/server.t +++ b/perl/t/general/server.t @@ -89,7 +89,7 @@ is ($server->acl_rename ('empty', 'test'), undef, is ($server->error, 'ACL empty not found', ' and returns the right error'); is ($server->acl_rename ('test', 'test2'), undef, ' and cannot rename to an existing name'); -like ($server->error, qr/^cannot rename ACL 6 to test2: /, +like ($server->error, qr/^cannot rename ACL test to test2: /, ' and returns the right error'); is ($server->acl_rename ('test', 'empty'), 1, 'Renaming does work'); is ($server->acl_rename ('test', 'empty'), undef, ' but not twice'); @@ -138,7 +138,7 @@ is ($server->error, 'ACL test not found', ' and returns the right error'); is ($server->acl_remove ('empty', 'krb5', $user2), undef, ' and removing an entry not there fails'); is ($server->error, - "cannot remove krb5:$user2 from 6: entry not found in ACL", + "cannot remove krb5:$user2 from empty: entry not found in ACL", ' and returns the right error'); is ($server->acl_show ('empty'), "Members of ACL empty (id: 6) are:\n krb5 $user1\n", @@ -148,7 +148,7 @@ is ($server->acl_remove ('empty', 'krb5', $user1), 1, is ($server->acl_remove ('empty', 'krb5', $user1), undef, ' but does not work twice'); is ($server->error, - "cannot remove krb5:$user1 from 6: entry not found in ACL", + "cannot remove krb5:$user1 from empty: entry not found in ACL", ' and returns the right error'); is ($server->acl_show ('empty'), "Members of ACL empty (id: 6) are:\n", ' and show returns the correct status'); @@ -168,7 +168,7 @@ is ($server->acl_remove ('ADMIN', 'krb5', $user1), 1, ' and then remove it'); is ($server->acl_remove ('ADMIN', 'krb5', $user1), undef, ' and remove a user not on it'); is ($server->error, - "cannot remove krb5:$user1 from 1: entry not found in ACL", + "cannot remove krb5:$user1 from ADMIN: entry not found in ACL", ' and get the right error'); # Now, create a few objects to use for testing and test the object API while @@ -994,7 +994,7 @@ is ($server->owner ('base', 'service/acl-user', 'test-destroy'), 1, is ($server->acl_destroy ('test-destroy'), undef, ' and now we cannot destroy that ACL'); is ($server->error, - 'cannot destroy ACL 9: ACL in use by base:service/acl-user', + 'cannot destroy ACL test-destroy: ACL in use by base:service/acl-user', ' with the right error'); is ($server->owner ('base', 'service/acl-user', ''), 1, ' but after we clear the owner'); -- cgit v1.2.3 From 86533bf43d071048d654691dc18a3004b6142081 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Mon, 8 Jun 2015 11:15:37 -0700 Subject: Added nested acl verifier This verifier will allow embedding one ACL in another for more flexible ACL handling. As part of thise we've also added the ability for each verifier to do a syntax check to see if a given name is valid for that verifier. For the moment this returns true for everything but Nested. Nested will check to make sure the given name is an existing group. Change-Id: Iacdf146d46ed882d57b7534058d34db6e6ec1de4 --- perl/lib/Wallet/ACL.pm | 38 +++++++- perl/lib/Wallet/ACL/Base.pm | 13 ++- perl/lib/Wallet/ACL/Nested.pm | 195 ++++++++++++++++++++++++++++++++++++++++++ perl/lib/Wallet/Admin.pm | 1 + perl/t/general/acl.t | 81 ++++++++++-------- perl/t/general/report.t | 9 +- perl/t/policy/stanford.t | 2 +- perl/t/verifier/nested.t | 84 ++++++++++++++++++ 8 files changed, 381 insertions(+), 42 deletions(-) create mode 100644 perl/lib/Wallet/ACL/Nested.pm create mode 100755 perl/t/verifier/nested.t diff --git a/perl/lib/Wallet/ACL.pm b/perl/lib/Wallet/ACL.pm index 260ff22..6d8005d 100644 --- a/perl/lib/Wallet/ACL.pm +++ b/perl/lib/Wallet/ACL.pm @@ -198,6 +198,19 @@ sub rename { $acls->ac_name ($name); $acls->update; $self->log_acl ('rename', undef, undef, $user, $host, $time); + + # Find any references to this being used as a nested verifier and + # update the name. This really breaks out of the normal flow, but + # it's hard to do otherwise. + %search = (ae_scheme => 'nested', + ae_identifier => $self->{name}, + ); + my @entries = $self->{schema}->resultset('AclEntry')->search(\%search); + for my $entry (@entries) { + $entry->ae_identifier ($name); + $entry->update; + } + $guard->commit; }; if ($@) { @@ -267,6 +280,17 @@ sub destroy { $entry->delete; } + # Find any references to this being used as a nested verifier and + # remove them. This really breaks out of the normal flow, but it's + # hard to do otherwise. + %search = (ae_scheme => 'nested', + ae_identifier => $self->{name}, + ); + @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); @@ -302,6 +326,18 @@ sub add { $self->error ("unknown ACL scheme $scheme"); return; } + + # Check to make sure that this entry has a valid name for the scheme. + my $class = $self->scheme_mapping ($scheme); + my $object = eval { + $class->new ($identifier, $self->{schema}); + }; + unless ($object && $object->syntax_check ($identifier)) { + $self->error ("invalid ACL identifier $identifier for $scheme"); + return; + }; + + # Actually create the scheme. eval { my $guard = $self->{schema}->txn_scope_guard; my %record = (ae_id => $self->{id}, @@ -446,7 +482,7 @@ sub history { push (@{ $self->{check_errors} }, "unknown scheme $scheme"); return; } - $verifier{$scheme} = $class->new; + $verifier{$scheme} = $class->new ($identifier, $self->{schema}); unless (defined $verifier{$scheme}) { push (@{ $self->{check_errors} }, "cannot verify $scheme"); return; diff --git a/perl/lib/Wallet/ACL/Base.pm b/perl/lib/Wallet/ACL/Base.pm index a2b07cc..19ca612 100644 --- a/perl/lib/Wallet/ACL/Base.pm +++ b/perl/lib/Wallet/ACL/Base.pm @@ -20,7 +20,7 @@ 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'; +$VERSION = '0.03'; ############################################################################## # Interface @@ -37,6 +37,11 @@ sub new { return $self; } +# The default name check method allows any name. +sub syntax_check { + return 1; +} + # The default check method denies all access. sub check { return 0; @@ -92,6 +97,12 @@ inherit from it. It is not used directly. Creates a new ACL verifier. The generic function provided here just creates and blesses an object. +=item syntax_check(PRINCIPAL, ACL) + +This method should be overridden by any child classes that want to +implement validating the name of an ACL before creation. The default +implementation allows any name for an ACL. + =item check(PRINCIPAL, ACL) This method should always be overridden by child classes. The default diff --git a/perl/lib/Wallet/ACL/Nested.pm b/perl/lib/Wallet/ACL/Nested.pm new file mode 100644 index 0000000..3be84bd --- /dev/null +++ b/perl/lib/Wallet/ACL/Nested.pm @@ -0,0 +1,195 @@ +# Wallet::ACL::Nested - ACL class for nesting ACLs +# +# Written by Jon Robertson +# Copyright 2015 +# The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +############################################################################## +# Modules and declarations +############################################################################## + +package Wallet::ACL::Nested; +require 5.006; + +use strict; +use warnings; +use vars qw($VERSION @ISA); + +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.01'; + +############################################################################## +# 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 ($name, $schema) = @_; + my $self = { + schema => $schema, + expanded => {}, + }; + bless ($self, $type); + return $self; +} + +# Name checking requires checking that there's an existing ACL already by +# this name. Try to create the ACL object and use that to determine. +sub syntax_check { + my ($self, $group) = @_; + + my $acl; + eval { $acl = Wallet::ACL->new ($group, $self->{schema}) }; + return 0 if $@; + return 0 unless $acl; + return 1; +} + +# For checking a nested ACL, we need to expand each entry and then check +# that entry. We also want to keep track of things already checked in order +# to avoid any loops. +sub check { + my ($self, $principal, $group) = @_; + unless ($principal) { + $self->error ('no principal specified'); + return; + } + unless ($group) { + $self->error ('malformed nested ACL'); + return; + } + + # Make an ACL object just so that we can use it to drop back into the + # normal ACL validation after we have expanded the nesting. + my $acl; + eval { $acl = Wallet::ACL->new ($group, $self->{schema}) }; + + # Get the list of all nested acl entries within this entry, and use it + # to go through each entry and decide if the given acl has access. + my @members = $self->get_membership ($group); + for my $entry (@members) { + my ($type, $name) = @{ $entry }; + my $result = $acl->check_line ($principal, $type, $name); + return 1 if $result; + } + return 0; +} + +# Get the membership of a group recursively. The final result will be a list +# of arrayrefs like that from Wallet::ACL->list, but expanded for full +# membership. +sub get_membership { + my ($self, $group) = @_; + + # Get the list of members for this nested acl. Consider any missing acls + # as empty. + my $schema = $self->{schema}; + my @members; + eval { + my $acl = Wallet::ACL->new ($group, $schema); + @members = $acl->list; + }; + + # Now go through and expand any other nested groups into their own + # memberships. + my @expanded; + for my $entry (@members) { + my ($type, $name) = @{ $entry }; + if ($type eq 'nested') { + + # Keep track of things we've already expanded and don't look them + # up again. + next if exists $self->{expanded}{$name}; + $self->{expanded}{$name} = 1; + push (@expanded, $self->get_membership ($name)); + + } else { + push (@expanded, $entry); + } + } + + return @expanded; +} + +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> 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. + +=head1 AUTHOR + +Russ Allbery + +=cut diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm index b38cc94..f6f1f90 100644 --- a/perl/lib/Wallet/Admin.pm +++ b/perl/lib/Wallet/Admin.pm @@ -118,6 +118,7 @@ sub default_data { [ 'krb5', 'Wallet::ACL::Krb5' ], [ 'krb5-regex', 'Wallet::ACL::Krb5::Regex' ], [ 'ldap-attr', 'Wallet::ACL::LDAP::Attribute' ], + [ 'nested', 'Wallet::ACL::Nested' ], [ 'netdb', 'Wallet::ACL::NetDB' ], [ 'netdb-root', 'Wallet::ACL::NetDB::Root' ], ]); diff --git a/perl/t/general/acl.t b/perl/t/general/acl.t index 80e8b3c..aad4b6d 100755 --- a/perl/t/general/acl.t +++ b/perl/t/general/acl.t @@ -12,7 +12,7 @@ use strict; use warnings; use POSIX qw(strftime); -use Test::More tests => 109; +use Test::More tests => 113; use Wallet::ACL; use Wallet::Admin; @@ -62,32 +62,6 @@ is ($@, '', ' with no exceptions'); ok ($acl->isa ('Wallet::ACL'), ' and the right class'); is ($acl->name, 'test', ' and the right name'); -# Test rename. -if ($acl->rename ('example', @trace)) { - ok (1, 'Renaming the ACL'); -} else { - is ($acl->error, '', 'Renaming the ACL'); -} -is ($acl->name, 'example', ' and the new name is right'); -is ($acl->id, 2, ' and the ID did not change'); -$acl = eval { Wallet::ACL->new ('test', $schema) }; -ok (!defined ($acl), ' and it cannot be found under the old name'); -is ($@, "ACL test not found\n", ' with the right error message'); -$acl = eval { Wallet::ACL->new ('example', $schema) }; -ok (defined ($acl), ' and it can be found with the new name'); -is ($@, '', ' with no exceptions'); -is ($acl->name, 'example', ' and the right name'); -is ($acl->id, 2, ' and the right ID'); -$acl = eval { Wallet::ACL->new (2, $schema) }; -ok (defined ($acl), ' and it can still found by ID'); -is ($@, '', ' with no exceptions'); -is ($acl->name, 'example', ' and the right name'); -is ($acl->id, 2, ' and the right ID'); -ok (! $acl->rename ('ADMIN', @trace), - ' but renaming to an existing name fails'); -like ($acl->error, qr/^cannot rename ACL example to ADMIN: /, - ' with the right error'); - # Test add, check, remove, list, and show. my @entries = $acl->list; is (scalar (@entries), 0, 'ACL starts empty'); @@ -124,14 +98,14 @@ is ($entries[0][1], $user1, ' and the right identifier for 1'); is ($entries[1][0], 'krb5', ' and the right scheme for 2'); is ($entries[1][1], $user2, ' and the right identifier for 2'); my $expected = <<"EOE"; -Members of ACL example (id: 2) are: +Members of ACL test (id: 2) are: krb5 $user1 krb5 $user2 EOE is ($acl->show, $expected, ' and show returns correctly'); ok (! $acl->remove ('krb5', $admin, @trace), 'Removing a nonexistent entry fails'); -is ($acl->error, "cannot remove krb5:$admin from example: entry not found in ACL", +is ($acl->error, "cannot remove krb5:$admin from test: entry not found in ACL", ' with the right error'); if ($acl->remove ('krb5', $user1, @trace)) { ok (1, ' but removing the first user works'); @@ -145,7 +119,7 @@ is (scalar (@entries), 1, ' and now there is one entry'); is ($entries[0][0], 'krb5', ' with the right scheme'); is ($entries[0][1], $user2, ' and the right identifier'); ok (! $acl->add ('krb5', $user2), 'Adding the same entry again fails'); -like ($acl->error, qr/^cannot add \Qkrb5:$user2\E to example: /, +like ($acl->error, qr/^cannot add \Qkrb5:$user2\E to test: /, ' with the right error'); if ($acl->add ('krb5', '', @trace)) { ok (1, 'Adding a bad entry works'); @@ -159,7 +133,7 @@ is ($entries[0][1], '', ' and the right identifier for 1'); is ($entries[1][0], 'krb5', ' and the right scheme for 2'); is ($entries[1][1], $user2, ' and the right identifier for 2'); $expected = <<"EOE"; -Members of ACL example (id: 2) are: +Members of ACL test (id: 2) are: krb5 krb5 $user2 EOE @@ -187,17 +161,50 @@ if ($acl->remove ('krb5', '', @trace)) { } @entries = $acl->list; is (scalar (@entries), 0, ' and now there are no entries'); -is ($acl->show, "Members of ACL example (id: 2) are:\n", ' and show concurs'); +is ($acl->show, "Members of ACL test (id: 2) are:\n", ' and show concurs'); is ($acl->check ($user2), 0, ' and the second user check fails'); is (scalar ($acl->check_errors), '', ' with no error message'); +# Test rename. +my $acl_nest = eval { Wallet::ACL->create ('test-nesting', $schema, @trace) }; +ok (defined ($acl_nest), 'ACL creation for setting up nested'); +if ($acl_nest->add ('nested', 'test', @trace)) { + ok (1, ' and adding the nesting'); +} else { + is ($acl_nest->error, '', ' and adding the nesting'); +} +if ($acl->rename ('example', @trace)) { + ok (1, 'Renaming the ACL'); +} else { + is ($acl->error, '', 'Renaming the ACL'); +} +is ($acl->name, 'example', ' and the new name is right'); +is ($acl->id, 2, ' and the ID did not change'); +$acl = eval { Wallet::ACL->new ('test', $schema) }; +ok (!defined ($acl), ' and it cannot be found under the old name'); +is ($@, "ACL test not found\n", ' with the right error message'); +$acl = eval { Wallet::ACL->new ('example', $schema) }; +ok (defined ($acl), ' and it can be found with the new name'); +is ($@, '', ' with no exceptions'); +is ($acl->name, 'example', ' and the right name'); +is ($acl->id, 2, ' and the right ID'); +$acl = eval { Wallet::ACL->new (2, $schema) }; +ok (defined ($acl), ' and it can still found by ID'); +is ($@, '', ' with no exceptions'); +is ($acl->name, 'example', ' and the right name'); +is ($acl->id, 2, ' and the right ID'); +ok (! $acl->rename ('ADMIN', @trace), + ' but renaming to an existing name fails'); +like ($acl->error, qr/^cannot rename ACL example to ADMIN: /, + ' with the right error'); +@entries = $acl_nest->list; +is ($entries[0][1], 'example', ' and the name in a nested ACL updated'); + # Test history. my $date = strftime ('%Y-%m-%d %H:%M:%S', localtime $trace[2]); my $history = <<"EOO"; $date create by $admin from $host -$date rename from test - by $admin from $host $date add krb5 $user1 by $admin from $host $date add krb5 $user2 @@ -210,6 +217,8 @@ $date remove krb5 $user2 by $admin from $host $date remove krb5 by $admin from $host +$date rename from test + by $admin from $host EOO is ($acl->history, $history, 'History is correct'); @@ -225,11 +234,13 @@ is ($@, "ACL example not found\n", ' with the right error message'); $acl = eval { Wallet::ACL->new (2, $schema) }; ok (!defined ($acl), ' or by ID'); is ($@, "ACL 2 not found\n", ' with the right error message'); +@entries = $acl_nest->list; +is (scalar (@entries), 0, ' and it is no longer a nested entry'); $acl = eval { Wallet::ACL->create ('example', $schema, @trace) }; ok (defined ($acl), ' and creating another with the same name works'); is ($@, '', ' with no exceptions'); is ($acl->name, 'example', ' and the right name'); -like ($acl->id, qr{\A[23]\z}, ' and an ID of 2 or 3'); +like ($acl->id, qr{\A[34]\z}, ' and an ID of 3 or 4'); # Test replace. by creating three acls, then assigning two objects to the # first, one to the second, and another to the third. Then replace the first diff --git a/perl/t/general/report.t b/perl/t/general/report.t index 170fe29..6f6b750 100755 --- a/perl/t/general/report.t +++ b/perl/t/general/report.t @@ -11,7 +11,7 @@ use strict; use warnings; -use Test::More tests => 218; +use Test::More tests => 219; use Wallet::Admin; use Wallet::Report; @@ -57,13 +57,14 @@ is ($types[9][0], 'wa-keyring', ' and the tenth member is correct'); # And that we have all schemes that we expect. my @schemes = $report->acl_schemes; -is (scalar (@schemes), 6, 'There are six acl schemes created'); +is (scalar (@schemes), 7, 'There are seven acl schemes created'); is ($schemes[0][0], 'base', ' and the first member is correct'); is ($schemes[1][0], 'krb5', ' and the second member is correct'); is ($schemes[2][0], 'krb5-regex', ' and the third member is correct'); is ($schemes[3][0], 'ldap-attr', ' and the fourth member is correct'); -is ($schemes[4][0], 'netdb', ' and the fifth member is correct'); -is ($schemes[5][0], 'netdb-root', ' and the sixth member is correct'); +is ($schemes[4][0], 'nested', ' and the fifth member is correct'); +is ($schemes[5][0], 'netdb', ' and the sixth member is correct'); +is ($schemes[6][0], 'netdb-root', ' and the seventh member is correct'); # Create an object. my $server = eval { Wallet::Server->new ('admin@EXAMPLE.COM', 'localhost') }; diff --git a/perl/t/policy/stanford.t b/perl/t/policy/stanford.t index 9ed0fa6..c58985b 100755 --- a/perl/t/policy/stanford.t +++ b/perl/t/policy/stanford.t @@ -140,7 +140,7 @@ is( 'example.stanford.edu'), 1, '...with netdb ACL line' -); + ); is( $server->acl_add('host/example.stanford.edu', 'krb5', 'host/example.stanford.edu@stanford.edu'), diff --git a/perl/t/verifier/nested.t b/perl/t/verifier/nested.t new file mode 100755 index 0000000..ec7ce40 --- /dev/null +++ b/perl/t/verifier/nested.t @@ -0,0 +1,84 @@ +#!/usr/bin/perl +# +# Tests for the wallet ACL nested verifier. +# +# Written by Jon Robertson +# Copyright 2015 +# The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +use strict; +use warnings; + +use Test::More tests => 22; + +use Wallet::ACL::Base; +use Wallet::ACL::Nested; +use Wallet::Admin; +use Wallet::Config; + +use lib 't/lib'; +use Util; + +# Some global defaults to use. +my $admin = 'admin@EXAMPLE.COM'; +my $user1 = 'alice@EXAMPLE.COM'; +my $user2 = 'bob@EXAMPLE.COM'; +my $user3 = 'jack@EXAMPLE.COM'; +my $host = 'localhost'; +my @trace = ($admin, $host, time); + +# Use Wallet::Admin to set up the database. +db_setup; +my $setup = eval { Wallet::Admin->new }; +is ($@, '', 'Database connection succeeded'); +is ($setup->reinitialize ($setup), 1, 'Database initialization succeeded'); +my $schema = $setup->schema; + +# Create a few ACLs for later testing. +my $acl = eval { Wallet::ACL->create ('test', $schema, @trace) }; +ok (defined ($acl), 'ACL creation'); +my $acl_nesting = eval { Wallet::ACL->create ('nesting', $schema, @trace) }; +ok (defined ($acl), ' and another'); +my $acl_deep = eval { Wallet::ACL->create ('deepnesting', $schema, @trace) }; +ok (defined ($acl), ' and another'); + +# Create an verifier to make sure that works +my $verifier = Wallet::ACL::Nested->new ('test', $schema); +ok (defined $verifier, 'Wallet::ACL::Nested creation'); +ok ($verifier->isa ('Wallet::ACL::Nested'), ' and class verification'); +is ($verifier->syntax_check ('notcreated'), 0, + ' and it rejects a nested name that is not already an ACL'); +is ($verifier->syntax_check ('test'), 1, + ' and accepts one that already exists'); + +# Add a few entries to one ACL and then see if they validate. +ok ($acl->add ('krb5', $user1, @trace), 'Added test scheme'); +ok ($acl->add ('krb5', $user2, @trace), ' and another'); +ok ($acl_nesting->add ('nested', 'test', @trace), ' and then nested it'); +ok ($acl_nesting->add ('krb5', $user3, @trace), + ' and added a non-nesting user'); +is ($acl_nesting->check ($user1), 1, ' so check of nested succeeds'); +is ($acl_nesting->check ($user3), 1, ' so check of non-nested succeeds'); +is (scalar ($acl_nesting->list), 2, + ' and the acl has the right number of items'); + +# Add a recursive nesting to make sure it doesn't send us into loop. +ok ($acl_deep->add ('nested', 'test', @trace), + 'Adding deep nesting for one nest succeeds'); +ok ($acl_deep->add ('nested', 'nesting', @trace), ' and another'); +ok ($acl_deep->add ('krb5', $user3, @trace), + ' and added a non-nesting user'); +is ($acl_deep->check ($user1), 1, ' so check of nested succeeds'); +is ($acl_deep->check ($user3), 1, ' so check of non-nested succeeds'); + +# Test getting an error in adding an invalid group to an ACL object itself. +isnt ($acl->add ('nested', 'doesnotexist', @trace), 1, + 'Adding bad nested acl fails'); + +# Clean up. +$setup->destroy; +END { + unlink 'wallet-db'; +} -- cgit v1.2.3 From e0b2ecf96ebc49fabd9c4b6af5c50b203d2b4832 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Mon, 8 Jun 2015 13:08:43 -0700 Subject: Wallet/Server.pm: Fix sorting of ACLs and entries There was an older mistake in sorting ACLs and entries, using && instead of || when sorting. Problem and fix pointed out to Chris Law. Change-Id: Iab46b4bcbd842978f88a7d9f63958ebea4806413 --- perl/lib/Wallet/Server.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/perl/lib/Wallet/Server.pm b/perl/lib/Wallet/Server.pm index 3ef5954..946ba10 100644 --- a/perl/lib/Wallet/Server.pm +++ b/perl/lib/Wallet/Server.pm @@ -154,8 +154,8 @@ sub create_check { $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; + @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; -- cgit v1.2.3 From 0b4201c8a65102227685f5cbe4f81407dce7e0b5 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Mon, 8 Jun 2015 21:59:25 -0700 Subject: Nested.pm: Updated comments around constructor Removed some default text and explained why we grab the database handle for future use. Change-Id: I50b3ae06c1761453de3140d501830c245d550c04 --- perl/lib/Wallet/ACL/Nested.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/perl/lib/Wallet/ACL/Nested.pm b/perl/lib/Wallet/ACL/Nested.pm index 3be84bd..945d881 100644 --- a/perl/lib/Wallet/ACL/Nested.pm +++ b/perl/lib/Wallet/ACL/Nested.pm @@ -30,10 +30,8 @@ $VERSION = '0.01'; # 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. +# Creates a new persistant verifier, taking a database handle to use for +# syntax check validation. sub new { my $type = shift; my ($name, $schema) = @_; -- cgit v1.2.3 From 43f386a6e3d0c141cd732b5ef5c2be8349f51f03 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 9 Jun 2015 13:06:56 -0700 Subject: ACL.pm: Destroying a nested ACL will now fail When destroying an ACL nested in other ACLs, we now fail with an explanation rather than going through to remove all the places it's nested. That's more in line with how we handle trying to destroy ACLs that own things. Change-Id: I8bc0530e37c54369ec52d9b369f8fabe98def77a --- perl/lib/Wallet/ACL.pm | 23 ++++++++++++----------- perl/t/general/acl.t | 14 +++++++++++--- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/perl/lib/Wallet/ACL.pm b/perl/lib/Wallet/ACL.pm index 6d8005d..f875185 100644 --- a/perl/lib/Wallet/ACL.pm +++ b/perl/lib/Wallet/ACL.pm @@ -273,19 +273,20 @@ sub destroy { 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; + # Also make certain the ACL isn't being nested in another. + my %search = (ae_scheme => 'nested', + ae_identifier => $self->{name}); + my %options = (join => 'acls', + prefetch => 'acls'); + @entries = $self->{schema}->resultset('AclEntry')->search(\%search, + \%options); + if (@entries) { + my ($entry) = @entries; + die "ACL is nested in ACL ".$entry->acls->ac_name; } - # Find any references to this being used as a nested verifier and - # remove them. This really breaks out of the normal flow, but it's - # hard to do otherwise. - %search = (ae_scheme => 'nested', - ae_identifier => $self->{name}, - ); + # Delete any entries (there may or may not be any). + %search = (ae_id => $self->{id}); @entries = $self->{schema}->resultset('AclEntry')->search(\%search); for my $entry (@entries) { $entry->delete; diff --git a/perl/t/general/acl.t b/perl/t/general/acl.t index aad4b6d..4de7493 100755 --- a/perl/t/general/acl.t +++ b/perl/t/general/acl.t @@ -12,7 +12,7 @@ use strict; use warnings; use POSIX qw(strftime); -use Test::More tests => 113; +use Test::More tests => 115; use Wallet::ACL; use Wallet::Admin; @@ -223,10 +223,18 @@ EOO is ($acl->history, $history, 'History is correct'); # Test destroy. +$acl->destroy (@trace); +is ($acl->error, 'cannot destroy ACL example: ACL is nested in ACL test-nesting', + 'Destroying a nested ACL fails'); +if ($acl_nest->remove ('nested', 'example', @trace)) { + ok (1, ' and removing the nesting succeeds'); +} else { + is ($acl_nest->error, '', 'and removing the nesting succeeds'); +} if ($acl->destroy (@trace)) { - ok (1, 'Destroying the ACL works'); + ok (1, ' and now destroying the ACL works'); } else { - is ($acl->error, '', 'Destroying the ACL works'); + is ($acl->error, '', ' and now destroying the ACL works'); } $acl = eval { Wallet::ACL->new ('example', $schema) }; ok (!defined ($acl), ' and now cannot be found'); -- cgit v1.2.3 From 5d668b86ced32e84fd0f49046326a0a5e20dc8eb Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 9 Jun 2015 15:04:14 -0700 Subject: Added wallet report for nested ACL We needed a way to report on where all a specific ACL might be nested, since we can't destroy an ACL until it's no longer being nested. For the immediate this is part of wallet-report. Change-Id: I41c11b73325d1eb3a28289eac3505bf965877be1 --- perl/lib/Wallet/Report.pm | 47 ++++++++++++++++++++++++++++++++++++++++------- perl/t/general/report.t | 9 ++++++++- server/wallet-report | 1 + 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/perl/lib/Wallet/Report.pm b/perl/lib/Wallet/Report.pm index fc7bb4d..353cd97 100644 --- a/perl/lib/Wallet/Report.pm +++ b/perl/lib/Wallet/Report.pm @@ -359,8 +359,7 @@ sub types { # ACL reports ############################################################################## -# Returns the SQL statement required to find and return all ACLs in the -# database. +# Returns the array of all ACLs in the database. sub acls_all { my ($self) = @_; my @acls; @@ -384,7 +383,7 @@ sub acls_all { return (@acls); } -# Returns the SQL statement required to find all empty ACLs in the database. +# Returns the array of all empty ACLs in the database. sub acls_empty { my ($self) = @_; my @acls; @@ -410,9 +409,36 @@ sub acls_empty { 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. +# Returns the array of ACLs that nest a given ACL. +sub acls_nesting { + my ($self, $name) = @_; + my @acls; + + my $schema = $self->{schema}; + my %search = (ae_scheme => 'nested', + ae_identifier => $name); + 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 array of all ACLs containing the specified entry. The given +# identifier is automatically surrounded by wildcards to do a substring +# search. sub acls_entry { my ($self, $type, $identifier) = @_; my @acls; @@ -440,7 +466,7 @@ sub acls_entry { return (@acls); } -# Returns the SQL statement required to find unused ACLs. +# Returns the array of all unused ACLs. sub acls_unused { my ($self) = @_; my @acls; @@ -553,6 +579,13 @@ sub acls { @acls = $self->acls_empty; } elsif ($type eq 'unused') { @acls = $self->acls_unused; + } elsif ($type eq 'nesting') { + if (@args == 0) { + $self->error ('ACL nesting search requires an ACL to search'); + return; + } else { + @acls = $self->acls_nesting (@args); + } } else { $self->error ("unknown search type: $type"); return; diff --git a/perl/t/general/report.t b/perl/t/general/report.t index 6f6b750..a841acd 100755 --- a/perl/t/general/report.t +++ b/perl/t/general/report.t @@ -11,7 +11,7 @@ use strict; use warnings; -use Test::More tests => 219; +use Test::More tests => 222; use Wallet::Admin; use Wallet::Report; @@ -366,6 +366,13 @@ is ($server->acl_add ('third', 'base', 'baz'), 1, is (scalar (@acls), 0, 'There are no duplicate ACLs'); is ($report->error, undef, ' and no error'); +# See if the acl nesting report works correctly. +is ($server->acl_add ('fourth', 'nested', 'second'), 1, + 'Adding an ACL as a nested entry for another works'); +@acls = $report->acls ('nesting', 'second'); +is (scalar (@acls), 1, ' and the nested report shows one nesting'); +is ($acls[0][1], 'fourth', ' with the correct ACL nesting it'); + # Clean up. $admin->destroy; system ('rm -r test-files') == 0 or die "cannot remove test-files\n"; diff --git a/server/wallet-report b/server/wallet-report index 77a2f8a..4719a8a 100755 --- a/server/wallet-report +++ b/server/wallet-report @@ -17,6 +17,7 @@ Wallet reporting help: acls duplicate ACLs that duplicate another acls empty All empty ACLs acls entry ACLs containing this entry (wildcarded) + acls nesting ACLs containing this ACL as a nested entry acls unused ACLs that are not referenced by any object audit acls name ACLs failing the naming policy audit objects name Objects failing the naming policy -- cgit v1.2.3 From 2e00a586996cf4e53bd9d03afca7d46471549171 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 25 Aug 2015 14:13:09 -0700 Subject: stanford.t: Added netdb configuration to policy tests Since we now check to see if something is a valid netdb node entry for the ACL verifiers, we need to have a valid netdb setup to run. Change-Id: Ic2651f8b8b306dfa1f426d91f329b5100a9a1d64 --- perl/t/policy/stanford.t | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/perl/t/policy/stanford.t b/perl/t/policy/stanford.t index c58985b..0c3dd2d 100755 --- a/perl/t/policy/stanford.t +++ b/perl/t/policy/stanford.t @@ -29,6 +29,11 @@ BEGIN { use_ok('Wallet::Server'); } +# Set up our configuration for netdb, needed for the netdb verifier. +$Wallet::Config::NETDB_REALM = 'stanford.edu'; +$Wallet::Config::NETDB_REMCTL_CACHE = $ENV{KRB5CCNAME}; +$Wallet::Config::NETDB_REMCTL_HOST = 'netdb-node-roles-rc.stanford.edu'; + # Various valid keytab names. my @VALID_KEYTABS = qw(host/example.stanford.edu HTTP/example.stanford.edu service/example example/cgi class-example01/cgi dept-01example/cgi -- cgit v1.2.3 From e353e236cf6828647820b2d83529cc4a4f08cef2 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Tue, 25 Aug 2015 14:29:16 -0700 Subject: ldap-attr.t: Updated tests to use jonrober rather than rra Change-Id: I842a7335a4b50c9c20b921ae2efc63aab571635e --- perl/t/verifier/ldap-attr.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/perl/t/verifier/ldap-attr.t b/perl/t/verifier/ldap-attr.t index 3c132e2..3caaf8b 100755 --- a/perl/t/verifier/ldap-attr.t +++ b/perl/t/verifier/ldap-attr.t @@ -31,7 +31,7 @@ require_ok ('Wallet::ACL::LDAP::Attribute'); my $host = 'ldap.stanford.edu'; my $base = 'cn=people,dc=stanford,dc=edu'; my $filter = 'uid'; -my $user = 'rra@stanford.edu'; +my $user = 'jonrober@stanford.edu'; my $attr = 'suPrivilegeGroup'; my $value = 'stanford:stanford'; @@ -68,7 +68,7 @@ SKIP: { is ($verifier->check ($user, "BOGUS=$value"), undef, "Checking BOGUS=$value fails with error"); is ($verifier->error, - 'cannot check LDAP attribute BOGUS for rra: Undefined attribute type', + 'cannot check LDAP attribute BOGUS for jonrober: Undefined attribute type', '...with correct error'); is ($verifier->check ('user-does-not-exist', "$attr=$value"), 0, "Checking for nonexistent user fails"); -- cgit v1.2.3 From 6b0cad572edef05d119abc8fc843c8c5d33665b8 Mon Sep 17 00:00:00 2001 From: Jon Robertson Date: Thu, 27 Aug 2015 10:34:22 -0700 Subject: Added Wallet::ACL::LDAP::Attribute::Root Added a version of the LDAP attribute ACL. Like the root version for NetDB, this requires that the principal end in /root, and then strips off /root before doing matching against the given LDAP attribute. Change-Id: I23119ef9c9ce3e0556f5d71a509815f2efc1bbe6 --- perl/lib/Wallet/ACL/LDAP/Attribute/Root.pm | 128 +++++++++++++++++++++++++++++ perl/lib/Wallet/Admin.pm | 13 +-- perl/lib/Wallet/Schema.pm | 4 + perl/lib/Wallet/Schema/Result/AclScheme.pm | 4 + perl/t/general/report.t | 11 +-- perl/t/verifier/ldap-attr.t | 37 +++++++-- 6 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 perl/lib/Wallet/ACL/LDAP/Attribute/Root.pm diff --git a/perl/lib/Wallet/ACL/LDAP/Attribute/Root.pm b/perl/lib/Wallet/ACL/LDAP/Attribute/Root.pm new file mode 100644 index 0000000..eb30931 --- /dev/null +++ b/perl/lib/Wallet/ACL/LDAP/Attribute/Root.pm @@ -0,0 +1,128 @@ +# Wallet::ACL::LDAP::Attribute::Root -- Wallet LDAP ACL verifier (root instances). +# +# Written by Jon Robertson +# From Wallet::ACL::NetDB::Root by Russ Allbery +# Copyright 2015 +# The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +############################################################################## +# Modules and declarations +############################################################################## + +package Wallet::ACL::LDAP::Attribute::Root; +require 5.006; + +use strict; +use warnings; +use vars qw(@ISA $VERSION); + +use Wallet::ACL::LDAP::Attribute; +use Wallet::Config; + +@ISA = qw(Wallet::ACL::LDAP::Attribute); + +# 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 +############################################################################## + +# Override the check method of Wallet::ACL::LDAP::Attribute 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) = @_; + undef $self->{error}; + 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 Allbery LDAP verifier + +=head1 NAME + +Wallet::ACL::LDAP::Attribute::Root - Wallet ACL verifier for LDAP attributes (root instances) + +=head1 SYNOPSIS + + my $verifier = Wallet::ACL::LDAP::Attribute::Root->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::Root works identically to +Wallet::ACL::LDAP::Attribute except that it requires the principal to +be a root instance (in other words, to be in the form +/root@) and strips the C portion from the +principal before checking against the LDAP attribute and value. As +with the base LDAP Attribute ACL verifier, the value of such a +C 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 (with C stripped) has that +attribute set to that value. + +To use this object, the same configuration parameters must be set as for +Wallet::ACL::LDAP::Attribute. 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 must be an +attribute name and a value, separated by an equal sign (with no +whitespace). PRINCIPAL will be granted access if it has an instance of +C and if (with C stripped off) its LDAP entry contains +that attribute with that value + +=back + +=head1 DIAGNOSTICS + +Same as for Wallet::ACL::LDAP::Attribute. + +=head1 CAVEATS + +The instance to strip is not currently configurable. + +=head1 SEE ALSO + +Net::Remctl(3), Wallet::ACL(3), Wallet::ACL::Base(3), +Wallet::ACL::LDAP::Attribute(3), Wallet::Config(3), wallet-backend(8) + +This module is part of the wallet system. The current version is +available from L. + +=head1 AUTHORS + +Jon Robertson +Russ Allbery + +=cut diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm index f6f1f90..b4246ba 100644 --- a/perl/lib/Wallet/Admin.pm +++ b/perl/lib/Wallet/Admin.pm @@ -115,12 +115,13 @@ sub default_data { # 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' ], - [ 'nested', 'Wallet::ACL::Nested' ], - [ 'netdb', 'Wallet::ACL::NetDB' ], - [ 'netdb-root', 'Wallet::ACL::NetDB::Root' ], + [ 'krb5', 'Wallet::ACL::Krb5' ], + [ 'krb5-regex', 'Wallet::ACL::Krb5::Regex' ], + [ 'ldap-attr', 'Wallet::ACL::LDAP::Attribute' ], + [ 'ldap-attr-root', 'Wallet::ACL::LDAP::Attribute::Root' ], + [ 'nested', 'Wallet::ACL::Nested' ], + [ 'netdb', 'Wallet::ACL::NetDB' ], + [ 'netdb-root', 'Wallet::ACL::NetDB::Root' ], ]); warn "default AclScheme not installed" unless defined $r1; diff --git a/perl/lib/Wallet/Schema.pm b/perl/lib/Wallet/Schema.pm index 5b850c0..386801a 100644 --- a/perl/lib/Wallet/Schema.pm +++ b/perl/lib/Wallet/Schema.pm @@ -113,6 +113,10 @@ Holds the supported ACL schemes and their corresponding Perl classes: 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 ('ldap-attr-root', 'Wallet::ACL::LDAP::Attribute::Root'); + insert into acl_schemes (as_name, as_class) + values ('nested', 'Wallet::ACL::Nested'); insert into acl_schemes (as_name, as_class) values ('netdb', 'Wallet::ACL::NetDB'); insert into acl_schemes (as_name, as_class) diff --git a/perl/lib/Wallet/Schema/Result/AclScheme.pm b/perl/lib/Wallet/Schema/Result/AclScheme.pm index 91a58b2..be4ec09 100644 --- a/perl/lib/Wallet/Schema/Result/AclScheme.pm +++ b/perl/lib/Wallet/Schema/Result/AclScheme.pm @@ -35,6 +35,10 @@ By default it contains the following entries: 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 ('ldap-attr-root', 'Wallet::ACL::LDAP::Attribute::Root'); + insert into acl_schemes (as_name, as_class) + values ('nested', 'Wallet::ACL::Nested'); insert into acl_schemes (as_name, as_class) values ('netdb', 'Wallet::ACL::NetDB'); insert into acl_schemes (as_name, as_class) diff --git a/perl/t/general/report.t b/perl/t/general/report.t index a841acd..e47cdc6 100755 --- a/perl/t/general/report.t +++ b/perl/t/general/report.t @@ -11,7 +11,7 @@ use strict; use warnings; -use Test::More tests => 222; +use Test::More tests => 223; use Wallet::Admin; use Wallet::Report; @@ -57,14 +57,15 @@ is ($types[9][0], 'wa-keyring', ' and the tenth member is correct'); # And that we have all schemes that we expect. my @schemes = $report->acl_schemes; -is (scalar (@schemes), 7, 'There are seven acl schemes created'); +is (scalar (@schemes), 8, 'There are seven acl schemes created'); is ($schemes[0][0], 'base', ' and the first member is correct'); is ($schemes[1][0], 'krb5', ' and the second member is correct'); is ($schemes[2][0], 'krb5-regex', ' and the third member is correct'); is ($schemes[3][0], 'ldap-attr', ' and the fourth member is correct'); -is ($schemes[4][0], 'nested', ' and the fifth member is correct'); -is ($schemes[5][0], 'netdb', ' and the sixth member is correct'); -is ($schemes[6][0], 'netdb-root', ' and the seventh member is correct'); +is ($schemes[4][0], 'ldap-attr-root', ' and the fifth member is correct'); +is ($schemes[5][0], 'nested', ' and the sixth member is correct'); +is ($schemes[6][0], 'netdb', ' and the seventh member is correct'); +is ($schemes[7][0], 'netdb-root', ' and the eighth member is correct'); # Create an object. my $server = eval { Wallet::Server->new ('admin@EXAMPLE.COM', 'localhost') }; diff --git a/perl/t/verifier/ldap-attr.t b/perl/t/verifier/ldap-attr.t index 3caaf8b..cff3b63 100755 --- a/perl/t/verifier/ldap-attr.t +++ b/perl/t/verifier/ldap-attr.t @@ -24,16 +24,18 @@ plan skip_all => 'LDAP verifier tests only run for maintainer' unless $ENV{RRA_MAINTAINER_TESTS}; # Declare a plan. -plan tests => 10; +plan tests => 22; require_ok ('Wallet::ACL::LDAP::Attribute'); +require_ok ('Wallet::ACL::LDAP::Attribute::Root'); -my $host = 'ldap.stanford.edu'; -my $base = 'cn=people,dc=stanford,dc=edu'; -my $filter = 'uid'; -my $user = 'jonrober@stanford.edu'; -my $attr = 'suPrivilegeGroup'; -my $value = 'stanford:stanford'; +my $host = 'ldap.stanford.edu'; +my $base = 'cn=people,dc=stanford,dc=edu'; +my $filter = 'uid'; +my $user = 'jonrober@stanford.edu'; +my $rootuser = 'jonrober/root@stanford.edu'; +my $attr = 'suPrivilegeGroup'; +my $value = 'stanford:stanford'; # Remove the realm from principal names. package Wallet::Config; @@ -73,4 +75,25 @@ SKIP: { is ($verifier->check ('user-does-not-exist', "$attr=$value"), 0, "Checking for nonexistent user fails"); is ($verifier->error, undef, '...with no error'); + + # Then also test the root version. + $verifier = eval { Wallet::ACL::LDAP::Attribute::Root->new }; + isa_ok ($verifier, 'Wallet::ACL::LDAP::Attribute::Root'); + is ($verifier->check ($user, "$attr=$value"), 0, + "Checking as a non /root user fails"); + is ($verifier->error, undef, '...with no error'); + is ($verifier->check ($rootuser, "$attr=$value"), 1, + "Checking $attr=$value succeeds"); + is ($verifier->error, undef, '...with no error'); + is ($verifier->check ($rootuser, "$attr=BOGUS"), 0, + "Checking $attr=BOGUS fails"); + is ($verifier->error, undef, '...with no error'); + is ($verifier->check ($rootuser, "BOGUS=$value"), undef, + "Checking BOGUS=$value fails with error"); + is ($verifier->error, + 'cannot check LDAP attribute BOGUS for jonrober: Undefined attribute type', + '...with correct error'); + is ($verifier->check ('user-does-not-exist', "$attr=$value"), 0, + "Checking for nonexistent user fails"); + is ($verifier->error, undef, '...with no error'); } -- cgit v1.2.3