@mondaydotcomorg/monday-authorization 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-2992133 → 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-36f311f

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 (37) hide show
  1. package/dist/authorization-service.d.ts.map +1 -1
  2. package/dist/authorization-service.js +7 -0
  3. package/dist/clients/graph-api.client.d.ts.map +1 -1
  4. package/dist/clients/graph-api.client.js +37 -2
  5. package/dist/clients/platform-api.client.d.ts.map +1 -1
  6. package/dist/esm/authorization-service.d.ts.map +1 -1
  7. package/dist/esm/authorization-service.mjs +7 -0
  8. package/dist/esm/clients/graph-api.client.d.ts.map +1 -1
  9. package/dist/esm/clients/graph-api.client.mjs +37 -2
  10. package/dist/esm/clients/platform-api.client.d.ts.map +1 -1
  11. package/dist/esm/types/graph-api.types.d.ts +9 -1
  12. package/dist/esm/types/graph-api.types.d.ts.map +1 -1
  13. package/dist/esm/utils/authorization.utils.d.ts.map +1 -1
  14. package/dist/types/graph-api.types.d.ts +9 -1
  15. package/dist/types/graph-api.types.d.ts.map +1 -1
  16. package/dist/utils/authorization.utils.d.ts.map +1 -1
  17. package/package.json +2 -3
  18. package/src/attributions-service.ts +0 -92
  19. package/src/authorization-attributes-service.ts +0 -234
  20. package/src/authorization-internal-service.ts +0 -129
  21. package/src/authorization-middleware.ts +0 -51
  22. package/src/authorization-service.ts +0 -357
  23. package/src/clients/graph-api.client.ts +0 -134
  24. package/src/clients/platform-api.client.ts +0 -118
  25. package/src/constants/sns.ts +0 -5
  26. package/src/constants.ts +0 -22
  27. package/src/index.ts +0 -46
  28. package/src/prometheus-service.ts +0 -147
  29. package/src/roles-service.ts +0 -125
  30. package/src/testKit/index.ts +0 -69
  31. package/src/types/authorization-attributes-contracts.ts +0 -33
  32. package/src/types/express.ts +0 -8
  33. package/src/types/general.ts +0 -32
  34. package/src/types/graph-api.types.ts +0 -19
  35. package/src/types/roles.ts +0 -42
  36. package/src/types/scoped-actions-contracts.ts +0 -48
  37. package/src/utils/authorization.utils.ts +0 -48
