midcom_core_privilege::_load()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.25

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 1
dl 0
loc 12
ccs 6
cts 8
cp 0.75
crap 4.25
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * Privilege class, used to interact with the privilege system. It encapsulates the actual
11
 * Database Level Object. As usual with MidCOM DBA, you <i>must never access the DB layer
12
 * object.</i>
13
 *
14
 * The main area of expertise of this class is privilege IO (loading and storing), their
15
 * validation and privilege merging.
16
 *
17
 * It is important to understand that you must never load privilege records directly, or
18
 * access them by their IDs. Instead, use the DBA level interface functions to locate
19
 * existing privilege sets. The only time where you use this class directly is when
20
 * creating new privilege, using the default constructor of this class (although the
21
 * create_new_privilege_object DBA member methods are the preferred way of doing this).
22
 *
23
 * <b>Caching:</b>
24
 *
25
 * This class uses the memcache cache module to speed up ACL accesses. It caches the ACL
26
 * objects retrieved from the database, not any merged privilege set (at this time, that is).
27
 * This should speed up regular operations quite a bit (along with the parent guid cache,
28
 * which is a second important key).
29
 *
30
 * @property string $objectguid GUID of the object the privilege applies to
31
 * @property string $privilegename Name of the privilege (for example `midgard:create`)
32
 * @property string $assignee Assignee of the privilege, for instance user or group identifier
33
 * @property string $classname MgdSchema class the privilege applies to, in case of class-level privileges
34
 * @property integer $value
35
                Value of the privilege:
36
37
                - 1: MIDCOM_PRIVILEGE_ALLOW
38
                - 2: MIDCOM_PRIVILEGE_DENY
39
                - 3: MIDCOM_PRIVILEGE_INHERIT
40
41
 * @property string $guid
42
 * @package midcom
43
 */
