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 
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 ?>