diff options
Diffstat (limited to 'app/Core/Security')
| -rw-r--r-- | app/Core/Security/AccessMap.php | 175 | ||||
| -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 | 46 | ||||
| -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 | 69 | ||||
| -rw-r--r-- | app/Core/Security/PreAuthenticationProviderInterface.php | 20 | ||||
| -rw-r--r-- | app/Core/Security/Role.php | 64 | ||||
| -rw-r--r-- | app/Core/Security/SessionCheckProviderInterface.php | 20 | ||||
| -rw-r--r-- | app/Core/Security/Token.php | 61 |
11 files changed, 752 insertions, 0 deletions
diff --git a/app/Core/Security/AccessMap.php b/app/Core/Security/AccessMap.php new file mode 100644 index 00000000..f34c4b00 --- /dev/null +++ b/app/Core/Security/AccessMap.php @@ -0,0 +1,175 @@ +<?php + +namespace Kanboard\Core\Security; + +/** + * Access Map Definition + * + * @package security + * @author Frederic Guillot + */ +class AccessMap +{ + /** + * Default role + * + * @access private + * @var string + */ + private $defaultRole = ''; + + /** + * Role hierarchy + * + * @access private + * @var array + */ + private $hierarchy = array(); + + /** + * Access map + * + * @access private + * @var array + */ + private $map = array(); + + /** + * Define the default role when nothing match + * + * @access public + * @param string $role + * @return Acl + */ + public function setDefaultRole($role) + { + $this->defaultRole = $role; + 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; + } + + /** + * Get the highest role from a list + * + * @access public + * @param array $roles + * @return string + */ + public function getHighestRole(array $roles) + { + $rank = array(); + + foreach ($roles as $role) { + $rank[$role] = count($this->getRoleHierarchy($role)); + } + + asort($rank); + + return key($rank); + } + + /** + * 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 string $role + * @return Acl + */ + private function addRule($controller, $method, $role) + { + $controller = strtolower($controller); + $method = strtolower($method); + + if (! isset($this->map[$controller])) { + $this->map[$controller] = array(); + } + + $this->map[$controller][$method] = $role; + + return $this; + } + + /** + * Get roles that match the given controller/method + * + * @access public + * @param string $controller + * @param string $method + * @return boolean + */ + public function getRoles($controller, $method) + { + $controller = strtolower($controller); + $method = strtolower($method); + + foreach (array($method, '*') as $key) { + if (isset($this->map[$controller][$key])) { + return $this->getRoleHierarchy($this->map[$controller][$key]); + } + } + + 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..b1ba76cf --- /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\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()) { + $this->logger->debug('Invalidate session for '.$this->userSession->getUsername()); + $this->sessionStorage->flush(); + $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 new file mode 100644 index 00000000..980db048 --- /dev/null +++ b/app/Core/Security/Authorization.php @@ -0,0 +1,46 @@ +<?php + +namespace Kanboard\Core\Security; + +/** + * Authorization Handler + * + * @package security + * @author Frederic Guillot + */ +class Authorization +{ + /** + * Access Map + * + * @access private + * @var AccessMap + */ + private $accessMap; + + /** + * Constructor + * + * @access public + * @param AccessMap $accessMap + */ + public function __construct(AccessMap $accessMap) + { + $this->accessMap = $accessMap; + } + + /** + * Check if the given role is allowed to access to the specified resource + * + * @access public + * @param string $controller + * @param string $method + * @param string $role + * @return boolean + */ + public function isAllowed($controller, $method, $role) + { + $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..3f628bb0 --- /dev/null +++ b/app/Core/Security/PostAuthenticationProviderInterface.php @@ -0,0 +1,69 @@ +<?php + +namespace Kanboard\Core\Security; + +/** + * Post Authentication Provider Interface + * + * @package security + * @author Frederic Guillot + */ +interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface +{ + /** + * Called only one time before to prompt the user for pin code + * + * @access public + */ + public function beforeCode(); + + /** + * Set user pin-code + * + * @access public + * @param string $code + */ + public function setCode($code); + + /** + * Generate secret if necessary + * + * @access public + * @return string + */ + public function generateSecret(); + + /** + * 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 new file mode 100644 index 00000000..cb45a8af --- /dev/null +++ b/app/Core/Security/Role.php @@ -0,0 +1,64 @@ +<?php + +namespace Kanboard\Core\Security; + +/** + * Role Definitions + * + * @package security + * @author Frederic Guillot + */ +class Role +{ + const APP_ADMIN = 'app-admin'; + const APP_MANAGER = 'app-manager'; + const APP_USER = 'app-user'; + const APP_PUBLIC = 'app-public'; + + 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 role name + * + * @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(); +} diff --git a/app/Core/Security/Token.php b/app/Core/Security/Token.php new file mode 100644 index 00000000..cbd784a8 --- /dev/null +++ b/app/Core/Security/Token.php @@ -0,0 +1,61 @@ +<?php + +namespace Kanboard\Core\Security; + +use Kanboard\Core\Base; + +/** + * Token Handler + * + * @package security + * @author Frederic Guillot + */ +class Token extends Base +{ + /** + * Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid() + * + * @static + * @access public + * @return string Random token + */ + public static function getToken() + { + return bin2hex(random_bytes(30)); + } + + /** + * Generate and store a CSRF token in the current session + * + * @access public + * @return string Random token + */ + public function getCSRFToken() + { + if (! isset($this->sessionStorage->csrf)) { + $this->sessionStorage->csrf = array(); + } + + $nonce = self::getToken(); + $this->sessionStorage->csrf[$nonce] = true; + + return $nonce; + } + + /** + * Check if the token exists for the current session (a token can be used only one time) + * + * @access public + * @param string $token CSRF token + * @return bool + */ + public function validateCSRFToken($token) + { + if (isset($this->sessionStorage->csrf[$token])) { + unset($this->sessionStorage->csrf[$token]); + return true; + } + + return false; + } +} |
