SPOPS::LDAP::MultiDatasource - SPOPS::LDAP functionality but fetching objects from multiple datasources


SPOPS documentation Contained in the SPOPS distribution.

Index


Code Index:

NAME

Top

SPOPS::LDAP::MultiDatasource -- SPOPS::LDAP functionality but fetching objects from multiple datasources

SYNOPSIS

Top

 # In your configuration
 my $config = {
    class      => 'My::LDAPThings',
    datasource => [ 'main', 'secondary', 'tertiary' ],
    isa        => [ ... 'SPOPS::LDAP::MultiDatasource' ],
    ...,
 };

 # Fetch an object and see where it came from

 my $object = My::LDAPThings->fetch( 'superuser' );
 print "My DN is ", $object->dn, " and I came from $object->{_datasource}";

DESCRIPTION

Top

This class extends SPOPS::LDAP with one purpose: be able to fetch objects from multiple datasources. This can happen when you have got objects dispersed among multiple directories -- for instance, your 'Accounting' department is on one LDAP server and your 'Development' department on another. One class can (more or less -- see below) link the two LDAP servers.

Every object is tagged with the datasource it came from (in the _datasource property, if you ever need it), and any calls to save() or remove() will use this datasource to retrieve the proper connection for the object.

Caveats

The fetch() method is the only functional method overridden from SPOPS::LDAP. The fetch_group() or fetch_iterator() methods will only use the first datasource in the listing, whatever datasource you pass in with the parameter 'connect_key' or whatever LDAP connection handle you pass in with the parameter 'ldap'. If you want to retrieve objects from multiple datasources using the same filter, use the fetch_group_all() method.

The fetch_iterator() method is not supported at all for multiple datasources -- use fetch_group_all() in conjunction with SPOPS::Iterator::WrapList if your implementation expects an SPOPS::Iterator object.

SETUP

Top

There are a number of items to configure and setup to use this class. Please see SPOPS::Manual::Configuration (SPOPS::Manual::Configuration) for the configuration keys used by this module.

Methods You Must Implement

connection_info( $connect_key )

This method should look at the $connect_key and return a hashref of information used to connect to the LDAP directory. Keys (hopefully self-explanatory) should be:

Other keys are optional and can be used in conjunction with a connection/resource manager (example below).

For example:

 package My::ConnectionManage;

 use strict;

 my $connections = {
    main        => { host => 'localhost',
                     base_dn => 'dc=MyCompanyEast,dc=com' },
    accounting  => { host => 'accounting.mycompany.com',
                     base_dn => 'dc=MyCompanyWest,dc=com' },
    development => { host => 'dev.mycompany.com',
                     base_dn => 'dc=MyCompanyNorth,dc=com' },
    etc         => { host => 'etc.mycompany.com',
                     base_dn => 'dc=MyCompanyBranch,dc=com' },
 };

 sub connection_info {
     my ( $class, $connect_key ) = @_;
     return \%{ $connections->{ $connect_key } };
 }

Then put this class into the 'isa' for your SPOPS class:

 my $spops = {
   class      => 'My::Person',
   isa        => [ 'My::ConnectionManage', 'SPOPS::LDAP::MultiDatasource' ],
 };




global_datasource_handle( $connect_key )

You will need an implementation that deals with multiple configurations. For example:

 package My::DSManage;

 use strict;
 use Net::LDAP;

 my %DS = ();

 sub global_datasource_handle {
     my ( $class, $connect_key ) = @_;
     unless ( $connect_key ) {
         SPOPS::Exception->throw( "Cannot retrieve handle without connect key" );
     }
     unless ( $DS{ $connect_key } ) {
         my $ldap_info = $class->connection_info( $connect_key );
         $ldap_info->{port} ||= 389;
         my $ldap = Net::LDAP->new( $ldap_info->{host},
                                    port => $ldap_info->{port} );
         unless ( $ldap ) {
             SPOPS::Exception->throw( "Cannot create LDAP connection: $@" );
         }
         my ( %bind_params );
         if ( $ldap_info->{bind_dn} ) {
             $bind_params{dn}       = $ldap_info->{bind_dn};
             $bind_params{password} = $ldap_info->{bind_password};
         }
         my $bind_msg = $ldap->bind( %bind_params );
         if ( $bind_msg->code ) {
             SPOPS::Exception::LDAP->throw( "Cannot bind to directory: " . $bind_msg->error,
                                            { code   => $bind_msg->code,
                                              action => 'global_datasource_handle' } );
         $DS{ $connect_key } = $ldap;
     }
     return $DS{ $connect_key };
 }

Then put this class into the 'isa' for your SPOPS class:

 my $spops = {
   class      => 'My::Person',
   isa        => [ 'My::DSManage', 'SPOPS::LDAP::MultiDatasource' ],
 };

Someone with a thinking cap on might put the previous two items in the same class :-)

METHODS

Top

fetch( $id, \%params )

Given the normal parameters for fetch(), tries to retrieve an object matching either the $id or the 'filter' specified in \%params from one of the datasources. When it finds an object it is immediately returned.

If you pass in the key 'ldap' in \%params, this functions as the fetch() does in SPOPS::LDAP and multiple datasources are not used.

Returns: SPOPS object (if found), or undef.

fetch_group_all( \%params )

Given the normal parameters for fetch_group(), retrieves all objects matching the parameters from all datasources. Use with caution.

Returns: Arrayref of SPOPS objects.

save( \%params )

Just pass along the right handle to the actual save() method in SPOPS::LDAP.

remove( \%params )

Just pass along the right handle to the actual remove() method in SPOPS::LDAP.

base_dn( $connect_key )

Returns the full base DN associated with $connect_key.

get_partial_dn( $connect_key )

