1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/kanboard_ynh.git synced 2024-09-03 19:36:17 +02:00

Update sources to kanboard v1.0.22

This commit is contained in:
mbugeia 2015-12-29 01:24:09 +01:00
parent 21e59f0725
commit 9b8806775a
452 changed files with 17917 additions and 10094 deletions

View file

@ -1,11 +1,41 @@
Version 1.0.22
--------------
Breaking changes:
* LDAP configuration parameters changes (See documentation)
* SQL table changes:
- "users" table: added new column "role" and removed columns "is_admin" and "is_project_admin"
- "project_has_users" table: replaced column "is_owner" with column "role"
- Sqlite does not support alter table, old columns still there but unused
* API procedure changes:
- createUser
- createLdapUser
- updateUser
- updateTask
* Event removed: "session.bootstrap", use "app.boostrap" instead
New features:
* Add pluggable authentication and authorization system (complete rewrite)
* Add groups (teams/organization)
* Add LDAP groups synchronization
* Add project group permissions
* Add new project role Viewer
* Add generic LDAP client library
* Add search query attribute for task link
* Add the possibility to define API token in config file
* Add capability to reopen Gitlab issues
* Try to load config.php from /data if not available
Version 1.0.21
--------------
Breaking changes:
* Projects with duplicate name are now allowed:
For Postgres and Mysql the unique constraint is removed by database migration
However Sqlite does not support alter table, only new databases will have the unique constraint removed
- For Postgres and Mysql the unique constraint is removed by database migration
- However Sqlite does not support alter table, only new databases will have the unique constraint removed
New features:

View file

@ -3,6 +3,7 @@
namespace Kanboard\Action;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\GitlabWebhook;
use Kanboard\Integration\BitbucketWebhook;
/**
@ -23,6 +24,7 @@ class TaskOpen extends Base
{
return array(
GithubWebhook::EVENT_ISSUE_REOPENED,
GitlabWebhook::EVENT_ISSUE_REOPENED,
BitbucketWebhook::EVENT_ISSUE_REOPENED,
);
}

View file

@ -3,7 +3,6 @@
namespace Kanboard\Api;
use JsonRPC\AuthenticationFailure;
use Symfony\Component\EventDispatcher\Event;
/**
* Base class
@ -24,15 +23,58 @@ class Auth extends Base
*/
public function checkCredentials($username, $password, $class, $method)
{
$this->container['dispatcher']->dispatch('api.bootstrap', new Event);
$this->container['dispatcher']->dispatch('app.bootstrap');
if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
if ($this->isUserAuthenticated($username, $password)) {
$this->checkProcedurePermission(true, $method);
$this->userSession->initialize($this->user->getByUsername($username));
} elseif ($username === 'jsonrpc' && $password === $this->config->get('api_token')) {
} elseif ($this->isAppAuthenticated($username, $password)) {
$this->checkProcedurePermission(false, $method);
} else {
throw new AuthenticationFailure('Wrong credentials');
}
}
/**
* Check user credentials
*
* @access public
* @param string $username
* @param string $password
* @return boolean
*/
private function isUserAuthenticated($username, $password)
{
return $username !== 'jsonrpc' &&
! $this->userLocking->isLocked($username) &&
$this->authenticationManager->passwordAuthentication($username, $password);
}
/**
* Check administrative credentials
*
* @access public
* @param string $username
* @param string $password
* @return boolean
*/
private function isAppAuthenticated($username, $password)
{
return $username === 'jsonrpc' && $password === $this->getApiToken();
}
/**
* Get API Token
*
* @access private
* @return string
*/
private function getApiToken()
{
if (defined('API_AUTHENTICATION_TOKEN')) {
return API_AUTHENTICATION_TOKEN;
}
return $this->config->get('api_token');
}
}

View file

@ -20,7 +20,7 @@ class Me extends Base
public function getMyDashboard()
{
$user_id = $this->userSession->getId();
$projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll();
$projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))->findAll();
$tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
return array(
@ -32,7 +32,7 @@ class Me extends Base
public function getMyActivityStream()
{
$project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
return $this->projectActivity->getProjects($project_ids, 100);
}
@ -50,7 +50,7 @@ class Me extends Base
public function getMyProjectsList()
{
return $this->projectPermission->getMemberProjects($this->userSession->getId());
return $this->projectUserRole->getProjectsByUser($this->userSession->getId());
}
public function getMyOverdueTasks()
@ -60,7 +60,7 @@ class Me extends Base
public function getMyProjects()
{
$project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
$projects = $this->project->getAllByIds($project_ids);
return $this->formatProjects($projects);

View file

@ -2,6 +2,8 @@
namespace Kanboard\Api;
use Kanboard\Core\Security\Role;
/**
* ProjectPermission API controller
*
@ -12,16 +14,16 @@ class ProjectPermission extends \Kanboard\Core\Base
{
public function getMembers($project_id)
{
return $this->projectPermission->getMembers($project_id);
return $this->projectUserRole->getAllUsers($project_id);
}
public function revokeUser($project_id, $user_id)
{
return $this->projectPermission->revokeMember($project_id, $user_id);
return $this->projectUserRole->removeUser($project_id, $user_id);
}
public function allowUser($project_id, $user_id)
{
return $this->projectPermission->addMember($project_id, $user_id);
return $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER);
}
}

View file

@ -71,6 +71,14 @@ class Task extends Base
{
$this->checkProjectPermission($project_id);
if ($owner_id !== 0 && ! $this->projectPermission->isMember($project_id, $owner_id)) {
return false;
}
if ($this->userSession->isLogged()) {
$creator_id = $this->userSession->getId();
}
$values = array(
'title' => $title,
'project_id' => $project_id,
@ -96,20 +104,28 @@ class Task extends Base
return $valid ? $this->taskCreation->create($values) : false;
}
public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null,
$creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null,
public function updateTask($id, $title = null, $color_id = null, $owner_id = null,
$date_due = null, $description = null, $category_id = null, $score = null,
$recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
$recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
{
$this->checkTaskPermission($id);
$project_id = $this->taskFinder->getProjectId($id);
if ($project_id === 0) {
return false;
}
if ($owner_id !== null && ! $this->projectPermission->isMember($project_id, $owner_id)) {
return false;
}
$values = array(
'id' => $id,
'title' => $title,
'project_id' => $project_id,
'color_id' => $color_id,
'owner_id' => $owner_id,
'creator_id' => $creator_id,
'date_due' => $date_due,
'description' => $description,
'category_id' => $category_id,

View file

@ -2,7 +2,11 @@
namespace Kanboard\Api;
use Kanboard\Auth\Ldap;
use LogicException;
use Kanboard\Core\Security\Role;
use Kanboard\Core\Ldap\Client as LdapClient;
use Kanboard\Core\Ldap\ClientException as LdapException;
use Kanboard\Core\Ldap\User as LdapUser;
/**
* User API controller
@ -27,7 +31,7 @@ class User extends \Kanboard\Core\Base
return $this->user->remove($user_id);
}
public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0)
public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER)
{
$values = array(
'username' => $username,
@ -35,44 +39,53 @@ class User extends \Kanboard\Core\Base
'confirmation' => $password,
'name' => $name,
'email' => $email,
'is_admin' => $is_admin,
'is_project_admin' => $is_project_admin,
'role' => $role,
);
list($valid, ) = $this->user->validateCreation($values);
return $valid ? $this->user->create($values) : false;
}
public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0)
public function createLdapUser($username)
{
$ldap = new Ldap($this->container);
$user = $ldap->lookup($username, $email);
try {
if (! $user) {
$ldap = LdapClient::connect();
$user = LdapUser::getUser($ldap, sprintf(LDAP_USER_FILTER, $username));
if ($user === null) {
$this->logger->info('User not found in LDAP server');
return false;
}
if ($user->getUsername() === '') {
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
$values = array(
'username' => $user->getUsername(),
'name' => $user->getName(),
'email' => $user->getEmail(),
'role' => $user->getRole(),
'is_ldap_user' => 1,
);
return $this->user->create($values);
} catch (LdapException $e) {
$this->logger->error($e->getMessage());
return false;
}
$values = array(
'username' => $user['username'],
'name' => $user['name'],
'email' => $user['email'],
'is_ldap_user' => 1,
'is_admin' => $is_admin,
'is_project_admin' => $is_project_admin,
);
return $this->user->create($values);
}
public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null)
public function updateUser($id, $username = null, $name = null, $email = null, $role = null)
{
$values = array(
'id' => $id,
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => $is_admin,
'is_project_admin' => $is_project_admin,
'role' => $role,
);
foreach ($values as $key => $value) {

View file

@ -1,49 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Model\User;
use Kanboard\Event\AuthEvent;
/**
* Database authentication
*
* @package auth
* @author Frederic Guillot
*/
class Database extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Database';
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
$user = $this->db
->table(User::TABLE)
->eq('username', $username)
->eq('disable_login_form', 0)
->eq('is_ldap_user', 0)
->findOne();
if (is_array($user) && password_verify($password, $user['password'])) {
$this->userSession->initialize($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
use Kanboard\Core\Security\SessionCheckProviderInterface;
use Kanboard\Model\User;
use Kanboard\User\DatabaseUserProvider;
/**
* Database Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterface, SessionCheckProviderInterface
{
/**
* User properties
*
* @access private
* @var array
*/
private $userInfo = array();
/**
* Username
*
* @access private
* @var string
*/
private $username = '';
/**
* Password
*
* @access private
* @var string
*/
private $password = '';
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'Database';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$user = $this->db
->table(User::TABLE)
->columns('id', 'password')
->eq('username', $this->username)
->eq('disable_login_form', 0)
->eq('is_ldap_user', 0)
->findOne();
if (! empty($user) && password_verify($this->password, $user['password'])) {
$this->userInfo = $user;
return true;
}
return false;
}
/**
* Check if the user session is valid
*
* @access public
* @return boolean
*/
public function isValidSession()
{
return $this->user->exists($this->userSession->getId());
}
/**
* Get user object
*
* @access public
* @return \Kanboard\User\DatabaseUserProvider
*/
public function getUser()
{
if (empty($this->userInfo)) {
return null;
}
return new DatabaseUserProvider($this->userInfo);
}
/**
* Set username
*
* @access public
* @param string $username
*/
public function setUsername($username)
{
$this->username = $username;
}
/**
* Set password
*
* @access public
* @param string $password
*/
public function setPassword($password)
{
$this->password = $password;
}
}

View file

@ -1,123 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Event\AuthEvent;
/**
* Github backend
*
* @package auth
*/
class Github extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Github';
/**
* OAuth2 instance
*
* @access private
* @var \Kanboard\Core\OAuth2
*/
private $service;
/**
* Authenticate a Github user
*
* @access public
* @param string $github_id Github user id
* @return boolean
*/
public function authenticate($github_id)
{
$user = $this->user->getByGithubId($github_id);
if (! empty($user)) {
$this->userSession->initialize($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Unlink a Github account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
return $this->user->update(array(
'id' => $user_id,
'github_id' => '',
));
}
/**
* Update the user table based on the Github profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile Github profile
* @return boolean
*/
public function updateUser($user_id, array $profile)
{
$user = $this->user->getById($user_id);
return $this->user->update(array(
'id' => $user_id,
'github_id' => $profile['id'],
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
));
}
/**
* Get OAuth2 configured service
*
* @access public
* @return Kanboard\Core\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
$this->helper->url->to('oauth', 'github', array(), '', true),
GITHUB_OAUTH_AUTHORIZE_URL,
GITHUB_OAUTH_TOKEN_URL,
array()
);
}
return $this->service;
}
/**
* Get Github profile
*
* @access public
* @param string $code
* @return array
*/
public function getProfile($code)
{
$this->getService()->getAccessToken($code);
return $this->httpClient->getJson(
GITHUB_API_URL.'user',
array($this->getService()->getAuthorizationHeader())
);
}
}

View file

@ -0,0 +1,143 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
use Kanboard\User\GithubUserProvider;
/**
* Github Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class GithubAuth extends Base implements OAuthAuthenticationProviderInterface
{
/**
* User properties
*
* @access private
* @var \Kanboard\User\GithubUserProvider
*/
private $userInfo = null;
/**
* OAuth2 instance
*
* @access private
* @var \Kanboard\Core\Http\OAuth2
*/
private $service;
/**
* OAuth2 code
*
* @access private
* @var string
*/
private $code = '';
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'Github';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$profile = $this->getProfile();
if (! empty($profile)) {
$this->userInfo = new GithubUserProvider($profile);
return true;
}
return false;
}
/**
* Set Code
*
* @access public
* @param string $code
* @return GithubAuth
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get user object
*
* @access public
* @return GithubUserProvider
*/
public function getUser()
{
return $this->userInfo;
}
/**
* Get configured OAuth2 service
*
* @access public
* @return \Kanboard\Core\Http\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
$this->helper->url->to('oauth', 'github', array(), '', true),
GITHUB_OAUTH_AUTHORIZE_URL,
GITHUB_OAUTH_TOKEN_URL,
array()
);
}
return $this->service;
}
/**
* Get Github profile
*
* @access public
* @return array
*/
public function getProfile()
{
$this->getService()->getAccessToken($this->code);
return $this->httpClient->getJson(
GITHUB_API_URL.'user',
array($this->getService()->getAuthorizationHeader())
);
}
/**
* Unlink user
*
* @access public
* @param integer $userId
* @return bool
*/
public function unlink($userId)
{
return $this->user->update(array('id' => $userId, 'github_id' => ''));
}
}

View file

@ -1,123 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Event\AuthEvent;
/**
* Gitlab backend
*
* @package auth
*/
class Gitlab extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Gitlab';
/**
* OAuth2 instance
*
* @access private
* @var \Kanboard\Core\OAuth2
*/
private $service;
/**
* Authenticate a Gitlab user
*
* @access public
* @param string $gitlab_id Gitlab user id
* @return boolean
*/
public function authenticate($gitlab_id)
{
$user = $this->user->getByGitlabId($gitlab_id);
if (! empty($user)) {
$this->userSession->initialize($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Unlink a Gitlab account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
return $this->user->update(array(
'id' => $user_id,
'gitlab_id' => '',
));
}
/**
* Update the user table based on the Gitlab profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile Gitlab profile
* @return boolean
*/
public function updateUser($user_id, array $profile)
{
$user = $this->user->getById($user_id);
return $this->user->update(array(
'id' => $user_id,
'gitlab_id' => $profile['id'],
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
));
}
/**
* Get OAuth2 configured service
*
* @access public
* @return Kanboard\Core\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GITLAB_CLIENT_ID,
GITLAB_CLIENT_SECRET,
$this->helper->url->to('oauth', 'gitlab', array(), '', true),
GITLAB_OAUTH_AUTHORIZE_URL,
GITLAB_OAUTH_TOKEN_URL,
array()
);
}
return $this->service;
}
/**
* Get Gitlab profile
*
* @access public
* @param string $code
* @return array
*/
public function getProfile($code)
{
$this->getService()->getAccessToken($code);
return $this->httpClient->getJson(
GITLAB_API_URL.'user',
array($this->getService()->getAuthorizationHeader())
);
}
}

View file

@ -0,0 +1,143 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
use Kanboard\User\GitlabUserProvider;
/**
* Gitlab Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class GitlabAuth extends Base implements OAuthAuthenticationProviderInterface
{
/**
* User properties
*
* @access private
* @var \Kanboard\User\GitlabUserProvider
*/
private $userInfo = null;
/**
* OAuth2 instance
*
* @access private
* @var \Kanboard\Core\Http\OAuth2
*/
private $service;
/**
* OAuth2 code
*
* @access private
* @var string
*/
private $code = '';
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'Gitlab';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$profile = $this->getProfile();
if (! empty($profile)) {
$this->userInfo = new GitlabUserProvider($profile);
return true;
}
return false;
}
/**
* Set Code
*
* @access public
* @param string $code
* @return GitlabAuth
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get user object
*
* @access public
* @return GitlabUserProvider
*/
public function getUser()
{
return $this->userInfo;
}
/**
* Get configured OAuth2 service
*
* @access public
* @return \Kanboard\Core\Http\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GITLAB_CLIENT_ID,
GITLAB_CLIENT_SECRET,
$this->helper->url->to('oauth', 'gitlab', array(), '', true),
GITLAB_OAUTH_AUTHORIZE_URL,
GITLAB_OAUTH_TOKEN_URL,
array()
);
}
return $this->service;
}
/**
* Get Gitlab profile
*
* @access public
* @return array
*/
public function getProfile()
{
$this->getService()->getAccessToken($this->code);
return $this->httpClient->getJson(
GITLAB_API_URL.'user',
array($this->getService()->getAuthorizationHeader())
);
}
/**
* Unlink user
*
* @access public
* @param integer $userId
* @return bool
*/
public function unlink($userId)
{
return $this->user->update(array('id' => $userId, 'gitlab_id' => ''));
}
}

