@mondaydotcomorg/monday-authorization 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-63c65ad → 3.3.1-fix-use-standard-env-var-for-metric-server-host-7ed2241

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 (100) hide show
  1. package/README.md +36 -10
  2. package/dist/attributions-service.d.ts +3 -2
  3. package/dist/attributions-service.d.ts.map +1 -1
  4. package/dist/attributions-service.js +1 -0
  5. package/dist/authorization-internal-service.d.ts +1 -1
  6. package/dist/authorization-internal-service.d.ts.map +1 -1
  7. package/dist/authorization-service.d.ts +5 -0
  8. package/dist/authorization-service.d.ts.map +1 -1
  9. package/dist/authorization-service.js +30 -26
  10. package/dist/clients/graph-api.d.ts +28 -0
  11. package/dist/clients/graph-api.d.ts.map +1 -0
  12. package/dist/clients/{graph-api.client.js → graph-api.js} +48 -40
  13. package/dist/clients/platform-api.d.ts +26 -0
  14. package/dist/clients/platform-api.d.ts.map +1 -0
  15. package/dist/clients/{platform-api.client.js → platform-api.js} +20 -20
  16. package/dist/constants.d.ts +1 -0
  17. package/dist/constants.d.ts.map +1 -1
  18. package/dist/constants.js +2 -0
  19. package/dist/esm/attributions-service.d.ts +3 -2
  20. package/dist/esm/attributions-service.d.ts.map +1 -1
  21. package/dist/esm/attributions-service.mjs +1 -0
  22. package/dist/esm/authorization-internal-service.d.ts +1 -1
  23. package/dist/esm/authorization-internal-service.d.ts.map +1 -1
  24. package/dist/esm/authorization-service.d.ts +5 -0
  25. package/dist/esm/authorization-service.d.ts.map +1 -1
  26. package/dist/esm/authorization-service.mjs +31 -27
  27. package/dist/esm/clients/graph-api.d.ts +28 -0
  28. package/dist/esm/clients/graph-api.d.ts.map +1 -0
  29. package/dist/esm/clients/{graph-api.client.mjs → graph-api.mjs} +48 -40
  30. package/dist/esm/clients/platform-api.d.ts +26 -0
  31. package/dist/esm/clients/platform-api.d.ts.map +1 -0
  32. package/dist/esm/clients/{platform-api.client.mjs → platform-api.mjs} +21 -21
  33. package/dist/esm/constants.d.ts +1 -0
  34. package/dist/esm/constants.d.ts.map +1 -1
  35. package/dist/esm/constants.mjs +2 -1
  36. package/dist/esm/index.d.ts +6 -0
  37. package/dist/esm/index.d.ts.map +1 -1
  38. package/dist/esm/index.mjs +8 -0
  39. package/dist/esm/metrics-service.d.ts +12 -0
  40. package/dist/esm/metrics-service.d.ts.map +1 -0
  41. package/dist/esm/metrics-service.mjs +54 -0
  42. package/dist/esm/prometheus-service.d.ts +1 -3
  43. package/dist/esm/prometheus-service.d.ts.map +1 -1
  44. package/dist/esm/prometheus-service.mjs +5 -58
  45. package/dist/esm/types/graph-api.types.d.ts +8 -7
  46. package/dist/esm/types/graph-api.types.d.ts.map +1 -1
  47. package/dist/esm/types/scoped-actions-contracts.d.ts +10 -1
  48. package/dist/esm/types/scoped-actions-contracts.d.ts.map +1 -1
  49. package/dist/esm/types/scoped-actions-contracts.mjs +9 -0
  50. package/dist/esm/utils/api-error-handler.d.ts +2 -0
  51. package/dist/esm/utils/api-error-handler.d.ts.map +1 -0
  52. package/dist/esm/utils/api-error-handler.mjs +18 -0
  53. package/dist/index.d.ts +6 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +8 -0
  56. package/dist/metrics-service.d.ts +12 -0
  57. package/dist/metrics-service.d.ts.map +1 -0
  58. package/dist/metrics-service.js +58 -0
  59. package/dist/prometheus-service.d.ts +1 -3
  60. package/dist/prometheus-service.d.ts.map +1 -1
  61. package/dist/prometheus-service.js +4 -59
  62. package/dist/types/graph-api.types.d.ts +8 -7
  63. package/dist/types/graph-api.types.d.ts.map +1 -1
  64. package/dist/types/scoped-actions-contracts.d.ts +10 -1
  65. package/dist/types/scoped-actions-contracts.d.ts.map +1 -1
  66. package/dist/types/scoped-actions-contracts.js +9 -0
  67. package/dist/utils/api-error-handler.d.ts +2 -0
  68. package/dist/utils/api-error-handler.d.ts.map +1 -0
  69. package/dist/utils/api-error-handler.js +20 -0
  70. package/package.json +5 -2
  71. package/src/attributions-service.ts +93 -0
  72. package/src/authorization-attributes-service.ts +234 -0
  73. package/src/authorization-internal-service.ts +129 -0
  74. package/src/authorization-middleware.ts +51 -0
  75. package/src/authorization-service.ts +356 -0
  76. package/src/clients/graph-api.ts +170 -0
  77. package/src/clients/platform-api.ts +117 -0
  78. package/src/constants/sns.ts +5 -0
  79. package/src/constants.ts +23 -0
  80. package/src/index.ts +62 -0
  81. package/src/metrics-service.ts +67 -0
  82. package/src/prometheus-service.ts +51 -0
  83. package/src/roles-service.ts +125 -0
  84. package/src/testKit/index.ts +69 -0
  85. package/src/types/authorization-attributes-contracts.ts +33 -0
  86. package/src/types/express.ts +8 -0
  87. package/src/types/general.ts +32 -0
  88. package/src/types/graph-api.types.ts +25 -0
  89. package/src/types/roles.ts +42 -0
  90. package/src/types/scoped-actions-contracts.ts +57 -0
  91. package/src/utils/api-error-handler.ts +21 -0
  92. package/src/utils/authorization.utils.ts +47 -0
  93. package/dist/clients/graph-api.client.d.ts +0 -24
  94. package/dist/clients/graph-api.client.d.ts.map +0 -1
  95. package/dist/clients/platform-api.client.d.ts +0 -31
  96. package/dist/clients/platform-api.client.d.ts.map +0 -1
  97. package/dist/esm/clients/graph-api.client.d.ts +0 -24
  98. package/dist/esm/clients/graph-api.client.d.ts.map +0 -1
  99. package/dist/esm/clients/platform-api.client.d.ts +0 -31
  100. package/dist/esm/clients/platform-api.client.d.ts.map +0 -1
