summaryrefslogtreecommitdiff
path: root/app/Core/Security
diff options
context:
space:
mode:
Diffstat (limited to 'app/Core/Security')
-rw-r--r--app/Core/Security/AccessMap.php175
-rw-r--r--app/Core/Security/AuthenticationManager.php187
-rw-r--r--app/Core/Security/AuthenticationProviderInterface.php28
-rw-r--r--app/Core/Security/Authorization.php46
-rw-r--r--app/Core/Security/OAuthAuthenticationProviderInterface.php46
-rw-r--r--app/Core/Security/PasswordAuthenticationProviderInterface.php36
-rw-r--r--app/Core/Security/PostAuthenticationProviderInterface.php69
-rw-r--r--app/Core/Security/PreAuthenticationProviderInterface.php20
-rw-r--r--app/Core/Security/Role.php64
-rw-r--r--app/Core/Security/SessionCheckProviderInterface.php20
-rw-r--r--app/Core/Security/Token.php61
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;
+ }
+}