View file

@ -1,124 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Event\AuthEvent;
/**
* Google backend
*
* @package auth
* @author Frederic Guillot
*/
class Google extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Google';
/**
* OAuth2 instance
*
* @access private
* @var \Kanboard\Core\OAuth2
*/
private $service;
/**
* Authenticate a Google user
*
* @access public
* @param string $google_id Google unique id
* @return boolean
*/
public function authenticate($google_id)
{
$user = $this->user->getByGoogleId($google_id);
if (! empty($user)) {
$this->userSession->initialize($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Unlink a Google account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
return $this->user->update(array(
'id' => $user_id,
'google_id' => '',
));
}
/**
* Update the user table based on the Google profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile Google profile
* @return boolean
*/
public function updateUser($user_id, array $profile)
{
$user = $this->user->getById($user_id);
return $this->user->update(array(
'id' => $user_id,
'google_id' => $profile['id'],
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
));
}
/**
* Get OAuth2 configured service
*
* @access public
* @return KanboardCore\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
$this->helper->url->to('oauth', 'google', array(), '', true),
'https://accounts.google.com/o/oauth2/auth',
'https://accounts.google.com/o/oauth2/token',
array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile')
);
}
return $this->service;
}
/**
* Get Google profile
*
* @access public
* @param string $code
* @return array
*/
public function getProfile($code)
{
$this->getService()->getAccessToken($code);
return $this->httpClient->getJson(
'https://www.googleapis.com/oauth2/v1/userinfo',
array($this->getService()->getAuthorizationHeader())
);
}
}

View file

@ -0,0 +1,143 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
use Kanboard\User\GoogleUserProvider;
/**
* Google Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class GoogleAuth extends Base implements OAuthAuthenticationProviderInterface
{
/**
* User properties
*
* @access private
* @var \Kanboard\User\GoogleUserProvider
*/
private $userInfo = null;
/**
* OAuth2 instance
*
* @access private
* @var \Kanboard\Core\Http\OAuth2
*/
private $service;
/**
* OAuth2 code
*
* @access private
* @var string
*/
private $code = '';
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'Google';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$profile = $this->getProfile();
if (! empty($profile)) {
$this->userInfo = new GoogleUserProvider($profile);
return true;
}
return false;
}
/**
* Set Code
*
* @access public
* @param string $code
* @return GoogleAuth
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get user object
*
* @access public
* @return GoogleUserProvider
*/
public function getUser()
{
return $this->userInfo;
}
/**
* Get configured OAuth2 service
*
* @access public
* @return \Kanboard\Core\Http\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
$this->helper->url->to('oauth', 'google', array(), '', true),
'https://accounts.google.com/o/oauth2/auth',
'https://accounts.google.com/o/oauth2/token',
array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile')
);
}
return $this->service;
}
/**
* Get Google profile
*
* @access public
* @return array
*/
public function getProfile()
{
$this->getService()->getAccessToken($this->code);
return $this->httpClient->getJson(
'https://www.googleapis.com/oauth2/v1/userinfo',
array($this->getService()->getAuthorizationHeader())
);
}
/**
* Unlink user
*
* @access public
* @param integer $userId
* @return bool
*/
public function unlink($userId)
{
return $this->user->update(array('id' => $userId, 'google_id' => ''));
}
}

View file

