@mondaydotcomorg/monday-authorization 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-46d4fc5 → 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-7ce3f8a
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 +0 -8
- package/dist/authorization-service.d.ts.map +1 -1
- package/dist/authorization-service.js +12 -160
- 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 +102 -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 +86 -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 +13 -155
- 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 +100 -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 +84 -0
- package/dist/esm/prometheus-service.d.ts +3 -3
- package/dist/esm/prometheus-service.d.ts.map +1 -1
- package/dist/esm/prometheus-service.mjs +65 -3
- package/dist/esm/types/graph-api.types.d.ts +11 -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 -3
- package/dist/prometheus-service.d.ts.map +1 -1
- package/dist/prometheus-service.js +65 -3
- package/dist/types/graph-api.types.d.ts +11 -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 +1 -1
|
@@ -1,18 +1,16 @@
|
|
|
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,
|
|
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
|
-
const CAN_ACTION_IN_SCOPE_GRAPH_PATH = '/permissions/is-allowed';
|
|
16
14
|
const ALLOWED_SDK_PLATFORM_PROFILES_KEY = 'allowed-sdk-platform-profiles';
|
|
17
15
|
const IN_RELEASE_SDK_PLATFORM_PROFILES_KEY = 'in-release-sdk-platform-profile';
|
|
18
16
|
const PLATFORM_PROFILE_RELEASE_FF = 'sdk-platform-profiles';
|
|
@@ -94,166 +92,26 @@ class AuthorizationService {
|
|
|
94
92
|
const startTime = performance.now();
|
|
95
93
|
let scopedActionResponseObjects;
|
|
96
94
|
if (shouldNavigateToGraph) {
|
|
97
|
-
|
|
98
|
-
scopedActionResponseObjects = this.mapGraphResponse(scopedActions, userId, response);
|
|
95
|
+
scopedActionResponseObjects = await GraphApiClient.checkPermissions(internalAuthToken, scopedActions);
|
|
99
96
|
}
|
|
100
97
|
else {
|
|
101
98
|
const profile = this.getProfile(accountId, userId);
|
|
102
|
-
|
|
103
|
-
const platformResponse = await this.fetchPlatformCanActions(profile, internalAuthToken, userId, scopedActionsPayload);
|
|
104
|
-
scopedActionResponseObjects = this.mapPlatformResponse(platformResponse);
|
|
99
|
+
scopedActionResponseObjects = await PlatformApiClient.checkPermissions(profile, internalAuthToken, userId, scopedActions);
|
|
105
100
|
}
|
|
106
101
|
const endTime = performance.now();
|
|
107
102
|
const time = endTime - startTime;
|
|
108
103
|
const apiType = shouldNavigateToGraph ? 'graph' : 'platform';
|
|
104
|
+
// Record metrics for each authorization check
|
|
109
105
|
for (const obj of scopedActionResponseObjects) {
|
|
110
|
-
const { action } = obj.scopedAction;
|
|
111
|
-
const
|
|
106
|
+
const { action, scope } = obj.scopedAction;
|
|
107
|
+
const { resourceType } = scopeToResource(scope);
|
|
112
108
|
const isAuthorized = obj.permit.can;
|
|
113
|
-
sendAuthorizationCheckResponseTimeMetric(
|
|
114
|
-
if (obj.permit.can)
|
|
115
|
-
|
|
116
|
-
return scopedActionResponseObjects;
|
|
117
|
-
}
|
|
118
|
-
static buildScopedActionsPayload(scopedActions) {
|
|
119
|
-
return scopedActions.map(scopedAction => {
|
|
120
|
-
return { ...scopedAction, scope: mapKeys(scopedAction.scope, (_, key) => snakeCase(key)) };
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
static scopeToResource(scope) {
|
|
124
|
-
if ('workspaceId' in scope) {
|
|
125
|
-
return { resourceType: 'workspace', resourceId: scope.workspaceId };
|
|
126
|
-
}
|
|
127
|
-
if ('boardId' in scope) {
|
|
128
|
-
return { resourceType: 'board', resourceId: scope.boardId };
|
|
129
|
-
}
|
|
130
|
-
if ('pulseId' in scope) {
|
|
131
|
-
return { resourceType: 'pulse', resourceId: scope.pulseId };
|
|
132
|
-
}
|
|
133
|
-
if ('accountProductId' in scope) {
|
|
134
|
-
return { resourceType: 'account_product', resourceId: scope.accountProductId };
|
|
135
|
-
}
|
|
136
|
-
if ('accountId' in scope) {
|
|
137
|
-
return { resourceType: 'account', resourceId: scope.accountId };
|
|
138
|
-
}
|
|
139
|
-
throw new Error('Unsupported scope provided');
|
|
140
|
-
}
|
|
141
|
-
static buildGraphRequestBody(scopedActions) {
|
|
142
|
-
const resourcesAccumulator = {};
|
|
143
|
-
for (const { action, scope } of scopedActions) {
|
|
144
|
-
const { resourceType, resourceId } = this.scopeToResource(scope);
|
|
145
|
-
if (!resourcesAccumulator[resourceType]) {
|
|
146
|
-
resourcesAccumulator[resourceType] = {};
|
|
147
|
-
}
|
|
148
|
-
if (!resourcesAccumulator[resourceType][resourceId]) {
|
|
149
|
-
resourcesAccumulator[resourceType][resourceId] = new Set();
|
|
150
|
-
}
|
|
151
|
-
resourcesAccumulator[resourceType][resourceId].add(action);
|
|
152
|
-
}
|
|
153
|
-
const resourcesPayload = {};
|
|
154
|
-
for (const [resourceType, idMap] of Object.entries(resourcesAccumulator)) {
|
|
155
|
-
resourcesPayload[resourceType] = {};
|
|
156
|
-
for (const [idStr, actionsSet] of Object.entries(idMap)) {
|
|
157
|
-
const idNum = Number(idStr);
|
|
158
|
-
resourcesPayload[resourceType][idNum] = Array.from(actionsSet);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return resourcesPayload;
|
|
162
|
-
}
|
|
163
|
-
static async fetchGraphIsAllowed(internalAuthToken, scopedActions) {
|
|
164
|
-
const httpClient = Api.getPart('httpClient');
|
|
165
|
-
const attributionHeaders = getAttributionsFromApi();
|
|
166
|
-
const bodyPayload = this.buildGraphRequestBody(scopedActions);
|
|
167
|
-
try {
|
|
168
|
-
const response = await httpClient.fetch({
|
|
169
|
-
url: {
|
|
170
|
-
appName: 'authorization-graph',
|
|
171
|
-
path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
|
|
172
|
-
},
|
|
173
|
-
method: 'POST',
|
|
174
|
-
headers: {
|
|
175
|
-
Authorization: internalAuthToken,
|
|
176
|
-
'Content-Type': 'application/json',
|
|
177
|
-
...attributionHeaders,
|
|
178
|
-
},
|
|
179
|
-
body: JSON.stringify(bodyPayload),
|
|
180
|
-
}, {
|
|
181
|
-
timeout: AuthorizationInternalService.getRequestTimeout(),
|
|
182
|
-
retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
|
|
183
|
-
});
|
|
184
|
-
setGraphAvailability(true);
|
|
185
|
-
return response;
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
if (err instanceof HttpFetcherError) {
|
|
189
|
-
AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
|
|
190
|
-
incrementAuthorizationError(this.scopeToResource(scopedActions[0].scope).resourceType, scopedActions[0].action, err.status);
|
|
109
|
+
sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthorized, 200, time, apiType);
|
|
110
|
+
if (obj.permit.can) {
|
|
111
|
+
incrementAuthorizationSuccess(resourceType, action);
|
|
191
112
|
}
|
|
192
|
-
throw err;
|
|
193
113
|
}
|
|
194
|
-
|
|
195
|
-
static mapGraphResponse(scopedActions, userId, graphResponse) {
|
|
196
|
-
const resources = graphResponse ?? {};
|
|
197
|
-
return scopedActions.map(scopedAction => {
|
|
198
|
-
const { action, scope } = scopedAction;
|
|
199
|
-
const { resourceType, resourceId } = this.scopeToResource(scope);
|
|
200
|
-
const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
|
|
201
|
-
const permit = {
|
|
202
|
-
can: permissionResult?.can ?? false,
|
|
203
|
-
reason: { key: permissionResult?.reason ?? 'unknown' },
|
|
204
|
-
technicalReason: 0,
|
|
205
|
-
};
|
|
206
|
-
return { scopedAction, permit };
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
static async fetchPlatformCanActions(profile, internalAuthToken, userId, scopedActionsPayload) {
|
|
210
|
-
const attributionHeaders = getAttributionsFromApi();
|
|
211
|
-
const httpClient = Api.getPart('httpClient');
|
|
212
|
-
try {
|
|
213
|
-
const response = await httpClient.fetch({
|
|
214
|
-
url: {
|
|
215
|
-
appName: 'platform',
|
|
216
|
-
path: PLATFORM_CAN_ACTIONS_IN_SCOPES_PATH,
|
|
217
|
-
profile,
|
|
218
|
-
},
|
|
219
|
-
method: 'POST',
|
|
220
|
-
headers: {
|
|
221
|
-
Authorization: internalAuthToken,
|
|
222
|
-
'Content-Type': 'application/json',
|
|
223
|
-
...attributionHeaders,
|
|
224
|
-
},
|
|
225
|
-
body: JSON.stringify({ user_id: userId, scoped_actions: scopedActionsPayload }),
|
|
226
|
-
}, {
|
|
227
|
-
timeout: AuthorizationInternalService.getRequestTimeout(),
|
|
228
|
-
retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
|
|
229
|
-
});
|
|
230
|
-
return response;
|
|
231
|
-
}
|
|
232
|
-
catch (err) {
|
|
233
|
-
if (err instanceof HttpFetcherError) {
|
|
234
|
-
AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
|
|
235
|
-
incrementAuthorizationError(this.scopeToResource(this.toCamelCase(scopedActionsPayload[0].scope)).resourceType, scopedActionsPayload[0].action, err.status);
|
|
236
|
-
}
|
|
237
|
-
throw err;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
static toCamelCase(obj) {
|
|
241
|
-
return mapKeys(obj, (_, key) => camelCase(key));
|
|
242
|
-
}
|
|
243
|
-
static mapPlatformResponse(response) {
|
|
244
|
-
if (!response) {
|
|
245
|
-
logger.error({ tag: 'authorization-service', response }, 'AuthorizationService: missing response');
|
|
246
|
-
throw new Error('AuthorizationService: missing response');
|
|
247
|
-
}
|
|
248
|
-
return response.result.map(responseObject => {
|
|
249
|
-
const { scopedAction, permit } = responseObject;
|
|
250
|
-
const { scope } = scopedAction;
|
|
251
|
-
return {
|
|
252
|
-
...responseObject,
|
|
253
|
-
scopedAction: { ...scopedAction, scope: this.toCamelCase(scope) },
|
|
254
|
-
permit: this.toCamelCase(permit),
|
|
255
|
-
};
|
|
256
|
-
});
|
|
114
|
+
return scopedActionResponseObjects;
|
|
257
115
|
}
|
|
258
116
|
static async isAuthorizedSingular(accountId, userId, resources, action) {
|
|
259
117
|
const { authorizationObjects } = createAuthorizationParams(resources, action);
|
|
@@ -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,EAAE,YAAY,EAAE,0BAA0B,EAAsB,MAAM,mCAAmC,CAAC;AAGjH,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;IAyClC;;OAEG;IACH,MAAM,CAAC,WAAW,CAChB,aAAa,EAAE,YAAY,EAAE,EAC7B,aAAa,EAAE,sBAAsB,GACpC,0BAA0B,EAAE;IAkB/B;;OAEG;WACU,gBAAgB,CAC3B,iBAAiB,EAAE,MAAM,EACzB,aAAa,EAAE,YAAY,EAAE,GAC5B,OAAO,CAAC,0BAA0B,EAAE,CAAC;CAIzC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Api } from '@mondaydotcomorg/trident-backend-api';
|
|
2
|
+
import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
|
|
3
|
+
import { AuthorizationInternalService } from '../authorization-internal-service.mjs';
|
|
4
|
+
import { getAttributionsFromApi } from '../attributions-service.mjs';
|
|
5
|
+
import { scopeToResource } from '../utils/authorization.utils.mjs';
|
|
6
|
+
import { setGraphAvailability, incrementAuthorizationError } from '../prometheus-service.mjs';
|
|
7
|
+
|
|
8
|
+
const CAN_ACTION_IN_SCOPE_GRAPH_PATH = '/permissions/is-allowed';
|
|
9
|
+
/**
|
|
10
|
+
* Client for handling Graph API authorization operations
|
|
11
|
+
*/
|
|
12
|
+
class GraphApiClient {
|
|
13
|
+
/**
|
|
14
|
+
* Builds the request body for Graph API calls
|
|
15
|
+
*/
|
|
16
|
+
static buildRequestBody(scopedActions) {
|
|
17
|
+
const resourcesAccumulator = {};
|
|
18
|
+
for (const { action, scope } of scopedActions) {
|
|
19
|
+
const { resourceType, resourceId } = scopeToResource(scope);
|
|
20
|
+
if (!resourcesAccumulator[resourceType]) {
|
|
21
|
+
resourcesAccumulator[resourceType] = {};
|
|
22
|
+
}
|
|
23
|
+
if (!resourcesAccumulator[resourceType][resourceId]) {
|
|
24
|
+
resourcesAccumulator[resourceType][resourceId] = new Set();
|
|
25
|
+
}
|
|
26
|
+
resourcesAccumulator[resourceType][resourceId].add(action);
|
|
27
|
+
}
|
|
28
|
+
const resourcesPayload = {};
|
|
29
|
+
for (const [resourceType, idMap] of Object.entries(resourcesAccumulator)) {
|
|
30
|
+
resourcesPayload[resourceType] = {};
|
|
31
|
+
for (const [idStr, actionsSet] of Object.entries(idMap)) {
|
|
32
|
+
const idNum = Number(idStr);
|
|
33
|
+
resourcesPayload[resourceType][idNum] = Array.from(actionsSet);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return resourcesPayload;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetches authorization data from the Graph API
|
|
40
|
+
*/
|
|
41
|
+
static async fetchPermissions(internalAuthToken, scopedActions) {
|
|
42
|
+
const httpClient = Api.getPart('httpClient');
|
|
43
|
+
const attributionHeaders = getAttributionsFromApi();
|
|
44
|
+
const bodyPayload = this.buildRequestBody(scopedActions);
|
|
45
|
+
try {
|
|
46
|
+
const response = await httpClient.fetch({
|
|
47
|
+
url: {
|
|
48
|
+
appName: 'authorization-graph',
|
|
49
|
+
path: CAN_ACTION_IN_SCOPE_GRAPH_PATH,
|
|
50
|
+
},
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: internalAuthToken,
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
...attributionHeaders,
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify(bodyPayload),
|
|
58
|
+
}, {
|
|
59
|
+
timeout: AuthorizationInternalService.getRequestTimeout(),
|
|
60
|
+
retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
|
|
61
|
+
});
|
|
62
|
+
setGraphAvailability(true);
|
|
63
|
+
return response;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
setGraphAvailability(false);
|
|
67
|
+
if (err instanceof HttpFetcherError) {
|
|
68
|
+
AuthorizationInternalService.throwOnHttpError(err.status, 'canActionInScopeMultiple');
|
|
69
|
+
incrementAuthorizationError(scopeToResource(scopedActions[0].scope).resourceType, scopedActions[0].action, err.status);
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Maps Graph API response to the expected format
|
|
76
|
+
*/
|
|
77
|
+
static mapResponse(scopedActions, graphResponse) {
|
|
78
|
+
const resources = graphResponse ?? {};
|
|
79
|
+
return scopedActions.map(scopedAction => {
|
|
80
|
+
const { action, scope } = scopedAction;
|
|
81
|
+
const { resourceType, resourceId } = scopeToResource(scope);
|
|
82
|
+
const permissionResult = resources?.[resourceType]?.[String(resourceId)]?.[action];
|
|
83
|
+
const permit = {
|
|
84
|
+
can: permissionResult?.can ?? false,
|
|
85
|
+
reason: { key: permissionResult?.reason ?? 'unknown' },
|
|
86
|
+
technicalReason: 0,
|
|
87
|
+
};
|
|
88
|
+
return { scopedAction, permit };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Performs a complete authorization check using the Graph API
|
|
93
|
+
*/
|
|
94
|
+
static async checkPermissions(internalAuthToken, scopedActions) {
|
|
95
|
+
const response = await this.fetchPermissions(internalAuthToken, scopedActions);
|
|
96
|
+
return this.mapResponse(scopedActions, response);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
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,84 @@
|
|
|
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
|
+
incrementAuthorizationError(scopeToResource(toCamelCase(scopedActionsPayload[0].scope)).resourceType, scopedActionsPayload[0].action, err.status);
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Maps Platform API response to the expected format
|
|
58
|
+
*/
|
|
59
|
+
static mapResponse(response) {
|
|
60
|
+
if (!response) {
|
|
61
|
+
logger.error({ tag: 'platform-api-client', response }, 'PlatformApiClient: missing response');
|
|
62
|
+
throw new Error('PlatformApiClient: missing response');
|
|
63
|
+
}
|
|
64
|
+
return response.result.map(responseObject => {
|
|
65
|
+
const { scopedAction, permit } = responseObject;
|
|
66
|
+
const { scope } = scopedAction;
|
|
67
|
+
return {
|
|
68
|
+
...responseObject,
|
|
69
|
+
scopedAction: { ...scopedAction, scope: toCamelCase(scope) },
|
|
70
|
+
permit: toCamelCase(permit),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Performs a complete authorization check using the Platform API
|
|
76
|
+
*/
|
|
77
|
+
static async checkPermissions(profile, internalAuthToken, userId, scopedActions) {
|
|
78
|
+
const scopedActionsPayload = this.buildRequestPayload(scopedActions);
|
|
79
|
+
const platformResponse = await this.fetchPermissions(profile, internalAuthToken, userId, scopedActionsPayload);
|
|
80
|
+
return this.mapResponse(platformResponse);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { PlatformApiClient };
|
|
@@ -7,7 +7,7 @@ export declare const METRICS: {
|
|
|
7
7
|
export declare function setPrometheus(customPrometheus: any): void;
|
|
8
8
|
export declare function getMetricsManager(): any;
|
|
9
9
|
export declare function sendAuthorizationCheckResponseTimeMetric(resourceType: string, action: Action, isAuthorized: boolean, responseStatus: number, time: number, apiType?: 'platform' | 'graph'): void;
|
|
10
|
-
export declare function incrementAuthorizationSuccess(
|
|
11
|
-
export declare function incrementAuthorizationError(
|
|
12
|
-
export declare function setGraphAvailability(
|
|
10
|
+
export declare function incrementAuthorizationSuccess(resourceType: string, action: Action): void;
|
|
11
|
+
export declare function incrementAuthorizationError(resourceType: string, action: Action, statusCode: number): void;
|
|
12
|
+
export declare function setGraphAvailability(isAvailable: boolean): void;
|
|
13
13
|
//# 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;AAQzC,eAAO,MAAM,OAAO;;;;CAInB,CAAC;AAQF,wBAAgB,aAAa,CAAC,gBAAgB,KAAA,QAsB7C;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;AAoBD,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAQjF;AAED,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAQnG;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,OAAO,QAQxD"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
let prometheus = null;
|
|
2
2
|
let authorizationCheckResponseTimeMetric = null;
|
|
3
|
+
let authorizationSuccessMetric = null;
|
|
4
|
+
let authorizationErrorMetric = null;
|
|
5
|
+
let graphAvailabilityMetric = null;
|
|
3
6
|
const METRICS = {
|
|
4
7
|
AUTHORIZATION_CHECK: 'authorization_check',
|
|
5
8
|
AUTHORIZATION_CHECKS_PER_REQUEST: 'authorization_checks_per_request',
|
|
@@ -14,12 +17,16 @@ function setPrometheus(customPrometheus) {
|
|
|
14
17
|
prometheus = customPrometheus;
|
|
15
18
|
if (!prometheus) {
|
|
16
19
|
authorizationCheckResponseTimeMetric = null;
|
|
20
|
+
authorizationSuccessMetric = null;
|
|
21
|
+
authorizationErrorMetric = null;
|
|
22
|
+
graphAvailabilityMetric = null;
|
|
17
23
|
return;
|
|
18
24
|
}
|
|
19
25
|
const { METRICS_TYPES } = prometheus;
|
|
20
26
|
const metricsManager = getMetricsManager();
|
|
21
27
|
if (metricsManager) {
|
|
22
28
|
authorizationCheckResponseTimeMetric = metricsManager.addMetric(METRICS_TYPES.SUMMARY, authorizationCheckResponseTimeMetricConfig.name, authorizationCheckResponseTimeMetricConfig.labels, authorizationCheckResponseTimeMetricConfig.description);
|
|
29
|
+
initializeAdditionalMetrics();
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
function getMetricsManager() {
|
|
@@ -37,8 +44,63 @@ function sendAuthorizationCheckResponseTimeMetric(resourceType, action, isAuthor
|
|
|
37
44
|
// ignore
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
const authorizationSuccessMetricConfig = {
|
|
48
|
+
name: 'authorization_success_total',
|
|
49
|
+
labels: ['resourceType', 'action'],
|
|
50
|
+
description: 'Total number of successful authorization checks',
|
|
51
|
+
};
|
|
52
|
+
const authorizationErrorMetricConfig = {
|
|
53
|
+
name: 'authorization_error_total',
|
|
54
|
+
labels: ['resourceType', 'action', 'statusCode'],
|
|
55
|
+
description: 'Total number of authorization errors',
|
|
56
|
+
};
|
|
57
|
+
const graphAvailabilityMetricConfig = {
|
|
58
|
+
name: 'graph_api_availability',
|
|
59
|
+
labels: ['available'],
|
|
60
|
+
description: 'Graph API availability status',
|
|
61
|
+
};
|
|
62
|
+
function incrementAuthorizationSuccess(resourceType, action) {
|
|
63
|
+
try {
|
|
64
|
+
if (authorizationSuccessMetric) {
|
|
65
|
+
authorizationSuccessMetric.labels(resourceType, action).inc();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function incrementAuthorizationError(resourceType, action, statusCode) {
|
|
73
|
+
try {
|
|
74
|
+
if (authorizationErrorMetric) {
|
|
75
|
+
authorizationErrorMetric.labels(resourceType, action, statusCode).inc();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function setGraphAvailability(isAvailable) {
|
|
83
|
+
try {
|
|
84
|
+
if (graphAvailabilityMetric) {
|
|
85
|
+
graphAvailabilityMetric.labels(isAvailable ? 'true' : 'false').set(isAvailable ? 1 : 0);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Initialize additional metrics when prometheus is set
|
|
93
|
+
function initializeAdditionalMetrics() {
|
|
94
|
+
if (!prometheus) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { METRICS_TYPES } = prometheus;
|
|
98
|
+
const metricsManager = getMetricsManager();
|
|
99
|
+
if (metricsManager) {
|
|
100
|
+
authorizationSuccessMetric = metricsManager.addMetric(METRICS_TYPES.COUNTER, authorizationSuccessMetricConfig.name, authorizationSuccessMetricConfig.labels, authorizationSuccessMetricConfig.description);
|
|
101
|
+
authorizationErrorMetric = metricsManager.addMetric(METRICS_TYPES.COUNTER, authorizationErrorMetricConfig.name, authorizationErrorMetricConfig.labels, authorizationErrorMetricConfig.description);
|
|
102
|
+
graphAvailabilityMetric = metricsManager.addMetric(METRICS_TYPES.GAUGE, graphAvailabilityMetricConfig.name, graphAvailabilityMetricConfig.labels, graphAvailabilityMetricConfig.description);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
43
105
|
|
|
44
106
|
export { METRICS, getMetricsManager, incrementAuthorizationError, incrementAuthorizationSuccess, sendAuthorizationCheckResponseTimeMetric, setGraphAvailability, setPrometheus };
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
};
|
|
9
|
+
export type GraphPermissionResults = Record<ActionName, GraphPermissionResult>;
|
|
10
|
+
export type GraphIsAllowedResponse = Record<ResourceType, Record<string, GraphPermissionResults>>;
|
|
11
|
+
//# 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,EAAE,MAAM,CAAC;CAChB,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,CAiB3G;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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import snakeCase from 'lodash/snakeCase.js';
|
|
2
|
+
import camelCase from 'lodash/camelCase.js';
|
|
3
|
+
import mapKeys from 'lodash/mapKeys.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts a scope object to resource type and resource ID
|
|
7
|
+
*/
|
|
8
|
+
function scopeToResource(scope) {
|
|
9
|
+
if ('workspaceId' in scope) {
|
|
10
|
+
return { resourceType: 'workspace', resourceId: scope.workspaceId };
|
|
11
|
+
}
|
|
12
|
+
if ('boardId' in scope) {
|
|
13
|
+
return { resourceType: 'board', resourceId: scope.boardId };
|
|
14
|
+
}
|
|
15
|
+
if ('pulseId' in scope) {
|
|
16
|
+
return { resourceType: 'pulse', resourceId: scope.pulseId };
|
|
17
|
+
}
|
|
18
|
+
if ('accountProductId' in scope) {
|
|
19
|
+
return { resourceType: 'account_product', resourceId: scope.accountProductId };
|
|
20
|
+
}
|
|
21
|
+
if ('accountId' in scope) {
|
|
22
|
+
return { resourceType: 'account', resourceId: scope.accountId };
|
|
23
|
+
}
|
|
24
|
+
throw new Error('Unsupported scope provided');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Converts object keys from snake_case to camelCase
|
|
28
|
+
*/
|
|
29
|
+
function toCamelCase(obj) {
|
|
30
|
+
return mapKeys(obj, (_, key) => camelCase(key));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Converts object keys from camelCase to snake_case
|
|
34
|
+
*/
|
|
35
|
+
function toSnakeCase(obj) {
|
|
36
|
+
return mapKeys(obj, (_, key) => snakeCase(key));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { scopeToResource, toCamelCase, toSnakeCase };
|