@adobe/spacecat-shared-data-access 3.24.0 → 3.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/models/access-grant-log/access-grant-log.collection.js +26 -0
- package/src/models/access-grant-log/access-grant-log.model.js +31 -0
- package/src/models/access-grant-log/access-grant-log.schema.js +62 -0
- package/src/models/access-grant-log/index.d.ts +37 -0
- package/src/models/access-grant-log/index.js +15 -0
- package/src/models/base/base.collection.js +14 -0
- package/src/models/base/base.model.js +14 -0
- package/src/models/base/entity.registry.js +6 -0
- package/src/models/index.d.ts +2 -0
- package/src/models/index.js +2 -0
- package/src/models/sentiment-topic/index.d.ts +0 -9
- package/src/models/sentiment-topic/sentiment-topic.schema.js +0 -19
- package/src/models/site/site.schema.js +5 -0
- package/src/models/site-ims-org-access/index.d.ts +64 -0
- package/src/models/site-ims-org-access/index.js +15 -0
- package/src/models/site-ims-org-access/site-ims-org-access.collection.js +264 -0
- package/src/models/site-ims-org-access/site-ims-org-access.model.js +32 -0
- package/src/models/site-ims-org-access/site-ims-org-access.schema.js +63 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-data-access-v3.26.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.25.0...@adobe/spacecat-shared-data-access-v3.26.0) (2026-03-19)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add access control helpers for cross-org delegation (Option 2a) ([#1453](https://github.com/adobe/spacecat-shared/issues/1453)) ([960623e](https://github.com/adobe/spacecat-shared/commit/960623ea9e77e849be8b0a5bd7ee047309377cef)), closes [adobe/spacecat-auth-service#503](https://github.com/adobe/spacecat-auth-service/issues/503) [#1448](https://github.com/adobe/spacecat-shared/issues/1448)
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-data-access-v3.25.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.24.0...@adobe/spacecat-shared-data-access-v3.25.0) (2026-03-19)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* cross-org delegation entity models and AuthInfo extensions ([#1448](https://github.com/adobe/spacecat-shared/issues/1448)) ([b0cb091](https://github.com/adobe/spacecat-shared/commit/b0cb091432f15eb304e20263c0a18ec794d93bb9))
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-data-access-v3.24.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.23.0...@adobe/spacecat-shared-data-access-v3.24.0) (2026-03-19)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseCollection from '../base/base.collection.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* AccessGrantLogCollection - Collection of immutable audit log entries.
|
|
17
|
+
* Supports creation and querying only (no update/delete at the app layer).
|
|
18
|
+
*
|
|
19
|
+
* @class AccessGrantLogCollection
|
|
20
|
+
* @extends BaseCollection
|
|
21
|
+
*/
|
|
22
|
+
class AccessGrantLogCollection extends BaseCollection {
|
|
23
|
+
static COLLECTION_NAME = 'AccessGrantLogCollection';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default AccessGrantLogCollection;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseModel from '../base/base.model.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* AccessGrantLog - Immutable audit log entry for cross-org delegation grant/revoke actions.
|
|
17
|
+
* site_id and organization_id are TEXT (not FK) — values survive entity deletion.
|
|
18
|
+
*
|
|
19
|
+
* @class AccessGrantLog
|
|
20
|
+
* @extends BaseModel
|
|
21
|
+
*/
|
|
22
|
+
class AccessGrantLog extends BaseModel {
|
|
23
|
+
static ENTITY_NAME = 'AccessGrantLog';
|
|
24
|
+
|
|
25
|
+
static GRANT_ACTIONS = {
|
|
26
|
+
GRANT: 'grant',
|
|
27
|
+
REVOKE: 'revoke',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default AccessGrantLog;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isValidUUID } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import { Entitlement } from '../entitlement/index.js';
|
|
15
|
+
import SiteImsOrgAccess from '../site-ims-org-access/site-ims-org-access.model.js';
|
|
16
|
+
import SchemaBuilder from '../base/schema.builder.js';
|
|
17
|
+
import AccessGrantLog from './access-grant-log.model.js';
|
|
18
|
+
import AccessGrantLogCollection from './access-grant-log.collection.js';
|
|
19
|
+
|
|
20
|
+
const PERFORMED_BY_PATTERN = /^(ims:\S+|slack:\S+|system)$/;
|
|
21
|
+
|
|
22
|
+
const schema = new SchemaBuilder(AccessGrantLog, AccessGrantLogCollection)
|
|
23
|
+
.allowUpdates(false)
|
|
24
|
+
.allowRemove(false)
|
|
25
|
+
// siteId, organizationId, and targetOrganizationId are plain addAttribute (not belongs_to)
|
|
26
|
+
// because the DB columns are TEXT, not FK. Audit logs must preserve UUIDs after entity deletion.
|
|
27
|
+
.addAttribute('siteId', {
|
|
28
|
+
type: 'string',
|
|
29
|
+
required: true,
|
|
30
|
+
validate: (v) => isValidUUID(v),
|
|
31
|
+
})
|
|
32
|
+
.addAttribute('organizationId', {
|
|
33
|
+
type: 'string',
|
|
34
|
+
required: true,
|
|
35
|
+
validate: (v) => isValidUUID(v),
|
|
36
|
+
})
|
|
37
|
+
.addAttribute('targetOrganizationId', {
|
|
38
|
+
type: 'string',
|
|
39
|
+
required: true,
|
|
40
|
+
validate: (v) => isValidUUID(v),
|
|
41
|
+
})
|
|
42
|
+
.addAttribute('productCode', {
|
|
43
|
+
type: Object.values(Entitlement.PRODUCT_CODES),
|
|
44
|
+
required: true,
|
|
45
|
+
})
|
|
46
|
+
.addAttribute('action', {
|
|
47
|
+
type: Object.values(AccessGrantLog.GRANT_ACTIONS),
|
|
48
|
+
required: true,
|
|
49
|
+
})
|
|
50
|
+
.addAttribute('role', {
|
|
51
|
+
type: Object.values(SiteImsOrgAccess.DELEGATION_ROLES),
|
|
52
|
+
required: true,
|
|
53
|
+
})
|
|
54
|
+
.addAttribute('performedBy', {
|
|
55
|
+
type: 'string',
|
|
56
|
+
required: true,
|
|
57
|
+
validate: (v) => PERFORMED_BY_PATTERN.test(v),
|
|
58
|
+
})
|
|
59
|
+
.addAllIndex(['organizationId'])
|
|
60
|
+
.addAllIndex(['siteId']);
|
|
61
|
+
|
|
62
|
+
export default schema.build();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BaseCollection, BaseModel, EntitlementProductCode,
|
|
15
|
+
} from '../index';
|
|
16
|
+
|
|
17
|
+
export type AccessGrantAction = 'grant' | 'revoke';
|
|
18
|
+
export type AccessGrantRole = 'collaborator' | 'agency' | 'viewer';
|
|
19
|
+
|
|
20
|
+
export interface AccessGrantLog extends BaseModel {
|
|
21
|
+
getSiteId(): string;
|
|
22
|
+
getOrganizationId(): string;
|
|
23
|
+
getTargetOrganizationId(): string;
|
|
24
|
+
getProductCode(): EntitlementProductCode;
|
|
25
|
+
getAction(): AccessGrantAction;
|
|
26
|
+
getRole(): AccessGrantRole;
|
|
27
|
+
getPerformedBy(): string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AccessGrantLogCollection extends
|
|
31
|
+
BaseCollection<AccessGrantLog> {
|
|
32
|
+
allByOrganizationId(organizationId: string): Promise<AccessGrantLog[]>;
|
|
33
|
+
allBySiteId(siteId: string): Promise<AccessGrantLog[]>;
|
|
34
|
+
|
|
35
|
+
findByOrganizationId(organizationId: string): Promise<AccessGrantLog | null>;
|
|
36
|
+
findBySiteId(siteId: string): Promise<AccessGrantLog | null>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { default as AccessGrantLog } from './access-grant-log.model.js';
|
|
14
|
+
export { default as AccessGrantLogCollection } from './access-grant-log.collection.js';
|
|
15
|
+
export { default as AccessGrantLogSchema } from './access-grant-log.schema.js';
|
|
@@ -585,6 +585,20 @@ class BaseCollection {
|
|
|
585
585
|
return this.#queryByIndexKeys(keys, { ...options, limit: 1 });
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Converts a raw PostgREST row (snake_case) to a model instance using the same field mapping
|
|
590
|
+
* pipeline as the internal create/find paths. Intended for use by collections that receive
|
|
591
|
+
* embedded sub-rows from PostgREST resource embedding (e.g. `sites!fkey(*)`), allowing them
|
|
592
|
+
* to return proper model instances rather than raw snake_case objects.
|
|
593
|
+
*
|
|
594
|
+
* @param {object} row - Raw PostgREST row with snake_case column names.
|
|
595
|
+
* @returns {object|null} A model instance, or null if the row is empty/invalid.
|
|
596
|
+
*/
|
|
597
|
+
createInstanceFromRow(row) {
|
|
598
|
+
if (!isNonEmptyObject(row)) return null;
|
|
599
|
+
return this.#createInstance(this.#toModelRecord(row));
|
|
600
|
+
}
|
|
601
|
+
|
|
588
602
|
async findById(id) {
|
|
589
603
|
guardId(this.idName, id, this.entityName);
|
|
590
604
|
if (this.entity) {
|
|
@@ -176,6 +176,20 @@ class BaseModel {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
return null;
|
|
179
|
+
})
|
|
180
|
+
.catch((error) => {
|
|
181
|
+
// Gracefully skip dependents whose table/column is absent from the PostgREST
|
|
182
|
+
// schema cache (e.g. when the legacy test DB pre-dates this table). All other
|
|
183
|
+
// errors are re-thrown so they still surface as removal failures.
|
|
184
|
+
const msg = error?.cause?.message || error?.message || '';
|
|
185
|
+
if (
|
|
186
|
+
msg.includes('Could not find the')
|
|
187
|
+
&& (msg.includes('table') || msg.includes('column'))
|
|
188
|
+
&& msg.includes('schema cache')
|
|
189
|
+
) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
179
193
|
}),
|
|
180
194
|
);
|
|
181
195
|
});
|
|
@@ -48,6 +48,8 @@ import PageCitabilityCollection from '../page-citability/page-citability.collect
|
|
|
48
48
|
import PlgOnboardingCollection from '../plg-onboarding/plg-onboarding.collection.js';
|
|
49
49
|
import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guideline.collection.js';
|
|
50
50
|
import SentimentTopicCollection from '../sentiment-topic/sentiment-topic.collection.js';
|
|
51
|
+
import AccessGrantLogCollection from '../access-grant-log/access-grant-log.collection.js';
|
|
52
|
+
import SiteImsOrgAccessCollection from '../site-ims-org-access/site-ims-org-access.collection.js';
|
|
51
53
|
|
|
52
54
|
import ApiKeySchema from '../api-key/api-key.schema.js';
|
|
53
55
|
import AsyncJobSchema from '../async-job/async-job.schema.js';
|
|
@@ -83,6 +85,8 @@ import PageCitabilitySchema from '../page-citability/page-citability.schema.js';
|
|
|
83
85
|
import PlgOnboardingSchema from '../plg-onboarding/plg-onboarding.schema.js';
|
|
84
86
|
import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline.schema.js';
|
|
85
87
|
import SentimentTopicSchema from '../sentiment-topic/sentiment-topic.schema.js';
|
|
88
|
+
import AccessGrantLogSchema from '../access-grant-log/access-grant-log.schema.js';
|
|
89
|
+
import SiteImsOrgAccessSchema from '../site-ims-org-access/site-ims-org-access.schema.js';
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
92
|
* EntityRegistry - A registry class responsible for managing entities, their schema and collection.
|
|
@@ -213,6 +217,8 @@ EntityRegistry.registerEntity(PageCitabilitySchema, PageCitabilityCollection);
|
|
|
213
217
|
EntityRegistry.registerEntity(PlgOnboardingSchema, PlgOnboardingCollection);
|
|
214
218
|
EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollection);
|
|
215
219
|
EntityRegistry.registerEntity(SentimentTopicSchema, SentimentTopicCollection);
|
|
220
|
+
EntityRegistry.registerEntity(AccessGrantLogSchema, AccessGrantLogCollection);
|
|
221
|
+
EntityRegistry.registerEntity(SiteImsOrgAccessSchema, SiteImsOrgAccessCollection);
|
|
216
222
|
EntityRegistry.defaultEntities = { ...EntityRegistry.entities };
|
|
217
223
|
|
|
218
224
|
export default EntityRegistry;
|
package/src/models/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
export type * from './access-grant-log';
|
|
13
14
|
export type * from './api-key';
|
|
14
15
|
export type * from './async-job';
|
|
15
16
|
export type * from './audit';
|
|
@@ -39,6 +40,7 @@ export type * from './sentiment-topic';
|
|
|
39
40
|
export type * from './site';
|
|
40
41
|
export type * from './site-candidate';
|
|
41
42
|
export type * from './site-enrollment';
|
|
43
|
+
export type * from './site-ims-org-access';
|
|
42
44
|
export type * from './site-top-form';
|
|
43
45
|
export type * from './site-top-page';
|
|
44
46
|
export type * from './suggestion';
|
package/src/models/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
export * from './access-grant-log/index.js';
|
|
13
14
|
export * from './api-key/index.js';
|
|
14
15
|
export * from './async-job/index.js';
|
|
15
16
|
export * from './audit/index.js';
|
|
@@ -32,6 +33,7 @@ export * from './scrape-job/index.js';
|
|
|
32
33
|
export * from './scrape-url/index.js';
|
|
33
34
|
export * from './site-candidate/index.js';
|
|
34
35
|
export * from './site-enrollment/index.js';
|
|
36
|
+
export * from './site-ims-org-access/index.js';
|
|
35
37
|
export * from './site-top-form/index.js';
|
|
36
38
|
export * from './site-top-page/index.js';
|
|
37
39
|
export * from './site/index.js';
|
|
@@ -16,18 +16,10 @@ import type { BaseCollection, BaseModel, Site } from '../index';
|
|
|
16
16
|
* SentimentTopic entity representing a topic for sentiment analysis.
|
|
17
17
|
* Composite primary key: siteId (PK) + topicId (SK)
|
|
18
18
|
*/
|
|
19
|
-
export interface SentimentTopicUrl {
|
|
20
|
-
url: string;
|
|
21
|
-
timesCited: number;
|
|
22
|
-
category?: string;
|
|
23
|
-
subPrompts?: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
19
|
export interface SentimentTopic extends BaseModel {
|
|
27
20
|
getTopicId(): string;
|
|
28
21
|
getName(): string;
|
|
29
22
|
getDescription(): string | undefined;
|
|
30
|
-
getUrls(): SentimentTopicUrl[];
|
|
31
23
|
getEnabled(): boolean;
|
|
32
24
|
getCreatedAt(): string;
|
|
33
25
|
getCreatedBy(): string;
|
|
@@ -38,7 +30,6 @@ export interface SentimentTopic extends BaseModel {
|
|
|
38
30
|
|
|
39
31
|
setName(name: string): SentimentTopic;
|
|
40
32
|
setDescription(description: string): SentimentTopic;
|
|
41
|
-
setUrls(urls: SentimentTopicUrl[]): SentimentTopic;
|
|
42
33
|
setEnabled(enabled: boolean): SentimentTopic;
|
|
43
34
|
setUpdatedBy(updatedBy: string): SentimentTopic;
|
|
44
35
|
}
|
|
@@ -55,25 +55,6 @@ const schema = new SchemaBuilder(SentimentTopic, SentimentTopicCollection)
|
|
|
55
55
|
type: 'string',
|
|
56
56
|
required: false,
|
|
57
57
|
})
|
|
58
|
-
.addAttribute('urls', {
|
|
59
|
-
type: 'list',
|
|
60
|
-
required: false,
|
|
61
|
-
default: [],
|
|
62
|
-
items: {
|
|
63
|
-
type: 'map',
|
|
64
|
-
properties: {
|
|
65
|
-
url: { type: 'string', required: true },
|
|
66
|
-
timesCited: { type: 'number', required: true },
|
|
67
|
-
category: { type: 'string', required: false },
|
|
68
|
-
subPrompts: {
|
|
69
|
-
type: 'list',
|
|
70
|
-
items: { type: 'string' },
|
|
71
|
-
required: false,
|
|
72
|
-
default: [],
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
58
|
.addAttribute('enabled', {
|
|
78
59
|
type: 'boolean',
|
|
79
60
|
required: true,
|
|
@@ -43,6 +43,11 @@ const schema = new SchemaBuilder(Site, SiteCollection)
|
|
|
43
43
|
.addReference('has_many', 'Opportunities')
|
|
44
44
|
.addReference('has_many', 'SiteCandidates')
|
|
45
45
|
.addReference('has_many', 'SiteEnrollments')
|
|
46
|
+
// TODO(Phase 2 audit gap): removeDependents silently removes all SiteImsOrgAccess records on
|
|
47
|
+
// site deletion without writing AccessGrantLog 'revoke' entries. Anyone auditing "why did
|
|
48
|
+
// agency X lose access to site Y" will find nothing in the log. Add a model-level pre-remove
|
|
49
|
+
// hook that writes revoke audit entries before this ships to production.
|
|
50
|
+
.addReference('has_many', 'SiteImsOrgAccesses', [], { removeDependents: true })
|
|
46
51
|
.addReference('has_many', 'SiteTopForms')
|
|
47
52
|
.addReference('has_many', 'SiteTopPages')
|
|
48
53
|
.addReference('has_many', 'TrialUserActivities')
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BaseCollection, BaseModel, EntitlementProductCode, Organization, Site,
|
|
15
|
+
} from '../index';
|
|
16
|
+
|
|
17
|
+
export type SiteImsOrgAccessRole = 'collaborator' | 'agency' | 'viewer';
|
|
18
|
+
|
|
19
|
+
export interface SiteImsOrgAccess extends BaseModel {
|
|
20
|
+
getSite(): Promise<Site>;
|
|
21
|
+
getOrganization(): Promise<Organization>;
|
|
22
|
+
getSiteId(): string;
|
|
23
|
+
/** organizationId is the delegate org receiving access (read-only, part of grant identity). */
|
|
24
|
+
getOrganizationId(): string;
|
|
25
|
+
/** targetOrganizationId is the site-owning org (read-only, part of grant identity). */
|
|
26
|
+
getTargetOrganizationId(): string;
|
|
27
|
+
/** productCode is read-only; changing scope requires a new grant. */
|
|
28
|
+
getProductCode(): EntitlementProductCode;
|
|
29
|
+
getRole(): SiteImsOrgAccessRole;
|
|
30
|
+
getGrantedBy(): string | null;
|
|
31
|
+
getExpiresAt(): string | null;
|
|
32
|
+
setRole(role: SiteImsOrgAccessRole): SiteImsOrgAccess;
|
|
33
|
+
setGrantedBy(grantedBy: string): SiteImsOrgAccess;
|
|
34
|
+
setExpiresAt(expiresAt: string): SiteImsOrgAccess;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SiteImsOrgAccessGrantWithTarget {
|
|
38
|
+
grant: SiteImsOrgAccess;
|
|
39
|
+
targetOrganization: {
|
|
40
|
+
id: string;
|
|
41
|
+
imsOrgId: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SiteImsOrgAccessGrantWithSite {
|
|
46
|
+
grant: SiteImsOrgAccess;
|
|
47
|
+
/** Site model instance. Null only if the FK is broken (should not occur given ON DELETE CASCADE). */
|
|
48
|
+
site: Site | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SiteImsOrgAccessCollection extends
|
|
52
|
+
BaseCollection<SiteImsOrgAccess> {
|
|
53
|
+
allBySiteId(siteId: string): Promise<SiteImsOrgAccess[]>;
|
|
54
|
+
allByOrganizationId(organizationId: string): Promise<SiteImsOrgAccess[]>;
|
|
55
|
+
allByTargetOrganizationId(targetOrganizationId: string): Promise<SiteImsOrgAccess[]>;
|
|
56
|
+
allByOrganizationIdWithTargetOrganization(organizationId: string): Promise<SiteImsOrgAccessGrantWithTarget[]>;
|
|
57
|
+
allByOrganizationIdsWithTargetOrganization(organizationIds: string[]): Promise<SiteImsOrgAccessGrantWithTarget[]>;
|
|
58
|
+
allByOrganizationIdWithSites(organizationId: string): Promise<SiteImsOrgAccessGrantWithSite[]>;
|
|
59
|
+
|
|
60
|
+
findBySiteId(siteId: string): Promise<SiteImsOrgAccess | null>;
|
|
61
|
+
findByOrganizationId(organizationId: string): Promise<SiteImsOrgAccess | null>;
|
|
62
|
+
findByTargetOrganizationId(targetOrganizationId: string): Promise<SiteImsOrgAccess | null>;
|
|
63
|
+
findBySiteIdAndOrganizationIdAndProductCode(siteId: string, organizationId: string, productCode: EntitlementProductCode): Promise<SiteImsOrgAccess | null>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { default as SiteImsOrgAccess } from './site-ims-org-access.model.js';
|
|
14
|
+
export { default as SiteImsOrgAccessCollection } from './site-ims-org-access.collection.js';
|
|
15
|
+
export { default as SiteImsOrgAccessSchema } from './site-ims-org-access.schema.js';
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseCollection from '../base/base.collection.js';
|
|
14
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
15
|
+
import { DEFAULT_PAGE_SIZE } from '../../util/postgrest.utils.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* SiteImsOrgAccessCollection - Collection of cross-org delegation grants.
|
|
19
|
+
* Provides idempotent create and the 50-delegate-per-site limit.
|
|
20
|
+
*
|
|
21
|
+
* @class SiteImsOrgAccessCollection
|
|
22
|
+
* @extends BaseCollection
|
|
23
|
+
*/
|
|
24
|
+
class SiteImsOrgAccessCollection extends BaseCollection {
|
|
25
|
+
static COLLECTION_NAME = 'SiteImsOrgAccessCollection';
|
|
26
|
+
|
|
27
|
+
static MAX_DELEGATES_PER_SITE = 50;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Idempotent create: if a grant already exists for (siteId, organizationId, productCode),
|
|
31
|
+
* return the existing record. Otherwise, enforce the 50-delegate-per-site limit and create.
|
|
32
|
+
* Follows the SiteEnrollment pattern (site-enrollment.collection.js:25-32).
|
|
33
|
+
*
|
|
34
|
+
* Note: the findByIndexKeys + allBySiteId + super.create sequence is not atomic. Concurrent
|
|
35
|
+
* requests can both pass the idempotency check (creating duplicates) or both pass the limit
|
|
36
|
+
* check (exceeding it). A DB-level unique constraint on (siteId, organizationId, productCode)
|
|
37
|
+
* is the authoritative guard against duplicates.
|
|
38
|
+
*/
|
|
39
|
+
async create(item, options = {}) {
|
|
40
|
+
if (item?.organizationId && item?.targetOrganizationId
|
|
41
|
+
&& item.organizationId === item.targetOrganizationId) {
|
|
42
|
+
const message = 'Cannot create self-delegation: organizationId and targetOrganizationId must differ';
|
|
43
|
+
this.log.warn(`[SiteImsOrgAccess] Self-delegation rejected: org=${item.organizationId}`);
|
|
44
|
+
const err = new DataAccessError(message);
|
|
45
|
+
err.status = 409;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (item?.siteId && item?.organizationId && item?.productCode) {
|
|
50
|
+
const existing = await this.findByIndexKeys({
|
|
51
|
+
siteId: item.siteId,
|
|
52
|
+
organizationId: item.organizationId,
|
|
53
|
+
productCode: item.productCode,
|
|
54
|
+
});
|
|
55
|
+
if (existing) {
|
|
56
|
+
this.log.info(`[SiteImsOrgAccess] Idempotent create: returning existing grant for site=${item.siteId} org=${item.organizationId} product=${item.productCode}`);
|
|
57
|
+
return existing;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Enforce 50-active-delegate-per-site limit; expired grants do not count.
|
|
62
|
+
if (item?.siteId) {
|
|
63
|
+
const allGrants = await this.allBySiteId(item.siteId);
|
|
64
|
+
const activeGrants = allGrants.filter(
|
|
65
|
+
(g) => !g.getExpiresAt() || new Date(g.getExpiresAt()) > new Date(),
|
|
66
|
+
);
|
|
67
|
+
if (activeGrants.length >= SiteImsOrgAccessCollection.MAX_DELEGATES_PER_SITE) {
|
|
68
|
+
const message = `Cannot add delegate: site already has ${activeGrants.length}/${SiteImsOrgAccessCollection.MAX_DELEGATES_PER_SITE} active delegates`;
|
|
69
|
+
this.log.warn(`[SiteImsOrgAccess] Delegate limit reached for site=${item.siteId}`);
|
|
70
|
+
const err = new DataAccessError(message);
|
|
71
|
+
err.status = 409;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const created = await super.create(item, options);
|
|
77
|
+
this.log.info(`[SiteImsOrgAccess] New grant created: id=${created.getId()} site=${item.siteId} org=${item.organizationId} product=${item.productCode}`);
|
|
78
|
+
return created;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Shared pagination loop for PostgREST embedding queries. Fetches all pages and maps
|
|
83
|
+
* each row using the provided mapper function.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} query - PostgREST query builder (result of .from(...).select(...))
|
|
86
|
+
* @param {Function} mapRow - Maps a raw row to the desired return shape
|
|
87
|
+
* @param {string} errorMessage - Used for logging and DataAccessError message
|
|
88
|
+
* @returns {Promise<Array>}
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
async #fetchPaginatedGrants(query, mapRow, errorMessage) {
|
|
92
|
+
const allResults = [];
|
|
93
|
+
let offset = 0;
|
|
94
|
+
let keepGoing = true;
|
|
95
|
+
const orderedQuery = query.order('id');
|
|
96
|
+
|
|
97
|
+
while (keepGoing) {
|
|
98
|
+
// eslint-disable-next-line no-await-in-loop
|
|
99
|
+
const { data, error } = await orderedQuery.range(offset, offset + DEFAULT_PAGE_SIZE - 1);
|
|
100
|
+
|
|
101
|
+
if (error) {
|
|
102
|
+
this.log.error(`[SiteImsOrgAccess] ${errorMessage} - ${error.message}`, error);
|
|
103
|
+
throw new DataAccessError(
|
|
104
|
+
errorMessage,
|
|
105
|
+
{ entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' },
|
|
106
|
+
error,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!data || data.length === 0) {
|
|
111
|
+
keepGoing = false;
|
|
112
|
+
} else {
|
|
113
|
+
allResults.push(...data);
|
|
114
|
+
keepGoing = data.length >= DEFAULT_PAGE_SIZE;
|
|
115
|
+
offset += DEFAULT_PAGE_SIZE;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return allResults.map(mapRow);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {object} query - PostgREST query builder
|
|
124
|
+
* @returns {Promise<Array<{grant: SiteImsOrgAccess, targetOrganization: object}>>}
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
async #fetchGrantsWithTargetOrg(query) {
|
|
128
|
+
return this.#fetchPaginatedGrants(
|
|
129
|
+
query,
|
|
130
|
+
(row) => ({
|
|
131
|
+
grant: this.createInstanceFromRow(row),
|
|
132
|
+
targetOrganization: { id: row.organizations.id, imsOrgId: row.organizations.ims_org_id },
|
|
133
|
+
}),
|
|
134
|
+
'Failed to query grants with target organization',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns all grants for the given delegate organization with the target organization's
|
|
140
|
+
* id and imsOrgId embedded via PostgREST resource embedding (INNER JOIN). This avoids
|
|
141
|
+
* a separate batch query to resolve target org IMS identifiers.
|
|
142
|
+
*
|
|
143
|
+
* Returns plain objects, not model instances. Access properties directly
|
|
144
|
+
* (e.g., `entry.grant.productCode`, `entry.targetOrganization.imsOrgId`).
|
|
145
|
+
*
|
|
146
|
+
* @param {string} organizationId - UUID of the delegate organization.
|
|
147
|
+
* @returns {Promise<Array<{
|
|
148
|
+
* grant: {id: string, siteId: string, organizationId: string,
|
|
149
|
+
* targetOrganizationId: string, productCode: string, role: string,
|
|
150
|
+
* grantedBy: string|null, expiresAt: string|null},
|
|
151
|
+
* targetOrganization: {id: string, imsOrgId: string}
|
|
152
|
+
* }>>}
|
|
153
|
+
*/
|
|
154
|
+
async allByOrganizationIdWithTargetOrganization(organizationId) {
|
|
155
|
+
if (!organizationId) {
|
|
156
|
+
throw new DataAccessError('organizationId is required', { entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' });
|
|
157
|
+
}
|
|
158
|
+
// eslint-disable-next-line max-len
|
|
159
|
+
const select = 'id, site_id, organization_id, target_organization_id, product_code, role, granted_by, expires_at, organizations!site_ims_org_accesses_target_organization_id_fkey(id, ims_org_id)';
|
|
160
|
+
return this.#fetchGrantsWithTargetOrg(
|
|
161
|
+
this.postgrestService.from('site_ims_org_accesses').select(select).eq('organization_id', organizationId),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Bulk variant of allByOrganizationIdWithTargetOrganization. Fetches grants for multiple
|
|
167
|
+
* delegate organizations in a single PostgREST IN query with target org embedding.
|
|
168
|
+
* Returns an empty array when organizationIds is empty.
|
|
169
|
+
*
|
|
170
|
+
* @param {string[]} organizationIds - UUIDs of the delegate organizations.
|
|
171
|
+
* @returns {Promise<Array<{
|
|
172
|
+
* grant: {id: string, siteId: string, organizationId: string,
|
|
173
|
+
* targetOrganizationId: string, productCode: string, role: string,
|
|
174
|
+
* grantedBy: string|null, expiresAt: string|null},
|
|
175
|
+
* targetOrganization: {id: string, imsOrgId: string}
|
|
176
|
+
* }>>}
|
|
177
|
+
*/
|
|
178
|
+
async allByOrganizationIdsWithTargetOrganization(organizationIds) {
|
|
179
|
+
if (!organizationIds || organizationIds.length === 0) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
if (organizationIds.length > SiteImsOrgAccessCollection.MAX_DELEGATES_PER_SITE) {
|
|
183
|
+
throw new DataAccessError(
|
|
184
|
+
`allByOrganizationIdsWithTargetOrganization: organizationIds array exceeds maximum of ${SiteImsOrgAccessCollection.MAX_DELEGATES_PER_SITE}`,
|
|
185
|
+
{ entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// eslint-disable-next-line max-len
|
|
189
|
+
const select = 'id, site_id, organization_id, target_organization_id, product_code, role, granted_by, expires_at, organizations!site_ims_org_accesses_target_organization_id_fkey(id, ims_org_id)';
|
|
190
|
+
return this.#fetchGrantsWithTargetOrg(
|
|
191
|
+
this.postgrestService.from('site_ims_org_accesses').select(select).in('organization_id', organizationIds),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Finds a single grant by the compound key (siteId, organizationId, productCode).
|
|
197
|
+
* Used by hasAccess() in the api-service to verify a grant still exists (Path A revocation
|
|
198
|
+
* check) or to perform a direct DB lookup when the JWT list was truncated (Path B).
|
|
199
|
+
*
|
|
200
|
+
* Returns a model instance so callers can use getExpiresAt(), getRole(), etc.
|
|
201
|
+
* Returns null when no matching grant exists.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} siteId - UUID of the site.
|
|
204
|
+
* @param {string} organizationId - UUID of the delegate organization.
|
|
205
|
+
* @param {string} productCode - Product code (e.g. 'LLMO', 'ASO').
|
|
206
|
+
* @returns {Promise<SiteImsOrgAccess|null>}
|
|
207
|
+
*/
|
|
208
|
+
async findBySiteIdAndOrganizationIdAndProductCode(siteId, organizationId, productCode) {
|
|
209
|
+
if (!siteId || !organizationId || !productCode) {
|
|
210
|
+
throw new DataAccessError(
|
|
211
|
+
'siteId, organizationId and productCode are required',
|
|
212
|
+
{ entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return this.findByIndexKeys({ siteId, organizationId, productCode });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Returns all grants for the given delegate organization with the full site row embedded
|
|
220
|
+
* via PostgREST resource embedding (INNER JOIN on site_id FK). This is a single round-trip
|
|
221
|
+
* query — no N+1 — suitable for populating the site dropdown for delegated users.
|
|
222
|
+
*
|
|
223
|
+
* Returns plain objects, not model instances. The `site` field contains the raw PostgREST
|
|
224
|
+
* row for the joined site (snake_case column names). It is null only when the FK is broken,
|
|
225
|
+
* which should not occur given ON DELETE CASCADE on site_id.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} organizationId - UUID of the delegate organization.
|
|
228
|
+
* @returns {Promise<Array<{
|
|
229
|
+
* grant: {id: string, siteId: string, organizationId: string,
|
|
230
|
+
* targetOrganizationId: string, productCode: string, role: string,
|
|
231
|
+
* grantedBy: string|null, expiresAt: string|null},
|
|
232
|
+
* site: object|null
|
|
233
|
+
* }>>}
|
|
234
|
+
*/
|
|
235
|
+
async allByOrganizationIdWithSites(organizationId) {
|
|
236
|
+
if (!organizationId) {
|
|
237
|
+
throw new DataAccessError('organizationId is required', { entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' });
|
|
238
|
+
}
|
|
239
|
+
// eslint-disable-next-line max-len
|
|
240
|
+
const select = 'id, site_id, organization_id, target_organization_id, product_code, role, granted_by, expires_at, sites!site_ims_org_accesses_site_id_fkey(*)';
|
|
241
|
+
return this.#fetchGrantsWithSite(
|
|
242
|
+
this.postgrestService.from('site_ims_org_accesses').select(select).eq('organization_id', organizationId),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {object} query - PostgREST query builder
|
|
248
|
+
* @returns {Promise<Array<{grant: SiteImsOrgAccess, site: Site|null}>>}
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
async #fetchGrantsWithSite(query) {
|
|
252
|
+
const siteCollection = this.entityRegistry.getCollection('SiteCollection');
|
|
253
|
+
return this.#fetchPaginatedGrants(
|
|
254
|
+
query,
|
|
255
|
+
(row) => ({
|
|
256
|
+
grant: this.createInstanceFromRow(row),
|
|
257
|
+
site: row.sites ? siteCollection.createInstanceFromRow(row.sites) : null,
|
|
258
|
+
}),
|
|
259
|
+
'Failed to query grants with site',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default SiteImsOrgAccessCollection;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseModel from '../base/base.model.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SiteImsOrgAccess - Maps delegate orgs to sites they can access, per product.
|
|
17
|
+
* Part of the cross-org delegation feature (Option 2a Phase 1).
|
|
18
|
+
*
|
|
19
|
+
* @class SiteImsOrgAccess
|
|
20
|
+
* @extends BaseModel
|
|
21
|
+
*/
|
|
22
|
+
class SiteImsOrgAccess extends BaseModel {
|
|
23
|
+
static ENTITY_NAME = 'SiteImsOrgAccess';
|
|
24
|
+
|
|
25
|
+
static DELEGATION_ROLES = {
|
|
26
|
+
COLLABORATOR: 'collaborator',
|
|
27
|
+
AGENCY: 'agency',
|
|
28
|
+
VIEWER: 'viewer',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default SiteImsOrgAccess;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isIsoDate, isValidUUID } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import { Entitlement } from '../entitlement/index.js';
|
|
15
|
+
import SchemaBuilder from '../base/schema.builder.js';
|
|
16
|
+
import SiteImsOrgAccess from './site-ims-org-access.model.js';
|
|
17
|
+
import SiteImsOrgAccessCollection from './site-ims-org-access.collection.js';
|
|
18
|
+
|
|
19
|
+
const GRANTED_BY_PATTERN = /^(ims:\S+|slack:\S+|system)$/;
|
|
20
|
+
|
|
21
|
+
const schema = new SchemaBuilder(SiteImsOrgAccess, SiteImsOrgAccessCollection)
|
|
22
|
+
.addReference('belongs_to', 'Site')
|
|
23
|
+
.addReference('belongs_to', 'Organization') // the agency/delegate org receiving access
|
|
24
|
+
// targetOrganizationId is addAttribute (not belongs_to) because SchemaBuilder's
|
|
25
|
+
// belongs_to uses the referenced model name as the FK column prefix (organization_id).
|
|
26
|
+
// We already have belongs_to Organization for the delegate org, so a second belongs_to
|
|
27
|
+
// would conflict. addAttribute with UUID validation achieves the same FK semantics
|
|
28
|
+
// without the naming collision. readOnly: true prevents the setter from being generated
|
|
29
|
+
// since the target org is part of the grant's identity — a different target means a
|
|
30
|
+
// different grant.
|
|
31
|
+
.addAttribute('targetOrganizationId', {
|
|
32
|
+
type: 'string',
|
|
33
|
+
required: true,
|
|
34
|
+
readOnly: true,
|
|
35
|
+
validate: (v) => isValidUUID(v),
|
|
36
|
+
})
|
|
37
|
+
// organizationId (delegate org, from belongs_to) and siteId are also part of the grant's
|
|
38
|
+
// identity and are readOnly by virtue of their belongs_to FK nature.
|
|
39
|
+
.addAttribute('productCode', {
|
|
40
|
+
type: Object.values(Entitlement.PRODUCT_CODES),
|
|
41
|
+
required: true,
|
|
42
|
+
readOnly: true,
|
|
43
|
+
})
|
|
44
|
+
.addAttribute('role', {
|
|
45
|
+
type: Object.values(SiteImsOrgAccess.DELEGATION_ROLES),
|
|
46
|
+
required: true,
|
|
47
|
+
default: SiteImsOrgAccess.DELEGATION_ROLES.AGENCY,
|
|
48
|
+
})
|
|
49
|
+
.addAttribute('grantedBy', {
|
|
50
|
+
type: 'string',
|
|
51
|
+
required: false,
|
|
52
|
+
validate: (v) => !v || GRANTED_BY_PATTERN.test(v),
|
|
53
|
+
})
|
|
54
|
+
.addAttribute('expiresAt', {
|
|
55
|
+
type: 'string',
|
|
56
|
+
required: false,
|
|
57
|
+
validate: (v) => !v || isIsoDate(v),
|
|
58
|
+
})
|
|
59
|
+
.addAllIndex(['organizationId'])
|
|
60
|
+
// Index for "show all delegations granted to org Z across all sites" admin query
|
|
61
|
+
.addAllIndex(['targetOrganizationId']);
|
|
62
|
+
|
|
63
|
+
export default schema.build();
|