Source of file PermissionService.php
Size: 26,526 Bytes - Last Modified: 2021-12-23T10:34:03+00:00
/var/www/docs.ssmods.com/process/src/code/services/PermissionService.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867 | <?php /** * A service interface to functionality related to getting and setting * permission information for nodes * * @author marcus@silverstripe.com.au * @license BSD License http://silverstripe.org/bsd-license/ */ class PermissionService { const SOURCES_MAP = 'sources_map'; const ITEM_PREFIX = 'perm_'; public function __construct() { } /** * * Allow this service to be accessed from the web * * @return array */ public function webEnabledMethods() { return array( 'removeAuthority' => 'POST', 'grantTo' => 'POST', 'checkPerm' => 'GET', 'getPermissionsFor' => 'GET', 'getPermissionDetails' => 'GET' ); } /** * @var Zend_Cache_Core */ protected $cache; protected $parents = array(); protected $groups = array(); /** * * @return Zend_Cache_Core */ public function getCache() { if (!$this->cache) { $this->cache = SS_Cache::factory('restricted_perms', 'Output', array( 'automatic_serialization' => true, 'automatic_cleaning_factor' => 0, // no need for cleaning, re-uses the same keys )); } return $this->cache; } public function getAllRoles() { return DataObject::get('AccessRole'); } public function getPermissionDetails() { return array( 'roles' => $this->getAllRoles(), 'permissions' => $this->allPermissions(), // 'users' => DataObject::get('Member'), // 'groups' => DataObject::get('Group') ); } protected $allPermissions; public function allPermissions() { if (!$this->allPermissions) { $options = array(); $definers = ClassInfo::implementorsOf('PermissionDefiner'); $perms = array(); foreach ($definers as $definer) { $cls = new $definer(); $perms = array_merge($perms, $cls->definePermissions()); } $this->allPermissions = $perms; } return $this->allPermissions; } public function flushCache() { $this->parents = array(); $this->groups = array(); } /** * Alternative grant method using group name or email address * * @param DataObject $node * @param type $perm * @param DataObject $to * @param type $grant */ public function grantTo(DataObject $node, $perm, $email, $group, $grant = 'GRANT') { $userObj = $groupObj = null; if (strlen($email)) { $userObj = DataObject::get_one('Member', '"Email" = \''. Convert::raw2sql($email).'\''); } elseif (strlen($group)) { $groupObj = DataObject::get_one('Group', '"Title" = \'' . Convert::raw2sql($group).'\''); } $to = $userObj ? $userObj : $groupObj; if (!$to) { return array('status' => false, 'message' => 'Unknown authority'); } return $this->grant($node, $perm, $to, $grant); } /** * Delete an authority * * @param DataObject $node * The node to delete from * @param DataObject $authority * The AccessAuthority we're removing */ public function removeAuthority(DataObject $node, DataObject $authority) { if (!$this->checkPerm($node, 'DeletePermissions')) { throw new PermissionDeniedException("You do not have permission to do that"); } if ($authority) { $authority->delete(); } return $node; } /** * Grants a specific permission to a given user or group * * @param string $perm * @param Member|Group $to */ public function grant(DataObject $node, $perm, DataObject $to, $grant = 'GRANT') { // make sure we can !! if (!$this->checkPerm($node, 'ChangePermissions')) { throw new PermissionDeniedException("You do not have permission to do that"); } $role = DataObject::get_one('AccessRole', '"Title" = \'' . Convert::raw2sql($perm) . '\''); $composedOf = array($perm); if ($role && $role->exists()) { $composedOf = $role->Composes->getValues(); } $type = $to instanceof Member ? 'Member' : get_class($to); $filter = array( 'Type' => $type, 'AuthorityID' => $to->ID, 'ItemID' => $node->ID, 'ItemType' => $node->class, 'Grant' => $grant, ); $list = DataList::create('AccessAuthority')->filter($filter); $existing = $list->first(); if (!$existing || !$existing->exists()) { $existing = new AccessAuthority; $existing->Type = $type; $existing->AuthorityID = $to->ID; $existing->ItemID = $node->ID; $existing->ItemType = $node->class; $existing->Grant = $grant; $existing->Role = $role ? $role->Title : ''; } $currentRoles = $existing->Perms->getValues(); if (!$currentRoles) { $new = $composedOf; } else { $new = array_merge($currentRoles, $composedOf); } $new = array_unique($new); $existing->Perms = $new; $existing->write(); $this->clearPermCacheFor($node); return $existing; } /** * Check for the presence of ALL permissions in a given role for the user to an object * * @param DataObject $node * The object to check perms on * @param string $role * The role to check against * @param Member $member * The member to check -if not set, the current user is used */ public function checkRole(DataObject $node, $role, $member = null) { $role = DataObject::get_one('AccessRole', '"Title" = \'' . Convert::raw2sql($role) . '\''); if ($role && $role->exists()) { $composedOf = $role->Composes->getValues(); if ($composedOf && is_array($composedOf)) { foreach ($composedOf as $perm) { if (!$this->checkPerm($node, $perm, $member)) { return false; } } return true; } } return false; } /** * Removes a set of permissions applied on an object to a particular user/group * * @param DataObject $node * @param type $perm * @param DataObject $to * @param type $grant */ public function removePermissions(DataObject $node, $perm, DataObject $userOrGroup, $grant = 'GRANT') { if (!$this->checkPerm($node, 'ChangePermissions')) { throw new PermissionDeniedException("You do not have permission to do that"); } $composedOf = $perm; if (!is_array($perm)) { $role = DataObject::get_one('AccessRole', '"Title" = \'' . Convert::raw2sql($perm) . '\''); $composedOf = array($perm); if ($role && $role->exists()) { $composedOf = $role->Composes->getValues(); } } $type = $userOrGroup instanceof Member ? 'Member' : get_class($userOrGroup); $filter = array( 'Type' => $type, 'AuthorityID' => $userOrGroup->ID, 'ItemID' => $node->ID, 'ItemType' => $node->class, 'Grant' => $grant, ); $existing = DataList::create('AccessAuthority')->filter($filter)->first(); if (!$existing || !$existing->exists()) { return; } $current = $existing->Perms->getValues(); if (is_array($current) && count($current)) { $new = array_diff($current, $composedOf); $existing->Perms = $new; $existing->write(); $this->clearPermCacheFor($node); if (!count($new)) { try { $this->removeAuthority($node, $existing); } catch (Exception $e) { // oh well } } } } /** * Return true or false as to whether a given user can access an object * * @param DataObject $node * The object to check perms on * @param string $perm * The permission to check against * @param Member $member * The member to check - if not set, the current user is used * * @return type */ public function checkPerm(DataObject $node, $perm, $member=null) { // if the node doesn't use the extension, fall back to SS logic if (!$node->hasExtension('Restrictable')) { switch ($perm) { case 'View': { return $node->canView($member); } case 'Write': { return $node->canEdit($member); } default: { return $node->can($perm, $member); } } } if (!$node) { return false; } if (!$member) { $member = singleton('SecurityContext')->getMember(); } if (is_int($member)) { $member = DataObject::get_by_id('Member', $member); } if (Permission::check('ADMIN', 'any', $member)) { return true; } $permCache = $this->getCache(); /* @var $permCache Zend_Cache_Core */ $key = $this->permCacheKey($node, $perm); $userGrants = null; if ($key) { $userGrants = $permCache->load($key); if (count($userGrants)) { $userGrants = $this->sanitiseCacheData($userGrants); } } if ($member && $userGrants && isset($userGrants[$perm][$member->ID])) { return $userGrants[$perm][$member->ID]; } // okay, we need to build up all the info we have about the node for permissions $s = $this->realiseAllSources($node); if (!$userGrants) { $userGrants = array(); } if (!isset($userGrants[$perm])) { $userGrants[$perm] = array(); } $result = null; // if no member, just check public view $public = $this->checkPublicPerms($node, $perm); if ($public) { $result = true; } // can return immediately if (!$member) { return $result; } if (is_null($result)) { // see whether we're the owner, and if the perm we're checking is in that list if ($this->checkOwnerPerms($node, $perm, $member)) { $result = true; } } $accessAuthority = ''; $directGrant = null; $can = false; if (is_null($result)) { $filter = array( 'ItemID' => $node->ID, 'ItemType' => $node->class, ); $existing = DataList::create('AccessAuthority')->filter($filter); // get all access authorities for this object $gids = isset($this->groups[$member->ID]) ? $this->groups[$member->ID] : null; if (!$gids) { $groups = $member ? $member->Groups() : array(); $gids = array(); if ($groups && $groups->Count()) { $gids = $groups->map('ID', 'ID')->toArray(); } $this->groups[$member->ID] = $gids; } $can = false; $directGrant = 'NONE'; if ($existing && $existing->count()) { foreach ($existing as $access) { // check if this mentions the perm in question $perms = $access->Perms->getValues(); if ($perms) { if (!in_array($perm, $perms)) { continue; } } $grant = null; $authority = $access->getAuthority(); if ($authority instanceof Group) { if (isset($gids[$access->AuthorityID])) { $grant = $access->Grant; } } elseif ($authority instanceof Member) { if ($member->ID == $access->AuthorityID) { $grant = $access->Grant; } } else { // another mechanism that will require a lookup of members in a list // TODO cache this if ($authority instanceof ListOfMembers) { $listMembers = $authority->getAllMembers()->map('ID', 'Title'); if (isset($listMembers[$member->ID])) { $grant = $access->Grant; } } } if ($grant) { // if it's deny, we can just break away immediately, otherwise we need to evaluate all the // others in case there's another DENY in there somewhere if ($grant === 'DENY') { $directGrant = 'DENY'; // immediately break break; } else { // mark that it's been granted for now $directGrant = 'GRANT'; } } } } } // return immediately if we have something if ($directGrant === 'GRANT') { $result = true; } if ($directGrant === 'DENY') { $result = false; } // otherwise query our parents if (is_null($result) && $node->InheritPerms) { $permParents = $this->getEffectiveParents($node); if (count($permParents) || $permParents instanceof IteratorAggregate) { foreach ($permParents as $permParent) { if ($permParent && $this->checkPerm($permParent, $perm, $member)) { $result = true; } } } } if (is_null($result)) { $result = false; } $userGrants[$perm][$member->ID] = $result; if ($key) { $permCache->save($userGrants, $key); } return $result; } protected function sanitiseCacheData($allGrants) { $sanitised = array(); if (!is_array($allGrants)) { return $sanitised; } foreach ($allGrants as $perm => $granted) { $sanitised[$perm] = array(); if (!is_array($granted)) { continue; } foreach ($granted as $memberId => $grant) { if (is_bool($grant)) { $sanitised[$perm][$memberId] = $grant; } } } return $sanitised; } /** * Checks the permissions for a public user * * @param string $perm */ public function checkPublicPerms(DataObject $node, $perm) { if ($perm == 'View') { if ($node->PublicAccess) { return true; } if ($node->InheritPerms) { $permParents = $this->getEffectiveParents($node); if (count($permParents) || $permParents instanceof IteratorAggregate) { foreach ($permParents as $permParent) { if ($permParent && $this->checkPublicPerms($permParent, $perm)) { return true; } } } } } return false; } public function getEffectiveParents($node) { $key = get_class($node) . '-' . $node->ID; if (isset($this->parents[$key])) { return $this->parents[$key]; } // determine what we're looking up if (method_exists($node, 'effectiveParents')) { $this->parents[$key] = $node->effectiveParents(); return $this->parents[$key]; } if (method_exists($node, 'effectiveParent')) { $this->parents[$key] = array($node->effectiveParent()); return $this->parents[$key]; } // otherwise, put it together ourselves $fullResult = array(); $result = null; if (method_exists($node, 'permissionSource')) { $result = $node->permissionSource(); } else { $result = $this->parentFor($node); if ($result && !$result->ID) { $result = null; } } if ($result) { $fullResult = array($result); } if (method_exists($node, 'permissionSources')) { $result = $node->permissionSources(); foreach ($result as $r) { $fullResult[] = $r; } } $node->extend('updateEffectiveParents', $fullResult); if ($key && $node->ID) { $this->parents[$key] = $fullResult; } return $fullResult; } /** * @deprecated * * @param string $type * @param DataObject $node * @return array */ public function getEffective($type, $node) { return $this->getEffectiveParents($node); } protected function parentFor($node) { if (!$node->ParentID) { return; } $key = 'parent-' . get_class($node) . '-' . $node->ParentID; if (isset($this->parents[$key])) { return $this->parents[$key]; } $this->parents[$key] = $node->Parent(); return $this->parents[$key]; } /** * Is the member the owner of this object, and is the permission being checked * in the list of permissions that owners have? * * @param string $perm * @param Member $member * @return boolean */ protected function checkOwnerPerms(DataObject $node, $perm, $member) { $ownerId = $node->OwnerID; if (!$node) { return; } if ($node->isChanged('OwnerID')) { $changed = $node->getChangedFields(); $ownerId = isset($changed['OwnerID']['before']) && $changed['OwnerID']['before'] ? $changed['OwnerID']['before'] : $ownerId; } if (!$member || ($ownerId != $member->ID)) { return false; } $cache = $this->getCache(); $perms = $cache->load('ownerperms'); if (!$perms) { // find the owner role and take the permissions of it $ownerRole = DataObject::get_one('AccessRole', '"Title" = \'Owner\''); if ($ownerRole && $ownerRole->exists()) { $perms = $ownerRole->Composes->getValues(); if (is_array($perms)) { $cache->save($perms, 'ownerperms'); } } else { // just fall back to checking OwnerID == $member->ID return $ownerId == $member->ID; } } if (is_array($perms) && in_array($perm, $perms)) { return true; } } /** * * @param DataObject $node * @param boolean $includeInherited * Include inherited permissions in the list? */ public function getPermissionsFor(DataObject $node, $includeInherited = false) { if ($this->checkPerm($node, 'ViewPermissions')) { $authorities = $node->getAuthorities(); $formatted = ArrayList::create(); if (!$authorities) { $formatted = ArrayList::create(); } else { foreach ($authorities as $authority) { $auth = $authority->getAuthority(); if ($auth) { $authority->DisplayName = $auth->getTitle(); $authority->PermList = implode(', ', $authority->Perms->getValues()); } else { $authority->DisplayName = 'INVALID AUTHORITY: #' . $authority->ID; } $formatted->push($authority); } } return $formatted; } } /** * Clear any cached permissions for this object * * @param DataObject $item * @param type $perm */ public function clearPermCacheFor($nodeStr) { if (!$nodeStr) { return; } if (is_object($nodeStr)) { $nodeStr = $this->idStr($nodeStr); } $key = self::ITEM_PREFIX . $nodeStr; $this->getCache()->remove($key); $this->clearSourcesCache($nodeStr); } public function idStr($node) { return str_replace("\\", '_', $node->ClassName.'_'.$node->ID); } /** * Clear all cached permissions for the given user * * @param int $userId */ public function clearUserCachedPerms($userId) { $cache = $this->getCache(); $ids = $cache->getIds(); foreach ($ids as $id) { // get the item, see if it has the user ID if ($id == self::SOURCES_MAP || strpos($id, 'source_') !== false || strpos($id, self::ITEM_PREFIX) === false) { continue; } $userPerms = $cache->load($id); if (is_array($userPerms)) { $changed = false; foreach ($userPerms as $type => $grantedUserIds) { $newIds = array(); foreach ($grantedUserIds as $grantedUserId => $grant) { if ($userId != $grantedUserId) { $newIds[$grantedUserId] = $grant; } } if (count($newIds) != count($userPerms[$type])) { $userPerms[$type] = $newIds; $changed = true; } } if ($changed) { $cache->save($userPerms, $id); } } } } public function purgePermissionCache() { $cache = $this->getCache(); $ids = $cache->getIds(); foreach ($ids as $id) { // get the item, see if it has the user ID if ($id == self::SOURCES_MAP || strpos($id, 'source_') !== false || strpos($id, self::ITEM_PREFIX) === false) { continue; } $cache->remove($id); } } protected function clearSourcesCache($nodeStr) { $sourcesMap = $this->getCache()->load(self::SOURCES_MAP); $kidsToClear = array(); if (isset($sourcesMap[$nodeStr])) { // store and do _after_ the cache write below $kidsToClear = $sourcesMap[$nodeStr]; } unset($sourcesMap[$nodeStr]); $this->getCache()->save($sourcesMap, self::SOURCES_MAP); $key = "sources_$nodeStr"; $this->getCache()->remove($key); foreach ($kidsToClear as $keystr => $marker) { $this->clearPermCacheFor($keystr); } } /** * Get the key for this item in the cache * * @param type $perm * @return string */ public function permCacheKey(DataObject $node) { if ($node && $node->ID) { return self::ITEM_PREFIX . $this->idStr($node); } } /** * Realise all parent sources of the given node * * This causes two things to be generated and stored * * - the list of nodes that $node derives inherited permissions from, * indexed as source_nodeType_nodeID . This includes all generational ancestors, * not just immediate parents * * - A graph of all the nodes that an item provides inherited permissions * to. * * * This allows us to provide more intelligent cache purging whenever nodes change, * so that the runtime permission cache can directly store the 'realised' permission * of things. * * * @param DataObject $node * @param array $addTo */ public function realiseAllSources($node, $sourceTo = null, &$addTo = null) { if (!$addTo) { $addTo = array(); } $myIdent = $this->idStr($node); // if needbe, update the source map if ($sourceTo) { $sourceMap = $this->getCache()->load(self::SOURCES_MAP); if (!$sourceMap) { $sourceMap = array(); } $myTree = isset($sourceMap[$myIdent]) ? $sourceMap[$myIdent] : array(); $myTree[$sourceTo] = 1; $sourceMap[$myIdent] = $myTree; $this->getCache()->save($sourceMap, self::SOURCES_MAP); } $parents = $this->getEffectiveParents($node); if ($parents) { foreach ($parents as $parent) { $addTo[] = "{$parent->ClassName},$parent->ID"; $this->realiseAllSources($parent, $myIdent, $addTo); } } $key = "sources_$myIdent"; $this->getCache()->save($addTo, $key); return $addTo; } } class PermissionDeniedException extends Exception { public function __construct($permission, $message = '', $code = null, $previous = null) { if ($previous) { parent::__construct($message . ' (' . $permission .')', $code, $previous); } else { parent::__construct($message . ' (' . $permission .')', $code); } } } |