@ -1,521 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Event\AuthEvent;
/**
* LDAP model
*
* @package auth
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'LDAP';
/**
* Get LDAP server name
*
* @access public
* @return string
*/
public function getLdapServer()
{
return LDAP_SERVER;
}
/**
* Get LDAP bind type
*
* @access public
* @return integer
*/
public function getLdapBindType()
{
return LDAP_BIND_TYPE;
}
/**
* Get LDAP server port
*
* @access public
* @return integer
*/
public function getLdapPort()
{
return LDAP_PORT;
}
/**
* 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;
}
/**
* Get LDAP Base DN
*
* @access public
* @return string
*/
public function getLdapBaseDn()
{
return LDAP_ACCOUNT_BASE;
}
/**
* Get LDAP account id attribute
*
* @access public
* @return string
*/
public function getLdapAccountId()
{
return LDAP_ACCOUNT_ID;
}
/**
* Get LDAP account email attribute
*
* @access public
* @return string
*/
public function getLdapAccountEmail()
{
return LDAP_ACCOUNT_EMAIL;
}
/**
* Get LDAP account name attribute
*
* @access public
* @return string
*/
public function getLdapAccountName()
{
return LDAP_ACCOUNT_FULLNAME;
}
/**
* Get LDAP account memberof attribute
*
* @access public
* @return string
*/
public function getLdapAccountMemberOf()
{
return LDAP_ACCOUNT_MEMBEROF;
}
/**
* Get LDAP admin group DN
*
* @access public
* @return string
*/
public function getLdapGroupAdmin()
{
return LDAP_GROUP_ADMIN_DN;
}
/**
* Get LDAP project admin group DN
*
* @access public
* @return string
*/
public function getLdapGroupProjectAdmin()
{
return LDAP_GROUP_PROJECT_ADMIN_DN;
}
/**
* Get LDAP username pattern
*
* @access public
* @param string $username
* @return string
*/
public function getLdapUserPattern($username)
{
return sprintf(LDAP_USER_PATTERN, $username);
}
/**
* Return true if the LDAP username is case sensitive
*
* @access public
* @return boolean
*/
public function isLdapAccountCaseSensitive()
{
return LDAP_USERNAME_CASE_SENSITIVE;
}
/**
* Return true if the automatic account creation is enabled
*
* @access public
* @return boolean
*/
public function isLdapAccountCreationEnabled()
{
return LDAP_ACCOUNT_CREATION;
}
/**
* Ge the list of attributes to fetch when reading the LDAP user 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 getProfileAttributes()
{
return array_values(array_filter(array(
$this->getLdapAccountId(),
$this->getLdapAccountName(),
$this->getLdapAccountEmail(),
$this->getLdapAccountMemberOf()
)));
}
/**
* Authenticate the user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
$username = $this->isLdapAccountCaseSensitive() ? $username : strtolower($username);
$result = $this->findUser($username, $password);
if (is_array($result)) {
$user = $this->user->getByUsername($username);
if (! empty($user)) {
// There is already a local user with that name
if ($user['is_ldap_user'] == 0) {
return false;
}
} else {
// We create automatically a new user
if ($this->isLdapAccountCreationEnabled() && $this->user->create($result) !== false) {
$user = $this->user->getByUsername($username);
} else {
return false;
}
}
// We open the session
$this->userSession->initialize($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Find the user from the LDAP server
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean|array
*/
public function findUser($username, $password)
{
$ldap = $this->connect();
if ($ldap !== false && $this->bind($ldap, $username, $password)) {
return $this->getProfile($ldap, $username, $password);
}
return false;
}
/**
* LDAP connection
*
* @access public
* @return resource|boolean
*/
public function connect()
{
if (! function_exists('ldap_connect')) {
$this->logger->error('LDAP: The PHP LDAP extension is required');
return false;
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect($this->getLdapServer(), $this->getLdapPort());
if ($ldap === false) {
$this->logger->error('LDAP: Unable to connect to the LDAP server');
return false;
}
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);
if (LDAP_START_TLS && ! @ldap_start_tls($ldap)) {
$this->logger->error('LDAP: Unable to use ldap_start_tls()');
return false;
}
return $ldap;
}
/**
* LDAP authentication
*
* @access public
* @param resource $ldap
* @param string $username
* @param string $password
* @return boolean
*/
public function bind($ldap, $username, $password)
{
if ($this->getLdapBindType() === 'user') {
$ldap_username = sprintf($this->getLdapUsername(), $username);
$ldap_password = $password;
} elseif ($this->getLdapBindType() === 'proxy') {
$ldap_username = $this->getLdapUsername();
$ldap_password = $this->getLdapPassword();
} else {
$ldap_username = null;
$ldap_password = null;
}
if (! @ldap_bind($ldap, $ldap_username, $ldap_password)) {
$this->logger->error('LDAP: Unable to bind to server with: '.$ldap_username);
$this->logger->error('LDAP: bind type='.$this->getLdapBindType());
return false;
}
return true;
}
/**
* Get LDAP user profile
*
* @access public
* @param resource $ldap
* @param string $username
* @param string $password
* @return boolean|array
*/
public function getProfile($ldap, $username, $password)
{
$user_pattern = $this->getLdapUserPattern($username);
$entries = $this->executeQuery($ldap, $user_pattern);
if ($entries === false) {
$this->logger->error('LDAP: Unable to get user profile: '.$user_pattern);
return false;
}
if (@ldap_bind($ldap, $entries[0]['dn'], $password)) {
return $this->prepareProfile($ldap, $entries, $username);
}
if (DEBUG) {
$this->logger->debug('LDAP: wrong password for '.$entries[0]['dn']);
}
return false;
}
/**
* Build user profile from LDAP information
*
* @access public
* @param resource $ldap
* @param array $entries
* @param string $username
* @return boolean|array
*/
public function prepareProfile($ldap, array $entries, $username)
{
if ($this->getLdapAccountId() !== '') {
$username = $this->getEntry($entries, $this->getLdapAccountId(), $username);
}
return array(
'username' => $username,
'name' => $this->getEntry($entries, $this->getLdapAccountName()),
'email' => $this->getEntry($entries, $this->getLdapAccountEmail()),
'is_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupAdmin()),
'is_project_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupProjectAdmin()),
'is_ldap_user' => 1,
);
}
/**
* 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;
}
/**
* Retrieve info on LDAP user by username or email
*
* @access public
* @param string $username
* @param string $email
* @return boolean|array
*/
public function lookup($username = null, $email = null)
{
$query = $this->getLookupQuery($username, $email);
if ($query === '') {
return false;
}
// Connect and attempt anonymous or proxy binding
$ldap = $this->connect();
if ($ldap === false || ! $this->bind($ldap, null, null)) {
return false;
}
// Try to find user
$entries = $this->executeQuery($ldap, $query);
if ($entries === false) {
return false;
}
// User id not retrieved: LDAP_ACCOUNT_ID not properly configured
if (empty($username) && ! isset($entries[0][$this->getLdapAccountId()][0])) {
return false;
}
return $this->prepareProfile($ldap, $entries, $username);
}
/**
* Execute LDAP query
*
* @access private
* @param resource $ldap
* @param string $query
* @return boolean|array
*/
private function executeQuery($ldap, $query)
{
$sr = @ldap_search($ldap, $this->getLdapBaseDn(), $query, $this->getProfileAttributes());
if ($sr === false) {
return false;
}
$entries = ldap_get_entries($ldap, $sr);
if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
return false;
}
return $entries;
}
/**
* Get the LDAP query to find a user
*
* @access private
* @param string $username
* @param string $email
* @return string
*/
private function getLookupQuery($username, $email)
{
if (! empty($username) && ! empty($email)) {
return '(&('.$this->getLdapUserPattern($username).')('.$this->getLdapAccountEmail().'='.$email.'))';
} elseif (! empty($username)) {
return $this->getLdapUserPattern($username);
} elseif (! empty($email)) {
return '('.$this->getLdapAccountEmail().'='.$email.')';
}
return '';
}
/**
* Return one entry from a list of entries
*
* @access private
* @param array $entries LDAP entries
* @param string $key Key
* @param string $default Default value if key not set in entry
* @return string
*/
private function getEntry(array $entries, $key, $default = '')
{
return isset($entries[0][$key][0]) ? $entries[0][$key][0] : $default;
}
/**
* Return subset of entries
*
* @access private
* @param array $entries
* @param string $key
* @param array $default
* @return array
*/
private function getEntries(array $entries, $key, $default = array())
{
return isset($entries[0][$key]) ? $entries[0][$key] : $default;
}
}

View file

@ -0,0 +1,172 @@
<?php
namespace Kanboard\Auth;
use LogicException;
use Kanboard\Core\Base;
use Kanboard\Core\Ldap\Client as LdapClient;
use Kanboard\Core\Ldap\ClientException as LdapException;
use Kanboard\Core\Ldap\User as LdapUser;
use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
/**
* LDAP Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
{
/**
* User properties
*
* @access private
* @var \Kanboard\User\LdapUserProvider
*/
private $userInfo = null;
/**
* Username
*
* @access private
* @var string
*/
private $username = '';
/**
* Password
*
* @access private
* @var string
*/
private $password = '';
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'LDAP';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
try {
$client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
$user = LdapUser::getUser($client, $this->username);
if ($user === null) {
$this->logger->info('User not found in LDAP server');
return false;
}
if ($user->getUsername() === '') {
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
if ($client->authenticate($user->getDn(), $this->password)) {
$this->userInfo = $user;
return true;
}
} catch (LdapException $e) {
$this->logger->error($e->getMessage());
}
return false;
}
/**
* Get user object
*
* @access public
* @return \Kanboard\User\LdapUserProvider
*/
public function getUser()
{
return $this->userInfo;
}
/**
* Set username
*
* @access public
* @param string $username
*/
public function setUsername($username)
{
$this->username = $username;
}
/**
* Set password
*
* @access public
* @param string $password
*/
public function setPassword($password)
{
$this->password = $password;
}
/**
* Get LDAP username (proxy auth)
*
* @access public
* @return string
*/
public function getLdapUsername()
{
switch ($this->getLdapBindType()) {
case 'proxy':
return LDAP_USERNAME;
case 'user':
return sprintf(LDAP_USERNAME, $this->username);
default:
return null;
}
}
/**
* Get LDAP password (proxy auth)
*
* @access public
* @return string
*/
public function getLdapPassword()
{
switch ($this->getLdapBindType()) {
case 'proxy':
return LDAP_PASSWORD;
case 'user':
return $this->password;
default:
return null;
}
}
/**
* Get LDAP bind type
*
* @access public
* @return integer
*/
public function getLdapBindType()
{
if (LDAP_BIND_TYPE !== 'user' && LDAP_BIND_TYPE !== 'proxy' && LDAP_BIND_TYPE !== 'anonymous') {
throw new LogicException('Wrong value for the parameter LDAP_BIND_TYPE');
}
return LDAP_BIND_TYPE;
}
}

View file

