Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.77% covered (success)
96.77%
60 / 62
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
IdentifyMethodsPolicy
96.77% covered (success)
96.77%
60 / 62
80.00% covered (warning)
80.00%
4 / 5
22
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
 keys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
11
 assertDelegatedOverrideOnlyEnablesGrantedFactors
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 extractEnabledFactorNames
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
7.29
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\IdentifyMethods;
10
11use OCA\Libresign\Service\IdentifyMethodService;
12use OCA\Libresign\Service\Policy\Contract\IPolicyDefinition;
13use OCA\Libresign\Service\Policy\Contract\IPolicyDefinitionProvider;
14use OCA\Libresign\Service\Policy\Model\PolicyContext;
15use OCA\Libresign\Service\Policy\Model\PolicyLayer;
16use OCA\Libresign\Service\Policy\Model\PolicySpec;
17use OCA\Libresign\Service\Policy\Provider\Helper\DelegationLayerHelper;
18use OCA\Libresign\Service\Policy\Provider\Helper\PolicyKeyNormalizer;
19
20final class IdentifyMethodsPolicy implements IPolicyDefinitionProvider {
21    public const KEY = 'identify_methods';
22    public const SYSTEM_APP_CONFIG_KEY = self::KEY;
23
24    public function __construct(
25        private IdentifyMethodService $identifyMethodService,
26    ) {
27    }
28
29    #[\Override]
30    public function keys(): array {
31        return [
32            self::KEY,
33        ];
34    }
35
36    #[\Override]
37    public function get(string|\BackedEnum $policyKey): IPolicyDefinition {
38        $identifyMethodService = $this->identifyMethodService;
39        return match (PolicyKeyNormalizer::normalize($policyKey)) {
40            self::KEY => new PolicySpec(
41                key: self::KEY,
42                defaultSystemValue: [
43                    'factors' => [],
44                ],
45                allowedValues: static fn (): array => [],
46                normalizer: fn (mixed $rawValue): array => IdentifyMethodsPolicyValue::normalize($rawValue, $identifyMethodService),
47                appConfigKey: self::SYSTEM_APP_CONFIG_KEY,
48                supportsRequestOverride: true,
49                requestLifecycle: PolicySpec::LIFECYCLE_REQUEST_SNAPSHOT,
50                groupPolicyManager: static function (PolicyContext $context, ?PolicyLayer $systemPolicy, array $groupLayers): bool {
51                    $actorRole = $context->getActorRole();
52                    if ($actorRole->canManageSystemPolicies) {
53                        return true;
54                    }
55
56                    if (!$actorRole->canManageGroupPolicies) {
57                        return false;
58                    }
59
60                    if ($actorRole->manageableGroupCount < 1) {
61                        return false;
62                    }
63
64                    return DelegationLayerHelper::hasExplicitGlobalDelegation($systemPolicy)
65                        || DelegationLayerHelper::hasSystemCreatedGroupDelegation($groupLayers);
66                },
67                systemCreatedGroupRuleEditor: static function (PolicyContext $context, ?PolicyLayer $systemPolicy, PolicyLayer $existingPolicy): bool {
68                    $actorRole = $context->getActorRole();
69                    if ($actorRole->canManageSystemPolicies) {
70                        return true;
71                    }
72
73                    if (!$actorRole->canManageGroupPolicies) {
74                        return false;
75                    }
76
77                    if (!$existingPolicy->isVisibleToChild() || !$existingPolicy->isAllowChildOverride() || $existingPolicy->getValue() === null) {
78                        return false;
79                    }
80
81                    if (DelegationLayerHelper::hasExplicitGlobalDelegation($systemPolicy)) {
82                        return true;
83                    }
84
85                    return $existingPolicy->isCreatedBySystemAdmin();
86                },
87                supportsGroupAdminDelegation: true,
88                delegatedValueValidator: static function (mixed $proposedNormalizedValue, mixed $parentSeedNormalizedValue): void {
89                    self::assertDelegatedOverrideOnlyEnablesGrantedFactors($proposedNormalizedValue, $parentSeedNormalizedValue);
90                },
91            ),
92            default => throw new \InvalidArgumentException('Unknown policy key: ' . PolicyKeyNormalizer::normalize($policyKey)),
93        };
94    }
95
96    private static function assertDelegatedOverrideOnlyEnablesGrantedFactors(mixed $proposedNormalizedValue, mixed $parentSeedNormalizedValue): void {
97        $parentEnabledFactorNames = self::extractEnabledFactorNames($parentSeedNormalizedValue);
98        $proposedEnabledFactorNames = self::extractEnabledFactorNames($proposedNormalizedValue);
99
100        $invalidFactorNames = array_values(array_diff($proposedEnabledFactorNames, $parentEnabledFactorNames));
101        if ($invalidFactorNames !== []) {
102            throw new \InvalidArgumentException('Delegated identify methods overrides can only enable factors already granted by the system administrator.');
103        }
104    }
105
106    /** @return list<string> */
107    private static function extractEnabledFactorNames(mixed $normalizedValue): array {
108        if (!is_array($normalizedValue)) {
109            return [];
110        }
111
112        $enabledFactorNames = [];
113        foreach (IdentifyMethodsPolicyValue::extractFactors($normalizedValue) as $factor) {
114            $name = isset($factor['name']) && is_string($factor['name'])
115                ? trim($factor['name'])
116                : '';
117            if ($name === '' || !($factor['enabled'] ?? false)) {
118                continue;
119            }
120
121            $enabledFactorNames[] = $name;
122        }
123
124        return array_values(array_unique($enabledFactorNames));
125    }
126
127}