@mondaydotcomorg/monday-authorization 3.2.3 → 3.3.0-feat-add-graph-api-routing-support-cb899c0

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 (40) hide show
  1. package/README.md +29 -0
  2. package/dist/authorization-service.d.ts.map +1 -1
  3. package/dist/authorization-service.js +43 -61
  4. package/dist/clients/graph-api.client.d.ts +24 -0
  5. package/dist/clients/graph-api.client.d.ts.map +1 -0
  6. package/dist/clients/graph-api.client.js +123 -0
  7. package/dist/clients/platform-api.client.d.ts +31 -0
  8. package/dist/clients/platform-api.client.d.ts.map +1 -0
  9. package/dist/clients/platform-api.client.js +89 -0
  10. package/dist/esm/authorization-service.d.ts.map +1 -1
  11. package/dist/esm/authorization-service.mjs +44 -56
  12. package/dist/esm/clients/graph-api.client.d.ts +24 -0
  13. package/dist/esm/clients/graph-api.client.d.ts.map +1 -0
  14. package/dist/esm/clients/graph-api.client.mjs +121 -0
  15. package/dist/esm/clients/platform-api.client.d.ts +31 -0
  16. package/dist/esm/clients/platform-api.client.d.ts.map +1 -0
  17. package/dist/esm/clients/platform-api.client.mjs +87 -0
  18. package/dist/esm/prometheus-service.d.ts +3 -1
  19. package/dist/esm/prometheus-service.d.ts.map +1 -1
  20. package/dist/esm/prometheus-service.mjs +61 -5
  21. package/dist/esm/testKit/index.d.ts.map +1 -1
  22. package/dist/esm/testKit/index.mjs +9 -7
  23. package/dist/esm/types/graph-api.types.d.ts +15 -0
  24. package/dist/esm/types/graph-api.types.d.ts.map +1 -0
  25. package/dist/esm/types/graph-api.types.mjs +1 -0
  26. package/dist/esm/utils/authorization.utils.d.ts +22 -0
  27. package/dist/esm/utils/authorization.utils.d.ts.map +1 -0
  28. package/dist/esm/utils/authorization.utils.mjs +39 -0
  29. package/dist/prometheus-service.d.ts +3 -1
  30. package/dist/prometheus-service.d.ts.map +1 -1
  31. package/dist/prometheus-service.js +62 -4
  32. package/dist/testKit/index.d.ts.map +1 -1
  33. package/dist/testKit/index.js +9 -7
  34. package/dist/types/graph-api.types.d.ts +15 -0
  35. package/dist/types/graph-api.types.d.ts.map +1 -0
  36. package/dist/types/graph-api.types.js +1 -0
  37. package/dist/utils/authorization.utils.d.ts +22 -0
  38. package/dist/utils/authorization.utils.d.ts.map +1 -0
  39. package/dist/utils/authorization.utils.js +49 -0
  40. package/package.json +1 -1
@@ -1,20 +1,20 @@
1
1
  import { performance } from 'perf_hooks';
2
- import snakeCase from 'lodash/snakeCase.js';
3
- import camelCase from 'lodash/camelCase.js';
4
- import mapKeys from 'lodash/mapKeys.js';
5
2
  import { Api } from '@mondaydotcomorg/trident-backend-api';
6
3
  import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
7
4
  import { getIgniteClient } from '@mondaydotcomorg/ignite-sdk';
8
- import { sendAuthorizationCheckResponseTimeMetric } from './prometheus-service.mjs';
5
+ import { sendAuthorizationCheckResponseTimeMetric, incrementAuthorizationSuccess } from './prometheus-service.mjs';
9
6
  import { AuthorizationInternalService, logger } from './authorization-internal-service.mjs';
10
7
  import { getProfile, PlatformProfile, getAttributionsFromApi } from './attributions-service.mjs';
8
+ import { GraphApiClient } from './clients/graph-api.client.mjs';
9
+ import { PlatformApiClient } from './clients/platform-api.client.mjs';
10
+ import { scopeToResource } from './utils/authorization.utils.mjs';
11
11
 
12
12
  const GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS = 5 * 60;
13
13
  const PLATFORM_AUTHORIZE_PATH = '/internal_ms/authorization/authorize';
14
- const PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH = '/internal_ms/authorization/can_actions_in_scopes';
15
14
  const ALLOWED_SDK_PLATFORM_PROFILES_KEY = 'allowed-sdk-platform-profiles';
16
15
  const IN_RELEASE_SDK_PLATFORM_PROFILES_KEY = 'in-release-sdk-platform-profile';
