@mondaydotcomorg/monday-authorization 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-752f21a → 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-13ad8e0

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 (38) hide show
  1. package/dist/authorization-service.d.ts.map +1 -1
  2. package/dist/authorization-service.js +0 -9
  3. package/dist/clients/graph-api.client.d.ts.map +1 -1
  4. package/dist/clients/graph-api.client.js +1 -21
  5. package/dist/clients/platform-api.client.d.ts.map +1 -1
  6. package/dist/clients/platform-api.client.js +1 -24
  7. package/dist/esm/authorization-service.d.ts.map +1 -1
  8. package/dist/esm/authorization-service.mjs +0 -9
  9. package/dist/esm/clients/graph-api.client.d.ts.map +1 -1
  10. package/dist/esm/clients/graph-api.client.mjs +2 -22
  11. package/dist/esm/clients/platform-api.client.d.ts.map +1 -1
  12. package/dist/esm/clients/platform-api.client.mjs +2 -25
  13. package/dist/esm/utils/authorization.utils.d.ts.map +1 -1
  14. package/dist/esm/utils/authorization.utils.mjs +0 -12
  15. package/dist/utils/authorization.utils.d.ts.map +1 -1
  16. package/dist/utils/authorization.utils.js +0 -12
  17. package/package.json +2 -7
  18. package/DEBUG.md +0 -203
  19. package/src/attributions-service.ts +0 -92
  20. package/src/authorization-attributes-service.ts +0 -234
  21. package/src/authorization-internal-service.ts +0 -129
  22. package/src/authorization-middleware.ts +0 -51
  23. package/src/authorization-service.ts +0 -384
  24. package/src/clients/graph-api.client.ts +0 -164
  25. package/src/clients/platform-api.client.ts +0 -151
  26. package/src/constants/sns.ts +0 -5
  27. package/src/constants.ts +0 -22
  28. package/src/index.ts +0 -46
  29. package/src/prometheus-service.ts +0 -147
  30. package/src/roles-service.ts +0 -125
  31. package/src/testKit/index.ts +0 -69
  32. package/src/types/authorization-attributes-contracts.ts +0 -33
  33. package/src/types/express.ts +0 -8
  34. package/src/types/general.ts +0 -32
  35. package/src/types/graph-api.types.ts +0 -19
  36. package/src/types/roles.ts +0 -42
  37. package/src/types/scoped-actions-contracts.ts +0 -48
  38. package/src/utils/authorization.utils.ts +0 -66
