From: Peter Karman Date: Wed, 22 Oct 2008 01:55:47 +0000 (+0000) Subject: release 0.1004 X-Git-Tag: v0.1004~1 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=405489b5976c386ddad2a777eb31f75d8319a168;p=catagits%2FCatalyst-Authentication-Store-LDAP.git release 0.1004 --- diff --git a/Changes b/Changes index 21e156b..862d3e3 100644 --- a/Changes +++ b/Changes @@ -1,4 +1,11 @@ -0.1003 xxxx +0.1004 21 Oct 2008 + - Add the ability to have the user inflated into a custom + user class with the user_class option (t0m) + - Add the ability for role lookup to be performed within + the same (user) bind context that the user's password is + checked in (t0m) + +0.1003 10 Sept 2008 - get entries in array context rather than scalar context, allowing for multiple values. patch by scpham. - lc() to compare Net::LDAP results with supplied $id diff --git a/MANIFEST b/MANIFEST index e84dfef..14471bb 100644 --- a/MANIFEST +++ b/MANIFEST @@ -18,8 +18,11 @@ MANIFEST This list of files META.yml t/02-realms_api.t t/03-entry_class.t +t/04-user_class.t +t/10-roles-mock.t t/50.auth.case.sensitivity.t t/lib/EntryClass.pm t/lib/LDAPTest.pm +t/lib/UserClass.pm t/pod-coverage.t t/pod.t diff --git a/Makefile.PL b/Makefile.PL index 1adc030..9b041d9 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -12,6 +12,7 @@ requires( 'Catalyst::Plugin::Authentication' => '0.10003' ); #requires('Catalyst::Model::LDAP'); build_requires('Net::LDAP::Server::Test' => '0.07'); build_requires('Test::More'); +build_requires('Test::MockObject'); auto_install(); diff --git a/lib/Catalyst/Authentication/Store/LDAP.pm b/lib/Catalyst/Authentication/Store/LDAP.pm index 896806b..bc558e2 100644 --- a/lib/Catalyst/Authentication/Store/LDAP.pm +++ b/lib/Catalyst/Authentication/Store/LDAP.pm @@ -3,7 +3,7 @@ package Catalyst::Authentication::Store::LDAP; use strict; use warnings; -our $VERSION = '0.1003'; +our $VERSION = '0.1004'; use Catalyst::Authentication::Store::LDAP::Backend; @@ -52,6 +52,7 @@ Catalyst::Authentication::Store::LDAP role_scope => "one", role_search_options => { deref => "always" }, role_value => "dn", + role_search_as_user => 0, start_tls => 1, start_tls_options => { verify => "none" }, entry_class => "MyApp::LDAP::Entry", @@ -301,6 +302,23 @@ Be careful not to specify: As they are already taken care of by other configuration options. +=head2 role_search_as_user + +By default this setting is false, and the role search will be performed +by binding to the directory with the details in the I and I +fields. If this is set to false, then the role search will instead be +performed when bound as the user you authenticated as. + +=head2 entry_class + +The name of the class of LDAP entries returned. This class should +exist and is expected to be a subclass of Net::LDAP::Entry + +=head2 user_class + +The name of the class of user object returned. By default, this is +L. + =head1 METHODS =head2 new diff --git a/lib/Catalyst/Authentication/Store/LDAP/Backend.pm b/lib/Catalyst/Authentication/Store/LDAP/Backend.pm index 915b89d..36df48b 100644 --- a/lib/Catalyst/Authentication/Store/LDAP/Backend.pm +++ b/lib/Catalyst/Authentication/Store/LDAP/Backend.pm @@ -38,6 +38,7 @@ Catalyst::Authentication::Store::LDAP::Backend }, 'user_results_filter' => sub { return shift->pop_entry }, 'entry_class' => 'MyApp::LDAP::Entry', + 'user_class' => 'MyUser', 'use_roles' => 1, 'role_basedn' => 'ou=groups,dc=yourcompany,dc=com', 'role_filter' => '(&(objectClass=posixGroup)(member=%s))', @@ -47,6 +48,7 @@ Catalyst::Authentication::Store::LDAP::Backend 'role_search_options' => { 'deref' => 'always', }, + 'role_search_as_user' => 0, ); our $users = Catalyst::Authentication::Store::LDAP::Backend->new(\%config); @@ -78,10 +80,11 @@ use base qw( Class::Accessor::Fast ); use strict; use warnings; -our $VERSION = '0.1003'; +our $VERSION = '0.1004'; use Catalyst::Authentication::Store::LDAP::User; use Net::LDAP; +use Catalyst::Utils (); BEGIN { __PACKAGE__->mk_accessors( @@ -91,7 +94,7 @@ BEGIN { user_attrs user_field use_roles role_basedn role_filter role_scope role_field role_value role_search_options start_tls start_tls_options - user_results_filter + user_results_filter user_class role_search_as_user ) ); } @@ -125,7 +128,10 @@ sub new { $config_hash{'use_roles'} ||= '1'; $config_hash{'start_tls'} ||= '0'; $config_hash{'entry_class'} ||= 'Catalyst::Model::LDAP::Entry'; + $config_hash{'user_class'} ||= 'Catalyst::Authentication::Store::LDAP::User'; + $config_hash{'role_search_as_user'} ||= 0; + Catalyst::Utils::ensure_class_loaded($config_hash{'user_class'}); my $self = \%config_hash; bless( $self, $class ); return $self; @@ -157,7 +163,7 @@ given User out of the Store. sub get_user { my ( $self, $id ) = @_; - my $user = Catalyst::Authentication::Store::LDAP::User->new( $self, + my $user = $self->user_class->new( $self, $self->lookup_user($id) ); return $user; } @@ -217,10 +223,7 @@ sub ldap_bind { $binddn ||= $self->binddn; $bindpw ||= $self->bindpw; if ( $binddn eq "anonymous" ) { - my $mesg = $ldap->bind; - if ( $mesg->is_error ) { - Catalyst::Exception->throw( "Error on Bind: " . $mesg->error ); - } + $self->_ldap_bind_anon($ldap); } else { if ($bindpw) { @@ -239,15 +242,20 @@ sub ldap_bind { } } else { - my $mesg = $ldap->bind($binddn); - if ( $mesg->is_error ) { - return undef; - } + $self->_ldap_bind_anon($ldap, $binddn); } } return $ldap; } +sub _ldap_bind_anon { + my ($self, $ldap, $dn) = @_; + my $mesg = $ldap->bind($dn); + if ( $mesg->is_error ) { + Catalyst::Exception->throw( "Error on Bind: " . $mesg->error ); + } +} + =head2 lookup_user($id) Given a User ID, this method will: @@ -341,10 +349,8 @@ sub lookup_user { $attrhash->{ lc($attr) } = \@attrvalues; } } - my $load_class = $self->entry_class . ".pm"; - $load_class =~ s|::|/|g; - - eval { require $load_class }; + + eval { Catalyst::Utils::ensure_class_loaded($self->entry_class) }; if ( !$@ ) { bless( $userentry, $self->entry_class ); $userentry->{_use_unicode}++; @@ -356,11 +362,12 @@ sub lookup_user { return $rv; } -=head2 lookup_roles($userobj) +=head2 lookup_roles($userobj, [$ldap]) This method looks up the roles for a given user. It takes a L object -as it's sole argument. +as it's first argument, and can optionally take a I object which +is used rather than the default binding if supplied. It returns an array containing the role_field attribute from all the objects that match it's criteria. @@ -368,11 +375,11 @@ objects that match it's criteria. =cut sub lookup_roles { - my ( $self, $userobj ) = @_; + my ( $self, $userobj, $ldap ) = @_; if ( $self->use_roles == 0 || $self->use_roles =~ /^false$/i ) { return undef; } - my $ldap = $self->ldap_bind; + $ldap ||= $self->ldap_bind; my @searchopts; if ( defined( $self->role_basedn ) ) { push( @searchopts, 'base' => $self->role_basedn ); diff --git a/lib/Catalyst/Authentication/Store/LDAP/User.pm b/lib/Catalyst/Authentication/Store/LDAP/User.pm index ac193ca..22f95c5 100644 --- a/lib/Catalyst/Authentication/Store/LDAP/User.pm +++ b/lib/Catalyst/Authentication/Store/LDAP/User.pm @@ -46,7 +46,7 @@ use base qw( Catalyst::Authentication::User Class::Accessor::Fast ); use strict; use warnings; -our $VERSION = '0.1003'; +our $VERSION = '0.1004'; BEGIN { __PACKAGE__->mk_accessors(qw/user store/) } @@ -135,6 +135,11 @@ sub check_password { = $self->store->ldap_bind( undef, $self->ldap_entry->dn, $password, 'forauth' ); if ( defined($ldap) ) { + if ($self->store->role_search_as_user) { + # Have to do the role lookup _now_, as this is the only time + # that we have the user's password/ldap bind.. + $self->roles($ldap); + } return 1; } else { @@ -150,7 +155,9 @@ Returns the results of L's "look sub roles { my $self = shift; - return $self->store->lookup_roles($self); + my $ldap = shift; + $self->{_roles} ||= [$self->store->lookup_roles($self, $ldap)]; + return @{$self->{_roles}}; } =head2 for_session diff --git a/t/04-user_class.t b/t/04-user_class.t new file mode 100644 index 0000000..283441f --- /dev/null +++ b/t/04-user_class.t @@ -0,0 +1,43 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Catalyst::Exception; + +use Test::More tests => 5; +use lib 't/lib'; +use LDAPTest; + +SKIP: { + + eval "use Catalyst::Model::LDAP"; + if ($@) { + skip "Catalyst::Model::LDAP not installed", 5; + } + + my $server = LDAPTest::spawn_server(); + + use_ok("Catalyst::Authentication::Store::LDAP::Backend"); + + my $back = Catalyst::Authentication::Store::LDAP::Backend->new( + { 'ldap_server' => LDAPTest::server_host(), + 'binddn' => 'anonymous', + 'bindpw' => 'dontcarehow', + 'start_tls' => 0, + 'user_basedn' => 'ou=foobar', + 'user_filter' => '(&(objectClass=person)(uid=%s))', + 'user_scope' => 'one', + 'user_field' => 'uid', + 'use_roles' => 0, + 'user_class' => 'UserClass', + } + ); + + isa_ok( $back, "Catalyst::Authentication::Store::LDAP::Backend" ); + my $user = $back->find_user( { username => 'somebody' } ); + isa_ok( $user, "Catalyst::Authentication::Store::LDAP::User" ); + isa_ok( $user, "UserClass"); + + is( $user->my_method, 'frobnitz', "methods on user class work" ); + +} diff --git a/t/10-roles-mock.t b/t/10-roles-mock.t new file mode 100644 index 0000000..d79a533 --- /dev/null +++ b/t/10-roles-mock.t @@ -0,0 +1,101 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Catalyst::Exception; + +use Test::More tests => 7; +use Test::MockObject::Extends; +use Net::LDAP::Entry; +use lib 't/lib'; + +SKIP: { + + eval "use Catalyst::Model::LDAP"; + if ($@) { + skip "Catalyst::Model::LDAP not installed", 7; + } + + use_ok("Catalyst::Authentication::Store::LDAP::Backend"); + + my (@searches, @binds); + for my $i (0..1) { + + my $back = Catalyst::Authentication::Store::LDAP::Backend->new({ + 'ldap_server' => 'ldap://127.0.0.1:555', + 'binddn' => 'anonymous', + 'bindpw' => 'dontcarehow', + 'start_tls' => 0, + 'user_basedn' => 'ou=foobar', + 'user_filter' => '(&(objectClass=inetOrgPerson)(uid=%s))', + 'user_scope' => 'one', + 'user_field' => 'uid', + 'use_roles' => 1, + 'role_basedn' => 'ou=roles', + 'role_filter' => '(&(objectClass=posixGroup)(memberUid=%s))', + 'role_scope' => 'one', + 'role_field' => 'userinrole', + 'role_value' => 'cn', + 'role_search_as_user' => $i, + }); + $back = Test::MockObject::Extends->new($back); + my $bind_msg = Test::MockObject->new; + $bind_msg->mock(is_error => sub {}); # Cause bind call to always succeed + my $ldap = Test::MockObject->new; + $ldap->mock('bind', sub { shift; push (@binds, [@_]); return $bind_msg}); + $ldap->mock('unbind' => sub {}); + $ldap->mock('disconnect' => sub {}); + my $search_res = Test::MockObject->new(); + $search_res->mock(is_error => sub {}); # Never an error + $search_res->mock(entries => sub { + return map + { my $id = $_; + Test::MockObject->new->mock( + get_value => sub { "quux$id" } + ) + } + qw/one two/ + }); + my @user_entries; + $search_res->mock(pop_entry => sub { return pop @user_entries }); + $ldap->mock('search', sub { shift; push(@searches, [@_]); return $search_res; }); + $back->mock('ldap_connect' => sub { $ldap }); + my $user_entry = Net::LDAP::Entry->new; + push(@user_entries, $user_entry); + $user_entry->dn('ou=foobar'); + $user_entry->add( + uid => 'somebody', + cn => 'test', + ); + my $user = $back->find_user( { username => 'somebody' } ); + isa_ok( $user, "Catalyst::Authentication::Store::LDAP::User" ); + $user->check_password('password'); + is_deeply( [sort $user->roles], + [sort qw/quuxone quuxtwo/], + "User has the expected set of roles" ); + } + is_deeply(\@searches, [ + ['base', 'ou=foobar', 'filter', '(&(objectClass=inetOrgPerson)(uid=somebody))', 'scope', 'one'], + ['base', 'ou=roles', 'filter', '(&(objectClass=posixGroup)(memberUid=test))', 'scope', 'one', 'attrs', [ 'userinrole' ]], + ['base', 'ou=foobar', 'filter', '(&(objectClass=inetOrgPerson)(uid=somebody))', 'scope', 'one'], + ['base', 'ou=roles', 'filter', '(&(objectClass=posixGroup)(memberUid=test))', 'scope', 'one', 'attrs', [ 'userinrole' ]], + ], 'User searches as expected'); + is_deeply(\@binds, [ + [ undef ], # First user search + [ + 'ou=foobar', + 'password', + 'password' + ], # Rebind to confirm user + [ + undef + ], # Rebind with initial credentials to find roles + # 2nd pass round main loop + [ undef ], # First user search + [ + 'ou=foobar', + 'password', + 'password' + ] # Rebind to confirm user _and_ lookup roles; + ], 'Binds as expected'); +} diff --git a/t/lib/UserClass.pm b/t/lib/UserClass.pm new file mode 100644 index 0000000..c6d2d40 --- /dev/null +++ b/t/lib/UserClass.pm @@ -0,0 +1,10 @@ +package UserClass; +use strict; +use warnings; +use base qw( Catalyst::Authentication::Store::LDAP::User ); + +sub my_method { + return 'frobnitz'; +} + +1;