@ -1,323 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Http\Request;
use Kanboard\Event\AuthEvent;
use Kanboard\Core\Security\Token;
/**
* RememberMe model
*
* @package auth
* @author Frederic Guillot
*/
class RememberMe extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'RememberMe';
/**
* SQL table name
*
* @var string
*/
const TABLE = 'remember_me';
/**
* Cookie name
*
* @var string
*/
const COOKIE_NAME = '__R';
/**
* Expiration (60 days)
*
* @var integer
*/
const EXPIRATION = 5184000;
/**
* Get a remember me record
*
* @access public
* @param $token
* @param $sequence
* @return mixed
*/
public function find($token, $sequence)
{
return $this->db
->table(self::TABLE)
->eq('token', $token)
->eq('sequence', $sequence)
->gt('expiration', time())
->findOne();
}
/**
* Get all sessions for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getAll($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->desc('date_creation')
->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
->findAll();
}
/**
* Authenticate the user with the cookie
*
* @access public
* @return bool
*/
public function authenticate()
{
$credentials = $this->readCookie();
if ($credentials !== false) {
$record = $this->find($credentials['token'], $credentials['sequence']);
if ($record) {
// Update the sequence
$this->writeCookie(
$record['token'],
$this->update($record['token']),
$record['expiration']
);
// Create the session
$this->userSession->initialize($this->user->getById($record['user_id']));
// Do not ask 2FA for remember me session
$this->sessionStorage->postAuth['validated'] = true;
$this->container['dispatcher']->dispatch(
'auth.success',
new AuthEvent(self::AUTH_NAME, $this->userSession->getId())
);
return true;
}
}
return false;
}
/**
* Remove a session record
*
* @access public
* @param integer $session_id Session id
* @return mixed
*/
public function remove($session_id)
{
return $this->db
->table(self::TABLE)
->eq('id', $session_id)
->remove();
}
/**
* Remove the current RememberMe session and the cookie
*
* @access public
* @param integer $user_id User id
*/
public function destroy($user_id)
{
$credentials = $this->readCookie();
if ($credentials !== false) {
$this->deleteCookie();
$this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->eq('token', $credentials['token'])
->remove();
}
}
/**
* Create a new RememberMe session
*
* @access public
* @param integer $user_id User id
* @param string $ip IP Address
* @param string $user_agent User Agent
* @return array
*/
public function create($user_id, $ip, $user_agent)
{
$token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken());
$sequence = Token::getToken();
$expiration = time() + self::EXPIRATION;
$this->cleanup($user_id);
$this
->db
->table(self::TABLE)
->insert(array(
'user_id' => $user_id,
'ip' => $ip,
'user_agent' => $user_agent,
'token' => $token,
'sequence' => $sequence,
'expiration' => $expiration,
'date_creation' => time(),
));
return array(
'token' => $token,
'sequence' => $sequence,
'expiration' => $expiration,
);
}
/**
* Remove old sessions for a given user
*
* @access public
* @param integer $user_id User id
* @return bool
*/
public function cleanup($user_id)
{
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
->lt('expiration', time())
->remove();
}
/**
* Return a new sequence token and update the database
*
* @access public
* @param string $token Session token
* @return string
*/
public function update($token)
{
$new_sequence = Token::getToken();
$this->db
->table(self::TABLE)
->eq('token', $token)
->update(array('sequence' => $new_sequence));
return $new_sequence;
}
/**
* Encode the cookie
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @return string
*/
public function encodeCookie($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 decodeCookie($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 ! empty($_COOKIE[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
*/
public function writeCookie($token, $sequence, $expiration)
{
setcookie(
self::COOKIE_NAME,
$this->encodeCookie($token, $sequence),
$expiration,
$this->helper->url->dir(),
null,
Request::isHTTPS(),
true
);
}
/**
* Read and decode the cookie
*
* @access public
* @return mixed
*/
public function readCookie()
{
if (empty($_COOKIE[self::COOKIE_NAME])) {
return false;
}
return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]);
}
/**
* Remove the cookie
*
* @access public
*/
public function deleteCookie()
{
setcookie(
self::COOKIE_NAME,
'',
time() - 3600,
$this->helper->url->dir(),
null,
Request::isHTTPS(),
true
);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Security\PreAuthenticationProviderInterface;
use Kanboard\User\DatabaseUserProvider;
/**
* Rember Me Cookie Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class RememberMeAuth extends Base implements PreAuthenticationProviderInterface
{
/**
* User properties
*
* @access private
* @var array
*/
private $userInfo = array();
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'RememberMe';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$credentials = $this->rememberMeCookie->read();
if ($credentials !== false) {
$session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']);
if (! empty($session)) {
$this->rememberMeCookie->write(
$session['token'],
$this->rememberMeSession->updateSequence($session['token']),
$session['expiration']
);
$this->userInfo = $this->user->getById($session['user_id']);
return true;
}
}
return false;
}
/**
* Get user object
*
* @access public
* @return DatabaseUserProvider
*/
public function getUser()
{
if (empty($this->userInfo)) {
return null;
}
return new DatabaseUserProvider($this->userInfo);
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Event\AuthEvent;
/**
* ReverseProxy backend
*
* @package auth
* @author Sylvain Veyrié
*/
class ReverseProxy extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'ReverseProxy';
/**
* Get username from the reverse proxy
*
* @access public
* @return string
*/
public function getUsername()
{
return isset($_SERVER[REVERSE_PROXY_USER_HEADER]) ? $_SERVER[REVERSE_PROXY_USER_HEADER] : '';
}
/**
* Authenticate the user with the HTTP header
*
* @access public
* @return bool
*/
public function authenticate()
{
if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) {
$login = $_SERVER[REVERSE_PROXY_USER_HEADER];
$user = $this->user->getByUsername($login);
if (empty($user)) {
$this->createUser($login);
$user = $this->user->getByUsername($login);
}
$this->userSession->initialize($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Create automatically a new local user after the authentication
*
* @access private
* @param string $login Username
* @return bool
*/
private function createUser($login)
{
$email = strpos($login, '@') !== false ? $login : '';
if (REVERSE_PROXY_DEFAULT_DOMAIN !== '' && empty($email)) {
$email = $login.'@'.REVERSE_PROXY_DEFAULT_DOMAIN;
}
return $this->user->create(array(
'email' => $email,
'username' => $login,
'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login,
'is_ldap_user' => 1,
'disable_login_form' => 1,
));
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Kanboard\Auth;
use Kanboard\Core\Base;
use Kanboard\Core\Security\PreAuthenticationProviderInterface;
use Kanboard\Core\Security\SessionCheckProviderInterface;
use Kanboard\User\ReverseProxyUserProvider;
/**
* Reverse-Proxy Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterface, SessionCheckProviderInterface
{
/**
* User properties
*
* @access protected
* @var \Kanboard\User\ReverseProxyUserProvider
*/
protected $userInfo = null;
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'ReverseProxy';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$username = $this->request->getRemoteUser();
if (! empty($username)) {
$this->userInfo = new ReverseProxyUserProvider($username);
return true;
}
return false;
}
/**
* Check if the user session is valid
*
* @access public
* @return boolean
*/
public function isValidSession()
{
return $this->request->getRemoteUser() === $this->userSession->getUsername();
}
/**
* Get user object
*
* @access public
* @return ReverseProxyUserProvider
*/
public function getUser()
{
return $this->userInfo;
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Kanboard\Auth;
use Otp\Otp;
use Otp\GoogleAuthenticator;
use Base32\Base32;
use Kanboard\Core\Base;
use Kanboard\Core\Security\PostAuthenticationProviderInterface;
/**
* TOTP Authentication Provider
*
* @package auth
* @author Frederic Guillot
*/
class TotpAuth extends Base implements PostAuthenticationProviderInterface
{
/**
* User pin code
*
* @access private
* @var string
*/
private $code = '';
/**
* Private key
*
* @access private
* @var string
*/
private $secret = '';
/**
* Get authentication provider name
*
* @access public
* @return string
*/
public function getName()
{
return 'Time-based One-time Password Algorithm';
}
/**
* Authenticate the user
*
* @access public
* @return boolean
*/
public function authenticate()
{
$otp = new Otp;
return $otp->checkTotp(Base32::decode($this->secret), $this->code);
}
/**
* Set validation code
*
* @access public
* @param string $code
*/
public function setCode($code)
{
$this->code = $code;
}
/**
* Set secret token
*
* @access public
* @param string $secret
*/
public function setSecret($secret)
{
$this->secret = $secret;
}
/**
* Get secret token
*
* @access public
* @return string
*/
public function getSecret()
{
if (empty($this->secret)) {
$this->secret = GoogleAuthenticator::generateRandom();
}
return $this->secret;
}
/**
* Get QR code url
*
* @access public
* @param string $label
* @return string
*/
public function getQrCodeUrl($label)
{
if (empty($this->secret)) {
return '';
}
return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret);
}
/**
* Get key url (empty if no url can be provided)
*
* @access public
* @param string $label
* @return string
*/
public function getKeyUrl($label)
{
if (empty($this->secret)) {
return '';
}
return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret);
}
}

View file

@ -27,7 +27,7 @@ class Action extends Base
'available_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id']),
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'projects_list' => $this->project->getList(false),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
@ -86,7 +86,7 @@ class Action extends Base
'values' => $values,
'action_params' => $action_params,
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id']),
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'projects_list' => $projects_list,
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),

View file

@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject();
$this->response->html($this->template->layout('activity/project', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])

View file

@ -20,7 +20,7 @@ class Analytic extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('analytic/layout', $params);
@ -132,6 +132,9 @@ class Analytic extends Base
* Common method for CFD and Burdown chart
*
* @access private
* @param string $template
* @param string $column
* @param string $title
*/
private function commonAggregateMetrics($template, $column, $title)
{

View file

@ -22,7 +22,7 @@ class App extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('app/layout', $params);
@ -42,7 +42,7 @@ class App extends Base
->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id))
->setMax($max)
->setOrder('name')
->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id)))
->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id)))
->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
}
@ -169,7 +169,7 @@ class App extends Base
$this->response->html($this->layout('app/activity', array(
'title' => t('My activity stream'),
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($user['id']), 100),
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
'user' => $user,
)));
}
@ -202,49 +202,4 @@ class App extends Base
'user' => $user,
)));
}
/**
* Render Markdown text and reply with the HTML Code
*
* @access public
*/
public function preview()
{
$payload = $this->request->getJson();
if (empty($payload['text'])) {
$this->response->html('<p>'.t('Nothing to preview...').'</p>');
}
$this->response->html($this->helper->text->markdown($payload['text']));
}
/**
* Task autocompletion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$projects = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
if (empty($projects)) {
$this->response->json(array());
}
$filter = $this->taskFilterAutoCompleteFormatter
->create()
->filterByProjects($projects)
->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
// Search by task id or by title
if (ctype_digit($search)) {
$filter->filterById($search);
} else {
$filter->filterByTitle($search);
}
$this->response->json($filter->format());
}
}

View file

@ -24,7 +24,7 @@ class Auth extends Base
}
$this->response->html($this->template->layout('auth/index', array(
'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']),
'errors' => $errors,
'values' => $values,
'no_layout' => true,
@ -40,18 +40,11 @@ class Auth extends Base
public function check()
{
$values = $this->request->getValues();
$this->sessionStorage->hasRememberMe = ! empty($values['remember_me']);
list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) {
if (isset($this->sessionStorage->redirectAfterLogin)
&& ! empty($this->sessionStorage->redirectAfterLogin)
&& ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
$redirect = $this->sessionStorage->redirectAfterLogin;
unset($this->sessionStorage->redirectAfterLogin);
$this->response->redirect($redirect);
}
$this->response->redirect($this->helper->url->to('app', 'index'));
$this->redirectAfterLogin();
}
$this->login($values, $errors);
@ -64,7 +57,6 @@ class Auth extends Base
*/
public function logout()
{
$this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
$this->sessionManager->close();
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
@ -83,4 +75,20 @@ class Auth extends Base
$this->sessionStorage->captcha = $builder->getPhrase();
$builder->output();
}
/**
* Redirect the user after the authentication
*
* @access private
*/
private function redirectAfterLogin()
{
if (isset($this->sessionStorage->redirectAfterLogin) && ! empty($this->sessionStorage->redirectAfterLogin) && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
$redirect = $this->sessionStorage->redirectAfterLogin;
unset($this->sessionStorage->redirectAfterLogin);
$this->response->redirect($redirect);
}
$this->response->redirect($this->helper->url->to('app', 'index'));
}
}

View file

