diff options
49 files changed, 337 insertions, 78 deletions
@@ -3,6 +3,7 @@ Version 1.0.18 (unreleased) New features: +* Add new role "Project Administrator" * Add login bruteforce protection with captcha and account lockdown * Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList() * Add user api access diff --git a/app/Api/User.php b/app/Api/User.php index 7409a41b..4884c45c 100644 --- a/app/Api/User.php +++ b/app/Api/User.php @@ -27,7 +27,7 @@ class User extends \Core\Base return $this->user->remove($user_id); } - public function createUser($username, $password, $name = '', $email = '', $is_admin = 0) + public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0) { $values = array( 'username' => $username, @@ -36,14 +36,14 @@ class User extends \Core\Base 'name' => $name, 'email' => $email, 'is_admin' => $is_admin, + 'is_project_admin' => $is_project_admin, ); list($valid,) = $this->user->validateCreation($values); - return $valid ? $this->user->create($values) : false; } - public function createLdapUser($username = '', $email = '', $is_admin = 0) + public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0) { $ldap = new Ldap($this->container); $user = $ldap->lookup($username, $email); @@ -58,12 +58,13 @@ class User extends \Core\Base '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) + public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null) { $values = array( 'id' => $id, @@ -71,6 +72,7 @@ class User extends \Core\Base 'name' => $name, 'email' => $email, 'is_admin' => $is_admin, + 'is_project_admin' => $is_project_admin, ); foreach ($values as $key => $value) { diff --git a/app/Controller/Project.php b/app/Controller/Project.php index 45bc2a46..9309cfae 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -141,8 +141,15 @@ class Project extends Base $project = $this->getProject(); $values = $this->request->getValues(); - if ($project['is_private'] == 1 && $this->userSession->isAdmin() && ! isset($values['is_private'])) { - $values += array('is_private' => 0); + if (isset($values['is_private'])) { + if (! $this->helper->user->isProjectAdministrationAllowed($project['id'])) { + unset($values['is_private']); + } + } + else if ($project['is_private'] == 1 && ! isset($values['is_private'])) { + if ($this->helper->user->isProjectAdministrationAllowed($project['id'])) { + $values += array('is_private' => 0); + } } list($valid, $errors) = $this->project->validateModification($values); @@ -402,7 +409,7 @@ class Project extends Base */ public function create(array $values = array(), array $errors = array()) { - $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() ? 0 : 1); + $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() || $this->userSession->isProjectAdmin() ? 0 : 1); $this->response->html($this->template->layout('project/new', array( 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), diff --git a/app/Controller/User.php b/app/Controller/User.php index 10a3a931..04e57417 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -303,12 +303,16 @@ class User extends Base $values = $this->request->getValues(); if ($this->userSession->isAdmin()) { - $values += array('is_admin' => 0); + $values += array('is_admin' => 0, 'is_project_admin' => 0); } else { - + // Regular users can't be admin if (isset($values['is_admin'])) { - unset($values['is_admin']); // Regular users can't be admin + unset($values['is_admin']); + } + + if (isset($values['is_project_admin'])) { + unset($values['is_project_admin']); } } diff --git a/app/Helper/User.php b/app/Helper/User.php index c1fff8c6..cb596fb0 100644 --- a/app/Helper/User.php +++ b/app/Helper/User.php @@ -77,19 +77,44 @@ class User extends \Core\Base } /** - * Proxy cache helper for acl::isManagerActionAllowed() + * Return if the logged user is project admin * * @access public - * @param integer $project_id * @return boolean */ - public function isManager($project_id) + public function isProjectAdmin() + { + return $this->userSession->isProjectAdmin(); + } + + /** + * Check for project administration actions access (Project Admin group) + * + * @access public + * @return boolean + */ + public function isProjectAdministrationAllowed($project_id) + { + if ($this->userSession->isAdmin()) { + return true; + } + + return $this->memoryCache->proxy('acl', 'handleProjectAdminPermissions', $project_id); + } + + /** + * Check for project management actions access (Regular users who are Project Managers) + * + * @access public + * @return boolean + */ + public function isProjectManagementAllowed($project_id) { if ($this->userSession->isAdmin()) { return true; } - return $this->memoryCache->proxy('acl', 'isManagerActionAllowed', $project_id); + return $this->memoryCache->proxy('acl', 'handleProjectManagerPermissions', $project_id); } /** diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index c83fe21b..d968c0b3 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index cd7a6f0d..c48a3339 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 891ed651..ca5a3fa4 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index a1ba9029..feda7b04 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 4acd409c..e65f6a34 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 11a429ee..3f26afe6 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1017,4 +1017,6 @@ return array( 'contributors' => 'contributeurs', 'License:' => 'Licence :', 'License' => 'Licence', + 'Project Administrator' => 'Administrateur de projet', + 'Enter the text below' => 'Entrez le texte ci-dessous', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 18737dc1..d697e9de 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 93800be6..665ecfaa 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 77ebd650..7b424f86 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index fedf0c65..42c10941 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index e55e2dcd..0b4a8b7a 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 8965c5af..009cd01f 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 2f294cfd..0a95b166 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1015,4 +1015,6 @@ return array( 'contributors' => 'contribuidores', 'License:' => 'Licença:', 'License' => 'Licença', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index bc02e35a..d81c5ea0 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index b6b4ca14..e053eddf 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index fd29872a..97cfe42f 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 28e40376..8b2ec551 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index a014dfc1..c16789f4 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index 71c89d91..f5fb4d37 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1015,4 +1015,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index a47886bb..0840f44c 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -32,7 +32,7 @@ class Acl extends Base * @access private * @var array */ - private $member_acl = array( + private $project_member_acl = array( 'board' => '*', 'comment' => '*', 'file' => '*', @@ -56,18 +56,28 @@ class Acl extends Base * @access private * @var array */ - private $manager_acl = array( + private $project_manager_acl = array( 'action' => '*', 'analytic' => '*', 'category' => '*', 'column' => '*', - 'export' => array('tasks', 'subtasks', 'summary'), + 'export' => '*', 'project' => array('edit', 'update', 'share', 'integration', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'), 'swimlane' => '*', 'budget' => '*', ); /** + * Controllers and actions for project admins + * + * @access private + * @var array + */ + private $project_admin_acl = array( + 'project' => array('remove'), + ); + + /** * Controllers and actions for admins * * @access private @@ -77,8 +87,6 @@ class Acl extends Base 'user' => array('index', 'create', 'save', 'remove', 'authentication'), 'config' => '*', 'link' => '*', - 'project' => array('remove'), - 'hourlyrate' => '*', 'currency' => '*', 'twofactor' => array('disable'), ); @@ -149,9 +157,22 @@ class Acl extends Base * @param string $action Action name * @return bool */ - public function isManagerAction($controller, $action) + public function isProjectManagerAction($controller, $action) { - return $this->matchAcl($this->manager_acl, $controller, $action); + return $this->matchAcl($this->project_manager_acl, $controller, $action); + } + + /** + * Return true if the given action is for application managers + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @return bool + */ + public function isProjectAdminAction($controller, $action) + { + return $this->matchAcl($this->project_admin_acl, $controller, $action); } /** @@ -162,9 +183,9 @@ class Acl extends Base * @param string $action Action name * @return bool */ - public function isMemberAction($controller, $action) + public function isProjectMemberAction($controller, $action) { - return $this->matchAcl($this->member_acl, $controller, $action); + return $this->matchAcl($this->project_member_acl, $controller, $action); } /** @@ -189,13 +210,18 @@ class Acl extends Base return false; } + // Check project admin permissions + if ($this->isProjectAdminAction($controller, $action)) { + return $this->handleProjectAdminPermissions($project_id); + } + // Check project manager permissions - if ($this->isManagerAction($controller, $action)) { - return $this->isManagerActionAllowed($project_id); + if ($this->isProjectManagerAction($controller, $action)) { + return $this->handleProjectManagerPermissions($project_id); } // Check project member permissions - if ($this->isMemberAction($controller, $action)) { + if ($this->isProjectMemberAction($controller, $action)) { return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId()); } @@ -203,12 +229,43 @@ class Acl extends Base return true; } - public function isManagerActionAllowed($project_id) + /** + * Handle permission for project manager + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function handleProjectManagerPermissions($project_id) { - if ($this->userSession->isAdmin()) { - return true; + if ($project_id > 0) { + if ($this->userSession->isProjectAdmin()) { + return $this->projectPermission->isMember($project_id, $this->userSession->getId()); + } + + return $this->projectPermission->isManager($project_id, $this->userSession->getId()); + } + + return false; + } + + /** + * Handle permission for project admins + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function handleProjectAdminPermissions($project_id) + { + if (! $this->userSession->isProjectAdmin()) { + return false; } - return $project_id > 0 && $this->projectPermission->isManager($project_id, $this->userSession->getId()); + if ($project_id > 0) { + return $this->projectPermission->isMember($project_id, $this->userSession->getId()); + } + + return true; } } diff --git a/app/Model/User.php b/app/Model/User.php index 8daef3f2..76af342d 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -57,6 +57,7 @@ class User extends Base 'name', 'email', 'is_admin', + 'is_project_admin', 'is_ldap_user', 'notifications_enabled', 'google_id', @@ -254,7 +255,7 @@ class User extends Base } $this->removeFields($values, array('confirmation', 'current_password')); - $this->resetFields($values, array('is_admin', 'is_ldap_user')); + $this->resetFields($values, array('is_admin', 'is_ldap_user', 'is_project_admin')); } /** @@ -442,6 +443,7 @@ class User extends Base new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), new Validators\Email('email', t('Email address invalid')), new Validators\Integer('is_admin', t('This value must be an integer')), + new Validators\Integer('is_project_admin', t('This value must be an integer')), new Validators\Integer('is_ldap_user', t('This value must be an integer')), ); } diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php index 44a9c2a2..1ae3fdf4 100644 --- a/app/Model/UserSession.php +++ b/app/Model/UserSession.php @@ -34,6 +34,7 @@ class UserSession extends Base $user['id'] = (int) $user['id']; $user['is_admin'] = (bool) $user['is_admin']; + $user['is_project_admin'] = (bool) $user['is_project_admin']; $user['is_ldap_user'] = (bool) $user['is_ldap_user']; $user['twofactor_activated'] = (bool) $user['twofactor_activated']; @@ -74,6 +75,17 @@ class UserSession extends Base } /** + * Return true if the logged user is project admin + * + * @access public + * @return bool + */ + public function isProjectAdmin() + { + return isset($this->session['user']['is_project_admin']) && $this->session['user']['is_project_admin'] === true; + } + + /** * Get the connected user id * * @access public diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 31ffaa32..9bd610bb 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,12 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 82; +const VERSION = 83; + +function version_83($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_project_admin INT DEFAULT 0"); +} function version_82($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 876df981..e1ba07e3 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 62; +const VERSION = 63; + +function version_63($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_project_admin INTEGER DEFAULT 0"); +} function version_62($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 4b8f5ac6..15268d9a 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 78; +const VERSION = 79; + +function version_79($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_project_admin INTEGER DEFAULT 0"); +} function version_78($pdo) { diff --git a/app/Template/activity/project.php b/app/Template/activity/project.php index 480bbadd..bc585212 100644 --- a/app/Template/activity/project.php +++ b/app/Template/activity/project.php @@ -19,7 +19,7 @@ <i class="fa fa-calendar fa-fw"></i> <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> </li> - <?php if ($this->user->isManager($project['id'])): ?> + <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?> <li> <i class="fa fa-cog fa-fw"></i> <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?> diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php index 9d6bf77c..fd2090ae 100644 --- a/app/Template/analytic/layout.php +++ b/app/Template/analytic/layout.php @@ -19,7 +19,7 @@ <i class="fa fa-calendar fa-fw"></i> <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> </li> - <?php if ($this->user->isManager($project['id'])): ?> + <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?> <li> <i class="fa fa-cog fa-fw"></i> <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?> diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php index 4a307a19..de561ded 100644 --- a/app/Template/app/layout.php +++ b/app/Template/app/layout.php @@ -1,7 +1,7 @@ <section id="main"> <div class="page-header page-header-mobile"> <ul> - <?php if ($this->user->isAdmin()): ?> + <?php if ($this->user->isProjectAdmin()): ?> <li> <i class="fa fa-plus fa-fw"></i> <?= $this->url->link(t('New project'), 'project', 'create') ?> diff --git a/app/Template/app/projects.php b/app/Template/app/projects.php index 627ad21b..43db85bd 100644 --- a/app/Template/app/projects.php +++ b/app/Template/app/projects.php @@ -16,7 +16,7 @@ <?= $this->url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?> </td> <td> - <?php if ($this->user->isManager($project['id'])): ?> + <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?> <?= $this->url->link('<i class="fa fa-cog"></i>', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Settings')) ?> <?php endif ?> diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php index aa4322e6..0a53cc05 100644 --- a/app/Template/project/dropdown.php +++ b/app/Template/project/dropdown.php @@ -9,7 +9,7 @@ </li> <?php endif ?> -<?php if ($this->user->isManager($project['id'])): ?> +<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?> <li> <i class="fa fa-line-chart fa-fw"></i> <?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?> diff --git a/app/Template/project/edit.php b/app/Template/project/edit.php index 794267f4..c8f235c7 100644 --- a/app/Template/project/edit.php +++ b/app/Template/project/edit.php @@ -13,7 +13,7 @@ <?= $this->form->text('identifier', $values, $errors, array('maxlength="50"')) ?> <p class="form-help"><?= t('The project identifier is an optional alphanumeric code used to identify your project.') ?></p> - <?php if ($this->user->isAdmin()): ?> + <?php if ($this->user->isAdmin() || $this->user->isProjectAdministrationAllowed($project['id'])): ?> <?= $this->form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?> <?php endif ?> diff --git a/app/Template/project/index.php b/app/Template/project/index.php index 971ba2ae..679bdd00 100644 --- a/app/Template/project/index.php +++ b/app/Template/project/index.php @@ -1,7 +1,7 @@ <section id="main"> <div class="page-header"> <ul> - <?php if ($this->user->isAdmin()): ?> + <?php if ($this->user->isProjectAdmin()): ?> <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li> <?php endif ?> <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?></li> diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php index 7ee39f53..d6f7db97 100644 --- a/app/Template/project/sidebar.php +++ b/app/Template/project/sidebar.php @@ -5,7 +5,7 @@ <?= $this->url->link(t('Summary'), 'project', 'show', array('project_id' => $project['id'])) ?> </li> - <?php if ($this->user->isManager($project['id'])): ?> + <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?> <li> <?= $this->url->link(t('Public access'), 'project', 'share', array('project_id' => $project['id'])) ?> </li> @@ -42,7 +42,7 @@ <?= $this->url->link(t('Enable'), 'project', 'enable', array('project_id' => $project['id']), true) ?> <?php endif ?> </li> - <?php if ($this->user->isAdmin()): ?> + <?php if ($this->user->isProjectAdministrationAllowed($project['id'])): ?> <li> <?= $this->url->link(t('Remove'), 'project', 'remove', array('project_id' => $project['id'])) ?> </li> diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php index bbccf177..6b6e827a 100644 --- a/app/Template/task/layout.php +++ b/app/Template/task/layout.php @@ -9,7 +9,7 @@ <i class="fa fa-calendar fa-fw"></i> <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?> </li> - <?php if ($this->user->isManager($task['project_id'])): ?> + <?php if ($this->user->isProjectManagementAllowed($task['project_id'])): ?> <li> <i class="fa fa-cog fa-fw"></i> <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?> diff --git a/app/Template/user/create_local.php b/app/Template/user/create_local.php index aeec300f..3c8b43b0 100644 --- a/app/Template/user/create_local.php +++ b/app/Template/user/create_local.php @@ -39,6 +39,7 @@ <?= $this->form->checkbox('notifications_enabled', t('Enable notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?> <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> + <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?> </div> <div class="form-actions"> diff --git a/app/Template/user/create_remote.php b/app/Template/user/create_remote.php index 52661585..6b3678d3 100644 --- a/app/Template/user/create_remote.php +++ b/app/Template/user/create_remote.php @@ -39,6 +39,7 @@ <?= $this->form->checkbox('notifications_enabled', t('Enable notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?> <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> + <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?> <?= $this->form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?> </div> diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php index ea7e3875..a60ee681 100644 --- a/app/Template/user/edit.php +++ b/app/Template/user/edit.php @@ -24,6 +24,7 @@ <?php if ($this->user->isAdmin()): ?> <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?><br/> + <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1) ?><br/> <?php endif ?> <div class="form-actions"> diff --git a/app/Template/user/index.php b/app/Template/user/index.php index edf043a6..d74aa748 100644 --- a/app/Template/user/index.php +++ b/app/Template/user/index.php @@ -18,9 +18,9 @@ <th><?= $paginator->order(t('Name'), 'name') ?></th> <th><?= $paginator->order(t('Email'), 'email') ?></th> <th><?= $paginator->order(t('Administrator'), 'is_admin') ?></th> + <th><?= $paginator->order(t('Project Administrator'), 'is_project_admin') ?></th> <th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th> <th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th> - <th><?= t('External accounts') ?></th> <th><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></th> </tr> <?php foreach ($paginator->getCollection() as $user): ?> @@ -41,6 +41,9 @@ <?= $user['is_admin'] ? t('Yes') : t('No') ?> </td> <td> + <?= $user['is_project_admin'] ? t('Yes') : t('No') ?> + </td> + <td> <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?> </td> <td> @@ -51,16 +54,6 @@ <?php endif ?> </td> <td> - <ul class="no-bullet"> - <?php if ($user['google_id']): ?> - <li><i class="fa fa-google fa-fw"></i><?= t('Google account linked') ?></li> - <?php endif ?> - <?php if ($user['github_id']): ?> - <li><i class="fa fa-github fa-fw"></i><?= t('Github account linked') ?></li> - <?php endif ?> - </ul> - </td> - <td> <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?> </td> </tr> diff --git a/app/Template/user/show.php b/app/Template/user/show.php index acb02f71..220ad87e 100644 --- a/app/Template/user/show.php +++ b/app/Template/user/show.php @@ -11,7 +11,7 @@ <h2><?= t('Security') ?></h2> </div> <ul class="listing"> - <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></strong></li> + <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : ($user['is_project_admin'] ? t('Project Administrator') : t('Regular user')) ?></strong></li> <li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li> <li><?= $user['twofactor_activated'] == 1 ? t('Two factor authentication enabled') : t('Two factor authentication disabled') ?></li> </ul> diff --git a/docs/api-json-rpc.markdown b/docs/api-json-rpc.markdown index 7c211081..61852933 100644 --- a/docs/api-json-rpc.markdown +++ b/docs/api-json-rpc.markdown @@ -2632,6 +2632,7 @@ Response example: - **name** (string, optional) - **email** (string, optional) - **is_admin** Set the value 1 for admins or 0 for regular users (integer, optional) + - **is_project_admin** Set the value 1 for project admins or 0 for regular users (integer, optional) - Result on success: **user_id** - Result on failure: **false** @@ -2666,6 +2667,7 @@ Response example: - **username** (string, optional if email is set) - **email** (string, optional if username is set) - **is_admin** Set the value 1 for admins or 0 for regular users (integer, optional) + - **is_project_admin** Set the value 1 for project admins or 0 for regular users (integer, optional) - Result on success: **user_id** - Result on failure: **false** @@ -2787,6 +2789,7 @@ Response example: - **name** (string, optional) - **email** (string, optional) - **is_admin** (integer, optional) + - **is_project_admin** Set the value 1 for project admins or 0 for regular users (integer, optional) - Result on success: **true** - Result on failure: **false** diff --git a/docs/project-permissions.markdown b/docs/project-permissions.markdown index af3adcd6..d4aa88e3 100644 --- a/docs/project-permissions.markdown +++ b/docs/project-permissions.markdown @@ -4,7 +4,7 @@ Project permissions A project can have two kinds of people: **project managers** and **project members**. - Project managers can manage the configuration of the project and access to the reports. -- Project members are standard users, they have less privileges. +- Project members can only do basic operations (create or move tasks). When you create a new project, you are automatically assigned as a project manager. diff --git a/docs/user-management.markdown b/docs/user-management.markdown index 98691ddd..25cc8539 100644 --- a/docs/user-management.markdown +++ b/docs/user-management.markdown @@ -1,22 +1,45 @@ User management =============== -Group of users --------------- +Roles at the application level +------------------------------ + +Kanboard use a basic permission system, there are 3 type of users: + +### Administrators + +- Access to everything + +### Project Administrators + +- Can create multi-users and private projects +- Can convert multi-users and private projects +- Can see only their own projects +- Cannot change application settings +- Cannot manage users + +### Standard Users + +- Can create only private projects +- Can see only their own projects +- Cannot remove projects + +Roles at the project level +-------------------------- -Kanboard use a basic permission system, there is two kind of users: +These role are related to the project permission. -- Administrators -- Standard users +### Project Managers -Administrator have access to everything. By example, they can add or remove projects. +- Can manage only their own projects +- Can access to reports and budget section -There is also permissions defined at the project level, users can be seen as: +### Project Members -- Project member -- Project manager +- Can do any daily operations in their projects (create and move tasks...) +- Cannot configure projects -Project managers have more privileges than a simple user member. +Note: Any "Standard User" can be promotted "Project Manager" for a given project, they don't necessary need to be "Project Administrator". Local and remote users ---------------------- diff --git a/tests/units/AclTest.php b/tests/units/AclTest.php index 05e8561e..4d735dfb 100644 --- a/tests/units/AclTest.php +++ b/tests/units/AclTest.php @@ -35,12 +35,18 @@ class AclTest extends Base public function testPublicActions() { $acl = new Acl($this->container); + $this->assertTrue($acl->isPublicAction('task', 'readonly')); $this->assertTrue($acl->isPublicAction('board', 'readonly')); $this->assertFalse($acl->isPublicAction('board', 'show')); $this->assertTrue($acl->isPublicAction('feed', 'project')); $this->assertTrue($acl->isPublicAction('feed', 'user')); + $this->assertTrue($acl->isPublicAction('ical', 'project')); + $this->assertTrue($acl->isPublicAction('ical', 'user')); $this->assertTrue($acl->isPublicAction('oauth', 'github')); $this->assertTrue($acl->isPublicAction('oauth', 'google')); + $this->assertTrue($acl->isPublicAction('auth', 'login')); + $this->assertTrue($acl->isPublicAction('auth', 'check')); + $this->assertTrue($acl->isPublicAction('auth', 'captcha')); } public function testAdminActions() @@ -54,21 +60,32 @@ class AclTest extends Base $this->assertTrue($acl->isAdminAction('user', 'save')); } - public function testManagerActions() + public function testProjectAdminActions() { $acl = new Acl($this->container); - $this->assertFalse($acl->isManagerAction('board', 'readonly')); - $this->assertFalse($acl->isManagerAction('project', 'remove')); - $this->assertFalse($acl->isManagerAction('project', 'show')); - $this->assertTrue($acl->isManagerAction('project', 'disable')); - $this->assertTrue($acl->isManagerAction('category', 'index')); - $this->assertTrue($acl->isManagerAction('project', 'users')); - $this->assertFalse($acl->isManagerAction('app', 'index')); + $this->assertFalse($acl->isProjectAdminAction('config', 'save')); + $this->assertFalse($acl->isProjectAdminAction('user', 'index')); + $this->assertTrue($acl->isProjectAdminAction('project', 'remove')); + } + + public function testProjectManagerActions() + { + $acl = new Acl($this->container); + $this->assertFalse($acl->isProjectManagerAction('board', 'readonly')); + $this->assertFalse($acl->isProjectManagerAction('project', 'remove')); + $this->assertFalse($acl->isProjectManagerAction('project', 'show')); + $this->assertTrue($acl->isProjectManagerAction('project', 'disable')); + $this->assertTrue($acl->isProjectManagerAction('category', 'index')); + $this->assertTrue($acl->isProjectManagerAction('project', 'users')); + $this->assertFalse($acl->isProjectManagerAction('app', 'index')); } public function testPageAccessNoSession() { $acl = new Acl($this->container); + $session = new Session; + $session = array(); + $this->assertFalse($acl->isAllowed('board', 'readonly')); $this->assertFalse($acl->isAllowed('task', 'show')); $this->assertFalse($acl->isAllowed('config', 'application')); @@ -81,7 +98,6 @@ class AclTest extends Base { $acl = new Acl($this->container); $session = new Session; - $session['user'] = array(); $this->assertFalse($acl->isAllowed('board', 'readonly')); @@ -106,15 +122,60 @@ class AclTest extends Base $this->assertTrue($acl->isAllowed('webhook', 'github')); $this->assertTrue($acl->isAllowed('task', 'show')); $this->assertTrue($acl->isAllowed('task', 'update')); - $this->assertTrue($acl->isAllowed('project', 'show')); $this->assertTrue($acl->isAllowed('config', 'application')); + $this->assertTrue($acl->isAllowed('project', 'show')); $this->assertTrue($acl->isAllowed('project', 'users')); + $this->assertTrue($acl->isAllowed('project', 'remove')); $this->assertTrue($acl->isAllowed('category', 'edit')); $this->assertTrue($acl->isAllowed('task', 'remove')); $this->assertTrue($acl->isAllowed('app', 'index')); } - public function testPageAccessManager() + public function testPageAccessProjectAdmin() + { + $acl = new Acl($this->container); + $p = new Project($this->container); + $pp = new ProjectPermission($this->container); + $u = new User($this->container); + $session = new Session; + + // We create our user + $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest'))); + + // We create a project and set our user as project manager + $this->assertEquals(1, $p->create(array('name' => 'UnitTest'))); + $this->assertTrue($pp->addMember(1, 2)); + $this->assertTrue($pp->isMember(1, 2)); + $this->assertFalse($pp->isManager(1, 2)); + + // We fake a session for him + $session['user'] = array( + 'id' => 2, + 'is_admin' => false, + 'is_project_admin' => true, + ); + + $this->assertTrue($acl->isAllowed('board', 'readonly', 1)); + $this->assertTrue($acl->isAllowed('task', 'readonly', 1)); + $this->assertTrue($acl->isAllowed('webhook', 'github', 1)); + $this->assertTrue($acl->isAllowed('task', 'show', 1)); + $this->assertFalse($acl->isAllowed('task', 'show', 2)); + $this->assertTrue($acl->isAllowed('task', 'update', 1)); + $this->assertTrue($acl->isAllowed('project', 'show', 1)); + $this->assertFalse($acl->isAllowed('config', 'application', 1)); + + $this->assertTrue($acl->isAllowed('project', 'users', 1)); + $this->assertFalse($acl->isAllowed('project', 'users', 2)); + + $this->assertTrue($acl->isAllowed('project', 'remove', 1)); + $this->assertFalse($acl->isAllowed('project', 'remove', 2)); + + $this->assertTrue($acl->isAllowed('category', 'edit', 1)); + $this->assertTrue($acl->isAllowed('task', 'remove', 1)); + $this->assertTrue($acl->isAllowed('app', 'index', 1)); + } + + public function testPageAccessProjectManager() { $acl = new Acl($this->container); $p = new Project($this->container); @@ -144,8 +205,13 @@ class AclTest extends Base $this->assertTrue($acl->isAllowed('task', 'update', 1)); $this->assertTrue($acl->isAllowed('project', 'show', 1)); $this->assertFalse($acl->isAllowed('config', 'application', 1)); + $this->assertTrue($acl->isAllowed('project', 'users', 1)); $this->assertFalse($acl->isAllowed('project', 'users', 2)); + + $this->assertFalse($acl->isAllowed('project', 'remove', 1)); + $this->assertFalse($acl->isAllowed('project', 'remove', 2)); + $this->assertTrue($acl->isAllowed('category', 'edit', 1)); $this->assertTrue($acl->isAllowed('task', 'remove', 1)); $this->assertTrue($acl->isAllowed('app', 'index', 1)); diff --git a/tests/units/UserTest.php b/tests/units/UserTest.php index 6c68dfd2..e3fa5f76 100644 --- a/tests/units/UserTest.php +++ b/tests/units/UserTest.php @@ -112,6 +112,7 @@ class UserTest extends Base $u = new User($this->container); $this->assertNotFalse($u->create(array('username' => 'toto', 'password' => '123456', 'name' => 'Toto'))); $this->assertNotFalse($u->create(array('username' => 'titi', 'is_ldap_user' => 1))); + $this->assertNotFalse($u->create(array('username' => 'papa', 'is_project_admin' => 1))); $this->assertFalse($u->create(array('username' => 'toto'))); $user = $u->getById(1); @@ -137,6 +138,13 @@ class UserTest extends Base $this->assertEquals('', $user['name']); $this->assertEquals(0, $user['is_admin']); $this->assertEquals(1, $user['is_ldap_user']); + + $user = $u->getById(4); + $this->assertNotFalse($user); + $this->assertTrue(is_array($user)); + $this->assertEquals('papa', $user['username']); + $this->assertEquals(0, $user['is_admin']); + $this->assertEquals(1, $user['is_project_admin']); } public function testUpdate() |