diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-12-05 20:31:27 -0500 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-12-05 20:31:27 -0500 |
commit | e9fedf3e5cd63aea4da7a71f6647ee427c62fa49 (patch) | |
tree | abc2de5aebace4a2d7c94805552264dab6b10bc7 /app/Core/Security | |
parent | 346b8312e5ac877ce3192c2db3a26b500018bbb5 (diff) |
Rewrite of the authentication and authorization system
Diffstat (limited to 'app/Core/Security')
-rw-r--r-- | app/Core/Security/AccessMap.php | 91 | ||||
-rw-r--r-- | app/Core/Security/AuthenticationManager.php | 187 | ||||
-rw-r--r-- | app/Core/Security/AuthenticationProviderInterface.php | 28 | ||||
-rw-r--r-- | app/Core/Security/Authorization.php | 10 | ||||
-rw-r--r-- | app/Core/Security/OAuthAuthenticationProviderInterface.php | 46 | ||||
-rw-r--r-- | app/Core/Security/PasswordAuthenticationProviderInterface.php | 36 | ||||
-rw-r--r-- | app/Core/Security/PostAuthenticationProviderInterface.php | 54 | ||||
-rw-r--r-- | app/Core/Security/PreAuthenticationProviderInterface.php | 20 | ||||
-rw-r--r-- | app/Core/Security/Role.php | 43 | ||||
-rw-r--r-- | app/Core/Security/SessionCheckProviderInterface.php | 20 |
10 files changed, 516 insertions, 19 deletions
diff --git a/app/Core/Security/AccessMap.php b/app/Core/Security/AccessMap.php index 10a29e1f..02a4ca45 100644 --- a/app/Core/Security/AccessMap.php +++ b/app/Core/Security/AccessMap.php @@ -19,6 +19,14 @@ class AccessMap private $defaultRole = ''; /** + * Role hierarchy + * + * @access private + * @var array + */ + private $hierarchy = array(); + + /** * Access map * * @access private @@ -40,15 +48,76 @@ class AccessMap } /** + * 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 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, $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 array $roles + * @param string $role * @return Acl */ - public function add($controller, $method, array $roles) + 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]; - } - - if (isset($this->map[$controller]['*'])) { - return $this->map[$controller]['*']; + foreach (array($method, '*') as $key) { + if (isset($this->map[$controller][$key])) { + return $this->getRoleHierarchy($this->map[$controller][$key]); + } } - return array($this->defaultRole); + return $this->getRoleHierarchy($this->defaultRole); } } diff --git a/app/Core/Security/AuthenticationManager.php b/app/Core/Security/AuthenticationManager.php new file mode 100644 index 00000000..cced58c0 --- /dev/null +++ b/app/Core/Security/AuthenticationManager.php @@ -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); + }); + } +} diff --git a/app/Core/Security/AuthenticationProviderInterface.php b/app/Core/Security/AuthenticationProviderInterface.php new file mode 100644 index 00000000..828e272c --- /dev/null +++ b/app/Core/Security/AuthenticationProviderInterface.php @@ -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(); +} diff --git a/app/Core/Security/Authorization.php b/app/Core/Security/Authorization.php index a04b3720..980db048 100644 --- a/app/Core/Security/Authorization.php +++ b/app/Core/Security/Authorization.php @@ -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); } } diff --git a/app/Core/Security/OAuthAuthenticationProviderInterface.php b/app/Core/Security/OAuthAuthenticationProviderInterface.php new file mode 100644 index 00000000..c32339e0 --- /dev/null +++ b/app/Core/Security/OAuthAuthenticationProviderInterface.php @@ -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); +} diff --git a/app/Core/Security/PasswordAuthenticationProviderInterface.php b/app/Core/Security/PasswordAuthenticationProviderInterface.php new file mode 100644 index 00000000..918a4aec --- /dev/null +++ b/app/Core/Security/PasswordAuthenticationProviderInterface.php @@ -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); +} diff --git a/app/Core/Security/PostAuthenticationProviderInterface.php b/app/Core/Security/PostAuthenticationProviderInterface.php new file mode 100644 index 00000000..88fc2fe5 --- /dev/null +++ b/app/Core/Security/PostAuthenticationProviderInterface.php @@ -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); +} diff --git a/app/Core/Security/PreAuthenticationProviderInterface.php b/app/Core/Security/PreAuthenticationProviderInterface.php new file mode 100644 index 00000000..391e8d0f --- /dev/null +++ b/app/Core/Security/PreAuthenticationProviderInterface.php @@ -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(); +} diff --git a/app/Core/Security/Role.php b/app/Core/Security/Role.php index 079ce14b..85d85743 100644 --- a/app/Core/Security/Role.php +++ b/app/Core/Security/Role.php @@ -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'); + } } diff --git a/app/Core/Security/SessionCheckProviderInterface.php b/app/Core/Security/SessionCheckProviderInterface.php new file mode 100644 index 00000000..232fe1db --- /dev/null +++ b/app/Core/Security/SessionCheckProviderInterface.php @@ -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(); +} |