@mondaydotcomorg/monday-authorization 3.2.3-feature-bashanye-navigate-can-action-in-scope-to-graph-af77c6b → 3.3.0-feat-add-graph-api-routing-support-2d70b30
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.
- package/README.md +29 -0
- package/dist/authorization-service.d.ts +0 -8
- package/dist/authorization-service.d.ts.map +1 -1
- package/dist/authorization-service.js +39 -151
- package/dist/clients/graph-api.client.d.ts +24 -0
- package/dist/clients/graph-api.client.d.ts.map +1 -0
- package/dist/clients/graph-api.client.js +123 -0
- package/dist/clients/platform-api.client.d.ts +31 -0
- package/dist/clients/platform-api.client.d.ts.map +1 -0
- package/dist/clients/platform-api.client.js +89 -0
- package/dist/esm/authorization-service.d.ts +0 -8
- package/dist/esm/authorization-service.d.ts.map +1 -1
- package/dist/esm/authorization-service.mjs +40 -146
- package/dist/esm/clients/graph-api.client.d.ts +24 -0
- package/dist/esm/clients/graph-api.client.d.ts.map +1 -0
- package/dist/esm/clients/graph-api.client.mjs +121 -0
- package/dist/esm/clients/platform-api.client.d.ts +31 -0
- package/dist/esm/clients/platform-api.client.d.ts.map +1 -0
- package/dist/esm/clients/platform-api.client.mjs +87 -0
- package/dist/esm/prometheus-service.d.ts +3 -1
- package/dist/esm/prometheus-service.d.ts.map +1 -1
- package/dist/esm/prometheus-service.mjs +61 -5
- package/dist/esm/testKit/index.d.ts.map +1 -1
- package/dist/esm/testKit/index.mjs +9 -7
- package/dist/esm/types/graph-api.types.d.ts +15 -0
- package/dist/esm/types/graph-api.types.d.ts.map +1 -0
- package/dist/esm/types/graph-api.types.mjs +1 -0
- package/dist/esm/utils/authorization.utils.d.ts +22 -0
- package/dist/esm/utils/authorization.utils.d.ts.map +1 -0
- package/dist/esm/utils/authorization.utils.mjs +39 -0
- package/dist/prometheus-service.d.ts +3 -1
- package/dist/prometheus-service.d.ts.map +1 -1
- package/dist/prometheus-service.js +62 -4
- package/dist/testKit/index.d.ts.map +1 -1
- package/dist/testKit/index.js +9 -7
- package/dist/types/graph-api.types.d.ts +15 -0
- package/dist/types/graph-api.types.d.ts.map +1 -0
- package/dist/types/graph-api.types.js +1 -0
- package/dist/utils/authorization.utils.d.ts +22 -0
- package/dist/utils/authorization.utils.d.ts.map +1 -0
- package/dist/utils/authorization.utils.js +49 -0
- package/package.json +2 -2
- package/CHANGELOG.md +0 -46
|
@@ -1,22 +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';
|
|
18
17
|
const NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF = 'navigate-can-action-in-scope-to-graph';
|
|
19
|
-
const GRAPH_IS_ALLOWED_PATH = '/permissions/is-allowed';
|
|
20
18
|
function setRequestFetchOptions(customMondayFetchOptions) {
|
|
21
19
|
AuthorizationInternalService.setRequestFetchOptions(customMondayFetchOptions);
|
|
22
20
|
}
|
|
@@ -89,153 +87,49 @@ class AuthorizationService {
|
|
|
89
87
|
return PlatformProfile.INTERNAL;
|
|
90
88
|
}
|
|
91
89
|
static async canActionInScopeMultiple(accountId, userId, scopedActions) {
|
|
90
|
+
if (scopedActions.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
92
93
|
const shouldNavigateToGraph = Boolean(this.igniteClient?.isReleased(NAVIGATE_CAN_ACTION_IN_SCOPE_TO_GRAPH_FF, { accountId, userId }));
|
|
93
94
|
const internalAuthToken = AuthorizationInternalService.generateInternalAuthToken(accountId, userId);
|
|
95
|
+
const startTime = performance.now();
|
|
96
|
+
let scopedActionResponseObjects;
|
|
97
|
+
let apiType;
|
|
94
98
|
if (shouldNavigateToGraph) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const profile = this.getProfile(accountId, userId);
|
|
99
|
-
const scopedActionsPayload = this.buildScopedActionsPayload(scopedActions);
|
|
100
|
-
const platformResponse = await this.fetchPlatformCanActions(profile, internalAuthToken, userId, scopedActionsPayload);
|
|
101
|
-
return this.mapPlatformResponse(platformResponse);
|
|
102
|
-
}
|
|
103
|
-
static buildScopedActionsPayload(scopedActions) {
|
|
104
|
-
return scopedActions.map(scopedAction => {
|
|
105
|
-
return { ...scopedAction, scope: mapKeys(scopedAction.scope, (_, key) => snakeCase(key)) };
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
static scopeToResource(scope) {
|
|
109
|
-
if ('workspaceId' in scope) {
|
|
110
|
-
return { resourceType: 'workspace', resourceId: scope.workspaceId };
|
|
111
|
-
}
|
|
112
|
-
if ('boardId' in scope) {
|
|
113
|
-
return { resourceType: 'board', resourceId: scope.boardId };
|
|
114
|
-
}
|
|
115
|
-
if ('pulseId' in scope) {
|
|
116
|
-
return { resourceType: 'pulse', resourceId: scope.pulseId };
|
|
117
|
-
}
|
|
118
|
-
if ('accountProductId' in scope) {
|
|
119
|
-
return { resourceType: 'account_product', resourceId: scope.accountProductId };
|
|
120
|
-
}
|
|
121
|
-
if ('accountId' in scope) {
|
|
122
|
-
return { resourceType: 'account', resourceId: scope.accountId };
|
|
123
|
-
}
|
|
124
|
-
throw new Error('Unsupported scope provided');
|
|
125
|
-
}
|
|
126
|
-
static buildGraphRequestBody(scopedActions) {
|
|
127
|
-
const resourcesAccumulator = {};
|
|
128
|
-
for (const { action, scope } of scopedActions) {
|
|
129
|
-
const { resourceType, resourceId } = this.scopeToResource(scope);
|
|
130
|
-
if (!resourcesAccumulator[resourceType]) {
|
|
131
|
-
resourcesAccumulator[resourceType] = {};
|
|
132
|
-
}
|
|
133
|
-
if (!resourcesAccumulator[resourceType][resourceId]) {
|
|
134
|
-
resourcesAccumulator[resourceType][resourceId] = new Set();
|
|
99
|
+
try {
|
|
100
|
+
scopedActionResponseObjects = await GraphApiClient.checkPermissions(internalAuthToken, scopedActions);
|
|
101
|
+
apiType = 'graph';
|
|
135
102
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return resourcesPayload;
|
|
147
|
-
}
|
|
148
|
-
static async fetchGraphIsAllowed(internalAuthToken, scopedActions) {
|
|
149
|
-
const httpClient = Api.getPart('httpClient');
|
|
150
|
-
const attributionHeaders = getAttributionsFromApi();
|
|
151
|
-
const bodyPayload = this.buildGraphRequestBody(scopedActions);
|
|
152
|
-
try {
|
|
153
|
-
const response = await httpClient.fetch({
|
|
154
|
-
url: {
|
|
155
|
-
appName: 'authorization-graph',
|
|
156
|
-
path: GRAPH_IS_ALLOWED_PATH,
|
|
157
|
-
},
|
|
158
|
-
method: 'POST',
|
|
159
|
-
headers: {
|
|
160
|
-
Authorization: internalAuthToken,
|
|
161
|
-
'Content-Type': 'application/json',
|
|
162
|
-
...attributionHeaders,
|
|
163
|
-
},
|
|
164
|
-
body: JSON.stringify(bodyPayload),
|
|
165
|
-
}, {
|
|
166
|
-
timeout: AuthorizationInternalService.getRequestTimeout(),
|
|
167
|
-
retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
|
|
168
|
-
});
|
|
169
|
-
return response;
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
if (err instanceof HttpFetcherError) {
|
|
173
|
-
AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
|
|
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;
|
|
174
113
|
}
|
|
175
|
-
throw err;
|
|
176
114
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const { action, scope } = scopedAction;
|
|
182
|
-
const { resourceType, resourceId } = this.scopeToResource(scope);
|
|
183
|
-
const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
|
|
184
|
-
const permit = {
|
|
185
|
-
can: permissionResult?.can ?? false,
|
|
186
|
-
reason: { key: permissionResult?.reason ?? 'unknown' },
|
|
187
|
-
technicalReason: 0,
|
|
188
|
-
};
|
|
189
|
-
return { scopedAction, permit };
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
static async fetchPlatformCanActions(profile, internalAuthToken, userId, scopedActionsPayload) {
|
|
193
|
-
const attributionHeaders = getAttributionsFromApi();
|
|
194
|
-
const httpClient = Api.getPart('httpClient');
|
|
195
|
-
try {
|
|
196
|
-
const response = await httpClient.fetch({
|
|
197
|
-
url: {
|
|
198
|
-
appName: 'platform',
|
|
199
|
-
path: PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH,
|
|
200
|
-
profile,
|
|
201
|
-
},
|
|
202
|
-
method: 'POST',
|
|
203
|
-
headers: {
|
|
204
|
-
Authorization: internalAuthToken,
|
|
205
|
-
'Content-Type': 'application/json',
|
|
206
|
-
...attributionHeaders,
|
|
207
|
-
},
|
|
208
|
-
body: JSON.stringify({ user_id: userId, scoped_actions: scopedActionsPayload }),
|
|
209
|
-
}, {
|
|
210
|
-
timeout: AuthorizationInternalService.getRequestTimeout(),
|
|
211
|
-
retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
|
|
212
|
-
});
|
|
213
|
-
return response;
|
|
115
|
+
else {
|
|
116
|
+
const profile = this.getProfile(accountId, userId);
|
|
117
|
+
scopedActionResponseObjects = await PlatformApiClient.checkPermissions(profile, internalAuthToken, userId, scopedActions);
|
|
118
|
+
apiType = 'platform';
|
|
214
119
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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);
|
|
218
130
|
}
|
|
219
|
-
throw err;
|
|
220
131
|
}
|
|
221
|
-
|
|
222
|
-
static toCamelCase(obj) {
|
|
223
|
-
return mapKeys(obj, (_, key) => camelCase(key));
|
|
224
|
-
}
|
|
225
|
-
static mapPlatformResponse(response) {
|
|
226
|
-
if (!response) {
|
|
227
|
-
logger.error({ tag: 'authorization-service', response }, 'AuthorizationService: missing response');
|
|
228
|
-
throw new Error('AuthorizationService: missing response');
|
|
229
|
-
}
|
|
230
|
-
return response.result.map(responseObject => {
|
|
231
|
-
const { scopedAction, permit } = responseObject;
|
|
232
|
-
const { scope } = scopedAction;
|
|
233
|
-
return {
|
|
234
|
-
...responseObject,
|
|
235
|
-
scopedAction: { ...scopedAction, scope: this.toCamelCase(scope) },
|
|
236
|
-
permit: this.toCamelCase(permit),
|
|
237
|
-
};
|
|
238
|
-
});
|
|
132
|
+
return scopedActionResponseObjects;
|
|
239
133
|
}
|
|
240
134
|
static async isAuthorizedSingular(accountId, userId, resources, action) {
|
|
241
135
|
const { authorizationObjects } = createAuthorizationParams(resources, action);
|
|
@@ -271,7 +165,7 @@ class AuthorizationService {
|
|
|
271
165
|
});
|
|
272
166
|
}
|
|
273
167
|
catch (err) {
|
|
274
|
-
if (err instanceof
|
|
168
|
+
if (err instanceof HttpFetcherError) {
|
|
275
169
|
AuthorizationInternalService.throwOnHttpError(err.status, 'isAuthorizedMultiple');
|
|
276
170
|
}
|
|
277
171
|
else {
|
|
@@ -290,7 +184,7 @@ class AuthorizationService {
|
|
|
290
184
|
if (!isAuthorized) {
|
|
291
185
|
unauthorizedObjects.push(authorizationObject);
|
|
292
186
|
}
|
|
293
|
-
sendAuthorizationCheckResponseTimeMetric(authorizationObject.resource_type, authorizationObject.action, isAuthorized, 200, time);
|
|
187
|
+
sendAuthorizationCheckResponseTimeMetric(authorizationObject.resource_type, authorizationObject.action, isAuthorized, 200, time, 'platform');
|
|
294
188
|
});
|
|
295
189
|
if (unauthorizedObjects.length > 0) {
|
|
296
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;
|
|
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
|
-
|
|
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
|
|
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;
|
|
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"}
|