@platform-mesh/portal-server-lib 0.9.95 → 0.9.97

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.
Files changed (56) hide show
  1. package/dist/portal-options/index.d.ts +4 -0
  2. package/dist/portal-options/index.js +4 -0
  3. package/dist/portal-options/index.js.map +1 -1
  4. package/dist/portal-options/logout-callback.service.js +16 -8
  5. package/dist/portal-options/logout-callback.service.js.map +1 -1
  6. package/dist/portal-options/service-providers/content-configuration-service-providers.service.d.ts +4 -1
  7. package/dist/portal-options/service-providers/content-configuration-service-providers.service.js +14 -3
  8. package/dist/portal-options/service-providers/content-configuration-service-providers.service.js.map +1 -1
  9. package/dist/portal-options/service-providers/kubernetes-service-providers.service.d.ts +4 -2
  10. package/dist/portal-options/service-providers/kubernetes-service-providers.service.js +10 -4
  11. package/dist/portal-options/service-providers/kubernetes-service-providers.service.js.map +1 -1
  12. package/dist/portal-options/services/kcp-k8s.service.d.ts +0 -1
  13. package/dist/portal-options/services/kcp-k8s.service.js +4 -13
  14. package/dist/portal-options/services/kcp-k8s.service.js.map +1 -1
  15. package/dist/portal-options/services/permissions/adapters/authz-webhook.service.d.ts +14 -0
  16. package/dist/portal-options/services/permissions/adapters/authz-webhook.service.js +183 -0
  17. package/dist/portal-options/services/permissions/adapters/authz-webhook.service.js.map +1 -0
  18. package/dist/portal-options/services/permissions/models/permissions.model.d.ts +51 -0
  19. package/dist/portal-options/services/permissions/models/permissions.model.js +2 -0
  20. package/dist/portal-options/services/permissions/models/permissions.model.js.map +1 -0
  21. package/dist/portal-options/services/permissions/permissions-proxy.service.d.ts +9 -0
  22. package/dist/portal-options/services/permissions/permissions-proxy.service.js +48 -0
  23. package/dist/portal-options/services/permissions/permissions-proxy.service.js.map +1 -0
  24. package/dist/portal-options/services/permissions/permissions.controller.d.ts +9 -0
  25. package/dist/portal-options/services/permissions/permissions.controller.js +49 -0
  26. package/dist/portal-options/services/permissions/permissions.controller.js.map +1 -0
  27. package/dist/portal-options/utils/account-hierarchy-resolver.js +7 -3
  28. package/dist/portal-options/utils/account-hierarchy-resolver.js.map +1 -1
  29. package/dist/portal-options/utils/build-workspace-path.util.d.ts +1 -0
  30. package/dist/portal-options/utils/build-workspace-path.util.js +4 -0
  31. package/dist/portal-options/utils/build-workspace-path.util.js.map +1 -0
  32. package/dist/portal-options/utils/extract-resource-definitions.util.d.ts +7 -0
  33. package/dist/portal-options/utils/extract-resource-definitions.util.js +33 -0
  34. package/dist/portal-options/utils/extract-resource-definitions.util.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/portal-options/index.ts +4 -0
  37. package/src/portal-options/logout-callback.service.spec.ts +57 -1
  38. package/src/portal-options/logout-callback.service.ts +34 -8
  39. package/src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts +8 -1
  40. package/src/portal-options/service-providers/content-configuration-service-providers.service.ts +17 -6
  41. package/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts +13 -4
  42. package/src/portal-options/service-providers/kubernetes-service-providers.service.ts +21 -8
  43. package/src/portal-options/services/kcp-k8s.service.ts +4 -15
  44. package/src/portal-options/services/permissions/adapters/authz-webhook.service.spec.ts +500 -0
  45. package/src/portal-options/services/permissions/adapters/authz-webhook.service.ts +238 -0
  46. package/src/portal-options/services/permissions/models/permissions.model.ts +56 -0
  47. package/src/portal-options/services/permissions/permissions-proxy.service.spec.ts +232 -0
  48. package/src/portal-options/services/permissions/permissions-proxy.service.ts +58 -0
  49. package/src/portal-options/services/permissions/permissions.controller.spec.ts +143 -0
  50. package/src/portal-options/services/permissions/permissions.controller.ts +31 -0
  51. package/src/portal-options/utils/account-hierarchy-resolver.ts +13 -8
  52. package/src/portal-options/utils/build-workspace-path.util.spec.ts +59 -0
  53. package/src/portal-options/utils/build-workspace-path.util.ts +21 -0
  54. package/src/portal-options/utils/extract-resource-definitions.util.spec.ts +303 -0
  55. package/src/portal-options/utils/extract-resource-definitions.util.ts +62 -0
  56. package/tsconfig.json +2 -1
