From ba441bbd587909caa4643d28201de0b4b2a06377 Mon Sep 17 00:00:00 2001 From: Andrew Ruthven Date: Mon, 6 Jul 2020 21:30:54 +1200 Subject: [PATCH 1/2] Create RT::Test::LDAP for common LDAP test logic In the interests of not repeating code, let's centralise it. This also forces the use of IPv4 for LDAP test, which solves an issue when the net.ipv6.bindv6only sysctl is set. It turns out this was enabled on my dev box, and is no longer a common setting. --- lib/RT/Test/LDAP.pm | 193 ++++++++++++++++++++++++++ t/externalauth/ldap.t | 107 +++++--------- t/externalauth/ldap_email_login.t | 43 +----- t/externalauth/ldap_escaping.t | 46 +----- t/externalauth/ldap_group.t | 49 ++----- t/externalauth/ldap_privileged.t | 38 +---- t/ldapimport/group-callbacks.t | 57 +++----- t/ldapimport/group-import.t | 48 ++----- t/ldapimport/group-member-import.t | 47 +++---- t/ldapimport/group-rename.t | 50 +++---- t/ldapimport/user-import-cfs.t | 34 ++--- t/ldapimport/user-import-privileged.t | 30 ++-- t/ldapimport/user-import.t | 39 ++---- 13 files changed, 358 insertions(+), 423 deletions(-) create mode 100644 lib/RT/Test/LDAP.pm diff --git a/lib/RT/Test/LDAP.pm b/lib/RT/Test/LDAP.pm new file mode 100644 index 00000000000..c91c4affe5a --- /dev/null +++ b/lib/RT/Test/LDAP.pm @@ -0,0 +1,193 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +# Portions Copyright 2023 Andrew Ruthven + +package RT::Test::LDAP; + +use strict; +use warnings; +use IO::Socket::INET; + +use base 'RT::Test'; + +sub new { + my $proto = shift; + my %options = @_; + my $class = ref($proto) ? ref($proto) : $proto; + my $self = bless { + ldap_ip => '127.0.0.1', + base_dn => $options{base_dn} || 'dc=bestpractical,dc=com', + }, $class; + + # Set a base default. Some tests will add or override portions of this + # hash. + $self->{'externalauth'} = { + 'My_LDAP' => { + 'type' => 'ldap', + 'base' => $self->{'base_dn'}, + 'filter' => '(objectClass=*)', + 'd_filter' => '()', + 'tls' => 0, + 'net_ldap_args' => [ version => 3 ], + 'attr_match_list' => [ 'Name', 'EmailAddress' ], + 'attr_map' => { + 'Name' => 'uid', + 'EmailAddress' => 'mail', + 'RealName' => 'cn', + 'Gecos' => 'uid', + 'NickName' => 'nick', + }, + }, + }; + + return $self; +} + +sub import { + my $class = shift; + my %args = @_; + + eval { + require RT::LDAPImport; + require RT::Authen::ExternalAuth; + require Net::LDAP::Server::Test; + 1; + } or do { + RT::Test::plan( + skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test' + ); + }; + + RT::Test::plan( tests => $args{'tests'} ) if $args{tests}; + + delete $args{tests}; + $class->SUPER::import(%args); + __PACKAGE__->export_to_level(1); +} + +sub new_server { + my $self = shift; + + $self->{'ldap_port'} = RT::Test->find_idle_port; + my $ldap_socket = IO::Socket::INET->new( + Listen => 5, + Proto => 'tcp', + Reuse => 1, + LocalAddr => $self->{'ldap_ip'}, + LocalPort => $self->{'ldap_port'}, + ) + || die "Failed to create socket: $IO::Socket::errstr"; + + $self->{'ldap_server'} + = Net::LDAP::Server::Test->new( $ldap_socket, auto_schema => 1 ) + || die "Failed to spawn test LDAP server on port " . $self->{'ldap_port'}; + + my $ldap_client + = Net::LDAP->new(join(':', $self->{'ldap_ip'}, $self->{'ldap_port'})) + || die "Failed to connect to LDAP server: $@"; + + $ldap_client->bind(); + $ldap_client->add($self->{'base_dn'}); + + return $ldap_client; +} + +sub config_set_externalauth { + my $self = shift; + my $settings = shift; + + $settings->{'ExternalAuthPriority'} //= ['My_LDAP']; + $settings->{'ExternalInfoPriority'} //= ['My_LDAP']; + $settings->{'AutoCreateNonExternalUsers'} //= 0; + $settings->{'AutoCreate'} //= undef; + + while (my ($key, $val) = each %{$settings}) { + RT->Config->Set($key, $val); + } + + $self->{'externalauth'}{'My_LDAP'}{'server'} //= + join(':', $self->{'ldap_ip'}, $self->{'ldap_port'}); + + RT->Config->Set(ExternalSettings => $self->{'externalauth'}); + RT->Config->PostLoadCheck; +} + +sub config_set_ldapimport { + my $self = shift; + my $settings = shift; + + $settings->{'LDAPHost'} + //= 'ldap://' . $self->{'ldap_ip'} . ':' . $self->{'ldap_port'}; + $settings->{'LDAPMapping'} //= { + Name => 'uid', + EmailAddress => 'mail', + RealName => 'cn', + }; + $settings->{'LDAPBase'} //= $self->{'base_dn'}; + $settings->{'LDAPFilter'} //= '(objectClass=User)'; + $settings->{'LDAPSkipAutogeneratedGroup'} //= 1; + + while (my ($key, $val) = each %{$settings}) { + RT->Config->Set($key, $val); + } +} + +sub config_set_ldapimport_group { + my $self = shift; + my $settings = shift; + + $settings->{'LDAPGroupBase'} //= $self->{'base_dn'}; + $settings->{'LDAPGroupFilter'} //= '(objectClass=Group)'; + + while (my ($key, $val) = each %{$settings}) { + RT->Config->Set($key, $val); + } +} + +1; diff --git a/t/externalauth/ldap.t b/t/externalauth/ldap.t index b0c707f8837..a1458475e9c 100644 --- a/t/externalauth/ldap.t +++ b/t/externalauth/ldap.t @@ -1,21 +1,13 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::Authen::ExternalAuth; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test'; -}; - - -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port" ); +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); my $username = "testuser"; -my $base = "dc=bestpractical,dc=com"; my $dn = "uid=$username,$base"; my $entry = { cn => $username, @@ -62,40 +54,24 @@ ok( $delegate_cf->Create( ); ok( $delegate_cf->AddToObject( RT::User->new( RT->SystemUser ) ), 'applied Delegate globally' ); -RT->Config->Set( ExternalAuthPriority => ['My_LDAP'] ); -RT->Config->Set( ExternalInfoPriority => ['My_LDAP'] ); -RT->Config->Set( AutoCreateNonExternalUsers => 0 ); -RT->Config->Set( AutoCreate => undef ); -RT->Config->Set( - ExternalSettings => { # AN EXAMPLE DB SERVICE - 'My_LDAP' => { - 'type' => 'ldap', - 'server' => "127.0.0.1:$ldap_port", - 'base' => $base, - 'filter' => '(objectClass=*)', - 'd_filter' => '()', - 'tls' => 0, - 'net_ldap_args' => [ version => 3 ], - 'attr_match_list' => [ 'Name', 'EmailAddress' ], - 'attr_map' => { - 'Name' => 'uid', - 'EmailAddress' => 'mail', - 'FreeformContactInfo' => [ 'uid', 'mail' ], - 'CF.Employee Type' => 'employeeType', - 'UserCF.Employee Type' => 'employeeType', - 'UserCF.Employee ID' => sub { - my %args = @_; - return ( 'employeeType', 'employeeID' ) unless $args{external_entry}; - return ( - $args{external_entry}->get_value('employeeType') // '', - $args{external_entry}->get_value('employeeID') // '', - ); - }, - } - }, - } -); -RT->Config->PostLoadCheck; +# Just wholesale replace the default attr_map. +$test->{'externalauth'}{'My_LDAP'}{'attr_map'} = { + 'Name' => 'uid', + 'EmailAddress' => 'mail', + 'FreeformContactInfo' => [ 'uid', 'mail' ], + 'CF.Employee Type' => 'employeeType', + 'UserCF.Employee Type' => 'employeeType', + 'UserCF.Employee ID' => sub { + my %args = @_; + return ( 'employeeType', 'employeeID' ) unless $args{external_entry}; + return ( + $args{external_entry}->get_value('employeeType') // '', + $args{external_entry}->get_value('employeeID') // '', + ); + }, +}; + +$test->config_set_externalauth(); my ( $baseurl, $m ) = RT::Test->started_ok(); @@ -217,31 +193,20 @@ diag "test user update via login"; diag 'Login with UserCF as username'; -RT::Test->stop_server(); - -RT->Config->Set( - ExternalSettings => { # AN EXAMPLE DB SERVICE - 'My_LDAP' => { - 'type' => 'ldap', - 'server' => "127.0.0.1:$ldap_port", - 'base' => $base, - 'filter' => '(objectClass=*)', - 'd_filter' => '()', - 'tls' => 0, - 'net_ldap_args' => [ version => 3 ], - 'attr_match_list' => [ 'UserCF.Employee ID', 'EmailAddress' ], - 'attr_map' => { - 'Name' => 'uid', - 'EmailAddress' => 'mail', - 'FreeformContactInfo' => [ 'uid', 'mail' ], - 'CF.Employee Type' => 'employeeType', - 'UserCF.Employee Type' => 'employeeType', - 'UserCF.Employee ID' => 'employeeID', - } - }, - } -); -RT->Config->PostLoadCheck; +$test->stop_server(); + +$test->{'externalauth'}{'My_LDAP'}{'attr_match_list'} + = [ 'UserCF.Employee ID', 'EmailAddress' ]; +$test->{'externalauth'}{'My_LDAP'}{'attr_map'} = { + 'Name' => 'uid', + 'EmailAddress' => 'mail', + 'FreeformContactInfo' => [ 'uid', 'mail' ], + 'CF.Employee Type' => 'employeeType', + 'UserCF.Employee Type' => 'employeeType', + 'UserCF.Employee ID' => 'employeeID', +}; + +$test->config_set_externalauth(); ( $baseurl, $m ) = RT::Test->started_ok(); diff --git a/t/externalauth/ldap_email_login.t b/t/externalauth/ldap_email_login.t index ffb726f7368..1561bbf22de 100644 --- a/t/externalauth/ldap_email_login.t +++ b/t/externalauth/ldap_email_login.t @@ -1,46 +1,13 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::Authen::ExternalAuth; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test'; -}; +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port" ); - -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); - -my $base = 'dc=bestpractical,dc=com'; - -RT->Config->Set( ExternalAuthPriority => ['My_LDAP'] ); -RT->Config->Set( ExternalInfoPriority => ['My_LDAP'] ); -RT->Config->Set( AutoCreate => undef ); -RT->Config->Set( - ExternalSettings => { - 'My_LDAP' => { - 'type' => 'ldap', - 'server' => "127.0.0.1:$ldap_port", - 'base' => $base, - 'filter' => '(objectClass=*)', - 'd_filter' => '()', - 'tls' => 0, - 'net_ldap_args' => [ version => 3 ], - 'attr_match_list' => [ 'Name', 'EmailAddress' ], - 'attr_map' => { - 'Name' => 'uid', - 'EmailAddress' => 'mail', - 'RealName' => 'cn', - 'Gecos' => 'uid', - 'NickName' => 'nick', - } - }, - } -); -RT->Config->PostLoadCheck; +$test->config_set_externalauth(); my ( $baseurl, $m ) = RT::Test->started_ok(); diff --git a/t/externalauth/ldap_escaping.t b/t/externalauth/ldap_escaping.t index 0b41e6d6538..4c89edd08a7 100644 --- a/t/externalauth/ldap_escaping.t +++ b/t/externalauth/ldap_escaping.t @@ -1,22 +1,14 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::Authen::ExternalAuth; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test'; -}; +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); - -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port" ); - -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); - -my $users_dn = "ou=users,dc=bestpractical,dc=com"; -my $group_dn = "cn=test group,ou=groups,dc=bestpractical,dc=com"; +my $users_dn = "ou=users,$base"; +my $group_dn = "cn=test group,ou=groups,$base"; $ldap->add($users_dn); $ldap->add( @@ -48,31 +40,7 @@ $ldap->add( ], ); -RT->Config->Set( ExternalAuthPriority => ['My_LDAP'] ); -RT->Config->Set( ExternalInfoPriority => ['My_LDAP'] ); -RT->Config->Set( AutoCreateNonExternalUsers => 0 ); -RT->Config->Set( AutoCreate => undef ); -RT->Config->Set( - ExternalSettings => { - 'My_LDAP' => { - 'type' => 'ldap', - 'server' => "127.0.0.1:$ldap_port", - 'base' => $users_dn, - 'filter' => '(objectClass=*)', - 'd_filter' => '()', - 'group' => $group_dn, - 'group_attr' => 'memberDN', - 'tls' => 0, - 'net_ldap_args' => [ version => 3 ], - 'attr_match_list' => [ 'Name', 'EmailAddress' ], - 'attr_map' => { - 'Name' => 'uid', - 'EmailAddress' => 'mail', - } - }, - } -); -RT->Config->PostLoadCheck; +$test->config_set_externalauth(); my ( $baseurl, $m ) = RT::Test->started_ok(); diff --git a/t/externalauth/ldap_group.t b/t/externalauth/ldap_group.t index 168c37b0778..ad272fcf6c1 100644 --- a/t/externalauth/ldap_group.t +++ b/t/externalauth/ldap_group.t @@ -6,22 +6,14 @@ BEGIN { $ENV{RT_TEST_WEB_HANDLER} = 'inline'; } -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::Authen::ExternalAuth; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test'; -}; +my $test = new RT::Test::LDAP; +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); - -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port" ); - -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); - -my $users_dn = "ou=users,dc=bestpractical,dc=com"; -my $group_dn = "cn=test group,ou=groups,dc=bestpractical,dc=com"; +my $users_dn = "ou=users,$base"; +my $group_dn = "cn=test group,ou=groups,$base"; $ldap->add($users_dn); for (1 .. 3) { @@ -55,31 +47,10 @@ $ldap->add( ], ); -RT->Config->Set( ExternalAuthPriority => ['My_LDAP'] ); -RT->Config->Set( ExternalInfoPriority => ['My_LDAP'] ); -RT->Config->Set( AutoCreateNonExternalUsers => 0 ); -RT->Config->Set( AutoCreate => undef ); -RT->Config->Set( - ExternalSettings => { - 'My_LDAP' => { - 'type' => 'ldap', - 'server' => "127.0.0.1:$ldap_port", - 'base' => $users_dn, - 'filter' => '(objectClass=*)', - 'd_filter' => '()', - 'group' => $group_dn, - 'group_attr' => 'memberDN', - 'tls' => 0, - 'net_ldap_args' => [ version => 3 ], - 'attr_match_list' => [ 'Name', 'EmailAddress' ], - 'attr_map' => { - 'Name' => 'uid', - 'EmailAddress' => 'mail', - } - }, - } -); -RT->Config->PostLoadCheck; +$test->{externalauth}{My_LDAP}{group} = $group_dn; +$test->{externalauth}{My_LDAP}{group_attr} = 'memberDN'; + +$test->config_set_externalauth(); my ( $baseurl, $m ) = RT::Test->started_ok(); diff --git a/t/externalauth/ldap_privileged.t b/t/externalauth/ldap_privileged.t index 02e760bf3d5..3ab5336b4f2 100644 --- a/t/externalauth/ldap_privileged.t +++ b/t/externalauth/ldap_privileged.t @@ -1,20 +1,13 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::Authen::ExternalAuth; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test'; -}; - -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port" ); +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); my $username = "testuser"; -my $base = "dc=bestpractical,dc=com"; my $dn = "uid=$username,$base"; my $entry = { cn => $username, @@ -26,28 +19,7 @@ my $entry = { $ldap->add( $base ); $ldap->add( $dn, attr => [%$entry] ); -RT->Config->Set( ExternalAuthPriority => ['My_LDAP'] ); -RT->Config->Set( ExternalInfoPriority => ['My_LDAP'] ); -RT->Config->Set( AutoCreateNonExternalUsers => 0 ); -RT->Config->Set( AutoCreate => { Privileged => 1 } ); -RT->Config->Set( - ExternalSettings => { # AN EXAMPLE DB SERVICE - 'My_LDAP' => { - 'type' => 'ldap', - 'server' => "127.0.0.1:$ldap_port", - 'base' => $base, - 'filter' => '(objectClass=*)', - 'tls' => 0, - 'net_ldap_args' => [ version => 3 ], - 'attr_match_list' => [ 'Name', 'EmailAddress' ], - 'attr_map' => { - 'Name' => 'uid', - 'EmailAddress' => 'mail', - } - }, - } -); -RT->Config->PostLoadCheck; +$test->config_set_externalauth({ AutoCreate => { Privileged => 1 } }); my ( $baseurl, $m ) = RT::Test->started_ok(); diff --git a/t/ldapimport/group-callbacks.t b/t/ldapimport/group-callbacks.t index 272d32921ab..8433f95f420 100644 --- a/t/ldapimport/group-callbacks.t +++ b/t/ldapimport/group-callbacks.t @@ -1,26 +1,19 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("dc=bestpractical,dc=com"); - my @ldap_user_entries; for ( 1 .. 12 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,ou=foo,$base"; my $entry = { dn => $dn, cn => "Test User $_", @@ -35,7 +28,7 @@ for ( 1 .. 12 ) { my @ldap_group_entries; for ( 1 .. 4 ) { my $groupname = "Test Group $_"; - my $dn = "cn=$groupname,ou=groups,dc=bestpractical,dc=com"; + my $dn = "cn=$groupname,ou=groups,$base"; my $entry = { cn => $groupname, gid => $_, @@ -46,29 +39,21 @@ for ( 1 .. 4 ) { push @ldap_group_entries, $entry; } -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPMapping', - {Name => 'uid', - EmailAddress => 'mail', - RealName => 'cn'}); -RT->Config->Set('LDAPBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); -RT->Config->Set('LDAPSkipAutogeneratedGroup',1); - -RT->Config->Set('LDAPGroupBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPGroupFilter','(objectClass=Group)'); -RT->Config->Set('LDAPGroupMapping', { - Name => 'cn', - Member_Attr => sub { - my %args = @_; - my $self = $args{'self'}; - my $members = $args{ldap_entry}->get_value('members', asref => 1); - foreach my $record ( @$members ) { - my $user = RT::User->new( RT->SystemUser ); - $user->LoadByEmail($record =~ /mail="(.*)"/); - $self->_users->{ lc $record } = $user->Name; - } - return @$members; +$test->config_set_ldapimport(); +$test->config_set_ldapimport_group({ + 'LDAPGroupMapping' => { + Name => 'cn', + Member_Attr => sub { + my %args = @_; + my $self = $args{'self'}; + my $members = $args{ldap_entry}->get_value('members', asref => 1); + foreach my $record ( @$members ) { + my $user = RT::User->new( RT->SystemUser ); + $user->LoadByEmail($record =~ /mail="(.*)"/); + $self->_users->{ lc $record } = $user->Name; + } + return @$members; + }, }, }); diff --git a/t/ldapimport/group-import.t b/t/ldapimport/group-import.t index fc3f97bd92b..9d29930e57a 100644 --- a/t/ldapimport/group-import.t +++ b/t/ldapimport/group-import.t @@ -1,26 +1,19 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("dc=bestpractical,dc=com"); - my @ldap_user_entries; for ( 1 .. 12 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,ou=foo,$base"; my $entry = { dn => $dn, cn => "Test User $_ ".int rand(200), @@ -35,7 +28,7 @@ for ( 1 .. 12 ) { my @ldap_group_entries; for ( 1 .. 4 ) { my $groupname = "Test Group $_"; - my $dn = "cn=$groupname,ou=groups,dc=bestpractical,dc=com"; + my $dn = "cn=$groupname,ou=groups,$base"; my $entry = { cn => $groupname, members => [ map { $_->{dn} } @ldap_user_entries[($_-1),($_+3),($_+7)] ], @@ -46,32 +39,24 @@ for ( 1 .. 4 ) { push @ldap_group_entries, $entry; } $ldap->add( - "cn=42,ou=groups,dc=bestpractical,dc=com", + "cn=42,ou=groups,$base", attr => [ cn => "42", - members => [ "uid=testuser1,ou=foo,dc=bestpractical,dc=com" ], + members => [ "uid=testuser1,ou=foo,$base" ], objectClass => 'Group', ], ); my $entry = { cn => "testdisabled", - members => ["uid=testuser1,ou=foo,dc=bestpractical,dc=com"], + members => ["uid=testuser1,ou=foo,$base"], objectClass => 'Group', disabled => 1, }; -$ldap->add( "cn=testdisabled,ou=groups,dc=bestpractical,dc=com", attr => [ %$entry ] ); +$ldap->add( "cn=testdisabled,ou=groups,$base", attr => [ %$entry ] ); push @ldap_group_entries, $entry; - -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPMapping', - {Name => 'uid', - EmailAddress => 'mail', - RealName => 'cn'}); -RT->Config->Set('LDAPBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); -RT->Config->Set('LDAPSkipAutogeneratedGroup',1); +$test->config_set_ldapimport(); ok($importer->import_users( import => 1 )); for my $entry (@ldap_user_entries) { @@ -82,11 +67,8 @@ for my $entry (@ldap_user_entries) { ok($user->Id, "Found $entry->{cn} as ".$user->Id); } -RT->Config->Set('LDAPGroupBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPGroupFilter','(objectClass=Group)'); -RT->Config->Set( - 'LDAPGroupMapping', - { +$test->config_set_ldapimport_group({ + 'LDAPGroupMapping' => { Name => 'cn', Member_Attr => 'members', Disabled => sub { @@ -94,7 +76,7 @@ RT->Config->Set( return $args{ldap_entry}->get_value('disabled') ? 1 : 0; }, } -); +}); # confirm that we skip the import ok( $importer->import_groups() ); @@ -110,7 +92,7 @@ my $group = RT::Group->new($RT::SystemUser); $group->LoadUserDefinedGroup('testdisabled'); ok( $group->Disabled, 'Group testdisabled is disabled' ); -$ldap->modify( "cn=testdisabled,ou=groups,dc=bestpractical,dc=com", replace => { disabled => 0 } ); +$ldap->modify( "cn=testdisabled,ou=groups,$base", replace => { disabled => 0 } ); ok( $importer->import_groups( import => 1 ), "imported groups" ); $group->LoadUserDefinedGroup('testdisabled'); ok( !$group->Disabled, 'Group testdisabled is enabled' ); diff --git a/t/ldapimport/group-member-import.t b/t/ldapimport/group-member-import.t index 651f5ab6c80..13d43172233 100644 --- a/t/ldapimport/group-member-import.t +++ b/t/ldapimport/group-member-import.t @@ -1,26 +1,19 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("dc=bestpractical,dc=com"); - my @ldap_user_entries; for ( 1 .. 12 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,ou=foo,$base"; my $entry = { dn => $dn, cn => "Test User $_ ".int rand(200), @@ -35,7 +28,7 @@ for ( 1 .. 12 ) { my @ldap_group_entries; for ( 1 .. 4 ) { my $groupname = "Test Group $_"; - my $dn = "cn=$groupname,ou=groups,dc=bestpractical,dc=com"; + my $dn = "cn=$groupname,ou=groups,$base"; my $entry = { cn => $groupname, members => [ map { $_->{dn} } @ldap_user_entries[($_-1),($_+3),($_+7)] ], @@ -46,30 +39,22 @@ for ( 1 .. 4 ) { push @ldap_group_entries, $entry; } $ldap->add( - "cn=42,ou=groups,dc=bestpractical,dc=com", + "cn=42,ou=groups,$base", attr => [ cn => "42", - members => [ "uid=testuser1,ou=foo,dc=bestpractical,dc=com" ], + members => [ "uid=testuser1,ou=foo,$base" ], objectClass => 'Group', ], ); -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPMapping', - {Name => 'uid', - EmailAddress => 'mail', - RealName => 'cn'}); -RT->Config->Set('LDAPBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); -RT->Config->Set('LDAPSkipAutogeneratedGroup',1); - -RT->Config->Set('LDAPGroupBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPGroupFilter','(objectClass=Group)'); -RT->Config->Set('LDAPGroupMapping', - {Name => 'cn', - Member_Attr => 'members', - }); -RT->Config->Set('LDAPImportGroupMembers',1); +$test->config_set_ldapimport(); +$test->config_set_ldapimport_group({ + 'LDAPGroupMapping' => { + Name => 'cn', + Member_Attr => 'members', + }, + 'LDAPImportGroupMembers' => 1, +}); # confirm that we skip the import ok( $importer->import_groups() ); diff --git a/t/ldapimport/group-rename.t b/t/ldapimport/group-rename.t index 786533ef9b0..973c03d4ba2 100644 --- a/t/ldapimport/group-rename.t +++ b/t/ldapimport/group-rename.t @@ -1,26 +1,19 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $test = RT::Test::LDAP->new(); +my $base = $test->{'base_dn'}; +my $ldap = $test->new_server(); my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("dc=bestpractical,dc=com"); - my @ldap_user_entries; for ( 1 .. 12 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,ou=foo,$base"; my $entry = { dn => $dn, cn => "Test User $_", @@ -35,7 +28,7 @@ for ( 1 .. 12 ) { my @ldap_group_entries; for ( 1 .. 4 ) { my $groupname = "Test Group $_"; - my $dn = "cn=$groupname,ou=groups,dc=bestpractical,dc=com"; + my $dn = "cn=$groupname,ou=groups,$base"; my $entry = { cn => $groupname, gid => $_, @@ -46,22 +39,13 @@ for ( 1 .. 4 ) { push @ldap_group_entries, $entry; } -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPMapping', - {Name => 'uid', - EmailAddress => 'mail', - RealName => 'cn'}); -RT->Config->Set('LDAPBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); -RT->Config->Set('LDAPSkipAutogeneratedGroup',1); - -RT->Config->Set('LDAPGroupBase','dc=bestpractical,dc=com'); -RT->Config->Set('LDAPGroupFilter','(objectClass=Group)'); -RT->Config->Set('LDAPGroupMapping', - { - Name => 'cn', - Member_Attr => 'members', - }); +$test->config_set_ldapimport(); +$test->config_set_ldapimport_group({ + 'LDAPGroupMapping' => { + Name => 'cn', + Member_Attr => 'members', + }, +}); ok( $importer->import_users( import => 1 ), 'imported users'); # no id mapping @@ -84,7 +68,7 @@ ok( $importer->import_users( import => 1 ), 'imported users'); # rename a group { $ldap->modify( - "cn=Test Group 1,ou=groups,dc=bestpractical,dc=com", + "cn=Test Group 1,ou=groups,$base", replace => { 'cn' => 'Test Group 1 Renamed' }, ); ok( $importer->import_groups( import => 1 ), "imported groups" ); @@ -98,11 +82,11 @@ ok( $importer->import_users( import => 1 ), 'imported users'); is_member_of('testuser2', 'Test Group 2'); is_member_of('testuser3', 'Test Group 3'); $ldap->modify( - "cn=Test Group 2,ou=groups,dc=bestpractical,dc=com", + "cn=Test Group 2,ou=groups,$base", replace => { 'cn' => 'Test Group 3' }, ); $ldap->modify( - "cn=Test Group 3,ou=groups,dc=bestpractical,dc=com", + "cn=Test Group 3,ou=groups,$base", replace => { 'cn' => 'Test Group 2' }, ); ok( $importer->import_groups( import => 1 ), "imported groups" ); @@ -134,5 +118,3 @@ sub get_group { $group->LoadUserDefinedGroup( $gname ); return $group; } - - diff --git a/t/ldapimport/user-import-cfs.t b/t/ldapimport/user-import-cfs.t index a0c723ee040..00a9e9956dd 100644 --- a/t/ldapimport/user-import-cfs.t +++ b/t/ldapimport/user-import-cfs.t @@ -1,11 +1,11 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $base = "ou=foo,dc=bestpractical,dc=com"; +my $test = RT::Test::LDAP->new(base => $base); +my $ldap = $test->new_server(); { my $cf = RT::CustomField->new(RT->SystemUser); @@ -25,18 +25,10 @@ eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); - -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("ou=foo,dc=bestpractical,dc=com"); - my @ldap_entries; for ( 0 .. 12 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,ou=foo,$base"; my $entry = { cn => "Test User $_ ".int rand(200), mail => "$username\@invalid.tld", @@ -48,14 +40,14 @@ for ( 0 .. 12 ) { $ldap->add( $dn, attr => [%$entry] ); } -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPMapping', - {Name => 'uid', - EmailAddress => 'mail', - RealName => 'cn', - 'UserCF.Employee Number' => 'employeeId',}); -RT->Config->Set('LDAPBase','ou=foo,dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); +$test->config_set_ldapimport({ + 'LDAPMapping' => { + Name => 'uid', + EmailAddress => 'mail', + RealName => 'cn', + 'UserCF.Employee Number' => 'employeeId', + }, +}); # check that we don't import ok($importer->import_users()); diff --git a/t/ldapimport/user-import-privileged.t b/t/ldapimport/user-import-privileged.t index 4b155eea74d..6527f4c5630 100644 --- a/t/ldapimport/user-import-privileged.t +++ b/t/ldapimport/user-import-privileged.t @@ -1,27 +1,19 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $base = "ou=foo,dc=bestpractical,dc=com"; +my $test = RT::Test::LDAP->new(base => $base); +my $ldap = $test->new_server(); my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); - -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("ou=foo,dc=bestpractical,dc=com"); - my @ldap_entries; for ( 1 .. 13 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,$base"; my $entry = { cn => "Test User $_ ".int rand(200), mail => "$username\@invalid.tld", @@ -32,15 +24,9 @@ for ( 1 .. 13 ) { $ldap->add( $dn, attr => [%$entry] ); } - -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPMapping', - {Name => 'uid', - EmailAddress => 'mail', - RealName => 'cn'}); -RT->Config->Set('LDAPBase','ou=foo,dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); -RT->Config->Set('LDAPCreatePrivileged', 1); +$test->config_set_ldapimport({ + 'LDAPCreatePrivileged' => 1, +}); # check that we don't import ok($importer->import_users()); diff --git a/t/ldapimport/user-import.t b/t/ldapimport/user-import.t index c4f6a5934c9..95ace177d89 100644 --- a/t/ldapimport/user-import.t +++ b/t/ldapimport/user-import.t @@ -1,27 +1,19 @@ use strict; use warnings; -use RT::Test tests => undef; +use RT::Test::LDAP tests => undef; -eval { require RT::LDAPImport; require Net::LDAP::Server::Test; 1; } or do { - plan skip_all => 'Unable to test without RT::LDAPImport and Net::LDAP::Server::Test'; -}; +my $base = "ou=foo,dc=bestpractical,dc=com"; +my $test = RT::Test::LDAP->new(base => $base); +my $ldap = $test->new_server(); my $importer = RT::LDAPImport->new; isa_ok($importer,'RT::LDAPImport'); -my $ldap_port = RT::Test->find_idle_port; -ok( my $server = Net::LDAP::Server::Test->new( $ldap_port, auto_schema => 1 ), - "spawned test LDAP server on port $ldap_port"); - -my $ldap = Net::LDAP->new("localhost:$ldap_port"); -$ldap->bind(); -$ldap->add("ou=foo,dc=bestpractical,dc=com"); - my @ldap_entries; for ( 1 .. 13 ) { my $username = "testuser$_"; - my $dn = "uid=$username,ou=foo,dc=bestpractical,dc=com"; + my $dn = "uid=$username,$base"; my $entry = { cn => "Test User $_ ".int rand(200), mail => "$username\@invalid.tld", @@ -32,7 +24,7 @@ for ( 1 .. 13 ) { $ldap->add( $dn, attr => [%$entry] ); } $ldap->add( - "uid=9000,ou=foo,dc=bestpractical,dc=com", + "uid=9000,$base", attr => [ cn => "Numeric user", mail => "numeric\@invalid.tld", @@ -42,7 +34,7 @@ $ldap->add( ); $ldap->add( - "uid=testdisabled,ou=foo,dc=bestpractical,dc=com", + "uid=testdisabled,$base", attr => [ cn => "Disabled user", mail => "testdisabled\@invalid.tld", @@ -52,11 +44,8 @@ $ldap->add( ], ); -RT->Config->Set('LDAPHost',"ldap://localhost:$ldap_port"); -RT->Config->Set('LDAPOptions', [ port => $ldap_port ]); -RT->Config->Set( - 'LDAPMapping', - { +$test->config_set_ldapimport({ + 'LDAPMapping' => { Name => 'uid', EmailAddress => 'mail', RealName => 'cn', @@ -64,11 +53,9 @@ RT->Config->Set( my %args = @_; return $args{ldap_entry}->get_value('disabled') ? 1 : 0; }, - } -); -RT->Config->Set('LDAPBase','ou=foo,dc=bestpractical,dc=com'); -RT->Config->Set('LDAPFilter','(objectClass=User)'); -RT->Config->Set('LDAPUpdateUsers', 1); + }, + 'LDAPUpdateUsers' => 1, +}); # check that we don't import ok($importer->import_users()); @@ -102,7 +89,7 @@ ok(!$user->Id); $user->Load('testdisabled'); ok( $user->Disabled, 'User testdisabled is disabled' ); -$ldap->modify( "uid=testdisabled,ou=foo,dc=bestpractical,dc=com", replace => { disabled => 0 } ); +$ldap->modify( "uid=testdisabled,$base", replace => { disabled => 0 } ); ok( $importer->import_users( import => 1 ) ); $user->Load('testdisabled'); ok( !$user->Disabled, 'User testdisabled is enabled' ); From 37af5c91199389d77bcf0fbbb59d5c425b3a167a Mon Sep 17 00:00:00 2001 From: Andrew Ruthven Date: Fri, 15 Aug 2025 23:10:16 +1200 Subject: [PATCH 2/2] Let Net::LDAP::Server::Test make the socket for us This restores the behaviour before 8a528942cf41c2d2db248be5c9bd02a90cb4c5d3 to let Net::LDAP::Server::Test make the socket. The differnt behaviour I was seeing was due to have a non-standard sysctl setting. I have left this commit in the history incase it is useful in the future. --- lib/RT/Test/LDAP.pm | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/RT/Test/LDAP.pm b/lib/RT/Test/LDAP.pm index c91c4affe5a..9a5256529da 100644 --- a/lib/RT/Test/LDAP.pm +++ b/lib/RT/Test/LDAP.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2025 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -46,13 +46,12 @@ # # END BPS TAGGED BLOCK }}} -# Portions Copyright 2023 Andrew Ruthven +# Portions Copyright 2023-2025 Andrew Ruthven package RT::Test::LDAP; use strict; use warnings; -use IO::Socket::INET; use base 'RT::Test'; @@ -61,7 +60,6 @@ sub new { my %options = @_; my $class = ref($proto) ? ref($proto) : $proto; my $self = bless { - ldap_ip => '127.0.0.1', base_dn => $options{base_dn} || 'dc=bestpractical,dc=com', }, $class; @@ -114,22 +112,14 @@ sub import { sub new_server { my $self = shift; - $self->{'ldap_port'} = RT::Test->find_idle_port; - my $ldap_socket = IO::Socket::INET->new( - Listen => 5, - Proto => 'tcp', - Reuse => 1, - LocalAddr => $self->{'ldap_ip'}, - LocalPort => $self->{'ldap_port'}, - ) - || die "Failed to create socket: $IO::Socket::errstr"; + $self->{'ldap_port'} = RT::Test->find_idle_port; - $self->{'ldap_server'} - = Net::LDAP::Server::Test->new( $ldap_socket, auto_schema => 1 ) - || die "Failed to spawn test LDAP server on port " . $self->{'ldap_port'}; + $self->{'ldap_server'} = Net::LDAP::Server::Test->new( + $self->{'ldap_port'}, auto_schema => 1 + ) || die "Failed to spawn test LDAP server on port " . $self->{'ldap_port'}; my $ldap_client - = Net::LDAP->new(join(':', $self->{'ldap_ip'}, $self->{'ldap_port'})) + = Net::LDAP->new('localhost:' . $self->{'ldap_port'}) || die "Failed to connect to LDAP server: $@"; $ldap_client->bind(); @@ -139,7 +129,7 @@ sub new_server { } sub config_set_externalauth { - my $self = shift; + my $self = shift; my $settings = shift; $settings->{'ExternalAuthPriority'} //= ['My_LDAP']; @@ -152,7 +142,7 @@ sub config_set_externalauth { } $self->{'externalauth'}{'My_LDAP'}{'server'} //= - join(':', $self->{'ldap_ip'}, $self->{'ldap_port'}); + 'localhost:' . $self->{'ldap_port'}; RT->Config->Set(ExternalSettings => $self->{'externalauth'}); RT->Config->PostLoadCheck; @@ -163,7 +153,7 @@ sub config_set_ldapimport { my $settings = shift; $settings->{'LDAPHost'} - //= 'ldap://' . $self->{'ldap_ip'} . ':' . $self->{'ldap_port'}; + //= 'ldap://localhost:' . $self->{'ldap_port'}; $settings->{'LDAPMapping'} //= { Name => 'uid', EmailAddress => 'mail',