Retrieves the partial base DN associated with $connect_key.

get_connect_key()

If called, returns either the value of the config key 'default_datasource' or the value of the class constant 'DEFAULT_CONNECT_KEY', which is normally 'main'.

BUGS

Top

None known.

TO DO

Top

Test some more.

SEE ALSO

Top

SPOPS::LDAP

Example in SPOPS distribution: eg/ldap_multidatasource.pl

COPYRIGHT

Top

AUTHORS

Top

Chris Winters <chris@cwinters.com>


SPOPS documentation Contained in the SPOPS distribution.

package SPOPS::LDAP::MultiDatasource;

# $Id: MultiDatasource.pm,v 3.3 2004/06/02 00:48:23 lachoy Exp $

use strict;
use base qw( SPOPS::LDAP );
use Log::Log4perl qw( get_logger );

use SPOPS;
use SPOPS::Exception::LDAP;

$SPOPS::LDAP::MultiDatasource::VERSION = sprintf("%d.%02d", q$Revision: 3.3 $ =~ /(\d+)\.(\d+)/);

use constant DEFAULT_CONNECT_KEY => 'main';

my $log = get_logger();

sub base_dn  {
    my ( $class, $connect_key ) = @_;
    my $partial_dn = $class->get_partial_dn( $connect_key );
    unless ( $partial_dn ) {
        SPOPS::Exception->throw( "No Base DN defined in configuration key 'ldap_base_dn'" );
    }
    my $connect_info = $class->connection_info( $connect_key );
    return join( ',', $partial_dn, $connect_info->{base_dn} );
}

# Retrieves the 'partial dn', or the section that's prepended to the
# server's 'base DN' to identify the branch on which these objects
# live

sub get_partial_dn {
    my ( $class, $connect_key ) = @_;
    my $base_dn_info = $class->CONFIG->{ldap_base_dn};
    return $base_dn_info unless ( ref $base_dn_info eq 'HASH' );
    $connect_key ||= $class->get_connect_key;
    return $base_dn_info->{ $connect_key };
}


sub get_connect_key {
    my ( $class ) = @_;
    return $class->CONFIG->{default_datasource} || DEFAULT_CONNECT_KEY;
}


sub fetch {
    my ( $class, $id, $p ) = @_;

    # If passed in a handle, we will always use only that

    if ( $p->{ldap} ) {
        return $class->SUPER::fetch( $id, $p );
    }

    my @ds_list = $class->_get_datasource_list( 'fetch', $id, $p );

    # Step through the datasource listing and try to retrieve each one
    # in turn

    foreach my $ds ( @ds_list ) {
        $log->is_info &&
            $log->info( "(fetch) Trying to use datasource ($ds) for class ($class)" );
        $p->{connect_key} = $ds;
        my $object = eval { $class->SUPER::fetch( $id, $p ) };
        if ( $object ) {
            $object->{_datasource} = $ds;
            return $object;
        }
    }
    return undef;
}


sub fetch_by_dn {
    my ( $class, $dn, $p ) = @_;
    if ( $p->{ldap} ) {
        return $class->SUPER::fetch_by_dn( $dn, $p );
    }
    my @ds_list = $class->_get_datasource_list( 'fetch_by_dn', $dn, $p );

    # Step through the datasource listing and try to retrieve each one
    # in turn

    foreach my $ds ( @ds_list ) {
        $log->is_info &&
            $log->info( "(fetch_by_dn) Trying to use datasource ($ds) for class ($class)" );
        $p->{connect_key} = $ds;
        my $object = eval { $class->SUPER::fetch_by_dn( $dn, $p ) };
        if ( $object ) {
            $object->{_datasource} = $ds;
            return $object
        }
    }
}

sub fetch_group_all {
    my ( $class, $p ) = @_;

    if ( $p->{ldap} ) {
        return $class->SUPER::fetch_group( $p );
    }
    my @ds_list = $class->_get_datasource_list( 'fetch_group', $p );
    my @all_objects = ();
    foreach my $ds ( @ds_list ) {
        $p->{connect_key} = $ds;
        $log->is_info &&
            $log->info( "Trying to fetch from datasource ($ds)" );
        my $object_list = $class->SUPER::fetch_group( $p );
        if ( $object_list and ref $object_list eq 'ARRAY' ) {
            foreach my $object ( @{ $object_list } ) {
                $object->{_datasource} = $ds;
                push @all_objects, $object;
            }
        }
    }
    return \@all_objects;
}


# Just be sure we grab the right LDAP handle before saving or removing
# -- if people pass in a handle, we'll defer to their judgement that
# they know what they're doing

sub save {
    my ( $self, $p ) = @_;
    $p->{ldap} ||= $self->global_datasource_handle( $self->{_datasource} );
    return $self->SUPER::save( $p );
}


sub remove {
    my ( $self, $p ) = @_;
    $p->{ldap} ||= $self->global_datasource_handle( $self->{_datasource} );
    return $self->SUPER::remove( $p );
}


# Retrieve a datasource list from the class configuration. If it
# doesn't work (no list or the list is empty), run a specified method
# in the parent class.

sub _get_datasource_list {
    my ( $class, $method, @args ) = @_;
    my $ds_list = $class->CONFIG->{datasource};

    # If there are not multiple datasources specified, then bounce
    # back to the SPOPS::LDAP-version of th method we were trying to
    # call in the first place.

    unless ( ref $ds_list eq 'ARRAY' and scalar @{ $ds_list } ) {
        $log->is_info &&
            $log->info( "No datasources in configuration for ($class).",
                        "Using SPOPS::LDAP->$method()" );
        my $full_method = "SUPER::$method";
        return $class->$full_method( @args );
    }
    return @{ $ds_list };
}

1;

__END__