Rewrite of the authentication and authorization system

This commit is contained in:
Frederic Guillot
2015-12-05 20:31:27 -05:00
parent 346b8312e5
commit e9fedf3e5c
255 changed files with 14114 additions and 9820 deletions

View File

@@ -5,29 +5,43 @@ namespace Kanboard\Core;
use Pimple\Container;
/**
* Base class
* Base Class
*
* @package core
* @author Frederic Guillot
*
* @property \Kanboard\Core\Session\SessionManager $sessionManager
* @property \Kanboard\Core\Session\SessionStorage $sessionStorage
* @property \Kanboard\Core\Session\FlashMessage $flash
* @property \Kanboard\Core\Helper $helper
* @property \Kanboard\Core\Mail\Client $emailClient
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Cache\MemoryCache $memoryCache
* @property \Kanboard\Core\Group\GroupManager $groupManager
* @property \Kanboard\Core\Http\Client $httpClient
* @property \Kanboard\Core\Http\OAuth2 $oauth
* @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie
* @property \Kanboard\Core\Http\Request $request
* @property \Kanboard\Core\Http\Router $router
* @property \Kanboard\Core\Http\Response $response
* @property \Kanboard\Core\Template $template
* @property \Kanboard\Core\OAuth2 $oauth
* @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Http\Router $router
* @property \Kanboard\Core\Mail\Client $emailClient
* @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
* @property \Kanboard\Core\Cache\Cache $memoryCache
* @property \Kanboard\Core\Plugin\Hook $hook
* @property \Kanboard\Core\Plugin\Loader $pluginLoader
* @property \Kanboard\Core\Security\AccessMap $projectAccessMap
* @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager
* @property \Kanboard\Core\Security\AccessMap $applicationAccessMap
* @property \Kanboard\Core\Security\AccessMap $projectAccessMap
* @property \Kanboard\Core\Security\Authorization $applicationAuthorization
* @property \Kanboard\Core\Security\Authorization $projectAuthorization
* @property \Kanboard\Core\Security\Role $role
* @property \Kanboard\Core\Security\Token $token
* @property \Kanboard\Core\Session\FlashMessage $flash
* @property \Kanboard\Core\Session\SessionManager $sessionManager
* @property \Kanboard\Core\Session\SessionStorage $sessionStorage
* @property \Kanboard\Core\User\GroupSync $groupSync
* @property \Kanboard\Core\User\UserProfile $userProfile
* @property \Kanboard\Core\User\UserSync $userSync
* @property \Kanboard\Core\User\UserSession $userSession
* @property \Kanboard\Core\DateParser $dateParser
* @property \Kanboard\Core\Helper $helper
* @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
* @property \Kanboard\Integration\BitbucketWebhook $bitbucketWebhook
* @property \Kanboard\Integration\GithubWebhook $githubWebhook
* @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook
@@ -36,7 +50,8 @@ use Pimple\Container;
* @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
* @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter
* @property \Kanboard\Model\Acl $acl
* @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\Authentication $authentication
* @property \Kanboard\Model\Board $board
@@ -46,8 +61,9 @@ use Pimple\Container;
* @property \Kanboard\Model\Config $config
* @property \Kanboard\Model\Currency $currency
* @property \Kanboard\Model\CustomFilter $customFilter
* @property \Kanboard\Model\DateParser $dateParser
* @property \Kanboard\Model\File $file
* @property \Kanboard\Model\Group $group
* @property \Kanboard\Model\GroupMember $groupMember
* @property \Kanboard\Model\LastLogin $lastLogin
* @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification
@@ -60,8 +76,11 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
* @property \Kanboard\Model\RememberMeSession $rememberMeSession
* @property \Kanboard\Model\Subtask $subtask
* @property \Kanboard\Model\SubtaskExport $subtaskExport
* @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking
@@ -84,16 +103,17 @@ use Pimple\Container;
* @property \Kanboard\Model\Transition $transition
* @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserImport $userImport
* @property \Kanboard\Model\UserLocking $userLocking
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationType $userNotificationType
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
* @property \Kanboard\Model\UserSession $userSession
* @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook
* @property \Psr\Log\LoggerInterface $logger
* @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
abstract class Base
{

View File

@@ -23,7 +23,7 @@ class MemoryCache extends Base implements CacheInterface
*
* @access public
* @param string $key
* @param string $value
* @param mixed $value
*/
public function set($key, $value)
{

View File

@@ -0,0 +1,21 @@
<?php
namespace Kanboard\Core\Group;
/**
* Group Backend Provider Interface
*
* @package group
* @author Frederic Guillot
*/
interface GroupBackendProviderInterface
{
/**
* Find a group from a search query
*
* @access public
* @param string $input
* @return GroupProviderInterface[]
*/
public function find($input);
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Kanboard\Core\Group;
/**
* Group Manager
*
* @package group
* @author Frederic Guillot
*/
class GroupManager
{
/**
* List of backend providers
*
* @access private
* @var array
*/
private $providers = array();
/**
* Register a new group backend provider
*
* @access public
* @param GroupBackendProviderInterface $provider
* @return GroupManager
*/
public function register(GroupBackendProviderInterface $provider)
{
$this->providers[] = $provider;
return $this;
}
/**
* Find a group from a search query
*
* @access public
* @param string $input
* @return GroupProviderInterface[]
*/
public function find($input)
{
$groups = array();
foreach ($this->providers as $provider) {
$groups = array_merge($groups, $provider->find($input));
}
return $this->removeDuplicates($groups);
}
/**
* Remove duplicated groups
*
* @access private
* @param array $groups
* @return GroupProviderInterface[]
*/
private function removeDuplicates(array $groups)
{
$result = array();
foreach ($groups as $group) {
if (! isset($result[$group->getName()])) {
$result[$group->getName()] = $group;
}
}
return $result;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Kanboard\Core\Group;
/**
* Group Provider Interface
*
* @package group
* @author Frederic Guillot
*/
interface GroupProviderInterface
{
/**
* Get internal id
*
* You must return 0 if the group come from an external backend
*
* @access public
* @return integer
*/
public function getInternalId();
/**
* Get external id
*
* You must return a unique id if the group come from an external provider
*
* @access public
* @return string
*/
public function getExternalId();
/**
* Get group name
*
* @access public
* @return string
*/
public function getName();
}

View File

@@ -1,11 +1,13 @@
<?php
namespace Kanboard\Core;
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
/**
* OAuth2 client
* OAuth2 Client
*
* @package core
* @package http
* @author Frederic Guillot
*/
class OAuth2 extends Base

View File

@@ -0,0 +1,120 @@
<?php
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
/**
* Remember Me Cookie
*
* @package http
* @author Frederic Guillot
*/
class RememberMeCookie extends Base
{
/**
* Cookie name
*
* @var string
*/
const COOKIE_NAME = 'KB_RM';
/**
* Encode the cookie
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @return string
*/
public function encode($token, $sequence)
{
return implode('|', array($token, $sequence));
}
/**
* Decode the value of a cookie
*
* @access public
* @param string $value Raw cookie data
* @return array
*/
public function decode($value)
{
list($token, $sequence) = explode('|', $value);
return array(
'token' => $token,
'sequence' => $sequence,
);
}
/**
* Return true if the current user has a RememberMe cookie
*
* @access public
* @return bool
*/
public function hasCookie()
{
return $this->request->getCookie(self::COOKIE_NAME) !== '';
}
/**
* Write and encode the cookie
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @param string $expiration Cookie expiration
* @return boolean
*/
public function write($token, $sequence, $expiration)
{
return setcookie(
self::COOKIE_NAME,
$this->encode($token, $sequence),
$expiration,
$this->helper->url->dir(),
null,
$this->request->isHTTPS(),
true
);
}
/**
* Read and decode the cookie
*
* @access public
* @return mixed
*/
public function read()
{
$cookie = $this->request->getCookie(self::COOKIE_NAME);
if (empty($cookie)) {
return false;
}
return $this->decode($cookie);
}
/**
* Remove the cookie
*
* @access public
* @return boolean
*/
public function remove()
{
return setcookie(
self::COOKIE_NAME,
'',
time() - 3600,
$this->helper->url->dir(),
null,
$this->request->isHTTPS(),
true
);
}
}

View File

@@ -2,6 +2,7 @@
namespace Kanboard\Core\Http;
use Pimple\Container;
use Kanboard\Core\Base;
/**
@@ -13,7 +14,35 @@ use Kanboard\Core\Base;
class Request extends Base
{
/**
* Get URL string parameter
* Pointer to PHP environment variables
*
* @access private
* @var array
*/
private $server;
private $get;
private $post;
private $files;
private $cookies;
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array())
{
parent::__construct($container);
$this->server = empty($server) ? $_SERVER : $server;
$this->get = empty($get) ? $_GET : $get;
$this->post = empty($post) ? $_POST : $post;
$this->files = empty($files) ? $_FILES : $files;
$this->cookies = empty($cookies) ? $_COOKIE : $cookies;
}
/**
* Get query string string parameter
*
* @access public
* @param string $name Parameter name
@@ -22,11 +51,11 @@ class Request extends Base
*/
public function getStringParam($name, $default_value = '')
{
return isset($_GET[$name]) ? $_GET[$name] : $default_value;
return isset($this->get[$name]) ? $this->get[$name] : $default_value;
}
/**
* Get URL integer parameter
* Get query string integer parameter
*
* @access public
* @param string $name Parameter name
@@ -35,7 +64,7 @@ class Request extends Base
*/
public function getIntegerParam($name, $default_value = 0)
{
return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value;
return isset($this->get[$name]) && ctype_digit($this->get[$name]) ? (int) $this->get[$name] : $default_value;
}
/**
@@ -59,9 +88,9 @@ class Request extends Base
*/
public function getValues()
{
if (! empty($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) {
unset($_POST['csrf_token']);
return $_POST;
if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) {
unset($this->post['csrf_token']);
return $this->post;
}
return array();
@@ -98,8 +127,8 @@ class Request extends Base
*/
public function getFileContent($name)
{
if (isset($_FILES[$name])) {
return file_get_contents($_FILES[$name]['tmp_name']);
if (isset($this->files[$name]['tmp_name'])) {
return file_get_contents($this->files[$name]['tmp_name']);
}
return '';
@@ -114,7 +143,7 @@ class Request extends Base
*/
public function getFilePath($name)
{
return isset($_FILES[$name]['tmp_name']) ? $_FILES[$name]['tmp_name'] : '';
return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : '';
}
/**
@@ -125,7 +154,7 @@ class Request extends Base
*/
public function isPost()
{
return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST';
return isset($this->server['REQUEST_METHOD']) && $this->server['REQUEST_METHOD'] === 'POST';
}
/**
@@ -144,13 +173,24 @@ class Request extends Base
*
* Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
*
* @static
* @access public
* @return boolean
*/
public static function isHTTPS()
public function isHTTPS()
{
return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off';
return isset($this->server['HTTPS']) && $this->server['HTTPS'] !== '' && $this->server['HTTPS'] !== 'off';
}
/**
* Get cookie value
*
* @access public
* @param string $name
* @return string
*/
public function getCookie($name)
{
return isset($this->cookies[$name]) ? $this->cookies[$name] : '';
}
/**
@@ -163,7 +203,18 @@ class Request extends Base
public function getHeader($name)
{
$name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
return isset($_SERVER[$name]) ? $_SERVER[$name] : '';
return isset($this->server[$name]) ? $this->server[$name] : '';
}
/**
* Get remote user
*
* @access public
* @return string
*/
public function getRemoteUser()
{
return isset($this->server[REVERSE_PROXY_USER_HEADER]) ? $this->server[REVERSE_PROXY_USER_HEADER] : '';
}
/**
@@ -174,41 +225,38 @@ class Request extends Base
*/
public function getQueryString()
{
return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
return isset($this->server['QUERY_STRING']) ? $this->server['QUERY_STRING'] : '';
}
/**
* Returns uri
* Return URI
*
* @access public
* @return string
*/
public function getUri()
{
return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
return isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
}
/**
* Get the user agent
*
* @static
* @access public
* @return string
*/
public static function getUserAgent()
public function getUserAgent()
{
return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT'];
return empty($this->server['HTTP_USER_AGENT']) ? t('Unknown') : $this->server['HTTP_USER_AGENT'];
}
/**
* Get the real IP address of the user
* Get the IP address of the user
*
* @static
* @access public
* @param bool $only_public Return only public IP address
* @return string
*/
public static function getIpAddress($only_public = false)
public function getIpAddress()
{
$keys = array(
'HTTP_CLIENT_IP',
@@ -221,23 +269,24 @@ class Request extends Base
);
foreach ($keys as $key) {
if (isset($_SERVER[$key])) {
foreach (explode(',', $_SERVER[$key]) as $ip_address) {
$ip_address = trim($ip_address);
if ($only_public) {
// Return only public IP address
if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
return $ip_address;
}
} else {
return $ip_address;
}
if (! empty($this->server[$key])) {
foreach (explode(',', $this->server[$key]) as $ipAddress) {
return trim($ipAddress);
}
}
}
return t('Unknown');
}
/**
* Get start time
*
* @access public
* @return float
*/
public function getStartTime()
{
return isset($this->server['REQUEST_TIME_FLOAT']) ? $this->server['REQUEST_TIME_FLOAT'] : 0;
}
}

View File

@@ -257,7 +257,7 @@ class Response extends Base
*/
public function hsts()
{
if (Request::isHTTPS()) {
if ($this->request->isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000');
}
}

View File

@@ -2,6 +2,8 @@
namespace Kanboard\Core\Ldap;
use LogicException;
/**
* LDAP Client
*
@@ -10,17 +12,61 @@ namespace Kanboard\Core\Ldap;
*/
class Client
{
/**
* LDAP resource
*
* @access private
* @var resource
*/
private $ldap;
/**
* Establish LDAP connection
*
* @static
* @access public
* @param string $username
* @param string $password
* @return Client
*/
public static function connect($username = null, $password = null)
{
$client = new self;
$client->open($client->getLdapServer());
$username = $username ?: $client->getLdapUsername();
$password = $password ?: $client->getLdapPassword();
if (empty($username) && empty($password)) {
$client->useAnonymousAuthentication();
} else {
$client->authenticate($username, $password);
}
return $client;
}
/**
* Get server connection
*
* @access public
* @return resource
*/
public function getConnection()
{
return $this->ldap;
}
/**
* Establish 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
* @return Client
*/
public function getConnection($server, $port = LDAP_PORT, $tls = LDAP_START_TLS, $verify = LDAP_SSL_VERIFY)
public function open($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');
@@ -30,34 +76,33 @@ class Client
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect($server, $port);
$this->ldap = ldap_connect($server, $port);
if ($ldap === false) {
if ($this->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);
ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, 0);
ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, 1);
if ($tls && ! @ldap_start_tls($ldap)) {
if ($tls && ! @ldap_start_tls($this->ldap)) {
throw new ClientException('LDAP: Unable to start TLS');
}
return $ldap;
return $this;
}
/**
* Anonymous authentication
*
* @access public
* @param resource $ldap
* @return boolean
*/
public function useAnonymousAuthentication($ldap)
public function useAnonymousAuthentication()
{
if (! ldap_bind($ldap)) {
if (! @ldap_bind($this->ldap)) {
throw new ClientException('Unable to perform anonymous binding');
}
@@ -68,17 +113,53 @@ class Client
* Authentication with username/password
*
* @access public
* @param resource $ldap
* @param string $username
* @param string $password
* @param string $bind_rdn
* @param string $bind_password
* @return boolean
*/
public function authenticate($ldap, $username, $password)
public function authenticate($bind_rdn, $bind_password)
{
if (! ldap_bind($ldap, $username, $password)) {
throw new ClientException('Unable to perform anonymous binding');
if (! @ldap_bind($this->ldap, $bind_rdn, $bind_password)) {
throw new ClientException('LDAP authentication failure for "'.$bind_rdn.'"');
}
return true;
}
/**
* Get LDAP server name
*
* @access public
* @return string
*/
public function getLdapServer()
{
if (! LDAP_SERVER) {
throw new LogicException('LDAP server not configured, check the parameter LDAP_SERVER');
}
return LDAP_SERVER;
}
/**
* Get LDAP username (proxy auth)
*
* @access public
* @return string
*/
public function getLdapUsername()
{
return LDAP_USERNAME;
}
/**
* Get LDAP password (proxy auth)
*
* @access public
* @return string
*/
public function getLdapPassword()
{
return LDAP_PASSWORD;
}
}

63
app/Core/Ldap/Entries.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
namespace Kanboard\Core\Ldap;
/**
* LDAP Entries
*
* @package ldap
* @author Frederic Guillot
*/
class Entries
{
/**
* LDAP entries
*
* @access private
* @var array
*/
private $entries = array();
/**
* Constructor
*
* @access public
* @param array $entries
*/
public function __construct(array $entries)
{
$this->entries = $entries;
}
/**
* Get all entries
*
* @access public
* @return []Entry
*/
public function getAll()
{
$entities = array();
if (! isset($this->entries['count'])) {
return $entities;
}
for ($i = 0; $i < $this->entries['count']; $i++) {
$entities[] = new Entry($this->entries[$i]);
}
return $entities;
}
/**
* Get first entry
*
* @access public
* @return Entry
*/
public function getFirstEntry()
{
return new Entry(isset($this->entries[0]) ? $this->entries[0] : array());
}
}

91
app/Core/Ldap/Entry.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
namespace Kanboard\Core\Ldap;
/**
* LDAP Entry
*
* @package ldap
* @author Frederic Guillot
*/
class Entry
{
/**
* LDAP entry
*
* @access private
* @var array
*/
private $entry = array();
/**
* Constructor
*
* @access public
* @param array $entry
*/
public function __construct(array $entry)
{
$this->entry = $entry;
}
/**
* Get all attribute values
*
* @access public
* @param string $attribute
* @return string[]
*/
public function getAll($attribute)
{
$attributes = array();
if (! isset($this->entry[$attribute]['count'])) {
return $attributes;
}
for ($i = 0; $i < $this->entry[$attribute]['count']; $i++) {
$attributes[] = $this->entry[$attribute][$i];
}
return $attributes;
}
/**
* Get first attribute value
*
* @access public
* @param string $attribute
* @param string $default
* @return string
*/
public function getFirstValue($attribute, $default = '')
{
return isset($this->entry[$attribute][0]) ? $this->entry[$attribute][0] : $default;
}
/**
* Get entry distinguished name
*
* @access public
* @return string
*/
public function getDn()
{
return isset($this->entry['dn']) ? $this->entry['dn'] : '';
}
/**
* Return true if the given value exists in attribute list
*
* @access public
* @param string $attribute
* @param string $value
* @return boolean
*/
public function hasValue($attribute, $value)
{
$attributes = $this->getAll($attribute);
return in_array($value, $attributes);
}
}

130
app/Core/Ldap/Group.php Normal file
View File

@@ -0,0 +1,130 @@
<?php
namespace Kanboard\Core\Ldap;
use LogicException;
use Kanboard\Group\LdapGroupProvider;
/**
* LDAP Group Finder
*
* @package ldap
* @author Frederic Guillot
*/
class Group
{
/**
* Query
*
* @access private
* @var Query
*/
private $query;
/**
* Constructor
*
* @access public
* @param Query $query
*/
public function __construct(Query $query)
{
$this->query = $query;
}
/**
* Get groups
*
* @static
* @access public
* @param Client $client
* @param string $query
* @return array
*/
public static function getGroups(Client $client, $query)
{
$self = new self(new Query($client));
return $self->find($query);
}
/**
* Find groups
*
* @access public
* @param string $query
* @return array
*/
public function find($query)
{
$this->query->execute($this->getBasDn(), $query, $this->getAttributes());
$groups = array();
if ($this->query->hasResult()) {
$groups = $this->build();
}
return $groups;
}
/**
* Build groups list
*
* @access protected
* @return array
*/
protected function build()
{
$groups = array();
foreach ($this->query->getEntries()->getAll() as $entry) {
$groups[] = new LdapGroupProvider($entry->getDn(), $entry->getFirstValue($this->getAttributeName()));
}
return $groups;
}
/**
* Ge the list of attributes to fetch when reading the LDAP group 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->getAttributeName(),
)));
}
/**
* Get LDAP group name attribute
*
* @access public
* @return string
*/
public function getAttributeName()
{
if (! LDAP_GROUP_ATTRIBUTE_NAME) {
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME');
}
return LDAP_GROUP_ATTRIBUTE_NAME;
}
/**
* Get LDAP group base DN
*
* @access public
* @return string
*/
public function getBasDn()
{
if (! LDAP_GROUP_BASE_DN) {
throw new LogicException('LDAP group base DN empty, check the parameter LDAP_GROUP_BASE_DN');
}
return LDAP_GROUP_BASE_DN;
}
}

View File

@@ -10,6 +10,14 @@ namespace Kanboard\Core\Ldap;
*/
class Query
{
/**
* LDAP client
*
* @access private
* @var Client
*/
private $client = null;
/**
* Query result
*
@@ -22,31 +30,30 @@ class Query
* Constructor
*
* @access public
* @param array $entries
* @param Client $client
*/
public function __construct(array $entries = array())
public function __construct(Client $client)
{
$this->entries = $entries;
$this->client = $client;
}
/**
* 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)
public function execute($baseDn, $filter, array $attributes)
{
$sr = ldap_search($ldap, $baseDn, $filter, $attributes);
$sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes);
if ($sr === false) {
return $this;
}
$entries = ldap_get_entries($ldap, $sr);
$entries = ldap_get_entries($this->client->getConnection(), $sr);
if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
return $this;
}
@@ -68,28 +75,13 @@ class Query
}
/**
* Return subset of entries
* Get LDAP Entries
*
* @access public
* @param string $key
* @param mixed $default
* @return array
* @return Entities
*/
public function getAttribute($key, $default = null)
public function getEntries()
{
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;
return new Entries($this->entries);
}
}

View File

@@ -2,8 +2,12 @@
namespace Kanboard\Core\Ldap;
use LogicException;
use Kanboard\Core\Security\Role;
use Kanboard\User\LdapUserProvider;
/**
* LDAP User
* LDAP User Finder
*
* @package ldap
* @author Frederic Guillot
@@ -24,74 +28,72 @@ class User
* @access public
* @param Query $query
*/
public function __construct(Query $query = null)
public function __construct(Query $query)
{
$this->query = $query ?: new Query;
$this->query = $query;
}
/**
* Get user profile
* Get user profile (helper)
*
* @static
* @access public
* @param resource $ldap
* @param string $baseDn
* @param Client $client
* @param string $query
* @return array
*/
public function getProfile($ldap, $baseDn, $query)
public static function getUser(Client $client, $query)
{
$this->query->execute($ldap, $baseDn, $query, $this->getAttributes());
$profile = array();
$self = new self(new Query($client));
return $self->find($query);
}
/**
* Find user
*
* @access public
* @param string $query
* @return null|LdapUserProvider
*/
public function find($query)
{
$this->query->execute($this->getBasDn(), $query, $this->getAttributes());
$user = null;
if ($this->query->hasResult()) {
$profile = $this->prepareProfile();
$user = $this->build();
}
return $profile;
return $user;
}
/**
* Build user profile
*
* @access private
* @return boolean|array
* @access protected
* @return LdapUserProvider
*/
private function prepareProfile()
protected function build()
{
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,
$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;
}
return new LdapUserProvider(
$entry->getDn(),
$entry->getFirstValue($this->getAttributeUsername()),
$entry->getFirstValue($this->getAttributeName()),
$entry->getFirstValue($this->getAttributeEmail()),
$role,
$entry->getAll($this->getAttributeGroup())
);
}
/**
* 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
*
@@ -118,7 +120,26 @@ class User
*/
public function getAttributeUsername()
{
return LDAP_ACCOUNT_ID;
if (! LDAP_USER_ATTRIBUTE_USERNAME) {
throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
return LDAP_USER_ATTRIBUTE_USERNAME;
}
/**
* Get LDAP user name attribute
*
* @access public
* @return string
*/
public function getAttributeName()
{
if (! LDAP_USER_ATTRIBUTE_FULLNAME) {
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME');
}
return LDAP_USER_ATTRIBUTE_FULLNAME;
}
/**
@@ -129,18 +150,11 @@ class User
*/
public function getAttributeEmail()
{
return LDAP_ACCOUNT_EMAIL;
}
if (! LDAP_USER_ATTRIBUTE_EMAIL) {
throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
}
/**
* Get LDAP account name attribute
*
* @access public
* @return string
*/
public function getAttributeName()
{
return LDAP_ACCOUNT_FULLNAME;
return LDAP_USER_ATTRIBUTE_EMAIL;
}
/**
@@ -151,7 +165,7 @@ class User
*/
public function getAttributeGroup()
{
return LDAP_ACCOUNT_MEMBEROF;
return LDAP_USER_ATTRIBUTE_GROUPS;
}
/**
@@ -166,13 +180,28 @@ class User
}
/**
* Get LDAP project admin group DN
* Get LDAP application manager group DN
*
* @access public
* @return string
*/
public function getGroupProjectAdminDn()
public function getGroupManagerDn()
{
return LDAP_GROUP_PROJECT_ADMIN_DN;
return LDAP_GROUP_MANAGER_DN;
}
/**
* Get LDAP user base DN
*
* @access public
* @return string
*/
public function getBasDn()
{
if (! LDAP_USER_BASE_DN) {
throw new LogicException('LDAP user base DN empty, check the parameter LDAP_USER_BASE_DN');
}
return LDAP_USER_BASE_DN;
}
}

View File

@@ -18,6 +18,14 @@ class AccessMap
*/
private $defaultRole = '';
/**
* Role hierarchy
*
* @access private
* @var array
*/
private $hierarchy = array();
/**
* Access map
*
@@ -39,16 +47,77 @@ class AccessMap
return $this;
}
/**
* Define role hierarchy
*
* @access public
* @param string $role
* @param array $subroles
* @return Acl
*/
public function setRoleHierarchy($role, array $subroles)
{
foreach ($subroles as $subrole) {
if (isset($this->hierarchy[$subrole])) {
$this->hierarchy[$subrole][] = $role;
} else {
$this->hierarchy[$subrole] = array($role);
}
}
return $this;
}
/**
* Get computed role hierarchy
*
* @access public
* @param string $role
* @return array
*/
public function getRoleHierarchy($role)
{
$roles = array($role);
if (isset($this->hierarchy[$role])) {
$roles = array_merge($roles, $this->hierarchy[$role]);
}
return $roles;
}
/**
* Add new access rules
*
* @access public
* @param string $controller
* @param string $method
* @param array $roles
* @param string $controller Controller class name
* @param mixed $methods List of method name or just one method
* @param string $role Lowest role required
* @return Acl
*/
public function add($controller, $method, array $roles)
public function add($controller, $methods, $role)
{
if (is_array($methods)) {
foreach ($methods as $method) {
$this->addRule($controller, $method, $role);
}
} else {
$this->addRule($controller, $methods, $role);
}
return $this;
}
/**
* Add new access rule
*
* @access private
* @param string $controller
* @param string $method
* @param string $role
* @return Acl
*/
private function addRule($controller, $method, $role)
{
$controller = strtolower($controller);
$method = strtolower($method);
@@ -57,11 +126,7 @@ class AccessMap
$this->map[$controller] = array();
}
if (! isset($this->map[$controller][$method])) {
$this->map[$controller][$method] = array();
}
$this->map[$controller][$method] = $roles;
$this->map[$controller][$method] = $role;
return $this;
}
@@ -79,14 +144,12 @@ class AccessMap
$controller = strtolower($controller);
$method = strtolower($method);
if (isset($this->map[$controller][$method])) {
return $this->map[$controller][$method];
foreach (array($method, '*') as $key) {
if (isset($this->map[$controller][$key])) {
return $this->getRoleHierarchy($this->map[$controller][$key]);
}
}
if (isset($this->map[$controller]['*'])) {
return $this->map[$controller]['*'];
}
return array($this->defaultRole);
return $this->getRoleHierarchy($this->defaultRole);
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace Kanboard\Core\Security;
use LogicException;
use Kanboard\Core\Base;
use Kanboard\Core\User\UserProviderInterface;
use Kanboard\Event\AuthFailureEvent;
use Kanboard\Event\AuthSuccessEvent;
/**
* Authentication Manager
*
* @package security
* @author Frederic Guillot
*/
class AuthenticationManager extends Base
{
/**
* Event names
*
* @var string
*/
const EVENT_SUCCESS = 'auth.success';
const EVENT_FAILURE = 'auth.failure';
/**
* List of authentication providers
*
* @access private
* @var array
*/
private $providers = array();
/**
* Register a new authentication provider
*
* @access public
* @param AuthenticationProviderInterface $provider
* @return AuthenticationManager
*/
public function register(AuthenticationProviderInterface $provider)
{
$this->providers[$provider->getName()] = $provider;
return $this;
}
/**
* Register a new authentication provider
*
* @access public
* @param string $name
* @return AuthenticationProviderInterface|OAuthAuthenticationProviderInterface|PasswordAuthenticationProviderInterface|PreAuthenticationProviderInterface|OAuthAuthenticationProviderInterface
*/
public function getProvider($name)
{
if (! isset($this->providers[$name])) {
throw new LogicException('Authentication provider not found: '.$name);
}
return $this->providers[$name];
}
/**
* Execute providers that are able to validate the current session
*
* @access public
* @return boolean
*/
public function checkCurrentSession()
{
if ($this->userSession->isLogged() ) {
foreach ($this->filterProviders('SessionCheckProviderInterface') as $provider) {
if (! $provider->isValidSession()) {
unset($this->sessionStorage->user);
$this->preAuthentication();
return false;
}
}
}
return true;
}
/**
* Execute pre-authentication providers
*
* @access public
* @return boolean
*/
public function preAuthentication()
{
foreach ($this->filterProviders('PreAuthenticationProviderInterface') as $provider) {
if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
$this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
return true;
}
}
return false;
}
/**
* Execute username/password authentication providers
*
* @access public
* @param string $username
* @param string $password
* @param boolean $fireEvent
* @return boolean
*/
public function passwordAuthentication($username, $password, $fireEvent = true)
{
foreach ($this->filterProviders('PasswordAuthenticationProviderInterface') as $provider) {
$provider->setUsername($username);
$provider->setPassword($password);
if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
if ($fireEvent) {
$this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
}
return true;
}
}
if ($fireEvent) {
$this->dispatcher->dispatch(self::EVENT_FAILURE, new AuthFailureEvent($username));
}
return false;
}
/**
* Perform OAuth2 authentication
*
* @access public
* @param string $name
* @return boolean
*/
public function oauthAuthentication($name)
{
$provider = $this->getProvider($name);
if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
$this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
return true;
}
$this->dispatcher->dispatch(self::EVENT_FAILURE, new AuthFailureEvent);
return false;
}
/**
* Get the last Post-Authentication provider
*
* @access public
* @return PostAuthenticationProviderInterface
*/
public function getPostAuthenticationProvider()
{
$providers = $this->filterProviders('PostAuthenticationProviderInterface');
if (empty($providers)) {
throw new LogicException('You must have at least one Post-Authentication Provider configured');
}
return array_pop($providers);
}
/**
* Filter registered providers by interface type
*
* @access private
* @param string $interface
* @return array
*/
private function filterProviders($interface)
{
$interface = '\Kanboard\Core\Security\\'.$interface;
return array_filter($this->providers, function(AuthenticationProviderInterface $provider) use ($interface) {
return is_a($provider, $interface);
});
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Kanboard\Core\Security;
/**
* Authentication Provider Interface
*
* @package security
* @author Frederic Guillot
*/
interface AuthenticationProviderInterface
{
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName();
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate();
}

View File

@@ -16,17 +16,17 @@ class Authorization
* @access private
* @var AccessMap
*/
private $acl;
private $accessMap;
/**
* Constructor
*
* @access public
* @param AccessMap $acl
* @param AccessMap $accessMap
*/
public function __construct(AccessMap $acl)
public function __construct(AccessMap $accessMap)
{
$this->acl = $acl;
$this->accessMap = $accessMap;
}
/**
@@ -40,7 +40,7 @@ class Authorization
*/
public function isAllowed($controller, $method, $role)
{
$roles = $this->acl->getRoles($controller, $method);
$roles = $this->accessMap->getRoles($controller, $method);
return in_array($role, $roles);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Kanboard\Core\Security;
/**
* OAuth2 Authentication Provider Interface
*
* @package security
* @author Frederic Guillot
*/
interface OAuthAuthenticationProviderInterface extends AuthenticationProviderInterface
{
/**
* Get user object
*
* @access public
* @return UserProviderInterface
*/
public function getUser();
/**
* Unlink user
*
* @access public
* @param integer $userId
* @return bool
*/
public function unlink($userId);
/**
* Get configured OAuth2 service
*
* @access public
* @return Kanboard\Core\Http\OAuth2
*/
public function getService();
/**
* Set OAuth2 code
*
* @access public
* @param string $code
* @return OAuthAuthenticationProviderInterface
*/
public function setCode($code);
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Kanboard\Core\Security;
/**
* Password Authentication Provider Interface
*
* @package security
* @author Frederic Guillot
*/
interface PasswordAuthenticationProviderInterface extends AuthenticationProviderInterface
{
/**
* Get user object
*
* @access public
* @return UserProviderInterface
*/
public function getUser();
/**
* Set username
*
* @access public
* @param string $username
*/
public function setUsername($username);
/**
* Set password
*
* @access public
* @param string $password
*/
public function setPassword($password);
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Kanboard\Core\Security;
/**
* Post Authentication Provider Interface
*
* @package security
* @author Frederic Guillot
*/
interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface
{
/**
* Set user pin-code
*
* @access public
* @param string $code
*/
public function setCode($code);
/**
* Set secret token (fetched from user profile)
*
* @access public
* @param string $secret
*/
public function setSecret($secret);
/**
* Get secret token (will be saved in user profile)
*
* @access public
* @return string
*/
public function getSecret();
/**
* Get QR code url (empty if no QR can be provided)
*
* @access public
* @param string $label
* @return string
*/
public function getQrCodeUrl($label);
/**
* Get key url (empty if no url can be provided)
*
* @access public
* @param string $label
* @return string
*/
public function getKeyUrl($label);
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Kanboard\Core\Security;
/**
* Pre-Authentication Provider Interface
*
* @package security
* @author Frederic Guillot
*/
interface PreAuthenticationProviderInterface extends AuthenticationProviderInterface
{
/**
* Get user object
*
* @access public
* @return UserProviderInterface
*/
public function getUser();
}

View File

@@ -18,4 +18,47 @@ class Role
const PROJECT_MANAGER = 'project-manager';
const PROJECT_MEMBER = 'project-member';
const PROJECT_VIEWER = 'project-viewer';
/**
* Get application roles
*
* @access public
* @return array
*/
public function getApplicationRoles()
{
return array(
self::APP_ADMIN => t('Administrator'),
self::APP_MANAGER => t('Manager'),
self::APP_USER => t('User'),
);
}
/**
* Get project roles
*
* @access public
* @return array
*/
public function getProjectRoles()
{
return array(
self::PROJECT_MANAGER => t('Project Manager'),
self::PROJECT_MEMBER => t('Project Member'),
self::PROJECT_VIEWER => t('Project Viewer'),
);
}
/**
* Get application roles
*
* @access public
* @param string $role
* @return string
*/
public function getRoleName($role)
{
$roles = $this->getApplicationRoles() + $this->getProjectRoles();
return isset($roles[$role]) ? $roles[$role] : t('Unknown');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Kanboard\Core\Security;
/**
* Session Check Provider Interface
*
* @package security
* @author Frederic Guillot
*/
interface SessionCheckProviderInterface
{
/**
* Check if the user session is valid
*
* @access public
* @return boolean
*/
public function isValidSession();
}

View File

@@ -13,6 +13,13 @@ use Kanboard\Core\Http\Request;
*/
class SessionManager extends Base
{
/**
* Event names
*
* @var string
*/
const EVENT_DESTROY = 'session.destroy';
/**
* Return true if the session is open
*
@@ -41,7 +48,7 @@ class SessionManager extends Base
session_name('KB_SID');
session_start();
$this->container['sessionStorage']->setStorage($_SESSION);
$this->sessionStorage->setStorage($_SESSION);
}
/**
@@ -51,6 +58,8 @@ class SessionManager extends Base
*/
public function close()
{
$this->dispatcher->dispatch(self::EVENT_DESTROY);
// Destroy the session cookie
$params = session_get_cookie_params();
@@ -80,7 +89,7 @@ class SessionManager extends Base
SESSION_DURATION,
$this->helper->url->dir() ?: '/',
null,
Request::isHTTPS(),
$this->request->isHTTPS(),
true
);

View File

@@ -12,12 +12,13 @@ namespace Kanboard\Core\Session;
* @property array $user
* @property array $flash
* @property array $csrf
* @property array $postAuth
* @property array $postAuthenticationValidated
* @property array $filters
* @property string $redirectAfterLogin
* @property string $captcha
* @property string $commentSorting
* @property bool $hasSubtaskInProgress
* @property bool $hasRememberMe
* @property bool $boardCollapsed
*/
class SessionStorage

View File

@@ -0,0 +1,32 @@
<?php
namespace Kanboard\Core\User;
use Kanboard\Core\Base;
/**
* Group Synchronization
*
* @package user
* @author Frederic Guillot
*/
class GroupSync extends Base
{
/**
* Synchronize group membership
*
* @access public
* @param integer $userId
* @param array $groupIds
*/
public function synchronize($userId, array $groupIds)
{
foreach ($groupIds as $groupId) {
$group = $this->group->getByExternalId($groupId);
if (! empty($group) && ! $this->groupMember->isMember($group['id'], $userId)) {
$this->groupMember->addUser($group['id'], $userId);
}
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Kanboard\Core\User;
use Kanboard\Core\Base;
/**
* User Profile
*
* @package user
* @author Frederic Guillot
*/
class UserProfile extends Base
{
/**
* Assign provider data to the local user
*
* @access public
* @param integer $userId
* @param UserProviderInterface $user
* @return boolean
*/
public function assign($userId, UserProviderInterface $user)
{
$profile = $this->user->getById($userId);
$values = UserProperty::filterProperties($profile, UserProperty::getProperties($user));
$values['id'] = $userId;
if ($this->user->update($values)) {
$profile = array_merge($profile, $values);
$this->userSession->initialize($profile);
return true;
}
return false;
}
/**
* Synchronize user properties with the local database and create the user session
*
* @access public
* @param UserProviderInterface $user
* @return boolean
*/
public function initialize(UserProviderInterface $user)
{
if ($user->getInternalId()) {
$profile = $this->user->getById($user->getInternalId());
} elseif ($user->getExternalIdColumn() && $user->getExternalId()) {
$profile = $this->userSync->synchronize($user);
$this->groupSync->synchronize($profile['id'], $user->getExternalGroupIds());
}
if (! empty($profile)) {
$this->userSession->initialize($profile);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Kanboard\Core\User;
/**
* User Property
*
* @package user
* @author Frederic Guillot
*/
class UserProperty
{
/**
* Get filtered user properties from user provider
*
* @static
* @access public
* @param UserProviderInterface $user
* @return array
*/
public static function getProperties(UserProviderInterface $user)
{
$properties = array(
'username' => $user->getUsername(),
'name' => $user->getName(),
'email' => $user->getEmail(),
'role' => $user->getRole(),
$user->getExternalIdColumn() => $user->getExternalId(),
);
$properties = array_merge($properties, $user->getExtraAttributes());
return array_filter($properties, array(__NAMESPACE__.'\UserProperty', 'isNotEmptyValue'));
}
/**
* Filter user properties compared to existing user profile
*
* @static
* @access public
* @param array $profile
* @param array $properties
* @return array
*/
public static function filterProperties(array $profile, array $properties)
{
$values = array();
foreach ($properties as $property => $value) {
if (array_key_exists($property, $profile) && ! self::isNotEmptyValue($profile[$property])) {
$values[$property] = $value;
}
}
return $values;
}
/**
* Check if a value is not empty
*
* @static
* @access public
* @param string $value
* @return boolean
*/
public static function isNotEmptyValue($value)
{
return $value !== null && $value !== '';
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Kanboard\Core\User;
/**
* User Provider Interface
*
* @package user
* @author Frederic Guillot
*/
interface UserProviderInterface
{
/**
* Return true to allow automatic user creation
*
* @access public
* @return boolean
*/
public function isUserCreationAllowed();
/**
* Get external id column name
*
* Example: google_id, github_id, gitlab_id...
*
* @access public
* @return string
*/
public function getExternalIdColumn();
/**
* Get internal id
*
* If a value is returned the user properties won't be updated in the local database
*
* @access public
* @return integer
*/
public function getInternalId();
/**
* Get external id
*
* @access public
* @return string
*/
public function getExternalId();
/**
* Get user role
*
* Return an empty string to not override role stored in the database
*
* @access public
* @return string
*/
public function getRole();
/**
* Get username
*
* @access public
* @return string
*/
public function getUsername();
/**
* Get user full name
*
* @access public
* @return string
*/
public function getName();
/**
* Get user email
*
* @access public
* @return string
*/
public function getEmail();
/**
* Get external group ids
*
* A synchronization is done at login time,
* the user will be member of those groups if they exists in the database
*
* @access public
* @return string[]
*/
public function getExternalGroupIds();
/**
* Get extra user attributes
*
* Example: is_ldap_user, disable_login_form, notifications_enabled...
*
* @access public
* @return array
*/
public function getExtraAttributes();
}

View File

@@ -0,0 +1,204 @@
<?php
namespace Kanboard\Core\User;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* User Session
*
* @package user
* @author Frederic Guillot
*/
class UserSession extends Base
{
/**
* Update user session
*
* @access public
* @param array $user
*/
public function initialize(array $user)
{
foreach (array('password', 'is_admin', 'is_project_admin', 'twofactor_secret') as $column) {
if (isset($user[$column])) {
unset($user[$column]);
}
}
$user['id'] = (int) $user['id'];
$user['is_ldap_user'] = isset($user['is_ldap_user']) ? (bool) $user['is_ldap_user'] : false;
$user['twofactor_activated'] = isset($user['twofactor_activated']) ? (bool) $user['twofactor_activated'] : false;
$this->sessionStorage->user = $user;
$this->sessionStorage->postAuthenticationValidated = false;
}
/**
* Get user application role
*
* @access public
* @return string
*/
public function getRole()
{
return $this->sessionStorage->user['role'];
}
/**
* Return true if the user has validated the 2FA key
*
* @access public
* @return bool
*/
public function isPostAuthenticationValidated()
{
return isset($this->sessionStorage->postAuthenticationValidated) && $this->sessionStorage->postAuthenticationValidated === true;
}
/**
* Validate 2FA for the current session
*
* @access public
*/
public function validatePostAuthentication()
{
$this->sessionStorage->postAuthenticationValidated = true;
}
/**
* Return true if the user has 2FA enabled
*
* @access public
* @return bool
*/
public function hasPostAuthentication()
{
return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true;
}
/**
* Disable 2FA for the current session
*
* @access public
*/
public function disablePostAuthentication()
{
$this->sessionStorage->user['twofactor_activated'] = false;
}
/**
* Return true if the logged user is admin
*
* @access public
* @return bool
*/
public function isAdmin()
{
return isset($this->sessionStorage->user['role']) && $this->sessionStorage->user['role'] === Role::APP_ADMIN;
}
/**
* Get the connected user id
*
* @access public
* @return integer
*/
public function getId()
{
return isset($this->sessionStorage->user['id']) ? (int) $this->sessionStorage->user['id'] : 0;
}
/**
* Get username
*
* @access public
* @return integer
*/
public function getUsername()
{
return isset($this->sessionStorage->user['username']) ? $this->sessionStorage->user['username'] : '';
}
/**
* Check is the user is connected
*
* @access public
* @return bool
*/
public function isLogged()
{
return isset($this->sessionStorage->user) && ! empty($this->sessionStorage->user);
}
/**
* Get project filters from the session
*
* @access public
* @param integer $project_id
* @return string
*/
public function getFilters($project_id)
{
return ! empty($this->sessionStorage->filters[$project_id]) ? $this->sessionStorage->filters[$project_id] : 'status:open';
}
/**
* Save project filters in the session
*
* @access public
* @param integer $project_id
* @param string $filters
*/
public function setFilters($project_id, $filters)
{
$this->sessionStorage->filters[$project_id] = $filters;
}
/**
* Is board collapsed or expanded
*
* @access public
* @param integer $project_id
* @return boolean
*/
public function isBoardCollapsed($project_id)
{
return ! empty($this->sessionStorage->boardCollapsed[$project_id]) ? $this->sessionStorage->boardCollapsed[$project_id] : false;
}
/**
* Set board display mode
*
* @access public
* @param integer $project_id
* @param boolean $is_collapsed
*/
public function setBoardDisplayMode($project_id, $is_collapsed)
{
$this->sessionStorage->boardCollapsed[$project_id] = $is_collapsed;
}
/**
* Set comments sorting
*
* @access public
* @param string $order
*/
public function setCommentSorting($order)
{
$this->sessionStorage->commentSorting = $order;
}
/**
* Get comments sorting direction
*
* @access public
* @return string
*/
public function getCommentSorting()
{
return empty($this->sessionStorage->commentSorting) ? 'ASC' : $this->sessionStorage->commentSorting;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Kanboard\Core\User;
use Kanboard\Core\Base;
/**
* User Synchronization
*
* @package user
* @author Frederic Guillot
*/
class UserSync extends Base
{
/**
* Synchronize user profile
*
* @access public
* @param UserProviderInterface $user
* @return array
*/
public function synchronize(UserProviderInterface $user)
{
$profile = $this->user->getByExternalId($user->getExternalIdColumn(), $user->getExternalId());
$properties = UserProperty::getProperties($user);
if (! empty($profile)) {
$profile = $this->updateUser($profile, $properties);
} elseif ($user->isUserCreationAllowed()) {
$profile = $this->createUser($user, $properties);
}
return $profile;
}
/**
* Update user profile
*
* @access public
* @param array $profile
* @param array $properties
* @return array
*/
private function updateUser(array $profile, array $properties)
{
$values = UserProperty::filterProperties($profile, $properties);
if (! empty($values)) {
$values['id'] = $profile['id'];
$result = $this->user->update($values);
return $result ? array_merge($profile, $properties) : $profile;
}
return $profile;
}
/**
* Create user
*
* @access public
* @param UserProviderInterface $user
* @param array $properties
* @return array
*/
private function createUser(UserProviderInterface $user, array $properties)
{
$id = $this->user->create($properties);
if ($id === false) {
$this->logger->error('Unable to create user profile: '.$user->getExternalId());
return array();
}
return $this->user->getById($id);
}
}