Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
Policy
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
8 / 8
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withDefaults
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 role
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 roles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 principals
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 permits
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 covers
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Security;
6
7use Cosray\User;
8
9final class Policy
10{
11    public const string EVERYONE = 'everyone';
12    public const string AUTHENTICATED = 'authenticated';
13
14    /** @var array<string, string> */
15    private array $roles = [];
16
17    public function __construct(
18        private readonly Acl $acl = new Acl(),
19    ) {}
20
21    public static function withDefaults(): self
22    {
23        $policy = new self();
24
25        $policy->role('superuser', 'Superuser');
26        $policy->role('admin', 'Administrator');
27        $policy->role('editor', 'Editor');
28
29        $policy->allow(
30            'role:superuser',
31            'panel',
32            'read-nodes',
33            'edit-nodes',
34            'edit-menus',
35            'manage-menus',
36            'edit-users',
37            'edit-settings',
38        );
39        $policy->allow('role:admin', 'panel', 'read-nodes', 'edit-nodes', 'edit-menus', 'edit-users');
40        $policy->allow('role:editor', 'panel', 'read-nodes', 'edit-nodes');
41        // What the website shows anyone: published nodes of routable types.
42        $policy->allow(self::EVERYONE, 'read-pages');
43
44        return $policy;
45    }
46
47    public function role(string $name, string $label): void
48    {
49        $this->roles[$name] = $label;
50    }
51
52    /** @return array<string, string> */
53    public function roles(): array
54    {
55        return $this->roles;
56    }
57
58    public function allow(string $principal, string ...$permissions): void
59    {
60        $this->acl->allow($principal, ...$permissions);
61    }
62
63    /**
64     * A stored role the app no longer defines grants nothing.
65     *
66     * @return list<string>
67     */
68    public function principals(?User $user): array
69    {
70        if ($user === null) {
71            return [self::EVERYONE];
72        }
73
74        $principals = [self::EVERYONE, self::AUTHENTICATED, "user:{$user->uid}", "type:{$user->type}"];
75
76        foreach ($user->roles as $role) {
77            if (isset($this->roles[$role])) {
78                $principals[] = "role:{$role}";
79            }
80        }
81
82        return $principals;
83    }
84
85    public function permits(?User $user, string $permission): bool
86    {
87        return $this->acl->permits($this->principals($user), $permission);
88    }
89
90    /**
91     * Whether the user holds everything the given roles grant. Managing an
92     * account or handing out a role must never reach beyond one's own
93     * permissions.
94     */
95    public function covers(User $user, string ...$roles): bool
96    {
97        foreach ($roles as $role) {
98            foreach ($this->acl->permissions("role:{$role}") as $permission) {
99                if (!$this->permits($user, $permission)) {
100                    return false;
101                }
102            }
103        }
104
105        return true;
106    }
107}