@@ -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,384 +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
- logger.debug(
158
- { tag: 'authorization-service', accountId, userId, scopedActionsCount: scopedActions.length },
159
- 'canActionInScopeMultiple called'
160
- );
161
-
162
- const shouldNavigateToGraph = Boolean(
163
- this.igniteClient?.isReleased(NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF, { accountId, userId })
164
- );
165
-
166
- logger.debug(
167
- { tag: 'authorization-service', accountId, userId, shouldNavigateToGraph },
168
- `Graph API routing feature flag: ${shouldNavigateToGraph ? 'ENABLED' : 'DISABLED'}`
169
- );
170
-
171
- const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
172
-
173
- const startTime = performance.now();
174
- let scopedActionResponseObjects: ScopedActionResponseObject[];
175
- let usedGraphApi = false;
176
-
177
- if (shouldNavigateToGraph) {
178
- logger.debug({ tag: 'authorization-service', accountId, userId }, 'Attempting Graph API authorization');
179
- try {
180
- scopedActionResponseObjects = await GraphApiClient.checkPermissions(internalAuthToken, scopedActions);
181
- usedGraphApi = true;
182
- logger.debug(
183
- { tag: 'authorization-service', accountId, userId, resultCount: scopedActionResponseObjects.length },
184
- 'Graph API authorization successful'
185
- );
186
- } catch (error) {
187
- // Fallback to Platform API if Graph API fails
188
- logger.warn(
189
- {
190
- tag: 'authorization-service',
191
- error: error instanceof Error ? error.message : String(error),
192
- accountId,
193
- userId,
194
- },
195
- 'Graph API authorization failed, falling back to Platform API'
196
- );
197
- logger.debug({ tag: 'authorization-service', accountId, userId }, 'Starting Platform API fallback');
198
- const profile = this.getProfile(accountId, userId);
199
- logger.debug(
200
- { tag: 'authorization-service', accountId, userId, profile },
201
- 'Retrieved Platform API profile for fallback'
202
- );
203
- scopedActionResponseObjects = await PlatformApiClient.checkPermissions(
204
- profile,
205
- internalAuthToken,
206
- userId,
207
- scopedActions
208
- );
209
- usedGraphApi = false;
210
- logger.debug(
211
- { tag: 'authorization-service', accountId, userId, resultCount: scopedActionResponseObjects.length },
212
- 'Platform API fallback successful'
213
- );
214
- }
215
- } else {
216
- logger.debug(
217
- { tag: 'authorization-service', accountId, userId },
218
- 'Using Platform API directly (Graph API FF disabled)'
219
- );
220
- const profile = this.getProfile(accountId, userId);
221
- logger.debug({ tag: 'authorization-service', accountId, userId, profile }, 'Retrieved Platform API profile');
222
- scopedActionResponseObjects = await PlatformApiClient.checkPermissions(
223
- profile,
224
- internalAuthToken,
225
- userId,
226
- scopedActions
227
- );
228
- usedGraphApi = false;
229
- }
230
-
231
- const endTime = performance.now();
232
- const time = endTime - startTime;
233
- const apiType = usedGraphApi ? 'graph' : 'platform';
234
-
235
- // Record metrics for each authorization check
236
- for (const obj of scopedActionResponseObjects) {
237
- const { action, scope } = obj.scopedAction;
238
- const { resourceType } = scopeToResource(scope);
239
- const isAuthorized = obj.permit.can;
240
- sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, 200, time, apiType);
241
- if (obj.permit.can) {
242
- incrementAuthorizationSuccess(resourceType, action);
243
- }
244
- }
245
-
246
- return scopedActionResponseObjects;
247
- }
248
-
249
- private static async isAuthorizedSingular(
250
- accountId: number,
251
- userId: number,
252
- resources: Resource[],
253
- action: Action
254
- ): Promise<AuthorizeResponse> {
255
- const { authorizationObjects } = createAuthorizationParams(resources, action);
256
- return this.isAuthorizedMultiple(accountId, userId, authorizationObjects);
257
- }
258
-
259
- private static async isAuthorizedMultiple(
260
- accountId: number,
261
- userId: number,
262
- authorizationRequestObjects: AuthorizationObject[]
263
- ): Promise<AuthorizeResponse> {
264
- const profile = this.getProfile(accountId, userId);
265
- const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
266
- const startTime = performance.now();
267
- const attributionHeaders = getAttributionsFromApi();
268
- const httpClient = Api.getPart('httpClient');
269
-
270
- let response: IsAuthorizedResponse | undefined;
271
- try {
272
- response = await httpClient!.fetch<IsAuthorizedResponse>(
273
- {
274
- url: {
275
- appName: 'platform',
276
- path: PLATFORM_AUTHORIZE_PATH,
277
- profile,
278
- },
279
- method: 'POST',
280
- headers: {
281
- Authorization: internalAuthToken,
282
- 'Content-Type': 'application/json',
283
- ...attributionHeaders,
284
- },
285
- body: JSON.stringify({
286
- user_id: userId,
287
- authorize_request_objects: authorizationRequestObjects,
288
- }),
289
- },
290
- {
291
- timeout: AuthorizationInternalService.getRequestTimeout(),
292
- retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
293
- }
294
- );
295
- } catch (err) {
296
- if (err instanceof HttpFetcherError) {
297
- AuthorizationInternalService.throwOnHttpError((err as HttpFetcherError).status, 'isAuthorizedMultiple');
298
- } else {
299
- throw err;
300
- }
301
- }
302
- const endTime = performance.now();
303
- const time = endTime - startTime;
304
-
305
- const unauthorizedObjects: AuthorizationObject[] = [];
306
-
307
- if (!response) {
308
- logger.error({ tag: 'authorization-service', response }, 'AuthorizationService: missing response');
309
- throw new Error('AuthorizationService: missing response');
310
- }
311
-
312
- response.result.forEach(function (isAuthorized, index) {
313
- const authorizationObject = authorizationRequestObjects[index];
314
- if (!isAuthorized) {
315
- unauthorizedObjects.push(authorizationObject);
316
- }
317
-
318
- sendAuthorizationCheckResponseTimeMetric(
319
- authorizationObject.resource_type,
320
- authorizationObject.action,
321
- isAuthorized,
322
- 200,
323
- time,
324
- 'platform'
325
- );
326
- });
327
-
328
- if (unauthorizedObjects.length > 0) {
329
- logger.info(
330
- {
331
- resources: JSON.stringify(unauthorizedObjects),
332
- },
333
- 'AuthorizationService: resource is unauthorized'
334
- );
335
- const unauthorizedIds = unauthorizedObjects
336
- .filter(obj => !!obj.resource_id)
337
- .map(obj => obj.resource_id) as number[];
338
- return { isAuthorized: false, unauthorizedIds, unauthorizedObjects };
339
- }
340
-
341
- return { isAuthorized: true };
342
- }
343
- }
344
-
345
- export function setRedisClient(
346
- client,
347
- grantedFeatureRedisExpirationInSeconds: number = GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS
348
- ) {
349
- AuthorizationService.redisClient = client;
350
- if (grantedFeatureRedisExpirationInSeconds && grantedFeatureRedisExpirationInSeconds > 0) {
351
- AuthorizationService.grantedFeatureRedisExpirationInSeconds = grantedFeatureRedisExpirationInSeconds;
352
- } else {
353
- logger.warn(
354
- { grantedFeatureRedisExpirationInSeconds },
355
- 'Invalid input for grantedFeatureRedisExpirationInSeconds, must be positive number. using default ttl.'
356
- );
357
- AuthorizationService.grantedFeatureRedisExpirationInSeconds = GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS;
358
- }
359
- }
360
-
361
- export async function setIgniteClient() {
362
- const igniteClient = await getIgniteClient({
363
- namespace: ['authorization-sdk'],
364
- });
365
- AuthorizationService.igniteClient = igniteClient;
366
- AuthorizationInternalService.setIgniteClient(igniteClient);
367
- }
368
-
369
- export function createAuthorizationParams(resources: Resource[], action: Action): AuthorizationParams {
370
- const params = {
371
- authorizationObjects: resources.map((resource: Resource) => {
372
- const authorizationObject: AuthorizationObject = {
373
- resource_id: resource.id,
374
- resource_type: resource.type,
375
- action,
376
- };
377
- if (resource.wrapperData) {
378
- authorizationObject.wrapper_data = resource.wrapperData;
379
- }
380
- return authorizationObject;
381
- }),
382
- };
383
- return params;
384
- }
@@ -1,164 +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, logger } 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
- logger.debug(
62
- {
63
- tag: 'graph-api-client',
64
- scopedActionsCount: scopedActions.length,
65
- appName: 'authorization-graph',
66
- path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
67
- timeout: AuthorizationInternalService.getRequestTimeout(),
68
- retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
69
- bodyPayloadKeys: Object.keys(bodyPayload),
70
- },
71
- '🔍 Graph API Debug: Starting request'
72
- );
73
-
74
- try {
75
- const response = await httpClient!.fetch<GraphIsAllowedResponse>(
76
- {
77
- url: {
78
- appName: 'authorization-graph',
79
- path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
80
- },
81
- method: 'POST',
82
- headers: {
83
- Authorization: internalAuthToken.substring(0, 20) + '...', // Mask token for security
84
- 'Content-Type': 'application/json',
85
- ...attributionHeaders,
86
- },
87
- body: JSON.stringify(bodyPayload),
88
- },
89
- {
90
- timeout: AuthorizationInternalService.getRequestTimeout(),
91
- retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
92
- }
93
- );
94
-
95
- logger.debug(
96
- {
97
- tag: 'graph-api-client',
98
- responseKeys: Object.keys(response),
99
- scopedActionsCount: scopedActions.length,
100
- },
101
- '✅ Graph API Debug: Request successful'
102
- );
103
-
104
- setGraphAvailability(true);
105
- return response;
106
- } catch (err) {
107
- logger.debug(
108
- {
109
- tag: 'graph-api-client',
110
- error: err instanceof Error ? err.message : String(err),
111
- status: err instanceof HttpFetcherError ? err.status : 'unknown',
112
- scopedActionsCount: scopedActions.length,
113
- },
114
- '❌ Graph API Debug: Request failed'
115
- );
116
-
117
- setGraphAvailability(false);
118
- if (err instanceof HttpFetcherError) {
119
- AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
120
- incrementAuthorizationError(
121
- scopeToResource(scopedActions[0].scope).resourceType,
122
- scopedActions[0].action,
123
- err.status
124
- );
125
- }
126
- throw err;
127
- }
128
- }
129
-
130
- /**
131
- * Maps Graph API response to the expected format
132
- */
133
- static mapResponse(
134
- scopedActions: ScopedAction[],
135
- graphResponse: GraphIsAllowedResponse
136
- ): ScopedActionResponseObject[] {
137
- const resources = graphResponse ?? {};
138
-
139
- return scopedActions.map(scopedAction => {
140
- const { action, scope } = scopedAction;
141
- const { resourceType, resourceId } = scopeToResource(scope);
142
- const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
143
-
144
- const permit: ScopedActionPermit = {
145
- can: permissionResult?.can ?? false,
146
- reason: { key: permissionResult?.reason ?? 'unknown' },
147
- technicalReason: 0,
148
- };
149
-
150
- return { scopedAction, permit };
151
- });
152
- }
153
-
154
- /**
155
- * Performs a complete authorization check using the Graph API
156
- */
157
- static async checkPermissions(
158
- internalAuthToken: string,
159
- scopedActions: ScopedAction[]
160
- ): Promise<ScopedActionResponseObject[]> {
161
- const response = await this.fetchPermissions(internalAuthToken, scopedActions);
162
- return this.mapResponse(scopedActions, response);
163
- }
164
- }