use BookStack\Access\Ldap;
use BookStack\Access\LdapService;
+use BookStack\Exceptions\LdapException;
use BookStack\Users\Models\Role;
use BookStack\Users\Models\User;
use Illuminate\Testing\TestResponse;
'services.ldap.id_attribute' => 'uid',
'services.ldap.user_to_groups' => false,
'services.ldap.version' => '3',
- 'services.ldap.user_filter' => '(&(uid=${user}))',
+ 'services.ldap.user_filter' => '(&(uid={user}))',
'services.ldap.follow_referrals' => false,
'services.ldap.tls_insecure' => false,
+ 'services.ldap.tls_ca_cert' => false,
'services.ldap.thumbnail_attribute' => null,
]);
$this->mockLdap = $this->mock(Ldap::class);
/**
* Set LDAP method mocks for things we commonly call without altering.
*/
- protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0)
+ protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0, int $groups = 0)
{
$this->mockLdap->shouldReceive('connect')->times($connects)->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->times($versions);
$this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true);
$this->mockEscapes($escapes);
$this->mockExplodes($explodes);
+ $this->mockGroupLookups($groups);
+ }
+
+ protected function mockGroupLookups(int $times = 1): void
+ {
+ $this->mockLdap->shouldReceive('read')->times($times)->andReturn(['count' => 0]);
+ $this->mockLdap->shouldReceive('getEntries')->times($times)->andReturn(['count' => 0]);
}
public function test_login()
$this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
}
+ public function test_login_works_when_ldap_server_does_not_provide_a_cn_value()
+ {
+ $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
+
+ $this->commonLdapMocks(1, 1, 1, 2, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
+ ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
+ ->andReturn(['count' => 1, 0 => [
+ 'dn' => $ldapDn,
+ 'mail' => [$this->mockUser->email],
+ ]]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/');
+ $this->assertDatabaseHas('users', [
+ 'name' => 'test-user',
+ 'email' => $this->mockUser->email,
+ ]);
+ }
+
public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
{
config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
$this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
}
+ public function test_user_filter_default_placeholder_format()
+ {
+ config()->set('services.ldap.user_filter', '(&(uid={user}))');
+ $this->mockUser->name = 'barryldapuser';
+ $expectedFilter = '(&(uid=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
+
+ $this->commonLdapMocks(1, 1, 1, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')
+ ->once()
+ ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
+ ->andReturn(['count' => 0, 0 => []]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ }
+
+ public function test_user_filter_old_placeholder_format()
+ {
+ config()->set('services.ldap.user_filter', '(&(username=${user}))');
+ $this->mockUser->name = 'barryldapuser';
+ $expectedFilter = '(&(username=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
+
+ $this->commonLdapMocks(1, 1, 1, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')
+ ->once()
+ ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
+ ->andReturn(['count' => 0, 0 => []]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ }
+
public function test_initial_incorrect_credentials()
{
$this->commonLdapMocks(1, 1, 1, 0, 1);
'services.ldap.remove_from_groups' => false,
]);
- $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
- $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
+ $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'services.ldap.remove_from_groups' => true,
]);
- $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
- $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
+ $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'dn' => 'dc=test,' . config('services.ldap.base_dn'),
'mail' => [$this->mockUser->email],
]];
- $this->commonLdapMocks(1, 1, 4, 5, 4, 2);
- $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
+ $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 0);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn($userResp, ['count' => 1,
- 0 => [
- 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
+ 0 => [
+ 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
'memberof' => [
'count' => 1,
- 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
],
],
- ], [
+ ]);
+
+ $this->mockLdap->shouldReceive('read')->times(2);
+ $this->mockLdap->shouldReceive('getEntries')->times(2)
+ ->andReturn([
'count' => 1,
- 0 => [
- 'dn' => 'cn=ldaptester,ou=groups,dc=example,dc=com',
- 'memberof' => [
+ 0 => [
+ 'dn' => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ 'memberof' => [
'count' => 1,
0 => 'cn=monsters,ou=groups,dc=example,dc=com',
],
],
],
'parsed_direct_user_groups' => [
- 'ldaptester',
+ 'cn=ldaptester,ou=groups,dc=example,dc=com',
],
'parsed_recursive_user_groups' => [
+ 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ 'cn=monsters,ou=groups,dc=example,dc=com',
+ ],
+ 'parsed_resulting_group_names' => [
'ldaptester',
'monsters',
],
]);
}
+ public function test_recursive_group_search_queries_via_full_dn()
+ {
+ app('config')->set([
+ 'services.ldap.user_to_groups' => true,
+ 'services.ldap.group_attribute' => 'memberOf',
+ ]);
+
+ $userResp = ['count' => 1, 0 => [
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
+ 'mail' => [$this->mockUser->email],
+ ]];
+ $groupResp = ['count' => 1,
+ 0 => [
+ 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
+ 'memberof' => [
+ 'count' => 1,
+ 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ ],
+ ],
+ ];
+
+ $this->commonLdapMocks(1, 1, 3, 4, 2, 1);
+
+ $escapedName = ldap_escape($this->mockUser->name);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->twice()
+ ->with($this->resourceId, config('services.ldap.base_dn'), "(&(uid={$escapedName}))", \Mockery::type('array'))
+ ->andReturn($userResp, $groupResp);
+
+ $this->mockLdap->shouldReceive('read')->times(1)
+ ->with($this->resourceId, 'cn=ldaptester,ou=groups,dc=example,dc=com', '(objectClass=*)', ['memberof'])
+ ->andReturn(['count' => 0]);
+ $this->mockLdap->shouldReceive('getEntries')->times(1)
+ ->with($this->resourceId, ['count' => 0])
+ ->andReturn(['count' => 0]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/');
+ }
+
public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
{
$role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
'services.ldap.remove_from_groups' => true,
]);
- $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
- $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
+ $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'services.ldap.remove_from_groups' => true,
]);
- $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
- $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
+ $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
$this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
}
+ public function test_login_uses_multiple_display_properties_if_defined()
+ {
+ app('config')->set([
+ 'services.ldap.display_name_attribute' => 'firstname|middlename|noname|lastname',
+ ]);
+
+ $this->commonLdapMocks(1, 1, 1, 2, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
+ ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
+ ->andReturn(['count' => 1, 0 => [
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => 'dc=test' . config('services.ldap.base_dn'),
+ 'firstname' => ['Barry'],
+ 'middlename' => ['Elliott'],
+ 'lastname' => ['Chuckle'],
+ 'mail' => [$this->mockUser->email],
+ ]]);
+
+ $this->mockUserLogin();
+
+ $this->assertDatabaseHas('users', [
+ 'email' => $this->mockUser->email,
+ 'name' => 'Barry Elliott Chuckle',
+ ]);
+ }
+
public function test_login_uses_default_display_name_attribute_if_specified_not_present()
{
app('config')->set([
'services.ldap.remove_from_groups' => true,
]);
- $this->commonLdapMocks(1, 1, 6, 8, 6, 4);
+ $this->commonLdapMocks(1, 1, 6, 8, 4, 2, 2);
$this->mockLdap->shouldReceive('searchAndGetEntries')
- ->times(6)
+ ->times(4)
->andReturn(['count' => 1, 0 => [
'uid' => [$user->name],
'cn' => [$user->name],
$this->assertNotNull($user->avatar);
$this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));
}
+
+ public function test_tls_ca_cert_option_throws_if_set_to_invalid_location()
+ {
+ $path = 'non_found_' . time();
+ config()->set(['services.ldap.tls_ca_cert' => $path]);
+
+ $this->commonLdapMocks(0, 0, 0, 0, 0);
+
+ $this->assertThrows(function () {
+ $this->withoutExceptionHandling()->mockUserLogin();
+ }, LdapException::class, "Provided path [{$path}] for LDAP TLS CA certs could not be resolved to an existing location");
+ }
+
+ public function test_tls_ca_cert_option_used_if_set_to_a_folder()
+ {
+ $path = $this->files->testFilePath('');
+ config()->set(['services.ldap.tls_ca_cert' => $path]);
+
+ $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTDIR, rtrim($path, '/'))->andReturn(true);
+ $this->runFailedAuthLogin();
+ }
+
+ public function test_tls_ca_cert_option_used_if_set_to_a_file()
+ {
+ $path = $this->files->testFilePath('test-file.txt');
+ config()->set(['services.ldap.tls_ca_cert' => $path]);
+
+ $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTFILE, $path)->andReturn(true);
+ $this->runFailedAuthLogin();
+ }
}