@@ -0,0 +1,356 @@
1
+ import { performance } from 'perf_hooks';
2
+ import { MondayFetchOptions } from '@mondaydotcomorg/monday-fetch';
3
+ import { Api } from '@mondaydotcomorg/trident-backend-api';
4
+ import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
5
+ import { getIgniteClient, IgniteClient } from '@mondaydotcomorg/ignite-sdk';
6
+ import { Action, AuthorizationObject, AuthorizationParams, Resource } from './types/general';
7
+ import { sendAuthorizationCheckResponseTimeMetric } from './prometheus-service';
8
+ import { recordAuthorizationTiming } from './metrics-service';
9
+ import {
10
+ ScopedAction,
11
+ ScopedActionPermit,
12
+ ScopedActionResponseObject,
13
+ ScopeOptions,
14
+ } from './types/scoped-actions-contracts';
15
+ import { AuthorizationInternalService, logger } from './authorization-internal-service';
16
+ import { getAttributionsFromApi, getProfile, PlatformProfile } from './attributions-service';
17
+ import { GraphApi } from './clients/graph-api';
18
+ import { PlatformApi } from './clients/platform-api';
19
+ import { scopeToResource } from './utils/authorization.utils';
20
+
21
+ const GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS = 5 * 60;
22
+ const PLATFORM_AUTHORIZE_PATH = '/internal_ms/authorization/authorize';
23
+
24
+ const ALLOWED_SDK_PLATFORM_PROFILES_KEY = 'allowed-sdk-platform-profiles';
25
+ const IN_RELEASE_SDK_PLATFORM_PROFILES_KEY = 'in-release-sdk-platform-profile';
26
+ const PLATFORM_PROFILE_RELEASE_FF = 'sdk-platform-profiles';
27
+ const NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF = 'navigate-can-action-in-scope-to-graph';
28
+
29
+ export interface AuthorizeResponse {
30
+ isAuthorized: boolean;
31
+ unauthorizedIds?: number[];
32
+ unauthorizedObjects?: AuthorizationObject[];
33
+ }
34
+
35
+ export function setRequestFetchOptions(customMondayFetchOptions: MondayFetchOptions) {
36
+ AuthorizationInternalService.setRequestFetchOptions(customMondayFetchOptions);
37
+ }
38
+
39
+ interface IsAuthorizedResponse {
40
+ result: boolean[];
41
+ }
42
+
43
+ export class AuthorizationService {
44
+ private static get graphApi(): GraphApi {
45
+ if (!this._graphApi) {
46
+ this._graphApi = new GraphApi();
47
+ }
48
+ return this._graphApi;
49
+ }
50
+ private static _graphApi?: GraphApi;
51
+
52
+ private static get platformApi(): PlatformApi {
53
+ if (!this._platformApi) {
54
+ this._platformApi = new PlatformApi();
55
+ }
56
+ return this._platformApi;
57
+ }
58
+ private static _platformApi?: PlatformApi;
59
+
60
+ static resetApiClients(): void {
61
+ this._graphApi = undefined;
62
+ this._platformApi = undefined;
63
+ }
64
+
65
+ static redisClient?;
66
+ static grantedFeatureRedisExpirationInSeconds?: number;
67
+ static igniteClient?: IgniteClient;
68
+
69
+ /**
70
+ * @deprecated use the second form with authorizationRequestObjects instead,
71
+ * support of this function will be dropped gradually
72
+ */
73
+ static async isAuthorized(
74
+ accountId: number,
75
+ userId: number,
76
+ resources: Resource[],
77
+ action: Action
78
+ ): Promise<AuthorizeResponse>;
79
+
80
+ static async isAuthorized(
81
+ accountId: number,
82
+ userId: number,
83
+ authorizationRequestObjects: AuthorizationObject[]
84
+ ): Promise<AuthorizeResponse>;
85
+
86
+ static async isAuthorized(...args: any[]) {
87
+ if (args.length === 3) {
88
+ return this.isAuthorizedMultiple(args[0], args[1], args[2]);
89
+ } else if (args.length == 4) {
90
+ return this.isAuthorizedSingular(args[0], args[1], args[2], args[3]);
91
+ } else {
92
+ throw new Error('isAuthorized accepts either 3 or 4 arguments');
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @deprecated - Please use Ignite instead: https://github.com/DaPulse/ignite-monorepo/blob/master/packages/ignite-sdk/README.md
98
+ * @sunsetDate 2024-12-31
99
+ */
100
+ static async isUserGrantedWithFeature(
101
+ accountId: number,
102
+ userId: number,
103
+ featureName: string,
104
+ options: { shouldSkipCache?: boolean } = {}
105
+ ): Promise<boolean> {
106
+ const cachedKey = this.getCachedKeyName(userId, featureName);
107
+ const shouldSkipCache = options.shouldSkipCache ?? false;
108
+ if (this.redisClient && !shouldSkipCache) {
109
+ const grantedFeatureValue = await this.redisClient.get(cachedKey);
110
+ if (!(grantedFeatureValue === undefined || grantedFeatureValue === null)) {
111
+ // redis returns the value as string
112
+ return grantedFeatureValue === 'true';
113
+ }
114
+ }
115
+
116
+ const grantedFeatureValue = await this.fetchIsUserGrantedWithFeature(featureName, accountId, userId);
117
+ if (this.redisClient) {
118
+ await this.redisClient.set(cachedKey, grantedFeatureValue, 'EX', this.grantedFeatureRedisExpirationInSeconds);
119
+ }
120
+ return grantedFeatureValue;
121
+ }
122
+
123
+ private static async fetchIsUserGrantedWithFeature(
124
+ featureName: string,
125
+ accountId: number,
126
+ userId: number
127
+ ): Promise<boolean> {
128
+ const authorizationObject: AuthorizationObject = {
129
+ action: featureName,
130
+ resource_type: 'feature',
131
+ };
132
+
133
+ const authorizeResponsePromise = await this.isAuthorized(accountId, userId, [authorizationObject]);
134
+ return authorizeResponsePromise.isAuthorized;
135
+ }
136
+
137
+ private static getCachedKeyName(userId: number, featureName: string): string {
138
+ return `granted-feature-${featureName}-${userId}`;
139
+ }
140
+
141
+ static async canActionInScope(
142
+ accountId: number,
143
+ userId: number,
144
+ action: string,
145
+ scope: ScopeOptions
146
+ ): Promise<ScopedActionPermit> {
147
+ const scopedActions = [{ action, scope }];
148
+ const scopedActionResponseObjects = await this.canActionInScopeMultiple(accountId, userId, scopedActions);
149
+ return scopedActionResponseObjects[0].permit;
150
+ }
151
+
152
+ private static getProfile(accountId: number, userId: number): PlatformProfile {
153
+ const appName: string = process.env.APP_NAME ?? 'INVALID_APP_NAME';
154
+ if (!this.igniteClient) {
155
+ logger.error({ tag: 'authorization-service' }, 'AuthorizationService: igniteClient is not set, failing request');
156
+ throw new Error('AuthorizationService: igniteClient is not set, failing request');
157
+ }
158
+ if (
159
+ this.igniteClient.configuration.getObjectValue<string[]>(ALLOWED_SDK_PLATFORM_PROFILES_KEY, []).includes(appName)
160
+ ) {
161
+ return getProfile();
162
+ }
163
+ if (
164
+ this.igniteClient.configuration
165
+ .getObjectValue<string[]>(IN_RELEASE_SDK_PLATFORM_PROFILES_KEY, [])
166
+ .includes(appName) &&
167
+ this.igniteClient.isReleased(PLATFORM_PROFILE_RELEASE_FF, { accountId, userId })
168
+ ) {
169
+ return getProfile();
170
+ }
171
+ return PlatformProfile.APP;
172
+ }
173
+
174
+ static async canActionInScopeMultiple(
175
+ accountId: number,
176
+ userId: number,
177
+ scopedActions: ScopedAction[]
178
+ ): Promise<ScopedActionResponseObject[]> {
179
+ if (scopedActions.length === 0) {
180
+ return [];
181
+ }
182
+
183
+ const shouldNavigateToGraph = Boolean(
184
+ this.igniteClient?.isReleased(NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF, { accountId, userId })
185
+ );
186
+
187
+ const startTime = performance.now();
188
+ let scopedActionResponseObjects: ScopedActionResponseObject[];
189
+ let apiType: 'graph' | 'platform';
190
+
191
+ if (shouldNavigateToGraph) {
192
+ apiType = 'graph';
193
+ scopedActionResponseObjects = await this.graphApi.checkPermissions(accountId, userId, scopedActions);
194
+ } else {
195
+ apiType = 'platform';
196
+ const profile = this.getProfile(accountId, userId);
197
+ const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
198
+
199
+ scopedActionResponseObjects = await this.platformApi.checkPermissions(
200
+ profile,
201
+ internalAuthToken,
202
+ userId,
203
+ scopedActions
204
+ );
205
+ }
206
+
207
+ const endTime = performance.now();
208
+ const time = endTime - startTime;
209
+
210
+ // Record metrics for each authorization check
211
+ for (const obj of scopedActionResponseObjects) {
212
+ const { action, scope } = obj.scopedAction;
213
+ const { resourceType } = scopeToResource(scope);
214
+ const isAuthorized = obj.permit.can;
215
+ sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, 200, time);
216
+ recordAuthorizationTiming(apiType, time);
217
+ }
218
+
219
+ return scopedActionResponseObjects;
220
+ }
221
+
222
+ private static async isAuthorizedSingular(
223
+ accountId: number,
224
+ userId: number,
225
+ resources: Resource[],
226
+ action: Action
227
+ ): Promise<AuthorizeResponse> {
228
+ const { authorizationObjects } = createAuthorizationParams(resources, action);
229
+ return this.isAuthorizedMultiple(accountId, userId, authorizationObjects);
230
+ }
231
+
232
+ private static async isAuthorizedMultiple(
233
+ accountId: number,
234
+ userId: number,
235
+ authorizationRequestObjects: AuthorizationObject[]
236
+ ): Promise<AuthorizeResponse> {
237
+ const profile = this.getProfile(accountId, userId);
238
+ const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
239
+ const startTime = performance.now();
240
+ const attributionHeaders = getAttributionsFromApi();
241
+ const httpClient = Api.getPart('httpClient');
242
+
243
+ let response: IsAuthorizedResponse | undefined;
244
+ try {
245
+ response = await httpClient!.fetch<IsAuthorizedResponse>(
246
+ {
247
+ url: {
248
+ appName: 'platform',
249
+ path: PLATFORM_AUTHORIZE_PATH,
250
+ profile,
251
+ },
252
+ method: 'POST',
253
+ headers: {
254
+ Authorization: internalAuthToken,
255
+ 'Content-Type': 'application/json',
256
+ ...attributionHeaders,
257
+ },
258
+ body: JSON.stringify({
259
+ user_id: userId,
260
+ authorize_request_objects: authorizationRequestObjects,
261
+ }),
262
+ },
263
+ {
264
+ timeout: AuthorizationInternalService.getRequestTimeout(),
265
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
266
+ }
267
+ );
268
+ } catch (err) {
269
+ if (err instanceof HttpFetcherError) {
270
+ AuthorizationInternalService.throwOnHttpError((err as HttpFetcherError).status, 'isAuthorizedMultiple');
271
+ } else {
272
+ throw err;
273
+ }
274
+ }
275
+ const endTime = performance.now();
276
+ const time = endTime - startTime;
277
+
278
+ const unauthorizedObjects: AuthorizationObject[] = [];
279
+
280
+ if (!response) {
281
+ logger.error({ tag: 'authorization-service', response }, 'AuthorizationService: missing response');
282
+ throw new Error('AuthorizationService: missing response');
283
+ }
284
+
285
+ response.result.forEach(function (isAuthorized, index) {
286
+ const authorizationObject = authorizationRequestObjects[index];
287
+ if (!isAuthorized) {
288
+ unauthorizedObjects.push(authorizationObject);
289
+ }
290
+
291
+ sendAuthorizationCheckResponseTimeMetric(
292
+ authorizationObject.resource_type,
293
+ authorizationObject.action,
294
+ isAuthorized,
295
+ 200,
296
+ time
297
+ );
298
+ });
299
+
300
+ if (unauthorizedObjects.length > 0) {
301
+ logger.info(
302
+ {
303
+ resources: JSON.stringify(unauthorizedObjects),
304
+ },
305
+ 'AuthorizationService: resource is unauthorized'
306
+ );
307
+ const unauthorizedIds = unauthorizedObjects
308
+ .filter(obj => !!obj.resource_id)
309
+ .map(obj => obj.resource_id) as number[];
310
+ return { isAuthorized: false, unauthorizedIds, unauthorizedObjects };
311
+ }
312
+
313
+ return { isAuthorized: true };
314
+ }
315
+ }
316
+
317
+ export function setRedisClient(
318
+ client,
319
+ grantedFeatureRedisExpirationInSeconds: number = GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS
320
+ ) {
321
+ AuthorizationService.redisClient = client;
322
+ if (grantedFeatureRedisExpirationInSeconds && grantedFeatureRedisExpirationInSeconds > 0) {
323
+ AuthorizationService.grantedFeatureRedisExpirationInSeconds = grantedFeatureRedisExpirationInSeconds;
324
+ } else {
325
+ logger.warn(
326
+ { grantedFeatureRedisExpirationInSeconds },
327
+ 'Invalid input for grantedFeatureRedisExpirationInSeconds, must be positive number. using default ttl.'
328
+ );
329
+ AuthorizationService.grantedFeatureRedisExpirationInSeconds = GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS;
330
+ }
331
+ }
332
+
333
+ export async function setIgniteClient() {
334
+ const igniteClient = await getIgniteClient({
335
+ namespace: ['authorization-sdk'],
336
+ });
337
+ AuthorizationService.igniteClient = igniteClient;
338
+ AuthorizationInternalService.setIgniteClient(igniteClient);
339
+ }
340
+
341
+ export function createAuthorizationParams(resources: Resource[], action: Action): AuthorizationParams {
342
+ const params = {
343
+ authorizationObjects: resources.map((resource: Resource) => {
344
+ const authorizationObject: AuthorizationObject = {
345
+ resource_id: resource.id,
346
+ resource_type: resource.type,
347
+ action,
348
+ };
349
+ if (resource.wrapperData) {
350
+ authorizationObject.wrapper_data = resource.wrapperData;
351
+ }
352
+ return authorizationObject;
353
+ }),
354
+ };
355
+ return params;
356
+ }
@@ -0,0 +1,170 @@
1
+ import { Api, HttpClient } from '@mondaydotcomorg/trident-backend-api';
2
+ import {
3
+ ScopedAction,
4
+ ScopedActionResponseObject,
5
+ ScopedActionPermit,
6
+ PermitTechnicalReason,
7
+ } from '../types/scoped-actions-contracts';
8
+ import { AuthorizationInternalService } from '../authorization-internal-service';
9
+ import { getAttributionsFromApi } from '../attributions-service';
10
+ import {
11
+ GraphIsAllowedDto,
12
+ GraphIsAllowedResponse,
13
+ ResourceType,
14
+ ResourceId,
15
+ ActionName,
16
+ GraphPermissionResult,
17
+ GraphPermissionReason,
18
+ } from '../types/graph-api.types';
19
+ import { scopeToResource } from '../utils/authorization.utils';
20
+ import { signAuthorizationHeader } from '@mondaydotcomorg/monday-jwt';
21
+ import { GRAPH_APP_NAME } from '../constants';
22
+ import { handleApiError } from '../utils/api-error-handler';
23
+
24
+ const CAN_ACTION_IN_SCOPE_GRAPH_PATH = '/permissions/is-allowed';
25
+ const APP_NAME_REQUIRED_ERROR = 'GraphApi: APP_NAME environment variable is required for Graph API authentication';
26
+
27
+ /**
28
+ * Client for handling Graph API authorization operations
29
+ */
30
+ export class GraphApi {
31
+ private readonly httpClient: HttpClient;
32
+ private readonly consumerAppName: string;
33
+
34
+ constructor() {
35
+ const httpClient = Api.getPart('httpClient');
36
+ if (!httpClient) {
37
+ throw new Error('GraphApi: http client is not initialized');
38
+ }
39
+ const consumerAppName = process.env.APP_NAME?.trim();
40
+ if (!consumerAppName) {
41
+ throw new Error(APP_NAME_REQUIRED_ERROR);
42
+ }
43
+ this.httpClient = httpClient;
44
+ this.consumerAppName = consumerAppName;
45
+ }
46
+
47
+ /**
48
+ * Builds the request body for Graph API calls
49
+ */
50
+ private static buildRequestBody(scopedActions: ScopedAction[]): GraphIsAllowedDto {
51
+ const resourcesAccumulator: Record<ResourceType, Record<ResourceId, Set<ActionName>>> = {};
52
+ for (const { action, scope } of scopedActions) {
53
+ const { resourceType, resourceId } = scopeToResource(scope);
54
+ if (!resourcesAccumulator[resourceType]) {
55
+ resourcesAccumulator[resourceType] = {};
56
+ }
57
+ if (!resourcesAccumulator[resourceType][resourceId]) {
58
+ resourcesAccumulator[resourceType][resourceId] = new Set<ActionName>();
59
+ }
60
+ resourcesAccumulator[resourceType][resourceId].add(action);
61
+ }
62
+
63
+ const resourcesPayload: GraphIsAllowedDto = {};
64
+ for (const [resourceType, idMap] of Object.entries(resourcesAccumulator)) {
65
+ resourcesPayload[resourceType] = {};
66
+ for (const [idStr, actionsSet] of Object.entries(idMap)) {
67
+ const idNum = Number(idStr);
68
+ resourcesPayload[resourceType][idNum] = Array.from(actionsSet);
69
+ }
70
+ }
71
+
72
+ return resourcesPayload;
73
+ }
74
+
75
+ /**
76
+ * Fetches authorization data from the Graph API
77
+ */
78
+ async fetchPermissions(authToken: string, scopedActions: ScopedAction[]): Promise<GraphIsAllowedResponse> {
79
+ const attributionHeaders = getAttributionsFromApi();
80
+ const bodyPayload = GraphApi.buildRequestBody(scopedActions);
81
+
82
+ try {
83
+ const response = await this.httpClient.fetch<GraphIsAllowedResponse>(
84
+ {
85
+ url: {
86
+ appName: GRAPH_APP_NAME,
87
+ path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
88
+ },
89
+ method: 'POST',
90
+ headers: {
91
+ Authorization: authToken,
92
+ 'Content-Type': 'application/json',
93
+ ...attributionHeaders,
94
+ },
95
+ body: JSON.stringify(bodyPayload),
96
+ },
97
+ {
98
+ timeout: AuthorizationInternalService.getRequestTimeout(),
99
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
100
+ }
101
+ );
102
+
103
+ return response;
104
+ } catch (err) {
105
+ // handleApiError never returns (throws)
106
+ return handleApiError(err, 'graph', 'canActionInScopeMultiple');
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Maps Graph API response to the expected format
112
+ */
113
+ private static mapResponse(
114
+ scopedActions: ScopedAction[],
115
+ graphResponse: GraphIsAllowedResponse
116
+ ): ScopedActionResponseObject[] {
117
+ const resources = graphResponse ?? {};
118
+
119
+ return scopedActions.map(scopedAction => {
120
+ const { action, scope } = scopedAction;
121
+ const { resourceType, resourceId } = scopeToResource(scope);
122
+ const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
123
+
124
+ const permit: ScopedActionPermit = {
125
+ can: permissionResult?.can ?? false,
126
+ reason: {
127
+ key: 'unknown',
128
+ },
129
+ technicalReason: PermitTechnicalReason.NO_REASON,
130
+ };
131
+
132
+ if (permissionResult) {
133
+ const graphReason = GraphApi.ensureGraphReason(permissionResult.reason, { resourceType, resourceId, action });
134
+ permit.reason = {
135
+ key: graphReason.key,
136
+ ...(graphReason.additionalOptions ?? {}),
137
+ };
138
+ permit.technicalReason = (graphReason.technicalReason ??
139
+ PermitTechnicalReason.NO_REASON) as PermitTechnicalReason;
140
+ }
141
+
142
+ return { scopedAction, permit };
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Performs a complete authorization check using the Graph API
148
+ */
149
+ async checkPermissions(
150
+ accountId: number,
151
+ userId: number,
152
+ scopedActions: ScopedAction[]
153
+ ): Promise<ScopedActionResponseObject[]> {
154
+ const authToken = signAuthorizationHeader({ appName: this.consumerAppName, accountId, userId });
155
+ const response = await this.fetchPermissions(authToken, scopedActions);
156
+ return GraphApi.mapResponse(scopedActions, response);
157
+ }
158
+
159
+ private static ensureGraphReason(
160
+ reason: GraphPermissionResult['reason'],
161
+ context: { resourceType: ResourceType; resourceId: ResourceId; action: ActionName }
162
+ ): GraphPermissionReason {
163
+ if (!reason || typeof reason !== 'object' || typeof reason.key !== 'string') {
164
+ throw new Error(
165
+ `GraphApi: unexpected reason format for ${context.resourceType}/${context.resourceId}/${context.action}`
166
+ );
167
+ }
168
+ return reason;
169
+ }
170
+ }
@@ -0,0 +1,117 @@
1
+ import { Api, HttpClient } from '@mondaydotcomorg/trident-backend-api';
2
+ import { ScopedAction, ScopedActionResponseObject } from '../types/scoped-actions-contracts';
3
+ import { AuthorizationInternalService, logger } from '../authorization-internal-service';
4
+ import { getAttributionsFromApi, PlatformProfile } from '../attributions-service';
5
+ import { toCamelCase, toSnakeCase } from '../utils/authorization.utils';
6
+ import { handleApiError } from '../utils/api-error-handler';
7
+
8
+ const PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH = '/internal_ms/authorization/can_actions_in_scopes';
9
+
10
+ // ScopedAction with snake_case scope keys for Platform API payload
11
+ type ScopedActionPlatformPayload = Omit<ScopedAction, 'scope'> & {
12
+ scope: Record<string, number>;
13
+ };
14
+
15
+ interface CanActionsInScopesResponse {
16
+ result: ScopedActionResponseObject[];
17
+ }
18
+
19
+ /**
20
+ * Client for handling Platform API authorization operations
21
+ */
22
+ export class PlatformApi {
23
+ private readonly httpClient: HttpClient;
24
+
25
+ constructor() {
26
+ const httpClient = Api.getPart('httpClient');
27
+ if (!httpClient) {
28
+ throw new Error('PlatformApi: http client is not initialized');
29
+ }
30
+ this.httpClient = httpClient;
31
+ }
32
+
33
+ /**
34
+ * Builds the request payload for Platform API calls
35
+ */
36
+ private static buildRequestPayload(scopedActions: ScopedAction[]): ScopedActionPlatformPayload[] {
37
+ return scopedActions.map(scopedAction => ({
38
+ ...scopedAction,
39
+ scope: toSnakeCase(scopedAction.scope) as Record<string, number>,
40
+ }));
41
+ }
42
+
43
+ /**
44
+ * Fetches authorization data from the Platform API
45
+ */
46
+ private async fetchPermissions(
47
+ profile: PlatformProfile,
48
+ internalAuthToken: string,
49
+ userId: number,
50
+ scopedActionsPayload: ScopedActionPlatformPayload[]
51
+ ): Promise<CanActionsInScopesResponse> {
52
+ const attributionHeaders = getAttributionsFromApi();
53
+
54
+ try {
55
+ const response = await this.httpClient.fetch<CanActionsInScopesResponse>(
56
+ {
57
+ url: {
58
+ appName: 'platform',
59
+ path: PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH,
60
+ profile,
61
+ },
62
+ method: 'POST',
63
+ headers: {
64
+ Authorization: internalAuthToken,
65
+ 'Content-Type': 'application/json',
66
+ ...attributionHeaders,
67
+ },
68
+ body: JSON.stringify({ user_id: userId, scoped_actions: scopedActionsPayload }),
69
+ },
70
+ {
71
+ timeout: AuthorizationInternalService.getRequestTimeout(),
72
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
73
+ }
74
+ );
75
+
76
+ return response;
77
+ } catch (err) {
78
+ // handleApiError never returns (throws)
79
+ return handleApiError(err, 'platform', 'canActionInScopeMultiple');
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Maps Platform API response to the expected format
85
+ */
86
+ private static mapResponse(response: CanActionsInScopesResponse): ScopedActionResponseObject[] {
87
+ if (!response) {
88
+ logger.error({ tag: 'platform-api', response }, 'PlatformApi: missing response');
89
+ throw new Error('PlatformApi: missing response');
90
+ }
91
+
92
+ return response.result.map(responseObject => {
93
+ const { scopedAction, permit } = responseObject;
94
+ const { scope } = scopedAction;
95
+
96
+ return {
97
+ ...responseObject,
98
+ scopedAction: { ...scopedAction, scope: toCamelCase(scope) },
99
+ permit: toCamelCase(permit),
100
+ };
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Performs a complete authorization check using the Platform API
106
+ */
107
+ async checkPermissions(
108
+ profile: PlatformProfile,
109
+ internalAuthToken: string,
110
+ userId: number,
111
+ scopedActions: ScopedAction[]
112
+ ): Promise<ScopedActionResponseObject[]> {
113
+ const scopedActionsPayload = PlatformApi.buildRequestPayload(scopedActions);
114
+ const platformResponse = await this.fetchPermissions(profile, internalAuthToken, userId, scopedActionsPayload);
115
+ return PlatformApi.mapResponse(platformResponse);
116
+ }
117
+ }
@@ -0,0 +1,5 @@
1
+ export const SNS_ARN_ENV_VAR_NAME = 'SHARED_AUTHORIZATION_SNS_ENDPOINT_RESOURCE_ATTRIBUTES';
2
+ export const SNS_DEV_TEST_NAME =
3
+ 'arn:aws:sns:us-east-1:000000000000:monday-authorization-resource-attributes-sns-local';
4
+ export const RESOURCE_ATTRIBUTES_SNS_UPDATE_OPERATION_MESSAGE_KIND = 'resourceAttributeModification';
5
+ export const ASYNC_RESOURCE_ATTRIBUTES_MAX_OPERATIONS_PER_MESSAGE = 100;
@@ -0,0 +1,23 @@
1
+ import { RecursivePartial } from '@mondaydotcomorg/monday-fetch-api';
2
+ import { FetcherConfig } from '@mondaydotcomorg/trident-backend-api';
3
+
4
+ export const APP_NAME = 'authorization';
5
+ export const GRAPH_APP_NAME = 'authorization-graph';
6
+
7
+ export const ERROR_MESSAGES = {
8
+ HTTP_CLIENT_NOT_INITIALIZED: 'MondayAuthorization: HTTP client is not initialized',
9
+ REQUEST_FAILED: (method: string, status: number, reason: string) =>
10
+ `MondayAuthorization: [${method}] request failed with status ${status} with reason: ${reason}`,
11
+ } as const;
12
+
13
+ export const DEFAULT_FETCH_OPTIONS: RecursivePartial<FetcherConfig> = {
14
+ retryPolicy: {
15
+ useRetries: true,
16
+ maxRetries: 3,
17
+ retryDelayMS: 10,
18
+ },
19
+ logPolicy: {
20
+ logErrors: 'error',
21
+ logRequests: 'info',
22
+ },
23
+ };