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 Version 1.0.21
-------------- --------------
Breaking changes: Breaking changes:
* Projects with duplicate name are now allowed: * Projects with duplicate name are now allowed:
For Postgres and Mysql the unique constraint is removed by database migration - 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 - However Sqlite does not support alter table, only new databases will have the unique constraint removed
New features: New features:

View file

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

View file

@ -3,7 +3,6 @@
namespace Kanboard\Api; namespace Kanboard\Api;
use JsonRPC\AuthenticationFailure; use JsonRPC\AuthenticationFailure;
use Symfony\Component\EventDispatcher\Event;
/** /**
* Base class * Base class
@ -24,15 +23,58 @@ class Auth extends Base
*/ */
public function checkCredentials($username, $password, $class, $method) 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->checkProcedurePermission(true, $method);
$this->userSession->initialize($this->user->getByUsername($username)); $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); $this->checkProcedurePermission(false, $method);
} else { } else {
throw new AuthenticationFailure('Wrong credentials'); 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() public function getMyDashboard()
{ {
$user_id = $this->userSession->getId(); $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(); $tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
return array( return array(
@ -32,7 +32,7 @@ class Me extends Base
public function getMyActivityStream() 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); return $this->projectActivity->getProjects($project_ids, 100);
} }
@ -50,7 +50,7 @@ class Me extends Base
public function getMyProjectsList() public function getMyProjectsList()
{ {
return $this->projectPermission->getMemberProjects($this->userSession->getId()); return $this->projectUserRole->getProjectsByUser($this->userSession->getId());
} }
public function getMyOverdueTasks() public function getMyOverdueTasks()
@ -60,7 +60,7 @@ class Me extends Base
public function getMyProjects() 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); $projects = $this->project->getAllByIds($project_ids);
return $this->formatProjects($projects); return $this->formatProjects($projects);

View file

@ -2,6 +2,8 @@
namespace Kanboard\Api; namespace Kanboard\Api;
use Kanboard\Core\Security\Role;
/** /**
* ProjectPermission API controller * ProjectPermission API controller
* *
@ -12,16 +14,16 @@ class ProjectPermission extends \Kanboard\Core\Base
{ {
public function getMembers($project_id) public function getMembers($project_id)
{ {
return $this->projectPermission->getMembers($project_id); return $this->projectUserRole->getAllUsers($project_id);
} }
public function revokeUser($project_id, $user_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) 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); $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( $values = array(
'title' => $title, 'title' => $title,
'project_id' => $project_id, 'project_id' => $project_id,
@ -96,20 +104,28 @@ class Task extends Base
return $valid ? $this->taskCreation->create($values) : false; return $valid ? $this->taskCreation->create($values) : false;
} }
public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null, public function updateTask($id, $title = null, $color_id = null, $owner_id = null,
$creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null, $date_due = null, $description = null, $category_id = null, $score = null,
$recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
$recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
{ {
$this->checkTaskPermission($id); $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( $values = array(
'id' => $id, 'id' => $id,
'title' => $title, 'title' => $title,
'project_id' => $project_id,
'color_id' => $color_id, 'color_id' => $color_id,
'owner_id' => $owner_id, 'owner_id' => $owner_id,
'creator_id' => $creator_id,
'date_due' => $date_due, 'date_due' => $date_due,
'description' => $description, 'description' => $description,
'category_id' => $category_id, 'category_id' => $category_id,

View file

@ -2,7 +2,11 @@
namespace Kanboard\Api; 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 * User API controller
@ -27,7 +31,7 @@ class User extends \Kanboard\Core\Base
return $this->user->remove($user_id); 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( $values = array(
'username' => $username, 'username' => $username,
@ -35,44 +39,53 @@ class User extends \Kanboard\Core\Base
'confirmation' => $password, 'confirmation' => $password,
'name' => $name, 'name' => $name,
'email' => $email, 'email' => $email,
'is_admin' => $is_admin, 'role' => $role,
'is_project_admin' => $is_project_admin,
); );
list($valid, ) = $this->user->validateCreation($values); list($valid, ) = $this->user->validateCreation($values);
return $valid ? $this->user->create($values) : false; 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); try {
$user = $ldap->lookup($username, $email);
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; 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( $values = array(
'id' => $id, 'id' => $id,
'username' => $username, 'username' => $username,
'name' => $name, 'name' => $name,
'email' => $email, 'email' => $email,
'is_admin' => $is_admin, 'role' => $role,
'is_project_admin' => $is_project_admin,
); );
foreach ($values as $key => $value) { 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_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(), 'available_params' => $this->action->getAllActionParameters(),
'columns_list' => $this->board->getColumnsList($project['id']), '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), 'projects_list' => $this->project->getList(false),
'colors_list' => $this->color->getList(), 'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']), 'categories_list' => $this->category->getList($project['id']),
@ -86,7 +86,7 @@ class Action extends Base
'values' => $values, 'values' => $values,
'action_params' => $action_params, 'action_params' => $action_params,
'columns_list' => $this->board->getColumnsList($project['id']), '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, 'projects_list' => $projects_list,
'colors_list' => $this->color->getList(), 'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']), 'categories_list' => $this->category->getList($project['id']),

View file

@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject(); $project = $this->getProject();
$this->response->html($this->template->layout('activity/project', array( $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']), 'events' => $this->projectActivity->getProject($project['id']),
'project' => $project, 'project' => $project,
'title' => t('%s\'s activity', $project['name']) 'title' => t('%s\'s activity', $project['name'])

View file

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

View file

@ -22,7 +22,7 @@ class App extends Base
*/ */
private function layout($template, array $params) 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['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('app/layout', $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)) ->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id))
->setMax($max) ->setMax($max)
->setOrder('name') ->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'); ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
} }
@ -169,7 +169,7 @@ class App extends Base
$this->response->html($this->layout('app/activity', array( $this->response->html($this->layout('app/activity', array(
'title' => t('My activity stream'), '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, 'user' => $user,
))); )));
} }
@ -202,49 +202,4 @@ class App extends Base
'user' => $user, '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( $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, 'errors' => $errors,
'values' => $values, 'values' => $values,
'no_layout' => true, 'no_layout' => true,
@ -40,18 +40,11 @@ class Auth extends Base
public function check() public function check()
{ {
$values = $this->request->getValues(); $values = $this->request->getValues();
$this->sessionStorage->hasRememberMe = ! empty($values['remember_me']);
list($valid, $errors) = $this->authentication->validateForm($values); list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) { if ($valid) {
if (isset($this->sessionStorage->redirectAfterLogin) $this->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->login($values, $errors); $this->login($values, $errors);
@ -64,7 +57,6 @@ class Auth extends Base
*/ */
public function logout() public function logout()
{ {
$this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
$this->sessionManager->close(); $this->sessionManager->close();
$this->response->redirect($this->helper->url->to('auth', 'login')); $this->response->redirect($this->helper->url->to('auth', 'login'));
} }
@ -83,4 +75,20 @@ class Auth extends Base
$this->sessionStorage->captcha = $builder->getPhrase(); $this->sessionStorage->captcha = $builder->getPhrase();
$builder->output(); $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; namespace Kanboard\Controller;
use Pimple\Container; use Kanboard\Core\Security\Role;
use Symfony\Component\EventDispatcher\Event;
/** /**
* Base controller * Base controller
@ -14,36 +13,22 @@ use Symfony\Component\EventDispatcher\Event;
abstract class Base extends \Kanboard\Core\Base abstract class Base extends \Kanboard\Core\Base
{ {
/** /**
* Constructor * Method executed before each action
*
* @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
* *
* @access public * @access public
*/ */
public function __destruct() public function beforeAction($controller, $action)
{ {
if (DEBUG) { $this->sessionManager->open();
foreach ($this->db->getLogMessages() as $message) { $this->dispatcher->dispatch('app.bootstrap');
$this->logger->debug($message); $this->sendHeaders($action);
} $this->authenticationManager->checkCurrentSession();
$this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries)); if (! $this->applicationAuthorization->isAllowed($controller, $action, Role::APP_PUBLIC)) {
$this->logger->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT'])); $this->handleAuthentication();
$this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage())); $this->handlePostAuthentication($controller, $action);
$this->logger->debug('END_REQUEST='.$_SERVER['REQUEST_URI']); $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 * 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()) { if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401); $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'); $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()) { if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401); $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'); $project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_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); $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(); $this->forbidden();
} }
} }
@ -147,10 +124,10 @@ abstract class Base extends \Kanboard\Core\Base
/** /**
* Application not found page (404 error) * Application not found page (404 error)
* *
* @access public * @access protected
* @param boolean $no_layout Display the layout or not * @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( $this->response->html($this->template->layout('app/notfound', array(
'title' => t('Page not found'), 'title' => t('Page not found'),
@ -161,11 +138,15 @@ abstract class Base extends \Kanboard\Core\Base
/** /**
* Application forbidden page * Application forbidden page
* *
* @access public * @access protected
* @param boolean $no_layout Display the layout or not * @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( $this->response->html($this->template->layout('app/forbidden', array(
'title' => t('Access Forbidden'), 'title' => t('Access Forbidden'),
'no_layout' => $no_layout, 'no_layout' => $no_layout,
@ -209,7 +190,7 @@ abstract class Base extends \Kanboard\Core\Base
$content = $this->template->render($template, $params); $content = $this->template->render($template, $params);
$params['task_content_for_layout'] = $content; $params['task_content_for_layout'] = $content;
$params['title'] = $params['task']['project_name'].' &gt; '.$params['task']['title']; $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); return $this->template->layout('task/layout', $params);
} }
@ -227,7 +208,7 @@ abstract class Base extends \Kanboard\Core\Base
$content = $this->template->render($template, $params); $content = $this->template->render($template, $params);
$params['project_content_for_layout'] = $content; $params['project_content_for_layout'] = $content;
$params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' &gt; '.$params['title']; $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; $params['sidebar_template'] = $sidebar_template;
return $this->template->layout('project/layout', $params); return $this->template->layout('project/layout', $params);
@ -300,12 +281,15 @@ abstract class Base extends \Kanboard\Core\Base
* Common method to get project filters * Common method to get project filters
* *
* @access protected * @access protected
* @param string $controller
* @param string $action
* @return array
*/ */
protected function getProjectFilters($controller, $action) protected function getProjectFilters($controller, $action)
{ {
$project = $this->getProject(); $project = $this->getProject();
$search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id'])); $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']]); unset($board_selector[$project['id']]);
$filters = array( $filters = array(

View file

@ -51,7 +51,7 @@ class Board extends Base
$this->response->html($this->template->layout('board/view_private', array( $this->response->html($this->template->layout('board/view_private', array(
'categories_list' => $this->category->getList($params['project']['id'], false), '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()), 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']), 'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
'description' => $params['project']['description'], 'description' => $params['project']['description'],
@ -142,195 +142,6 @@ class Board extends Base
$this->response->html($this->renderBoard($project_id)); $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 * Enable collapsed mode
* *
@ -355,6 +166,7 @@ class Board extends Base
* Change display mode * Change display mode
* *
* @access private * @access private
* @param boolean $mode
*/ */
private function changeDisplayMode($mode) private function changeDisplayMode($mode)
{ {
@ -372,6 +184,7 @@ class Board extends Base
* Render board * Render board
* *
* @access private * @access private
* @param integer $project_id
*/ */
private function renderBoard($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) 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['values'] = $this->config->getAll();
$params['errors'] = array(); $params['errors'] = array();
$params['config_content_for_layout'] = $this->template->render($template, $params); $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) 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); $params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params); return $this->template->layout('config/layout', $params);

View file

@ -2,6 +2,8 @@
namespace Kanboard\Controller; namespace Kanboard\Controller;
use Kanboard\Core\Security\Role;
/** /**
* Custom Filter management * Custom Filter management
* *
@ -137,7 +139,7 @@ class Customfilter extends Base
{ {
$user_id = $this->userSession->getId(); $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(); $this->forbidden();
} }
} }

View file

@ -53,7 +53,7 @@ class Doc extends Base
} }
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array( $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); $this->forbidden(true);
} }
$projects = $this->projectPermission->getActiveMemberProjects($user['id']);
$this->response->xml($this->template->render('feed/user', array( $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, 'user' => $user,
))); )));
} }

View file

@ -20,13 +20,13 @@ class Gantt extends Base
if ($this->userSession->isAdmin()) { if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds(); $project_ids = $this->project->getAllIds();
} else { } 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( $this->response->html($this->template->layout('gantt/projects', array(
'projects' => $this->projectGanttFormatter->filter($project_ids)->format(), 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
'title' => t('Gantt chart for all projects'), '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( $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, 'sorting' => $sorting,
'tasks' => $filter->format(), 'tasks' => $filter->format(),
))); )));
@ -109,7 +109,7 @@ class Gantt extends Base
'column_id' => $this->board->getFirstColumn($project['id']), 'column_id' => $this->board->getFirstColumn($project['id']),
'position' => 1 '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(), 'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']), 'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $this->swimlane->getList($project['id'], false, true), '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) 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); $params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params); return $this->template->layout('config/layout', $params);

View file

@ -17,7 +17,7 @@ class Oauth extends Base
*/ */
public function google() public function google()
{ {
$this->step1('google'); $this->step1('Google');
} }
/** /**
@ -27,7 +27,7 @@ class Oauth extends Base
*/ */
public function github() public function github()
{ {
$this->step1('github'); $this->step1('Github');
} }
/** /**
@ -37,7 +37,7 @@ class Oauth extends Base
*/ */
public function gitlab() public function gitlab()
{ {
$this->step1('gitlab'); $this->step1('Gitlab');
} }
/** /**
@ -45,12 +45,12 @@ class Oauth extends Base
* *
* @access public * @access public
*/ */
public function unlink($backend = '') public function unlink()
{ {
$backend = $this->request->getStringParam('backend', $backend); $backend = $this->request->getStringParam('backend');
$this->checkCSRFParam(); $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.')); $this->flash->success(t('Your external account is not linked anymore to your profile.'));
} else { } else {
$this->flash->failure(t('Unable to unlink your external account.')); $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 * Redirect to the provider if no code received
* *
* @access private * @access private
* @param string $provider
*/ */
private function step1($backend) private function step1($provider)
{ {
$code = $this->request->getStringParam('code'); $code = $this->request->getStringParam('code');
if (! empty($code)) { if (! empty($code)) {
$this->step2($backend, $code); $this->step2($provider, $code);
} else { } 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 * Link or authenticate the user
* *
* @access private * @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()) { if ($this->userSession->isLogged()) {
$this->link($backend, $profile); $this->link($provider);
} }
$this->authenticate($backend, $profile); $this->authenticate($provider);
} }
/** /**
* Link the account * Link the account
* *
* @access private * @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')); $this->flash->failure(t('External authentication failed'));
} else { } else {
$this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.')); $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()))); $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 * Authenticate the account
* *
* @access private * @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')); $this->response->redirect($this->helper->url->to('app', 'index'));
} else { } else {
$this->response->html($this->template->layout('auth/index', array( $this->response->html($this->template->layout('auth/index', array(

View file

@ -20,7 +20,7 @@ class Project extends Base
if ($this->userSession->isAdmin()) { if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds(); $project_ids = $this->project->getAllIds();
} else { } else {
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId()); $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
} }
$nb_projects = count($project_ids); $nb_projects = count($project_ids);
@ -33,7 +33,7 @@ class Project extends Base
->calculate(); ->calculate();
$this->response->html($this->template->layout('project/index', array( $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, 'paginator' => $paginator,
'nb_projects' => $nb_projects, 'nb_projects' => $nb_projects,
'title' => t('Projects').' ('.$nb_projects.')' 'title' => t('Projects').' ('.$nb_projects.')'
@ -160,11 +160,11 @@ class Project extends Base
$values = $this->request->getValues(); $values = $this->request->getValues();
if (isset($values['is_private'])) { 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']); unset($values['is_private']);
} }
} elseif ($project['is_private'] == 1 && ! isset($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); $values += array('is_private' => 0);
} }
} }
@ -183,120 +183,6 @@ class Project extends Base
$this->edit($values, $errors); $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 * Remove a project
* *
@ -413,17 +299,28 @@ class Project extends Base
*/ */
public function create(array $values = array(), array $errors = array()) 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( $this->response->html($this->template->layout('project/new', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'values' => empty($values) ? array('is_private' => $is_private) : $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
'is_private' => $is_private, 'is_private' => $is_private,
'title' => $is_private ? t('New private project') : t('New project'), '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 * 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\User as UserModel;
use Kanboard\Model\Task as TaskModel; use Kanboard\Model\Task as TaskModel;
use Kanboard\Core\Security\Role;
/** /**
* Project User overview * Project User overview
@ -23,7 +24,7 @@ class Projectuser extends Base
*/ */
private function layout($template, array $params) 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['content_for_sublayout'] = $this->template->render($template, $params);
$params['filter'] = array('user_id' => $params['user_id']); $params['filter'] = array('user_id' => $params['user_id']);
@ -37,17 +38,17 @@ class Projectuser extends Base
if ($this->userSession->isAdmin()) { if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds(); $project_ids = $this->project->getAllIds();
} else { } 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)); 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(); 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) { if ($user_id !== UserModel::EVERYBODY_ID) {
$query->eq(UserModel::TABLE.'.id', $user_id); $query->eq(UserModel::TABLE.'.id', $user_id);
@ -101,7 +102,7 @@ class Projectuser extends Base
*/ */
public function managers() 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() 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() public function index()
{ {
$projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$search = urldecode($this->request->getStringParam('search')); $search = urldecode($this->request->getStringParam('search'));
$nb_tasks = 0; $nb_tasks = 0;

View file

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

View file

@ -76,7 +76,7 @@ class Task extends Base
'link_label_list' => $this->link->getList(0, false), 'link_label_list' => $this->link->getList(0, false),
'columns_list' => $this->board->getColumnsList($task['project_id']), 'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(), '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_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(), 'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $task['project_name'].' &gt; '.$task['title'], '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, 'errors' => $errors,
'values' => $values + array('project_id' => $project['id']), 'values' => $values + array('project_id' => $project['id']),
'columns_list' => $this->board->getColumnsList($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(), 'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']), 'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $swimlanes_list, 'swimlanes_list' => $swimlanes_list,

View file

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

View file

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

View file

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

View file

@ -3,6 +3,8 @@
namespace Kanboard\Controller; namespace Kanboard\Controller;
use Kanboard\Notification\Mail as MailNotification; use Kanboard\Notification\Mail as MailNotification;
use Kanboard\Model\Project as ProjectModel;
use Kanboard\Core\Security\Role;
/** /**
* User controller * User controller
@ -24,7 +26,7 @@ class User extends Base
{ {
$content = $this->template->render($template, $params); $content = $this->template->render($template, $params);
$params['user_content_for_layout'] = $content; $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'])) { if (isset($params['user'])) {
$params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
@ -49,7 +51,7 @@ class User extends Base
$this->response->html( $this->response->html(
$this->template->layout('user/index', array( $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().')', 'title' => t('Users').' ('.$paginator->getTotal().')',
'paginator' => $paginator, '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( $this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array(
'timezones' => $this->config->getTimezones(true), 'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(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(), 'projects' => $this->project->getList(),
'errors' => $errors, 'errors' => $errors,
'values' => $values, 'values' => $values + array('role' => Role::APP_USER),
'title' => t('New user') 'title' => t('New user')
))); )));
} }
@ -92,7 +95,7 @@ class User extends Base
$user_id = $this->user->create($values); $user_id = $this->user->create($values);
if ($user_id !== false) { 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'])) { if (! empty($values['notifications_enabled'])) {
$this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE)); $this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE));
@ -170,7 +173,7 @@ class User extends Base
{ {
$user = $this->getUser(); $user = $this->getUser();
$this->response->html($this->layout('user/sessions', array( $this->response->html($this->layout('user/sessions', array(
'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']), 'sessions' => $this->rememberMeSession->getAll($user['id']),
'user' => $user, 'user' => $user,
))); )));
} }
@ -184,8 +187,8 @@ class User extends Base
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
$user = $this->getUser(); $user = $this->getUser();
$this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id')); $this->rememberMeSession->remove($this->request->getIntegerParam('id'));
$this->response->redirect($this->helper->url->to('user', 'session', array('user_id' => $user['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( $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']), 'notifications' => $this->userNotification->readSettings($user['id']),
'types' => $this->userNotificationType->getTypes(), 'types' => $this->userNotificationType->getTypes(),
'filters' => $this->userNotificationFilter->getFilters(), 'filters' => $this->userNotificationFilter->getFilters(),
@ -326,16 +329,9 @@ class User extends Base
if ($this->request->isPost()) { if ($this->request->isPost()) {
$values = $this->request->getValues(); $values = $this->request->getValues();
if ($this->userSession->isAdmin()) { if (! $this->userSession->isAdmin()) {
$values += array('is_admin' => 0, 'is_project_admin' => 0); if (isset($values['role'])) {
} else { unset($values['role']);
// 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']);
} }
} }
@ -358,6 +354,7 @@ class User extends Base
'user' => $user, 'user' => $user,
'timezones' => $this->config->getTimezones(true), 'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(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; use Pimple\Container;
/** /**
* Base class * Base Class
* *
* @package core * @package core
* @author Frederic Guillot * @author Frederic Guillot
* *
* @property \Kanboard\Core\Session\SessionManager $sessionManager * @property \Kanboard\Core\Cache\MemoryCache $memoryCache
* @property \Kanboard\Core\Session\SessionStorage $sessionStorage * @property \Kanboard\Core\Group\GroupManager $groupManager
* @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\Http\Client $httpClient * @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\Request $request
* @property \Kanboard\Core\Http\Router $router
* @property \Kanboard\Core\Http\Response $response * @property \Kanboard\Core\Http\Response $response
* @property \Kanboard\Core\Template $template * @property \Kanboard\Core\Http\Router $router
* @property \Kanboard\Core\OAuth2 $oauth * @property \Kanboard\Core\Mail\Client $emailClient
* @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
* @property \Kanboard\Core\Cache\Cache $memoryCache
* @property \Kanboard\Core\Plugin\Hook $hook * @property \Kanboard\Core\Plugin\Hook $hook
* @property \Kanboard\Core\Plugin\Loader $pluginLoader * @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\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\BitbucketWebhook $bitbucketWebhook
* @property \Kanboard\Integration\GithubWebhook $githubWebhook * @property \Kanboard\Integration\GithubWebhook $githubWebhook
* @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook * @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook
@ -36,7 +50,8 @@ use Pimple\Container;
* @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
* @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter * @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\Action $action
* @property \Kanboard\Model\Authentication $authentication * @property \Kanboard\Model\Authentication $authentication
* @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Board $board
@ -46,8 +61,9 @@ use Pimple\Container;
* @property \Kanboard\Model\Config $config * @property \Kanboard\Model\Config $config
* @property \Kanboard\Model\Currency $currency * @property \Kanboard\Model\Currency $currency
* @property \Kanboard\Model\CustomFilter $customFilter * @property \Kanboard\Model\CustomFilter $customFilter
* @property \Kanboard\Model\DateParser $dateParser
* @property \Kanboard\Model\File $file * @property \Kanboard\Model\File $file
* @property \Kanboard\Model\Group $group
* @property \Kanboard\Model\GroupMember $groupMember
* @property \Kanboard\Model\LastLogin $lastLogin * @property \Kanboard\Model\LastLogin $lastLogin
* @property \Kanboard\Model\Link $link * @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification * @property \Kanboard\Model\Notification $notification
@ -60,8 +76,11 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @property \Kanboard\Model\ProjectMetadata $projectMetadata * @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission * @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification * @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
* @property \Kanboard\Model\RememberMeSession $rememberMeSession
* @property \Kanboard\Model\Subtask $subtask * @property \Kanboard\Model\Subtask $subtask
* @property \Kanboard\Model\SubtaskExport $subtaskExport * @property \Kanboard\Model\SubtaskExport $subtaskExport
* @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking * @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking
@ -84,16 +103,17 @@ use Pimple\Container;
* @property \Kanboard\Model\Transition $transition * @property \Kanboard\Model\Transition $transition
* @property \Kanboard\Model\User $user * @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserImport $userImport * @property \Kanboard\Model\UserImport $userImport
* @property \Kanboard\Model\UserLocking $userLocking
* @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationType $userNotificationType * @property \Kanboard\Model\UserNotificationType $userNotificationType
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
* @property \Kanboard\Model\UserSession $userSession
* @property \Kanboard\Model\UserMetadata $userMetadata * @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook * @property \Kanboard\Model\Webhook $webhook
* @property \Psr\Log\LoggerInterface $logger * @property \Psr\Log\LoggerInterface $logger
* @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter * @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter
* @property \PicoDb\Database $db * @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/ */
abstract class Base abstract class Base
{ {

View file

@ -23,7 +23,7 @@ class MemoryCache extends Base implements CacheInterface
* *
* @access public * @access public
* @param string $key * @param string $key
* @param string $value * @param mixed $value
*/ */
public function set($key, $value) public function set($key, $value)
{ {

View file

@ -93,7 +93,7 @@ class Csv
{ {
if (! empty($value)) { if (! empty($value)) {
$value = trim(strtolower($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; 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 <?php
namespace Kanboard\Core; namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
/** /**
* OAuth2 client * OAuth2 Client
* *
* @package core * @package http
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class OAuth2 extends Base 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; namespace Kanboard\Core\Http;
use Pimple\Container;
use Kanboard\Core\Base; use Kanboard\Core\Base;
/** /**
@ -13,7 +14,35 @@ use Kanboard\Core\Base;
class Request extends 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 * @access public
* @param string $name Parameter name * @param string $name Parameter name
@ -22,11 +51,11 @@ class Request extends Base
*/ */
public function getStringParam($name, $default_value = '') 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 * @access public
* @param string $name Parameter name * @param string $name Parameter name
@ -35,7 +64,7 @@ class Request extends Base
*/ */
public function getIntegerParam($name, $default_value = 0) 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() public function getValues()
{ {
if (! empty($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) { if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) {
unset($_POST['csrf_token']); unset($this->post['csrf_token']);
return $_POST; return $this->post;
} }
return array(); return array();
@ -98,8 +127,8 @@ class Request extends Base
*/ */
public function getFileContent($name) public function getFileContent($name)
{ {
if (isset($_FILES[$name])) { if (isset($this->files[$name]['tmp_name'])) {
return file_get_contents($_FILES[$name]['tmp_name']); return file_get_contents($this->files[$name]['tmp_name']);
} }
return ''; return '';
@ -114,7 +143,7 @@ class Request extends Base
*/ */
public function getFilePath($name) 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() 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 * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
* *
* @static
* @access public * @access public
* @return boolean * @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) public function getHeader($name)
{ {
$name = 'HTTP_'.str_replace('-', '_', strtoupper($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() 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 * @access public
* @return string * @return string
*/ */
public function getUri() 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 * Get the user agent
* *
* @static
* @access public * @access public
* @return string * @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 * @access public
* @param bool $only_public Return only public IP address
* @return string * @return string
*/ */
public static function getIpAddress($only_public = false) public function getIpAddress()
{ {
$keys = array( $keys = array(
'HTTP_CLIENT_IP', 'HTTP_CLIENT_IP',
@ -221,23 +269,24 @@ class Request extends Base
); );
foreach ($keys as $key) { foreach ($keys as $key) {
if (isset($_SERVER[$key])) { if (! empty($this->server[$key])) {
foreach (explode(',', $_SERVER[$key]) as $ip_address) { foreach (explode(',', $this->server[$key]) as $ipAddress) {
$ip_address = trim($ip_address); return trim($ipAddress);
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;
}
} }
} }
} }
return t('Unknown'); 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() public function hsts()
{ {
if (Request::isHTTPS()) { if ($this->request->isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000'); 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', "/^(swimlane:)/" => 'T_SWIMLANE',
"/^(ref:)/" => 'T_REFERENCE', "/^(ref:)/" => 'T_REFERENCE',
"/^(reference:)/" => 'T_REFERENCE', "/^(reference:)/" => 'T_REFERENCE',
"/^(link:)/" => 'T_LINK',
"/^(\s+)/" => 'T_WHITESPACE', "/^(\s+)/" => 'T_WHITESPACE',
'/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
'/^(yesterday|tomorrow|today)/' => 'T_DATE', '/^(yesterday|tomorrow|today)/' => 'T_DATE',
@ -118,6 +119,7 @@ class Lexer
case 'T_COLUMN': case 'T_COLUMN':
case 'T_PROJECT': case 'T_PROJECT':
case 'T_SWIMLANE': case 'T_SWIMLANE':
case 'T_LINK':
$next = next($tokens); $next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') { if ($next !== false && $next['token'] === 'T_STRING') {

View file

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

View file

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

View file

@ -16,7 +16,7 @@ class Smtp extends Mail
* Get SwiftMailer transport * Get SwiftMailer transport
* *
* @access protected * @access protected
* @return \Swift_Transport * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport
*/ */
protected function getTransport() 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; namespace Kanboard\Core\Session;
use Kanboard\Core\Base; use Kanboard\Core\Base;
use Kanboard\Core\Http\Request;
/** /**
* Session Manager * Session Manager
@ -13,6 +12,13 @@ use Kanboard\Core\Http\Request;
*/ */
class SessionManager extends Base class SessionManager extends Base
{ {
/**
* Event names
*
* @var string
*/
const EVENT_DESTROY = 'session.destroy';
/** /**
* Return true if the session is open * Return true if the session is open
* *
@ -41,7 +47,7 @@ class SessionManager extends Base
session_name('KB_SID'); session_name('KB_SID');
session_start(); session_start();
$this->container['sessionStorage']->setStorage($_SESSION); $this->sessionStorage->setStorage($_SESSION);
} }
/** /**
@ -51,6 +57,8 @@ class SessionManager extends Base
*/ */
public function close() public function close()
{ {
$this->dispatcher->dispatch(self::EVENT_DESTROY);
// Destroy the session cookie // Destroy the session cookie
$params = session_get_cookie_params(); $params = session_get_cookie_params();
@ -80,7 +88,7 @@ class SessionManager extends Base
SESSION_DURATION, SESSION_DURATION,
$this->helper->url->dir() ?: '/', $this->helper->url->dir() ?: '/',
null, null,
Request::isHTTPS(), $this->request->isHTTPS(),
true true
); );

View file

@ -12,12 +12,13 @@ namespace Kanboard\Core\Session;
* @property array $user * @property array $user
* @property array $flash * @property array $flash
* @property array $csrf * @property array $csrf
* @property array $postAuth * @property array $postAuthenticationValidated
* @property array $filters * @property array $filters
* @property string $redirectAfterLogin * @property string $redirectAfterLogin
* @property string $captcha * @property string $captcha
* @property string $commentSorting * @property string $commentSorting
* @property bool $hasSubtaskInProgress * @property bool $hasSubtaskInProgress
* @property bool $hasRememberMe
* @property bool $boardCollapsed * @property bool $boardCollapsed
*/ */
class SessionStorage class SessionStorage
@ -60,6 +61,21 @@ class SessionStorage
return $session; 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 * 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 <?php
namespace Kanboard\Model; namespace Kanboard\Core\User;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
/** /**
* User Session * User Session
* *
* @package model * @package user
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class UserSession extends Base class UserSession extends Base
@ -18,22 +21,29 @@ class UserSession extends Base
*/ */
public function initialize(array $user) public function initialize(array $user)
{ {
if (isset($user['password'])) { foreach (array('password', 'is_admin', 'is_project_admin', 'twofactor_secret') as $column) {
unset($user['password']); if (isset($user[$column])) {
} unset($user[$column]);
}
if (isset($user['twofactor_secret'])) {
unset($user['twofactor_secret']);
} }
$user['id'] = (int) $user['id']; $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['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; $user['twofactor_activated'] = isset($user['twofactor_activated']) ? (bool) $user['twofactor_activated'] : false;
$this->sessionStorage->user = $user; $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 * @access public
* @return bool * @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 * @access public
* @return bool * @return bool
*/ */
public function has2FA() public function hasPostAuthentication()
{ {
return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true; return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true;
} }
@ -63,7 +83,7 @@ class UserSession extends Base
* *
* @access public * @access public
*/ */
public function disable2FA() public function disablePostAuthentication()
{ {
$this->sessionStorage->user['twofactor_activated'] = false; $this->sessionStorage->user['twofactor_activated'] = false;
} }
@ -76,18 +96,7 @@ class UserSession extends Base
*/ */
public function isAdmin() public function isAdmin()
{ {
return isset($this->sessionStorage->user['is_admin']) && $this->sessionStorage->user['is_admin'] === true; return isset($this->sessionStorage->user['role']) && $this->sessionStorage->user['role'] === Role::APP_ADMIN;
}
/**
* 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;
} }
/** /**
@ -105,7 +114,7 @@ class UserSession extends Base
* Get username * Get username
* *
* @access public * @access public
* @return integer * @return string
*/ */
public function getUsername() 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'])), 'gantt_link' => $this->helper->url->href('gantt', 'project', array('project_id' => $project['id'])),
'color' => $color, 'color' => $color,
'not_defined' => empty($project['start_date']) || empty($project['end_date']), '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