Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.47% covered (warning)
76.47%
104 / 136
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
User
76.47% covered (warning)
76.47%
104 / 136
71.43% covered (warning)
71.43%
10 / 14
117.36
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 readWorkstation
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 testWorkstationAssigend
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 testWorkstationAccessRights
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
5.20
 testWorkstationAssignedRoles
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 hasLogin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 checkPermissions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkAnyPermission
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkDepartments
60.00% covered (warning)
60.00%
21 / 35
0.00% covered (danger)
0.00%
0 / 1
21.22
 checkDepartment
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 hasXApiKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 testWorkstationIsOveraged
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 testReadDepartmentByOrganisation
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 normalizeDepartmentIds
64.29% covered (warning)
64.29%
9 / 14
0.00% covered (danger)
0.00%
0 / 1
6.14
1<?php
2
3namespace BO\Zmsbackend\Helper;
4
5use BO\Slim\Render;
6use BO\Zmsbackend\Workstation\Service\Workstation;
7use BO\Zmsbackend\Helper\UserAuth;
8use BO\Zmsentities\Collection\DepartmentList;
9
10/**
11 *
12 * @SuppressWarnings(CouplingBetweenObjects)
13 */
14class User
15{
16    public static $workstation = null;
17    public static $workstationResolved = null;
18
19    public static $assignedWorkstation = null;
20
21    public static $request = null;
22
23    private const array SUPERUSER_ONLY_ROLES = [
24        'system_admin',
25        'audit_viewer',
26    ];
27
28    public function __construct($request, $resolveReferences = 0)
29    {
30        static::$request = $request;
31        static::readWorkstation($resolveReferences);
32    }
33
34    public static function readWorkstation(int $resolveReferences = 0)
35    {
36        $request = (static::$request) ? static::$request : Render::$request;
37        if (! static::$workstation) {
38            $useraccount = UserAuth::getUseraccountByAuthMethod($request);
39            if ($useraccount && $useraccount->hasId()) {
40                static::$workstation = (new \BO\Zmsbackend\Workstation\Service\Workstation())->readEntity($useraccount->id, $resolveReferences);
41                if ($resolveReferences < 1) {
42                    static::$workstation->useraccount = $useraccount;
43                }
44                static::$workstationResolved = $resolveReferences;
45            } else {
46                static::$workstation = new \BO\Zmsentities\Workstation();
47            }
48        }
49        if ($resolveReferences > static::$workstationResolved && static::$workstation->hasId()) {
50            static::$workstation = (new \BO\Zmsbackend\Workstation\Service\Workstation())
51                ->readResolvedReferences(static::$workstation, $resolveReferences);
52        }
53        return static::$workstation;
54    }
55
56    /**
57     * @throws \BO\Zmsbackend\Workstation\Exception\WorkstationAlreadyAssigned
58     *
59     * @psalm-api
60     *
61     * @return void
62     */
63    public static function testWorkstationAssigend(\BO\Zmsentities\Workstation $entity, $resolveReferences = 0)
64    {
65        if (! static::$assignedWorkstation && $entity->name) {
66            static::$assignedWorkstation = (new \BO\Zmsbackend\Workstation\Service\Workstation())->readWorkstationByScopeAndName(
67                $entity->scope['id'],
68                $entity->name,
69                $resolveReferences
70            );
71        }
72        if (
73            static::$assignedWorkstation &&
74            static::$assignedWorkstation->id != $entity->id &&
75            static::$assignedWorkstation->name == $entity->name &&
76            static::$assignedWorkstation->scope['id'] == $entity->scope['id'] &&
77            ! static::$assignedWorkstation->getUseraccount()->isOveraged(\App::$now)
78        ) {
79            throw new \BO\Zmsbackend\Workstation\Exception\WorkstationAlreadyAssigned();
80        }
81    }
82
83    /**
84     * @throws \BO\Zmsentities\Exception\UserAccountAccessRightsFailed
85     *
86     * @return void
87     */
88    public static function testWorkstationAccessRights(\BO\Zmsentities\Useraccount $useraccount)
89    {
90        if (
91            (
92                ! static::$workstation->getUseraccount()->isSuperUser() &&
93                ! static::$workstation->hasAccessToUseraccount($useraccount)
94            ) ||
95            (
96                ! static::$workstation->getUseraccount()->isSuperUser() &&
97                $useraccount->isSuperUser()
98            )
99        ) {
100            throw new \BO\Zmsentities\Exception\UserAccountAccessRightsFailed();
101        }
102    }
103
104
105    public static function testWorkstationAssignedRoles(\BO\Zmsentities\Useraccount $useraccount): void
106    {
107        if (! $useraccount->offsetExists('roles') || ! is_array($useraccount['roles'])) {
108            throw new \BO\Zmsbackend\Useraccount\Exception\UseraccountInvalidRoleAssignment();
109        }
110
111        $roleNames = array_values(array_unique(array_filter(
112            array_map(
113                static fn ($roleName) => is_string($roleName) ? trim($roleName) : '',
114                $useraccount['roles']
115            )
116        )));
117
118        if (count($roleNames) !== 1) {
119            throw new \BO\Zmsbackend\Useraccount\Exception\UseraccountInvalidRoleAssignment();
120        }
121
122        $roleName = $roleNames[0];
123        $existingRole = (new \BO\Zmsbackend\Role\Service\Role())->readRoleByName($roleName, 0);
124        if ($existingRole === null) {
125            throw new \BO\Zmsbackend\Useraccount\Exception\UseraccountInvalidRoleAssignment();
126        }
127
128        if (
129            ! static::$workstation->getUseraccount()->isSuperUser()
130            && array_intersect($roleNames, self::SUPERUSER_ONLY_ROLES)
131        ) {
132            throw new \BO\Zmsentities\Exception\UserAccountMissingRights();
133        }
134
135        $useraccount['roles'] = $roleNames;
136    }
137
138    public static function hasLogin(): bool
139    {
140        $userAccount = static::readWorkstation()->getUseraccount();
141        return $userAccount->hasId();
142    }
143
144    /**
145     * @param \BO\Zmsentities\Useraccount\EntityAccess|string $requiredPermissions
146     */
147    public static function checkPermissions(string|\BO\Zmsentities\Useraccount\EntityAccess ...$requiredPermissions)
148    {
149        $workstation = static::readWorkstation();
150
151        if (\App::RIGHTSCHECK_ENABLED) {
152            $workstation->getUseraccount()->testPermissions($requiredPermissions);
153        }
154
155        return $workstation;
156    }
157
158    public static function checkAnyPermission(string ...$requiredPermissions)
159    {
160        $workstation = static::readWorkstation();
161
162        if (\App::RIGHTSCHECK_ENABLED) {
163            $workstation->getUseraccount()->testAnyPermission($requiredPermissions);
164        }
165
166        return $workstation;
167    }
168
169    public static function checkDepartments($departmentIds): DepartmentList
170    {
171        $normalizedIds = self::normalizeDepartmentIds($departmentIds);
172        $departments = new DepartmentList();
173
174        if (empty($normalizedIds)) {
175            return $departments;
176        }
177
178        $workstation = static::readWorkstation(2);
179        $userAccount = $workstation->getUseraccount();
180
181        if (! $userAccount->hasId()) {
182            throw new \BO\Zmsentities\Exception\UserAccountMissingLogin();
183        }
184
185        if ($userAccount->isSuperUser()) {
186            // Bulk-load all departments in one query for superusers
187            $departmentMap = (new \BO\Zmsbackend\Department\Service\Department())->readEntitiesByIds($normalizedIds, 1);
188            foreach ($normalizedIds as $departmentId) {
189                if (!isset($departmentMap[$departmentId])) {
190                    throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
191                        "No access to department " . htmlspecialchars((string) $departmentId)
192                    );
193                }
194                $departments->addEntity($departmentMap[$departmentId]);
195            }
196        } elseif ($userAccount->hasPermissions(['department'])) {
197            // Users with 'department' permission: need organisation-based access checks
198            // Group departments by organisation and load in batches
199            foreach ($normalizedIds as $departmentId) {
200                $departments->addEntity(self::checkDepartment($departmentId));
201            }
202        } else {
203            // Regular users: extract departments directly from already-loaded user department list
204            $userDepartmentList = $userAccount->getDepartmentList();
205            $accessibleDepartmentIds = $userDepartmentList->getIds();
206            $accessibleRequestedIds = array_intersect($normalizedIds, $accessibleDepartmentIds);
207
208            if (count($accessibleRequestedIds) !== count($normalizedIds)) {
209                // Some requested departments are not accessible
210                $missingIds = array_diff($normalizedIds, $accessibleRequestedIds);
211                throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
212                    "No access to department(s): " . implode(', ', array_map('htmlspecialchars', $missingIds))
213                );
214            }
215
216            // Extract requested departments from already-loaded list (no DB query needed)
217            foreach ($accessibleRequestedIds as $departmentId) {
218                $department = $userDepartmentList->getEntity($departmentId);
219                if (!$department || !$department->hasId()) {
220                    throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
221                        "No access to department " . htmlspecialchars((string) $departmentId)
222                    );
223                }
224                $departments->addEntity($department);
225            }
226        }
227
228        return $departments;
229    }
230
231    /**
232     * @return \BO\Zmsentities\Department
233     *
234     */
235    public static function checkDepartment($departmentId)
236    {
237        $workstation = static::readWorkstation(2);
238        $userAccount = $workstation->getUseraccount();
239        if (! $userAccount->hasId()) {
240            throw new \BO\Zmsentities\Exception\UserAccountMissingLogin();
241        }
242        if ($userAccount->isSuperUser()) {
243            $department = (new \BO\Zmsbackend\Department\Service\Department())->readEntity($departmentId);
244        } elseif ($userAccount->hasPermissions(['department'])) {
245            $department = self::testReadDepartmentByOrganisation($departmentId, $userAccount);
246        } else {
247            $department = $userAccount->testDepartmentById($departmentId);
248        }
249        if (! $department) {
250            throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
251                "No access to department " . htmlspecialchars($departmentId)
252            );
253        }
254        return $department;
255    }
256
257
258    /**
259     * Get X-Api-Key from header
260     */
261    public static function hasXApiKey(\Psr\Http\Message\RequestInterface $request): bool
262    {
263        $xApiKeyEntity = null;
264        $xApiKey = $request->getHeaderLine('x-api-key');
265        if ($xApiKey) {
266            $xApiKeyEntity = (new \BO\Zmsbackend\Apikey\Service\Apikey())->readEntity($xApiKey);
267        }
268        return ($xApiKeyEntity && $xApiKeyEntity->hasId());
269    }
270
271    /**
272     * @return void
273     */
274    public static function testWorkstationIsOveraged(\BO\Zmsentities\Workstation $workstation)
275    {
276        if ($workstation->hasId() && $workstation->getUseraccount()->isOveraged(\App::$now)) {
277            $exception = new \BO\Zmsbackend\Useraccount\Exception\AuthKeyFound();
278            $exception->data = $workstation;
279            throw $exception;
280        }
281    }
282
283    protected static function testReadDepartmentByOrganisation($departmentId, $userAccount)
284    {
285        $organisation = (new \BO\Zmsbackend\Organisation\Service\Organisation())->readByDepartmentId($departmentId, 1);
286        $organisation->departments = $organisation->getDepartmentList()->withAccess($userAccount);
287        $department = $organisation->departments->getEntity($departmentId);
288        return $department;
289    }
290
291    /**
292     * @return int[]
293     *
294     */
295    public static function normalizeDepartmentIds(array $departmentIds): array
296    {
297        $normalized = [];
298        foreach ($departmentIds as $departmentId) {
299            if ($departmentId === null) {
300                continue;
301            }
302            $departmentId = trim((string) $departmentId);
303            if ($departmentId === '') {
304                continue;
305            }
306            $validatedId = filter_var($departmentId, FILTER_VALIDATE_INT);
307            if ($validatedId === false) {
308                throw new \BO\Zmsbackend\Exception\BadRequest(
309                    "Invalid department ID: " . htmlspecialchars($departmentId)
310                );
311            }
312            $normalized[] = $validatedId;
313        }
314
315        return array_values(array_unique($normalized));
316    }
317}