@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.
- package/dist/authorization-service.d.ts.map +1 -1
- package/dist/authorization-service.js +0 -9
- package/dist/clients/graph-api.client.d.ts.map +1 -1
- package/dist/clients/graph-api.client.js +1 -21
- package/dist/clients/platform-api.client.d.ts.map +1 -1
- package/dist/clients/platform-api.client.js +1 -24
- package/dist/esm/authorization-service.d.ts.map +1 -1
- package/dist/esm/authorization-service.mjs +0 -9
- package/dist/esm/clients/graph-api.client.d.ts.map +1 -1
- package/dist/esm/clients/graph-api.client.mjs +2 -22
- package/dist/esm/clients/platform-api.client.d.ts.map +1 -1
- package/dist/esm/clients/platform-api.client.mjs +2 -25
- package/dist/esm/utils/authorization.utils.d.ts.map +1 -1
- package/dist/esm/utils/authorization.utils.mjs +0 -12
- package/dist/utils/authorization.utils.d.ts.map +1 -1
- package/dist/utils/authorization.utils.js +0 -12
- package/package.json +2 -7
- package/DEBUG.md +0 -203
- package/src/attributions-service.ts +0 -92
- package/src/authorization-attributes-service.ts +0 -234
- package/src/authorization-internal-service.ts +0 -129
- package/src/authorization-middleware.ts +0 -51
- package/src/authorization-service.ts +0 -384
- package/src/clients/graph-api.client.ts +0 -164
- package/src/clients/platform-api.client.ts +0 -151
- package/src/constants/sns.ts +0 -5
- package/src/constants.ts +0 -22
- package/src/index.ts +0 -46
- package/src/prometheus-service.ts +0 -147
- package/src/roles-service.ts +0 -125
- package/src/testKit/index.ts +0 -69
- package/src/types/authorization-attributes-contracts.ts +0 -33
- package/src/types/express.ts +0 -8
- package/src/types/general.ts +0 -32
- package/src/types/graph-api.types.ts +0 -19
- package/src/types/roles.ts +0 -42
- package/src/types/scoped-actions-contracts.ts +0 -48
- 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
|
-
}
|