Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.01% covered (success)
97.01%
65 / 67
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestSignGroupsPolicyValue
97.01% covered (success)
97.01%
65 / 67
75.00% covered (warning)
75.00%
6 / 8
24
0.00% covered (danger)
0.00%
0 / 1
 decodePolicy
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
7
 decode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 decodeDenied
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 decodeScopedGroups
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 canUserGroupsRequestSign
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 encode
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 normalizePolicyPayload
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 normalizeGroupIds
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Policy\Provider\RequestSignGroups;
10
11final class RequestSignGroupsPolicyValue {
12    /** @var list<string> */
13    public const DEFAULT_ALLOW_GROUPS = ['admin'];
14
15    /** @var list<string> */
16    public const DEFAULT_DENY_GROUPS = [];
17
18    /**
19     * @return array{allowGroups: list<string>, denyGroups: list<string>}
20     */
21    public static function decodePolicy(mixed $rawValue): array {
22        if (is_array($rawValue)) {
23            if (array_is_list($rawValue)) {
24                return [
25                    'allowGroups' => self::normalizeGroupIds($rawValue),
26                    'denyGroups' => [],
27                ];
28            }
29
30            return self::normalizePolicyPayload($rawValue);
31        }
32
33        if (!is_string($rawValue)) {
34            return self::normalizePolicyPayload([]);
35        }
36
37        $trimmed = trim($rawValue);
38        if ($trimmed === '') {
39            return self::normalizePolicyPayload([]);
40        }
41
42        $decoded = json_decode($trimmed, true);
43        if (is_array($decoded)) {
44            if (array_is_list($decoded)) {
45                return [
46                    'allowGroups' => self::normalizeGroupIds($decoded),
47                    'denyGroups' => [],
48                ];
49            }
50
51            return self::normalizePolicyPayload($decoded);
52        }
53
54        return self::normalizePolicyPayload([]);
55    }
56
57    /** @return list<string> */
58    public static function decode(mixed $rawValue): array {
59        return self::decodePolicy($rawValue)['allowGroups'];
60    }
61
62    /** @return list<string> */
63    public static function decodeDenied(mixed $rawValue): array {
64        return self::decodePolicy($rawValue)['denyGroups'];
65    }
66
67    /**
68     * Return all policy-scoped groups (allow + deny), normalized and deduplicated.
69     *
70     * @return list<string>
71     */
72    public static function decodeScopedGroups(mixed $rawValue): array {
73        $policy = self::decodePolicy($rawValue);
74
75        return self::normalizeGroupIds([
76            ...$policy['allowGroups'],
77            ...$policy['denyGroups'],
78        ]);
79    }
80
81    /**
82     * Evaluate if at least one user group is authorized to request sign
83     * and none of the user groups are explicitly denied.
84     *
85     * @param array<mixed> $userGroups
86     */
87    public static function canUserGroupsRequestSign(mixed $rawValue, array $userGroups): bool {
88        $authorizedGroups = self::decode($rawValue);
89        if ($authorizedGroups === []) {
90            return false;
91        }
92
93        $normalizedUserGroups = self::normalizeGroupIds($userGroups);
94        if ($normalizedUserGroups === []) {
95            return false;
96        }
97
98        if (array_intersect($normalizedUserGroups, $authorizedGroups) === []) {
99            return false;
100        }
101
102        return array_intersect($normalizedUserGroups, self::decodeDenied($rawValue)) === [];
103    }
104
105    public static function encode(mixed $rawValue): string {
106        $payload = is_array($rawValue) && array_is_list($rawValue)
107            ? [
108                'allowGroups' => self::normalizeGroupIds($rawValue),
109                'denyGroups' => [],
110            ]
111            : self::decodePolicy($rawValue);
112        return json_encode($payload, JSON_THROW_ON_ERROR);
113    }
114
115    /** @param array<string, mixed> $rawValue */
116    private static function normalizePolicyPayload(array $rawValue): array {
117        $rawAllow = $rawValue['allowGroups'] ?? [];
118        $rawDeny = $rawValue['denyGroups'] ?? [];
119
120        $allowGroups = is_array($rawAllow)
121            ? self::normalizeGroupIds($rawAllow)
122            : [];
123        $denyGroups = is_array($rawDeny)
124            ? self::normalizeGroupIds($rawDeny)
125            : [];
126
127        return [
128            'allowGroups' => $allowGroups,
129            'denyGroups' => $denyGroups,
130        ];
131    }
132
133    /** @param array<mixed> $rawGroups
134     * @return list<string>
135     */
136    private static function normalizeGroupIds(array $rawGroups): array {
137        $normalized = [];
138        foreach ($rawGroups as $groupId) {
139            if (!is_string($groupId)) {
140                continue;
141            }
142
143            $trimmed = trim($groupId);
144            if ($trimmed === '') {
145                continue;
146            }
147
148            $normalized[] = $trimmed;
149        }
150
151        $unique = array_values(array_unique($normalized));
152        sort($unique);
153
154        return $unique;
155    }
156}