summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--app/Api/User.php10
-rw-r--r--app/Controller/Project.php13
-rw-r--r--app/Controller/User.php10
-rw-r--r--app/Helper/User.php33
-rw-r--r--app/Locale/cs_CZ/translations.php2
-rw-r--r--app/Locale/da_DK/translations.php2
-rw-r--r--app/Locale/de_DE/translations.php2
-rw-r--r--app/Locale/es_ES/translations.php2
-rw-r--r--app/Locale/fi_FI/translations.php2
-rw-r--r--app/Locale/fr_FR/translations.php2
-rw-r--r--app/Locale/hu_HU/translations.php2
-rw-r--r--app/Locale/it_IT/translations.php2
-rw-r--r--app/Locale/ja_JP/translations.php2
-rw-r--r--app/Locale/nl_NL/translations.php2
-rw-r--r--app/Locale/pl_PL/translations.php2
-rw-r--r--app/Locale/pt_BR/translations.php2
-rw-r--r--app/Locale/pt_PT/translations.php2
-rw-r--r--app/Locale/ru_RU/translations.php2
-rw-r--r--app/Locale/sr_Latn_RS/translations.php2
-rw-r--r--app/Locale/sv_SE/translations.php2
-rw-r--r--app/Locale/th_TH/translations.php2
-rw-r--r--app/Locale/tr_TR/translations.php2
-rw-r--r--app/Locale/zh_CN/translations.php2
-rw-r--r--app/Model/Acl.php89
-rw-r--r--app/Model/User.php4
-rw-r--r--app/Model/UserSession.php12
-rw-r--r--app/Schema/Mysql.php7
-rw-r--r--app/Schema/Postgres.php7
-rw-r--r--app/Schema/Sqlite.php7
-rw-r--r--app/Template/activity/project.php2
-rw-r--r--app/Template/analytic/layout.php2
-rw-r--r--app/Template/app/layout.php2
-rw-r--r--app/Template/app/projects.php2
-rw-r--r--app/Template/project/dropdown.php2
-rw-r--r--app/Template/project/edit.php2
-rw-r--r--app/Template/project/index.php2
-rw-r--r--app/Template/project/sidebar.php4
-rw-r--r--app/Template/task/layout.php2
-rw-r--r--app/Template/user/create_local.php1
-rw-r--r--app/Template/user/create_remote.php1
-rw-r--r--app/Template/user/edit.php1
-rw-r--r--app/Template/user/index.php15
-rw-r--r--app/Template/user/show.php2
-rw-r--r--docs/api-json-rpc.markdown3
-rw-r--r--docs/project-permissions.markdown2
-rw-r--r--docs/user-management.markdown43
-rw-r--r--tests/units/AclTest.php88
-rw-r--r--tests/units/UserTest.php8
49 files changed, 337 insertions, 78 deletions
diff --git a/ChangeLog b/ChangeLog
index 6c29301b..230e9973 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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')) ?>&nbsp;
<?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()