AbstractPermissionManager.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\security\impl;
12 
23 use wcmf\lib\util\StringUtil;
24 
25 /**
26  * AbstractPermissionManager is the base class for concrete PermissionManager
27  * implementations.
28  *
29  * @author ingo herwig <ingo@wemove.com>
30  */
31 abstract class AbstractPermissionManager implements PermissionManager {
32 
33  const RESOURCE_TYPE_ENTITY_TYPE = 'entity.type';
34  const RESOURCE_TYPE_ENTITY_TYPE_PROPERTY = 'entity.type.property';
35  const RESOURCE_TYPE_ENTITY_INSTANCE = 'entity.instance';
36  const RESOURCE_TYPE_ENTITY_INSTANCE_PROPERTY = 'entity.instance.property';
37  const RESOURCE_TYPE_OTHER = 'other';
38 
39  private $tempPermissions = [];
40  private $tempPermissionIndex = 0;
41 
42  private static $logger = null;
43 
44  protected $persistenceFacade = null;
45  protected $session = null;
46  protected $dynamicRoles = [];
47  protected $principalFactory = null;
48 
49  /**
50  * Constructor
51  * @param $persistenceFacade
52  * @param $session
53  */
56  array $dynamicRoles=[]) {
57  $this->persistenceFacade = $persistenceFacade;
58  $this->session = $session;
59  if (self::$logger == null) {
60  self::$logger = LogManager::getLogger(__CLASS__);
61  }
62  $this->dynamicRoles = $dynamicRoles;
63  }
64 
65  /**
66  * Set the principal factory instances.
67  * @param $principalFactory
68  */
70  $this->principalFactory = $principalFactory;
71  }
72 
73  /**
74  * @see PermissionManager::authorize()
75  */
76  public function authorize($resource, $context, $action, $login=null, $applyDefaultPolicy=true) {
77  // get authenticated user, if no user is given
78  if ($login == null) {
79  $login = $this->session->getAuthUser();
80  }
81  if (self::$logger->isDebugEnabled()) {
82  self::$logger->debug("Checking authorization for: '$resource?$context?$action' and user '".$login."'");
83  }
84 
85  // normalize resource to string
86  $resourceStr = ($resource instanceof ObjectId) ? $resource->__toString() : $resource;
87 
88  // determine the resource type and set entity type, oid and property if applicable
89  $resourceDesc = $this->parseResource($resourceStr);
90  $resourceType = $resourceDesc['resourceType'];
91  $oid = $resourceDesc['oid'];
92  $type = $resourceDesc['type'];
93  $oidProperty = $resourceDesc['oidProperty'];
94  $typeProperty = $resourceDesc['typeProperty'];
95  if (self::$logger->isDebugEnabled()) {
96  self::$logger->debug("Resource type: ".$resourceType);
97  }
98 
99  // proceed by authorizing type depending resource
100  // always start checking from most specific
101  switch ($resourceType) {
102  case (self::RESOURCE_TYPE_ENTITY_INSTANCE_PROPERTY):
103  $authorized = $this->authorizeAction($oidProperty, $oidProperty, $context, $action, $login);
104  if ($authorized === null) {
105  $authorized = $this->authorizeAction($oidProperty, $typeProperty, $context, $action, $login);
106  if ($authorized === null) {
107  $authorized = $this->authorizeAction($oidProperty, $oid, $context, $action, $login);
108  if ($authorized === null) {
109  $authorized = $this->authorizeAction($oidProperty, $type, $context, $action, $login);
110  }
111  }
112  }
113  break;
114 
115  case (self::RESOURCE_TYPE_ENTITY_INSTANCE):
116  $authorized = $this->authorizeAction($oid, $oid, $context, $action, $login);
117  if ($authorized === null) {
118  $authorized = $this->authorizeAction($oid, $type, $context, $action, $login);
119  }
120  break;
121 
122  case (self::RESOURCE_TYPE_ENTITY_TYPE_PROPERTY):
123  $authorized = $this->authorizeAction($typeProperty, $typeProperty, $context, $action, $login);
124  if ($authorized === null) {
125  $authorized = $this->authorizeAction($typeProperty, $type, $context, $action, $login);
126  }
127  break;
128 
129  default:
130  $authorized = $this->authorizeAction($resourceStr, $resourceStr, $context, $action, $login);
131  break;
132  }
133 
134  // check parent entities in composite relations
135  if ($authorized === null && $resourceType == self::RESOURCE_TYPE_ENTITY_INSTANCE) {
136  if (self::$logger->isDebugEnabled()) {
137  self::$logger->debug("Check parent objects");
138  }
139  $mapper = $this->persistenceFacade->getMapper($type);
140  $parentRelations = $mapper->getRelations('parent');
141  if (sizeof($parentRelations) > 0) {
142 
143  $oidObj = ObjectId::parse($oid);
144  $tmpPerm = $this->addTempPermission($oidObj, $context, PersistenceAction::READ);
145  $object = $this->persistenceFacade->load($oidObj);
146  $this->removeTempPermission($tmpPerm);
147 
148  if ($object != null) {
149  foreach ($parentRelations as $parentRelation) {
150  if ($parentRelation->getThisAggregationKind() == 'composite') {
151  $parentType = $parentRelation->getOtherType();
152 
153  $tmpPerm = $this->addTempPermission($parentType, $context, PersistenceAction::READ);
154  $parents = $object->getValue($parentRelation->getOtherRole());
155  $this->removeTempPermission($tmpPerm);
156 
157  if ($parents != null) {
158  if (!$parentRelation->isMultiValued()) {
159  $parents = [$parents];
160  }
161  foreach ($parents as $parent) {
162  $authorized = $this->authorize($parent->getOID(), $context, $action);
163  if (!$authorized) {
164  break;
165  }
166  }
167  }
168  }
169  }
170  }
171  }
172  }
173 
174  if ($authorized === null && $applyDefaultPolicy) {
175  $authorized = $this->getDefaultPolicy($login);
176  }
177  if (self::$logger->isDebugEnabled()) {
178  self::$logger->debug("Result for $resource?$context?$action: ".(!$authorized ? "not " : "")."authorized");
179  }
180 
181  return $authorized;
182  }
183 
184  /**
185  * Authorize a resource, context, action triple by using the permissions set
186  * on another resource (e.g. authorize an action on an entity instance base
187  * on the permissions defined for it's type).
188  * @param $requestedResource The resource string to authorize.
189  * @param $permissionResource The resource string to use for selecting permissions.
190  * @param $context The context in which the action takes place.
191  * @param $action The action to process.
192  * @param $login The login of the user to use for authorization
193  * @return Boolean or null if undefined
194  */
195  protected function authorizeAction($requestedResource, $permissionResource,
196  $context, $action, $login) {
197  if (self::$logger->isDebugEnabled()) {
198  self::$logger->debug("Authorizing $requestedResource?$context?$action ".
199  "using permissions of $permissionResource?$context?$action");
200  }
201  $authorized = null;
202 
203  // check temporary permissions
204  if ($this->hasTempPermission($permissionResource, $context, $action)) {
205  if (self::$logger->isDebugEnabled()) {
206  self::$logger->debug("Has temporary permission");
207  }
208  $authorized = true;
209  }
210  else {
211  // check other permissions
212  $permissions = $this->getPermissions($permissionResource, $context, $action);
213  if (self::$logger->isDebugEnabled()) {
214  self::$logger->debug("Permissions: ".StringUtil::getDump($permissions));
215  }
216  if ($permissions != null) {
217  // matching permissions found, check user roles
218  $authorized = $this->matchRoles($requestedResource, $permissions, $login);
219  }
220  }
221  if (self::$logger->isDebugEnabled()) {
222  self::$logger->debug("Result: ".(is_bool($authorized) ? ((!$authorized ? "not " : "")."authorized") : "not defined"));
223  }
224  return $authorized;
225  }
226 
227  /**
228  * Get the default policy that is used if no permission is set up
229  * for a requested action.
230  * @param $login The login of the user to get the default policy for
231  * @return Boolean
232  */
233  protected function getDefaultPolicy($login) {
234  return ($login == AnonymousUser::USER_GROUP_NAME) ? false : true;
235  }
236 
237  /**
238  * Get the resource type and parameters (as applicable) from a resource
239  * @param $resource The resource represented as string
240  * @return Associative array with keys
241  * 'resourceType' (one of the RESOURCE_TYPE_ constants),
242  * 'oid' (object id),
243  * 'type' (entity type),
244  * 'oidProperty' (object id with instance property),
245  * 'typeProperty' (type id with entity property)
246  */
247  protected function parseResource($resource) {
248  $resourceType = null;
249  $oid = null;
250  $type = null;
251  $oidProperty = null;
252  $typeProperty = null;
253  $extensionRemoved = preg_replace('/\.[^\.]*?$/', '', $resource);
254  if (($oidObj = ObjectId::parse($resource)) !== null) {
255  $resourceType = self::RESOURCE_TYPE_ENTITY_INSTANCE;
256  $oid = $resource;
257  $type = $oidObj->getType();
258  }
259  elseif (($oidObj = ObjectId::parse($extensionRemoved)) !== null) {
261  $oid = $extensionRemoved;
262  $type = $oidObj->getType();
263  $oidProperty = $resource;
264  $typeProperty = $type.substr($resource, strlen($extensionRemoved));
265  }
266  elseif ($this->persistenceFacade->isKnownType($resource)) {
267  $resourceType = self::RESOURCE_TYPE_ENTITY_TYPE;
268  $type = $resource;
269  }
270  elseif ($this->persistenceFacade->isKnownType($extensionRemoved)) {
272  $type = $extensionRemoved;
273  $typeProperty = $resource;
274  }
275  else {
276  // defaults to other
277  $resourceType = self::RESOURCE_TYPE_OTHER;
278  }
279  return [
280  'resourceType' => $resourceType,
281  'oid' => $oid,
282  'type' => $type,
283  'oidProperty' => $oidProperty,
284  'typeProperty' => $typeProperty
285  ];
286  }
287 
288  /**
289  * Parse a permissions string and return an associative array with the keys
290  * 'default', 'allow', 'deny', where 'allow', 'deny' are arrays itself holding roles
291  * and 'default' is a boolean value derived from the wildcard policy (+* or -*).
292  * @param $value A role string (+*, +administrators, -guest, entries without '+' or '-'
293  * prefix default to allow rules).
294  * @return Associative array containing the permissions as an associative array with the keys
295  * 'default', 'allow', 'deny' or null, if value is empty
296  */
297  protected function deserializePermissions($value) {
298  if (strlen($value) == 0) {
299  return null;
300  }
301  $result = [
302  'default' => null,
303  'allow' => [],
304  'deny' => [],
305  ];
306 
307  $roleValues = explode(" ", $value);
308  foreach ($roleValues as $roleValue) {
309  $roleValue = trim($roleValue);
310  $matches = [];
311  preg_match('/^([+-]?)(.+)$/', $roleValue, $matches);
312  if (sizeof($matches) > 0) {
313  $prefix = $matches[1];
314  $role = $matches[2];
315  if ($role === '*') {
316  $result['default'] = $prefix == '-' ? false : true;
317  }
318  else {
319  if ($prefix === '-') {
320  $result['deny'][] = $role;
321  }
322  else {
323  // entries without '+' or '-' prefix default to allow rules
324  $result['allow'][] = $role;
325  }
326  }
327  }
328  }
329  // if no wildcard policy is defined, set default to false
330  if (!isset($result['default'])) {
331  $result['default'] = false;
332  }
333  return $result;
334  }
335 
336  /**
337  * Convert an associative permissions array with keys 'default', 'allow', 'deny'
338  * into a string.
339  * @param $permissions Associative array with keys 'default', 'allow', 'deny',
340  * where 'allow', 'deny' are arrays itself holding roles and 'default' is a
341  * boolean value derived from the wildcard policy (+* or -*).
342  * @return A role string (+*, +administrators, -guest, entries without '+' or '-'
343  * prefix default to allow rules).
344  */
345  protected function serializePermissions($permissions) {
346  $result = $permissions['default'] === true ? PermissionManager::PERMISSION_MODIFIER_ALLOW.'* ' :
348  if (isset($permissions['allow'])) {
349  foreach ($permissions['allow'] as $role) {
351  }
352  }
353  if (isset($permissions['deny'])) {
354  foreach ($permissions['deny'] as $role) {
355  $result .= PermissionManager::PERMISSION_MODIFIER_DENY.$role.' ';
356  }
357  }
358  return trim($result);
359  }
360 
361  /**
362  * Matches the roles of the user and the roles in the given permissions
363  * @param $resource The resource string to authorize.
364  * @param $permissions An array containing permissions as an associative array
365  * with the keys 'default', 'allow', 'deny', where 'allow', 'deny' are arrays
366  * itself holding roles and 'default' is a boolean value derived from the
367  * wildcard policy (+* or -*). 'allow' overwrites 'deny' overwrites 'default'
368  * @param $login the login of the user to match the roles for
369  * @return Boolean whether the user is authorized according to the permissions
370  */
371  protected function matchRoles($resource, $permissions, $login) {
372  if (self::$logger->isDebugEnabled()) {
373  self::$logger->debug("Matching roles for ".$login);
374  }
375  $user = $this->principalFactory->getUser($login, true);
376  if ($user != null) {
377  foreach (['allow' => true, 'deny' => false] as $key => $result) {
378  if (isset($permissions[$key])) {
379  foreach ($permissions[$key] as $role) {
380  if ($this->matchRole($user, $role, $resource)) {
381  if (self::$logger->isDebugEnabled()) {
382  self::$logger->debug($key." because of role ".$role);
383  }
384  return $result;
385  }
386  }
387  }
388  }
389  }
390  if (self::$logger->isDebugEnabled()) {
391  self::$logger->debug("Check default ".$permissions['default']);
392  }
393  return (isset($permissions['default']) ? $permissions['default'] : false);
394  }
395 
396  /**
397  * Check if a user matches the role for a resource
398  * @param $user The user instance.
399  * @param $role The role name.
400  * @param $resource The resource string to authorize.
401  * @return Boolean
402  */
403  protected function matchRole(User $user, $role, $resource) {
404  $isDynamicRole = isset($this->dynamicRoles[$role]);
405  return (($isDynamicRole && $this->dynamicRoles[$role]->match($user, $resource) === true)
406  || (!$isDynamicRole && $user->hasRole($role)));
407  }
408 
409  /**
410  * @see PermissionManager::addTempPermission()
411  */
412  public function addTempPermission($resource, $context, $action) {
413  $this->tempPermissionIndex++;
414  $actionKey = ActionKey::createKey($resource, $context, $action);
415  if (self::$logger->isDebugEnabled()) {
416  self::$logger->debug("Adding temporary permission for '$actionKey'");
417  }
418  $handle = $actionKey.'#'.$this->tempPermissionIndex;
419  $this->tempPermissions[$handle] = $actionKey;
420  return $handle;
421  }
422 
423  /**
424  * @see PermissionManager::removeTempPermission()
425  */
426  public function removeTempPermission($handle) {
427  if (self::$logger->isDebugEnabled()) {
428  self::$logger->debug("Removing temporary permission for '$handle'");
429  }
430  unset($this->tempPermissions[$handle]);
431  }
432 
433  /**
434  * @see PermissionManager::hasTempPermission()
435  */
436  public function hasTempPermission($resource, $context, $action) {
437  if (sizeof($this->tempPermissions) == 0) {
438  return false;
439  }
440 
441  // check if the resource has a direct permission
442  $permissions = array_flip($this->tempPermissions);
443  $actionKey = ActionKey::createKey($resource, $context, $action);
444  if (!isset($permissions[$actionKey])) {
445  // if not and the resource belongs to an entity instance,
446  // we might have a permission for the type
447  $resourceDesc = $this->parseResource($resource);
448  switch ($resourceDesc['resourceType']) {
450  $typeResource = $resourceDesc['type'];
451  break;
453  $typeResource = $resourceDesc['typeProperty'];
454  break;
455  default:
456  $typeResource = null;
457  }
458  // set alternative action key
459  if ($typeResource != null) {
460  $actionKey = ActionKey::createKey($typeResource, $context, $action);
461  }
462  }
463  return isset($permissions[$actionKey]);
464  }
465 
466  /**
467  * @see PermissionManager::clearTempPermissions()
468  */
469  public function clearTempPermissions() {
470  $this->tempPermissions = [];
471  }
472 }
473 ?>
Session is the interface for session implementations and defines access to session variables.
Definition: Session.php:19
getPermissions($resource, $context, $action)
Permission management.
setPrincipalFactory(PrincipalFactory $principalFactory)
Set the principal factory instances.
__construct(PersistenceFacade $persistenceFacade, Session $session, array $dynamicRoles=[])
Constructor.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
hasRole($roleName)
Check for a certain role in the user roles.
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:134
matchRoles($resource, $permissions, $login)
Matches the roles of the user and the roles in the given permissions.
static createKey($resource, $context, $action)
Create an action key from the given values.
Definition: ActionKey.php:33
PersistenceFacade defines the interface for PersistenceFacade implementations.
deserializePermissions($value)
Parse a permissions string and return an associative array with the keys 'default',...
static getLogger($name)
Get the logger with the given name.
Definition: LogManager.php:37
An action key is a combination of a resource, context and action that is represented as a string.
Definition: ActionKey.php:22
parseResource($resource)
Get the resource type and parameters (as applicable) from a resource.
authorize($resource, $context, $action, $login=null, $applyDefaultPolicy=true)
PrincipalFactory implementations are used to retrieve User and Role instances.
AbstractPermissionManager is the base class for concrete PermissionManager implementations.
PermissionManager implementations are used to handle all authorization requests.
LogManager is used to retrieve Logger instances.
Definition: LogManager.php:20
PersistenceAction values are used to define actions on PersistentObject instances.
matchRole(User $user, $role, $resource)
Check if a user matches the role for a resource.
authorizeAction($requestedResource, $permissionResource, $context, $action, $login)
Authorize a resource, context, action triple by using the permissions set on another resource (e....
User is the interface for users.
Definition: User.php:18
getDefaultPolicy($login)
Get the default policy that is used if no permission is set up for a requested action.
serializePermissions($permissions)
Convert an associative permissions array with keys 'default', 'allow', 'deny' into a string.