diff options
Diffstat (limited to 'perl')
29 files changed, 2378 insertions, 42 deletions
diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm index d32a0c2..8120e9c 100644 --- a/perl/lib/Wallet/Admin.pm +++ b/perl/lib/Wallet/Admin.pm @@ -126,6 +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' ], [ 'file', 'Wallet::Object::File' ], [ 'keytab', 'Wallet::Object::Keytab' ], [ 'wa-keyring', 'Wallet::Object::WAKeyring' ]); diff --git a/perl/lib/Wallet/Config.pm b/perl/lib/Wallet/Config.pm index 527658c..2eb57f9 100644 --- a/perl/lib/Wallet/Config.pm +++ b/perl/lib/Wallet/Config.pm @@ -217,9 +217,9 @@ our $DUO_KEY_FILE; =item DUO_TYPE -The type of integration to create. Currently, only one type of integration -can be created by one wallet configuration. This restriction may be relaxed -in the future. The default value is C<unix> to create UNIX integrations. +The type of integration to create. The default value is C<unix> to create +UNIX integrations, since this was the first integration created and users +may rely on it to still be the default. =cut diff --git a/perl/lib/Wallet/Object/Base.pm b/perl/lib/Wallet/Object/Base.pm index a6a78bf..bdd61fb 100644 --- a/perl/lib/Wallet/Object/Base.pm +++ b/perl/lib/Wallet/Object/Base.pm @@ -187,7 +187,7 @@ sub log_set { } my %fields = map { $_ => 1 } qw(owner acl_get acl_store acl_show acl_destroy acl_flags expires - comment flags type_data); + comment flags type_data name); unless ($fields{$field}) { die "invalid history field $field"; } diff --git a/perl/lib/Wallet/Object/Duo.pm b/perl/lib/Wallet/Object/Duo.pm index 6edc4fa..d08294b 100644 --- a/perl/lib/Wallet/Object/Duo.pm +++ b/perl/lib/Wallet/Object/Duo.pm @@ -1,4 +1,4 @@ -# Wallet::Object::Duo -- Duo integration object implementation for the wallet. +# Wallet::Object::Duo -- Base Duo object implementation for the wallet # # Written by Russ Allbery <eagle@eyrie.org> # Copyright 2014 @@ -29,7 +29,7 @@ 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.01'; +$VERSION = '0.02'; ############################################################################## # Core methods @@ -41,7 +41,9 @@ sub attr_show { my $output = ''; my $key; eval { - my %search = (du_name => $self->{name}); + my %search = (du_name => $self->{name}, + du_type => $self->{type}, + ); my $row = $self->{schema}->resultset ('Duo')->find (\%search); $key = $row->get_column ('du_key'); }; @@ -84,7 +86,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) = @_; + my ($class, $type, $name, $schema, $creator, $host, $time, $duo_type) = @_; # We have to have a Duo integration key file set. if (not $Wallet::Config::DUO_KEY_FILE) { @@ -104,10 +106,11 @@ sub create { # Create the object in Duo. require Net::Duo::Admin::Integration; + $duo_type ||= $Wallet::Config::DUO_TYPE; my %data = ( - name => "$name ($Wallet::Config::DUO_TYPE)", + name => "$name ($duo_type)", notes => 'Managed by wallet', - type => $Wallet::Config::DUO_TYPE, + type => $duo_type, ); my $integration = Net::Duo::Admin::Integration->create ($duo, \%data); @@ -121,6 +124,7 @@ sub create { eval { my %record = ( du_name => $name, + du_type => $type, du_key => $integration->integration_key, ); $self->{schema}->resultset ('Duo')->create (\%record); @@ -147,7 +151,9 @@ sub destroy { my $schema = $self->{schema}; my $guard = $schema->txn_scope_guard; eval { - my %search = (du_name => $self->{name}); + my %search = (du_name => $self->{name}, + du_type => $self->{type}, + ); my $row = $schema->resultset ('Duo')->find (\%search); my $key = $row->get_column ('du_key'); my $int = Net::Duo::Admin::Integration->new ($self->{duo}, $key); @@ -178,7 +184,9 @@ sub get { # Retrieve the integration from Duo. my $key; eval { - my %search = (du_name => $self->{name}); + my %search = (du_name => $self->{name}, + du_type => $self->{type}, + ); my $row = $self->{schema}->resultset ('Duo')->find (\%search); $key = $row->get_column ('du_key'); }; @@ -194,10 +202,10 @@ sub get { 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"; + 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); @@ -234,12 +242,11 @@ create a Duo integration, return a configuration file containing the key and API information for that integration, and delete the integration from Duo when the wallet object is destroyed. -Currently, only one configured integration type can be managed by the -wallet, and the integration information is always returned in the -configuration file format expected by the Duo UNIX integration. The -results of retrieving this object will be text, suitable for putting in -the UNIX integration configuration file, containing the integration key, -secret key, and admin hostname for that integration. +Usually you will want to use one of the subclasses of this module, which +override the output to give you a configuration fragment suited for a +specific application type. However, you can always use this module for +generic integrations where you don't mind massaging the output into the +configuration for the application using Duo. This object can be retrieved repeatedly without changing the secret key, matching Duo's native behavior with integrations. To change the keys of @@ -258,7 +265,7 @@ implementation. =over 4 -=item create(TYPE, NAME, DBH, PRINCIPAL, HOSTNAME [, DATETIME]) +=item create(TYPE, NAME, DBH, PRINCIPAL, HOSTNAME [, DATETIME, INTEGRATION_TYPE]) This is a class method and should be called on the Wallet::Object::Duo class. It creates a new object with the given TYPE and NAME (TYPE is @@ -272,9 +279,9 @@ time is used. When a new Duo integration object is created, a new integration will be created in the configured Duo account and the integration key will be stored in the wallet object. If the integration already exists, create() -will fail. The new integration's type is controlled by the DUO_TYPE -configuration variable, which defaults to C<unix>. See L<Wallet::Config> -for more information. +will fail. If an integration type isn't given, the new integration's type +is controlled by the DUO_TYPE configuration variable, which defaults to +C<unix>. See L<Wallet::Config> for more information. If create() fails, it throws an exception. @@ -314,9 +321,6 @@ isn't given, the current time is used. =head1 LIMITATIONS Only one Duo account is supported for a given wallet implementation. -Currently, only one Duo integration type is supported as well. Further -development should expand the available integration types, possibly as -additional wallet object types. =head1 SEE ALSO diff --git a/perl/lib/Wallet/Object/Duo/LDAPProxy.pm b/perl/lib/Wallet/Object/Duo/LDAPProxy.pm new file mode 100644 index 0000000..74ff43c --- /dev/null +++ b/perl/lib/Wallet/Object/Duo/LDAPProxy.pm @@ -0,0 +1,202 @@ +# Wallet::Object::Duo::LDAPProxy -- Duo auth proxy integration for LDAP +# +# Written by Jon Robertson <jonrober@stanford.edu> +# 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<Wallet::Config> for details on supported configuration parameters and +information about how to set wallet configuration. + +=head1 METHODS + +This object mostly inherits from Wallet::Object::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<duo-ldap> 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 = <integration-key> + skey = <secret-key> + api_host = <api-hostname> + +The C<host> parameter will be taken from the configuration file pointed +to by the DUO_KEY_FILE configuration variable. + +PRINCIPAL, HOSTNAME, and DATETIME are stored as history information. +PRINCIPAL should be the user who is downloading the keytab. If DATETIME +isn't given, the current time is used. + +=back + +=head1 LIMITATIONS + +Only one Duo account is supported for a given wallet implementation. + +=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<http://www.eyrie.org/~eagle/software/wallet/>. + +=head1 AUTHORS + +Jon Robertson <jonrober@stanford.edu> + +=cut diff --git a/perl/lib/Wallet/Object/Duo/PAM.pm b/perl/lib/Wallet/Object/Duo/PAM.pm new file mode 100644 index 0000000..6f90ba1 --- /dev/null +++ b/perl/lib/Wallet/Object/Duo/PAM.pm @@ -0,0 +1,205 @@ +# Wallet::Object::Duo::PAM -- Duo PAM int. object implementation for wallet +# +# Written by Russ Allbery <eagle@eyrie.org> +# Jon Robertson <jonrober@stanford.edu> +# 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<Wallet::Config> for details on supported configuration parameters and +information about how to set wallet configuration. + +=head1 METHODS + +This object mostly inherits from Wallet::Object::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<duo-pam> 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 = <integration-key> + skey = <secret-key> + host = <api-hostname> + +The C<host> parameter will be taken from the configuration file pointed +to by the DUO_KEY_FILE configuration variable. + +PRINCIPAL, HOSTNAME, and DATETIME are stored as history information. +PRINCIPAL should be the user who is downloading the keytab. If DATETIME +isn't given, the current time is used. + +=back + +=head1 LIMITATIONS + +Only one Duo account is supported for a given wallet implementation. + +=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<http://www.eyrie.org/~eagle/software/wallet/>. + +=head1 AUTHORS + +Russ Allbery <eagle@eyrie.org> +Jon Robertson <eagle@eyrie.org> + +=cut diff --git a/perl/lib/Wallet/Object/Duo/RDP.pm b/perl/lib/Wallet/Object/Duo/RDP.pm new file mode 100644 index 0000000..2e975fc --- /dev/null +++ b/perl/lib/Wallet/Object/Duo/RDP.pm @@ -0,0 +1,204 @@ +# Wallet::Object::Duo::RDP -- Duo RDP int. object implementation for wallet +# +# Written by Russ Allbery <eagle@eyrie.org> +# Jon Robertson <jonrober@stanford.edu> +# 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<Wallet::Config> for details on supported configuration parameters and +information about how to set wallet configuration. + +=head1 METHODS + +This object mostly inherits from Wallet::Object::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<duo-pam> 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: <integration-key> + Secret key: <secret-key> + Host: <api-hostname> + +The C<host> parameter will be taken from the configuration file pointed +to by the DUO_KEY_FILE configuration variable. + +PRINCIPAL, HOSTNAME, and DATETIME are stored as history information. +PRINCIPAL should be the user who is downloading the keytab. If DATETIME +isn't given, the current time is used. + +=back + +=head1 LIMITATIONS + +Only one Duo account is supported for a given wallet implementation. + +=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<http://www.eyrie.org/~eagle/software/wallet/>. + +=head1 AUTHORS + +Russ Allbery <eagle@eyrie.org> +Jon Robertson <eagle@eyrie.org> + +=cut diff --git a/perl/lib/Wallet/Object/Duo/RadiusProxy.pm b/perl/lib/Wallet/Object/Duo/RadiusProxy.pm new file mode 100644 index 0000000..faa0c2f --- /dev/null +++ b/perl/lib/Wallet/Object/Duo/RadiusProxy.pm @@ -0,0 +1,204 @@ +# Wallet::Object::Duo::RadiusProxy -- Duo auth proxy integration for radius +# +# Written by Jon Robertson <jonrober@stanford.edu> +# 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<Wallet::Config> for details on supported configuration parameters and +information about how to set wallet configuration. + +=head1 METHODS + +This object mostly inherits from Wallet::Object::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<duo-radius> 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 = <integration-key> + skey = <secret-key> + api_host = <api-hostname> + client = radius_client + +The C<host> parameter will be taken from the configuration file pointed +to by the DUO_KEY_FILE configuration variable. + +PRINCIPAL, HOSTNAME, and DATETIME are stored as history information. +PRINCIPAL should be the user who is downloading the keytab. If DATETIME +isn't given, the current time is used. + +=back + +=head1 LIMITATIONS + +Only one Duo account is supported for a given wallet implementation. + +=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<http://www.eyrie.org/~eagle/software/wallet/>. + +=head1 AUTHORS + +Jon Robertson <jonrober@stanford.edu> + +=cut diff --git a/perl/lib/Wallet/Object/File.pm b/perl/lib/Wallet/Object/File.pm index 1ff1288..226e32c 100644 --- a/perl/lib/Wallet/Object/File.pm +++ b/perl/lib/Wallet/Object/File.pm @@ -18,6 +18,7 @@ use warnings; use vars qw(@ISA $VERSION); use Digest::MD5 qw(md5_hex); +use File::Copy qw(move); use Wallet::Config (); use Wallet::Object::Base; @@ -55,6 +56,59 @@ sub file_path { return "$Wallet::Config::FILE_BUCKET/$hash/$name"; } +# Rename a file object. This includes renaming both the object itself, and +# updating the file location for that object. +sub rename { + my ($self, $new_name, $user, $host, $time) = @_; + $time ||= time; + + my $old_name = $self->{name}; + my $type = $self->{type}; + my $schema = $self->{schema}; + my $old_path = $self->file_path; + + eval { + + # Find the current object record. + my $guard = $schema->txn_scope_guard; + my %search = (ob_type => $type, + ob_name => $old_name); + my $object = $schema->resultset('Object')->find (\%search); + die "cannot find ${type}:${old_name}\n" + unless ($object and $object->ob_name eq $old_name); + + # Update the object name but don't yet commit. + $object->ob_name ($new_name); + + # Update the file to the path for the new name, and die if we can't. + # If the old path isn't there, then assume we haven't yet stored and + # keep going. + if ($old_path) { + $self->{name} = $new_name; + my $new_path = $self->file_path; + move($old_path, $new_path) or die $!; + } + + $object->update; + $guard->commit; + }; + if ($@) { + $self->{name} = $old_name; + $self->error ("cannot rename object $type $old_name: $!"); + return; + } + + eval { + $self->log_set ('name', $old_name, $new_name, $user, $host, $time); + }; + if ($@) { + $self->error ("object $type $old_name was renamed but not logged: $!"); + return 1; + } + + return 1; +} + ############################################################################## # Core methods ############################################################################## @@ -145,7 +199,7 @@ API HOSTNAME DATETIME keytab remctld backend nul Allbery wallet-backend my @name = qw(file mysql-lsdb) my @trace = ($user, $host, time); - my $object = Wallet::Object::Keytab->create (@name, $schema, @trace); + my $object = Wallet::Object::File->create (@name, $schema, @trace); unless ($object->store ("the-password\n")) { die $object->error, "\n"; } diff --git a/perl/lib/Wallet/Policy/Stanford.pm b/perl/lib/Wallet/Policy/Stanford.pm index 5ac29e0..a392476 100644 --- a/perl/lib/Wallet/Policy/Stanford.pm +++ b/perl/lib/Wallet/Policy/Stanford.pm @@ -174,6 +174,13 @@ sub _host_for_keytab { return $host; } +# Map a duo-type object name to a hostname. Currently all Duo objects are +# named just for the hostname, so this is easy. +sub _host_for_duo { + my ($name) = @_; + return $name; +} + # 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 @@ -183,8 +190,13 @@ sub default_owner { # How to determine the host for host-based objects. my %host_for = ( - keytab => \&_host_for_keytab, - file => \&_host_for_file, + 'keytab' => \&_host_for_keytab, + 'file' => \&_host_for_file, + '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. @@ -368,6 +380,14 @@ sub verify_name { } } + # Check the naming conventions for all Duo object types. The object + # should simply be the host name for now. + if ($type =~ m{^duo(-\w+)?$}) { + if ($name !~ m{ [.] }xms) { + return "host name $name is not fully qualified"; + } + } + # Success. return; } diff --git a/perl/lib/Wallet/Schema.pm b/perl/lib/Wallet/Schema.pm index cb4c93e..5b850c0 100644 --- a/perl/lib/Wallet/Schema.pm +++ b/perl/lib/Wallet/Schema.pm @@ -18,7 +18,7 @@ use base 'DBIx::Class::Schema'; # This version should be increased on any code change to this module. Always # use two digits for the minor version with a leading zero if necessary so # that it will sort properly. -our $VERSION = '0.09'; +our $VERSION = '0.10'; __PACKAGE__->load_namespaces; __PACKAGE__->load_components (qw/Schema::Versioned/); diff --git a/perl/lib/Wallet/Schema/Result/Duo.pm b/perl/lib/Wallet/Schema/Result/Duo.pm index 80a71dc..6ad61e9 100644 --- a/perl/lib/Wallet/Schema/Result/Duo.pm +++ b/perl/lib/Wallet/Schema/Result/Duo.pm @@ -45,9 +45,19 @@ __PACKAGE__->table("duo"); __PACKAGE__->add_columns( "du_name", { data_type => "varchar", is_nullable => 0, size => 255 }, + "du_type", + { data_type => "varchar", is_nullable => 0, size => 16 }, "du_key", { data_type => "varchar", is_nullable => 0, size => 255 }, ); -__PACKAGE__->set_primary_key("du_name"); - +__PACKAGE__->set_primary_key("du_name", "du_type"); + +__PACKAGE__->belongs_to( + 'object', + 'Wallet::Schema::Result::Object', + { + 'foreign.ob_type' => 'self.du_type', + 'foreign.ob_name' => 'self.du_name', + }, + ); 1; diff --git a/perl/lib/Wallet/Server.pm b/perl/lib/Wallet/Server.pm index 95fd4e6..f6ea342 100644 --- a/perl/lib/Wallet/Server.pm +++ b/perl/lib/Wallet/Server.pm @@ -244,6 +244,52 @@ sub autocreate { return 1; } +# Rename an object. We validate that the new name also falls within naming +# constraints, then need to change all references to that. If any updates +# fail, we'll revert the entire commit. +sub rename { + my ($self, $type, $name, $new_name) = @_; + + my $schema = $self->{schema}; + my $user = $self->{user}; + my $host = $self->{host}; + + # Currently we only can rename file objects. + if ($type ne 'file') { + $self->error ('rename is only supported for file objects'); + return; + } + + # Validate the new name. + if (defined (&Wallet::Config::verify_name)) { + my $error = Wallet::Config::verify_name ($type, $new_name, $user); + if ($error) { + $self->error ("${type}:${name} rejected: $error"); + return; + } + } + + # Get the object and error if it does not already exist. + my $class = $self->type_mapping ($type); + unless ($class) { + $self->error ("unknown object type $type"); + return; + } + my $object = eval { $class->new ($type, $name, $schema) }; + if ($@) { + $self->error ($@); + return; + } + + # Rename the object. + eval { $object->rename ($new_name, $schema, $user, $host) }; + if ($@) { + $self->error ($@); + return; + } + return $object; +} + # Given the name and type of an object, returns a Perl object representing it # or returns undef and sets the internal error. sub retrieve { diff --git a/perl/sql/Wallet-Schema-0.09-0.10-MySQL.sql b/perl/sql/Wallet-Schema-0.09-0.10-MySQL.sql new file mode 100644 index 0000000..3c54c6d --- /dev/null +++ b/perl/sql/Wallet-Schema-0.09-0.10-MySQL.sql @@ -0,0 +1,14 @@ +-- Convert schema 'sql/Wallet-Schema-0.09-MySQL.sql' to 'Wallet::Schema v0.10':; + +BEGIN; + +ALTER TABLE duo DROP PRIMARY KEY, + ADD COLUMN du_type varchar(16) NOT NULL, + ADD INDEX duo_idx_du_type_du_name (du_type, du_name), + ADD PRIMARY KEY (du_name, du_type), + ADD CONSTRAINT duo_fk_du_type_du_name FOREIGN KEY (du_type, du_name) REFERENCES objects (ob_type, ob_name), + ENGINE=InnoDB; + + +COMMIT; + diff --git a/perl/sql/Wallet-Schema-0.09-0.10-PostgreSQL.sql b/perl/sql/Wallet-Schema-0.09-0.10-PostgreSQL.sql new file mode 100644 index 0000000..c69e6a5 --- /dev/null +++ b/perl/sql/Wallet-Schema-0.09-0.10-PostgreSQL.sql @@ -0,0 +1,18 @@ +-- Convert schema 'sql/Wallet-Schema-0.09-PostgreSQL.sql' to 'sql/Wallet-Schema-0.10-PostgreSQL.sql':; + +BEGIN; + +ALTER TABLE duo DROP CONSTRAINT duo_pkey; + +ALTER TABLE duo ADD COLUMN du_type character varying(16) NOT NULL; + +CREATE INDEX duo_idx_du_type_du_name on duo (du_type, du_name); + +ALTER TABLE duo ADD PRIMARY KEY (du_name, du_type); + +ALTER TABLE duo ADD CONSTRAINT duo_fk_du_type_du_name FOREIGN KEY (du_type, du_name) + REFERENCES objects (ob_type, ob_name) DEFERRABLE; + + +COMMIT; + diff --git a/perl/sql/Wallet-Schema-0.09-0.10-SQLite.sql b/perl/sql/Wallet-Schema-0.09-0.10-SQLite.sql new file mode 100644 index 0000000..5feb89f --- /dev/null +++ b/perl/sql/Wallet-Schema-0.09-0.10-SQLite.sql @@ -0,0 +1,26 @@ +-- Convert schema 'sql/Wallet-Schema-0.09-SQLite.sql' to 'sql/Wallet-Schema-0.10-SQLite.sql':; + +BEGIN; + +-- Back up Duo data to a temp table. SQLite has limited ALTER TABLE support, +-- so we need to do this to alter the keys on the table. +CREATE TEMPORARY TABLE duo_backup ( + du_name varchar(255) NOT NULL, + du_key varchar(255) NOT NULL, + PRIMARY KEY (du_name) +); +INSERT INTO duo_backup SELECT du_name,du_key FROM duo; +DROP TABLE duo; + +-- Create the new Duo table and move the old data into it. +CREATE TABLE duo ( + du_name varchar(255) NOT NULL, + du_type varchar(16) NOT NULL, + du_key varchar(255) NOT NULL, + PRIMARY KEY (du_name, du_type), + FOREIGN KEY (du_type, du_name) REFERENCES objects(ob_type, ob_name) +); +INSERT INTO duo SELECT du_name,du_key,'duo' FROM duo_backup; +DROP TABLE duo_backup; + +COMMIT; diff --git a/perl/sql/Wallet-Schema-0.10-MySQL.sql b/perl/sql/Wallet-Schema-0.10-MySQL.sql new file mode 100644 index 0000000..c0b7fcc --- /dev/null +++ b/perl/sql/Wallet-Schema-0.10-MySQL.sql @@ -0,0 +1,209 @@ +-- +-- Created by SQL::Translator::Producer::MySQL +-- Created on Thu Oct 9 20:54:55 2014 +-- +SET foreign_key_checks=0; + +DROP TABLE IF EXISTS `acl_history`; + +-- +-- Table: `acl_history` +-- +CREATE TABLE `acl_history` ( + `ah_id` integer NOT NULL auto_increment, + `ah_acl` integer NOT NULL, + `ah_name` varchar(255) NULL, + `ah_action` varchar(16) NOT NULL, + `ah_scheme` varchar(32) NULL, + `ah_identifier` varchar(255) NULL, + `ah_by` varchar(255) NOT NULL, + `ah_from` varchar(255) NOT NULL, + `ah_on` datetime NOT NULL, + INDEX `acl_history_idx_ah_acl` (`ah_acl`), + INDEX `acl_history_idx_ah_name` (`ah_name`), + PRIMARY KEY (`ah_id`) +); + +DROP TABLE IF EXISTS `acl_schemes`; + +-- +-- Table: `acl_schemes` +-- +CREATE TABLE `acl_schemes` ( + `as_name` varchar(32) NOT NULL, + `as_class` varchar(64) NULL, + PRIMARY KEY (`as_name`) +) ENGINE=InnoDB; + +DROP TABLE IF EXISTS `acls`; + +-- +-- Table: `acls` +-- +CREATE TABLE `acls` ( + `ac_id` integer NOT NULL auto_increment, + `ac_name` varchar(255) NOT NULL, + PRIMARY KEY (`ac_id`), + UNIQUE `ac_name` (`ac_name`) +) ENGINE=InnoDB; + +DROP TABLE IF EXISTS `enctypes`; + +-- +-- Table: `enctypes` +-- +CREATE TABLE `enctypes` ( + `en_name` varchar(255) NOT NULL, + PRIMARY KEY (`en_name`) +); + +DROP TABLE IF EXISTS `flags`; + +-- +-- Table: `flags` +-- +CREATE TABLE `flags` ( + `fl_type` varchar(16) NOT NULL, + `fl_name` varchar(255) NOT NULL, + `fl_flag` enum('locked', 'unchanging') NOT NULL, + PRIMARY KEY (`fl_type`, `fl_name`, `fl_flag`) +); + +DROP TABLE IF EXISTS `keytab_enctypes`; + +-- +-- Table: `keytab_enctypes` +-- +CREATE TABLE `keytab_enctypes` ( + `ke_name` varchar(255) NOT NULL, + `ke_enctype` varchar(255) NOT NULL, + PRIMARY KEY (`ke_name`, `ke_enctype`) +); + +DROP TABLE IF EXISTS `keytab_sync`; + +-- +-- Table: `keytab_sync` +-- +CREATE TABLE `keytab_sync` ( + `ks_name` varchar(255) NOT NULL, + `ks_target` varchar(255) NOT NULL, + PRIMARY KEY (`ks_name`, `ks_target`) +); + +DROP TABLE IF EXISTS `object_history`; + +-- +-- Table: `object_history` +-- +CREATE TABLE `object_history` ( + `oh_id` integer NOT NULL auto_increment, + `oh_type` varchar(16) NOT NULL, + `oh_name` varchar(255) NOT NULL, + `oh_action` varchar(16) NOT NULL, + `oh_field` varchar(16) NULL, + `oh_type_field` varchar(255) NULL, + `oh_old` varchar(255) NULL, + `oh_new` varchar(255) NULL, + `oh_by` varchar(255) NOT NULL, + `oh_from` varchar(255) NOT NULL, + `oh_on` datetime NOT NULL, + INDEX `object_history_idx_oh_type_oh_name` (`oh_type`, `oh_name`), + PRIMARY KEY (`oh_id`) +); + +DROP TABLE IF EXISTS `sync_targets`; + +-- +-- Table: `sync_targets` +-- +CREATE TABLE `sync_targets` ( + `st_name` varchar(255) NOT NULL, + PRIMARY KEY (`st_name`) +); + +DROP TABLE IF EXISTS `types`; + +-- +-- Table: `types` +-- +CREATE TABLE `types` ( + `ty_name` varchar(16) NOT NULL, + `ty_class` varchar(64) NULL, + PRIMARY KEY (`ty_name`) +) ENGINE=InnoDB; + +DROP TABLE IF EXISTS `acl_entries`; + +-- +-- Table: `acl_entries` +-- +CREATE TABLE `acl_entries` ( + `ae_id` integer NOT NULL, + `ae_scheme` varchar(32) NOT NULL, + `ae_identifier` varchar(255) NOT NULL, + INDEX `acl_entries_idx_ae_scheme` (`ae_scheme`), + INDEX `acl_entries_idx_ae_id` (`ae_id`), + PRIMARY KEY (`ae_id`, `ae_scheme`, `ae_identifier`), + CONSTRAINT `acl_entries_fk_ae_scheme` FOREIGN KEY (`ae_scheme`) REFERENCES `acl_schemes` (`as_name`), + CONSTRAINT `acl_entries_fk_ae_id` FOREIGN KEY (`ae_id`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB; + +DROP TABLE IF EXISTS `objects`; + +-- +-- Table: `objects` +-- +CREATE TABLE `objects` ( + `ob_type` varchar(16) NOT NULL, + `ob_name` varchar(255) NOT NULL, + `ob_owner` integer NULL, + `ob_acl_get` integer NULL, + `ob_acl_store` integer NULL, + `ob_acl_show` integer NULL, + `ob_acl_destroy` integer NULL, + `ob_acl_flags` integer NULL, + `ob_expires` datetime NULL, + `ob_created_by` varchar(255) NOT NULL, + `ob_created_from` varchar(255) NOT NULL, + `ob_created_on` datetime NOT NULL, + `ob_stored_by` varchar(255) NULL, + `ob_stored_from` varchar(255) NULL, + `ob_stored_on` datetime NULL, + `ob_downloaded_by` varchar(255) NULL, + `ob_downloaded_from` varchar(255) NULL, + `ob_downloaded_on` datetime NULL, + `ob_comment` varchar(255) NULL, + INDEX `objects_idx_ob_acl_destroy` (`ob_acl_destroy`), + INDEX `objects_idx_ob_acl_flags` (`ob_acl_flags`), + INDEX `objects_idx_ob_acl_get` (`ob_acl_get`), + INDEX `objects_idx_ob_owner` (`ob_owner`), + INDEX `objects_idx_ob_acl_show` (`ob_acl_show`), + INDEX `objects_idx_ob_acl_store` (`ob_acl_store`), + INDEX `objects_idx_ob_type` (`ob_type`), + PRIMARY KEY (`ob_name`, `ob_type`), + CONSTRAINT `objects_fk_ob_acl_destroy` FOREIGN KEY (`ob_acl_destroy`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `objects_fk_ob_acl_flags` FOREIGN KEY (`ob_acl_flags`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `objects_fk_ob_acl_get` FOREIGN KEY (`ob_acl_get`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `objects_fk_ob_owner` FOREIGN KEY (`ob_owner`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `objects_fk_ob_acl_show` FOREIGN KEY (`ob_acl_show`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `objects_fk_ob_acl_store` FOREIGN KEY (`ob_acl_store`) REFERENCES `acls` (`ac_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `objects_fk_ob_type` FOREIGN KEY (`ob_type`) REFERENCES `types` (`ty_name`) +) ENGINE=InnoDB; + +DROP TABLE IF EXISTS `duo`; + +-- +-- Table: `duo` +-- +CREATE TABLE `duo` ( + `du_name` varchar(255) NOT NULL, + `du_type` varchar(16) NOT NULL, + `du_key` varchar(255) NOT NULL, + INDEX `duo_idx_du_type_du_name` (`du_type`, `du_name`), + PRIMARY KEY (`du_name`, `du_type`), + CONSTRAINT `duo_fk_du_type_du_name` FOREIGN KEY (`du_type`, `du_name`) REFERENCES `objects` (`ob_type`, `ob_name`) +) ENGINE=InnoDB; + +SET foreign_key_checks=1; + diff --git a/perl/sql/Wallet-Schema-0.10-PostgreSQL.sql b/perl/sql/Wallet-Schema-0.10-PostgreSQL.sql new file mode 100644 index 0000000..3bcb0ae --- /dev/null +++ b/perl/sql/Wallet-Schema-0.10-PostgreSQL.sql @@ -0,0 +1,216 @@ +-- +-- Created by SQL::Translator::Producer::PostgreSQL +-- Created on Thu Oct 9 20:54:56 2014 +-- +-- +-- Table: acl_history. +-- +DROP TABLE "acl_history" CASCADE; +CREATE TABLE "acl_history" ( + "ah_id" serial NOT NULL, + "ah_acl" integer NOT NULL, + "ah_name" character varying(255), + "ah_action" character varying(16) NOT NULL, + "ah_scheme" character varying(32), + "ah_identifier" character varying(255), + "ah_by" character varying(255) NOT NULL, + "ah_from" character varying(255) NOT NULL, + "ah_on" timestamp NOT NULL, + PRIMARY KEY ("ah_id") +); +CREATE INDEX "acl_history_idx_ah_acl" on "acl_history" ("ah_acl"); +CREATE INDEX "acl_history_idx_ah_name" on "acl_history" ("ah_name"); + +-- +-- Table: acl_schemes. +-- +DROP TABLE "acl_schemes" CASCADE; +CREATE TABLE "acl_schemes" ( + "as_name" character varying(32) NOT NULL, + "as_class" character varying(64), + PRIMARY KEY ("as_name") +); + +-- +-- Table: acls. +-- +DROP TABLE "acls" CASCADE; +CREATE TABLE "acls" ( + "ac_id" serial NOT NULL, + "ac_name" character varying(255) NOT NULL, + PRIMARY KEY ("ac_id"), + CONSTRAINT "ac_name" UNIQUE ("ac_name") +); + +-- +-- Table: enctypes. +-- +DROP TABLE "enctypes" CASCADE; +CREATE TABLE "enctypes" ( + "en_name" character varying(255) NOT NULL, + PRIMARY KEY ("en_name") +); + +-- +-- Table: flags. +-- +DROP TABLE "flags" CASCADE; +CREATE TABLE "flags" ( + "fl_type" character varying(16) NOT NULL, + "fl_name" character varying(255) NOT NULL, + "fl_flag" character varying NOT NULL, + PRIMARY KEY ("fl_type", "fl_name", "fl_flag") +); + +-- +-- Table: keytab_enctypes. +-- +DROP TABLE "keytab_enctypes" CASCADE; +CREATE TABLE "keytab_enctypes" ( + "ke_name" character varying(255) NOT NULL, + "ke_enctype" character varying(255) NOT NULL, + PRIMARY KEY ("ke_name", "ke_enctype") +); + +-- +-- Table: keytab_sync. +-- +DROP TABLE "keytab_sync" CASCADE; +CREATE TABLE "keytab_sync" ( + "ks_name" character varying(255) NOT NULL, + "ks_target" character varying(255) NOT NULL, + PRIMARY KEY ("ks_name", "ks_target") +); + +-- +-- Table: object_history. +-- +DROP TABLE "object_history" CASCADE; +CREATE TABLE "object_history" ( + "oh_id" serial NOT NULL, + "oh_type" character varying(16) NOT NULL, + "oh_name" character varying(255) NOT NULL, + "oh_action" character varying(16) NOT NULL, + "oh_field" character varying(16), + "oh_type_field" character varying(255), + "oh_old" character varying(255), + "oh_new" character varying(255), + "oh_by" character varying(255) NOT NULL, + "oh_from" character varying(255) NOT NULL, + "oh_on" timestamp NOT NULL, + PRIMARY KEY ("oh_id") +); +CREATE INDEX "object_history_idx_oh_type_oh_name" on "object_history" ("oh_type", "oh_name"); + +-- +-- Table: sync_targets. +-- +DROP TABLE "sync_targets" CASCADE; +CREATE TABLE "sync_targets" ( + "st_name" character varying(255) NOT NULL, + PRIMARY KEY ("st_name") +); + +-- +-- Table: types. +-- +DROP TABLE "types" CASCADE; +CREATE TABLE "types" ( + "ty_name" character varying(16) NOT NULL, + "ty_class" character varying(64), + PRIMARY KEY ("ty_name") +); + +-- +-- Table: acl_entries. +-- +DROP TABLE "acl_entries" CASCADE; +CREATE TABLE "acl_entries" ( + "ae_id" integer NOT NULL, + "ae_scheme" character varying(32) NOT NULL, + "ae_identifier" character varying(255) NOT NULL, + PRIMARY KEY ("ae_id", "ae_scheme", "ae_identifier") +); +CREATE INDEX "acl_entries_idx_ae_scheme" on "acl_entries" ("ae_scheme"); +CREATE INDEX "acl_entries_idx_ae_id" on "acl_entries" ("ae_id"); + +-- +-- Table: objects. +-- +DROP TABLE "objects" CASCADE; +CREATE TABLE "objects" ( + "ob_type" character varying(16) NOT NULL, + "ob_name" character varying(255) NOT NULL, + "ob_owner" integer, + "ob_acl_get" integer, + "ob_acl_store" integer, + "ob_acl_show" integer, + "ob_acl_destroy" integer, + "ob_acl_flags" integer, + "ob_expires" timestamp, + "ob_created_by" character varying(255) NOT NULL, + "ob_created_from" character varying(255) NOT NULL, + "ob_created_on" timestamp NOT NULL, + "ob_stored_by" character varying(255), + "ob_stored_from" character varying(255), + "ob_stored_on" timestamp, + "ob_downloaded_by" character varying(255), + "ob_downloaded_from" character varying(255), + "ob_downloaded_on" timestamp, + "ob_comment" character varying(255), + PRIMARY KEY ("ob_name", "ob_type") +); +CREATE INDEX "objects_idx_ob_acl_destroy" on "objects" ("ob_acl_destroy"); +CREATE INDEX "objects_idx_ob_acl_flags" on "objects" ("ob_acl_flags"); +CREATE INDEX "objects_idx_ob_acl_get" on "objects" ("ob_acl_get"); +CREATE INDEX "objects_idx_ob_owner" on "objects" ("ob_owner"); +CREATE INDEX "objects_idx_ob_acl_show" on "objects" ("ob_acl_show"); +CREATE INDEX "objects_idx_ob_acl_store" on "objects" ("ob_acl_store"); +CREATE INDEX "objects_idx_ob_type" on "objects" ("ob_type"); + +-- +-- Table: duo. +-- +DROP TABLE "duo" CASCADE; +CREATE TABLE "duo" ( + "du_name" character varying(255) NOT NULL, + "du_type" character varying(16) NOT NULL, + "du_key" character varying(255) NOT NULL, + PRIMARY KEY ("du_name", "du_type") +); +CREATE INDEX "duo_idx_du_type_du_name" on "duo" ("du_type", "du_name"); + +-- +-- Foreign Key Definitions +-- + +ALTER TABLE "acl_entries" ADD CONSTRAINT "acl_entries_fk_ae_scheme" FOREIGN KEY ("ae_scheme") + REFERENCES "acl_schemes" ("as_name") DEFERRABLE; + +ALTER TABLE "acl_entries" ADD CONSTRAINT "acl_entries_fk_ae_id" FOREIGN KEY ("ae_id") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_acl_destroy" FOREIGN KEY ("ob_acl_destroy") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_acl_flags" FOREIGN KEY ("ob_acl_flags") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_acl_get" FOREIGN KEY ("ob_acl_get") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_owner" FOREIGN KEY ("ob_owner") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_acl_show" FOREIGN KEY ("ob_acl_show") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_acl_store" FOREIGN KEY ("ob_acl_store") + REFERENCES "acls" ("ac_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +ALTER TABLE "objects" ADD CONSTRAINT "objects_fk_ob_type" FOREIGN KEY ("ob_type") + REFERENCES "types" ("ty_name") DEFERRABLE; + +ALTER TABLE "duo" ADD CONSTRAINT "duo_fk_du_type_du_name" FOREIGN KEY ("du_type", "du_name") + REFERENCES "objects" ("ob_type", "ob_name") DEFERRABLE; + diff --git a/perl/sql/Wallet-Schema-0.10-SQLite.sql b/perl/sql/Wallet-Schema-0.10-SQLite.sql new file mode 100644 index 0000000..94a185c --- /dev/null +++ b/perl/sql/Wallet-Schema-0.10-SQLite.sql @@ -0,0 +1,220 @@ +-- +-- Created by SQL::Translator::Producer::SQLite +-- Created on Thu Oct 9 20:51:25 2014 +-- + +BEGIN TRANSACTION; + +-- +-- Table: acl_history +-- +DROP TABLE IF EXISTS acl_history; + +CREATE TABLE acl_history ( + ah_id INTEGER PRIMARY KEY NOT NULL, + ah_acl integer NOT NULL, + ah_name varchar(255), + ah_action varchar(16) NOT NULL, + ah_scheme varchar(32), + ah_identifier varchar(255), + ah_by varchar(255) NOT NULL, + ah_from varchar(255) NOT NULL, + ah_on datetime NOT NULL +); + +CREATE INDEX acl_history_idx_ah_acl ON acl_history (ah_acl); + +CREATE INDEX acl_history_idx_ah_name ON acl_history (ah_name); + +-- +-- Table: acl_schemes +-- +DROP TABLE IF EXISTS acl_schemes; + +CREATE TABLE acl_schemes ( + as_name varchar(32) NOT NULL, + as_class varchar(64), + PRIMARY KEY (as_name) +); + +-- +-- Table: acls +-- +DROP TABLE IF EXISTS acls; + +CREATE TABLE acls ( + ac_id INTEGER PRIMARY KEY NOT NULL, + ac_name varchar(255) NOT NULL +); + +CREATE UNIQUE INDEX ac_name ON acls (ac_name); + +-- +-- Table: enctypes +-- +DROP TABLE IF EXISTS enctypes; + +CREATE TABLE enctypes ( + en_name varchar(255) NOT NULL, + PRIMARY KEY (en_name) +); + +-- +-- Table: flags +-- +DROP TABLE IF EXISTS flags; + +CREATE TABLE flags ( + fl_type varchar(16) NOT NULL, + fl_name varchar(255) NOT NULL, + fl_flag enum NOT NULL, + PRIMARY KEY (fl_type, fl_name, fl_flag) +); + +-- +-- Table: keytab_enctypes +-- +DROP TABLE IF EXISTS keytab_enctypes; + +CREATE TABLE keytab_enctypes ( + ke_name varchar(255) NOT NULL, + ke_enctype varchar(255) NOT NULL, + PRIMARY KEY (ke_name, ke_enctype) +); + +-- +-- Table: keytab_sync +-- +DROP TABLE IF EXISTS keytab_sync; + +CREATE TABLE keytab_sync ( + ks_name varchar(255) NOT NULL, + ks_target varchar(255) NOT NULL, + PRIMARY KEY (ks_name, ks_target) +); + +-- +-- Table: object_history +-- +DROP TABLE IF EXISTS object_history; + +CREATE TABLE object_history ( + oh_id INTEGER PRIMARY KEY NOT NULL, + oh_type varchar(16) NOT NULL, + oh_name varchar(255) NOT NULL, + oh_action varchar(16) NOT NULL, + oh_field varchar(16), + oh_type_field varchar(255), + oh_old varchar(255), + oh_new varchar(255), + oh_by varchar(255) NOT NULL, + oh_from varchar(255) NOT NULL, + oh_on datetime NOT NULL +); + +CREATE INDEX object_history_idx_oh_type_oh_name ON object_history (oh_type, oh_name); + +-- +-- Table: sync_targets +-- +DROP TABLE IF EXISTS sync_targets; + +CREATE TABLE sync_targets ( + st_name varchar(255) NOT NULL, + PRIMARY KEY (st_name) +); + +-- +-- Table: types +-- +DROP TABLE IF EXISTS types; + +CREATE TABLE types ( + ty_name varchar(16) NOT NULL, + ty_class varchar(64), + PRIMARY KEY (ty_name) +); + +-- +-- Table: acl_entries +-- +DROP TABLE IF EXISTS acl_entries; + +CREATE TABLE acl_entries ( + ae_id integer NOT NULL, + ae_scheme varchar(32) NOT NULL, + ae_identifier varchar(255) NOT NULL, + PRIMARY KEY (ae_id, ae_scheme, ae_identifier), + FOREIGN KEY (ae_scheme) REFERENCES acl_schemes(as_name), + FOREIGN KEY (ae_id) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX acl_entries_idx_ae_scheme ON acl_entries (ae_scheme); + +CREATE INDEX acl_entries_idx_ae_id ON acl_entries (ae_id); + +-- +-- Table: objects +-- +DROP TABLE IF EXISTS objects; + +CREATE TABLE objects ( + ob_type varchar(16) NOT NULL, + ob_name varchar(255) NOT NULL, + ob_owner integer, + ob_acl_get integer, + ob_acl_store integer, + ob_acl_show integer, + ob_acl_destroy integer, + ob_acl_flags integer, + ob_expires datetime, + ob_created_by varchar(255) NOT NULL, + ob_created_from varchar(255) NOT NULL, + ob_created_on datetime NOT NULL, + ob_stored_by varchar(255), + ob_stored_from varchar(255), + ob_stored_on datetime, + ob_downloaded_by varchar(255), + ob_downloaded_from varchar(255), + ob_downloaded_on datetime, + ob_comment varchar(255), + PRIMARY KEY (ob_name, ob_type), + FOREIGN KEY (ob_acl_destroy) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (ob_acl_flags) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (ob_acl_get) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (ob_owner) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (ob_acl_show) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (ob_acl_store) REFERENCES acls(ac_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (ob_type) REFERENCES types(ty_name) +); + +CREATE INDEX objects_idx_ob_acl_destroy ON objects (ob_acl_destroy); + +CREATE INDEX objects_idx_ob_acl_flags ON objects (ob_acl_flags); + +CREATE INDEX objects_idx_ob_acl_get ON objects (ob_acl_get); + +CREATE INDEX objects_idx_ob_owner ON objects (ob_owner); + +CREATE INDEX objects_idx_ob_acl_show ON objects (ob_acl_show); + +CREATE INDEX objects_idx_ob_acl_store ON objects (ob_acl_store); + +CREATE INDEX objects_idx_ob_type ON objects (ob_type); + +-- +-- Table: duo +-- +DROP TABLE IF EXISTS duo; + +CREATE TABLE duo ( + du_name varchar(255) NOT NULL, + du_type varchar(16) NOT NULL, + du_key varchar(255) NOT NULL, + PRIMARY KEY (du_name, du_type), + FOREIGN KEY (du_type, du_name) REFERENCES objects(ob_type, ob_name) +); + +CREATE INDEX duo_idx_du_type_du_name ON duo (du_type, du_name); + +COMMIT; diff --git a/perl/t/data/duo/integration-ldap.json b/perl/t/data/duo/integration-ldap.json new file mode 100644 index 0000000..78a4c9f --- /dev/null +++ b/perl/t/data/duo/integration-ldap.json @@ -0,0 +1,11 @@ +{ + "enroll_policy": "enroll", + "greeting": "", + "groups_allowed": [], + "integration_key": "DIRWIH0ZZPV4G88B37VQ", + "name": "Integration for LDAP proxy", + "notes": "", + "secret_key": "QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o", + "type": "ldap", + "visual_style": "default" +} diff --git a/perl/t/data/duo/integration-radius.json b/perl/t/data/duo/integration-radius.json new file mode 100644 index 0000000..514a33e --- /dev/null +++ b/perl/t/data/duo/integration-radius.json @@ -0,0 +1,11 @@ +{ + "enroll_policy": "enroll", + "greeting": "", + "groups_allowed": [], + "integration_key": "DIRWIH0ZZPV4G88B37VQ", + "name": "Integration for Radius proxy", + "notes": "", + "secret_key": "QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o", + "type": "radius", + "visual_style": "default" +} diff --git a/perl/t/data/duo/integration-rdp.json b/perl/t/data/duo/integration-rdp.json new file mode 100644 index 0000000..28e925f --- /dev/null +++ b/perl/t/data/duo/integration-rdp.json @@ -0,0 +1,11 @@ +{ + "enroll_policy": "enroll", + "greeting": "", + "groups_allowed": [], + "integration_key": "DIRWIH0ZZPV4G88B37VQ", + "name": "Integration for Radius proxy", + "notes": "", + "secret_key": "QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o", + "type": "rdp", + "visual_style": "default" +} diff --git a/perl/t/general/admin.t b/perl/t/general/admin.t index 47396c6..17671b6 100755 --- a/perl/t/general/admin.t +++ b/perl/t/general/admin.t @@ -69,7 +69,7 @@ is ($admin->reinitialize ('admin@EXAMPLE.COM'), 1, SKIP: { my @path = (split (':', $ENV{PATH})); my ($sqlite) = grep { -x $_ } map { "$_/sqlite3" } @path; - skip 'sqlite3 not found', 5 unless $sqlite; + skip 'sqlite3 not found', 7 unless $sqlite; # Delete all tables and then redump them straight from the SQL file to # avoid getting the version table. diff --git a/perl/t/object/duo-ldap.t b/perl/t/object/duo-ldap.t new file mode 100644 index 0000000..3648eba --- /dev/null +++ b/perl/t/object/duo-ldap.t @@ -0,0 +1,160 @@ +#!/usr/bin/perl +# +# Tests for the Duo Auth proxy LDAP integration object implementation. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014 +# 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; + +BEGIN { + eval 'use Net::Duo'; + plan skip_all => 'Net::Duo required for testing duo' + if $@; + eval 'use Net::Duo::Mock::Agent'; + plan skip_all => 'Net::Duo::Mock::Agent required for testing duo' + if $@; +} + +BEGIN { + use_ok('Wallet::Admin'); + use_ok('Wallet::Config'); + use_ok('Wallet::Object::Duo::LDAPProxy'); +} + +use lib 't/lib'; +use Util; + +# Some global defaults to use. +my $user = 'admin@EXAMPLE.COM'; +my $host = 'localhost'; +my @trace = ($user, $host, time); +my $date = strftime ('%Y-%m-%d %H:%M:%S', localtime $trace[2]); + +# Flush all output immediately. +$| = 1; + +# Use Wallet::Admin to set up the database. +db_setup; +my $admin = eval { Wallet::Admin->new }; +is ($@, '', 'Database connection succeeded'); +is ($admin->reinitialize ($user), 1, 'Database initialization succeeded'); +my $schema = $admin->schema; + +# Create a mock object to use for Duo calls. +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); +}; +is ($object, undef, 'Wallet::Object::Duo::LDAPProxy 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); +}; +is ($object, undef, 'Wallet::Object::Duo::LDAPProxy creation with no config failed'); +is ($@, "duo object implementation not configured\n", '...with correct error'); + +# Set up the Duo configuration. +$Wallet::Config::DUO_AGENT = $mock; +$Wallet::Config::DUO_KEY_FILE = 't/data/duo/keys.json'; + +# Test creating an integration. +note ('Test creating an integration'); +my $expected = { + name => 'test (ldapproxy)', + notes => 'Managed by wallet', + type => 'ldapproxy', +}; +$mock->expect ( + { + method => 'POST', + uri => '/admin/v1/integrations', + content => $expected, + 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'); + +# Check the metadata about the new wallet object. +$expected = <<"EOO"; + Type: duo-ldap + Name: test + Duo key: DIRWIH0ZZPV4G88B37VQ + Created by: $user + Created from: $host + Created on: $date +EOO +is ($object->show, $expected, 'Show output is correct'); + +# Test retrieving the integration information. +note ('Test retrieving an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration-ldap.json', + } +); +my $data = $object->get (@trace); +ok (defined ($data), 'Retrieval succeeds'); +$expected = <<'EOO'; +[ldap_server_challenge] +ikey = DIRWIH0ZZPV4G88B37VQ +skey = QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o +api_host = example-admin.duosecurity.com +EOO +is ($data, $expected, '...and integration data is correct'); + +# Ensure that we can't retrieve the object when locked. +is ($object->flag_set ('locked', @trace), 1, + 'Setting object to locked succeeds'); +is ($object->get, undef, '...and now get fails'); +is ($object->error, 'cannot get duo-ldap:test: object is locked', + '...with correct error'); +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); + +# Test deleting an integration. We can't test this entirely properly because +# currently Net::Duo::Mock::Agent doesn't support stacking multiple expected +# calls and delete makes two calls. +note ('Test deleting an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration.json', + } +); +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) }; + is ($object, undef, '...and now object cannot be retrieved'); + is ($@, "cannot find duo:test\n", '...with correct error'); +} + +# Clean up. +$admin->destroy; +END { + unlink ('wallet-db'); +} + +# Done testing. +done_testing (); diff --git a/perl/t/object/duo-pam.t b/perl/t/object/duo-pam.t new file mode 100644 index 0000000..7b88787 --- /dev/null +++ b/perl/t/object/duo-pam.t @@ -0,0 +1,159 @@ +#!/usr/bin/perl +# +# Tests for the Duo PAM integration object implementation. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014 +# 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; + +BEGIN { + eval 'use Net::Duo'; + plan skip_all => 'Net::Duo required for testing duo' + if $@; + eval 'use Net::Duo::Mock::Agent'; + plan skip_all => 'Net::Duo::Mock::Agent required for testing duo' + if $@; +} + +BEGIN { + use_ok('Wallet::Admin'); + use_ok('Wallet::Config'); + use_ok('Wallet::Object::Duo::PAM'); +} + +use lib 't/lib'; +use Util; + +# Some global defaults to use. +my $user = 'admin@EXAMPLE.COM'; +my $host = 'localhost'; +my @trace = ($user, $host, time); +my $date = strftime ('%Y-%m-%d %H:%M:%S', localtime $trace[2]); + +# Flush all output immediately. +$| = 1; + +# Use Wallet::Admin to set up the database. +db_setup; +my $admin = eval { Wallet::Admin->new }; +is ($@, '', 'Database connection succeeded'); +is ($admin->reinitialize ($user), 1, 'Database initialization succeeded'); +my $schema = $admin->schema; + +# Create a mock object to use for Duo calls. +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); +}; +is ($object, undef, 'Wallet::Object::Duo::PAM 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); +}; +is ($object, undef, 'Wallet::Object::Duo::PAM creation with no config failed'); +is ($@, "duo object implementation not configured\n", '...with correct error'); + +# Set up the Duo configuration. +$Wallet::Config::DUO_AGENT = $mock; +$Wallet::Config::DUO_KEY_FILE = 't/data/duo/keys.json'; + +# Test creating an integration. +note ('Test creating an integration'); +my $expected = { + name => 'test (unix)', + notes => 'Managed by wallet', + type => 'unix', +}; +$mock->expect ( + { + method => 'POST', + uri => '/admin/v1/integrations', + content => $expected, + 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'); + +# Check the metadata about the new wallet object. +$expected = <<"EOO"; + Type: duo-pam + Name: test + Duo key: DIRWIH0ZZPV4G88B37VQ + Created by: $user + Created from: $host + Created on: $date +EOO +is ($object->show, $expected, 'Show output is correct'); + +# Test retrieving the integration information. +note ('Test retrieving an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration.json', + } +); +my $data = $object->get (@trace); +ok (defined ($data), 'Retrieval succeeds'); +$expected = <<'EOO'; +[duo] +ikey = DIRWIH0ZZPV4G88B37VQ +skey = QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o +host = example-admin.duosecurity.com +EOO +is ($data, $expected, '...and integration data is correct'); + +# Ensure that we can't retrieve the object when locked. +is ($object->flag_set ('locked', @trace), 1, + 'Setting object to locked succeeds'); +is ($object->get, undef, '...and now get fails'); +is ($object->error, 'cannot get duo-pam:test: object is locked', + '...with correct error'); +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); + +# Test deleting an integration. We can't test this entirely properly because +# currently Net::Duo::Mock::Agent doesn't support stacking multiple expected +# calls and delete makes two calls. +note ('Test deleting an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration.json', + } +); +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) }; + is ($object, undef, '...and now object cannot be retrieved'); + is ($@, "cannot find duo:test\n", '...with correct error'); +} + +# Clean up. +$admin->destroy; +END { + unlink ('wallet-db'); +} + +# Done testing. +done_testing (); diff --git a/perl/t/object/duo-radius.t b/perl/t/object/duo-radius.t new file mode 100644 index 0000000..f258518 --- /dev/null +++ b/perl/t/object/duo-radius.t @@ -0,0 +1,165 @@ +#!/usr/bin/perl +# +# Tests for the Duo Auth proxy Radius integration object implementation. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014 +# 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; + +BEGIN { + eval 'use Net::Duo'; + plan skip_all => 'Net::Duo required for testing duo' + if $@; + eval 'use Net::Duo::Mock::Agent'; + plan skip_all => 'Net::Duo::Mock::Agent required for testing duo' + if $@; +} + +BEGIN { + use_ok('Wallet::Admin'); + use_ok('Wallet::Config'); + use_ok('Wallet::Object::Duo::RadiusProxy'); +} + +use lib 't/lib'; +use Util; + +# Some global defaults to use. +my $user = 'admin@EXAMPLE.COM'; +my $host = 'localhost'; +my @trace = ($user, $host, time); +my $date = strftime ('%Y-%m-%d %H:%M:%S', localtime $trace[2]); + +# Flush all output immediately. +$| = 1; + +# Use Wallet::Admin to set up the database. +db_setup; +my $admin = eval { Wallet::Admin->new }; +is ($@, '', 'Database connection succeeded'); +is ($admin->reinitialize ($user), 1, 'Database initialization succeeded'); +my $schema = $admin->schema; + +# Create a mock object to use for Duo calls. +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); +}; +is ($object, undef, + 'Wallet::Object::Duo::RadiusProxy 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); +}; +is ($object, undef, + 'Wallet::Object::Duo::RadiusProxy creation with no config failed'); +is ($@, "duo object implementation not configured\n", '...with correct error'); + +# Set up the Duo configuration. +$Wallet::Config::DUO_AGENT = $mock; +$Wallet::Config::DUO_KEY_FILE = 't/data/duo/keys.json'; + +# Test creating an integration. +note ('Test creating an integration'); +my $expected = { + name => 'test (radius)', + notes => 'Managed by wallet', + type => 'radius', +}; +$mock->expect ( + { + method => 'POST', + uri => '/admin/v1/integrations', + content => $expected, + 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'); + +# Check the metadata about the new wallet object. +$expected = <<"EOO"; + Type: duo-radius + Name: test + Duo key: DIRWIH0ZZPV4G88B37VQ + Created by: $user + Created from: $host + Created on: $date +EOO +is ($object->show, $expected, 'Show output is correct'); + +# Test retrieving the integration information. +note ('Test retrieving an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration-radius.json', + } +); +my $data = $object->get (@trace); +ok (defined ($data), 'Retrieval succeeds'); +$expected = <<'EOO'; +[radius_server_challenge] +ikey = DIRWIH0ZZPV4G88B37VQ +skey = QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o +api_host = example-admin.duosecurity.com +client = radius_client +EOO +is ($data, $expected, '...and integration data is correct'); + +# Ensure that we can't retrieve the object when locked. +is ($object->flag_set ('locked', @trace), 1, + 'Setting object to locked succeeds'); +is ($object->get, undef, '...and now get fails'); +is ($object->error, 'cannot get duo-radius:test: object is locked', + '...with correct error'); +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); + +# Test deleting an integration. We can't test this entirely properly because +# currently Net::Duo::Mock::Agent doesn't support stacking multiple expected +# calls and delete makes two calls. +note ('Test deleting an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration.json', + } +); +TODO: { + local $TODO = 'Net::Duo::Mock::Agent not yet capable'; + + is ($object->destroy (@trace), 1, 'Duo object deletion succeeded'); + $object = eval { + Wallet::Object::Duo::RadiusProxy->new ('duo-radius', 'test', $schema); + }; + is ($object, undef, '...and now object cannot be retrieved'); + is ($@, "cannot find duo:test\n", '...with correct error'); +} + +# Clean up. +$admin->destroy; +END { + unlink ('wallet-db'); +} + +# Done testing. +done_testing (); diff --git a/perl/t/object/duo-rdp.t b/perl/t/object/duo-rdp.t new file mode 100644 index 0000000..9b2d566 --- /dev/null +++ b/perl/t/object/duo-rdp.t @@ -0,0 +1,158 @@ +#!/usr/bin/perl +# +# Tests for the Duo RDP integration object implementation. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014 +# 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; + +BEGIN { + eval 'use Net::Duo'; + plan skip_all => 'Net::Duo required for testing duo' + if $@; + eval 'use Net::Duo::Mock::Agent'; + plan skip_all => 'Net::Duo::Mock::Agent required for testing duo' + if $@; +} + +BEGIN { + use_ok('Wallet::Admin'); + use_ok('Wallet::Config'); + use_ok('Wallet::Object::Duo::RDP'); +} + +use lib 't/lib'; +use Util; + +# Some global defaults to use. +my $user = 'admin@EXAMPLE.COM'; +my $host = 'localhost'; +my @trace = ($user, $host, time); +my $date = strftime ('%Y-%m-%d %H:%M:%S', localtime $trace[2]); + +# Flush all output immediately. +$| = 1; + +# Use Wallet::Admin to set up the database. +db_setup; +my $admin = eval { Wallet::Admin->new }; +is ($@, '', 'Database connection succeeded'); +is ($admin->reinitialize ($user), 1, 'Database initialization succeeded'); +my $schema = $admin->schema; + +# Create a mock object to use for Duo calls. +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); +}; +is ($object, undef, 'Wallet::Object::Duo::RDP 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); +}; +is ($object, undef, 'Wallet::Object::Duo::RDP creation with no config failed'); +is ($@, "duo object implementation not configured\n", '...with correct error'); + +# Set up the Duo configuration. +$Wallet::Config::DUO_AGENT = $mock; +$Wallet::Config::DUO_KEY_FILE = 't/data/duo/keys.json'; + +# Test creating an integration. +note ('Test creating an integration'); +my $expected = { + name => 'test (rdp)', + notes => 'Managed by wallet', + type => 'rdp', +}; +$mock->expect ( + { + method => 'POST', + uri => '/admin/v1/integrations', + content => $expected, + 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'); + +# Check the metadata about the new wallet object. +$expected = <<"EOO"; + Type: duo-rdp + Name: test + Duo key: DIRWIH0ZZPV4G88B37VQ + Created by: $user + Created from: $host + Created on: $date +EOO +is ($object->show, $expected, 'Show output is correct'); + +# Test retrieving the integration information. +note ('Test retrieving an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration-rdp.json', + } +); +my $data = $object->get (@trace); +ok (defined ($data), 'Retrieval succeeds'); +$expected = <<'EOO'; +Integration key: DIRWIH0ZZPV4G88B37VQ +Secret key: QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o +Host: example-admin.duosecurity.com +EOO +is ($data, $expected, '...and integration data is correct'); + +# Ensure that we can't retrieve the object when locked. +is ($object->flag_set ('locked', @trace), 1, + 'Setting object to locked succeeds'); +is ($object->get, undef, '...and now get fails'); +is ($object->error, 'cannot get duo-rdp:test: object is locked', + '...with correct error'); +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); + +# Test deleting an integration. We can't test this entirely properly because +# currently Net::Duo::Mock::Agent doesn't support stacking multiple expected +# calls and delete makes two calls. +note ('Test deleting an integration'); +$mock->expect ( + { + method => 'GET', + uri => '/admin/v1/integrations/DIRWIH0ZZPV4G88B37VQ', + response_file => 't/data/duo/integration-rdp.json', + } +); +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) }; + is ($object, undef, '...and now object cannot be retrieved'); + is ($@, "cannot find duo:test\n", '...with correct error'); +} + +# Clean up. +$admin->destroy; +END { + unlink ('wallet-db'); +} + +# Done testing. +done_testing (); diff --git a/perl/t/object/duo.t b/perl/t/object/duo.t index f73fe7e..a975597 100755 --- a/perl/t/object/duo.t +++ b/perl/t/object/duo.t @@ -108,10 +108,9 @@ $mock->expect ( my $data = $object->get (@trace); ok (defined ($data), 'Retrieval succeeds'); $expected = <<'EOO'; -[duo] -ikey = DIRWIH0ZZPV4G88B37VQ -skey = QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o -host = example-admin.duosecurity.com +Integration key: DIRWIH0ZZPV4G88B37VQ +Secret key: QO4ZLqQVRIOZYkHfdPDORfcNf8LeXIbCWwHazY7o +Host: example-admin.duosecurity.com EOO is ($data, $expected, '...and integration data is correct'); diff --git a/perl/t/object/file.t b/perl/t/object/file.t index 201f46d..b7f295a 100755 --- a/perl/t/object/file.t +++ b/perl/t/object/file.t @@ -12,7 +12,7 @@ use strict; use warnings; use POSIX qw(strftime); -use Test::More tests => 56; +use Test::More tests => 60; use Wallet::Admin; use Wallet::Config; @@ -101,9 +101,15 @@ is ($object->error, 'data exceeds maximum of 1024 bytes', is ($object->store ('', @trace), 1, 'Storing the empty object works'); is ($object->get (@trace), '', ' and get returns the right thing'); +# Test renaming a file object. +is ($object->rename ('test-rename', @trace), 1, 'Renaming the object works'); +is ($object->{name}, 'test-rename', ' and the object is renamed'); +ok (-f 'test-files/2b/test-rename', ' and the file is in the new location'); +ok (! -f 'test-files/09/test', ' and nothing is in the old location'); + # Test destruction. is ($object->destroy (@trace), 1, 'Destroying the object works'); -ok (! -f 'test-files/09/test', ' and the file is gone'); +ok (! -f 'test-files/2b/test-rename', ' and the file is gone'); # Now try some aggressive names. $object = eval { |