Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.92% covered (success)
95.92%
47 / 49
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
PermissionContext
97.92% covered (success)
97.92%
47 / 48
88.89% covered (warning)
88.89%
8 / 9
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAuthenticated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isImpersonating
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getAuditActorUserId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getRealUserName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 anonymous
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 fromSession
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
9
 createSuperuser
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Domain\Model;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Permission Context Value Object.
13 *
14 * Immutable value object carrying the security context of the current
15 * HTTP request actor. Created once per request by PermissionContextMiddleware
16 * and passed down through the engine stack to PermissionGuard.
17 *
18 * @package App\Core\Engine\Domain\Model
19 */
20final readonly class PermissionContext
21{
22    /**
23     * PermissionContext constructor.
24     *
25     * @param int             $actorUserId       ID of the authenticated user (0 for anonymous).
26     * @param string          $actorIp           Remote IP address of the request.
27     * @param bool            $isSuperuser       Whether the actor has full superuser privileges.
28     * @param bool            $ownerScopeEnabled Whether results should be filtered by owner.
29     * @param string|null     $requestId         Optional request correlation UUID.
30     * @param array<int, int> $actorStructureIds Structure node IDs actor belongs to.
31     * @param int|null        $actorProfileId    Optional assigned functional permission profile ID.
32     * @param int             $realUserId        Original authenticated user ID (audit/authorship context).
33     * @param string          $realUserName      Original authenticated user username.
34     */
35    public function __construct(
36        public readonly int     $actorUserId,
37        public readonly string  $actorIp,
38        public readonly bool    $isSuperuser,
39        public readonly bool    $ownerScopeEnabled,
40        public readonly ?string $requestId = null,
41        public readonly array   $actorStructureIds = [],
42        public readonly ?int    $actorProfileId = null,
43        public readonly int     $realUserId = 0,
44        public readonly string  $realUserName = '',
45    ) {
46    }
47
48    /**
49     * Checks whether the actor is authenticated (non-anonymous).
50     *
51     * @return bool True if actor has a valid user ID.
52     */
53    public function isAuthenticated(): bool
54    {
55        return $this->actorUserId > 0;
56    }
57
58    /**
59     * Checks whether the request is operating under user impersonation.
60     *
61     * @return bool True if original real user differs from active effective user.
62     */
63    public function isImpersonating(): bool
64    {
65        return $this->realUserId > 0 && $this->realUserId !== $this->actorUserId;
66    }
67
68    /**
69     * Returns the user ID credited for audit trail logs, record creation, and comments.
70     *
71     * In normal sessions, this equals actorUserId. Under impersonation, it credits the real original user.
72     *
73     * @return int Audit actor user ID.
74     */
75    public function getAuditActorUserId(): int
76    {
77        return $this->realUserId > 0 ? $this->realUserId : $this->actorUserId;
78    }
79
80    /**
81     * Returns the real user name for audit display and comment attribution.
82     *
83     * @return string Real user name.
84     */
85    public function getRealUserName(): string
86    {
87        return $this->realUserName;
88    }
89
90    /**
91     * Creates an anonymous permission context for unauthenticated requests.
92     *
93     * @param string      $ipAddress The remote IP address of the request.
94     * @param string|null $requestId Optional request correlation UUID.
95     * @return self Anonymous permission context with no privileges.
96     */
97    public static function anonymous(string $ipAddress, ?string $requestId = null): self
98    {
99        return new self(
100            actorUserId:       0,
101            actorIp:           $ipAddress,
102            isSuperuser:       false,
103            ownerScopeEnabled: false,
104            requestId:         $requestId,
105            actorStructureIds: [],
106        );
107    }
108
109    /**
110     * Creates a permission context from a session user data array.
111     *
112     * @param array<string, mixed>      $sessionUser       Authenticated user data from session.
113     * @param string                    $ipAddress         The remote IP address of the request.
114     * @param bool                      $ownerScoped       Whether owner scope is active.
115     * @param string|null               $requestId         Optional request correlation UUID.
116     * @param array<int, int>           $actorStructureIds Structure node IDs actor belongs to.
117     * @param array<string, mixed>|null $impersonatorUser  Optional original impersonator user data.
118     * @return self Hydrated permission context.
119     */
120    public static function fromSession(
121        array   $sessionUser,
122        string  $ipAddress,
123        bool    $ownerScoped = false,
124        ?string $requestId = null,
125        array   $actorStructureIds = [],
126        ?array  $impersonatorUser = null
127    ): self {
128        if (!empty($actorStructureIds)) {
129            $structIds = $actorStructureIds;
130        } elseif (isset($sessionUser['structure_ids']) && is_array($sessionUser['structure_ids'])) {
131            $structIds = array_map('intval', $sessionUser['structure_ids']);
132        } else {
133            $structIds = [];
134        }
135
136        $effectiveUserId = (int) ($sessionUser['id'] ?? 0);
137        $realUserId = $impersonatorUser !== null && isset($impersonatorUser['id'])
138            ? (int) $impersonatorUser['id']
139            : $effectiveUserId;
140        $realUserName = $impersonatorUser !== null && isset($impersonatorUser['username'])
141            ? (string) $impersonatorUser['username']
142            : (string) ($sessionUser['username'] ?? '');
143
144        return new self(
145            actorUserId:       $effectiveUserId,
146            actorIp:           $ipAddress,
147            isSuperuser:       (bool) ($sessionUser['is_superuser'] ?? false),
148            ownerScopeEnabled: $ownerScoped,
149            requestId:         $requestId,
150            actorStructureIds: $structIds,
151            actorProfileId:    isset($sessionUser['profile_id']) ? (int) $sessionUser['profile_id'] : null,
152            realUserId:        $realUserId,
153            realUserName:      $realUserName,
154        );
155    }
156
157    /**
158     * Creates a privileged superuser permission context.
159     *
160     * @param int             $actorUserId       Authenticated superuser ID (default 1).
161     * @param string          $actorIp           Remote client IP (default '127.0.0.1').
162     * @param string|null     $requestId         Optional request correlation UUID.
163     * @param array<int, int> $actorStructureIds Structure node IDs (default empty).
164     * @param int|null        $actorProfileId    Profile ID (default 1 for admin).
165     * @return self Superuser permission context with unrestricted access.
166     */
167    public static function createSuperuser(
168        int     $actorUserId = 1,
169        string  $actorIp = '127.0.0.1',
170        ?string $requestId = null,
171        array   $actorStructureIds = [],
172        ?int    $actorProfileId = 1
173    ): self {
174        return new self(
175            actorUserId:       $actorUserId,
176            actorIp:           $actorIp,
177            isSuperuser:       true,
178            ownerScopeEnabled: false,
179            requestId:         $requestId,
180            actorStructureIds: $actorStructureIds,
181            actorProfileId:    $actorProfileId,
182        );
183    }
184
185    /**
186     * Backward-compatible property accessor for aliases (e.g. $userId).
187     *
188     * @param string $name Property name.
189     * @return mixed Property value or null.
190     */
191    public function __get(string $name): mixed
192    {
193        if ($name === 'userId') {
194            return $this->actorUserId;
195        }
196
197        return null;
198    }
199}
200