diff options
Diffstat (limited to 'perl/Wallet')
| -rw-r--r-- | perl/Wallet/Config.pm | 47 | ||||
| -rw-r--r-- | perl/Wallet/Object/Duo.pm | 331 | ||||
| -rw-r--r-- | perl/Wallet/Schema.pm | 17 | ||||
| -rw-r--r-- | perl/Wallet/Schema/Result/Duo.pm | 53 | 
4 files changed, 446 insertions, 2 deletions
| diff --git a/perl/Wallet/Config.pm b/perl/Wallet/Config.pm index 0d9d506..ed3dded 100644 --- a/perl/Wallet/Config.pm +++ b/perl/Wallet/Config.pm @@ -1,7 +1,7 @@  # Wallet::Config -- Configuration handling for the wallet server.  #  # Written by Russ Allbery <eagle@eyrie.org> -# Copyright 2007, 2008, 2010, 2013 +# Copyright 2007, 2008, 2010, 2013, 2014  #     The Board of Trustees of the Leland Stanford Junior University  #  # See LICENSE for licensing terms. @@ -181,6 +181,51 @@ our $DB_PASSWORD;  =back +=head1 DUO OBJECT CONFIGURATION + +These configuration variables only need to be set if you intend to use the +C<duo> object type (the Wallet::Object::Duo class). + +=over 4 + +=item DUO_AGENT + +If this configuration variable is set, its value should be an object that +is call-compatible with LWP::UserAgent.  This object will be used instead +of LWP::UserAgent to make API calls to Duo.  This is primarily useful for +testing, allowing replacement of the user agent with a mock implementation +so that a test can run without needing a Duo account. + +=cut + +our $DUO_AGENT; + +=item DUO_KEY_FILE + +The path to a file in JSON format that contains the key and hostname data +for the Duo Admin API integration used to manage integrations via wallet. +This file should be in the format expected by the C<key_file> parameter +to the Net::Duo::Admin constructor.  See L<Net::Duo::Admin> for more +information. + +DUO_KEY_FILE must be set to use Duo objects. + +=cut + +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. + +=cut + +our $DUO_TYPE = 'unix'; + +=back +  =head1 FILE OBJECT CONFIGURATION  These configuration variables only need to be set if you intend to use the diff --git a/perl/Wallet/Object/Duo.pm b/perl/Wallet/Object/Duo.pm new file mode 100644 index 0000000..af2dfab --- /dev/null +++ b/perl/Wallet/Object/Duo.pm @@ -0,0 +1,331 @@ +# Wallet::Object::Duo -- Duo integration object implementation for the wallet. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014 +#     The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +############################################################################## +# Modules and declarations +############################################################################## + +package Wallet::Object::Duo; +require 5.006; + +use strict; +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::Base; + +@ISA = qw(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'; + +############################################################################## +# Core methods +############################################################################## + +# Override attr_show to display the Duo integration key attribute. +sub attr_show { +    my ($self) = @_; +    my $output = ''; +    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; +    } +    return sprintf ("%15s: %s\n", 'Duo key', $key); +} + +# Override new to start by creating a Net::Duo::Admin object for subsequent +# calls. +sub new { +    my ($class, $type, $name, $schema) = @_; + +    # We have to have a Duo integration key file set. +    if (not $Wallet::Config::DUO_KEY_FILE) { +        die "duo object implementation not configured\n"; +    } +    my $key_file = $Wallet::Config::DUO_KEY_FILE; +    my $agent    = $Wallet::Config::DUO_AGENT; + +    # Construct the Net::Duo::Admin object. +    require Net::Duo::Admin; +    my $duo = Net::Duo::Admin->new ( +        { +            key_file   => $key_file, +            user_agent => $agent, +        } +    ); + +    # Construct the object. +    my $self = $class->SUPER::new ($type, $name, $schema); +    $self->{duo} = $duo; +    return $self; +} + +# Override create to start by creating a new integration in Duo, and only +# create the entry in the database if that succeeds.  Error handling isn't +# 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) = @_; + +    # We have to have a Duo integration key file set. +    if (not $Wallet::Config::DUO_KEY_FILE) { +        die "duo object implementation not configured\n"; +    } +    my $key_file = $Wallet::Config::DUO_KEY_FILE; +    my $agent    = $Wallet::Config::DUO_AGENT; + +    # Construct the Net::Duo::Admin object. +    require Net::Duo::Admin; +    my $duo = Net::Duo::Admin->new ( +        { +            key_file   => $key_file, +            user_agent => $agent, +        } +    ); + +    # Create the object in Duo. +    require Net::Duo::Admin::Integration; +    my %data = ( +        name  => $name, +        notes => 'Managed by wallet', +        type  => $Wallet::Config::DUO_TYPE, +    ); +    my $integration = Net::Duo::Admin::Integration->create ($duo, \%data); + +    # Create the object in wallet. +    my @trace = ($creator, $host, $time); +    my $self = $class->SUPER::create ($type, $name, $schema, @trace); +    $self->{duo} = $duo; + +    # Add the integration key to the object metadata. +    my $guard = $self->{schema}->txn_scope_guard; +    eval { +        my %record = ( +            du_name => $name, +            du_key  => $integration->integration_key, +        ); +        $self->{schema}->resultset ('Duo')->create (\%record); +        $guard->commit; +    }; +    if ($@) { +        my $id = $self->{type} . ':' . $self->{name}; +        $self->error ("cannot set Duo key for $id: $@"); +        return; +    } + +    # Done.  Return the object. +    return $self; +} + +# Override destroy to delete the integration out of Duo as well. +sub destroy { +    my ($self, $user, $host, $time) = @_; +    my $id = $self->{type} . ':' . $self->{name}; +    if ($self->flag_check ('locked')) { +        $self->error ("cannot destroy $id: object is locked"); +        return; +    } +    my $schema = $self->{schema}; +    my $guard = $schema->txn_scope_guard; +    eval { +        my %search = (du_name => $self->{name}); +        my $row = $schema->resultset ('Duo')->find (\%search); +        my $key = $row->get_column ('du_key'); +        my $int = Net::Duo::Admin::Integration->new ($self->{duo}, $key); +        $int->delete; +        $row->delete; +        $guard->commit; +    }; +    if ($@) { +        $self->error ($@); +        return; +    } +    return $self->SUPER::destroy ($user, $host, $time); +} + +# Our get implementation.  Retrieve the integration information from Duo and +# construct the configuration file expected by the Duo 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); +    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 + +=head1 NAME + +Wallet::Object::Duo - Duo integration object implementation for wallet + +=head1 SYNOPSIS + +    my @name = qw(duo host.example.com); +    my @trace = ($user, $host, time); +    my $object = Wallet::Object::Duo->create (@name, $schema, @trace); +    my $config = $object->get (@trace); +    $object->destroy (@trace); + +=head1 DESCRIPTION + +Wallet::Object::Duo is a representation of Duo integrations the wallet. +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. + +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. + +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::Base.  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 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 +normally C<duo> 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.  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. + +=item destroy(PRINCIPAL, HOSTNAME [, DATETIME]) + +Destroys a Duo integration object by removing it from the database and +deleting the integration from Duo.  If deleting the Duo integration fails, +destroy() fails.  Returns true on success and false on failure.  The +caller should call error() to get the error message after a failure. +PRINCIPAL, HOSTNAME, and DATETIME are stored as history information. +PRINCIPAL should be the user who is destroying the object.  If DATETIME +isn't given, the current time is used. + +=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. +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 + +Net::Duo(3), Wallet::Config(3), Wallet::Object::Base(3), wallet-backend(8) + +This module is part of the wallet system.  The current version is +available from L<http://www.eyrie.org/~eagle/software/wallet/>. + +=head1 AUTHOR + +Russ Allbery <eagle@eyrie.org> + +=cut diff --git a/perl/Wallet/Schema.pm b/perl/Wallet/Schema.pm index 2176cab..74b4c99 100644 --- a/perl/Wallet/Schema.pm +++ b/perl/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.08'; +our $VERSION = '0.09';  __PACKAGE__->load_namespaces;  __PACKAGE__->load_components (qw/Schema::Versioned/); @@ -272,6 +272,21 @@ oh_by stores the authenticated identity that made the change, oh_from  stores the host from which they made the change, and oh_on stores the time  the change was made. +=head2 Duo Backend Data + +Duo integration objects store some additional metadata about the +integration to aid in synchronization with Duo. + +  create table duo +     (du_name             varchar(255) +          not null references objects(ob_name), +      du_key              varchar(255) not null); +  create index du_key on duo (du_key); + +du_key holds the Duo integration key, which is the unique name of the +integration within Duo.  Additional data may be added later to represent +the other possible settings within Duo. +  =head2 Keytab Backend Data  The keytab backend has stub support for synchronizing keys with an diff --git a/perl/Wallet/Schema/Result/Duo.pm b/perl/Wallet/Schema/Result/Duo.pm new file mode 100644 index 0000000..80a71dc --- /dev/null +++ b/perl/Wallet/Schema/Result/Duo.pm @@ -0,0 +1,53 @@ +# Wallet schema for Duo metadata. +# +# Written by Jon Robertson <jonrober@stanford.edu> +# Copyright 2014 +#     The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +package Wallet::Schema::Result::Duo; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=for stopwords +keytab enctype + +=head1 NAME + +Wallet::Schema::Result::Duo - Wallet schema for Duo metadata + +=head1 DESCRIPTION + +=cut + +__PACKAGE__->table("duo"); + +=head1 ACCESSORS + +=head2 du_name + +  data_type: 'varchar' +  is_nullable: 0 +  size: 255 + +=head2 du_key + +  data_type: 'varchar' +  is_nullable: 0 +  size: 255 + +=cut + +__PACKAGE__->add_columns( +  "du_name", +  { data_type => "varchar", is_nullable => 0, size => 255 }, +  "du_key", +  { data_type => "varchar", is_nullable => 0, size => 255 }, +); +__PACKAGE__->set_primary_key("du_name"); + +1; | 