@@ -1,129 +0,0 @@
1
- import { signAuthorizationHeader } from '@mondaydotcomorg/monday-jwt';
2
- import { fetch, MondayFetchOptions } from '@mondaydotcomorg/monday-fetch';
3
- import * as MondayLogger from '@mondaydotcomorg/monday-logger';
4
- import { NullableErrorWithType, OnRetryCallback, RetryPolicy } from '@mondaydotcomorg/monday-fetch-api';
5
- import { IgniteClient } from '@mondaydotcomorg/ignite-sdk';
6
- import { BaseRequest } from './types/general';
7
-
8
- const INTERNAL_APP_NAME = 'internal_ms';
9
- const MAX_RETRIES = 3;
10
- const RETRY_DELAY_MS = 10;
11
- export const logger = MondayLogger.getLogger();
12
-
13
- const defaultMondayFetchOptions: MondayFetchOptions = {
14
- retries: MAX_RETRIES,
15
- callback: logOnFetchFail,
16
- };
17
-
18
- export const onRetryCallback: OnRetryCallback = (attempt: number, error?: NullableErrorWithType) => {
19
- if (attempt == MAX_RETRIES) {
20
- logger.error({ tag: 'authorization-service', attempt, error }, 'Authorization attempt failed');
21
- } else {
22
- logger.info({ tag: 'authorization-service', attempt, error }, 'Authorization attempt failed, trying again');
23
- }
24
- };
25
-
26
- function logOnFetchFail(retriesLeft: number, error: Error) {
27
- if (retriesLeft == 0) {
28
- logger.error({ retriesLeft, error }, 'Authorization attempt failed due to network issues');
29
- } else {
30
- logger.info({ retriesLeft, error }, 'Authorization attempt failed due to network issues, trying again');
31
- }
32
- }
33
-
34
- let mondayFetchOptions: MondayFetchOptions = defaultMondayFetchOptions;
35
-
36
- export class AuthorizationInternalService {
37
- static igniteClient?: IgniteClient;
38
- static skipAuthorization(requset: BaseRequest): void {
39
- requset.authorizationSkipPerformed = true;
40
- }
41
-
42
- static markAuthorized(request: BaseRequest): void {
43
- request.authorizationCheckPerformed = true;
44
- }
45
-
46
- static failIfNotCoveredByAuthorization(request: BaseRequest): void {
47
- if (!request.authorizationCheckPerformed && !request.authorizationSkipPerformed) {
48
- throw 'Endpoint is not covered by authorization check';
49
- }
50
- }
51
-
52
- static throwOnHttpErrorIfNeeded(response: Awaited<ReturnType<typeof fetch>>, placement: string): void {
53
- if (response.ok) {
54
- return;
55
- }
56
-
57
- const status = response.status;
58
- logger.error(
59
- { tag: 'authorization-service', placement, status },
60
- 'AuthorizationService: authorization request failed'
61
- );
62
-
63
- throw new Error(`AuthorizationService: [${placement}] authorization request failed with status ${status}`);
64
- }
65
-
66
- static throwOnHttpError(status: number, placement: string) {
67
- logger.error(
68
- { tag: 'authorization-service', placement, status },
69
- 'AuthorizationService: authorization request failed'
70
- );
71
- throw new Error(`AuthorizationService: [${placement}] authorization request failed with status ${status}`);
72
- }
73
-
74
- static generateInternalAuthToken(accountId: number, userId: number) {
75
- return signAuthorizationHeader({ appName: INTERNAL_APP_NAME, accountId, userId });
76
- }
77
-
78
- static setRequestFetchOptions(customMondayFetchOptions: MondayFetchOptions) {
79
- mondayFetchOptions = {
80
- ...defaultMondayFetchOptions,
81
- ...customMondayFetchOptions,
82
- };
83
- }
84
-
85
- static getRequestFetchOptions(): MondayFetchOptions {
86
- return mondayFetchOptions;
87
- }
88
-
89
- static setIgniteClient(client: IgniteClient) {
90
- this.igniteClient = client;
91
- }
92
-
93
- static getRequestTimeout() {
94
- const isDevEnv = process.env.NODE_ENV === 'development';
95
- const defaultTimeout = isDevEnv ? 60000 : 2000;
96
-
97
- if (!this.igniteClient) {
98
- return defaultTimeout;
99
- }
100
-
101
- const overrideTimeouts = this.igniteClient.configuration.getObjectValue<Record<string, number>>(
102
- 'override-outgoing-request-timeout-ms',
103
- {}
104
- );
105
- try {
106
- if (process.env.APP_NAME && process.env.APP_NAME in overrideTimeouts) {
107
- return overrideTimeouts[process.env.APP_NAME];
108
- } else {
109
- return this.igniteClient.configuration.getNumberValue('outgoing-request-timeout-ms', defaultTimeout);
110
- }
111
- } catch (error) {
112
- logger.error(
113
- { tag: 'authorization-service', error, defaultTimeout },
114
- 'Failed to get timeout from ignite configuration, returning default timeout'
115
- );
116
- return defaultTimeout;
117
- }
118
- }
119
-
120
- static getRetriesPolicy(): RetryPolicy {
121
- const fetchOptions = AuthorizationInternalService.getRequestFetchOptions();
122
- return {
123
- useRetries: fetchOptions.retries !== undefined,
124
- maxRetries: fetchOptions.retries !== undefined ? fetchOptions.retries : 0,
125
- onRetry: onRetryCallback,
126
- retryDelayMS: fetchOptions.retryDelay ?? RETRY_DELAY_MS,
127
- };
128
- }
129
- }
@@ -1,51 +0,0 @@
1
- import onHeaders from 'on-headers';
2
- import { AuthorizationInternalService } from './authorization-internal-service';
3
- import { AuthorizationService, createAuthorizationParams } from './authorization-service';
4
- import { Action, BaseRequest, BaseResponse, Context, ContextGetter, ResourceGetter } from './types/general';
5
- import type { NextFunction } from 'express';
6
-
7
- // getAuthorizationMiddleware is duplicated in testKit/index.ts
8
- // If you are making changes to this function, please make sure to update the other file as well
9
- export function getAuthorizationMiddleware(
10
- action: Action,
11
- resourceGetter: ResourceGetter,
12
- contextGetter?: ContextGetter
13
- ) {
14
- return async function authorizationMiddleware(
15
- request: BaseRequest,
16
- response: BaseResponse,
17
- next: NextFunction
18
- ): Promise<void> {
19
- contextGetter ||= defaultContextGetter;
20
- const { userId, accountId } = contextGetter(request);
21
- const resources = resourceGetter(request);
22
- const { authorizationObjects } = createAuthorizationParams(resources, action);
23
- const { isAuthorized } = await AuthorizationService.isAuthorized(accountId, userId, authorizationObjects);
24
- AuthorizationInternalService.markAuthorized(request);
25
- if (!isAuthorized) {
26
- response.status(403).json({ message: 'Access denied' });
27
- return;
28
- }
29
- next();
30
- };
31
- }
32
-
33
- export function skipAuthorizationMiddleware(request: BaseRequest, response: BaseResponse, next: NextFunction): void {
34
- AuthorizationInternalService.skipAuthorization(request);
35
- next();
36
- }
37
-
38
- export function authorizationCheckMiddleware(request: BaseRequest, response: BaseResponse, next: NextFunction): void {
39
- if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
40
- onHeaders(response, function () {
41
- if (response.statusCode < 400) {
42
- AuthorizationInternalService.failIfNotCoveredByAuthorization(request);
43
- }
44
- });
45
- }
46
- next();
47
- }
48
-
49
- export function defaultContextGetter(request: BaseRequest): Context {
50
- return request.payload;
51
- }
@@ -1,357 +0,0 @@
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, incrementAuthorizationSuccess } from './prometheus-service';
8
- import {
9
- ScopedAction,
10
- ScopedActionPermit,
11
- ScopedActionResponseObject,
12
- ScopeOptions,
13
- } from './types/scoped-actions-contracts';
14
- import { AuthorizationInternalService, logger } from './authorization-internal-service';
15
- import { getAttributionsFromApi, getProfile, PlatformProfile } from './attributions-service';
16
- import { GraphApiClient } from './clients/graph-api.client';
17
- import { PlatformApiClient } from './clients/platform-api.client';
18
- import { scopeToResource } from './utils/authorization.utils';
19
-
20
- const GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS = 5 * 60;
21
- const PLATFORM_AUTHORIZE_PATH = '/internal_ms/authorization/authorize';
22
-
23
- const ALLOWED_SDK_PLATFORM_PROFILES_KEY = 'allowed-sdk-platform-profiles';
24
- const IN_RELEASE_SDK_PLATFORM_PROFILES_KEY = 'in-release-sdk-platform-profile';
25
- const PLATFORM_PROFILE_RELEASE_FF = 'sdk-platform-profiles';
26
- const NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF = 'navigate-can-action-in-scope-to-graph';
27
-
28
- export interface AuthorizeResponse {
29
- isAuthorized: boolean;
30
- unauthorizedIds?: number[];
31
- unauthorizedObjects?: AuthorizationObject[];
32
- }
33
-
34
- export function setRequestFetchOptions(customMondayFetchOptions: MondayFetchOptions) {
35
- AuthorizationInternalService.setRequestFetchOptions(customMondayFetchOptions);
36
- }
37
-
38
- interface IsAuthorizedResponse {
39
- result: boolean[];
40
- }
41
-
42
- export class AuthorizationService {
43
- static redisClient?;
44
- static grantedFeatureRedisExpirationInSeconds?: number;
45
- static igniteClient?: IgniteClient;
46
-
47
- /**
48
- * @deprecated use the second form with authorizationRequestObjects instead,
49
- * support of this function will be dropped gradually
50
- */
51
- static async isAuthorized(
52
- accountId: number,
53
- userId: number,
54
- resources: Resource[],
55
- action: Action
56
- ): Promise<AuthorizeResponse>;
57
-
58
- static async isAuthorized(
59
- accountId: number,
60
- userId: number,
61
- authorizationRequestObjects: AuthorizationObject[]
62
- ): Promise<AuthorizeResponse>;
63
-
64
- static async isAuthorized(...args: any[]) {
65
- if (args.length === 3) {
66
- return this.isAuthorizedMultiple(args[0], args[1], args[2]);
67
- } else if (args.length == 4) {
68
- return this.isAuthorizedSingular(args[0], args[1], args[2], args[3]);
69
- } else {
70
- throw new Error('isAuthorized accepts either 3 or 4 arguments');
71
- }
72
- }
73
-
74
- /**
75
- * @deprecated - Please use Ignite instead: https://github.com/DaPulse/ignite-monorepo/blob/master/packages/ignite-sdk/README.md
76
- * @sunsetDate 2024-12-31
77
- */
78
- static async isUserGrantedWithFeature(
79
- accountId: number,
80
- userId: number,
81
- featureName: string,
82
- options: { shouldSkipCache?: boolean } = {}
83
- ): Promise<boolean> {
84
- const cachedKey = this.getCachedKeyName(userId, featureName);
85
- const shouldSkipCache = options.shouldSkipCache ?? false;
86
- if (this.redisClient && !shouldSkipCache) {
87
- const grantedFeatureValue = await this.redisClient.get(cachedKey);
88
- if (!(grantedFeatureValue === undefined || grantedFeatureValue === null)) {
89
- // redis returns the value as string
90
- return grantedFeatureValue === 'true';
91
- }
92
- }
93
-
94
- const grantedFeatureValue = await this.fetchIsUserGrantedWithFeature(featureName, accountId, userId);
95
- if (this.redisClient) {
96
- await this.redisClient.set(cachedKey, grantedFeatureValue, 'EX', this.grantedFeatureRedisExpirationInSeconds);
97
- }
98
- return grantedFeatureValue;
99
- }
100
-
101
- private static async fetchIsUserGrantedWithFeature(
102
- featureName: string,
103
- accountId: number,
104
- userId: number
105
- ): Promise<boolean> {
106
- const authorizationObject: AuthorizationObject = {
107
- action: featureName,
108
- resource_type: 'feature',
109
- };
110
-
111
- const authorizeResponsePromise = await this.isAuthorized(accountId, userId, [authorizationObject]);
112
- return authorizeResponsePromise.isAuthorized;
113
- }
114
-
115
- private static getCachedKeyName(userId: number, featureName: string): string {
116
- return `granted-feature-${featureName}-${userId}`;
117
- }
118
-
119
- static async canActionInScope(
120
- accountId: number,
121
- userId: number,
122
- action: string,
123
- scope: ScopeOptions
124
- ): Promise<ScopedActionPermit> {
125
- const scopedActions = [{ action, scope }];
126
- const scopedActionResponseObjects = await this.canActionInScopeMultiple(accountId, userId, scopedActions);
127
- return scopedActionResponseObjects[0].permit;
128
- }
129
-
130
- private static getProfile(accountId: number, userId: number): PlatformProfile {
131
- const appName: string = process.env.APP_NAME ?? 'INVALID_APP_NAME';
132
- if (!this.igniteClient) {
133
- logger.error({ tag: 'authorization-service' }, 'AuthorizationService: igniteClient is not set, failing request');
134
- throw new Error('AuthorizationService: igniteClient is not set, failing request');
135
- }
136
- if (
137
- this.igniteClient.configuration.getObjectValue<string[]>(ALLOWED_SDK_PLATFORM_PROFILES_KEY, []).includes(appName)
138
- ) {
139
- return getProfile();
140
- }
141
- if (
142
- this.igniteClient.configuration
143
- .getObjectValue<string[]>(IN_RELEASE_SDK_PLATFORM_PROFILES_KEY, [])
144
- .includes(appName) &&
145
- this.igniteClient.isReleased(PLATFORM_PROFILE_RELEASE_FF, { accountId, userId })
146
- ) {
147
- return getProfile();
148
- }
149
- return PlatformProfile.INTERNAL;
150
- }
151
-
152
- static async canActionInScopeMultiple(
153
- accountId: number,
154
- userId: number,
155
- scopedActions: ScopedAction[]
156
- ): Promise<ScopedActionResponseObject[]> {
157
-
158
- const shouldNavigateToGraph = Boolean(
159
- this.igniteClient?.isReleased(NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF, { accountId, userId })
160
- );
161
-
162
-
163
- const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
164
-
165
- const startTime = performance.now();
166
- let scopedActionResponseObjects: ScopedActionResponseObject[];
167
- let usedGraphApi = false;
168
-
169
- if (shouldNavigateToGraph) {
170
- try {
171
- scopedActionResponseObjects = await GraphApiClient.checkPermissions(internalAuthToken, scopedActions);
172
- usedGraphApi = true;
173
- } catch (error) {
174
- // Fallback to Platform API if Graph API fails
175
- logger.warn(
176
- {
177
- tag: 'authorization-service',
178
- error: error instanceof Error ? error.message : String(error),
179
- accountId,
180
- userId,
181
- },
182
- 'Graph API authorization failed, falling back to Platform API'
183
- );
184
- const profile = this.getProfile(accountId, userId);
185
- scopedActionResponseObjects = await PlatformApiClient.checkPermissions(
186
- profile,
187
- internalAuthToken,
188
- userId,
189
- scopedActions
190
- );
191
- usedGraphApi = false;
192
- }
193
- } else {
194
- const profile = this.getProfile(accountId, userId);
195
- scopedActionResponseObjects = await PlatformApiClient.checkPermissions(
196
- profile,
197
- internalAuthToken,
198
- userId,
199
- scopedActions
200
- );
201
- usedGraphApi = false;
202
- }
203
-
204
- const endTime = performance.now();
205
- const time = endTime - startTime;
206
- const apiType = usedGraphApi ? 'graph' : 'platform';
207
-
208
- // Record metrics for each authorization check
209
- for (const obj of scopedActionResponseObjects) {
210
- const { action, scope } = obj.scopedAction;
211
- const { resourceType } = scopeToResource(scope);
212
- const isAuthorized = obj.permit.can;
213
- sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, 200, time, apiType);
214
- if (obj.permit.can) {
215
- incrementAuthorizationSuccess(resourceType, action);
216
- }
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
- 'platform'
298
- );
299
- });
300
-
301
- if (unauthorizedObjects.length > 0) {
302
- logger.info(
303
- {
304
- resources: JSON.stringify(unauthorizedObjects),
305
- },
306
- 'AuthorizationService: resource is unauthorized'
307
- );
308
- const unauthorizedIds = unauthorizedObjects
309
- .filter(obj => !!obj.resource_id)
310
- .map(obj => obj.resource_id) as number[];
311
- return { isAuthorized: false, unauthorizedIds, unauthorizedObjects };
312
- }
313
-
314
- return { isAuthorized: true };
315
- }
316
- }
317
-
318
- export function setRedisClient(
319
- client,
320
- grantedFeatureRedisExpirationInSeconds: number = GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS
321
- ) {
322
- AuthorizationService.redisClient = client;
323
- if (grantedFeatureRedisExpirationInSeconds && grantedFeatureRedisExpirationInSeconds > 0) {
324
- AuthorizationService.grantedFeatureRedisExpirationInSeconds = grantedFeatureRedisExpirationInSeconds;
325
- } else {
326
- logger.warn(
327
- { grantedFeatureRedisExpirationInSeconds },
328
- 'Invalid input for grantedFeatureRedisExpirationInSeconds, must be positive number. using default ttl.'
329
- );
330
- AuthorizationService.grantedFeatureRedisExpirationInSeconds = GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS;
331
- }
332
- }
333
-
334
- export async function setIgniteClient() {
335
- const igniteClient = await getIgniteClient({
336
- namespace: ['authorization-sdk'],
337
- });
338
- AuthorizationService.igniteClient = igniteClient;
339
- AuthorizationInternalService.setIgniteClient(igniteClient);
340
- }
341
-
342
- export function createAuthorizationParams(resources: Resource[], action: Action): AuthorizationParams {
343
- const params = {
344
- authorizationObjects: resources.map((resource: Resource) => {
345
- const authorizationObject: AuthorizationObject = {
346
- resource_id: resource.id,
347
- resource_type: resource.type,
348
- action,
349
- };
350
- if (resource.wrapperData) {
351
- authorizationObject.wrapper_data = resource.wrapperData;
352
- }
353
- return authorizationObject;
354
- }),
355
- };
356
- return params;
357
- }
@@ -1,134 +0,0 @@
1
- import { Api } from '@mondaydotcomorg/trident-backend-api';
2
- import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
3
- import { ScopedAction, ScopedActionResponseObject, ScopedActionPermit } from '../types/scoped-actions-contracts';
4
- import { AuthorizationInternalService } from '../authorization-internal-service';
5
- import { getAttributionsFromApi } from '../attributions-service';
6
- import {
7
- GraphIsAllowedDto,
8
- GraphIsAllowedResponse,
9
- ResourceType,
10
- ResourceId,
11
- ActionName,
12
- } from '../types/graph-api.types';
13
- import { scopeToResource } from '../utils/authorization.utils';
14
- import { incrementAuthorizationError, setGraphAvailability } from '../prometheus-service';
15
-
16
- const CAN_ACTION_IN_SCOPE_GRAPH_PATH = '/permissions/is-allowed';
17
-
18
- /**
19
- * Client for handling Graph API authorization operations
20
- */
21
- export class GraphApiClient {
22
- /**
23
- * Builds the request body for Graph API calls
24
- */
25
- static buildRequestBody(scopedActions: ScopedAction[]): GraphIsAllowedDto {
26
- const resourcesAccumulator: Record<ResourceType, Record<ResourceId, Set<ActionName>>> = {};
27
- for (const { action, scope } of scopedActions) {
28
- const { resourceType, resourceId } = scopeToResource(scope);
29
- if (!resourcesAccumulator[resourceType]) {
30
- resourcesAccumulator[resourceType] = {};
31
- }
32
- if (!resourcesAccumulator[resourceType][resourceId]) {
33
- resourcesAccumulator[resourceType][resourceId] = new Set<ActionName>();
34
- }
35
- resourcesAccumulator[resourceType][resourceId].add(action);
36
- }
37
-
38
- const resourcesPayload: GraphIsAllowedDto = {};
39
- for (const [resourceType, idMap] of Object.entries(resourcesAccumulator)) {
40
- resourcesPayload[resourceType] = {};
41
- for (const [idStr, actionsSet] of Object.entries(idMap)) {
42
- const idNum = Number(idStr);
43
- resourcesPayload[resourceType][idNum] = Array.from(actionsSet);
44
- }
45
- }
46
-
47
- return resourcesPayload;
48
- }
49
-
50
- /**
51
- * Fetches authorization data from the Graph API
52
- */
53
- static async fetchPermissions(
54
- internalAuthToken: string,
55
- scopedActions: ScopedAction[]
56
- ): Promise<GraphIsAllowedResponse> {
57
- const httpClient = Api.getPart('httpClient');
58
- const attributionHeaders = getAttributionsFromApi();
59
- const bodyPayload = this.buildRequestBody(scopedActions);
60
-
61
-
62
- try {
63
- const response = await httpClient!.fetch<GraphIsAllowedResponse>(
64
- {
65
- url: {
66
- appName: 'authorization-graph',
67
- path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
68
- },
69
- method: 'POST',
70
- headers: {
71
- Authorization: internalAuthToken,
72
- 'Content-Type': 'application/json',
73
- ...attributionHeaders,
74
- },
75
- body: JSON.stringify(bodyPayload),
76
- },
77
- {
78
- timeout: AuthorizationInternalService.getRequestTimeout(),
79
- retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
80
- }
81
- );
82
-
83
-
84
- setGraphAvailability(true);
85
- return response;
86
- } catch (err) {
87
- setGraphAvailability(false);
88
- if (err instanceof HttpFetcherError) {
89
- AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
90
- incrementAuthorizationError(
91
- scopeToResource(scopedActions[0].scope).resourceType,
92
- scopedActions[0].action,
93
- err.status
94
- );
95
- }
96
- throw err;
97
- }
98
- }
99
-
100
- /**
101
- * Maps Graph API response to the expected format
102
- */
103
- static mapResponse(
104
- scopedActions: ScopedAction[],
105
- graphResponse: GraphIsAllowedResponse
106
- ): ScopedActionResponseObject[] {
107
- const resources = graphResponse ?? {};
108
-
109
- return scopedActions.map(scopedAction => {
110
- const { action, scope } = scopedAction;
111
- const { resourceType, resourceId } = scopeToResource(scope);
112
- const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
113
-
114
- const permit: ScopedActionPermit = {
115
- can: permissionResult?.can ?? false,
116
- reason: { key: permissionResult?.reason ?? 'unknown' },
117
- technicalReason: 0,
118
- };
119
-
120
- return { scopedAction, permit };
121
- });
122
- }
123
-
124
- /**
125
- * Performs a complete authorization check using the Graph API
126
- */
127
- static async checkPermissions(
128
- internalAuthToken: string,
129
- scopedActions: ScopedAction[]
130
- ): Promise<ScopedActionResponseObject[]> {
131
- const response = await this.fetchPermissions(internalAuthToken, scopedActions);
132
- return this.mapResponse(scopedActions, response);
133
- }
134
- }