44
class midcom_core_privilege
45
{
46
    /**
47
     * Cached actual midcom_core_privilege_db data for this privilege.
48
     */
49
    private array $__privilege = [
50
        'guid' => '',
51
        'objectguid' => '',
52
        'privilegename'=> '',
53
        'assignee' => null,
54
        'scope' => -1,
55
        'classname' => '',
56
        'value' => null
57
    ];
58
59
    /**
60
     * The actual object for this privilege.
61
     */
62
    private ?midcom_core_privilege_db $__privilege_object = null;
63
64
    /**
65
     * GUID of the midcom_core_privilege_db object, used when values are retrieved via collector instead of QB
66
     */
67
    private string $__guid = '';
68
69
    /**
70
     * Cached content object, based on $objectguid.
71
     */
72
    private ?midcom_core_dbaobject $__cached_object = null;
73
74
    /**
75
     * The Default constructor creates an empty privilege, if you specify
76
     * another privilege object in the constructor, a copy is constructed.
77
     */
78 134
    public function __construct(midcom_core_privilege_db|array|string|null $src = null)
79
    {
80 134
        if (is_array($src)) {
81
            // Store given values to our privilege array
82 81
            $this->__privilege = array_merge($this->__privilege, $src);
83
        } else {
84 129
            $this->_load($src);
85 129
            if ($src !== null) {
0 ignored issues
show
introduced by
The condition $src !== null is always false.
Loading history...
86 25
                $this->_sync_from_db_object();
87
            }
88
        }
89
    }
90
91
    // Magic getter and setter for object property mapping
92 139
    public function __get($property)
93
    {
94 139
        return $this->__privilege[$property] ?? null;
95
    }
96
97 129
    public function __set($property, $value)
98
    {
99 129
        $this->__privilege[$property] = $value;
100
    }
101
102
    public function __isset($property)
103
    {
104
        return isset($this->__privilege[$property]);
105
    }
106
107
    /**
108
     * Get the object referenced by the guid value of this privilege.
109
     */
110 125
    private function get_object() : ?midcom_core_dbaobject
111
    {
112 125
        if ($this->__cached_object === null) {
113
            try {
114 25
                $this->__cached_object = midcom::get()->dbfactory->get_object_by_guid($this->objectguid);
115
            } catch (midcom_error) {
116
                return null;
117
            }
118
        }
119 125
        return $this->__cached_object;
120
    }
121
122
    /**
123
     * Set a privilege to a given content object.
124
     */
125 129
    public function set_object(midcom_core_dbaobject $object)
126
    {
127 129
        $this->__cached_object = $object;
128 129
        $this->objectguid = $object->guid;
129
    }
130
131
    /**
132
     * Determine whether a given privilege applies for the given
133
     * user in content mode. This means, that all SELF privileges are skipped at this point,
134
     * EVERYONE privileges apply always, and all other privileges are checked against the
135
     * user.
136
     */
137 81
    public function does_privilege_apply(string $user_id) : bool
138
    {
139 81
        switch ($this->__privilege['assignee']) {
140 81
            case 'EVERYONE':
141
                return true;
142 81
            case 'ANONYMOUS':
143
                return in_array($user_id, ['EVERYONE', 'ANONYMOUS']);
144 81
            case 'USERS':
145
                return !in_array($user_id, ['EVERYONE', 'ANONYMOUS']);
146
            default:
147 81
                if ($this->__privilege['assignee'] == $user_id) {
148 51
                    return true;
149
                }
150 36
                if (str_starts_with($this->__privilege['assignee'], 'group:')) {
151 1
                    if ($user = midcom::get()->auth->get_user($user_id)) {
152 1
                        return $user->is_in_group($this->__privilege['assignee']);
153
                    }
154
                }
155 36
                return false;
156
        }
157
    }
158
159
    /**
160
     * Returns the privilege's scope (or -1 for SELF and broken privileges)
161
     */
162 81
    public function get_scope() : int
163
    {
164 81
        if (defined('MIDCOM_PRIVILEGE_SCOPE_' . $this->__privilege['assignee'])) {
165
            return constant('MIDCOM_PRIVILEGE_SCOPE_' . $this->__privilege['assignee']);
166
        }
167 81
        if ($assignee = $this->get_assignee()) {
168 81
            return $assignee->scope;
169
        }
170
        debug_print_r('Could not resolve the assignee of this privilege', $this);
171
172
        return -1;
173
    }
174
175
    /**
176
     * If the assignee has an object representation (at this time, only users and groups have), this call
177
     * will return the assignee object held by the authentication service.
178
     *
179
     * Use is_magic_assignee to determine if you have an assignee object.
180
     *
181
     * @see midcom_services_auth::get_assignee()
182
     * @return midcom_core_user|midcom_core_group|null object as returned by the auth service, null on failure.
183
     */
184 131
    public function get_assignee() : ?object
185
    {
186 131
        if ($this->is_magic_assignee()) {
187
            return null;
188
        }
189
190 131
        return midcom::get()->auth->get_assignee($this->assignee);
191
    }
192
193
    /**
194
     * Checks whether the current assignee is a magic assignee or an object identifier.
195
     */
196 136
    public function is_magic_assignee(?string $assignee = null) : bool
197
    {
198 136
        $assignee ??= $this->assignee;
199 136
        return in_array($assignee, ['SELF', 'EVERYONE', 'USERS', 'ANONYMOUS', 'OWNER']);
200
    }
201
202
    /**
203
     * Set the assignee member string to the correct value to represent the
204
     * object passed, in general, this resolves users and groups to their strings and
205
     * leaves magic assignees intact.
206
     *
207
     * Possible argument types:
208
     *
209
     * - Any one of the magic assignees SELF, EVERYONE, ANONYMOUS, USERS.
210
     * - Any midcom_core_user or midcom_core_group object or subtype thereof.
211
     * - Any string identifier which can be resolved using midcom_services_auth::get_assignee().
212
     */
213 129
    public function set_assignee(midcom_core_group|midcom_core_user|string $assignee) : bool
214
    {
215 129
        if (is_string($assignee)) {
216 129
            if ($this->is_magic_assignee($assignee)) {
217 12
                $this->assignee = $assignee;
218
            } else {
219 126
                $tmp = midcom::get()->auth->get_assignee($assignee);
220 126
                if (!$tmp) {
221
                    debug_add("Could not resolve the assignee string '{$assignee}', see above for more information.", MIDCOM_LOG_INFO);
222
                    return false;
223
                }
224 129
                $this->assignee = $tmp->id;
225
            }
226
        } else {
227 1
            $this->assignee = $assignee->id;
228
        }
229
230 129
        return true;
231
    }
232
233
    /**
234
     * Validate the privilege for correctness of all set options. This includes:
235
     *
236
     * - A check against the list of registered privileges to ensure the existence of the
237
     *   privilege itself.
238
     * - A check for a valid and existing assignee, this includes a class existence check for classname restrictions
239
     *   for SELF privileges.
240
     * - A check for an existing content object GUID (this implicitly checks for midgard:read as well).
241
     * - Enough privileges of the current user to update the object's privileges (the user
242
     *   must have midgard:update and midgard:privileges for this to succeed).
243
     * - A valid privilege value.
244
     */
245 125
    public function validate() : bool
246
    {
247 125
        if (!midcom::get()->auth->acl->privilege_exists($this->privilegename)) {
248
            debug_add("The privilege name '{$this->privilegename}' is unknown to the system. Perhaps the corresponding component is not loaded?",
249
                MIDCOM_LOG_INFO);
250
            return false;
251
        }
252
253 125
        if (!in_array($this->value, [MIDCOM_PRIVILEGE_ALLOW, MIDCOM_PRIVILEGE_DENY, MIDCOM_PRIVILEGE_INHERIT])) {
254
            debug_add("Invalid privilege value '{$this->value}'.", MIDCOM_LOG_INFO);
255
            return false;
256
        }
257
258 125
        if ($this->classname != '') {
259 1
            if ($this->assignee != 'SELF') {
260
                debug_add("The classname parameter was specified without having the magic assignee SELF set, this is invalid.", MIDCOM_LOG_INFO);
261
                return false;
262
            }
263 1
            if (!class_exists($this->classname)) {
264
                debug_add("The class '{$this->classname}' is not found, the SELF magic assignee with class restriction is invalid therefore.", MIDCOM_LOG_INFO);
265
                return false;
266
            }
267
        }
268
269 125
        if (   !$this->is_magic_assignee()
270 125
            && !$this->get_assignee()) {
271
            debug_add("The assignee identifier '{$this->assignee}' is invalid.", MIDCOM_LOG_INFO);
272
            return false;
273
        }
274 125
        if (   $this->assignee == 'OWNER'
275 125
            && $this->privilegename == 'midgard:owner') {
276
            debug_add("Tried to assign midgard:owner to the OWNER magic assignee, this is invalid.", MIDCOM_LOG_INFO);
277
            return false;
278
        }
279
280 125
        $object = $this->get_object();
281 125
        if (!$object) {
282
            debug_add("Could not retrieve the content object with the GUID '{$this->objectguid}'; see the debug level log for more information.",
283
                MIDCOM_LOG_INFO);
284
            return false;
285
        }
286 125
        if (   !$object->can_do('midgard:update')
287 125
            || !$object->can_do('midgard:privileges')) {
288
            debug_add("Insufficient privileges on the content object with the GUID '{$this->__guid}', midgard:update and midgard:privileges required.",
289
                MIDCOM_LOG_INFO);
290
            return false;
291
        }
292
293 125
        return true;
294
    }
295
296
    /**
297
     * List all content privileges assigned to a given object.
298
     * Essentially, this will exclude all SELF style assignees.
299
     *
300
     * This function is for use in the authentication framework only.
301
     *
302
     * @return midcom_core_privilege[]
303
     */
304 133
    public static function get_content_privileges(string $guid) : array
305
    {
306 133
        return self::_get_privileges($guid, 'CONTENT');
307
    }
308
309
    /**
310
     * List all privileges assigned directly to a user or group.
311
     * These are all SELF privileges.
312
     *
313
     * This function is for use in the authentication framework only.
314
     *
315
     * @return midcom_core_privilege[]
316
     */
317 14
    public static function get_self_privileges(string $guid) : array
318
    {
319 14
        return self::_get_privileges($guid, 'SELF');
320
    }
321
322
    /**
323
     * List all privileges assigned an object unfiltered.
324
     *
325
     * This function is for use in the authentication framework only
326
     *
327
     * @return midcom_core_privilege[]
328
     */
329 1
    public static function get_all_privileges(string $guid) : array
330
    {
331 1
        return array_merge(self::get_content_privileges($guid), self::get_self_privileges($guid));
332
    }
333
334
    /**
335
     * List all privileges assigned an object unfiltered.
336
     *
337
     * @return midcom_core_privilege[]
338
     */
339 134
    private static function _get_privileges(string $guid, string $type) : array
340
    {
341 134
        static $cache = [];
342
343 134
        $cache_key = $type . '-' . $guid;
344
345 134
        if (!array_key_exists($cache_key, $cache)) {
346 132
            $return = midcom::get()->cache->memcache->get('ACL', $cache_key);
347
348 132
            if (!is_array($return)) {
349
                // Didn't get privileges from cache, get them from DB
350 132
                $return = self::_query_privileges($guid, $type);
351 132
                midcom::get()->cache->memcache->put('ACL', $cache_key, $return);
352
            }
353
354 132
            $cache[$cache_key] = $return;
355
        }
356
357 134
        return $cache[$cache_key];
358
    }
359
360
    /**
361
     * Query the database for privileges and construct all necessary objects out of it.
362
     *
363
     * @param string $type SELF or CONTENT
364
     * @return midcom_core_privilege[]
365
     */
366 132
    protected static function _query_privileges(string $guid, string $type) : array
367
    {
368 132
        $result = [];
369
370 132
        $mc = new midgard_collector('midcom_core_privilege_db', 'objectguid', $guid);
371 132
        $mc->add_constraint('value', '<>', MIDCOM_PRIVILEGE_INHERIT);
372
373 132
        if ($type == 'CONTENT') {
374 128
            $mc->add_constraint('assignee', 'NOT IN', ['SELF', '']);
375
        } else {
376 14
            $mc->add_constraint('assignee', '=', 'SELF');
377
        }
378
379 132
        $mc->set_key_property('guid');
380 132
        $mc->add_value_property('id');
381 132
        $mc->add_value_property('privilegename');
382 132
        $mc->add_value_property('assignee');
383 132
        $mc->add_value_property('classname');
384 132
        $mc->add_value_property('value');
385 132
        $mc->execute();
386 132
        $privileges = $mc->list_keys();
387
388 132
        foreach (array_keys($privileges) as $privilege_guid) {
389 80
            $privilege = $mc->get($privilege_guid);
390 80
            $privilege['objectguid'] = $guid;
391 80
            $privilege['guid'] = $privilege_guid;
392 80
            $privilege_object = new static($privilege);
393 80
            $result[] = $privilege_object;
394
        }
395
396 132
        return $result;
397
    }
398
399
    /**
400
     * Retrieve a single given privilege at a content object, identified by
401
     * the combination of assignee and privilege name.
402
     *
403
     * This call will return an object even if the privilege is set to INHERITED at
404
     * the given object (i.e. does not exist) for consistency reasons. Errors are
405
     * thrown for example on database inconsistencies.
406
     *
407
     * This function is for use in the authentication framework only.
408
     *
409
     * @param string $classname The optional classname required only for class-limited SELF privileges.
410
     */
411 129
    public static function get_privilege(midcom_core_dbaobject $object, string $name, string $assignee, string $classname = '') : midcom_core_privilege
412
    {
413 129
        $qb = new midgard_query_builder('midcom_core_privilege_db');
414 129
        $qb->add_constraint('objectguid', '=', $object->guid);
415 129
        $qb->add_constraint('privilegename', '=', $name);
416 129
        $qb->add_constraint('assignee', '=', $assignee);
417 129
        $qb->add_constraint('classname', '=', $classname);
418 129
        $result = $qb->execute();
419
420 129
        if (empty($result)) {
421
            // No such privilege stored, return non-persistent one
422 121
            $privilege = new self;
423 121
            $privilege->set_object($object);
424 121
            $privilege->set_assignee($assignee);
425 121
            $privilege->privilegename = $name;
426 121
            $privilege->classname = $classname;
427 121
            $privilege->value = MIDCOM_PRIVILEGE_INHERIT;
428 121
            return $privilege;
429
        }
430 25
        if (count($result) > 1) {
431
            debug_add('A DB inconsistency has been detected. There is more than one record for privilege specified. Deleting all excess records after the first one!',
432
                MIDCOM_LOG_ERROR);
433
            debug_print_r('Content Object:', $object);
434
            debug_add("Privilege {$name} for assignee {$assignee} with classname {$classname} was queried.", MIDCOM_LOG_INFO);
435
            debug_print_r('Resultset was:', $result);
436
            midcom::get()->auth->request_sudo('midcom.core');
437
            while (count($result) > 1) {
438
                $privilege = array_pop($result);
439
                $privilege->purge();
440
            }
441
            midcom::get()->auth->drop_sudo();
442
        }
443
444 25
        return new midcom_core_privilege($result[0]);
445
    }
446
447 129
    private function _load(midcom_core_privilege_db|string|null $src = null)
448
    {
449 129
        if ($src instanceof midcom_core_privilege_db) {
450
            // Got a privilege object as argument, use that
451 25
            $this->__guid = $src->guid;
452 25
            $this->__privilege_object = $src;
453 129
        } elseif (is_string($src) && mgd_is_guid($src)) {
454
            $this->__guid = $src;
455
            $this->__privilege_object = new midcom_core_privilege_db($src);
456
        } else {
457
            // Have a nonpersistent privilege
458 129
            $this->__privilege_object = new midcom_core_privilege_db();
459
        }
460
    }
461
462 125
    private function _sync_to_db_object()
463
    {
464 125
        if (!$this->__privilege_object) {
465 1
            $this->_load($this->guid);
466
        }
467 125
        $this->__privilege_object->objectguid = $this->objectguid;
468 125
        $this->__privilege_object->privilegename = $this->privilegename;
469 125
        $this->__privilege_object->assignee = $this->assignee;
470 125
        $this->__privilege_object->classname = $this->classname;
471 125
        $this->__privilege_object->value = $this->value;
472
    }
473
474 25
    private function _sync_from_db_object()
475
    {
476 25
        $this->objectguid = $this->__privilege_object->objectguid;
477 25
        $this->privilegename = $this->__privilege_object->privilegename;
478 25
        $this->assignee = $this->__privilege_object->assignee;
479 25
        $this->classname = $this->__privilege_object->classname;
480 25
        $this->value = $this->__privilege_object->value;
481 25
        $this->guid = $this->__privilege_object->guid;
482
    }
483
484
    /**
485
     * Store the privilege. This will validate it first and then either
486
     * update an existing privilege record, or create a new one, depending on the
487
     * DB state.
488
     */
489 125
    public function store() : bool
490
    {
491 125
        if (!$this->validate()) {
492
            debug_add('This privilege failed to validate, rejecting it, see the debug log for details.', MIDCOM_LOG_WARN);
493
            $this->__cached_object = null;
494
            debug_print_r('Privilege dump (w/o cached object):', $this);
495
            return false;
496
        }
497
498 125
        $this->_sync_to_db_object();
499
500 125
        if ($this->value == MIDCOM_PRIVILEGE_INHERIT) {
501
            if ($this->__guid) {
502
                // Already a persistent record, drop it.
503
                return $this->drop();
504
            }
505
            // This is a temporary object only, try to load the real object first. If it is not found,
506
            // exit silently, as this is the desired final state.
507
            $object = $this->get_object();
508
            $privilege = self::get_privilege($object, $this->privilegename, $this->assignee, $this->classname);
509
            if (!empty($privilege->__guid)) {
510
                if (!$privilege->drop()) {
511
                    return false;
512
                }
513
                $this->_invalidate_cache();
514
            }
515
            return true;
516
        }
517
518 125
        if ($this->__guid) {
519 20
            if (!$this->__privilege_object->update()) {
520
                return false;
521
            }
522 20
            $this->_invalidate_cache();
523 20
            return true;
524
        }
525
526 125
        $object = $this->get_object();
527 125
        $privilege = self::get_privilege($object, $this->privilegename, $this->assignee, $this->classname);
528 125
        if (!empty($privilege->__guid)) {
529 20
            $privilege->value = $this->value;
530 20
            if (!$privilege->store()) {
531
                debug_add('Update of the existing privilege failed.', MIDCOM_LOG_WARN);
532
                return false;
533
            }
534 20
            $this->__guid = $privilege->__guid;
535 20
            $this->objectguid = $privilege->objectguid;
536 20
            $this->privilegename = $privilege->privilegename;
537 20
            $this->assignee = $privilege->assignee;
538 20
            $this->classname = $privilege->classname;
539 20
            $this->value = $privilege->value;
540
541 20
            $this->_invalidate_cache();
542 20
            return true;
543
        }
544
545 117
        if (!$this->__privilege_object->create()) {
546
            debug_add('Creating new privilege failed: ' . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
547
            return false;
548
        }
549 117
        $this->__guid = $this->__privilege_object->guid;
550 117
        $this->_invalidate_cache();
551 117
        return true;
552
    }
553
554
    /**
555
     * Invalidate the memcache after I/O operations
556
     */
557 125
    private function _invalidate_cache()
558
    {
559 125
        midcom::get()->cache->invalidate($this->objectguid);
560
    }
561
562
    /**
563
     * Drop the privilege. If we are a known DB record, we delete us, otherwise
564
     * we return silently.
565
     */
566 9
    public function drop() : bool
567
    {
568 9
        $this->_sync_to_db_object();
569
570 9
        if (!$this->__guid) {
571 3
            debug_add('We are not stored, GUID is empty. Ignoring silently.');
572 3
            return true;
573
        }
574
575 7
        if (!$this->validate()) {
576
            debug_add('This privilege failed to validate, rejecting to drop it, see the debug log for details.', MIDCOM_LOG_WARN);
577
            debug_print_r('Privilege dump:', $this);
578
            return false;
579
        }
580
581 7
        if (!$this->__privilege_object->guid) {
582
            // We created this via collector, instantiate a new one
583
            $privilege = new midcom_core_privilege($this->__guid);
584
            return $privilege->drop();
585
        }
586
587 7
        if (!$this->__privilege_object->purge()) {
588
            debug_add('Failed to delete privilege record, aborting. Error: ' . midcom_connection::get_error_string(), MIDCOM_LOG_ERROR);
589
            return false;
590
        }
591
592 7
        debug_add("Deleted privilege record {$this->__guid} ({$this->__privilege_object->objectguid} {$this->__privilege_object->privilegename} {$this->__privilege_object->assignee} {$this->__privilege_object->value}");
593
594 7
        $this->_invalidate_cache();
595 7
        $this->value = MIDCOM_PRIVILEGE_INHERIT;
596
597 7
        return true;
598
    }
599
}
600