Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.35% covered (warning)
82.35%
14 / 17
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
GrantAccessPolicy
82.35% covered (warning)
82.35%
14 / 17
50.00% covered (danger)
50.00%
3 / 6
16.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 addInternal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allowEveryone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decide
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Server;
6
7use LambdaTwelve\OneRecord\Api\Permission;
8use LambdaTwelve\OneRecord\Rdf\Iri;
9use LambdaTwelve\OneRecord\Server\Spi\AccessDelegationStore;
10use LambdaTwelve\OneRecord\Server\Spi\AccessPolicy;
11use LambdaTwelve\OneRecord\Server\Spi\Action;
12use LambdaTwelve\OneRecord\Server\Spi\Agent;
13use LambdaTwelve\OneRecord\Server\Spi\Decision;
14use LambdaTwelve\OneRecord\Server\Spi\Grant;
15use Psr\Clock\ClockInterface;
16
17/**
18 * The spec's access model over a grant store: deny by default; per-object
19 * grants to single agents or to everyone authenticated, whether given by the
20 * host or created by accepting an access delegation; and a set of internal
21 * agents (the host's own systems) allowed everything, internal-only
22 * endpoints included. Denials answer Forbid (403) unless told to Hide (404).
23 *
24 * Every grant lives in the AccessDelegationStore, including the "everyone"
25 * ones (a grant to the EVERYONE agent), so a persistent store keeps all of
26 * the state and this class holds only the internal agents. It is the policy
27 * production hosts run over their database; both wrapper packages do.
28 */
29final class GrantAccessPolicy implements AccessPolicy
30{
31    /** The agent IRI a public grant is written to: "anyone with a valid token". */
32    public const string EVERYONE = 'urn:lambda-twelve:one-record:everyone';
33
34    /** @var array<string, true> */
35    private array $internal = [];
36
37    /**
38     * @param list<Iri> $internalAgents
39     */
40    public function __construct(
41        private readonly AccessDelegationStore $delegations,
42        private readonly ClockInterface $clock,
43        private readonly Decision $denial = Decision::Forbid,
44        array $internalAgents = [],
45    ) {
46        foreach ($internalAgents as $agent) {
47            $this->addInternal($agent);
48        }
49    }
50
51    /**
52     * An agent that acts for the host: full access, internal endpoints included.
53     */
54    public function addInternal(Iri $agent): void
55    {
56        $this->internal[$agent->value] = true;
57    }
58
59    public function isInternal(Iri $agent): bool
60    {
61        return isset($this->internal[$agent->value]);
62    }
63
64    /**
65     * @param non-empty-list<Permission> $permissions
66     */
67    public function allow(Iri $agent, Iri $logisticsObject, array $permissions): void
68    {
69        $this->delegations->grant(new Grant($agent, $logisticsObject, $permissions));
70    }
71
72    /**
73     * @param non-empty-list<Permission> $permissions
74     */
75    public function allowEveryone(Iri $logisticsObject, array $permissions): void
76    {
77        $this->delegations->grant(new Grant(new Iri(self::EVERYONE), $logisticsObject, $permissions));
78    }
79
80    public function decide(Agent $agent, Action $action, ?Iri $resource): Decision
81    {
82        if (isset($this->internal[$agent->iri->value])) {
83            return Decision::Allow;
84        }
85        $permission = $action->permission() ?? ($action === Action::ReadAuditTrail ? Permission::GetLogisticsObject : null);
86        if ($permission === null || $resource === null) {
87            return $this->denial;
88        }
89        $now = $this->clock->now();
90        foreach ([$agent->iri, new Iri(self::EVERYONE)] as $grantee) {
91            foreach ($this->delegations->grantsFor($grantee, $resource) as $grant) {
92                if ($grant->isActiveAt($now) && $grant->allows($permission)) {
93                    return Decision::Allow;
94                }
95            }
96        }
97
98        return $this->denial;
99    }
100}