summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--app/Core/Ldap/Client.php84
-rw-r--r--app/Core/Ldap/ClientException.php15
-rw-r--r--app/Core/Ldap/Query.php95
-rw-r--r--app/Core/Ldap/User.php178
-rw-r--r--config.default.php13
-rw-r--r--tests/units/Core/Ldap/ClientTest.php195
-rw-r--r--tests/units/Core/Ldap/QueryTest.php137
-rw-r--r--tests/units/Core/Ldap/UserTest.php95
9 files changed, 800 insertions, 13 deletions
diff --git a/ChangeLog b/ChangeLog
index ed94807d..5a858885 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -4,6 +4,7 @@ Version 1.0.22 (unreleased)
New features:
* User groups (Teams)
+* Add generic LDAP client library
* Pluggable authentication and authorization system (Work in progress)
* Add new project role Viewer (Work in progress)
* Assign project permissions to a group (Work in progress)
diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php
new file mode 100644
index 00000000..a523428c
--- /dev/null
+++ b/app/Core/Ldap/Client.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Client
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Client
+{
+ /**
+ * Get server connection
+ *
+ * @access public
+ * @param string $server LDAP server hostname or IP
+ * @param integer $port LDAP port
+ * @param boolean $tls Start TLS
+ * @param boolean $verify Skip SSL certificate verification
+ * @return resource
+ */
+ public function getConnection($server, $port = LDAP_PORT, $tls = LDAP_START_TLS, $verify = LDAP_SSL_VERIFY)
+ {
+ if (! function_exists('ldap_connect')) {
+ throw new ClientException('LDAP: The PHP LDAP extension is required');
+ }
+
+ if (! $verify) {
+ putenv('LDAPTLS_REQCERT=never');
+ }
+
+ $ldap = ldap_connect($server, $port);
+
+ if ($ldap === false) {
+ throw new ClientException('LDAP: Unable to connect to the LDAP server');
+ }
+
+ ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
+ ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
+ ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
+ ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1);
+
+ if ($tls && ! @ldap_start_tls($ldap)) {
+ throw new ClientException('LDAP: Unable to start TLS');
+ }
+
+ return $ldap;
+ }
+
+ /**
+ * Anonymous authentication
+ *
+ * @access public
+ * @param resource $ldap
+ * @return boolean
+ */
+ public function useAnonymousAuthentication($ldap)
+ {
+ if (! ldap_bind($ldap)) {
+ throw new ClientException('Unable to perform anonymous binding');
+ }
+
+ return true;
+ }
+
+ /**
+ * Authentication with username/password
+ *
+ * @access public
+ * @param resource $ldap
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ */
+ public function authenticate($ldap, $username, $password)
+ {
+ if (! ldap_bind($ldap, $username, $password)) {
+ throw new ClientException('Unable to perform anonymous binding');
+ }
+
+ return true;
+ }
+}
diff --git a/app/Core/Ldap/ClientException.php b/app/Core/Ldap/ClientException.php
new file mode 100644
index 00000000..a0f9f842
--- /dev/null
+++ b/app/Core/Ldap/ClientException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+use Exception;
+
+/**
+ * LDAP Client Exception
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class ClientException extends Exception
+{
+}
diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php
new file mode 100644
index 00000000..1c34fa10
--- /dev/null
+++ b/app/Core/Ldap/Query.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Query
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Query
+{
+ /**
+ * Query result
+ *
+ * @access private
+ * @var array
+ */
+ private $entries = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $entries
+ */
+ public function __construct(array $entries = array())
+ {
+ $this->entries = $entries;
+ }
+
+ /**
+ * Execute query
+ *
+ * @access public
+ * @param resource $ldap
+ * @param string $baseDn
+ * @param string $filter
+ * @param array $attributes
+ * @return Query
+ */
+ public function execute($ldap, $baseDn, $filter, array $attributes)
+ {
+ $sr = ldap_search($ldap, $baseDn, $filter, $attributes);
+ if ($sr === false) {
+ return $this;
+ }
+
+ $entries = ldap_get_entries($ldap, $sr);
+ if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
+ return $this;
+ }
+
+ $this->entries = $entries;
+
+ return $this;
+ }
+
+ /**
+ * Return true if the query returned a result
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasResult()
+ {
+ return ! empty($this->entries);
+ }
+
+ /**
+ * Return subset of entries
+ *
+ * @access public
+ * @param string $key
+ * @param mixed $default
+ * @return array
+ */
+ public function getAttribute($key, $default = null)
+ {
+ return isset($this->entries[0][$key]) ? $this->entries[0][$key] : $default;
+ }
+
+ /**
+ * Return one entry from a list of entries
+ *
+ * @access public
+ * @param string $key Key
+ * @param string $default Default value if key not set in entry
+ * @return string
+ */
+ public function getAttributeValue($key, $default = '')
+ {
+ return isset($this->entries[0][$key][0]) ? $this->entries[0][$key][0] : $default;
+ }
+}
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
new file mode 100644
index 00000000..e44a4dda
--- /dev/null
+++ b/app/Core/Ldap/User.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP User
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class User
+{
+ /**
+ * Query
+ *
+ * @access private
+ * @var Query
+ */
+ private $query;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param Query $query
+ */
+ public function __construct(Query $query = null)
+ {
+ $this->query = $query ?: new Query;
+ }
+
+ /**
+ * Get user profile
+ *
+ * @access public
+ * @param resource $ldap
+ * @param string $baseDn
+ * @param string $query
+ * @return array
+ */
+ public function getProfile($ldap, $baseDn, $query)
+ {
+ $this->query->execute($ldap, $baseDn, $query, $this->getAttributes());
+ $profile = array();
+
+ if ($this->query->hasResult()) {
+ $profile = $this->prepareProfile();
+ }
+
+ return $profile;
+ }
+
+ /**
+ * Build user profile
+ *
+ * @access private
+ * @return boolean|array
+ */
+ private function prepareProfile()
+ {
+ return array(
+ 'ldap_id' => $this->query->getAttribute('dn', ''),
+ 'username' => $this->query->getAttributeValue($this->getAttributeUsername()),
+ 'name' => $this->query->getAttributeValue($this->getAttributeName()),
+ 'email' => $this->query->getAttributeValue($this->getAttributeEmail()),
+ 'is_admin' => (int) $this->isMemberOf($this->query->getAttribute($this->getAttributeGroup(), array()), $this->getGroupAdminDn()),
+ 'is_project_admin' => (int) $this->isMemberOf($this->query->getAttribute($this->getAttributeGroup(), array()), $this->getGroupProjectAdminDn()),
+ 'is_ldap_user' => 1,
+ );
+ }
+
+ /**
+ * Check group membership
+ *
+ * @access public
+ * @param array $group_entries
+ * @param string $group_dn
+ * @return boolean
+ */
+ public function isMemberOf(array $group_entries, $group_dn)
+ {
+ if (! isset($group_entries['count']) || empty($group_dn)) {
+ return false;
+ }
+
+ for ($i = 0; $i < $group_entries['count']; $i++) {
+ if ($group_entries[$i] === $group_dn) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Ge the list of attributes to fetch when reading the LDAP user entry
+ *
+ * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
+ *
+ * @access public
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return array_values(array_filter(array(
+ $this->getAttributeUsername(),
+ $this->getAttributeName(),
+ $this->getAttributeEmail(),
+ $this->getAttributeGroup(),
+ )));
+ }
+
+ /**
+ * Get LDAP account id attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeUsername()
+ {
+ return LDAP_ACCOUNT_ID;
+ }
+
+ /**
+ * Get LDAP account email attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeEmail()
+ {
+ return LDAP_ACCOUNT_EMAIL;
+ }
+
+ /**
+ * Get LDAP account name attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeName()
+ {
+ return LDAP_ACCOUNT_FULLNAME;
+ }
+
+ /**
+ * Get LDAP account memberof attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeGroup()
+ {
+ return LDAP_ACCOUNT_MEMBEROF;
+ }
+
+ /**
+ * Get LDAP admin group DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getGroupAdminDn()
+ {
+ return LDAP_GROUP_ADMIN_DN;
+ }
+
+ /**
+ * Get LDAP project admin group DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getGroupProjectAdminDn()
+ {
+ return LDAP_GROUP_PROJECT_ADMIN_DN;
+ }
+}
diff --git a/config.default.php b/config.default.php
index 91bb8d17..067d9d60 100644
--- a/config.default.php
+++ b/config.default.php
@@ -32,19 +32,6 @@ define('MAIL_SMTP_ENCRYPTION', null); // Valid values are "null", "ssl" or "tls"
// Sendmail command to use when the transport is "sendmail"
define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs');
-// Postmark API token (used to send emails through their API)
-define('POSTMARK_API_TOKEN', '');
-
-// Mailgun API key (used to send emails through their API)
-define('MAILGUN_API_TOKEN', '');
-
-// Mailgun domain name
-define('MAILGUN_DOMAIN', '');
-
-// Sendgrid API configuration
-define('SENDGRID_API_USER', '');
-define('SENDGRID_API_KEY', '');
-
// Database driver: sqlite, mysql or postgres (sqlite by default)
define('DB_DRIVER', 'sqlite');
diff --git a/tests/units/Core/Ldap/ClientTest.php b/tests/units/Core/Ldap/ClientTest.php
new file mode 100644
index 00000000..7b6e983d
--- /dev/null
+++ b/tests/units/Core/Ldap/ClientTest.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+require_once __DIR__.'/../../Base.php';
+
+function ldap_connect($hostname, $port)
+{
+ return ClientTest::$functions->ldap_connect($hostname, $port);
+}
+
+function ldap_set_option()
+{
+}
+
+function ldap_bind($link_identifier, $bind_rdn = null, $bind_password = null)
+{
+ return ClientTest::$functions->ldap_bind($link_identifier, $bind_rdn, $bind_password);
+}
+
+function ldap_start_tls($link_identifier)
+{
+ return ClientTest::$functions->ldap_start_tls($link_identifier);
+}
+
+class ClientTest extends \Base
+{
+ public static $functions;
+ private $ldap;
+
+ public function setUp()
+ {
+ parent::setup();
+
+ self::$functions = $this
+ ->getMockBuilder('stdClass')
+ ->setMethods(array(
+ 'ldap_connect',
+ 'ldap_set_option',
+ 'ldap_bind',
+ 'ldap_start_tls',
+ ))
+ ->getMock();
+ }
+
+ public function tearDown()
+ {
+ parent::tearDown();
+ self::$functions = null;
+ }
+
+ public function testConnectSuccess()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_connect')
+ ->with(
+ $this->equalTo('my_ldap_server'),
+ $this->equalTo(389)
+ )
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $ldap = new Client;
+ $this->assertEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server'));
+ }
+
+ public function testConnectFailure()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_connect')
+ ->with(
+ $this->equalTo('my_ldap_server'),
+ $this->equalTo(389)
+ )
+ ->will($this->returnValue(false));
+
+ $this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
+
+ $ldap = new Client;
+ $this->assertNotEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server'));
+ }
+
+ public function testConnectSuccessWithTLS()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_connect')
+ ->with(
+ $this->equalTo('my_ldap_server'),
+ $this->equalTo(389)
+ )
+ ->will($this->returnValue('my_ldap_resource'));
+
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_start_tls')
+ ->with(
+ $this->equalTo('my_ldap_resource')
+ )
+ ->will($this->returnValue(true));
+
+ $ldap = new Client;
+ $this->assertEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server', 389, true));
+ }
+
+ public function testConnectFailureWithTLS()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_connect')
+ ->with(
+ $this->equalTo('my_ldap_server'),
+ $this->equalTo(389)
+ )
+ ->will($this->returnValue('my_ldap_resource'));
+
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_start_tls')
+ ->with(
+ $this->equalTo('my_ldap_resource')
+ )
+ ->will($this->returnValue(false));
+
+ $this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
+
+ $ldap = new Client;
+ $this->assertNotEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server', 389, true));
+ }
+
+ public function testAnonymousAuthenticationSuccess()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_bind')
+ ->with(
+ $this->equalTo('my_ldap_resource')
+ )
+ ->will($this->returnValue(true));
+
+ $ldap = new Client;
+ $this->assertTrue($ldap->useAnonymousAuthentication('my_ldap_resource'));
+ }
+
+ public function testAnonymousAuthenticationFailure()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_bind')
+ ->with(
+ $this->equalTo('my_ldap_resource')
+ )
+ ->will($this->returnValue(false));
+
+ $this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
+
+ $ldap = new Client;
+ $ldap->useAnonymousAuthentication('my_ldap_resource');
+ }
+
+ public function testUserAuthenticationSuccess()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_bind')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('my_ldap_user'),
+ $this->equalTo('my_ldap_password')
+ )
+ ->will($this->returnValue(true));
+
+ $ldap = new Client;
+ $this->assertTrue($ldap->authenticate('my_ldap_resource', 'my_ldap_user', 'my_ldap_password'));
+ }
+
+ public function testUserAuthenticationFailure()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_bind')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('my_ldap_user'),
+ $this->equalTo('my_ldap_password')
+ )
+ ->will($this->returnValue(false));
+
+ $this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
+
+ $ldap = new Client;
+ $ldap->authenticate('my_ldap_resource', 'my_ldap_user', 'my_ldap_password');
+ }
+}
diff --git a/tests/units/Core/Ldap/QueryTest.php b/tests/units/Core/Ldap/QueryTest.php
new file mode 100644
index 00000000..2eb3940f
--- /dev/null
+++ b/tests/units/Core/Ldap/QueryTest.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+require_once __DIR__.'/../../Base.php';
+
+function ldap_search($link_identifier, $base_dn, $filter, array $attributes)
+{
+ return QueryTest::$functions->ldap_search($link_identifier, $base_dn, $filter, $attributes);
+}
+
+function ldap_get_entries($link_identifier, $result_identifier)
+{
+ return QueryTest::$functions->ldap_get_entries($link_identifier, $result_identifier);
+}
+
+class QueryTest extends \Base
+{
+ public static $functions;
+
+ public function setUp()
+ {
+ parent::setup();
+
+ self::$functions = $this
+ ->getMockBuilder('stdClass')
+ ->setMethods(array(
+ 'ldap_search',
+ 'ldap_get_entries',
+ ))
+ ->getMock();
+ }
+
+ public function tearDown()
+ {
+ parent::tearDown();
+ self::$functions = null;
+ }
+
+ public function testExecuteQuerySuccessfully()
+ {
+ $entries = array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 2,
+ 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
+ 'displayname' => array(
+ 'count' => 1,
+ 0 => 'My user',
+ ),
+ 'mail' => array(
+ 'count' => 2,
+ 0 => 'user1@localhost',
+ 1 => 'user2@localhost',
+ ),
+ 0 => 'displayname',
+ 1 => 'mail',
+ )
+ );
+
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_search')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('uid=my_user'),
+ $this->equalTo(array('displayname'))
+ )
+ ->will($this->returnValue('search_resource'));
+
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_get_entries')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('search_resource')
+ )
+ ->will($this->returnValue($entries));
+
+ $query = new Query;
+ $query->execute('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
+ $this->assertTrue($query->hasResult());
+
+ $this->assertEquals('My user', $query->getAttributeValue('displayname'));
+ $this->assertEquals('user1@localhost', $query->getAttributeValue('mail'));
+ $this->assertEquals('', $query->getAttributeValue('not_found'));
+
+ $this->assertEquals('uid=my_user,ou=People,dc=kanboard,dc=local', $query->getAttribute('dn'));
+ $this->assertEquals(null, $query->getAttribute('missing'));
+ }
+
+ public function testExecuteQueryNotFound()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_search')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('uid=my_user'),
+ $this->equalTo(array('displayname'))
+ )
+ ->will($this->returnValue('search_resource'));
+
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_get_entries')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('search_resource')
+ )
+ ->will($this->returnValue(array()));
+
+ $query = new Query;
+ $query->execute('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
+ $this->assertFalse($query->hasResult());
+ }
+
+ public function testExecuteQueryFailed()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('ldap_search')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('uid=my_user'),
+ $this->equalTo(array('displayname'))
+ )
+ ->will($this->returnValue(false));
+
+ $query = new Query;
+ $query->execute('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
+ $this->assertFalse($query->hasResult());
+ }
+}
diff --git a/tests/units/Core/Ldap/UserTest.php b/tests/units/Core/Ldap/UserTest.php
new file mode 100644
index 00000000..b19592c2
--- /dev/null
+++ b/tests/units/Core/Ldap/UserTest.php
@@ -0,0 +1,95 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Ldap\User;
+
+class UserTest extends Base
+{
+ public function testGetProfile()
+ {
+ $entries = array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 2,
+ 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
+ 'displayname' => array(
+ 'count' => 1,
+ 0 => 'My LDAP user',
+ ),
+ 'mail' => array(
+ 'count' => 2,
+ 0 => 'user1@localhost',
+ 1 => 'user2@localhost',
+ ),
+ 'samaccountname' => array(
+ 'count' => 1,
+ 0 => 'my_ldap_user',
+ ),
+ 0 => 'displayname',
+ 1 => 'mail',
+ 2 => 'samaccountname',
+ )
+ );
+
+ $expected = array(
+ 'ldap_id' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
+ 'username' => 'my_ldap_user',
+ 'name' => 'My LDAP user',
+ 'email' => 'user1@localhost',
+ 'is_admin' => 0,
+ 'is_project_admin' => 0,
+ 'is_ldap_user' => 1,
+ );
+
+ $query = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Query')
+ ->setConstructorArgs(array($entries))
+ ->setMethods(array(
+ 'execute',
+ 'hasResult',
+ ))
+ ->getMock();
+
+ $query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('my_ldap_resource'),
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('(uid=my_user)')
+ );
+
+ $query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(true));
+
+ $user = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\User')
+ ->setConstructorArgs(array($query))
+ ->setMethods(array(
+ 'getAttributeUsername',
+ 'getAttributeEmail',
+ 'getAttributeName',
+ ))
+ ->getMock();
+
+ $user
+ ->expects($this->any())
+ ->method('getAttributeUsername')
+ ->will($this->returnValue('samaccountname'));
+
+ $user
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('displayname'));
+
+ $user
+ ->expects($this->any())
+ ->method('getAttributeEmail')
+ ->will($this->returnValue('mail'));
+
+ $this->assertEquals($expected, $user->getProfile('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', '(uid=my_user)'));
+ }
+}