@ -2,8 +2,7 @@
namespace Kanboard\Controller;
use Pimple\Container;
use Symfony\Component\EventDispatcher\Event;
use Kanboard\Core\Security\Role;
/**
* Base controller
@ -14,36 +13,22 @@ use Symfony\Component\EventDispatcher\Event;
abstract class Base extends \Kanboard\Core\Base
{
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
if (DEBUG) {
$this->logger->debug('START_REQUEST='.$_SERVER['REQUEST_URI']);
}
}
/**
* Destructor
* Method executed before each action
*
* @access public
*/
public function __destruct()
public function beforeAction($controller, $action)
{
if (DEBUG) {
foreach ($this->db->getLogMessages() as $message) {
$this->logger->debug($message);
}
$this->sessionManager->open();
$this->dispatcher->dispatch('app.bootstrap');
$this->sendHeaders($action);
$this->authenticationManager->checkCurrentSession();
$this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries));
$this->logger->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT']));
$this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage()));
$this->logger->debug('END_REQUEST='.$_SERVER['REQUEST_URI']);
if (! $this->applicationAuthorization->isAllowed($controller, $action, Role::APP_PUBLIC)) {
$this->handleAuthentication();
$this->handlePostAuthentication($controller, $action);
$this->checkApplicationAuthorization($controller, $action);
$this->checkProjectAuthorization($controller, $action);
}
}
@ -69,34 +54,14 @@ abstract class Base extends \Kanboard\Core\Base
}
}
/**
* Method executed before each action
*
* @access public
*/
public function beforeAction($controller, $action)
{
$this->sessionManager->open();
$this->sendHeaders($action);
$this->container['dispatcher']->dispatch('session.bootstrap', new Event);
if (! $this->acl->isPublicAction($controller, $action)) {
$this->handleAuthentication();
$this->handle2FA($controller, $action);
$this->handleAuthorization($controller, $action);
$this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
}
}
/**
* Check authentication
*
* @access public
* @access private
*/
public function handleAuthentication()
private function handleAuthentication()
{
if (! $this->authentication->isAuthenticated()) {
if (! $this->userSession->isLogged() && ! $this->authenticationManager->preAuthentication()) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
@ -107,15 +72,15 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
* Check 2FA
* Handle Post-Authentication (2FA)
*
* @access public
* @access private
*/
public function handle2FA($controller, $action)
private function handlePostAuthentication($controller, $action)
{
$ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout');
if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) {
if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
@ -125,11 +90,23 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
* Check page access and authorization
* Check application authorization
*
* @access public
* @access private
*/
public function handleAuthorization($controller, $action)
private function checkApplicationAuthorization($controller, $action)
{
if (! $this->helper->user->hasAccess($controller, $action)) {
$this->forbidden();
}
}
/**
* Check project authorization
*
* @access private
*/
private function checkProjectAuthorization($controller, $action)
{
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
@ -139,7 +116,7 @@ abstract class Base extends \Kanboard\Core\Base
$project_id = $this->taskFinder->getProjectId($task_id);
}
if (! $this->acl->isAllowed($controller, $action, $project_id)) {
if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($controller, $action, $project_id)) {
$this->forbidden();
}
}
@ -147,10 +124,10 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Application not found page (404 error)
*
* @access public
* @access protected
* @param boolean $no_layout Display the layout or not
*/
public function notfound($no_layout = false)
protected function notfound($no_layout = false)
{
$this->response->html($this->template->layout('app/notfound', array(
'title' => t('Page not found'),
@ -161,11 +138,15 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Application forbidden page
*
* @access public
* @access protected
* @param boolean $no_layout Display the layout or not
*/
public function forbidden($no_layout = false)
protected function forbidden($no_layout = false)
{
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
$this->response->html($this->template->layout('app/forbidden', array(
'title' => t('Access Forbidden'),
'no_layout' => $no_layout,
@ -209,7 +190,7 @@ abstract class Base extends \Kanboard\Core\Base
$content = $this->template->render($template, $params);
$params['task_content_for_layout'] = $content;
$params['title'] = $params['task']['project_name'].' &gt; '.$params['task']['title'];
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
return $this->template->layout('task/layout', $params);
}
@ -227,7 +208,7 @@ abstract class Base extends \Kanboard\Core\Base
$content = $this->template->render($template, $params);
$params['project_content_for_layout'] = $content;
$params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' &gt; '.$params['title'];
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['sidebar_template'] = $sidebar_template;
return $this->template->layout('project/layout', $params);
@ -300,12 +281,15 @@ abstract class Base extends \Kanboard\Core\Base
* Common method to get project filters
*
* @access protected
* @param string $controller
* @param string $action
* @return array
*/
protected function getProjectFilters($controller, $action)
{
$project = $this->getProject();
$search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
$board_selector = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$board_selector = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
unset($board_selector[$project['id']]);
$filters = array(

View file

@ -51,7 +51,7 @@ class Board extends Base
$this->response->html($this->template->layout('board/view_private', array(
'categories_list' => $this->category->getList($params['project']['id'], false),
'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
'description' => $params['project']['description'],
@ -142,195 +142,6 @@ class Board extends Base
$this->response->html($this->renderBoard($project_id));
}
/**
* Get links on mouseover
*
* @access public
*/
public function tasklinks()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_tasklinks', array(
'links' => $this->taskLink->getAll($task['id']),
'task' => $task,
)));
}
/**
* Get subtasks on mouseover
*
* @access public
*/
public function subtasks()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_subtasks', array(
'subtasks' => $this->subtask->getAll($task['id']),
'task' => $task,
)));
}
/**
* Display all attachments during the task mouseover
*
* @access public
*/
public function attachments()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_files', array(
'files' => $this->file->getAll($task['id']),
'task' => $task,
)));
}
/**
* Display comments during a task mouseover
*
* @access public
*/
public function comments()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_comments', array(
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
)));
}
/**
* Display task description
*
* @access public
*/
public function description()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_description', array(
'task' => $task
)));
}
/**
* Change a task assignee directly from the board
*
* @access public
*/
public function changeAssignee()
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$this->response->html($this->template->render('board/popover_assignee', array(
'values' => $task,
'users_list' => $this->projectPermission->getMemberList($project['id']),
'project' => $project,
)));
}
/**
* Validate an assignee modification
*
* @access public
*/
public function updateAssignee()
{
$values = $this->request->getValues();
list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
if ($valid && $this->taskModification->update($values)) {
$this->flash->success(t('Task updated successfully.'));
} else {
$this->flash->failure(t('Unable to update your task.'));
}
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
}
/**
* Change a task category directly from the board
*
* @access public
*/
public function changeCategory()
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$this->response->html($this->template->render('board/popover_category', array(
'values' => $task,
'categories_list' => $this->category->getList($project['id']),
'project' => $project,
)));
}
/**
* Validate a category modification
*
* @access public
*/
public function updateCategory()
{
$values = $this->request->getValues();
list($valid, ) = $this->taskValidator->validateCategoryModification($values);
if ($valid && $this->taskModification->update($values)) {
$this->flash->success(t('Task updated successfully.'));
} else {
$this->flash->failure(t('Unable to update your task.'));
}
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
}
/**
* Screenshot popover
*
* @access public
*/
public function screenshot()
{
$task = $this->getTask();
$this->response->html($this->template->render('file/screenshot', array(
'task' => $task,
'redirect' => 'board',
)));
}
/**
* Get recurrence information on mouseover
*
* @access public
*/
public function recurrence()
{
$task = $this->getTask();
$this->response->html($this->template->render('task/recurring_info', array(
'task' => $task,
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
)));
}
/**
* Display swimlane description in tooltip
*
* @access public
*/
public function swimlane()
{
$this->getProject();
$swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
$this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
}
/**
* Enable collapsed mode
*
@ -355,6 +166,7 @@ class Board extends Base
* Change display mode
*
* @access private
* @param boolean $mode
*/
private function changeDisplayMode($mode)
{
@ -372,6 +184,7 @@ class Board extends Base
* Render board
*
* @access private
* @param integer $project_id
*/
private function renderBoard($project_id)
{

View file

@ -0,0 +1,101 @@
<?php
namespace Kanboard\Controller;
/**
* Board Popover
*
* @package controller
* @author Frederic Guillot
*/
class BoardPopover extends Base
{
/**
* Change a task assignee directly from the board
*
* @access public
*/
public function changeAssignee()
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$this->response->html($this->template->render('board/popover_assignee', array(
'values' => $task,
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'project' => $project,
)));
}
/**
* Validate an assignee modification
*
* @access public
*/
public function updateAssignee()
{
$values = $this->request->getValues();
list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
if ($valid && $this->taskModification->update($values)) {
$this->flash->success(t('Task updated successfully.'));
} else {
$this->flash->failure(t('Unable to update your task.'));
}
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
}
/**
* Change a task category directly from the board
*
* @access public
*/
public function changeCategory()
{
$task = $this->getTask();
$project = $this->project->getById($task['project_id']);
$this->response->html($this->template->render('board/popover_category', array(
'values' => $task,
'categories_list' => $this->category->getList($project['id']),
'project' => $project,
)));
}
/**
* Validate a category modification
*
* @access public
*/
public function updateCategory()
{
$values = $this->request->getValues();
list($valid, ) = $this->taskValidator->validateCategoryModification($values);
if ($valid && $this->taskModification->update($values)) {
$this->flash->success(t('Task updated successfully.'));
} else {
$this->flash->failure(t('Unable to update your task.'));
}
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
}
/**
* Screenshot popover
*
* @access public
*/
public function screenshot()
{
$task = $this->getTask();
$this->response->html($this->template->render('file/screenshot', array(
'task' => $task,
'redirect' => 'board',
)));
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Kanboard\Controller;
/**
* Board Tooltip
*
* @package controller
* @author Frederic Guillot
*/
class BoardTooltip extends Base
{
/**
* Get links on mouseover
*
* @access public
*/
public function tasklinks()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_tasklinks', array(
'links' => $this->taskLink->getAll($task['id']),
'task' => $task,
)));
}
/**
* Get subtasks on mouseover
*
* @access public
*/
public function subtasks()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_subtasks', array(
'subtasks' => $this->subtask->getAll($task['id']),
'task' => $task,
)));
}
/**
* Display all attachments during the task mouseover
*
* @access public
*/
public function attachments()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_files', array(
'files' => $this->file->getAll($task['id']),
'task' => $task,
)));
}
/**
* Display comments during a task mouseover
*
* @access public
*/
public function comments()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_comments', array(
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
)));
}
/**
* Display task description
*
* @access public
*/
public function description()
{
$task = $this->getTask();
$this->response->html($this->template->render('board/tooltip_description', array(
'task' => $task
)));
}
/**
* Get recurrence information on mouseover
*
* @access public
*/
public function recurrence()
{
$task = $this->getTask();
$this->response->html($this->template->render('task/recurring_info', array(
'task' => $task,
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
)));
}
/**
* Display swimlane description in tooltip
*
* @access public
*/
public function swimlane()
{
$this->getProject();
$swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
$this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
}
}

View file

@ -20,7 +20,7 @@ class Config extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['values'] = $this->config->getAll();
$params['errors'] = array();
$params['config_content_for_layout'] = $this->template->render($template, $params);

View file

@ -20,7 +20,7 @@ class Currency extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params);

View file

