From e9fedf3e5cd63aea4da7a71f6647ee427c62fa49 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 5 Dec 2015 20:31:27 -0500 Subject: Rewrite of the authentication and authorization system --- app/Core/Security/AccessMap.php | 91 ++++++++-- app/Core/Security/AuthenticationManager.php | 187 +++++++++++++++++++++ .../Security/AuthenticationProviderInterface.php | 28 +++ app/Core/Security/Authorization.php | 10 +- .../OAuthAuthenticationProviderInterface.php | 46 +++++ .../PasswordAuthenticationProviderInterface.php | 36 ++++ .../PostAuthenticationProviderInterface.php | 54 ++++++ .../PreAuthenticationProviderInterface.php | 20 +++ app/Core/Security/Role.php | 43 +++++ .../Security/SessionCheckProviderInterface.php | 20 +++ 10 files changed, 516 insertions(+), 19 deletions(-) create mode 100644 app/Core/Security/AuthenticationManager.php create mode 100644 app/Core/Security/AuthenticationProviderInterface.php create mode 100644 app/Core/Security/OAuthAuthenticationProviderInterface.php create mode 100644 app/Core/Security/PasswordAuthenticationProviderInterface.php create mode 100644 app/Core/Security/PostAuthenticationProviderInterface.php create mode 100644 app/Core/Security/PreAuthenticationProviderInterface.php create mode 100644 app/Core/Security/SessionCheckProviderInterface.php (limited to 'app/Core/Security') 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 @@ -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 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 @@ +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 @@ +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 @@ + 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 @@ +