aboutsummaryrefslogtreecommitdiff
path: root/perl
diff options
context:
space:
mode:
authorJon Robertson <jonrober@stanford.edu>2014-10-13 22:28:30 -0700
committerJon Robertson <jonrober@stanford.edu>2014-10-13 22:28:30 -0700
commit0e39650c6a831987057a8a9ed615dc3023fcab4a (patch)
tree64dec2780b44078d8e46969b6607238e33a6bd41 /perl
parent9c639ffd83e2dd9b14503b62740589ee5cca25df (diff)
Added Duo RDP object to the wallet
Change-Id: Ic728297fa830ffdd40c1580e32a81f8c5123f66a
Diffstat (limited to 'perl')
-rw-r--r--perl/lib/Wallet/Admin.pm3
-rw-r--r--perl/lib/Wallet/Object/Duo/RDP.pm204
-rw-r--r--perl/t/data/duo/integration-rdp.json11
-rw-r--r--perl/t/object/duo-rdp.t158
4 files changed, 375 insertions, 1 deletions
diff --git a/perl/lib/Wallet/Admin.pm b/perl/lib/Wallet/Admin.pm
index 381ffc9..8120e9c 100644
--- a/perl/lib/Wallet/Admin.pm
+++ b/perl/lib/Wallet/Admin.pm
@@ -126,9 +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-ldap', 'Wallet::Object::Duo::LDAPProxy' ],
+ [ '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/Object/Duo/RDP.pm b/perl/lib/Wallet/Object/Duo/RDP.pm
new file mode 100644
index 0000000..4e7ff04
--- /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
+
+=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/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/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 ();