@@ -0,0 +1,238 @@
1
+ import { HttpService } from '@nestjs/axios';
2
+ import { Injectable, Logger } from '@nestjs/common';
3
+ import { randomUUID } from 'crypto';
4
+ import { firstValueFrom } from 'rxjs';
5
+ import { buildWorkspacePath } from '../../../utils/build-workspace-path.util.js';
6
+ import { AuthorizationRequest, IAuthzService, Permission, SubjectAccessReview } from '../models/permissions.model.js';
7
+
8
+ // Server default for the cluster-path extra key (flag --webhook-cluster-path-key,
9
+ // see rebac-authz-webhook pkg/config/config.go).
10
+ const CLUSTER_PATH_KEY = 'authorization.kubernetes.io/cluster-path';
11
+
12
+ interface BatchAuthzResult {
13
+ id: string;
14
+ allowed: boolean;
15
+ }
16
+
17
+ @Injectable()
18
+ export class AuthzWebhookService implements IAuthzService {
19
+ private readonly logger = new Logger(AuthzWebhookService.name);
20
+ private readonly webhookUrl: string | undefined;
21
+
22
+ constructor(private readonly httpService: HttpService) {
23
+ this.webhookUrl = process.env['OPENMFP_PORTAL_CONTEXT_AUTHZ_WEBHOOK_URL'];
24
+ if (!this.webhookUrl) {
25
+ this.logger.warn(
26
+ 'OPENMFP_PORTAL_CONTEXT_AUTHZ_WEBHOOK_URL not set — permissions checks are disabled (fail-open)',
27
+ );
28
+ }
29
+ }
30
+
31
+ // Tier 1: batch check for all resource types at startup
32
+ async checkActionsForResource(
33
+ req: AuthorizationRequest,
34
+ ): Promise<Permission[] | undefined> {
35
+ if (!this.webhookUrl) {
36
+ return undefined;
37
+ }
38
+
39
+ const user = this.extractUserEmail(req.token);
40
+ const clusterPath = buildWorkspacePath([req.organization, req.accountPath]);
41
+
42
+ const items: SubjectAccessReview[] = [];
43
+ const correlationMap = new Map<string, { resource: string; verb: string }>();
44
+
45
+ for (const check of req.checks) {
46
+ for (const verb of check.actions) {
47
+ const id = randomUUID();
48
+ correlationMap.set(id, { resource: check.resource, verb });
49
+ items.push({
50
+ apiVersion: 'authorization.k8s.io/v1',
51
+ kind: 'SubjectAccessReview',
52
+ metadata: { name: id },
53
+ spec: {
54
+ user,
55
+ extra: { [CLUSTER_PATH_KEY]: [clusterPath] },
56
+ resourceAttributes: {
57
+ verb,
58
+ group: check.group,
59
+ resource: check.resource.toLowerCase(),
60
+ },
61
+ },
62
+ });
63
+ }
64
+ }
65
+
66
+ if (!items.length) {
67
+ return undefined;
68
+ }
69
+
70
+ const results = await this.sendBatch(items);
71
+ if (!results) {
72
+ return undefined;
73
+ }
74
+
75
+ const permissions = this.mapToPermissions(results, correlationMap);
76
+ this.logger.log(
77
+ `permissions resolved for "${user}" "${clusterPath}": ${JSON.stringify(permissions)}`,
78
+ );
79
+ return permissions;
80
+ }
81
+
82
+ // Tier 2: check specific verbs on concrete resource instances
83
+ async checkActionsForInstance(
84
+ req: AuthorizationRequest,
85
+ ): Promise<Permission[] | undefined> {
86
+ if (!this.webhookUrl) {
87
+ return undefined;
88
+ }
89
+
90
+ const user = this.extractUserEmail(req.token);
91
+ const clusterPath = buildWorkspacePath([req.organization, req.accountPath]);
92
+
93
+ const items: SubjectAccessReview[] = [];
94
+ const correlationMap = new Map<
95
+ string,
96
+ { resource: string; namespace?: string; name?: string; verb: string }
97
+ >();
98
+
99
+ for (const check of req.checks) {
100
+ for (const verb of check.actions) {
101
+ const id = randomUUID();
102
+ correlationMap.set(id, {
103
+ resource: check.resource,
104
+ ...(check.namespace ? { namespace: check.namespace } : {}),
105
+ ...(check.name ? { name: check.name } : {}),
106
+ verb,
107
+ });
108
+ items.push({
109
+ apiVersion: 'authorization.k8s.io/v1',
110
+ kind: 'SubjectAccessReview',
111
+ metadata: { name: id },
112
+ spec: {
113
+ user,
114
+ extra: { [CLUSTER_PATH_KEY]: [clusterPath] },
115
+ resourceAttributes: {
116
+ verb,
117
+ group: check.group,
118
+ resource: check.resource.toLowerCase(),
119
+ // Namespaced when a namespace is present; cluster-scoped otherwise.
120
+ namespace: check.namespace,
121
+ name: check.name,
122
+ },
123
+ },
124
+ });
125
+ }
126
+ }
127
+
128
+ if (!items.length) {
129
+ return undefined;
130
+ }
131
+
132
+ const results = await this.sendBatch(items);
133
+ if (!results) {
134
+ return undefined;
135
+ }
136
+
137
+ return this.mapToInstancePermissions(results, correlationMap);
138
+ }
139
+
140
+ private extractUserEmail(token: string): string {
141
+ try {
142
+ const payload = JSON.parse(
143
+ Buffer.from(token.split('.')[1], 'base64url').toString(),
144
+ );
145
+ return payload.email ?? '';
146
+ } catch {
147
+ return '';
148
+ }
149
+ }
150
+
151
+ private async sendBatch(
152
+ items: SubjectAccessReview[],
153
+ ): Promise<Record<string, boolean> | undefined> {
154
+ try {
155
+ const response = await firstValueFrom(
156
+ this.httpService.post<BatchAuthzResult[]>(
157
+ `${this.webhookUrl}/batch-authz `,
158
+ items,
159
+ {
160
+ headers: { 'Content-Type': 'application/json' },
161
+ },
162
+ ),
163
+ );
164
+
165
+ const results = Array.isArray(response.data) ? response.data : [];
166
+ return results.reduce<Record<string, boolean>>((acc, result) => {
167
+ acc[result.id] = result.allowed;
168
+ return acc;
169
+ }, {});
170
+ } catch (err) {
171
+ this.logger.error('authz-webhook batch-authz failed — failing open', err);
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ private mapToPermissions(
177
+ results: Record<string, boolean>,
178
+ correlationMap: Map<string, { resource: string; verb: string }>,
179
+ ): Permission[] {
180
+ const permissionsMap = new Map<string, string[]>();
181
+
182
+ for (const [id, allowed] of Object.entries(results)) {
183
+ if (!allowed) {
184
+ continue;
185
+ }
186
+
187
+ const entry = correlationMap.get(id);
188
+ if (!entry) {
189
+ continue;
190
+ }
191
+
192
+ const actions = permissionsMap.get(entry.resource) ?? [];
193
+ actions.push(entry.verb);
194
+ permissionsMap.set(entry.resource, actions);
195
+ }
196
+
197
+ return Array.from(permissionsMap.entries()).map(([resource, actions]) => ({
198
+ resource,
199
+ actions,
200
+ }));
201
+ }
202
+
203
+ private mapToInstancePermissions(
204
+ results: Record<string, boolean>,
205
+ correlationMap: Map<
206
+ string,
207
+ { resource: string; namespace?: string; name?: string; verb: string }
208
+ >,
209
+ ): Permission[] {
210
+ const permissionsMap = new Map<
211
+ string,
212
+ { resource: string; namespace?: string; name?: string; actions: string[] }
213
+ >();
214
+
215
+ for (const [id, allowed] of Object.entries(results)) {
216
+ if (!allowed) {
217
+ continue;
218
+ }
219
+
220
+ const entry = correlationMap.get(id);
221
+ if (!entry) {
222
+ continue;
223
+ }
224
+
225
+ const key = `${entry.resource}|${entry.namespace ?? ''}|${entry.name ?? ''}`;
226
+ const permission = permissionsMap.get(key) ?? {
227
+ resource: entry.resource,
228
+ ...(entry.namespace ? { namespace: entry.namespace } : {}),
229
+ ...(entry.name ? { name: entry.name } : {}),
230
+ actions: [],
231
+ };
232
+ permission.actions.push(entry.verb);
233
+ permissionsMap.set(key, permission);
234
+ }
235
+
236
+ return Array.from(permissionsMap.values());
237
+ }
238
+ }
@@ -0,0 +1,56 @@
1
+ export interface PermissionsDefinition {
2
+ group: string;
3
+ resource: string;
4
+ entityActions: string[];
5
+ resourceActions: string[];
6
+ entityContextKey: string;
7
+ }
8
+
9
+ export interface AuthorizationRequest {
10
+ token: string;
11
+ organization: string;
12
+ accountPath: string;
13
+ checks: {
14
+ resource: string;
15
+ group: string;
16
+ namespace?: string | null;
17
+ name?: string;
18
+ actions: string[];
19
+ }[];
20
+ }
21
+
22
+ export interface Permission {
23
+ resource: string;
24
+ namespace?: string;
25
+ name?: string;
26
+ actions: string[];
27
+ }
28
+
29
+ export interface AuthorizationResponse {
30
+ accountPath: string;
31
+ permissions: Permission[];
32
+ }
33
+
34
+ export interface SubjectAccessReview {
35
+ apiVersion?: string;
36
+ kind?: string;
37
+ metadata: { name: string };
38
+ spec: {
39
+ user: string;
40
+ extra: Record<string, string[]>; // cluster path lives under the cluster-path key
41
+ resourceAttributes: {
42
+ verb: string;
43
+ group: string;
44
+ resource: string; // plural resource name, lowercase
45
+ namespace?: string | null;
46
+ name?: string | null;
47
+ };
48
+ };
49
+ }
50
+
51
+ export interface IAuthzService {
52
+ checkActionsForResource(req: AuthorizationRequest): Promise<Permission[] | undefined>;
53
+ checkActionsForInstance(
54
+ req: AuthorizationRequest,
55
+ ): Promise<Permission[] | undefined>;
56
+ }
@@ -0,0 +1,232 @@
1
+ import { ContentConfiguration } from '@openmfp/portal-server-lib';
2
+ import { mock } from 'jest-mock-extended';
3
+ import { AuthzWebhookService } from './adapters/authz-webhook.service.js';
4
+ import {
5
+ AuthorizationRequest,
6
+ Permission,
7
+ PermissionsDefinition,
8
+ } from './models/permissions.model.js';
9
+ import { PermissionsProxyService } from './permissions-proxy.service.js';
10
+
11
+ function makeCC(
12
+ nodes: ContentConfiguration['luigiConfigFragment']['data']['nodes'] = [],
13
+ ): ContentConfiguration {
14
+ return {
15
+ name: 'test',
16
+ creationTimestamp: '',
17
+ luigiConfigFragment: { data: { nodes } },
18
+ };
19
+ }
20
+
21
+ function makePd(
22
+ overrides: Partial<PermissionsDefinition> = {},
23
+ ): PermissionsDefinition {
24
+ return {
25
+ group: 'core.platform-mesh.io',
26
+ resource: 'Accounts',
27
+ entityActions: ['get', 'delete'],
28
+ resourceActions: ['create', 'list'],
29
+ entityContextKey: 'entityName',
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function makeNode(rd: Record<string, unknown>) {
35
+ return { context: { resourceDefinition: rd } };
36
+ }
37
+
38
+ const CC_WITH_CHECK = makeCC([
39
+ makeNode({ entity: 'Account', permissionsDefinition: makePd() }),
40
+ ]);
41
+
42
+ const CC_WITHOUT_PD = makeCC([makeNode({ entity: 'NoCheck' })]);
43
+
44
+ const PERMISSIONS: Permission[] = [
45
+ { resource: 'Accounts', actions: ['create', 'list'] },
46
+ ];
47
+
48
+ function makeAuthzRequest(
49
+ overrides: Partial<AuthorizationRequest> = {},
50
+ ): AuthorizationRequest {
51
+ return {
52
+ token: 'my-token',
53
+ organization: 'my-org',
54
+ accountPath: 'sub-path',
55
+ checks: [
56
+ {
57
+ resource: 'HttpBins',
58
+ group: 'example.com',
59
+ namespace: 'default',
60
+ name: 'my-bin',
61
+ actions: ['get', 'list'],
62
+ },
63
+ ],
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ describe('PermissionsProxyService', () => {
69
+ let authzWebhook: jest.Mocked<AuthzWebhookService>;
70
+ let svc: PermissionsProxyService;
71
+
72
+ beforeEach(() => {
73
+ authzWebhook = mock<AuthzWebhookService>();
74
+ svc = new PermissionsProxyService(authzWebhook);
75
+ });
76
+
77
+ describe('resolvePermissions', () => {
78
+ it('returns undefined when no resource definitions have a permissionsDefinition', async () => {
79
+ const result = await svc.resolvePermissions('tok', 'org', '', [
80
+ CC_WITHOUT_PD,
81
+ ]);
82
+ expect(result).toBeUndefined();
83
+ expect(authzWebhook.checkActionsForResource).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('returns undefined when content configurations are empty', async () => {
87
+ const result = await svc.resolvePermissions('tok', 'org', '', []);
88
+ expect(result).toBeUndefined();
89
+ });
90
+
91
+ it('returns undefined when permissionsDefinition has empty resourceActions', async () => {
92
+ const cc = makeCC([
93
+ makeNode({
94
+ entity: 'Account',
95
+ permissionsDefinition: makePd({ resourceActions: [] }),
96
+ }),
97
+ ]);
98
+
99
+ const result = await svc.resolvePermissions('tok', 'org', '', [cc]);
100
+
101
+ expect(result).toBeUndefined();
102
+ expect(authzWebhook.checkActionsForResource).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it('delegates to authzWebhook.checkActionsForResource with a check built from permissionsDefinition', async () => {
106
+ authzWebhook.checkActionsForResource.mockResolvedValue(PERMISSIONS);
107
+
108
+ const result = await svc.resolvePermissions(
109
+ 'my-token',
110
+ 'my-org',
111
+ 'sub-path',
112
+ [CC_WITH_CHECK],
113
+ );
114
+
115
+ expect(result).toEqual(PERMISSIONS);
116
+ expect(authzWebhook.checkActionsForResource).toHaveBeenCalledWith({
117
+ token: 'my-token',
118
+ organization: 'my-org',
119
+ accountPath: 'sub-path',
120
+ checks: [
121
+ {
122
+ resource: 'Accounts',
123
+ group: 'core.platform-mesh.io',
124
+ actions: ['create', 'list'],
125
+ },
126
+ ],
127
+ });
128
+ });
129
+
130
+ it('passes token, organization, accountPath to webhook', async () => {
131
+ authzWebhook.checkActionsForResource.mockResolvedValue([]);
132
+
133
+ await svc.resolvePermissions('my-token', 'my-org', 'sub', [
134
+ CC_WITH_CHECK,
135
+ ]);
136
+
137
+ const req = authzWebhook.checkActionsForResource.mock.calls[0][0];
138
+ expect(req.token).toBe('my-token');
139
+ expect(req.organization).toBe('my-org');
140
+ expect(req.accountPath).toBe('sub');
141
+ });
142
+
143
+ it('returns undefined when webhook returns undefined', async () => {
144
+ authzWebhook.checkActionsForResource.mockResolvedValue(undefined);
145
+
146
+ const result = await svc.resolvePermissions('tok', 'org', '', [
147
+ CC_WITH_CHECK,
148
+ ]);
149
+
150
+ expect(result).toBeUndefined();
151
+ });
152
+
153
+ it('filters out resource definitions whose permissionsDefinition has no resourceActions', async () => {
154
+ authzWebhook.checkActionsForResource.mockResolvedValue([]);
155
+ const mixed = makeCC([
156
+ makeNode({
157
+ entity: 'NoActions',
158
+ permissionsDefinition: makePd({
159
+ resource: 'NoActions',
160
+ resourceActions: [],
161
+ }),
162
+ }),
163
+ makeNode({
164
+ entity: 'WithCheck',
165
+ permissionsDefinition: makePd({
166
+ resource: 'WithChecks',
167
+ resourceActions: ['get'],
168
+ }),
169
+ }),
170
+ ]);
171
+
172
+ await svc.resolvePermissions('tok', 'org', '', [mixed]);
173
+
174
+ const req = authzWebhook.checkActionsForResource.mock.calls[0][0];
175
+ expect(req.checks).toHaveLength(1);
176
+ expect(req.checks[0].resource).toBe('WithChecks');
177
+ expect(req.checks[0].actions).toEqual(['get']);
178
+ });
179
+ });
180
+
181
+ describe('checkResourceInstance', () => {
182
+ it('delegates AuthorizationRequest directly to authzWebhook.checkActionsForInstance', async () => {
183
+ const mockResponse: Permission[] = [
184
+ {
185
+ resource: 'HttpBins',
186
+ namespace: 'default',
187
+ name: 'my-bin',
188
+ actions: ['get'],
189
+ },
190
+ ];
191
+ authzWebhook.checkActionsForInstance.mockResolvedValue(mockResponse);
192
+
193
+ const req = makeAuthzRequest();
194
+ const result = await svc.checkResourceInstance(req);
195
+
196
+ expect(result).toEqual(mockResponse);
197
+ expect(authzWebhook.checkActionsForInstance).toHaveBeenCalledWith(req);
198
+ });
199
+
200
+ it('returns undefined when webhook returns undefined', async () => {
201
+ authzWebhook.checkActionsForInstance.mockResolvedValue(undefined);
202
+
203
+ const result = await svc.checkResourceInstance(makeAuthzRequest());
204
+
205
+ expect(result).toBeUndefined();
206
+ });
207
+
208
+ it('returns empty array when webhook returns empty array', async () => {
209
+ authzWebhook.checkActionsForInstance.mockResolvedValue([]);
210
+
211
+ const result = await svc.checkResourceInstance(makeAuthzRequest());
212
+
213
+ expect(result).toEqual([]);
214
+ });
215
+
216
+ it('returns Permission[] when webhook returns results', async () => {
217
+ const permissions: Permission[] = [
218
+ {
219
+ resource: 'HttpBins',
220
+ namespace: 'default',
221
+ name: 'my-bin',
222
+ actions: ['get', 'list'],
223
+ },
224
+ ];
225
+ authzWebhook.checkActionsForInstance.mockResolvedValue(permissions);
226
+
227
+ const result = await svc.checkResourceInstance(makeAuthzRequest());
228
+
229
+ expect(result).toEqual(permissions);
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,58 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ContentConfiguration } from '@openmfp/portal-server-lib';
3
+ import { extractResourceDefinitions } from '../../utils/extract-resource-definitions.util.js';
4
+ import { AuthzWebhookService } from './adapters/authz-webhook.service.js';
5
+ import {
6
+ AuthorizationRequest,
7
+ Permission,
8
+ PermissionsDefinition,
9
+ } from './models/permissions.model.js';
10
+
11
+ @Injectable()
12
+ export class PermissionsProxyService {
13
+ constructor(private readonly authzWebhook: AuthzWebhookService) {}
14
+
15
+ // Tier 1 — called by KubernetesServiceProvidersService / ContentConfigurationServiceProvidersService
16
+ async resolvePermissions(
17
+ token: string,
18
+ organization: string,
19
+ accountPath: string,
20
+ contentConfigurations: ContentConfiguration[],
21
+ ): Promise<Permission[] | undefined> {
22
+ const resourceDefinitions = extractResourceDefinitions(
23
+ contentConfigurations,
24
+ );
25
+
26
+ const checks = resourceDefinitions
27
+ .map((rd) => rd.permissionsDefinition)
28
+ .filter(
29
+ (pd): pd is PermissionsDefinition =>
30
+ !!pd && pd.resourceActions.length > 0,
31
+ )
32
+ .map((pd) => ({
33
+ resource: pd.resource,
34
+ group: pd.group,
35
+ actions: pd.resourceActions,
36
+ }));
37
+
38
+ if (!checks.length) {
39
+ return undefined;
40
+ }
41
+
42
+ const req: AuthorizationRequest = {
43
+ token,
44
+ organization,
45
+ accountPath,
46
+ checks,
47
+ };
48
+
49
+ return this.authzWebhook.checkActionsForResource(req);
50
+ }
51
+
52
+ // Tier 2 — called by PermissionsController
53
+ async checkResourceInstance(
54
+ req: AuthorizationRequest,
55
+ ): Promise<Permission[] | undefined> {
56
+ return this.authzWebhook.checkActionsForInstance(req);
57
+ }
58
+ }