Added support for LDAP Posix Groups (OpenLDAP with memberUid)

This commit is contained in:
Frederic Guillot 2016-04-30 20:38:16 -04:00
parent 2afd7ee834
commit 3872dee261
12 changed files with 516 additions and 33 deletions

View File

@ -3,6 +3,7 @@ Version 1.0.28 (unreleased)
New features:
* Added support for LDAP Posix Groups (OpenLDAP with memberUid)
* Search in activity stream
* Search in comments
* Search by task creator

View File

@ -39,12 +39,11 @@ class Group
* @access public
* @param Client $client
* @param string $query
* @return array
* @return LdapGroupProvider[]
*/
public static function getGroups(Client $client, $query)
{
$className = get_called_class();
$self = new $className(new Query($client));
$self = new static(new Query($client));
return $self->find($query);
}
@ -111,7 +110,7 @@ class Group
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME');
}
return LDAP_GROUP_ATTRIBUTE_NAME;
return strtolower(LDAP_GROUP_ATTRIBUTE_NAME);
}
/**

View File

@ -66,6 +66,10 @@ class Query
$this->entries = $entries;
if (DEBUG && $this->client->hasLogger()) {
$this->client->getLogger()->debug('NbEntries='.$entries['count']);
}
return $this;
}

View File

@ -22,15 +22,25 @@ class User
*/
protected $query;
/**
* LDAP Group object
*
* @access protected
* @var Group
*/
protected $group;
/**
* Constructor
*
* @access public
* @param Query $query
* @param Query $query
* @param Group $group
*/
public function __construct(Query $query)
public function __construct(Query $query, Group $group = null)
{
$this->query = $query;
$this->group = $group;
}
/**
@ -44,7 +54,7 @@ class User
*/
public static function getUser(Client $client, $username)
{
$self = new static(new Query($client));
$self = new static(new Query($client), new Group(new Query($client)));
return $self->find($self->getLdapUserPattern($username));
}
@ -53,7 +63,7 @@ class User
*
* @access public
* @param string $query
* @return null|LdapUserProvider
* @return LdapUserProvider
*/
public function find($query)
{
@ -67,6 +77,56 @@ class User
return $user;
}
/**
* Get user groupIds (DN)
*
* 1) If configured, use memberUid and posixGroup
* 2) Otherwise, use memberOf
*
* @access protected
* @param Entry $entry
* @param string $username
* @return string[]
*/
protected function getGroups(Entry $entry, $username)
{
$groupIds = array();
if (! empty($username) && $this->group !== null && $this->hasGroupUserFilter()) {
$groups = $this->group->find(sprintf($this->getGroupUserFilter(), $username));
foreach ($groups as $group) {
$groupIds[] = $group->getExternalId();
}
} else {
$groupIds = $entry->getAll($this->getAttributeGroup());
}
return $groupIds;
}
/**
* Get role from LDAP groups
*
* @access protected
* @param string[] $groupIds
* @return string
*/
protected function getRole(array $groupIds)
{
foreach ($groupIds as $groupId) {
$groupId = strtolower($groupId);
if ($groupId === strtolower($this->getGroupAdminDn())) {
return Role::APP_ADMIN;
} elseif ($groupId === strtolower($this->getGroupManagerDn())) {
return Role::APP_MANAGER;
}
}
return Role::APP_USER;
}
/**
* Build user profile
*
@ -76,21 +136,16 @@ class User
protected function build()
{
$entry = $this->query->getEntries()->getFirstEntry();
$role = Role::APP_USER;
if ($entry->hasValue($this->getAttributeGroup(), $this->getGroupAdminDn())) {
$role = Role::APP_ADMIN;
} elseif ($entry->hasValue($this->getAttributeGroup(), $this->getGroupManagerDn())) {
$role = Role::APP_MANAGER;
}
$username = $entry->getFirstValue($this->getAttributeUsername());
$groupIds = $this->getGroups($entry, $username);
return new LdapUserProvider(
$entry->getDn(),
$entry->getFirstValue($this->getAttributeUsername()),
$username,
$entry->getFirstValue($this->getAttributeName()),
$entry->getFirstValue($this->getAttributeEmail()),
$role,
$entry->getAll($this->getAttributeGroup())
$this->getRole($groupIds),
$groupIds
);
}
@ -124,7 +179,7 @@ class User
throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
return LDAP_USER_ATTRIBUTE_USERNAME;
return strtolower(LDAP_USER_ATTRIBUTE_USERNAME);
}
/**
@ -139,7 +194,7 @@ class User
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME');
}
return LDAP_USER_ATTRIBUTE_FULLNAME;
return strtolower(LDAP_USER_ATTRIBUTE_FULLNAME);
}
/**
@ -154,18 +209,40 @@ class User
throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
}
return LDAP_USER_ATTRIBUTE_EMAIL;
return strtolower(LDAP_USER_ATTRIBUTE_EMAIL);
}
/**
* Get LDAP account memberof attribute
* Get LDAP account memberOf attribute
*
* @access public
* @return string
*/
public function getAttributeGroup()
{
return LDAP_USER_ATTRIBUTE_GROUPS;
return strtolower(LDAP_USER_ATTRIBUTE_GROUPS);
}
/**
* Get LDAP Group User filter
*
* @access public
* @return string
*/
public function getGroupUserFilter()
{
return LDAP_GROUP_USER_FILTER;
}
/**
* Return true if LDAP Group User filter is defined
*
* @access public
* @return string
*/
public function hasGroupUserFilter()
{
return $this->getGroupUserFilter() !== '' && $this->getGroupUserFilter() !== null;
}
/**
@ -176,7 +253,7 @@ class User
*/
public function getGroupAdminDn()
{
return LDAP_GROUP_ADMIN_DN;
return strtolower(LDAP_GROUP_ADMIN_DN);
}
/**

View File

@ -170,10 +170,10 @@ class LdapUserProvider implements UserProviderInterface
}
/**
* Get groups
* Get groups DN
*
* @access public
* @return array
* @return string[]
*/
public function getExternalGroupIds()
{

View File

@ -64,6 +64,7 @@ defined('LDAP_GROUP_MANAGER_DN') or define('LDAP_GROUP_MANAGER_DN', '');
defined('LDAP_GROUP_PROVIDER') or define('LDAP_GROUP_PROVIDER', false);
defined('LDAP_GROUP_BASE_DN') or define('LDAP_GROUP_BASE_DN', '');
defined('LDAP_GROUP_FILTER') or define('LDAP_GROUP_FILTER', '');
defined('LDAP_GROUP_USER_FILTER') or define('LDAP_GROUP_USER_FILTER', '');
defined('LDAP_GROUP_ATTRIBUTE_NAME') or define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn');
// Proxy authentication

View File

@ -127,6 +127,11 @@ define('LDAP_GROUP_BASE_DN', '');
// Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*))
define('LDAP_GROUP_FILTER', '');
// LDAP user group filter
// If this filter is configured, Kanboard will search user groups in LDAP_GROUP_BASE_DN with this filter
// Example for OpenLDAP: (&(objectClass=posixGroup)(memberUid=%s))
define('LDAP_GROUP_USER_FILTER', '');
// LDAP attribute for the group name
define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn');

View File

@ -170,6 +170,11 @@ define('LDAP_GROUP_BASE_DN', '');
// Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*))
define('LDAP_GROUP_FILTER', '');
// LDAP user group filter
// If this filter is configured, Kanboard will search user groups in LDAP_GROUP_BASE_DN
// Example for OpenLDAP: (&(objectClass=posixGroup)(memberUid=%s))
define('LDAP_GROUP_USER_FILTER', '');
// LDAP attribute for the group name
define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn');
```

View File

@ -167,8 +167,8 @@ Just change the value of `LDAP_ACCOUNT_CREATION` to `false`:
define('LDAP_ACCOUNT_CREATION', false);
```
Troubleshootings
----------------
Troubleshooting
---------------
### SELinux restrictions

View File

@ -5,7 +5,7 @@ Requirements
------------
- Have LDAP authentication properly configured
- Use a LDAP server that supports `memberOf`
- Use a LDAP server that supports `memberOf` or `memberUid` (PosixGroups)
Define automatically user roles based on LDAP groups
----------------------------------------------------
@ -15,7 +15,7 @@ Use these constants in your config file:
- `LDAP_GROUP_ADMIN_DN`: Distinguished names for application administrators
- `LDAP_GROUP_MANAGER_DN`: Distinguished names for application managers
Example:
### Example for Active Directory:
```php
define('LDAP_GROUP_ADMIN_DN', 'CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local');
@ -26,6 +26,18 @@ define('LDAP_GROUP_MANAGER_DN', 'CN=Kanboard Managers,CN=Users,DC=kanboard,DC=lo
- People member of "Kanboard Managers" will have the role "Managers"
- Everybody else will have the role "User"
### Example for OpenLDAP with Posix Groups:
```php
define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local');
define('LDAP_GROUP_USER_FILTER', '(&(objectClass=posixGroup)(memberUid=%s))');
define('LDAP_GROUP_ADMIN_DN', 'cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local');
define('LDAP_GROUP_MANAGER_DN', 'cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local');
```
You **must define the parameter** `LDAP_GROUP_USER_FILTER` if your LDAP server use `memberUid` instead of `memberOf`.
All parameters of this example are mandatory.
Automatically load LDAP groups for project permissions
------------------------------------------------------
@ -41,7 +53,7 @@ If the group doesn't exist in the local database, it will be automatically synce
- `LDAP_GROUP_FILTER`: LDAP filter used to perform the query
- `LDAP_GROUP_ATTRIBUTE_NAME`: LDAP attribute used to fetch the group name
Example:
### Example for Active Directory:
```php
define('LDAP_GROUP_PROVIDER', true);
@ -52,7 +64,15 @@ define('LDAP_GROUP_FILTER', '(&(objectClass=group)(sAMAccountName=%s*))');
With the filter given as example above, Kanboard will search for groups that match the query.
If the end-user enter the text "My group" in the auto-complete box, Kanboard will return all groups that match the pattern: `(&(objectClass=group)(sAMAccountName=My group*))`.
- Note 1: The special characters `*` is important here, otherwise an exact match will be done.
- Note 1: The special characters `*` is important here, **otherwise an exact match will be done**.
- Note 2: This feature is only compatible with LDAP authentication configured in "proxy" or "anonymous" mode
[More examples of LDAP filters for Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx)
### Example for OpenLDAP with Posix Groups:
```php
define('LDAP_GROUP_PROVIDER', true);
define('LDAP_GROUP_BASE_DN', 'ou=Groups,dc=kanboard,dc=local');
define('LDAP_GROUP_FILTER', '(&(objectClass=posixGroup)(cn=%s*))');
```

View File

@ -26,6 +26,7 @@ Here is the list of available LDAP parameters:
| `LDAP_GROUP_PROVIDER` | false | Enable LDAP group provider for project permissions |
| `LDAP_GROUP_BASE_DN` | Empty | LDAP Base DN for groups |
| `LDAP_GROUP_FILTER` | Empty | LDAP group filter (Example: "(&(objectClass=group)(sAMAccountName=%s*))") |
| `LDAP_GROUP_USER_FILTER` | Empty | If defined, Kanboard will search user groups in LDAP_GROUP_BASE_DN with this filter, it's useful only for posixGroups (Example: `(&(objectClass=posixGroup)(memberUid=%s))`) |
| `LDAP_GROUP_ATTRIBUTE_NAME` | cn | LDAP attribute for the group name |
Notes:

View File

@ -2,15 +2,18 @@
require_once __DIR__.'/../../Base.php';
use Kanboard\Core\Ldap\Query;
use Kanboard\Core\Ldap\User;
use Kanboard\Core\Ldap\Entries;
use Kanboard\Core\Security\Role;
use Kanboard\Group\LdapGroupProvider;
class LdapUserTest extends Base
{
private $query;
private $client;
private $user;
private $group;
public function setUp()
{
@ -33,14 +36,23 @@ class LdapUserTest extends Base
))
->getMock();
$this->group = $this
->getMockBuilder('\Kanboard\Core\Ldap\Group')
->setConstructorArgs(array(new Query($this->client)))
->setMethods(array(
'find',
))
->getMock();
$this->user = $this
->getMockBuilder('\Kanboard\Core\Ldap\User')
->setConstructorArgs(array($this->query))
->setConstructorArgs(array($this->query, $this->group))
->setMethods(array(
'getAttributeUsername',
'getAttributeEmail',
'getAttributeName',
'getAttributeGroup',
'getGroupUserFilter',
'getGroupAdminDn',
'getGroupManagerDn',
'getBasDn',
@ -367,6 +379,328 @@ class LdapUserTest extends Base
$this->assertEquals(null, $user);
}
public function testGetUserWithAdminRoleAndPosixGroups()
{
$entries = new Entries(array(
'count' => 1,
0 => array(
'count' => 2,
'dn' => 'uid=my_ldap_user,ou=Users,dc=kanboard,dc=local',
'cn' => array(
'count' => 1,
0 => 'My LDAP user',
),
'mail' => array(
'count' => 2,
0 => 'user1@localhost',
1 => 'user2@localhost',
),
'uid' => array(
'count' => 1,
0 => 'my_ldap_user',
),
0 => 'cn',
1 => 'mail',
2 => 'uid',
)
));
$groups = array(
new LdapGroupProvider('CN=Kanboard Admins,OU=Groups,DC=kanboard,DC=local', 'Kanboard Admins')
);
$this->client
->expects($this->any())
->method('getConnection')
->will($this->returnValue('my_ldap_resource'));
$this->query
->expects($this->once())
->method('execute')
->with(
$this->equalTo('OU=Users,DC=kanboard,DC=local'),
$this->equalTo('(uid=my_ldap_user)')
);
$this->query
->expects($this->once())
->method('hasResult')
->will($this->returnValue(true));
$this->query
->expects($this->once())
->method('getEntries')
->will($this->returnValue($entries));
$this->user
->expects($this->any())
->method('getAttributeUsername')
->will($this->returnValue('uid'));
$this->user
->expects($this->any())
->method('getAttributeName')
->will($this->returnValue('cn'));
$this->user
->expects($this->any())
->method('getAttributeEmail')
->will($this->returnValue('mail'));
$this->user
->expects($this->any())
->method('getAttributeGroup')
->will($this->returnValue(''));
$this->user
->expects($this->any())
->method('getGroupUserFilter')
->will($this->returnValue('(&(objectClass=posixGroup)(memberUid=%s))'));
$this->user
->expects($this->any())
->method('getGroupAdminDn')
->will($this->returnValue('cn=Kanboard Admins,ou=Groups,dc=kanboard,dc=local'));
$this->user
->expects($this->any())
->method('getBasDn')
->will($this->returnValue('OU=Users,DC=kanboard,DC=local'));
$this->group
->expects($this->once())
->method('find')
->with('(&(objectClass=posixGroup)(memberUid=my_ldap_user))')
->will($this->returnValue($groups));
$user = $this->user->find('(uid=my_ldap_user)');
$this->assertInstanceOf('Kanboard\User\LdapUserProvider', $user);
$this->assertEquals('uid=my_ldap_user,ou=Users,dc=kanboard,dc=local', $user->getDn());
$this->assertEquals('my_ldap_user', $user->getUsername());
$this->assertEquals('My LDAP user', $user->getName());
$this->assertEquals('user1@localhost', $user->getEmail());
$this->assertEquals(array('CN=Kanboard Admins,OU=Groups,DC=kanboard,DC=local'), $user->getExternalGroupIds());
$this->assertEquals(Role::APP_ADMIN, $user->getRole());
$this->assertEquals(array('is_ldap_user' => 1), $user->getExtraAttributes());
}
public function testGetUserWithManagerRoleAndPosixGroups()
{
$entries = new Entries(array(
'count' => 1,
0 => array(
'count' => 2,
'dn' => 'uid=my_ldap_user,ou=Users,dc=kanboard,dc=local',
'cn' => array(
'count' => 1,
0 => 'My LDAP user',
),
'mail' => array(
'count' => 2,
0 => 'user1@localhost',
1 => 'user2@localhost',
),
'uid' => array(
'count' => 1,
0 => 'my_ldap_user',
),
0 => 'cn',
1 => 'mail',
2 => 'uid',
)
));
$groups = array(
new LdapGroupProvider('CN=Kanboard Users,OU=Groups,DC=kanboard,DC=local', 'Kanboard Users'),
new LdapGroupProvider('CN=Kanboard Managers,OU=Groups,DC=kanboard,DC=local', 'Kanboard Managers'),
);
$this->client
->expects($this->any())
->method('getConnection')
->will($this->returnValue('my_ldap_resource'));
$this->query
->expects($this->once())
->method('execute')
->with(
$this->equalTo('OU=Users,DC=kanboard,DC=local'),
$this->equalTo('(uid=my_ldap_user)')
);
$this->query
->expects($this->once())
->method('hasResult')
->will($this->returnValue(true));
$this->query
->expects($this->once())
->method('getEntries')
->will($this->returnValue($entries));
$this->user
->expects($this->any())
->method('getAttributeUsername')
->will($this->returnValue('uid'));
$this->user
->expects($this->any())
->method('getAttributeName')
->will($this->returnValue('cn'));
$this->user
->expects($this->any())
->method('getAttributeEmail')
->will($this->returnValue('mail'));
$this->user
->expects($this->any())
->method('getAttributeGroup')
->will($this->returnValue(''));
$this->user
->expects($this->any())
->method('getGroupUserFilter')
->will($this->returnValue('(&(objectClass=posixGroup)(memberUid=%s))'));
$this->user
->expects($this->any())
->method('getGroupManagerDn')
->will($this->returnValue('cn=Kanboard Managers,ou=Groups,dc=kanboard,dc=local'));
$this->user
->expects($this->any())
->method('getBasDn')
->will($this->returnValue('OU=Users,DC=kanboard,DC=local'));
$this->group
->expects($this->once())
->method('find')
->with('(&(objectClass=posixGroup)(memberUid=my_ldap_user))')
->will($this->returnValue($groups));
$user = $this->user->find('(uid=my_ldap_user)');
$this->assertInstanceOf('Kanboard\User\LdapUserProvider', $user);
$this->assertEquals('uid=my_ldap_user,ou=Users,dc=kanboard,dc=local', $user->getDn());
$this->assertEquals('my_ldap_user', $user->getUsername());
$this->assertEquals('My LDAP user', $user->getName());
$this->assertEquals('user1@localhost', $user->getEmail());
$this->assertEquals(
array(
'CN=Kanboard Users,OU=Groups,DC=kanboard,DC=local',
'CN=Kanboard Managers,OU=Groups,DC=kanboard,DC=local',
),
$user->getExternalGroupIds()
);
$this->assertEquals(Role::APP_MANAGER, $user->getRole());
$this->assertEquals(array('is_ldap_user' => 1), $user->getExtraAttributes());
}
public function testGetUserWithUserRoleAndPosixGroups()
{
$entries = new Entries(array(
'count' => 1,
0 => array(
'count' => 2,
'dn' => 'uid=my_ldap_user,ou=Users,dc=kanboard,dc=local',
'cn' => array(
'count' => 1,
0 => 'My LDAP user',
),
'mail' => array(
'count' => 2,
0 => 'user1@localhost',
1 => 'user2@localhost',
),
'uid' => array(
'count' => 1,
0 => 'my_ldap_user',
),
0 => 'cn',
1 => 'mail',
2 => 'uid',
)
));
$groups = array(
new LdapGroupProvider('CN=Kanboard Users,OU=Groups,DC=kanboard,DC=local', 'Kanboard Users'),
);
$this->client
->expects($this->any())
->method('getConnection')
->will($this->returnValue('my_ldap_resource'));
$this->query
->expects($this->once())
->method('execute')
->with(
$this->equalTo('OU=Users,DC=kanboard,DC=local'),
$this->equalTo('(uid=my_ldap_user)')
);
$this->query
->expects($this->once())
->method('hasResult')
->will($this->returnValue(true));
$this->query
->expects($this->once())
->method('getEntries')
->will($this->returnValue($entries));
$this->user
->expects($this->any())
->method('getAttributeUsername')
->will($this->returnValue('uid'));
$this->user
->expects($this->any())
->method('getAttributeName')
->will($this->returnValue('cn'));
$this->user
->expects($this->any())
->method('getAttributeEmail')
->will($this->returnValue('mail'));
$this->user
->expects($this->any())
->method('getAttributeGroup')
->will($this->returnValue(''));
$this->user
->expects($this->any())
->method('getGroupUserFilter')
->will($this->returnValue('(&(objectClass=posixGroup)(memberUid=%s))'));
$this->user
->expects($this->any())
->method('getBasDn')
->will($this->returnValue('OU=Users,DC=kanboard,DC=local'));
$this->group
->expects($this->once())
->method('find')
->with('(&(objectClass=posixGroup)(memberUid=my_ldap_user))')
->will($this->returnValue($groups));
$user = $this->user->find('(uid=my_ldap_user)');
$this->assertInstanceOf('Kanboard\User\LdapUserProvider', $user);
$this->assertEquals('uid=my_ldap_user,ou=Users,dc=kanboard,dc=local', $user->getDn());
$this->assertEquals('my_ldap_user', $user->getUsername());
$this->assertEquals('My LDAP user', $user->getName());
$this->assertEquals('user1@localhost', $user->getEmail());
$this->assertEquals(
array(
'CN=Kanboard Users,OU=Groups,DC=kanboard,DC=local',
),
$user->getExternalGroupIds()
);
$this->assertEquals(Role::APP_USER, $user->getRole());
$this->assertEquals(array('is_ldap_user' => 1), $user->getExtraAttributes());
}
public function testGetBaseDnNotConfigured()
{
$this->setExpectedException('\LogicException');
@ -400,4 +734,40 @@ class LdapUserTest extends Base
$user = new User($this->query);
$this->assertEquals($expected, $user->getLdapUserPattern('test', $filter));
}
public function testGetGroupUserFilter()
{
$user = new User($this->query);
$this->assertSame('', $user->getGroupUserFilter());
}
public function testHasGroupUserFilterWithEmptyString()
{
$this->user
->expects($this->any())
->method('getGroupUserFilter')
->will($this->returnValue(''));
$this->assertFalse($this->user->hasGroupUserFilter());
}
public function testHasGroupUserFilterWithNull()
{
$this->user
->expects($this->any())
->method('getGroupUserFilter')
->will($this->returnValue(null));
$this->assertFalse($this->user->hasGroupUserFilter());
}
public function testHasGroupUserFilterWithValue()
{
$this->user
->expects($this->any())
->method('getGroupUserFilter')
->will($this->returnValue('foobar'));
$this->assertTrue($this->user->hasGroupUserFilter());
}
}