Tilmeld Server API  1.0.0
Nymph user and group management with access controls.
Tilmeld.php
1 <?php namespace Tilmeld;
2 
3 use SciActive\Hook;
4 use Nymph\Nymph;
7 
17 class Tilmeld {
18  const VERSION = '1.0.0';
19 
20  const NO_ACCESS = 0;
21  const READ_ACCESS = 1;
22  const WRITE_ACCESS = 2;
23  const FULL_ACCESS = 4;
24 
31  public static $config;
32 
39  public static $currentUser = null;
40 
47  private static $serverTimezone;
48 
58  public static function gatekeeper($ability = null) {
59  if (!isset(self::$currentUser)) {
60  return false;
61  }
62  return self::$currentUser->gatekeeper($ability);
63  }
64 
78  public static function configure($config = []) {
79  $defaults = include dirname(__DIR__).'/conf/defaults.php';
80  self::$config = array_replace($defaults, $config);
81 
82  // Set up access control hooks when Nymph is called.
83  if (!isset(Nymph::$driver)) {
84  throw new Exception('Tilmeld can\'t be configured before Nymph.');
85  }
86  HookMethods::setup();
87  self::authenticate();
88  }
89 
96  public static function addAccessControlSelectors(&$optionsAndSelectors) {
97  $user = self::$currentUser;
98 
99  if (isset($user) && $user->gatekeeper('system/admin')) {
100  // The user is a system admin, so they can see everything.
101  return;
102  }
103 
104  if (!isset($optionsAndSelectors[0])) {
105  throw new Exception('No options in argument.');
106  } elseif (isset($optionsAndSelectors[0]['class'])
107  && (
108  $optionsAndSelectors[0]['class'] === '\Tilmeld\Entities\User'
109  || $optionsAndSelectors[0]['class'] === '\Tilmeld\Entities\Group'
110  || $optionsAndSelectors[0]['class'] === 'Tilmeld\Entities\User'
111  || $optionsAndSelectors[0]['class'] === 'Tilmeld\Entities\Group'
112  )
113  ) {
114  // They are requesting a user/group. Always accessible for reading.
115  return;
116  }
117 
118  if ($user === null) {
119  $optionsAndSelectors[] = ['|',
120  // Other access control is sufficient.
121  'gte' => ['acOther', Tilmeld::READ_ACCESS],
122  // The user and group are not set.
123  ['&',
124  '!isset' => ['user', 'group']
125  ]
126  ];
127  } else {
128  $selector = ['|',
129  // Other access control is sufficient.
130  'gte' => ['acOther', Tilmeld::READ_ACCESS],
131  // The user and group are not set.
132  ['&',
133  '!isset' => [
134  'user',
135  'group'
136  ]
137  ],
138  // It is owned by the user.
139  ['&',
140  'ref' => ['user', $user],
141  'gte' => ['acUser', Tilmeld::READ_ACCESS]
142  ],
143  // The user is listed in acRead, acWrite, or acFull.
144  'ref' => [
145  ['acRead', $user],
146  ['acWrite', $user],
147  ['acFull', $user]
148  ]
149  ];
150  $groupRefs = [];
151  $acRefs = [];
152  if (isset($user->group) && isset($user->group->guid)) {
153  // It belongs to the user's primary group.
154  $groupRefs[] = ['group', $user->group];
155  // User's primary group is listed in acRead, acWrite, or acFull.
156  $acRefs[] = ['acRead', $user->group];
157  $acRefs[] = ['acWrite', $user->group];
158  $acRefs[] = ['acFull', $user->group];
159  }
160  foreach ($user->groups as $curSecondaryGroup) {
161  if (isset($curSecondaryGroup) && isset($curSecondaryGroup->guid)) {
162  // It belongs to the user's secondary group.
163  $groupRefs[] = ['group', $curSecondaryGroup];
164  // User's secondary group is listed in acRead, acWrite, or acFull.
165  $acRefs[] = ['acRead', $curSecondaryGroup];
166  $acRefs[] = ['acWrite', $curSecondaryGroup];
167  $acRefs[] = ['acFull', $curSecondaryGroup];
168  }
169  }
170  foreach ($user->getDescendantGroups() as $curDescendantGroup) {
171  if (isset($curDescendantGroup) && isset($curDescendantGroup->guid)) {
172  // It belongs to the user's secondary group.
173  $groupRefs[] = ['group', $curDescendantGroup];
174  // User's secondary group is listed in acRead, acWrite, or acFull.
175  $acRefs[] = ['acRead', $curDescendantGroup];
176  $acRefs[] = ['acWrite', $curDescendantGroup];
177  $acRefs[] = ['acFull', $curDescendantGroup];
178  }
179  }
180  // All the group refs.
181  if (!empty($groupRefs)) {
182  $selector[] = ['&',
183  'gte' => ['acGroup', Tilmeld::READ_ACCESS],
184  ['|',
185  'ref' => $groupRefs
186  ]
187  ];
188  }
189  // All the acRead, acWrite, and acFull refs.
190  if (!empty($acRefs)) {
191  $selector[] = ['|',
192  'ref' => $acRefs
193  ];
194  }
195  $optionsAndSelectors[] = $selector;
196  }
197  }
198 
268  public static function checkPermissions(
269  &$entity,
270  $type = Tilmeld::READ_ACCESS,
271  $user = null
272  ) {
273  // Only works for entities.
274  if (!is_object($entity) || !is_callable([$entity, 'is'])) {
275  return false;
276  }
277 
278  // Calculate the user.
279  if ($user === null) {
280  $userOrNull = self::$currentUser;
281  $userOrEmpty = User::current(true);
282  } elseif ($user === false) {
283  $userOrNull = null;
284  $userOrEmpty = User::factory();
285  } else {
286  $userOrNull = $userOrEmpty = $user;
287  }
288 
289  if ($userOrEmpty->gatekeeper('system/admin')) {
290  return true;
291  }
292 
293  // Users and groups are always readable. Editable by Tilmeld admins.
294  if ((
295  is_a($entity, '\Tilmeld\Entities\User')
296  || is_a($entity, '\Tilmeld\Entities\Group')
297  || is_a($entity, '\SciActive\HookOverride_Tilmeld_Entities_User')
298  || is_a($entity, '\SciActive\HookOverride_Tilmeld_Entities_Group')
299  )
300  && (
301  $type === Tilmeld::READ_ACCESS
302  || $userOrEmpty->gatekeeper('tilmeld/admin')
303  )
304  ) {
305  return true;
306  }
307 
308  // Entities with no owners are always editable.
309  if (!isset($entity->user) && !isset($entity->group)) {
310  return true;
311  }
312 
313  // Load access control, since we need it now...
314  $acUser = $entity->acUser ?? Tilmeld::FULL_ACCESS;
315  $acGroup = $entity->acGroup ?? Tilmeld::READ_ACCESS;
316  $acOther = $entity->acOther ?? Tilmeld::NO_ACCESS;
317 
318  if ($userOrNull === null) {
319  return ($acOther >= $type);
320  }
321 
322  // Check if the entity is the user.
323  if ($userOrEmpty->is($entity)) {
324  return true;
325  }
326 
327  // Check if the entity is the user's group. Always readable.
328  if (isset($userOrEmpty->group)
329  && is_callable([$userOrEmpty->group, 'is'])
330  && $userOrEmpty->group->is($entity)
331  && $type === Tilmeld::READ_ACCESS
332  ) {
333  return true;
334  }
335 
336  // Calculate all the groups the user belongs to.
337  $allGroups = isset($userOrEmpty->group) ? [$userOrEmpty->group] : [];
338  $allGroups = array_merge($allGroups, $userOrEmpty->groups);
339  $allGroups = array_merge($allGroups, $userOrEmpty->getDescendantGroups());
340 
341  // Check access ac properties.
342  $checks = [
343  ['type' => Tilmeld::FULL_ACCESS, 'array' => (array) $entity->acFull],
344  ['type' => Tilmeld::WRITE_ACCESS, 'array' => (array) $entity->acWrite],
345  ['type' => Tilmeld::READ_ACCESS, 'array' => (array) $entity->acRead]
346  ];
347  foreach ($checks as $curCheck) {
348  if ($type <= $curCheck['type']) {
349  if ($userOrEmpty->inArray($curCheck['array'])) {
350  return true;
351  }
352  foreach ($allGroups as $curGroup) {
353  if (is_callable([$curGroup, 'inArray'])
354  && $curGroup->inArray($curCheck['array'])
355  ) {
356  return true;
357  }
358  }
359  }
360  }
361 
362  // Check ownership ac properties.
363  if (is_callable([$entity->user, 'is'])
364  && $entity->user->is($userOrNull)
365  ) {
366  return ($acUser >= $type);
367  }
368  if (is_callable([$entity->group, 'is'])
369  && $entity->group->inArray($allGroups)
370  ) {
371  return ($acGroup >= $type);
372  }
373  return ($acOther >= $type);
374  }
375 
383  public static function fillSession($user) {
384  if (!isset(self::$serverTimezone)) {
385  self::$serverTimezone = date_default_timezone_get();
386  }
387  // Read groups right now, since gatekeeper needs them, so
388  // udpateDataProtection will fail to read them (since it runs gatekeeper).
389  isset($user->group);
390  isset($user->groups);
391  self::$currentUser = $user;
392  date_default_timezone_set(self::$currentUser->getTimezone());
393  // Now update the data protection on the user and all the groups.
394  self::$currentUser->updateDataProtection();
395  if (isset(self::$currentUser->group)) {
396  self::$currentUser->group->updateDataProtection();
397  }
398  foreach (self::$currentUser->groups as $group) {
399  $group->updateDataProtection();
400  }
401  }
402 
408  public static function clearSession() {
409  $user = self::$currentUser;
410  self::$currentUser = null;
411  if (isset(self::$serverTimezone)) {
412  date_default_timezone_set(self::$serverTimezone);
413  }
414  if ($user) {
415  $user->updateDataProtection();
416  }
417  }
418 
425  public static function extractToken($token) {
426  $extract = self::$config['jwt_extract']($token);
427  if (!$extract) {
428  return false;
429  }
430  $guid = $extract['guid'];
431 
432  $user = Nymph::getEntity(
433  ['class' => '\Tilmeld\Entities\User'],
434  ['&',
435  'guid' => $guid
436  ]
437  );
438  if (!$user || !$user->guid || !$user->enabled) {
439  return false;
440  }
441 
442  return $user;
443  }
444 
450  public static function authenticate() {
451  // If a client does't support cookies, they can use the X-TILMELDAUTH header
452  // to provide the auth token.
453  if (!empty($_SERVER['HTTP_X_TILMELDAUTH']) && empty($_COOKIE['TILMELDAUTH'])) {
454  $fromAuthHeader = true;
455  $authToken = $_SERVER['HTTP_X_TILMELDAUTH'];
456  } elseif (!empty($_COOKIE['TILMELDAUTH'])) {
457  $fromAuthHeader = false;
458  $authToken = $_COOKIE['TILMELDAUTH'];
459  } else {
460  return false;
461  }
462  $setupUrlParts = parse_url(self::$config['setup_url']);
463  $setupHost = $setupUrlParts['host'].
464  (array_key_exists('port', $setupUrlParts) ? ':'.$setupUrlParts['port'] : '');
465  if ($_SERVER['HTTP_HOST'] === $setupHost
466  && $_SERVER['REQUEST_URI'] === $setupUrlParts['path']
467  ) {
468  // The request is for the setup app, so don't check for the XSRF token.
469  $extract = self::$config['jwt_extract']($authToken);
470  } else {
471  // The request is for something else, so check for a valid XSRF token.
472  if (empty($_SERVER['HTTP_X_XSRF_TOKEN'])) {
473  return false;
474  }
475 
476  $extract = self::$config['jwt_extract'](
477  $authToken,
478  $_SERVER['HTTP_X_XSRF_TOKEN']
479  );
480  }
481 
482  if (!$extract) {
483  self::logout();
484  return false;
485  }
486  $guid = $extract['guid'];
487  $expire = $extract['expire'];
488 
489  $user = User::factory($guid);
490  if (!$user || !$user->guid || !$user->enabled) {
491  self::logout();
492  return false;
493  }
494 
495  if ($expire < time() + self::$config['jwt_renew']) {
496  // If the user is less than renew time from needing a new token, give them
497  // a new one.
498  self::login($user, $fromAuthHeader);
499  } else {
500  self::fillSession($user);
501  }
502  return true;
503  }
504 
513  public static function login($user, $sendAuthHeader) {
514  if (isset($user->guid) && $user->enabled) {
515  $token = self::$config['jwt_builder']($user);
516  $appUrlParts = parse_url(self::$config['app_url']);
517  setcookie(
518  'TILMELDAUTH',
519  $token,
520  time() + self::$config['jwt_expire'],
521  $appUrlParts['path'],
522  $appUrlParts['host'],
523  $appUrlParts['scheme'] === 'https',
524  false // Allow JS access (for CSRF protection).
525  );
526  if ($sendAuthHeader) {
527  header("X-TILMELDAUTH: $token");
528  }
529  self::fillSession($user);
530  return true;
531  }
532  return false;
533  }
534 
538  public static function logout() {
539  self::clearSession();
540  $appUrlParts = parse_url(self::$config['app_url']);
541  setcookie(
542  'TILMELDAUTH',
543  '',
544  null,
545  $appUrlParts['path'],
546  $appUrlParts['host']
547  );
548  }
549 
562  public static function groupSort(
563  &$array,
564  $property = null,
565  $caseSensitive = false,
566  $reverse = false
567  ) {
568  Nymph::hsort($array, $property, 'parent', $caseSensitive, $reverse);
569  }
570 }
static authenticate()
Check for a TILMELDAUTH cookie, and, if set, authenticate from it.
Definition: Tilmeld.php:450
static fillSession($user)
Fill session user data.
Definition: Tilmeld.php:383
static gatekeeper($ability=null)
Check to see if the current user has an ability.
Definition: Tilmeld.php:58
static extractToken($token)
Validate and extract the user from a token.
Definition: Tilmeld.php:425
A user entity.
Definition: User.php:59
A group entity.
Definition: Group.php:49
static addAccessControlSelectors(&$optionsAndSelectors)
Add selectors to a list of options and selectors which will limit results to only entities the curren...
Definition: Tilmeld.php:96
Tilmeld main class.
Definition: Tilmeld.php:17
static checkPermissions(&$entity, $type=Tilmeld::READ_ACCESS, $user=null)
Check an entity's permissions for a user.
Definition: Tilmeld.php:268
static groupSort(&$array, $property=null, $caseSensitive=false, $reverse=false)
Sort an array of groups hierarchically.
Definition: Tilmeld.php:562
static login($user, $sendAuthHeader)
Logs the given user into the system.
Definition: Tilmeld.php:513
static configure($config=[])
Apply configuration to Tilmeld.
Definition: Tilmeld.php:78
static clearSession()
Clear session user data.
Definition: Tilmeld.php:408
static logout()
Logs the current user out of the system.
Definition: Tilmeld.php:538