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