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