Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.06% |
66 / 68 |
|
92.86% |
26 / 28 |
CRAP | |
0.00% |
0 / 1 |
| PolicySpec | |
97.06% |
66 / 68 |
|
92.86% |
26 / 28 |
45 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| key | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| resolutionMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| retainDefaultInGroupChoices | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| supportedScopes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| supportsScope | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getAppConfigKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getUserPreferenceKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isBackendOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isHelper | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| parentPolicyKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| compositeChildren | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| normalizeValue | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| validateValue | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| validateValueForPersistence | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| validateCompositeValuesForPersistence | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| allowedValues | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| defaultSystemValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| resolvedStateMeta | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| supportsUserPreference | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| supportsRequestOverride | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| requestLifecycle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| shouldFilterVisibleGroupCountsForActor | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| canCurrentActorManageGroupPolicy | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| canCurrentActorEditSystemCreatedGroupPolicy | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| hasExplicitSystemGroupManagementDelegation | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| supportsGroupAdminDelegation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| validateGroupAdminDelegatedValue | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\Policy\Model; |
| 10 | |
| 11 | use Closure; |
| 12 | use OCA\Libresign\Service\Policy\Contract\IPolicyDefinition; |
| 13 | use function in_array; |
| 14 | use function sprintf; |
| 15 | |
| 16 | final class PolicySpec implements IPolicyDefinition { |
| 17 | public const RESOLUTION_MODE_RESOLVED = 'resolved'; |
| 18 | public const RESOLUTION_MODE_VALUE_CHOICE = 'value_choice'; |
| 19 | public const SCOPE_SYSTEM = 'system'; |
| 20 | public const SCOPE_GROUP = 'group'; |
| 21 | public const SCOPE_USER = 'user'; |
| 22 | public const LIFECYCLE_RUNTIME = 'runtime'; |
| 23 | public const LIFECYCLE_REQUEST_SNAPSHOT = 'request_snapshot'; |
| 24 | |
| 25 | /** @var list<mixed>|Closure(PolicyContext): list<mixed> */ |
| 26 | private array|Closure $allowedValuesResolver; |
| 27 | /** @var Closure(mixed): mixed|null */ |
| 28 | private ?Closure $normalizer; |
| 29 | /** @var Closure(mixed, PolicyContext): void|null */ |
| 30 | private ?Closure $validator; |
| 31 | /** @var Closure(mixed, PolicyContext): void|null */ |
| 32 | private ?Closure $persistenceValidator; |
| 33 | /** @var Closure(array<string, mixed>, list<string>, PolicyContext): void|null */ |
| 34 | private ?Closure $compositeValidator; |
| 35 | /** @var array<string, mixed>|Closure(PolicyContext): array<string, mixed> */ |
| 36 | private array|Closure $resolvedStateMetaResolver; |
| 37 | /** @var Closure(PolicyContext, ?PolicyLayer): bool|null */ |
| 38 | private ?Closure $visibleGroupCountFilterResolver; |
| 39 | /** @var Closure(PolicyContext, ?PolicyLayer, array<array-key, PolicyLayer>): bool|null */ |
| 40 | private ?Closure $groupPolicyManagerResolver; |
| 41 | /** @var Closure(PolicyContext, ?PolicyLayer, PolicyLayer): bool|null */ |
| 42 | private ?Closure $systemCreatedGroupRuleEditorResolver; |
| 43 | /** @var Closure(mixed, mixed, PolicyContext): void|null */ |
| 44 | private ?Closure $delegatedValidator; |
| 45 | |
| 46 | /** |
| 47 | * @param list<mixed>|Closure(PolicyContext): list<mixed> $allowedValues |
| 48 | * @param bool $supportsRequestOverride No default on purpose: every policy decides it, independently of $supportsUserPreference |
| 49 | * @param self::LIFECYCLE_* $requestLifecycle No default on purpose: every policy decides whether a sent request keeps its value |
| 50 | * @param Closure(mixed): mixed|null $normalizer |
| 51 | * @param Closure(mixed, PolicyContext): void|null $validator |
| 52 | * @param Closure(mixed, PolicyContext): void|null $persistenceValidator Extra checks applied only when a value is saved |
| 53 | * @param Closure(array<string, mixed>, list<string>, PolicyContext): void|null $compositeValidator Checks applied to the whole composite family before any key is saved |
| 54 | * @param array<string, mixed>|Closure(PolicyContext): array<string, mixed> $resolvedStateMeta |
| 55 | * @param Closure(mixed, mixed, PolicyContext): void|null $delegatedValueValidator |
| 56 | * @param Closure(PolicyContext, ?PolicyLayer): bool|null $visibleGroupCountFilter |
| 57 | * @param Closure(PolicyContext, ?PolicyLayer, array<array-key, PolicyLayer>): bool|null $groupPolicyManager |
| 58 | * @param Closure(PolicyContext, ?PolicyLayer, PolicyLayer): bool|null $systemCreatedGroupRuleEditor |
| 59 | * @param list<string> $supportedScopes |
| 60 | * @param list<string> $compositeChildren |
| 61 | * @param bool $retainDefaultInGroupChoices Keep valid defaults in delegated value-choice group options; false preserves sentinel semantics |
| 62 | */ |
| 63 | public function __construct( |
| 64 | private string $key, |
| 65 | private mixed $defaultSystemValue, |
| 66 | array|Closure $allowedValues, |
| 67 | private bool $supportsRequestOverride, |
| 68 | private string $requestLifecycle, |
| 69 | ?Closure $normalizer = null, |
| 70 | ?Closure $validator = null, |
| 71 | private ?string $appConfigKey = null, |
| 72 | private string $resolutionMode = self::RESOLUTION_MODE_RESOLVED, |
| 73 | private bool $supportsUserPreference = true, |
| 74 | array|Closure $resolvedStateMeta = [], |
| 75 | ?Closure $visibleGroupCountFilter = null, |
| 76 | ?Closure $groupPolicyManager = null, |
| 77 | ?Closure $systemCreatedGroupRuleEditor = null, |
| 78 | private bool $supportsGroupAdminDelegation = false, |
| 79 | ?Closure $delegatedValueValidator = null, |
| 80 | private array $supportedScopes = [self::SCOPE_SYSTEM, self::SCOPE_GROUP, self::SCOPE_USER], |
| 81 | private bool $backendOnly = false, |
| 82 | private bool $helper = false, |
| 83 | private ?string $parentPolicyKey = null, |
| 84 | private array $compositeChildren = [], |
| 85 | ?Closure $persistenceValidator = null, |
| 86 | ?Closure $compositeValidator = null, |
| 87 | private bool $retainDefaultInGroupChoices = false, |
| 88 | ) { |
| 89 | $this->allowedValuesResolver = $allowedValues; |
| 90 | $this->normalizer = $normalizer; |
| 91 | $this->validator = $validator; |
| 92 | $this->persistenceValidator = $persistenceValidator; |
| 93 | $this->compositeValidator = $compositeValidator; |
| 94 | $this->resolvedStateMetaResolver = $resolvedStateMeta; |
| 95 | $this->visibleGroupCountFilterResolver = $visibleGroupCountFilter; |
| 96 | $this->groupPolicyManagerResolver = $groupPolicyManager; |
| 97 | $this->systemCreatedGroupRuleEditorResolver = $systemCreatedGroupRuleEditor; |
| 98 | $this->delegatedValidator = $delegatedValueValidator; |
| 99 | } |
| 100 | |
| 101 | #[\Override] |
| 102 | public function key(): string { |
| 103 | return $this->key; |
| 104 | } |
| 105 | |
| 106 | #[\Override] |
| 107 | public function resolutionMode(): string { |
| 108 | return $this->resolutionMode; |
| 109 | } |
| 110 | |
| 111 | #[\Override] |
| 112 | public function retainDefaultInGroupChoices(): bool { |
| 113 | return $this->retainDefaultInGroupChoices; |
| 114 | } |
| 115 | |
| 116 | /** @return list<string> */ |
| 117 | #[\Override] |
| 118 | public function supportedScopes(): array { |
| 119 | return $this->supportedScopes; |
| 120 | } |
| 121 | |
| 122 | #[\Override] |
| 123 | public function supportsScope(string $scope): bool { |
| 124 | return in_array($scope, $this->supportedScopes, true); |
| 125 | } |
| 126 | |
| 127 | #[\Override] |
| 128 | public function getAppConfigKey(): string { |
| 129 | return $this->appConfigKey ?? $this->key; |
| 130 | } |
| 131 | |
| 132 | #[\Override] |
| 133 | public function getUserPreferenceKey(): string { |
| 134 | return 'policy.' . $this->key; |
| 135 | } |
| 136 | |
| 137 | #[\Override] |
| 138 | public function isBackendOnly(): bool { |
| 139 | return $this->backendOnly; |
| 140 | } |
| 141 | |
| 142 | #[\Override] |
| 143 | public function isHelper(): bool { |
| 144 | return $this->helper; |
| 145 | } |
| 146 | |
| 147 | #[\Override] |
| 148 | public function parentPolicyKey(): ?string { |
| 149 | return $this->parentPolicyKey; |
| 150 | } |
| 151 | |
| 152 | /** @return list<string> */ |
| 153 | #[\Override] |
| 154 | public function compositeChildren(): array { |
| 155 | return $this->compositeChildren; |
| 156 | } |
| 157 | |
| 158 | #[\Override] |
| 159 | public function normalizeValue(mixed $rawValue): mixed { |
| 160 | if ($this->normalizer !== null) { |
| 161 | return ($this->normalizer)($rawValue); |
| 162 | } |
| 163 | |
| 164 | return $rawValue; |
| 165 | } |
| 166 | |
| 167 | #[\Override] |
| 168 | public function validateValue(mixed $value, PolicyContext $context): void { |
| 169 | if ($this->validator !== null) { |
| 170 | ($this->validator)($value, $context); |
| 171 | return; |
| 172 | } |
| 173 | |
| 174 | // Empty allowedValues means "no explicit restriction" for this policy key. |
| 175 | if ($this->allowedValues($context) === []) { |
| 176 | return; |
| 177 | } |
| 178 | |
| 179 | if (!in_array($value, $this->allowedValues($context), true)) { |
| 180 | throw new \InvalidArgumentException(sprintf('Invalid value for %s', $this->key())); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | #[\Override] |
| 185 | public function validateValueForPersistence(mixed $value, PolicyContext $context): void { |
| 186 | $this->validateValue($value, $context); |
| 187 | if ($this->persistenceValidator !== null) { |
| 188 | ($this->persistenceValidator)($value, $context); |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | #[\Override] |
| 193 | public function validateCompositeValuesForPersistence(array $normalizedValues, array $submittedKeys, PolicyContext $context): void { |
| 194 | if ($this->compositeValidator !== null) { |
| 195 | ($this->compositeValidator)($normalizedValues, $submittedKeys, $context); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | #[\Override] |
| 200 | public function allowedValues(PolicyContext $context): array { |
| 201 | if ($this->allowedValuesResolver instanceof Closure) { |
| 202 | return ($this->allowedValuesResolver)($context); |
| 203 | } |
| 204 | |
| 205 | return $this->allowedValuesResolver; |
| 206 | } |
| 207 | |
| 208 | #[\Override] |
| 209 | public function defaultSystemValue(): mixed { |
| 210 | return $this->defaultSystemValue; |
| 211 | } |
| 212 | |
| 213 | /** @return array<string, mixed> */ |
| 214 | #[\Override] |
| 215 | public function resolvedStateMeta(PolicyContext $context): array { |
| 216 | if ($this->resolvedStateMetaResolver instanceof Closure) { |
| 217 | return ($this->resolvedStateMetaResolver)($context); |
| 218 | } |
| 219 | |
| 220 | return $this->resolvedStateMetaResolver; |
| 221 | } |
| 222 | |
| 223 | #[\Override] |
| 224 | public function supportsUserPreference(): bool { |
| 225 | return $this->supportsUserPreference; |
| 226 | } |
| 227 | |
| 228 | #[\Override] |
| 229 | public function supportsRequestOverride(): bool { |
| 230 | return $this->supportsRequestOverride; |
| 231 | } |
| 232 | |
| 233 | #[\Override] |
| 234 | public function requestLifecycle(): string { |
| 235 | return $this->requestLifecycle; |
| 236 | } |
| 237 | |
| 238 | #[\Override] |
| 239 | public function shouldFilterVisibleGroupCountsForActor(PolicyContext $context, ?PolicyLayer $systemPolicy): bool { |
| 240 | if ($this->visibleGroupCountFilterResolver !== null) { |
| 241 | return ($this->visibleGroupCountFilterResolver)($context, $systemPolicy); |
| 242 | } |
| 243 | |
| 244 | if ($context->getActorRole()->canManageSystemPolicies) { |
| 245 | return false; |
| 246 | } |
| 247 | |
| 248 | return !$this->hasExplicitSystemGroupManagementDelegation($systemPolicy); |
| 249 | } |
| 250 | |
| 251 | /** @param list<PolicyLayer> $groupLayers */ |
| 252 | #[\Override] |
| 253 | public function canCurrentActorManageGroupPolicy(PolicyContext $context, ?PolicyLayer $systemPolicy, array $groupLayers): bool { |
| 254 | if ($this->groupPolicyManagerResolver !== null) { |
| 255 | return ($this->groupPolicyManagerResolver)($context, $systemPolicy, $groupLayers); |
| 256 | } |
| 257 | |
| 258 | $actorRole = $context->getActorRole(); |
| 259 | if ($actorRole->canManageSystemPolicies) { |
| 260 | return true; |
| 261 | } |
| 262 | |
| 263 | if (!$actorRole->canManageGroupPolicies) { |
| 264 | return false; |
| 265 | } |
| 266 | |
| 267 | return $this->hasExplicitSystemGroupManagementDelegation($systemPolicy); |
| 268 | } |
| 269 | |
| 270 | #[\Override] |
| 271 | public function canCurrentActorEditSystemCreatedGroupPolicy(PolicyContext $context, ?PolicyLayer $systemPolicy, PolicyLayer $existingPolicy): bool { |
| 272 | if ($this->systemCreatedGroupRuleEditorResolver !== null) { |
| 273 | return ($this->systemCreatedGroupRuleEditorResolver)($context, $systemPolicy, $existingPolicy); |
| 274 | } |
| 275 | |
| 276 | return true; |
| 277 | } |
| 278 | |
| 279 | private function hasExplicitSystemGroupManagementDelegation(?PolicyLayer $systemPolicy): bool { |
| 280 | return $systemPolicy instanceof PolicyLayer |
| 281 | && $systemPolicy->getScope() === 'global' |
| 282 | && $systemPolicy->isAllowChildOverride(); |
| 283 | } |
| 284 | |
| 285 | #[\Override] |
| 286 | public function supportsGroupAdminDelegation(): bool { |
| 287 | return $this->supportsGroupAdminDelegation; |
| 288 | } |
| 289 | |
| 290 | #[\Override] |
| 291 | public function validateGroupAdminDelegatedValue( |
| 292 | mixed $proposedNormalizedValue, |
| 293 | mixed $parentSeedNormalizedValue, |
| 294 | PolicyContext $context, |
| 295 | ): void { |
| 296 | if ($this->delegatedValidator !== null) { |
| 297 | ($this->delegatedValidator)($proposedNormalizedValue, $parentSeedNormalizedValue, $context); |
| 298 | } |
| 299 | } |
| 300 | } |