Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.56% |
40 / 41 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
| NodeAccess | |
97.56% |
40 / 41 |
|
90.00% |
9 / 10 |
30 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| defaults | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| permission | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
6.10 | |||
| allows | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| serves | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| reads | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| readable | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| registered | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| declaresRead | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
| live | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Cosray\Security; |
| 6 | |
| 7 | use Celema\Container\Container; |
| 8 | use Cosray\Bootstrap; |
| 9 | use Cosray\Node\Factory; |
| 10 | use Cosray\Node\Types; |
| 11 | use Cosray\Node\Wrapper; |
| 12 | use Cosray\User; |
| 13 | |
| 14 | /** |
| 15 | * Who may read and write which nodes. A node type maps the actions `read`, |
| 16 | * `create`, `change` and `delete` to permissions through `#[Permission]`; |
| 17 | * actions it leaves out keep the defaults. `read` covers published, |
| 18 | * undeleted nodes; any other state takes the type's `change` permission. |
| 19 | */ |
| 20 | final class NodeAccess |
| 21 | { |
| 22 | public const string READ = 'read'; |
| 23 | public const string CREATE = 'create'; |
| 24 | public const string CHANGE = 'change'; |
| 25 | public const string DELETE = 'delete'; |
| 26 | |
| 27 | /** @var array<string, class-string>|null */ |
| 28 | private ?array $registered = null; |
| 29 | |
| 30 | public function __construct( |
| 31 | private readonly Policy $policy, |
| 32 | private readonly Types $types, |
| 33 | private readonly Container $container, |
| 34 | ) {} |
| 35 | |
| 36 | /** @return array{read: string, create: string, change: string, delete: string} */ |
| 37 | public static function defaults(bool $routable): array |
| 38 | { |
| 39 | return [ |
| 40 | self::READ => $routable ? 'read-pages' : 'read-nodes', |
| 41 | self::CREATE => 'edit-nodes', |
| 42 | self::CHANGE => 'edit-nodes', |
| 43 | self::DELETE => 'edit-nodes', |
| 44 | ]; |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * A string `#[Permission]` applies to every action. |
| 49 | * |
| 50 | * @param class-string $class |
| 51 | */ |
| 52 | public function permission(string $class, string $action): string |
| 53 | { |
| 54 | $declared = $this->types->get($class, 'permission'); |
| 55 | |
| 56 | if (is_string($declared) && $declared !== '') { |
| 57 | return $declared; |
| 58 | } |
| 59 | |
| 60 | $permission = is_array($declared) ? $declared[$action] ?? null : null; |
| 61 | |
| 62 | if (is_string($permission) && $permission !== '') { |
| 63 | return $permission; |
| 64 | } |
| 65 | |
| 66 | return self::defaults((bool) $this->types->get($class, 'routable', false))[$action] ?? 'edit-nodes'; |
| 67 | } |
| 68 | |
| 69 | /** @param class-string $class */ |
| 70 | public function allows(?User $user, string $action, string $class): bool |
| 71 | { |
| 72 | return $this->policy->permits($user, $this->permission($class, $action)); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * Whether the website serves the node at its path to this user. It serves |
| 77 | * any published node with a path, unless the type declares a read |
| 78 | * permission, which then applies as in the API, even when it names the |
| 79 | * default permission. |
| 80 | */ |
| 81 | public function serves(?User $user, object $node): bool |
| 82 | { |
| 83 | $class = Wrapper::unwrap($node)::class; |
| 84 | |
| 85 | return !$this->declaresRead($class) || $this->allows($user, self::READ, $class); |
| 86 | } |
| 87 | |
| 88 | public function reads(?User $user, object $node): bool |
| 89 | { |
| 90 | $inner = Wrapper::unwrap($node); |
| 91 | |
| 92 | return $this->allows($user, self::live($inner) ? self::READ : self::CHANGE, $inner::class); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Handles of the registered types whose live nodes the user reads, and |
| 97 | * of those whose nodes the user reads in any state. |
| 98 | * |
| 99 | * @return array{live: list<string>, any: list<string>} |
| 100 | */ |
| 101 | public function readable(?User $user): array |
| 102 | { |
| 103 | $live = []; |
| 104 | $any = []; |
| 105 | |
| 106 | foreach ($this->registered() as $handle => $class) { |
| 107 | if ($this->allows($user, self::CHANGE, $class)) { |
| 108 | $any[] = $handle; |
| 109 | } elseif ($this->allows($user, self::READ, $class)) { |
| 110 | $live[] = $handle; |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | return ['live' => $live, 'any' => $any]; |
| 115 | } |
| 116 | |
| 117 | /** @return array<string, class-string> registered node classes by type handle */ |
| 118 | public function registered(): array |
| 119 | { |
| 120 | if ($this->registered !== null) { |
| 121 | return $this->registered; |
| 122 | } |
| 123 | |
| 124 | $registered = []; |
| 125 | $tag = $this->container->tag(Bootstrap::NODE_TAG); |
| 126 | |
| 127 | foreach ($tag->entries() as $handle) { |
| 128 | $class = $tag->entry($handle)->definition(); |
| 129 | |
| 130 | if (is_string($class) && class_exists($class)) { |
| 131 | $registered[$handle] = $class; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | return $this->registered = $registered; |
| 136 | } |
| 137 | |
| 138 | /** @param class-string $class */ |
| 139 | private function declaresRead(string $class): bool |
| 140 | { |
| 141 | if (!$this->types->schemaOf($class)->declares('permission')) { |
| 142 | return false; |
| 143 | } |
| 144 | |
| 145 | $declared = $this->types->get($class, 'permission'); |
| 146 | |
| 147 | return is_string($declared) && $declared !== '' || is_array($declared) && ($declared[self::READ] ?? '') !== ''; |
| 148 | } |
| 149 | |
| 150 | private static function live(object $node): bool |
| 151 | { |
| 152 | return (bool) Factory::meta($node, 'published') && Factory::meta($node, 'deleted') === null; |
| 153 | } |
| 154 | } |