Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
100 / 105
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
AccessApiController
95.19% covered (success)
95.19%
99 / 104
81.82% covered (warning)
81.82%
9 / 11
29
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
 matrix
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 validateUserAccess
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 userAccessMatrix
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 updateModuleLevel
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 bulkUpdateModuleLevel
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 listRules
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 createRule
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
5.39
 deleteRule
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 recompile
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 jsonResponse
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
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\Access\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Access\Application\Service\AccessManagerServiceInterface;
12use App\Core\Engine\Application\Security\PermissionContextFactory;
13use Psr\Http\Message\ResponseFactoryInterface;
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\ServerRequestInterface;
16
17/**
18 * Access Management REST API Controller.
19 *
20 * Exposes endpoints for managing module access levels, explicit sharing rules, and fast-lookup cache.
21 *
22 * @package App\Core\Access\Presentation\Api
23 */
24final readonly class AccessApiController
25{
26    /**
27     * AccessApiController constructor.
28     *
29     * @param AccessManagerServiceInterface $accessManager   Access manager service.
30     * @param PermissionContextFactory      $contextFactory  Security context factory.
31     * @param ResponseFactoryInterface      $responseFactory PSR-7 response factory.
32     */
33    public function __construct(
34        private AccessManagerServiceInterface $accessManager,
35        private PermissionContextFactory $contextFactory,
36        private ResponseFactoryInterface $responseFactory
37    ) {
38    }
39
40    /**
41     * Returns full module permissions matrix.
42     *
43     * @return ResponseInterface JSON response.
44     */
45    public function matrix(): ResponseInterface
46    {
47        $matrix = $this->accessManager->getModuleMatrix();
48
49        return $this->jsonResponse([
50            'success' => true,
51            'data'    => $matrix,
52        ]);
53    }
54
55    private function validateUserAccess(ServerRequestInterface $request, int $userId): ?ResponseInterface
56    {
57        if ($userId <= 0) {
58            return $this->jsonResponse([
59                'success' => false,
60                'error'   => 'Invalid user identifier.',
61            ], 400);
62        }
63
64        $context = $this->contextFactory->createFromRequest($request);
65        if (!$context->isAuthenticated()) {
66            return $this->jsonResponse(['error' => 'Authentication required'], 401);
67        }
68
69        return (!$context->isSuperuser && $context->actorUserId !== $userId)
70            ? $this->jsonResponse(['success' => false, 'error' => 'Access denied.'], 403)
71            : null;
72    }
73
74    /**
75     * Returns precalculated access matrix for a specific user.
76     *
77     * @param ServerRequestInterface $request HTTP request.
78     * @param int                    $userId  Target user identifier.
79     * @return ResponseInterface JSON response.
80     */
81    public function userAccessMatrix(ServerRequestInterface $request, int $userId): ResponseInterface
82    {
83        $error = $this->validateUserAccess($request, $userId);
84        if ($error !== null) {
85            return $error;
86        }
87
88        $matrix = $this->accessManager->getUserAccessMatrix($userId);
89
90        return $this->jsonResponse([
91            'success' => true,
92            'data'    => $matrix,
93        ]);
94    }
95
96    /**
97     * Updates base access level for a module.
98     *
99     * @param ServerRequestInterface $request HTTP request with JSON payload.
100     * @return ResponseInterface JSON response.
101     */
102    public function updateModuleLevel(ServerRequestInterface $request): ResponseInterface
103    {
104        $context = $this->contextFactory->createFromRequest($request);
105        $body = (string) $request->getBody();
106        $payload = json_decode($body, true);
107
108        if (!is_array($payload) || empty($payload['module_name']) || empty($payload['access_level'])) {
109            return $this->jsonResponse([
110                'success' => false,
111                'error'   => 'Fields module_name and access_level are required.',
112            ], 400);
113        }
114
115        $moduleName = (string) $payload['module_name'];
116        $levelVal = (string) $payload['access_level'];
117
118        $newLevel = $this->accessManager->updateModuleLevel(
119            $moduleName,
120            $levelVal,
121            $context->actorUserId > 0 ? $context->actorUserId : null
122        );
123
124        return $this->jsonResponse([
125            'success' => true,
126            'data'    => [
127                'module_name'  => $moduleName,
128                'access_level' => $newLevel->value,
129                'level_label'  => $newLevel->label(),
130            ],
131        ]);
132    }
133
134    /**
135     * Updates base access level for multiple modules.
136     *
137     * @param ServerRequestInterface $request HTTP request with JSON payload.
138     * @return ResponseInterface JSON response.
139     */
140    public function bulkUpdateModuleLevel(ServerRequestInterface $request): ResponseInterface
141    {
142        $context = $this->contextFactory->createFromRequest($request);
143        $body = (string) $request->getBody();
144        $payload = json_decode($body, true);
145
146        if (!is_array($payload) || empty($payload['access_level'])) {
147            return $this->jsonResponse([
148                'success' => false,
149                'error'   => 'Field access_level is required.',
150            ], 400);
151        }
152
153        $levelVal = (string) $payload['access_level'];
154        $rawModules = $payload['modules'] ?? [];
155        $moduleNames = is_array($rawModules) ? array_map('strval', $rawModules) : [];
156
157        $count = $this->accessManager->bulkUpdateModuleLevels(
158            $levelVal,
159            $moduleNames,
160            $context->actorUserId > 0 ? $context->actorUserId : null
161        );
162
163        return $this->jsonResponse([
164            'success' => true,
165            'data'    => [
166                'access_level'  => $levelVal,
167                'updated_count' => $count,
168            ],
169        ]);
170    }
171
172    /**
173     * Returns rules defined for a specific module.
174     *
175     * @param string $moduleName System module name.
176     * @return ResponseInterface JSON response.
177     */
178    public function listRules(string $moduleName): ResponseInterface
179    {
180        $rules = $this->accessManager->getModuleRules($moduleName);
181
182        return $this->jsonResponse([
183            'success' => true,
184            'data'    => $rules,
185        ]);
186    }
187
188    /**
189     * Creates a new access rule for a module.
190     *
191     * @param ServerRequestInterface $request HTTP request with JSON payload.
192     * @return ResponseInterface JSON response.
193     */
194    public function createRule(ServerRequestInterface $request): ResponseInterface
195    {
196        $context = $this->contextFactory->createFromRequest($request);
197        $body = (string) $request->getBody();
198        $payload = json_decode($body, true);
199
200        if (!is_array($payload) || empty($payload['module_name']) || empty($payload['subject_id'])) {
201            return $this->jsonResponse([
202                'success' => false,
203                'error'   => 'Module name and subject ID are required.',
204            ], 400);
205        }
206
207        $rule = $this->accessManager->createRule(
208            $payload,
209            $context->actorUserId > 0 ? $context->actorUserId : null
210        );
211
212        return $this->jsonResponse([
213            'success' => true,
214            'data'    => $rule->toArray(),
215        ], 201);
216    }
217
218    /**
219     * Deletes an existing access rule.
220     *
221     * @param int $id Rule identifier.
222     * @return ResponseInterface JSON response.
223     */
224    public function deleteRule(int $id): ResponseInterface
225    {
226        $success = $this->accessManager->deleteRule($id);
227
228        return $this->jsonResponse([
229            'success' => $success,
230        ], $success ? 200 : 404);
231    }
232
233    /**
234     * Triggers full recompile of all fast-lookup rows.
235     *
236     * @return ResponseInterface JSON response.
237     */
238    public function recompile(): ResponseInterface
239    {
240        $this->accessManager->recompileAll();
241
242        return $this->jsonResponse([
243            'success' => true,
244            'message' => 'Materialized access permissions recompiled successfully.',
245        ]);
246    }
247
248    /**
249     * Helper to render standardized JSON response.
250     *
251     * @param array<string, mixed> $data   Response payload.
252     * @param int                  $status HTTP status code.
253     * @return ResponseInterface PSR-7 response.
254     */
255    private function jsonResponse(array $data, int $status = 200): ResponseInterface
256    {
257        $response = $this->responseFactory->createResponse($status);
258        $response->getBody()->write((string) json_encode($data, JSON_THROW_ON_ERROR));
259
260        return $response->withHeader('Content-Type', 'application/json; charset=UTF-8');
261    }
262}