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__ =pod =head1 NAME SPOPS::LDAP::MultiDatasource -- SPOPS::LDAP functionality but fetching objects from multiple datasources =head1 SYNOPSIS # 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}"; =head1 DESCRIPTION This class extends L 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 C<_datasource> property, if you ever need it), and any calls to C or C will use this datasource to retrieve the proper connection for the object. =head2 Caveats The C method is the only functional method overridden from L. The C or C 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 C method. The C method is not supported at all for multiple datasources -- use C in conjunction with L if your implementation expects an L object. =head1 SETUP There are a number of items to configure and setup to use this class. Please see L for the configuration keys used by this module. =head2 Methods You Must Implement B This method should look at the C<$connect_key> and return a hashref of information used to connect to the LDAP directory. Keys (hopefully self-explanatory) should be: =over 4 =item * B ($) =item * B ($) =back Other keys are optional and can be used in conjunction with a connection/resource manager (example below). =over 4 =item * B ($) (optional, default is '389') =item * B ($) (optional, will use anonymous bind without) =item * B ($) (optional, only used if 'bind_dn' specified) =back 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' ], }; B 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 :-) =head1 METHODS B Given the normal parameters for C, tries to retrieve an object matching either the C<$id> or the 'filter' specified in C<\%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 C does in L and multiple datasources are not used. Returns: SPOPS object (if found), or undef. B Given the normal parameters for C, retrieves B objects matching the parameters from B datasources. Use with caution. Returns: Arrayref of SPOPS objects. B Just pass along the right handle to the actual C method in L. B Just pass along the right handle to the actual C method in L. B Returns the B base DN associated with C<$connect_key>. B Retrieves the B base DN associated with C<$connect_key>. B 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'. =head1 BUGS None known. =head1 TO DO Test some more. =head1 SEE ALSO L Example in SPOPS distribution: eg/ldap_multidatasource.pl =head1 COPYRIGHT Copyright (c) 2001-2004 MSN Marketing Service Nordwest, GmbH. All rights reserved. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHORS Chris Winters Echris@cwinters.comE =cut