diff options
Diffstat (limited to 'modules-available/permissionmanager/inc')
3 files changed, 292 insertions, 98 deletions
diff --git a/modules-available/permissionmanager/inc/getpermissiondata.inc.php b/modules-available/permissionmanager/inc/getpermissiondata.inc.php index 982fa0b7..660c94ae 100644 --- a/modules-available/permissionmanager/inc/getpermissiondata.inc.php +++ b/modules-available/permissionmanager/inc/getpermissiondata.inc.php @@ -1,27 +1,32 @@ <?php -class GetPermissionData { +class GetPermissionData +{ + + const WITH_USER_COUNT = 1; + const WITH_LOCATION_COUNT = 2; /** * Get data for all users. * * @return array array of users (each with userid, username and roles (each with roleid and rolename)) */ - public static function getUserData() { + public static function getUserData() + { $res = Database::simpleQuery("SELECT user.userid AS userid, user.login AS login, role.rolename AS rolename, role.roleid AS roleid FROM user - LEFT JOIN user_x_role ON user.userid = user_x_role.userid - LEFT JOIN role ON user_x_role.roleid = role.roleid + LEFT JOIN role_x_user ON user.userid = role_x_user.userid + LEFT JOIN role ON role_x_user.roleid = role.roleid "); - $userdata= array(); + $userdata = array(); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $userdata[$row['userid'].' '.$row['login']][] = array( + $userdata[$row['userid'] . ' ' . $row['login']][] = array( 'roleid' => $row['roleid'], 'rolename' => $row['rolename'] ); } $data = array(); - foreach($userdata AS $user => $roles) { + foreach ($userdata AS $user => $roles) { $user = explode(" ", $user, 2); $data[] = array( 'userid' => $user[0], @@ -37,8 +42,9 @@ class GetPermissionData { * * @return array array of locations (each including the roles that have permissions for them) */ - public static function getLocationData() { - $res = Database::simpleQuery("SELECT role.roleid as roleid, rolename, GROUP_CONCAT(COALESCE(locationid, 0)) AS locationids FROM role + public static function getLocationData() + { + $res = Database::simpleQuery("SELECT role.roleid AS roleid, rolename, GROUP_CONCAT(COALESCE(locationid, 0)) AS locationids FROM role INNER JOIN role_x_location ON role.roleid = role_x_location.roleid GROUP BY roleid ORDER BY rolename ASC"); $locations = Location::getLocations(0, 0, false, true); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { @@ -61,18 +67,26 @@ class GetPermissionData { /** * Get all roles. * + * @param int $flags Bitmask specifying additional data to fetch (WITH_* constants of this class) * @return array array roles (each with roleid and rolename) */ - public static function getRoles() { - $res = Database::simpleQuery("SELECT roleid, rolename FROM role ORDER BY rolename ASC"); - $data = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $data[] = array( - 'roleid' => $row['roleid'], - 'rolename' => $row['rolename'] - ); + public static function getRoles($flags = 0) + { + $cols = $joins = ''; + if ($flags & self::WITH_USER_COUNT) { + $cols .= ', Count(DISTINCT rxu.userid) AS users'; + $joins .= ' LEFT JOIN role_x_user rxu ON (r.roleid = rxu.roleid)'; } - return $data; + if ($flags & self::WITH_LOCATION_COUNT) { + $cols .= ', Count(DISTINCT rxl.locationid) AS locations'; + $joins .= ' LEFT JOIN role_x_location rxl ON (r.roleid = rxl.roleid)'; + } + if (!empty($joins)) { + $joins .= ' GROUP BY r.roleid'; + } + return Database::queryAll("SELECT r.roleid, r.rolename, r.roledescription $cols FROM role r + $joins + ORDER BY rolename ASC"); } /** @@ -81,8 +95,9 @@ class GetPermissionData { * @param string $roleid id of the role * @return array array containing an array of permissions and an array of locations */ - public static function getRoleData($roleid) { - $query = "SELECT roleid, rolename FROM role WHERE roleid = :roleid"; + public static function getRoleData($roleid) + { + $query = "SELECT roleid, rolename, roledescription FROM role WHERE roleid = :roleid"; $data = Database::queryFirst($query, array("roleid" => $roleid)); $query = "SELECT roleid, locationid FROM role_x_location WHERE roleid = :roleid"; $res = Database::simpleQuery($query, array("roleid" => $roleid)); diff --git a/modules-available/permissionmanager/inc/permissiondbupdate.inc.php b/modules-available/permissionmanager/inc/permissiondbupdate.inc.php index ffe5fac0..0cd89b3a 100644 --- a/modules-available/permissionmanager/inc/permissiondbupdate.inc.php +++ b/modules-available/permissionmanager/inc/permissiondbupdate.inc.php @@ -1,67 +1,107 @@ <?php -class PermissionDbUpdate { +class PermissionDbUpdate +{ /** - * Insert all user/role combinations into the user_x_role table. + * Insert all user/role combinations into the role_x_user table. * - * @param array $users userids - * @param array $roles roleids + * @param int[] $users userids + * @param int[] $roles roleids */ - public static function addRoleToUser($users, $roles) { - $query = "INSERT IGNORE INTO user_x_role (userid, roleid) VALUES (:userid, :roleid)"; - foreach($users AS $userid) { + public static function addRoleToUser($users, $roles) + { + if (empty($users) || empty($roles)) + return 0; + $arg = array(); + foreach ($users AS $userid) { foreach ($roles AS $roleid) { - Database::exec($query, array("userid" => $userid, "roleid" => $roleid)); + $arg[] = compact('userid', 'roleid'); } } + return Database::exec("INSERT IGNORE INTO role_x_user (userid, roleid) VALUES :arg", + ['arg' => $arg]); } /** - * Remove all user/role combinations from the user_x_role table. + * Remove all user/role combinations from the role_x_user table. * - * @param array $users userids - * @param array $roles roleids + * @param int[] $users userids + * @param int[] $roles roleids */ - public static function removeRoleFromUser($users, $roles) { - $query = "DELETE FROM user_x_role WHERE userid IN (:users) AND roleid IN (:roles)"; - Database::exec($query, array("users" => $users, "roles" => $roles)); + public static function removeRoleFromUser($users, $roles) + { + if (empty($users) || empty($roles)) + return 0; + $query = "DELETE FROM role_x_user WHERE userid IN (:users) AND roleid IN (:roles)"; + return Database::exec($query, array("users" => $users, "roles" => $roles)); + } + + /** + * Assign the specified roles to given users, removing any roles from the users + * that are not in the given set. + * + * @param int[] $users list of user ids + * @param int[] $roles list of role ids + */ + public static function setRolesForUser($users, $roles) + { + $count = Database::exec("DELETE FROM role_x_user WHERE userid in (:users) AND roleid NOT IN (:roles)", + compact('users', 'roles')); + return $count + self::addRoleToUser($users, $roles); } /** * Delete role from the role table. * - * @param string $roleid roleid + * @param int $roleid roleid */ - public static function deleteRole($roleid) { - Database::exec("DELETE FROM role WHERE roleid = :roleid", array("roleid" => $roleid)); + public static function deleteRole($roleid) + { + return Database::exec("DELETE FROM role WHERE roleid = :roleid", array("roleid" => $roleid)); } /** * Save changes to a role or create a new one. * - * @param string $rolename rolename - * @param array $locations array of locations - * @param array $permissions array of permissions - * @param string|null $roleid roleid or null if the role does not exist yet + * @param string $roleName rolename + * @param int[] $locations array of locations + * @param string[] $permissions array of permissions + * @param int|null $roleId roleid or null if the role does not exist yet */ - public static function saveRole($rolename, $locations, $permissions, $roleid = NULL) { - if ($roleid) { - Database::exec("UPDATE role SET rolename = :rolename WHERE roleid = :roleid", - array("rolename" => $rolename, "roleid" => $roleid)); - Database::exec("DELETE FROM role_x_location WHERE roleid = :roleid", array("roleid" => $roleid)); - Database::exec("DELETE FROM role_x_permission WHERE roleid = :roleid", array("roleid" => $roleid)); + public static function saveRole($roleName, $roleDescription, $locations, $permissions, $roleId = null) + { + foreach ($permissions as &$permission) { + $permission = strtolower($permission); + } + unset($permission); + if ($roleId) { + Database::exec("UPDATE role SET rolename = :rolename, roledescription = :roledescription WHERE roleid = :roleid", + array("rolename" => $roleName, "roledescription" => $roleDescription, "roleid" => $roleId)); + Database::exec("DELETE FROM role_x_location + WHERE roleid = :roleid AND (locationid NOT IN (:locations) OR locationid IS NULL)", + array("roleid" => $roleId, 'locations' => $locations)); + Database::exec("DELETE FROM role_x_permission + WHERE roleid = :roleid AND permissionid NOT IN (:permissions)", + array("roleid" => $roleId, 'permissions' => $permissions)); } else { - Database::exec("INSERT INTO role (rolename) VALUES (:rolename)", array("rolename" => $rolename)); - $roleid = Database::lastInsertId(); + Database::exec("INSERT INTO role (rolename, roledescription) VALUES (:rolename, :roledescription)", + array("rolename" => $roleName, "roledescription" => $roleDescription)); + $roleId = Database::lastInsertId(); } - foreach ($locations as $locationid) { - Database::exec("INSERT INTO role_x_location (roleid, locationid) VALUES (:roleid, :locationid)", - array("roleid" => $roleid, "locationid" => $locationid)); + + if (!empty($locations)) { + $arg = array_map(function ($loc) use ($roleId) { + return compact('roleId', 'loc'); + }, $locations); + Database::exec("INSERT IGNORE INTO role_x_location (roleid, locationid) VALUES :arg", ['arg' => $arg]); } - foreach ($permissions as $permissionid) { - Database::exec("INSERT INTO role_x_permission (roleid, permissionid) VALUES (:roleid, :permissionid)", - array("roleid" => $roleid, "permissionid" => $permissionid)); + + if (!empty($permissions)) { + $arg = array_map(function ($perm) use ($roleId) { + return compact('roleId', 'perm'); + }, $permissions); + Database::exec("INSERT IGNORE INTO role_x_permission (roleid, permissionid) VALUES :arg", ['arg' => $arg]); } } diff --git a/modules-available/permissionmanager/inc/permissionutil.inc.php b/modules-available/permissionmanager/inc/permissionutil.inc.php index 5ff41046..a3a2b610 100644 --- a/modules-available/permissionmanager/inc/permissionutil.inc.php +++ b/modules-available/permissionmanager/inc/permissionutil.inc.php @@ -2,6 +2,44 @@ class PermissionUtil { + + /** + * Generate all possible variants to match against, eg. $permissionid = a.b.c then we get: + * [ *, a.*, a.b.*, a.b.c ] + * In case $permissionid ends with an asterisk, also set $wildcard and $wclen, e.g. + * $permissionid = a.b.* --> $wildcard = a.b. and $wclen = 4 + * + * @param $permission string|string[] permission to mangle + * @param string[] $compare all the generated variants + * @param string|false $wildcard if $permission is a wildcard string this returns the matching variant + * @param int|false $wclen if $permission is a wildcard string, this is the length of the matching variant + */ + private static function makeComparisonVariants($permission, &$compare, &$wildcard, &$wclen) + { + if (!is_array($permission)) { + $permission = explode('.', $permission); + } + $partCount = count($permission); + $compare = []; + for ($i = 0; $i < $partCount; ++$i) { + $compare[] = $permission[0]; + } + for ($i = 1; $i < $partCount; ++$i) { + $compare[$i - 1] .= '.*'; + for ($j = $i; $j < $partCount; ++$j) { + $compare[$j] .= '.' . $permission[$i]; + } + } + $compare[] = '*'; + + if ($permission[$partCount - 1] === '*') { + $wildcard = substr($compare[$partCount - 1], 0, -1); + $wclen = strlen($wildcard); + } else { + $wclen = $wildcard = false; + } + } + /** * Check if the user has the given permission (for the given location). * @@ -10,26 +48,48 @@ class PermissionUtil * @param int|null $locationid locationid to check or null if the location should be disregarded * @return bool true if user has permission, false if not */ - public static function userHasPermission($userid, $permissionid, $locationid) { - $locations = array(); - if (!is_null($locationid)) { - $locations = Location::getLocationRootChain($locationid); - if (count($locations) == 0) return false; - else $locations[] = 0; + public static function userHasPermission($userid, $permissionid, $locationid) + { + $permissionid = strtolower($permissionid); + self::validatePermission($permissionid); + $parts = explode('.', $permissionid); + // Special case: To prevent lockout, userid === 1 always has permissionmanager.* + if ($parts[0] === 'permissionmanager' && User::getId() === 1) + return true; + // Limit query to first part of permissionid, which is always the module id + $prefix = $parts[0] . '.%'; + if (is_null($locationid)) { + $res = Database::simpleQuery("SELECT permissionid FROM role_x_permission + INNER JOIN role_x_user USING (roleid) + WHERE role_x_user.userid = :userid AND (permissionid LIKE :prefix OR permissionid LIKE '*')", + compact('userid', 'prefix')); + } else { + if ($locationid === 0) { + $locations = [0]; + } else { + $locations = Location::getLocationRootChain($locationid); + if (empty($locations)) { // Non-existent location, still continue as user might have global perms + $locations = [0]; + } + } + $res = Database::simpleQuery("SELECT permissionid FROM role_x_permission + INNER JOIN role_x_user USING (roleid) + INNER JOIN role_x_location USING (roleid) + WHERE role_x_user.userid = :userid AND (permissionid LIKE :prefix OR permissionid LIKE '*') + AND (locationid IN (:locations) OR locationid IS NULL)", + compact('userid', 'prefix', 'locations')); } + // Quick bailout - no results + if ($res->rowCount() === 0) + return false; - $res = Database::simpleQuery("SELECT permissionid, locationid FROM user_x_role - INNER JOIN role_x_permission ON user_x_role.roleid = role_x_permission.roleid - LEFT JOIN (SELECT roleid, COALESCE(locationid, 0) AS locationid FROM role_x_location) t1 - ON role_x_permission.roleid = t1.roleid - WHERE user_x_role.userid = :userid", array("userid" => $userid)); - + // Compare to database result + self::makeComparisonVariants($parts, $compare, $wildcard, $wclen); while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $userPermission = rtrim($row["permissionid"], ".*")."."; - if ((is_null($locationid) || (!is_null($row["locationid"]) && in_array($row["locationid"], $locations))) && - (substr($permissionid.".", 0, strlen($userPermission)) === $userPermission || $userPermission === ".")) { + if (in_array($row['permissionid'], $compare, true)) + return true; + if ($wildcard !== false && strncmp($row['permissionid'], $wildcard, $wclen) === 0) return true; - } } return false; } @@ -41,26 +101,40 @@ class PermissionUtil * @param string $permissionid permissionid to check * @return array array of locationids where the user has the given permission */ - public static function getAllowedLocations($userid, $permissionid) { - - $res = Database::simpleQuery("SELECT permissionid, COALESCE(locationid, 0) AS locationid FROM user_x_role - INNER JOIN role_x_permission ON user_x_role.roleid = role_x_permission.roleid - INNER JOIN role_x_location ON role_x_permission.roleid = role_x_location.roleid - WHERE user_x_role.userid = :userid", array("userid" => $userid)); + public static function getAllowedLocations($userid, $permissionid) + { + $permissionid = strtolower($permissionid); + self::validatePermission($permissionid); + $parts = explode('.', $permissionid); + // Special case: To prevent lockout, userid === 1 always has permissionmanager.* + if ($parts[0] === 'permissionmanager' && User::getId() === 1) { + $allowedLocations = [true]; + } else { + // Limit query to first part of permissionid, which is always the module id + $prefix = $parts[0] . '.%'; + $res = Database::simpleQuery("SELECT permissionid, locationid FROM role_x_permission + INNER JOIN role_x_user USING (roleid) + INNER JOIN role_x_location USING (roleid) + WHERE role_x_user.userid = :userid AND (permissionid LIKE :prefix OR permissionid LIKE '*')", + compact('userid', 'prefix')); - $allowedLocations = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $userPermission = rtrim($row["permissionid"], ".*")."."; - if (substr($permissionid.".", 0, strlen($userPermission)) === $userPermission || $userPermission === ".") { - $allowedLocations[$row["locationid"]] = 1; + // Gather locationid from relevant rows + self::makeComparisonVariants($parts, $compare, $wildcard, $wclen); + $allowedLocations = array(); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if (in_array($row['permissionid'], $compare, true) + || ($wildcard !== false && strncmp($row['permissionid'], $wildcard, $wclen) === 0)) { + $allowedLocations[(int)$row['locationid']] = true; + } } } - $allowedLocations = array_keys($allowedLocations); $locations = Location::getTree(); - if (in_array("0", $allowedLocations)) { - $allowedLocations = array_map("intval", Location::extractIds($locations)); - $allowedLocations[] = 0; + if (isset($allowedLocations[0])) { + // Trivial case - have permission for all locations, so populate list with all valid locationds + $allowedLocations = Location::extractIds($locations); + $allowedLocations[] = 0; // .. plus 0 to show that we have global perms } else { + // We have a specific list of locationds - add any sublocations to list $allowedLocations = self::getSublocations($locations, $allowedLocations); } return $allowedLocations; @@ -70,17 +144,18 @@ class PermissionUtil * Extend an array of locations by adding all sublocations. * * @param array $tree tree of all locations (structured like Location::getTree()) - * @param array $locations the array of locationids to extend + * @param array $allowedLocations the array of locationids to extend * @return array extended array of locationids */ - public static function getSublocations($tree, $locations) { - $result = array_flip($locations); + public static function getSublocations($tree, $allowedLocations) + { + $result = $allowedLocations; foreach ($tree as $location) { if (array_key_exists("children", $location)) { - if (in_array($location["locationid"], $locations)) { + if (isset($allowedLocations[$location["locationid"]])) { $result += array_flip(Location::extractIds($location["children"])); } else { - $result += array_flip(self::getSublocations($location["children"], $locations)); + $result += array_flip(self::getSublocations($location["children"], $allowedLocations)); } } } @@ -88,6 +163,37 @@ class PermissionUtil } /** + * If in debug mode, validate that the checked permission is actually defined + * in the according permissions.json and complain if that's not the case. + * This is supposed to catch misspelled permission checks. + * + * @param string $permissionId permission to check + */ + private static function validatePermission($permissionId) + { + if (!CONFIG_DEBUG || $permissionId === '*') + return; + $split = explode('.', $permissionId, 2); + if (count($split) !== 2) { + trigger_error('[skip:3]Cannot check malformed permission "' . $permissionId . '"', E_USER_WARNING); + return; + } + if ($split[1] === '*') + return; + $data = json_decode(file_get_contents('modules/' . $split[0] . '/permissions/permissions.json'), true); + if (substr($split[1], -2) === '.*') { + $len = strlen($split[1]) - 1; + foreach ($data as $perm => $v) { + if (strncmp($split[1], $perm, $len) === 0) + return; + } + trigger_error('[skip:3]Permission "' . $permissionId . '" does not match anything defined for module', E_USER_WARNING); + } elseif (!is_array($data) || !array_key_exists($split[1], $data)) { + trigger_error('[skip:3]Permission "' . $permissionId . '" not defined for module', E_USER_WARNING); + } + } + + /** * Get all permissions of all active modules that have permissions in their permissions/permissions.json file. * * @return array permission tree as a multidimensional array @@ -100,30 +206,62 @@ class PermissionUtil if (!is_array($data)) continue; preg_match('#^modules/([^/]+)/#', $file, $out); - foreach( $data as $p ) { - $description = Dictionary::translateFileModule($out[1], "permissions", $p); - self::putInPermissionTree($out[1].".".$p, $description, $permissions); + $moduleId = $out[1]; + if (Module::get($moduleId) === false) + continue; + foreach ($data as $perm => $permissionFlags) { + $description = Dictionary::translateFileModule($moduleId, "permissions", $perm); + self::putInPermissionTree($moduleId . "." . $perm, $permissionFlags['location-aware'], $description, $permissions); } } ksort($permissions); global $MENU_CAT_OVERRIDE; $sortingOrder = $MENU_CAT_OVERRIDE; - foreach ($permissions as $module => $v) $sortingOrder[Module::get($module)->getCategory()][] = $module; + foreach ($permissions as $module => $v) { + $sortingOrder[Module::get($module)->getCategory()][] = $module; + } $permissions = array_replace(array_flip(call_user_func_array('array_merge', $sortingOrder)), $permissions); - foreach ($permissions as $module => $v) if (is_int($v)) unset($permissions[$module]); + foreach ($permissions as $module => $v) { + if (is_int($v)) { + unset($permissions[$module]); + } + } return $permissions; } /** + * Get all existing roles. + * + * @param int|false $userid Which user to consider, false = none + * @param bool $onlyMatching true = filter roles the user doesn't have + * @return array list of roles + */ + public static function getRoles($userid = false, $onlyMatching = true) + { + if ($userid === false) { + return Database::queryAll('SELECT roleid, rolename FROM role ORDER BY rolename ASC'); + } + $ret = Database::queryAll('SELECT r.roleid, r.rolename, u.userid AS hasRole FROM role r + LEFT JOIN role_x_user u ON (r.roleid = u.roleid AND u.userid = :userid) + GROUP BY r.roleid + ORDER BY rolename ASC', ['userid' => $userid]); + foreach ($ret as &$role) { + settype($role['hasRole'], 'bool'); + } + return $ret; + } + + /** * Place a permission into the given permission tree. * * @param string $permission the permission to place in the tree + * @param bool $locationAware whether this permissions can be restricted to specific locations only * @param string $description the description of the permission * @param array $tree the permission tree to modify */ - private static function putInPermissionTree($permission, $description, &$tree) + private static function putInPermissionTree($permission, $locationAware, $description, &$tree) { $subPermissions = explode('.', $permission); foreach ($subPermissions as $subPermission) { @@ -134,6 +272,7 @@ class PermissionUtil $tree =& $tree[$subPermission]; } } - $tree = $description; + $tree = array('description' => $description, 'location-aware' => $locationAware, 'isLeaf' => true); } + }
\ No newline at end of file |