summaryrefslogtreecommitdiff
path: root/perl
diff options
context:
space:
mode:
authorRuss Allbery <eagle@eyrie.org>2014-12-08 20:57:57 -0800
committerRuss Allbery <eagle@eyrie.org>2014-12-08 20:57:57 -0800
commit7856dc7cc5e16140c0084474fe54338f293bf77e (patch)
tree5948678fb9c0a30b7d72057c9952ac8836ae2499 /perl
parentdd295a55a6f02e7585a9f5be9e8b434c6d14d040 (diff)
parente73a80c6bc23f16544c35e7dc3bf61ca9292c3b5 (diff)
Imported Upstream version 1.2upstream/1.2
Diffstat (limited to 'perl')
-rw-r--r--perl/lib/Wallet/Admin.pm4
-rw-r--r--perl/lib/Wallet/Config.pm6
-rw-r--r--perl/lib/Wallet/Object/Base.pm2
-rw-r--r--perl/lib/Wallet/Object/Duo.pm54
-rw-r--r--perl/lib/Wallet/Object/Duo/LDAPProxy.pm202
-rw-r--r--perl/lib/Wallet/Object/Duo/PAM.pm205
-rw-r--r--perl/lib/Wallet/Object/Duo/RDP.pm204
-rw-r--r--perl/lib/Wallet/Object/Duo/RadiusProxy.pm204
-rw-r--r--perl/lib/Wallet/Object/File.pm56
-rw-r--r--perl/lib/Wallet/Policy/Stanford.pm24
-rw-r--r--perl/lib/Wallet/Schema.pm2
-rw-r--r--perl/lib/Wallet/Schema/Result/Duo.pm14
-rw-r--r--perl/lib/Wallet/Server.pm46
-rw-r--r--perl/sql/Wallet-Schema-0.09-0.10-MySQL.sql14
-rw-r--r--perl/sql/Wallet-Schema-0.09-0.10-PostgreSQL.sql18
-rw-r--r--perl/sql/Wallet-Schema-0.09-0.10-SQLite.sql26
-rw-r--r--perl/sql/Wallet-Schema-0.10-MySQL.sql209
-rw-r--r--perl/sql/Wallet-Schema-0.10-PostgreSQL.sql216
-rw-r--r--perl/sql/Wallet-Schema-0.10-SQLite.sql220
-rw-r--r--perl/t/data/duo/integration-ldap.json11
-rw-r--r--perl/t/data/duo/integration-radius.json11
-rw-r--r--perl/t/data/duo/integration-rdp.json11
-rwxr-xr-xperl/t/general/admin.t2
-rw-r--r--perl/t/object/duo-ldap.t160
-rw-r--r--perl/t/object/duo-pam.t159
-rw-r--r--perl/t/object/duo-radius.t165
-rw-r--r--perl/t/object/duo-rdp.t158
-rwxr-xr-xperl/t/object/duo.t7
-rwxr-xr-xperl/t/object/file.t10
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 {