17
16
  const PLATFORM_PROFILE_RELEASE_FF = 'sdk-platform-profiles';
17
+ const NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF = 'navigate-can-action-in-scope-to-graph';
18
18
  function setRequestFetchOptions(customMondayFetchOptions) {
19
19
  AuthorizationInternalService.setRequestFetchOptions(customMondayFetchOptions);
20
20
  }
@@ -87,61 +87,49 @@ class AuthorizationService {
87
87
  return PlatformProfile.INTERNAL;
88
88
  }
89
89
  static async canActionInScopeMultiple(accountId, userId, scopedActions) {
90
- const profile = this.getProfile(accountId, userId);
91
- const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
92
- const scopedActionsPayload = scopedActions.map(scopedAction => {
93
- return { ...scopedAction, scope: mapKeys(scopedAction.scope, (_, key) => snakeCase(key)) }; // for example: { workspaceId: 1 } => { workspace_id: 1 }
94
- });
95
- const attributionHeaders = getAttributionsFromApi();
96
- const httpClient = Api.getPart('httpClient');
97
- let response;
98
- try {
99
- response = await httpClient.fetch({
100
- url: {
101
- appName: 'platform',
102
- path: PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH,
103
- profile,
104
- },
105
- method: 'POST',
106
- headers: {
107
- Authorization: internalAuthToken,
108
- 'Content-Type': 'application/json',
109
- ...attributionHeaders,
110
- },
111
- body: JSON.stringify({
112
- user_id: userId,
113
- scoped_actions: scopedActionsPayload,
114
- }),
115
- }, {
116
- timeout: AuthorizationInternalService.getRequestTimeout(),
117
- retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
118
- });
90
+ if (scopedActions.length === 0) {
91
+ return [];
119
92
  }
120
- catch (err) {
121
- if (err instanceof HttpFetcherError) {
122
- AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
93
+ const shouldNavigateToGraph = Boolean(this.igniteClient?.isReleased(NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF, { accountId, userId }));
94
+ const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
95
+ const startTime = performance.now();
96
+ let scopedActionResponseObjects;
97
+ let apiType;
98
+ if (shouldNavigateToGraph) {
99
+ try {
100
+ scopedActionResponseObjects = await GraphApiClient.checkPermissions(internalAuthToken, scopedActions);
101
+ apiType = 'graph';
123
102
  }
124
- else {
125
- throw err;
103
+ catch (error) {
104
+ const status = error instanceof HttpFetcherError ? error.status : undefined;
105
+ logger.warn({
106
+ tag: 'authorization-service',
107
+ error: error instanceof Error ? error.message : String(error),
108
+ accountId,
109
+ userId,
110
+ status,
111
+ }, 'Graph API authorization failed');
112
+ throw error;
126
113
  }
127
114
  }
128
- function toCamelCase(obj) {
129
- return mapKeys(obj, (_, key) => camelCase(key));
115
+ else {
116
+ const profile = this.getProfile(accountId, userId);
117
+ scopedActionResponseObjects = await PlatformApiClient.checkPermissions(profile, internalAuthToken, userId, scopedActions);
118
+ apiType = 'platform';
130
119
  }
131
- if (!response) {
132
- logger.error({ tag: 'authorization-service', response }, 'AuthorizationService: missing response');
133
- throw new Error('AuthorizationService: missing response');
120
+ const endTime = performance.now();
121
+ const time = endTime - startTime;
122
+ // Record metrics for each authorization check
123
+ for (const obj of scopedActionResponseObjects) {
124
+ const { action, scope } = obj.scopedAction;
125
+ const { resourceType } = scopeToResource(scope);
126
+ const isAuthorized = obj.permit.can;
127
+ sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, 200, time, apiType);
128
+ if (obj.permit.can) {
129
+ incrementAuthorizationSuccess(resourceType, action, apiType);
130
+ }
134
131
  }
135
- const scopedActionsResponseObjects = response.result.map(responseObject => {
136
- const { scopedAction, permit } = responseObject;
137
- const { scope } = scopedAction;
138
- return {
139
- ...responseObject,
140
- scopedAction: { ...scopedAction, scope: toCamelCase(scope) },
141
- permit: toCamelCase(permit),
142
- };
143
- });
144
- return scopedActionsResponseObjects;
132
+ return scopedActionResponseObjects;
145
133
  }
146
134
  static async isAuthorizedSingular(accountId, userId, resources, action) {
147
135
  const { authorizationObjects } = createAuthorizationParams(resources, action);
@@ -177,7 +165,7 @@ class AuthorizationService {
177
165
  });
178
166
  }
179
167
  catch (err) {
180
- if (err instanceof httpClient.HttpFetcherError) {
168
+ if (err instanceof HttpFetcherError) {
181
169
  AuthorizationInternalService.throwOnHttpError(err.status, 'isAuthorizedMultiple');
182
170
  }
183
171
  else {
@@ -196,7 +184,7 @@ class AuthorizationService {
196
184
  if (!isAuthorized) {
197
185
  unauthorizedObjects.push(authorizationObject);
198
186
  }
199
- sendAuthorizationCheckResponseTimeMetric(authorizationObject.resource_type, authorizationObject.action, isAuthorized, 200, time);
187
+ sendAuthorizationCheckResponseTimeMetric(authorizationObject.resource_type, authorizationObject.action, isAuthorized, 200, time, 'platform');
200
188
  });
201
189
  if (unauthorizedObjects.length > 0) {
202
190
  logger.info({
@@ -0,0 +1,24 @@
1
+ import { ScopedAction, ScopedActionResponseObject } from '../types/scoped-actions-contracts';
2
+ import { GraphIsAllowedDto, GraphIsAllowedResponse } from '../types/graph-api.types';
3
+ /**
4
+ * Client for handling Graph API authorization operations
5
+ */
6
+ export declare class GraphApiClient {
7
+ /**
8
+ * Builds the request body for Graph API calls
9
+ */
10
+ static buildRequestBody(scopedActions: ScopedAction[]): GraphIsAllowedDto;
11
+ /**
12
+ * Fetches authorization data from the Graph API
13
+ */
14
+ static fetchPermissions(internalAuthToken: string, scopedActions: ScopedAction[]): Promise<GraphIsAllowedResponse>;
15
+ /**
16
+ * Maps Graph API response to the expected format
17
+ */
18
+ static mapResponse(scopedActions: ScopedAction[], graphResponse: GraphIsAllowedResponse): ScopedActionResponseObject[];
19
+ /**
20
+ * Performs a complete authorization check using the Graph API
21
+ */
22
+ static checkPermissions(internalAuthToken: string, scopedActions: ScopedAction[]): Promise<ScopedActionResponseObject[]>;
23
+ }
24
+ //# sourceMappingURL=graph-api.client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-api.client.d.ts","sourceRoot":"","sources":["../../../src/clients/graph-api.client.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,0BAA0B,EAG3B,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EAIvB,MAAM,0BAA0B,CAAC;AAMlC;;GAEG;AACH,qBAAa,cAAc;IACzB;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,iBAAiB;IAyBzE;;OAEG;WACU,gBAAgB,CAC3B,iBAAiB,EAAE,MAAM,EACzB,aAAa,EAAE,YAAY,EAAE,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IA2ClC;;OAEG;IACH,MAAM,CAAC,WAAW,CAChB,aAAa,EAAE,YAAY,EAAE,EAC7B,aAAa,EAAE,sBAAsB,GACpC,0BAA0B,EAAE;IAsC/B;;OAEG;WACU,gBAAgB,CAC3B,iBAAiB,EAAE,MAAM,EACzB,aAAa,EAAE,YAAY,EAAE,GAC5B,OAAO,CAAC,0BAA0B,EAAE,CAAC;CAIzC"}
@@ -0,0 +1,121 @@
1
+ import { Api } from '@mondaydotcomorg/trident-backend-api';
2
+ import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
3
+ import { PermitTechnicalReason } from '../types/scoped-actions-contracts.mjs';
4
+ import { AuthorizationInternalService } from '../authorization-internal-service.mjs';
5
+ import { getAttributionsFromApi } from '../attributions-service.mjs';
6
+ import { scopeToResource } from '../utils/authorization.utils.mjs';
7
+ import { incrementAuthorizationError } from '../prometheus-service.mjs';
8
+
9
+ const CAN_ACTION_IN_SCOPE_GRAPH_PATH = '/permissions/is-allowed';
10
+ /**
11
+ * Client for handling Graph API authorization operations
12
+ */
13
+ class GraphApiClient {
14
+ /**
15
+ * Builds the request body for Graph API calls
16
+ */
17
+ static buildRequestBody(scopedActions) {
18
+ const resourcesAccumulator = {};
19
+ for (const { action, scope } of scopedActions) {
20
+ const { resourceType, resourceId } = scopeToResource(scope);
21
+ if (!resourcesAccumulator[resourceType]) {
22
+ resourcesAccumulator[resourceType] = {};
23
+ }
24
+ if (!resourcesAccumulator[resourceType][resourceId]) {
25
+ resourcesAccumulator[resourceType][resourceId] = new Set();
26
+ }
27
+ resourcesAccumulator[resourceType][resourceId].add(action);
28
+ }
29
+ const resourcesPayload = {};
30
+ for (const [resourceType, idMap] of Object.entries(resourcesAccumulator)) {
31
+ resourcesPayload[resourceType] = {};
32
+ for (const [idStr, actionsSet] of Object.entries(idMap)) {
33
+ const idNum = Number(idStr);
34
+ resourcesPayload[resourceType][idNum] = Array.from(actionsSet);
35
+ }
36
+ }
37
+ return resourcesPayload;
38
+ }
39
+ /**
40
+ * Fetches authorization data from the Graph API
41
+ */
42
+ static async fetchPermissions(internalAuthToken, scopedActions) {
43
+ const httpClient = Api.getPart('httpClient');
44
+ const attributionHeaders = getAttributionsFromApi();
45
+ const bodyPayload = this.buildRequestBody(scopedActions);
46
+ try {
47
+ const response = await httpClient.fetch({
48
+ url: {
49
+ appName: 'authorization-graph',
50
+ path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
51
+ },
52
+ method: 'POST',
53
+ headers: {
54
+ Authorization: internalAuthToken,
55
+ 'Content-Type': 'application/json',
56
+ ...attributionHeaders,
57
+ },
58
+ body: JSON.stringify(bodyPayload),
59
+ }, {
60
+ timeout: AuthorizationInternalService.getRequestTimeout(),
61
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
62
+ });
63
+ return response;
64
+ }
65
+ catch (err) {
66
+ if (err instanceof HttpFetcherError) {
67
+ AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
68
+ if (scopedActions.length > 0) {
69
+ incrementAuthorizationError(scopeToResource(scopedActions[0].scope).resourceType, scopedActions[0].action, err.status, 'graph');
70
+ }
71
+ }
72
+ throw err;
73
+ }
74
+ }
75
+ /**
76
+ * Maps Graph API response to the expected format
77
+ */
78
+ static mapResponse(scopedActions, graphResponse) {
79
+ const resources = graphResponse ?? {};
80
+ return scopedActions.map(scopedAction => {
81
+ const { action, scope } = scopedAction;
82
+ const { resourceType, resourceId } = scopeToResource(scope);
83
+ const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
84
+ const graphReason = permissionResult?.reason;
85
+ let reasonKey;
86
+ let additionalOptions = {};
87
+ let technicalReason = PermitTechnicalReason.NO_REASON;
88
+ if (typeof graphReason === 'string') {
89
+ reasonKey = graphReason;
90
+ }
91
+ else if (graphReason && typeof graphReason === 'object') {
92
+ reasonKey = graphReason.key ?? 'unknown';
93
+ additionalOptions = graphReason.additionalOptions ?? {};
94
+ if (graphReason.technicalReason !== undefined) {
95
+ technicalReason = (graphReason.technicalReason ?? PermitTechnicalReason.NO_REASON);
96
+ }
97
+ }
98
+ else {
99
+ reasonKey = 'unknown';
100
+ }
101
+ const permit = {
102
+ can: permissionResult?.can ?? false,
103
+ reason: {
104
+ key: reasonKey,
105
+ ...additionalOptions,
106
+ },
107
+ technicalReason,
108
+ };
109
+ return { scopedAction, permit };
110
+ });
111
+ }
112
+ /**
113
+ * Performs a complete authorization check using the Graph API
114
+ */
115
+ static async checkPermissions(internalAuthToken, scopedActions) {
116
+ const response = await this.fetchPermissions(internalAuthToken, scopedActions);
117
+ return this.mapResponse(scopedActions, response);
118
+ }
119
+ }
120
+
121
+ export { GraphApiClient };
@@ -0,0 +1,31 @@
1
+ import { ScopedAction, ScopedActionResponseObject } from '../types/scoped-actions-contracts';
2
+ import { PlatformProfile } from '../attributions-service';
3
+ type ScopedActionPlatformPayload = Omit<ScopedAction, 'scope'> & {
4
+ scope: Record<string, number>;
5
+ };
6
+ interface CanActionsInScopesResponse {
7
+ result: ScopedActionResponseObject[];
8
+ }
9
+ /**
10
+ * Client for handling Platform API authorization operations
11
+ */
12
+ export declare class PlatformApiClient {
13
+ /**
14
+ * Builds the request payload for Platform API calls
15
+ */
16
+ static buildRequestPayload(scopedActions: ScopedAction[]): ScopedActionPlatformPayload[];
17
+ /**
18
+ * Fetches authorization data from the Platform API
19
+ */
20
+ static fetchPermissions(profile: PlatformProfile, internalAuthToken: string, userId: number, scopedActionsPayload: ScopedActionPlatformPayload[]): Promise<CanActionsInScopesResponse>;
21
+ /**
22
+ * Maps Platform API response to the expected format
23
+ */
24
+ static mapResponse(response: CanActionsInScopesResponse): ScopedActionResponseObject[];
25
+ /**
26
+ * Performs a complete authorization check using the Platform API
27
+ */
28
+ static checkPermissions(profile: PlatformProfile, internalAuthToken: string, userId: number, scopedActions: ScopedAction[]): Promise<ScopedActionResponseObject[]>;
29
+ }
30
+ export {};
31
+ //# sourceMappingURL=platform-api.client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-api.client.d.ts","sourceRoot":"","sources":["../../../src/clients/platform-api.client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAE7F,OAAO,EAA0B,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAOlF,KAAK,2BAA2B,GAAG,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG;IAC/D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF,UAAU,0BAA0B;IAClC,MAAM,EAAE,0BAA0B,EAAE,CAAC;CACtC;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAC5B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,2BAA2B,EAAE;IAOxF;;OAEG;WACU,gBAAgB,CAC3B,OAAO,EAAE,eAAe,EACxB,iBAAiB,EAAE,MAAM,EACzB,MAAM,EAAE,MAAM,EACd,oBAAoB,EAAE,2BAA2B,EAAE,GAClD,OAAO,CAAC,0BAA0B,CAAC;IAuCtC;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,0BAA0B,GAAG,0BAA0B,EAAE;IAkBtF;;OAEG;WACU,gBAAgB,CAC3B,OAAO,EAAE,eAAe,EACxB,iBAAiB,EAAE,MAAM,EACzB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,YAAY,EAAE,GAC5B,OAAO,CAAC,0BAA0B,EAAE,CAAC;CAKzC"}
@@ -0,0 +1,87 @@
1
+ import { Api } from '@mondaydotcomorg/trident-backend-api';
2
+ import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
3
+ import { AuthorizationInternalService, logger } from '../authorization-internal-service.mjs';
4
+ import { getAttributionsFromApi } from '../attributions-service.mjs';
5
+ import { toSnakeCase, scopeToResource, toCamelCase } from '../utils/authorization.utils.mjs';
6
+ import { incrementAuthorizationError } from '../prometheus-service.mjs';
7
+
8
+ const PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH = '/internal_ms/authorization/can_actions_in_scopes';
9
+ /**
10
+ * Client for handling Platform API authorization operations
11
+ */
12
+ class PlatformApiClient {
13
+ /**
14
+ * Builds the request payload for Platform API calls
15
+ */
16
+ static buildRequestPayload(scopedActions) {
17
+ return scopedActions.map(scopedAction => ({
18
+ ...scopedAction,
19
+ scope: toSnakeCase(scopedAction.scope),
20
+ }));
21
+ }
22
+ /**
23
+ * Fetches authorization data from the Platform API
24
+ */
25
+ static async fetchPermissions(profile, internalAuthToken, userId, scopedActionsPayload) {
26
+ const attributionHeaders = getAttributionsFromApi();
27
+ const httpClient = Api.getPart('httpClient');
28
+ try {
29
+ const response = await httpClient.fetch({
30
+ url: {
31
+ appName: 'platform',
32
+ path: PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH,
33
+ profile,
34
+ },
35
+ method: 'POST',
36
+ headers: {
37
+ Authorization: internalAuthToken,
38
+ 'Content-Type': 'application/json',
39
+ ...attributionHeaders,
40
+ },
41
+ body: JSON.stringify({ user_id: userId, scoped_actions: scopedActionsPayload }),
42
+ }, {
43
+ timeout: AuthorizationInternalService.getRequestTimeout(),
44
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
45
+ });
46
+ return response;
47
+ }
48
+ catch (err) {
49
+ if (err instanceof HttpFetcherError) {
50
+ AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
51
+ if (scopedActionsPayload.length > 0) {
52
+ const { resourceType } = scopeToResource(toCamelCase(scopedActionsPayload[0].scope));
53
+ incrementAuthorizationError(resourceType, scopedActionsPayload[0].action, err.status, 'platform');
54
+ }
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+ /**
60
+ * Maps Platform API response to the expected format
61
+ */
62
+ static mapResponse(response) {
63
+ if (!response) {
64
+ logger.error({ tag: 'platform-api-client', response }, 'PlatformApiClient: missing response');
65
+ throw new Error('PlatformApiClient: missing response');
66
+ }
67
+ return response.result.map(responseObject => {
68
+ const { scopedAction, permit } = responseObject;
69
+ const { scope } = scopedAction;
70
+ return {
71
+ ...responseObject,
72
+ scopedAction: { ...scopedAction, scope: toCamelCase(scope) },
73
+ permit: toCamelCase(permit),
74
+ };
75
+ });
76
+ }
77
+ /**
78
+ * Performs a complete authorization check using the Platform API
79
+ */
80
+ static async checkPermissions(profile, internalAuthToken, userId, scopedActions) {
81
+ const scopedActionsPayload = this.buildRequestPayload(scopedActions);
82
+ const platformResponse = await this.fetchPermissions(profile, internalAuthToken, userId, scopedActionsPayload);
83
+ return this.mapResponse(platformResponse);
84
+ }
85
+ }
86
+
87
+ export { PlatformApiClient };
@@ -6,5 +6,7 @@ export declare const METRICS: {
6
6
  };
7
7
  export declare function setPrometheus(customPrometheus: any): void;
8
8
  export declare function getMetricsManager(): any;
9
- export declare function sendAuthorizationCheckResponseTimeMetric(resourceType: string, action: Action, isAuthorized: boolean, responseStatus: number, time: number): void;
9
+ export declare function sendAuthorizationCheckResponseTimeMetric(resourceType: string, action: Action, isAuthorized: boolean, responseStatus: number, time: number, apiType?: 'platform' | 'graph'): void;
10
+ export declare function incrementAuthorizationSuccess(resourceType: string, action: Action, apiType: 'platform' | 'graph'): void;
11
+ export declare function incrementAuthorizationError(resourceType: string, action: Action, statusCode: number, apiType: 'platform' | 'graph'): void;
10
12
  //# sourceMappingURL=prometheus-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prometheus-service.d.ts","sourceRoot":"","sources":["../../src/prometheus-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAKzC,eAAO,MAAM,OAAO;;;;CAInB,CAAC;AAQF,wBAAgB,aAAa,CAAC,gBAAgB,KAAA,QAU7C;AAED,wBAAgB,iBAAiB,QAEhC;AAED,wBAAgB,wCAAwC,CACtD,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,QASb"}
1
+ {"version":3,"file":"prometheus-service.d.ts","sourceRoot":"","sources":["../../src/prometheus-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAOzC,eAAO,MAAM,OAAO;;;;CAInB,CAAC;AAQF,wBAAgB,aAAa,CAAC,gBAAgB,KAAA,QAqB7C;AAED,wBAAgB,iBAAiB,QAEhC;AAED,wBAAgB,wCAAwC,CACtD,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,UAAU,GAAG,OAAoB,QAW3C;AAcD,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,QAQhH;AAED,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,UAAU,GAAG,OAAO,QAS9B"}
@@ -1,5 +1,7 @@
1
1
  let prometheus = null;
2
2
  let authorizationCheckResponseTimeMetric = null;
3
+ let authorizationSuccessMetric = null;
4
+ let authorizationErrorMetric = null;
3
5
  const METRICS = {
4
6
  AUTHORIZATION_CHECK: 'authorization_check',
5
7
  AUTHORIZATION_CHECKS_PER_REQUEST: 'authorization_checks_per_request',
@@ -7,26 +9,80 @@ const METRICS = {
7
9
  };
8
10
  const authorizationCheckResponseTimeMetricConfig = {
9
11
  name: METRICS.AUTHORIZATION_CHECK_RESPONSE_TIME,
10
- labels: ['resourceType', 'action', 'isAuthorized', 'responseStatus'],
12
+ labels: ['resourceType', 'action', 'isAuthorized', 'responseStatus', 'apiType'],
11
13
  description: 'Authorization check response time summary',
12
14
  };
13
15
  function setPrometheus(customPrometheus) {
14
16
  prometheus = customPrometheus;
17
+ if (!prometheus) {
18
+ authorizationCheckResponseTimeMetric = null;
19
+ authorizationSuccessMetric = null;
20
+ authorizationErrorMetric = null;
21
+ return;
22
+ }
15
23
  const { METRICS_TYPES } = prometheus;
16
- authorizationCheckResponseTimeMetric = getMetricsManager().addMetric(METRICS_TYPES.SUMMARY, authorizationCheckResponseTimeMetricConfig.name, authorizationCheckResponseTimeMetricConfig.labels, authorizationCheckResponseTimeMetricConfig.description);
24
+ const metricsManager = getMetricsManager();
25
+ if (metricsManager) {
26
+ authorizationCheckResponseTimeMetric = metricsManager.addMetric(METRICS_TYPES.SUMMARY, authorizationCheckResponseTimeMetricConfig.name, authorizationCheckResponseTimeMetricConfig.labels, authorizationCheckResponseTimeMetricConfig.description);
27
+ initializeAdditionalMetrics();
28
+ }
17
29
  }
18
30
  function getMetricsManager() {
19
31
  return prometheus?.metricsManager;
20
32
  }
21
- function sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, responseStatus, time) {
33
+ function sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, responseStatus, time, apiType = 'platform') {
22
34
  try {
23
35
  if (authorizationCheckResponseTimeMetric) {
24
- authorizationCheckResponseTimeMetric.labels(resourceType, action, isAuthorized, responseStatus).observe(time);
36
+ authorizationCheckResponseTimeMetric
37
+ .labels(resourceType, action, isAuthorized, responseStatus, apiType)
38
+ .observe(time);
39
+ }
40
+ }
41
+ catch (e) {
42
+ // ignore
43
+ }
44
+ }
45
+ const authorizationSuccessMetricConfig = {
46
+ name: 'authorization_success_total',
47
+ labels: ['resourceType', 'action', 'apiType'],
48
+ description: 'Total number of successful authorization checks',
49
+ };
50
+ const authorizationErrorMetricConfig = {
51
+ name: 'authorization_error_total',
52
+ labels: ['resourceType', 'action', 'statusCode', 'apiType'],
53
+ description: 'Total number of authorization errors',
54
+ };
55
+ function incrementAuthorizationSuccess(resourceType, action, apiType) {
56
+ try {
57
+ if (authorizationSuccessMetric) {
58
+ authorizationSuccessMetric.labels(resourceType, action, apiType).inc();
25
59
  }
26
60
  }
27
61
  catch (e) {
28
62
  // ignore
29
63
  }
30
64
  }
65
+ function incrementAuthorizationError(resourceType, action, statusCode, apiType) {
66
+ try {
67
+ if (authorizationErrorMetric) {
68
+ authorizationErrorMetric.labels(resourceType, action, statusCode, apiType).inc();
69
+ }
70
+ }
71
+ catch (e) {
72
+ // ignore
73
+ }
74
+ }
75
+ // Initialize additional metrics when prometheus is set
76
+ function initializeAdditionalMetrics() {
77
+ if (!prometheus) {
78
+ return;
79
+ }
80
+ const { METRICS_TYPES } = prometheus;
81
+ const metricsManager = getMetricsManager();
82
+ if (metricsManager) {
83
+ authorizationSuccessMetric = metricsManager.addMetric(METRICS_TYPES.COUNTER, authorizationSuccessMetricConfig.name, authorizationSuccessMetricConfig.labels, authorizationSuccessMetricConfig.description);
84
+ authorizationErrorMetric = metricsManager.addMetric(METRICS_TYPES.COUNTER, authorizationErrorMetricConfig.name, authorizationErrorMetricConfig.labels, authorizationErrorMetricConfig.description);
85
+ }
86
+ }
31
87
 
32
- export { METRICS, getMetricsManager, sendAuthorizationCheckResponseTimeMetric, setPrometheus };
88
+ export { METRICS, getMetricsManager, incrementAuthorizationError, incrementAuthorizationSuccess, sendAuthorizationCheckResponseTimeMetric, setPrometheus };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/testKit/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAG9G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,eAAO,MAAM,sBAAsB,GAAI,WAAW,MAAM,EAAE,QAAQ,MAAM,EAAE,WAAW,QAAQ,EAAE,EAAE,QAAQ,MAAM,SAE9G,CAAC;AAEF,eAAO,MAAM,yBAAyB,YAErC,CAAC;AAyBF,eAAO,MAAM,8BAA8B,GACzC,QAAQ,MAAM,EACd,gBAAgB,cAAc,EAC9B,gBAAgB,aAAa,MAG3B,SAAS,WAAW,EACpB,UAAU,YAAY,EACtB,MAAM,YAAY,KACjB,OAAO,CAAC,IAAI,CAYhB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/testKit/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAG9G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,eAAO,MAAM,sBAAsB,GAAI,WAAW,MAAM,EAAE,QAAQ,MAAM,EAAE,WAAW,QAAQ,EAAE,EAAE,QAAQ,MAAM,SAE9G,CAAC;AAEF,eAAO,MAAM,yBAAyB,YAErC,CAAC;AA4BF,eAAO,MAAM,8BAA8B,GACzC,QAAQ,MAAM,EACd,gBAAgB,cAAc,EAC9B,gBAAgB,aAAa,MAG3B,SAAS,WAAW,EACpB,UAAU,YAAY,EACtB,MAAM,YAAY,KACjB,OAAO,CAAC,IAAI,CAYhB,CAAC"}
@@ -9,18 +9,20 @@ const clearTestPermittedActions = () => {
9
9
  testPermittedActions = [];
10
10
  };
11
11
  const isActionAuthorized = (accountId, userId, resources, action) => {
12
+ // If no resources to check, deny access
13
+ if (resources.length === 0) {
14
+ return { isAuthorized: false };
15
+ }
12
16
  return {
13
- isAuthorized: resources.every(_ => {
17
+ isAuthorized: resources.every(resource => {
14
18
  return testPermittedActions.some(combination => {
15
19
  return (combination.accountId === accountId &&
16
20
  combination.userId === userId &&
17
21
  combination.action === action &&
18
22
  combination.resources.some(combinationResource => {
19
- return resources.some(resource => {
20
- return (combinationResource.id === resource.id &&
21
- combinationResource.type === resource.type &&
22
- JSON.stringify(combinationResource.wrapperData) === JSON.stringify(resource.wrapperData));
23
- });
23
+ return (combinationResource.id === resource.id &&
24
+ combinationResource.type === resource.type &&
25
+ JSON.stringify(combinationResource.wrapperData) === JSON.stringify(resource.wrapperData));
24
26
  }));
25
27
  });
26
28
  }),
@@ -32,11 +34,11 @@ const getTestAuthorizationMiddleware = (action, resourceGetter, contextGetter) =
32
34
  const { userId, accountId } = contextGetter(request);
33
35
  const resources = resourceGetter(request);
34
36
  const { isAuthorized } = isActionAuthorized(accountId, userId, resources, action);
35
- AuthorizationInternalService.markAuthorized(request);
36
37
  if (!isAuthorized) {
37
38
  response.status(403).json({ message: 'Access denied' });
38
39
  return;
39
40
  }
41
+ AuthorizationInternalService.markAuthorized(request);
40
42
  next();
41
43
  };
42
44
  };
@@ -0,0 +1,15 @@
1
+ export type ResourceType = string;
2
+ export type ResourceId = number;
3
+ export type ActionName = string;
4
+ export type GraphIsAllowedDto = Record<ResourceType, Record<ResourceId, ActionName[]>>;
5
+ export type GraphPermissionResult = {
6
+ can: boolean;
7
+ reason: string | {
8
+ key: string;
9
+ additionalOptions?: Record<string, string>;
10
+ technicalReason?: number;
11
+ };
12
+ };
13
+ export type GraphPermissionResults = Record<ActionName, GraphPermissionResult>;
14
+ export type GraphIsAllowedResponse = Record<ResourceType, Record<string, GraphPermissionResults>>;
15
+ //# sourceMappingURL=graph-api.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-api.types.d.ts","sourceRoot":"","sources":["../../../src/types/graph-api.types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAClC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC;AAChC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC;AAEhC,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;AAEvF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EACF,MAAM,GACN;QACE,GAAG,EAAE,MAAM,CAAC;QACZ,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACP,CAAC;AAGF,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,UAAU,EAAE,qBAAqB,CAAC,CAAC;AAI/E,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,22 @@
1
+ import { ScopeOptions } from '../types/scoped-actions-contracts';
2
+ import { ResourceType, ResourceId } from '../types/graph-api.types';
3
+ export type CamelCase<S extends string> = S extends `${infer F}_${infer R}` ? `${F}${Capitalize<CamelCase<R>>}` : S;
4
+ export type CamelCaseKeys<T> = T extends object ? {
5
+ [K in keyof T as K extends string ? CamelCase<K> : K]: CamelCaseKeys<T[K]>;
6
+ } : T;
7
+ /**
8
+ * Converts a scope object to resource type and resource ID
9
+ */
10
+ export declare function scopeToResource(scope: ScopeOptions): {
11
+ resourceType: ResourceType;
12
+ resourceId: ResourceId;
13
+ };
14
+ /**
15
+ * Converts object keys from snake_case to camelCase
16
+ */
17
+ export declare function toCamelCase<T extends object>(obj: T): CamelCaseKeys<T>;
18
+ /**
19
+ * Converts object keys from camelCase to snake_case
20
+ */
21
+ export declare function toSnakeCase<T extends object>(obj: T): Record<string, any>;
22
+ //# sourceMappingURL=authorization.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/authorization.utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEpE,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,MAAM,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,GAC3C;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAC9E,CAAC,CAAC;AAEN;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG;IAAE,YAAY,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,CAkB3G;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEtE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAEzE"}