Skip to content

Commit ca16981

Browse files
committed
feat(policy): migrate approval groups setting for id docs
Signed-off-by: Vitor Mattos <[email protected]>
1 parent 9efed20 commit ca16981

14 files changed

Lines changed: 337 additions & 47 deletions

File tree

lib/Helper/ValidateHelper.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
use OCA\Libresign\Service\FileService;
2929
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
3030
use OCA\Libresign\Service\IdentifyMethodService;
31+
use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicyValue;
3132
use OCA\Libresign\Service\Policy\RequestSignAuthorizationService;
3233
use OCA\Libresign\Service\SequentialSigningService;
3334
use OCA\Libresign\Service\SignerElementsService;
3435
use OCP\AppFramework\Db\DoesNotExistException;
36+
use OCP\Exceptions\AppConfigTypeConflictException;
3537
use OCP\Files\IMimeTypeDetector;
3638
use OCP\Files\IRootFolder;
3739
use OCP\Files\NotFoundException;
@@ -959,8 +961,8 @@ public function userCanApproveValidationDocuments(?IUser $user, bool $throw = tr
959961
return false;
960962
}
961963

962-
$authorized = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
963-
if (!$authorized || !is_array($authorized) || empty($authorized)) {
964+
$authorized = $this->getApprovalGroups();
965+
if (empty($authorized)) {
964966
$authorized = ['admin'];
965967
}
966968
$userGroups = $this->groupManager->getUserGroupIds($user);
@@ -973,6 +975,17 @@ public function userCanApproveValidationDocuments(?IUser $user, bool $throw = tr
973975
return true;
974976
}
975977

978+
/** @return list<string> */
979+
private function getApprovalGroups(): array {
980+
try {
981+
$value = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
982+
return ApprovalGroupsPolicyValue::decode($value);
983+
} catch (AppConfigTypeConflictException) {
984+
$value = $this->appConfig->getValueString(Application::APP_ID, 'approval_group', '[]');
985+
return ApprovalGroupsPolicyValue::decode($value);
986+
}
987+
}
988+
976989
private function validateDocMdpPdfRestrictions(array $data): void {
977990
if (empty($data['uuid']) || empty($data['signers'])) {
978991
return;

lib/Service/File/AccountSettingsProvider.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
use OCA\Libresign\AppInfo\Application;
1313
use OCA\Libresign\Exception\LibresignException;
1414
use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
15+
use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicyValue;
1516
use OCP\Accounts\IAccountManager;
17+
use OCP\Exceptions\AppConfigTypeConflictException;
1618
use OCP\IAppConfig;
1719
use OCP\IGroupManager;
1820
use OCP\IUser;
@@ -42,7 +44,7 @@ private function canRequestSign(?IUser $user = null): bool {
4244
if (!$user) {
4345
return false;
4446
}
45-
$authorized = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
47+
$authorized = $this->getApprovalGroups();
4648
if (empty($authorized)) {
4749
return false;
4850
}
@@ -69,11 +71,22 @@ private function isApprover(?IUser $user = null): bool {
6971
if (!$user) {
7072
return false;
7173
}
72-
$approvalGroups = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
74+
$approvalGroups = $this->getApprovalGroups();
7375
if (empty($approvalGroups)) {
7476
return false;
7577
}
7678
$userGroups = $this->groupManager->getUserGroupIds($user);
7779
return (bool)array_intersect($userGroups, $approvalGroups);
7880
}
81+
82+
/** @return list<string> */
83+
private function getApprovalGroups(): array {
84+
try {
85+
$value = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
86+
return ApprovalGroupsPolicyValue::decode($value);
87+
} catch (AppConfigTypeConflictException) {
88+
$value = $this->appConfig->getValueString(Application::APP_ID, 'approval_group', '[]');
89+
return ApprovalGroupsPolicyValue::decode($value);
90+
}
91+
}
7992
}

lib/Service/File/SettingsLoader.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
use OCA\Libresign\Service\IdDocsPolicyService;
1919
use OCA\Libresign\Service\IdentifyMethodService;
2020
use OCA\Libresign\Service\Policy\PolicyService;
21+
use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicyValue;
2122
use OCA\Libresign\Service\Policy\Provider\IdentificationDocuments\IdentificationDocumentsPolicy;
2223
use OCA\Libresign\Service\Policy\Provider\IdentificationDocuments\IdentificationDocumentsPolicyValue;
24+
use OCP\Exceptions\AppConfigTypeConflictException;
2325
use OCP\IAppConfig;
2426
use OCP\IGroupManager;
2527
use OCP\IUser;
@@ -91,7 +93,7 @@ public function getIdentificationDocumentsStatus(?IUser $user = null, ?SignReque
9193
return self::IDENTIFICATION_DOCUMENTS_DISABLED;
9294
}
9395

94-
$approvalGroups = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
96+
$approvalGroups = $this->getApprovalGroups();
9597
if ($user && !empty($approvalGroups) && is_array($approvalGroups)) {
9698
$userGroups = $this->groupManager->getUserGroupIds($user);
9799
if (array_intersect($userGroups, $approvalGroups)) {
@@ -144,6 +146,17 @@ private function isIdentificationDocumentsEnabled(?IUser $user): bool {
144146
return IdentificationDocumentsPolicyValue::normalize($resolved->getEffectiveValue(), false);
145147
}
146148

149+
/** @return list<string> */
150+
private function getApprovalGroups(): array {
151+
try {
152+
$value = $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']);
153+
return ApprovalGroupsPolicyValue::decode($value);
154+
} catch (AppConfigTypeConflictException) {
155+
$value = $this->appConfig->getValueString(Application::APP_ID, 'approval_group', '[]');
156+
return ApprovalGroupsPolicyValue::decode($value);
157+
}
158+
}
159+
147160
/**
148161
* Get user identification documents settings
149162
* These are user-specific settings, not file-specific
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Provider\ApprovalGroups;
10+
11+
use OCA\Libresign\Service\Policy\Contract\IPolicyDefinition;
12+
use OCA\Libresign\Service\Policy\Contract\IPolicyDefinitionProvider;
13+
use OCA\Libresign\Service\Policy\Model\PolicyContext;
14+
use OCA\Libresign\Service\Policy\Model\PolicySpec;
15+
16+
final class ApprovalGroupsPolicy implements IPolicyDefinitionProvider {
17+
public const KEY = 'approval_group';
18+
public const SYSTEM_APP_CONFIG_KEY = self::KEY;
19+
20+
#[\Override]
21+
public function keys(): array {
22+
return [
23+
self::KEY,
24+
];
25+
}
26+
27+
#[\Override]
28+
public function get(string|\BackedEnum $policyKey): IPolicyDefinition {
29+
return match ($this->normalizePolicyKey($policyKey)) {
30+
self::KEY => new PolicySpec(
31+
key: self::KEY,
32+
defaultSystemValue: ApprovalGroupsPolicyValue::encode(ApprovalGroupsPolicyValue::DEFAULT_GROUPS),
33+
allowedValues: static fn (PolicyContext $context): array => [],
34+
normalizer: static fn (mixed $rawValue): mixed => ApprovalGroupsPolicyValue::encode($rawValue),
35+
validator: static function (mixed $value): void {
36+
if (!is_string($value)) {
37+
throw new \InvalidArgumentException('Invalid value for ' . self::KEY);
38+
}
39+
40+
$decoded = ApprovalGroupsPolicyValue::decode($value);
41+
if ($decoded === []) {
42+
throw new \InvalidArgumentException('At least one authorized group is required for ' . self::KEY);
43+
}
44+
},
45+
appConfigKey: self::SYSTEM_APP_CONFIG_KEY,
46+
supportsUserPreference: false,
47+
),
48+
default => throw new \InvalidArgumentException('Unknown policy key: ' . $this->normalizePolicyKey($policyKey)),
49+
};
50+
}
51+
52+
private function normalizePolicyKey(string|\BackedEnum $policyKey): string {
53+
if ($policyKey instanceof \BackedEnum) {
54+
return (string)$policyKey->value;
55+
}
56+
57+
return $policyKey;
58+
}
59+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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\Provider\ApprovalGroups;
10+
11+
final class ApprovalGroupsPolicyValue {
12+
/** @var list<string> */
13+
public const DEFAULT_GROUPS = ['admin'];
14+
15+
/** @return list<string> */
16+
public static function decode(mixed $rawValue): array {
17+
if (is_array($rawValue)) {
18+
return self::normalizeGroupIds($rawValue);
19+
}
20+
21+
if (!is_string($rawValue)) {
22+
return [];
23+
}
24+
25+
$trimmed = trim($rawValue);
26+
if ($trimmed === '') {
27+
return [];
28+
}
29+
30+
$decoded = json_decode($trimmed, true);
31+
if (is_array($decoded)) {
32+
return self::normalizeGroupIds($decoded);
33+
}
34+
35+
return self::normalizeGroupIds(array_map('trim', explode(',', $trimmed)));
36+
}
37+
38+
public static function encode(mixed $rawValue): string {
39+
return json_encode(self::decode($rawValue), JSON_THROW_ON_ERROR);
40+
}
41+
42+
/**
43+
* @param array<mixed> $rawGroups
44+
* @return list<string>
45+
*/
46+
private static function normalizeGroupIds(array $rawGroups): array {
47+
$normalized = [];
48+
foreach ($rawGroups as $groupId) {
49+
if (!is_string($groupId)) {
50+
continue;
51+
}
52+
53+
$trimmed = trim($groupId);
54+
if ($trimmed === '') {
55+
continue;
56+
}
57+
58+
$normalized[] = $trimmed;
59+
}
60+
61+
$unique = array_values(array_unique($normalized));
62+
sort($unique);
63+
64+
return $unique;
65+
}
66+
}

lib/Service/Policy/Provider/PolicyProviders.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace OCA\Libresign\Service\Policy\Provider;
1010

11+
use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicy;
1112
use OCA\Libresign\Service\Policy\Provider\CollectMetadata\CollectMetadataPolicy;
1213
use OCA\Libresign\Service\Policy\Provider\DocMdp\DocMdpPolicy;
1314
use OCA\Libresign\Service\Policy\Provider\Footer\FooterPolicy;
@@ -19,6 +20,7 @@
1920
final class PolicyProviders {
2021
/** @var array<string, class-string> */
2122
public const BY_KEY = [
23+
ApprovalGroupsPolicy::KEY => ApprovalGroupsPolicy::class,
2224
CollectMetadataPolicy::KEY => CollectMetadataPolicy::class,
2325
FooterPolicy::KEY => FooterPolicy::class,
2426
DocMdpPolicy::KEY => DocMdpPolicy::class,

lib/Settings/Admin.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use OCA\Libresign\Service\IdDocsPolicyService;
1919
use OCA\Libresign\Service\IdentifyMethodService;
2020
use OCA\Libresign\Service\Policy\PolicyService;
21+
use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicy;
22+
use OCA\Libresign\Service\Policy\Provider\ApprovalGroups\ApprovalGroupsPolicyValue;
2123
use OCA\Libresign\Service\SignatureBackgroundService;
2224
use OCA\Libresign\Service\SignatureTextService;
2325
use OCP\AppFramework\Http\ContentSecurityPolicy;
@@ -96,7 +98,12 @@ public function getForm(): TemplateResponse {
9698
$this->initialState->provideInitialState('signing_mode', $this->getSigningModeInitialState());
9799
$this->initialState->provideInitialState('worker_type', $this->getWorkerTypeInitialState());
98100
$this->initialState->provideInitialState('identification_documents', $this->idDocsPolicyService->isIdentificationDocumentsEnabled());
99-
$this->initialState->provideInitialState('approval_group', $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']));
101+
$this->initialState->provideInitialState(
102+
'approval_group',
103+
ApprovalGroupsPolicyValue::decode(
104+
$this->policyService->resolve(ApprovalGroupsPolicy::KEY)->getEffectiveValue(),
105+
),
106+
);
100107
$this->initialState->provideInitialState('envelope_enabled', $this->appConfig->getValueBool(Application::APP_ID, 'envelope_enabled', true));
101108
$this->initialState->provideInitialState('parallel_workers', $this->appConfig->getValueString(Application::APP_ID, 'parallel_workers', '4'));
102109
$this->initialState->provideInitialState('show_confetti_after_signing', $this->appConfig->getValueBool(Application::APP_ID, 'show_confetti_after_signing', true));

src/tests/views/Settings/IdentificationDocuments.spec.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
77
import { flushPromises, mount } from '@vue/test-utils'
88

9-
const loadStateMock = vi.fn()
109
const axiosGetMock = vi.fn()
10+
const fetchEffectivePoliciesMock = vi.fn(async () => {})
11+
const getEffectiveValueMock = vi.fn((policyKey: string) => {
12+
if (policyKey === 'identification_documents') {
13+
return true
14+
}
1115

12-
vi.mock('@nextcloud/initial-state', () => ({
13-
loadState: (...args: unknown[]) => loadStateMock(...args),
14-
}))
16+
if (policyKey === 'approval_group') {
17+
return []
18+
}
19+
20+
return null
21+
})
22+
const saveSystemPolicyMock = vi.fn(async (_policyKey: string, value: string) => ({ effectiveValue: value }))
1523

1624
vi.mock('@nextcloud/axios', () => ({
1725
default: {
@@ -21,13 +29,13 @@ vi.mock('@nextcloud/axios', () => ({
2129

2230
vi.mock('@nextcloud/l10n', () => globalThis.mockNextcloudL10n())
2331

24-
const OCP = {
25-
AppConfig: {
26-
setValue: vi.fn(),
27-
},
28-
}
29-
30-
;(globalThis as typeof globalThis & { OCP: typeof OCP }).OCP = OCP
32+
vi.mock('../../../store/policies', () => ({
33+
usePoliciesStore: () => ({
34+
fetchEffectivePolicies: fetchEffectivePoliciesMock,
35+
getEffectiveValue: getEffectiveValueMock,
36+
saveSystemPolicy: saveSystemPolicyMock,
37+
}),
38+
}))
3139

3240
let IdentificationDocuments: unknown
3341

@@ -37,28 +45,13 @@ beforeAll(async () => {
3745

3846
describe('IdentificationDocuments', () => {
3947
beforeEach(() => {
40-
loadStateMock.mockReset()
4148
axiosGetMock.mockReset()
42-
OCP.AppConfig.setValue.mockClear()
49+
fetchEffectivePoliciesMock.mockClear()
50+
getEffectiveValueMock.mockClear()
51+
saveSystemPolicyMock.mockClear()
4352
})
4453

4554
it('saves groups on update:modelValue', async () => {
46-
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => {
47-
if (key === 'approval_group') {
48-
return []
49-
}
50-
if (key === 'effective_policies') {
51-
return {
52-
policies: {
53-
identification_documents: {
54-
effectiveValue: true,
55-
},
56-
},
57-
}
58-
}
59-
return fallback
60-
})
61-
6255
axiosGetMock.mockImplementation((url: string) => {
6356
if (url.includes('cloud/groups/details')) {
6457
return Promise.resolve({
@@ -95,6 +88,6 @@ describe('IdentificationDocuments', () => {
9588
ncSelect.vm.$emit('update:modelValue', [{ id: 'grpA', displayname: 'Group A' }])
9689
await flushPromises()
9790

98-
expect(OCP.AppConfig.setValue).toHaveBeenCalledWith('libresign', 'approval_group', '["grpA"]')
91+
expect(saveSystemPolicyMock).toHaveBeenCalledWith('approval_group', '["grpA"]', false)
9992
})
10093
})

0 commit comments

Comments
 (0)