@ -2,6 +2,8 @@
namespace Kanboard\Controller;
use Kanboard\Core\Security\Role;
/**
* Custom Filter management
*
@ -137,7 +139,7 @@ class Customfilter extends Base
{
$user_id = $this->userSession->getId();
if ($filter['user_id'] != $user_id && (! $this->projectPermission->isManager($project['id'], $user_id) || ! $this->userSession->isAdmin())) {
if ($filter['user_id'] != $user_id && ($this->projectUserRole->getUserRole($project['id'], $user_id) === Role::PROJECT_MANAGER || ! $this->userSession->isAdmin())) {
$this->forbidden();
}
}

View file

@ -53,7 +53,7 @@ class Doc extends Base
}
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
)));
}
}

View file

@ -25,10 +25,8 @@ class Feed extends Base
$this->forbidden(true);
}
$projects = $this->projectPermission->getActiveMemberProjects($user['id']);
$this->response->xml($this->template->render('feed/user', array(
'events' => $this->projectActivity->getProjects(array_keys($projects)),
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])),
'user' => $user,
)));
}

View file

@ -20,13 +20,13 @@ class Gantt extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
$this->response->html($this->template->layout('gantt/projects', array(
'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
'title' => t('Gantt chart for all projects'),
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
)));
}
@ -66,7 +66,7 @@ class Gantt extends Base
}
$this->response->html($this->template->layout('gantt/project', $params + array(
'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'sorting' => $sorting,
'tasks' => $filter->format(),
)));
@ -109,7 +109,7 @@ class Gantt extends Base
'column_id' => $this->board->getFirstColumn($project['id']),
'position' => 1
),
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $this->swimlane->getList($project['id'], false, true),

View file

@ -0,0 +1,255 @@
<?php
namespace Kanboard\Controller;
/**
* Group Controller
*
* @package controller
* @author Frederic Guillot
*/
class Group extends Base
{
/**
* List all groups
*
* @access public
*/
public function index()
{
$paginator = $this->paginator
->setUrl('group', 'index')
->setMax(30)
->setOrder('name')
->setQuery($this->group->getQuery())
->calculate();
$this->response->html($this->template->layout('group/index', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'title' => t('Groups').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
)));
}
/**
* List all users
*
* @access public
*/
public function users()
{
$group_id = $this->request->getIntegerParam('group_id');
$group = $this->group->getById($group_id);
$paginator = $this->paginator
->setUrl('group', 'users')
->setMax(30)
->setOrder('username')
->setQuery($this->groupMember->getQuery($group_id))
->calculate();
$this->response->html($this->template->layout('group/users', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')',
'paginator' => $paginator,
'group' => $group,
)));
}
/**
* Display a form to create a new group
*
* @access public
*/
public function create(array $values = array(), array $errors = array())
{
$this->response->html($this->template->layout('group/create', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'errors' => $errors,
'values' => $values,
'title' => t('New group')
)));
}
/**
* Validate and save a new group
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->group->validateCreation($values);
if ($valid) {
if ($this->group->create($values['name']) !== false) {
$this->flash->success(t('Group created successfully.'));
$this->response->redirect($this->helper->url->to('group', 'index'));
} else {
$this->flash->failure(t('Unable to create your group.'));
}
}
$this->create($values, $errors);
}
/**
* Display a form to update a group
*
* @access public
*/
public function edit(array $values = array(), array $errors = array())
{
if (empty($values)) {
$values = $this->group->getById($this->request->getIntegerParam('group_id'));
}
$this->response->html($this->template->layout('group/edit', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'errors' => $errors,
'values' => $values,
'title' => t('Edit group')
)));
}
/**
* Validate and save a group
*
* @access public
*/
public function update()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->group->validateModification($values);
if ($valid) {
if ($this->group->update($values) !== false) {
$this->flash->success(t('Group updated successfully.'));
$this->response->redirect($this->helper->url->to('group', 'index'));
} else {
$this->flash->failure(t('Unable to update your group.'));
}
}
$this->edit($values, $errors);
}
/**
* Form to associate a user to a group
*
* @access public
*/
public function associate(array $values = array(), array $errors = array())
{
$group_id = $this->request->getIntegerParam('group_id');
$group = $this->group->getbyId($group_id);
if (empty($values)) {
$values['group_id'] = $group_id;
}
$this->response->html($this->template->layout('group/associate', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)),
'group' => $group,
'errors' => $errors,
'values' => $values,
'title' => t('Add group member to "%s"', $group['name']),
)));
}
/**
* Add user to a group
*
* @access public
*/
public function addUser()
{
$values = $this->request->getValues();
if (isset($values['group_id']) && isset($values['user_id'])) {
if ($this->groupMember->addUser($values['group_id'], $values['user_id'])) {
$this->flash->success(t('Group member added successfully.'));
$this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $values['group_id'])));
} else {
$this->flash->failure(t('Unable to add group member.'));
}
}
$this->associate($values);
}
/**
* Confirmation dialog to remove a user from a group
*
* @access public
*/
public function dissociate()
{
$group_id = $this->request->getIntegerParam('group_id');
$user_id = $this->request->getIntegerParam('user_id');
$group = $this->group->getById($group_id);
$user = $this->user->getById($user_id);
$this->response->html($this->template->layout('group/dissociate', array(
'group' => $group,
'user' => $user,
'title' => t('Remove user from group "%s"', $group['name']),
)));
}
/**
* Remove a user from a group
*
* @access public
*/
public function removeUser()
{
$this->checkCSRFParam();
$group_id = $this->request->getIntegerParam('group_id');
$user_id = $this->request->getIntegerParam('user_id');
if ($this->groupMember->removeUser($group_id, $user_id)) {
$this->flash->success(t('User removed successfully from this group.'));
} else {
$this->flash->failure(t('Unable to remove this user from the group.'));
}
$this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $group_id)));
}
/**
* Confirmation dialog to remove a group
*
* @access public
*/
public function confirm()
{
$group_id = $this->request->getIntegerParam('group_id');
$group = $this->group->getById($group_id);
$this->response->html($this->template->layout('group/remove', array(
'group' => $group,
'title' => t('Remove group'),
)));
}
/**
* Remove a group
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$group_id = $this->request->getIntegerParam('group_id');
if ($this->group->remove($group_id)) {
$this->flash->success(t('Group removed successfully.'));
} else {
$this->flash->failure(t('Unable to remove this group.'));
}
$this->response->redirect($this->helper->url->to('group', 'index'));
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Kanboard\Controller;
/**
* Group Helper
*
* @package controller
* @author Frederic Guillot
*/
class GroupHelper extends Base
{
/**
* Group autocompletion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$groups = $this->groupManager->find($search);
$this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
}
}

View file

@ -21,7 +21,7 @@ class Link extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params);

View file

@ -17,7 +17,7 @@ class Oauth extends Base
*/
public function google()
{
$this->step1('google');
$this->step1('Google');
}
/**
@ -27,7 +27,7 @@ class Oauth extends Base
*/
public function github()
{
$this->step1('github');
$this->step1('Github');
}
/**
@ -37,7 +37,7 @@ class Oauth extends Base
*/
public function gitlab()
{
$this->step1('gitlab');
$this->step1('Gitlab');
}
/**
@ -45,12 +45,12 @@ class Oauth extends Base
*
* @access public
*/
public function unlink($backend = '')
public function unlink()
{
$backend = $this->request->getStringParam('backend', $backend);
$backend = $this->request->getStringParam('backend');
$this->checkCSRFParam();
if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) {
if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) {
$this->flash->success(t('Your external account is not linked anymore to your profile.'));
} else {
$this->flash->failure(t('Unable to unlink your external account.'));
@ -63,15 +63,16 @@ class Oauth extends Base
* Redirect to the provider if no code received
*
* @access private
* @param string $provider
*/
private function step1($backend)
private function step1($provider)
{
$code = $this->request->getStringParam('code');
if (! empty($code)) {
$this->step2($backend, $code);
$this->step2($provider, $code);
} else {
$this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl());
$this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
}
@ -79,30 +80,35 @@ class Oauth extends Base
* Link or authenticate the user
*
* @access private
* @param string $provider
* @param string $code
*/
private function step2($backend, $code)
private function step2($provider, $code)
{
$profile = $this->authentication->backend($backend)->getProfile($code);
$this->authenticationManager->getProvider($provider)->setCode($code);
if ($this->userSession->isLogged()) {
$this->link($backend, $profile);
$this->link($provider);
}
$this->authenticate($backend, $profile);
$this->authenticate($provider);
}
/**
* Link the account
*
* @access private
* @param string $provider
*/
private function link($backend, $profile)
private function link($provider)
{
if (empty($profile)) {
$authProvider = $this->authenticationManager->getProvider($provider);
if (! $authProvider->authenticate()) {
$this->flash->failure(t('External authentication failed'));
} else {
$this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.'));
$this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile);
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
@ -112,10 +118,11 @@ class Oauth extends Base
* Authenticate the account
*
* @access private
* @param string $provider
*/
private function authenticate($backend, $profile)
private function authenticate($provider)
{
if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) {
if ($this->authenticationManager->oauthAuthentication($provider)) {
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
$this->response->html($this->template->layout('auth/index', array(

View file

@ -20,7 +20,7 @@ class Project extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
$nb_projects = count($project_ids);
@ -33,7 +33,7 @@ class Project extends Base
->calculate();
$this->response->html($this->template->layout('project/index', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'paginator' => $paginator,
'nb_projects' => $nb_projects,
'title' => t('Projects').' ('.$nb_projects.')'
@ -160,11 +160,11 @@ class Project extends Base
$values = $this->request->getValues();
if (isset($values['is_private'])) {
if (! $this->helper->user->isProjectAdministrationAllowed($project['id'])) {
if (! $this->helper->user->hasProjectAccess('project', 'create', $project['id'])) {
unset($values['is_private']);
}
} elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) {
if ($this->helper->user->isProjectAdministrationAllowed($project['id'])) {
if ($this->helper->user->hasProjectAccess('project', 'create', $project['id'])) {
$values += array('is_private' => 0);
}
}
@ -183,120 +183,6 @@ class Project extends Base
$this->edit($values, $errors);
}
/**
* Users list for the selected project
*
* @access public
*/
public function users()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('project/users', array(
'project' => $project,
'users' => $this->projectPermission->getAllUsers($project['id']),
'title' => t('Edit project access list')
)));
}
/**
* Allow everybody
*
* @access public
*/
public function allowEverybody()
{
$project = $this->getProject();
$values = $this->request->getValues() + array('is_everybody_allowed' => 0);
list($valid, ) = $this->projectPermission->validateProjectModification($values);
if ($valid) {
if ($this->project->update($values)) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
}
$this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $project['id'])));
}
/**
* Allow a specific user (admin only)
*
* @access public
*/
public function allow()
{
$values = $this->request->getValues();
list($valid, ) = $this->projectPermission->validateUserModification($values);
if ($valid) {
if ($this->projectPermission->addMember($values['project_id'], $values['user_id'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
}
$this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
}
/**
* Change the role of a project member
*
* @access public
*/
public function role()
{
$this->checkCSRFParam();
$values = array(
'project_id' => $this->request->getIntegerParam('project_id'),
'user_id' => $this->request->getIntegerParam('user_id'),
'is_owner' => $this->request->getIntegerParam('is_owner'),
);
list($valid, ) = $this->projectPermission->validateUserModification($values);
if ($valid) {
if ($this->projectPermission->changeRole($values['project_id'], $values['user_id'], $values['is_owner'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
}
$this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
}
/**
* Revoke user access (admin only)
*
* @access public
*/
public function revoke()
{
$this->checkCSRFParam();
$values = array(
'project_id' => $this->request->getIntegerParam('project_id'),
'user_id' => $this->request->getIntegerParam('user_id'),
);
list($valid, ) = $this->projectPermission->validateUserModification($values);
if ($valid) {
if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
}
$this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
}
/**
* Remove a project
*
@ -413,17 +299,28 @@ class Project extends Base
*/
public function create(array $values = array(), array $errors = array())
{
$is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() || $this->userSession->isProjectAdmin() ? 0 : 1);
$is_private = isset($values['is_private']) && $values['is_private'] == 1;
$this->response->html($this->template->layout('project/new', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'values' => empty($values) ? array('is_private' => $is_private) : $values,
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'values' => $values,
'errors' => $errors,
'is_private' => $is_private,
'title' => $is_private ? t('New private project') : t('New project'),
)));
}
/**
* Display a form to create a private project
*
* @access public
*/
public function createPrivate(array $values = array(), array $errors = array())
{
$values['is_private'] = 1;
$this->create($values, $errors);
}
/**
* Validate and save a new project
*

View file

@ -0,0 +1,177 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Core\Security\Role;
/**
* Project Permission
*
* @package controller
* @author Frederic Guillot
*/
class ProjectPermission extends Base
{
/**
* Show all permissions
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
$project = $this->getProject();
if (empty($values)) {
$values['role'] = Role::PROJECT_MEMBER;
}
$this->response->html($this->projectLayout('project_permission/index', array(
'project' => $project,
'users' => $this->projectUserRole->getUsers($project['id']),
'groups' => $this->projectGroupRole->getGroups($project['id']),
'roles' => $this->role->getProjectRoles(),
'values' => $values,
'errors' => $errors,
'title' => t('Project Permissions'),
)));
}
/**
* Allow everybody
*
* @access public
*/
public function allowEverybody()
{
$project = $this->getProject();
$values = $this->request->getValues() + array('is_everybody_allowed' => 0);
if ($this->project->update($values)) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
$this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
}
/**
* Add user to the project
*
* @access public
*/
public function addUser()
{
$values = $this->request->getValues();
if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
$this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
}
/**
* Revoke user access
*
* @access public
*/
public function removeUser()
{
$this->checkCSRFParam();
$values = array(
'project_id' => $this->request->getIntegerParam('project_id'),
'user_id' => $this->request->getIntegerParam('user_id'),
);
if ($this->projectUserRole->removeUser($values['project_id'], $values['user_id'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
$this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
}
/**
* Change user role
*
* @access public
*/
public function changeUserRole()
{
$project_id = $this->request->getIntegerParam('project_id');
$values = $this->request->getJson();
if (! empty($project_id) && ! empty($values) && $this->projectUserRole->changeUserRole($project_id, $values['id'], $values['role'])) {
$this->response->json(array('status' => 'ok'));
} else {
$this->response->json(array('status' => 'error'));
}
}
/**
* Add group to the project
*
* @access public
*/
public function addGroup()
{
$values = $this->request->getValues();
if (empty($values['group_id']) && ! empty($values['external_id'])) {
$values['group_id'] = $this->group->create($values['name'], $values['external_id']);
}
if ($this->projectGroupRole->addGroup($values['project_id'], $values['group_id'], $values['role'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
$this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
}
/**
* Revoke group access
*
* @access public
*/
public function removeGroup()
{
$this->checkCSRFParam();
$values = array(
'project_id' => $this->request->getIntegerParam('project_id'),
'group_id' => $this->request->getIntegerParam('group_id'),
);
if ($this->projectGroupRole->removeGroup($values['project_id'], $values['group_id'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
}
$this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
}
/**
* Change group role
*
* @access public
*/
public function changeGroupRole()
{
$project_id = $this->request->getIntegerParam('project_id');
$values = $this->request->getJson();
if (! empty($project_id) && ! empty($values) && $this->projectGroupRole->changeGroupRole($project_id, $values['id'], $values['role'])) {
$this->response->json(array('status' => 'ok'));
} else {
$this->response->json(array('status' => 'error'));
}
}
}

View file

@ -4,6 +4,7 @@ namespace Kanboard\Controller;
use Kanboard\Model\User as UserModel;
use Kanboard\Model\Task as TaskModel;
use Kanboard\Core\Security\Role;
/**
* Project User overview
@ -23,7 +24,7 @@ class Projectuser extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
$params['filter'] = array('user_id' => $params['user_id']);
@ -37,17 +38,17 @@ class Projectuser extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
return array($user_id, $project_ids, $this->user->getList(true));
}
private function role($is_owner, $action, $title, $title_user)
private function role($role, $action, $title, $title_user)
{
list($user_id, $project_ids, $users) = $this->common();
$query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats'));
$query = $this->projectPermission->getQueryByRole($project_ids, $role)->callback(array($this->project, 'applyColumnStats'));
if ($user_id !== UserModel::EVERYBODY_ID) {
$query->eq(UserModel::TABLE.'.id', $user_id);
@ -101,7 +102,7 @@ class Projectuser extends Base
*/
public function managers()
{
$this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
$this->role(Role::PROJECT_MANAGER, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
}
/**
@ -110,7 +111,7 @@ class Projectuser extends Base
*/
public function members()
{
$this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member');
$this->role(ROLE::PROJECT_MEMBER, 'members', t('People who are project members'), 'Projects where "%s" is member');
}
/**

View file

@ -12,7 +12,7 @@ class Search extends Base
{
public function index()
{
$projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$search = urldecode($this->request->getStringParam('search'));
$nb_tasks = 0;

View file

@ -48,7 +48,7 @@ class Subtask extends Base
$this->response->html($this->taskLayout('subtask/create', array(
'values' => $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'task' => $task,
)));
}
@ -95,7 +95,7 @@ class Subtask extends Base
$this->response->html($this->taskLayout('subtask/edit', array(
'values' => empty($values) ? $subtask : $values,
'errors' => $errors,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'status_list' => $this->subtask->getStatusList(),
'subtask' => $subtask,
'task' => $task,

View file

@ -76,7 +76,7 @@ class Task extends Base
'link_label_list' => $this->link->getList(0, false),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
'users_list' => $this->projectPermission->getMemberList($task['project_id'], true, false, false),
'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $task['project_name'].' &gt; '.$task['title'],

View file

@ -0,0 +1,57 @@
<?php
namespace Kanboard\Controller;
/**
* Task Ajax Helper
*
* @package controller
* @author Frederic Guillot
*/
class TaskHelper extends Base
{
/**
* Render Markdown text and reply with the HTML Code
*
* @access public
*/
public function preview()
{
$payload = $this->request->getJson();
if (empty($payload['text'])) {
$this->response->html('<p>'.t('Nothing to preview...').'</p>');
}
$this->response->html($this->helper->text->markdown($payload['text']));
}
/**
* Task autocompletion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
if (empty($projects)) {
$this->response->json(array());
}
$filter = $this->taskFilterAutoCompleteFormatter
->create()
->filterByProjects($projects)
->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
// Search by task id or by title
if (ctype_digit($search)) {
$filter->filterById($search);
} else {
$filter->filterByTitle($search);
}
$this->response->json($filter->format());
}
}

View file

@ -36,7 +36,7 @@ class Taskcreation extends Base
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $swimlanes_list,

View file

@ -2,6 +2,8 @@
namespace Kanboard\Controller;
use Kanboard\Model\Project as ProjectModel;
/**
* Task Duplication controller
*
@ -107,7 +109,7 @@ class Taskduplication extends Base
private function chooseDestination(array $task, $template)
{
$values = array();
$projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
$projects_list = $this->projectUserRole->getProjectsByUser($this->userSession->getId(), array(ProjectModel::ACTIVE));
unset($projects_list[$task['project_id']]);
@ -117,7 +119,7 @@ class Taskduplication extends Base
$swimlanes_list = $this->swimlane->getList($dst_project_id, false, true);
$columns_list = $this->board->getColumnsList($dst_project_id);
$categories_list = $this->category->getList($dst_project_id);
$users_list = $this->projectPermission->getMemberList($dst_project_id);
$users_list = $this->projectUserRole->getAssignableUsersList($dst_project_id);
$values = $this->taskDuplication->checkDestinationProjectValues($task);
$values['project_id'] = $dst_project_id;

View file

@ -110,7 +110,7 @@ class Taskmodification extends Base
'values' => $values,
'errors' => $errors,
'task' => $task,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($task['project_id']),
'date_format' => $this->config->get('application_date_format'),

View file

@ -2,10 +2,6 @@
namespace Kanboard\Controller;
use Otp\Otp;
use Otp\GoogleAuthenticator;
use Base32\Base32;
/**
* Two Factor Auth controller
*
@ -36,12 +32,15 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
$provider = $this->authenticationManager->getPostAuthenticationProvider();
$label = $user['email'] ?: $user['username'];
$provider->setSecret($user['twofactor_secret']);
$this->response->html($this->layout('twofactor/index', array(
'user' => $user,
'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '',
'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '',
'qrcode_url' => $user['twofactor_activated'] == 1 ? $provider->getQrCodeUrl($label) : '',
'key_url' => $user['twofactor_activated'] == 1 ? $provider->getKeyUrl($label) : '',
)));
}
@ -61,7 +60,7 @@ class Twofactor extends User
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 1,
'twofactor_secret' => GoogleAuthenticator::generateRandom(),
'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(),
));
} else {
$this->user->update(array(
@ -72,14 +71,14 @@ class Twofactor extends User
}
// Allow the user to test or disable the feature
$this->userSession->disable2FA();
$this->userSession->disablePostAuthentication();
$this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
}
/**
* Test 2FA
* Test code
*
* @access public
*/
@ -88,10 +87,13 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
$otp = new Otp;
$values = $this->request->getValues();
if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
$provider = $this->authenticationManager->getPostAuthenticationProvider();
$provider->setCode(empty($values['code']) ? '' : $values['code']);
$provider->setSecret($user['twofactor_secret']);
if ($provider->authenticate()) {
$this->flash->success(t('The two factor authentication code is valid.'));
} else {
$this->flash->failure(t('The two factor authentication code is not valid.'));
@ -110,11 +112,14 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
$otp = new Otp;
$values = $this->request->getValues();
if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
$this->sessionStorage->postAuth['validated'] = true;
$provider = $this->authenticationManager->getPostAuthenticationProvider();
$provider->setCode(empty($values['code']) ? '' : $values['code']);
$provider->setSecret($user['twofactor_secret']);
if ($provider->authenticate()) {
$this->userSession->validatePostAuthentication();
$this->flash->success(t('The two factor authentication code is valid.'));
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {

View file

@ -3,6 +3,8 @@
namespace Kanboard\Controller;
use Kanboard\Notification\Mail as MailNotification;
use Kanboard\Model\Project as ProjectModel;
use Kanboard\Core\Security\Role;
/**
* User controller
@ -24,7 +26,7 @@ class User extends Base
{
$content = $this->template->render($template, $params);
$params['user_content_for_layout'] = $content;
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
if (isset($params['user'])) {
$params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
@ -49,7 +51,7 @@ class User extends Base
$this->response->html(
$this->template->layout('user/index', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'title' => t('Users').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
)));
@ -67,10 +69,11 @@ class User extends Base
$this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array(
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'roles' => $this->role->getApplicationRoles(),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'projects' => $this->project->getList(),
'errors' => $errors,
'values' => $values,
'values' => $values + array('role' => Role::APP_USER),
'title' => t('New user')
)));
}
@ -92,7 +95,7 @@ class User extends Base
$user_id = $this->user->create($values);
if ($user_id !== false) {
$this->projectPermission->addMember($project_id, $user_id);
$this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER);
if (! empty($values['notifications_enabled'])) {
$this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE));
@ -170,7 +173,7 @@ class User extends Base
{
$user = $this->getUser();
$this->response->html($this->layout('user/sessions', array(
'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']),
'sessions' => $this->rememberMeSession->getAll($user['id']),
'user' => $user,
)));
}
@ -184,8 +187,8 @@ class User extends Base
{
$this->checkCSRFParam();
$user = $this->getUser();
$this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id'));
$this->response->redirect($this->helper->url->to('user', 'session', array('user_id' => $user['id'])));
$this->rememberMeSession->remove($this->request->getIntegerParam('id'));
$this->response->redirect($this->helper->url->to('user', 'sessions', array('user_id' => $user['id'])));
}
/**
@ -205,7 +208,7 @@ class User extends Base
}
$this->response->html($this->layout('user/notifications', array(
'projects' => $this->projectPermission->getMemberProjects($user['id']),
'projects' => $this->projectUserRole->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)),
'notifications' => $this->userNotification->readSettings($user['id']),
'types' => $this->userNotificationType->getTypes(),
'filters' => $this->userNotificationFilter->getFilters(),
@ -326,16 +329,9 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
if ($this->userSession->isAdmin()) {
$values += array('is_admin' => 0, 'is_project_admin' => 0);
} else {
// Regular users can't be admin
if (isset($values['is_admin'])) {
unset($values['is_admin']);
}
if (isset($values['is_project_admin'])) {
unset($values['is_project_admin']);
if (! $this->userSession->isAdmin()) {
if (isset($values['role'])) {
unset($values['role']);
}
}
@ -358,6 +354,7 @@ class User extends Base
'user' => $user,
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
'roles' => $this->role->getApplicationRoles(),
)));
}

View file

@ -0,0 +1,24 @@
<?php
namespace Kanboard\Controller;
/**
* User Helper
*
* @package controller
* @author Frederic Guillot
*/
class UserHelper extends Base
{
/**
* User autocompletion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
$this->response->json($users);
}
}

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

@ -93,7 +93,7 @@ class Csv
{
if (! empty($value)) {
$value = trim(strtolower($value));
return $value === '1' || $value{0} === 't' ? 1 : 0;
return $value === '1' || $value{0} === 't' || $value{0} === 'y' ? 1 : 0;
}
return 0;

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 array_values($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

@ -0,0 +1,165 @@
<?php
namespace Kanboard\Core\Ldap;
use LogicException;
/**
* LDAP Client
*
* @package ldap
* @author Frederic Guillot
*/
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 Client
*/
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');
}
if (! $verify) {
putenv('LDAPTLS_REQCERT=never');
}
$this->ldap = ldap_connect($server, $port);
if ($this->ldap === false) {
throw new ClientException('LDAP: Unable to connect to the LDAP server');
}
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($this->ldap)) {
throw new ClientException('LDAP: Unable to start TLS');
}
return $this;
}
/**
* Anonymous authentication
*
* @access public
* @return boolean
*/
public function useAnonymousAuthentication()
{
if (! @ldap_bind($this->ldap)) {
throw new ClientException('Unable to perform anonymous binding');
}
return true;
}
/**
* Authentication with username/password
*
* @access public
* @param string $bind_rdn
* @param string $bind_password
* @return boolean
*/
public function authenticate($bind_rdn, $bind_password)
{
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;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace Kanboard\Core\Ldap;
use Exception;
/**
* LDAP Client Exception
*
* @package ldap
* @author Frederic Guillot
*/
class ClientException extends Exception
{
}

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());
}
}

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);
}
}

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

@ -0,0 +1,87 @@
<?php
namespace Kanboard\Core\Ldap;
/**
* LDAP Query
*
* @package ldap
* @author Frederic Guillot
*/
class Query
{
/**
* LDAP client
*
* @access private
* @var Client
*/
private $client = null;
/**
* Query result
*
* @access private
* @var array
*/
private $entries = array();
/**
* Constructor
*
* @access public
* @param Client $client
*/
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* Execute query
*
* @access public
* @param string $baseDn
* @param string $filter
* @param array $attributes
* @return Query
*/
public function execute($baseDn, $filter, array $attributes)
{
$sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes);
if ($sr === false) {
return $this;
}
$entries = ldap_get_entries($this->client->getConnection(), $sr);
if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
return $this;
}
$this->entries = $entries;
return $this;
}
/**
* Return true if the query returned a result
*
* @access public
* @return boolean
*/
public function hasResult()
{
return ! empty($this->entries);
}
/**
* Get LDAP Entries
*
* @access public
* @return Entities
*/
public function getEntries()
{
return new Entries($this->entries);
}
}

View file

@ -0,0 +1,223 @@
<?php
namespace Kanboard\Core\Ldap;
use LogicException;
use Kanboard\Core\Security\Role;
use Kanboard\User\LdapUserProvider;
/**
* LDAP User Finder
*
* @package ldap
* @author Frederic Guillot
*/
class User
{
/**
* Query
*
* @access private
* @var Query
*/
private $query;
/**
* Constructor
*
* @access public
* @param Query $query
*/
public function __construct(Query $query)
{
$this->query = $query;
}
/**
* Get user profile
*
* @static
* @access public
* @param Client $client
* @param string $username
* @return array
*/
public static function getUser(Client $client, $username)
{
$self = new self(new Query($client));
return $self->find($self->getLdapUserPattern($username));
}
/**
* 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()) {
$user = $this->build();
}
return $user;
}
/**
* Build user profile
*
* @access protected
* @return LdapUserProvider
*/
protected function build()
{
$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())
);
}
/**
* Ge the list of attributes to fetch when reading the LDAP user 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->getAttributeUsername(),
$this->getAttributeName(),
$this->getAttributeEmail(),
$this->getAttributeGroup(),
)));
}
/**
* Get LDAP account id attribute
*
* @access public
* @return string
*/
public function getAttributeUsername()
{
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;
}
/**
* Get LDAP account email attribute
*
* @access public
* @return string
*/
public function getAttributeEmail()
{
if (! LDAP_USER_ATTRIBUTE_EMAIL) {
throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
}
return LDAP_USER_ATTRIBUTE_EMAIL;
}
/**
* Get LDAP account memberof attribute
*
* @access public
* @return string
*/
public function getAttributeGroup()
{
return LDAP_USER_ATTRIBUTE_GROUPS;
}
/**
* Get LDAP admin group DN
*
* @access public
* @return string
*/
public function getGroupAdminDn()
{
return LDAP_GROUP_ADMIN_DN;
}
/**
* Get LDAP application manager group DN
*
* @access public
* @return string
*/
public function getGroupManagerDn()
{
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;
}
/**
* Get LDAP user pattern
*
* @access public
* @param string $username
* @return string
*/
public function getLdapUserPattern($username)
{
if (! LDAP_USER_FILTER) {
throw new LogicException('LDAP user filter empty, check the parameter LDAP_USER_FILTER');
}
return sprintf(LDAP_USER_FILTER, $username);
}
}

View file

@ -39,6 +39,7 @@ class Lexer
"/^(swimlane:)/" => 'T_SWIMLANE',
"/^(ref:)/" => 'T_REFERENCE',
"/^(reference:)/" => 'T_REFERENCE',
"/^(link:)/" => 'T_LINK',
"/^(\s+)/" => 'T_WHITESPACE',
'/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
'/^(yesterday|tomorrow|today)/' => 'T_DATE',
@ -118,6 +119,7 @@ class Lexer
case 'T_COLUMN':
case 'T_PROJECT':
case 'T_SWIMLANE':
case 'T_LINK':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {

View file

@ -46,7 +46,7 @@ class Mail extends Base implements ClientInterface
* Get SwiftMailer transport
*
* @access protected
* @return \Swift_Transport
* @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/
protected function getTransport()
{

View file

@ -16,7 +16,7 @@ class Sendmail extends Mail
* Get SwiftMailer transport
*
* @access protected
* @return \Swift_Transport
* @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/
protected function getTransport()
{

View file

@ -16,7 +16,7 @@ class Smtp extends Mail
* Get SwiftMailer transport
*
* @access protected
* @return \Swift_Transport
* @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/
protected function getTransport()
{

View file

@ -0,0 +1,155 @@
<?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;
}
/**
* 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);
}
}

View file

@ -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);
});
}
}

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

@ -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);
}
}

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

@ -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 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

@ -3,7 +3,6 @@
namespace Kanboard\Core\Session;
use Kanboard\Core\Base;
use Kanboard\Core\Http\Request;
/**
* Session Manager
@ -13,6 +12,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 +47,7 @@ class SessionManager extends Base
session_name('KB_SID');
session_start();
$this->container['sessionStorage']->setStorage($_SESSION);
$this->sessionStorage->setStorage($_SESSION);
}
/**
@ -51,6 +57,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 +88,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
@ -60,6 +61,21 @@ class SessionStorage
return $session;
}
/**
* Flush session data
*
* @access public
*/
public function flush()
{
$session = get_object_vars($this);
unset($session['storage']);
foreach (array_keys($session) as $property) {
unset($this->$property);
}
}
/**
* Copy class properties to external storage
*

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

@ -1,11 +1,14 @@
<?php
namespace Kanboard\Model;
namespace Kanboard\Core\User;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/**
* User Session
*
* @package model
* @package user
* @author Frederic Guillot
*/
class UserSession extends Base
@ -18,22 +21,29 @@ class UserSession extends Base
*/
public function initialize(array $user)
{
if (isset($user['password'])) {
unset($user['password']);
}
if (isset($user['twofactor_secret'])) {
unset($user['twofactor_secret']);
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_admin'] = isset($user['is_admin']) ? (bool) $user['is_admin'] : false;
$user['is_project_admin'] = isset($user['is_project_admin']) ? (bool) $user['is_project_admin'] : false;
$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->postAuth = array('validated' => false);
$this->sessionStorage->postAuthenticationValidated = false;
}
/**
* Get user application role
*
* @access public
* @return string
*/
public function getRole()
{
return $this->sessionStorage->user['role'];
}
/**
@ -42,9 +52,19 @@ class UserSession extends Base
* @access public
* @return bool
*/
public function check2FA()
public function isPostAuthenticationValidated()
{
return isset($this->sessionStorage->postAuth['validated']) && $this->sessionStorage->postAuth['validated'] === true;
return isset($this->sessionStorage->postAuthenticationValidated) && $this->sessionStorage->postAuthenticationValidated === true;
}
/**
* Validate 2FA for the current session
*
* @access public
*/
public function validatePostAuthentication()
{
$this->sessionStorage->postAuthenticationValidated = true;
}
/**
@ -53,7 +73,7 @@ class UserSession extends Base
* @access public
* @return bool
*/
public function has2FA()
public function hasPostAuthentication()
{
return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true;
}
@ -63,7 +83,7 @@ class UserSession extends Base
*
* @access public
*/
public function disable2FA()
public function disablePostAuthentication()
{
$this->sessionStorage->user['twofactor_activated'] = false;
}
@ -76,18 +96,7 @@ class UserSession extends Base
*/
public function isAdmin()
{
return isset($this->sessionStorage->user['is_admin']) && $this->sessionStorage->user['is_admin'] === true;
}
/**
* Return true if the logged user is project admin
*
* @access public
* @return bool
*/
public function isProjectAdmin()
{
return isset($this->sessionStorage->user['is_project_admin']) && $this->sessionStorage->user['is_project_admin'] === true;
return isset($this->sessionStorage->user['role']) && $this->sessionStorage->user['role'] === Role::APP_ADMIN;
}
/**
@ -105,7 +114,7 @@ class UserSession extends Base
* Get username
*
* @access public
* @return integer
* @return string
*/
public function getUsername()
{

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);
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace Kanboard\Event;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
class AuthEvent extends BaseEvent
{
private $auth_name;
private $user_id;
public function __construct($auth_name, $user_id)
{
$this->auth_name = $auth_name;
$this->user_id = $user_id;
}
public function getUserId()
{
return $this->user_id;
}
public function getAuthType()
{
return $this->auth_name;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Kanboard\Event;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
/**
* Authentication Failure Event
*
* @package event
* @author Frederic Guillot
*/
class AuthFailureEvent extends BaseEvent
{
/**
* Username
*
* @access private
* @var string
*/
private $username = '';
/**
* Constructor
*
* @access public
* @param string $username
*/
public function __construct($username = '')
{
$this->username = $username;
}
/**
* Get username
*
* @access public
* @return string
*/
public function getUsername()
{
return $this->username;
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Kanboard\Event;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
/**
* Authentication Success Event
*
* @package event
* @author Frederic Guillot
*/
class AuthSuccessEvent extends BaseEvent
{
/**
* Authentication provider name
*
* @access private
* @var string
*/
private $authType;
/**
* Constructor
*
* @access public
* @param string $authType
*/
public function __construct($authType)
{
$this->authType = $authType;
}
/**
* Get authentication type
*
* @return string
*/
public function getAuthType()
{
return $this->authType;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Kanboard\Formatter;
/**
* Autocomplete formatter for groups
*
* @package formatter
* @author Frederic Guillot
*/
class GroupAutoCompleteFormatter implements FormatterInterface
{
/**
* Groups found
*
* @access private
* @var array
*/
private $groups;
/**
* Format groups for the ajax autocompletion
*
* @access public
* @param array $groups
* @return GroupAutoCompleteFormatter
*/
public function setGroups(array $groups)
{
$this->groups = $groups;
return $this;
}
/**
* Format groups for the ajax autocompletion
*
* @access public
* @return array
*/
public function format()
{
$result = array();
foreach ($this->groups as $group) {
$result[] = array(
'id' => $group->getInternalId(),
'external_id' => $group->getExternalId(),
'value' => $group->getName(),
'label' => $group->getName(),
);
}
return $result;
}
}

View file

@ -79,7 +79,7 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
'gantt_link' => $this->helper->url->href('gantt', 'project', array('project_id' => $project['id'])),
'color' => $color,
'not_defined' => empty($project['start_date']) || empty($project['end_date']),
'users' => $this->projectPermission->getProjectUsers($project['id']),
'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
);
}

View file

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Formatter;
use Kanboard\Model\User;
use Kanboard\Model\UserFilter;
/**
* Autocomplete formatter for user filter
*
* @package formatter
* @author Frederic Guillot
*/
class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface
{
/**
* Format the tasks for the ajax autocompletion
*
* @access public
* @return array
*/
public function format()
{
$users = $this->query->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')->findAll();
foreach ($users as &$user) {
$user['value'] = $user['username'].' (#'.$user['id'].')';
if (empty($user['name'])) {
$user['label'] = $user['username'];
} else {
$user['label'] = $user['name'].' ('.$user['username'].')';
}
}
return $users;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Kanboard\Group;
use Kanboard\Core\Base;
use Kanboard\Core\Group\GroupBackendProviderInterface;
/**
* Database Backend Group Provider
*
* @package group
* @author Frederic Guillot
*/
class DatabaseBackendGroupProvider extends Base implements GroupBackendProviderInterface
{
/**
* Find a group from a search query
*
* @access public
* @param string $input
* @return DatabaseGroupProvider[]
*/
public function find($input)
{
$result = array();
$groups = $this->group->search($input);
foreach ($groups as $group) {
$result[] = new DatabaseGroupProvider($group);
}
return $result;
}
}

Some files were not shown because too many files have changed in this diff Show more