@mondaydotcomorg/monday-authorization 3.3.0-feature-bashanye-navigate-can-action-in-scope-to-graph-2d70b30 → 3.3.1-feature-bashanye-add-membership-create-delete-api-d00c165

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +143 -11
  2. package/dist/attributions-service.d.ts +3 -2
  3. package/dist/attributions-service.d.ts.map +1 -1
  4. package/dist/attributions-service.js +1 -0
  5. package/dist/authorization-internal-service.d.ts +1 -1
  6. package/dist/authorization-internal-service.d.ts.map +1 -1
  7. package/dist/authorization-service.d.ts +5 -0
  8. package/dist/authorization-service.d.ts.map +1 -1
  9. package/dist/authorization-service.js +30 -26
  10. package/dist/clients/graph-api.d.ts +28 -0
  11. package/dist/clients/graph-api.d.ts.map +1 -0
  12. package/dist/clients/{graph-api.client.js → graph-api.js} +48 -40
  13. package/dist/clients/platform-api.d.ts +26 -0
  14. package/dist/clients/platform-api.d.ts.map +1 -0
  15. package/dist/clients/{platform-api.client.js → platform-api.js} +20 -20
  16. package/dist/constants.d.ts +1 -0
  17. package/dist/constants.d.ts.map +1 -1
  18. package/dist/constants.js +2 -0
  19. package/dist/esm/attributions-service.d.ts +3 -2
  20. package/dist/esm/attributions-service.d.ts.map +1 -1
  21. package/dist/esm/attributions-service.mjs +1 -0
  22. package/dist/esm/authorization-internal-service.d.ts +1 -1
  23. package/dist/esm/authorization-internal-service.d.ts.map +1 -1
  24. package/dist/esm/authorization-service.d.ts +5 -0
  25. package/dist/esm/authorization-service.d.ts.map +1 -1
  26. package/dist/esm/authorization-service.mjs +31 -27
  27. package/dist/esm/clients/graph-api.d.ts +28 -0
  28. package/dist/esm/clients/graph-api.d.ts.map +1 -0
  29. package/dist/esm/clients/{graph-api.client.mjs → graph-api.mjs} +48 -40
  30. package/dist/esm/clients/platform-api.d.ts +26 -0
  31. package/dist/esm/clients/platform-api.d.ts.map +1 -0
  32. package/dist/esm/clients/{platform-api.client.mjs → platform-api.mjs} +21 -21
  33. package/dist/esm/constants.d.ts +1 -0
  34. package/dist/esm/constants.d.ts.map +1 -1
  35. package/dist/esm/constants.mjs +2 -1
  36. package/dist/esm/index.d.ts +7 -0
  37. package/dist/esm/index.d.ts.map +1 -1
  38. package/dist/esm/index.mjs +9 -0
  39. package/dist/esm/memberships.d.ts +30 -0
  40. package/dist/esm/memberships.d.ts.map +1 -0
  41. package/dist/esm/memberships.mjs +98 -0
  42. package/dist/esm/metrics-service.d.ts +12 -0
  43. package/dist/esm/metrics-service.d.ts.map +1 -0
  44. package/dist/esm/metrics-service.mjs +54 -0
  45. package/dist/esm/prometheus-service.d.ts +1 -3
  46. package/dist/esm/prometheus-service.d.ts.map +1 -1
  47. package/dist/esm/prometheus-service.mjs +5 -58
  48. package/dist/esm/types/graph-api.types.d.ts +8 -7
  49. package/dist/esm/types/graph-api.types.d.ts.map +1 -1
  50. package/dist/esm/types/memberships.d.ts +42 -0
  51. package/dist/esm/types/memberships.d.ts.map +1 -0
  52. package/dist/esm/types/memberships.mjs +1 -0
  53. package/dist/esm/types/scoped-actions-contracts.d.ts +10 -1
  54. package/dist/esm/types/scoped-actions-contracts.d.ts.map +1 -1
  55. package/dist/esm/types/scoped-actions-contracts.mjs +9 -0
  56. package/dist/esm/utils/api-error-handler.d.ts +2 -0
  57. package/dist/esm/utils/api-error-handler.d.ts.map +1 -0
  58. package/dist/esm/utils/api-error-handler.mjs +18 -0
  59. package/dist/index.d.ts +7 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +10 -0
  62. package/dist/memberships.d.ts +30 -0
  63. package/dist/memberships.d.ts.map +1 -0
  64. package/dist/memberships.js +100 -0
  65. package/dist/metrics-service.d.ts +12 -0
  66. package/dist/metrics-service.d.ts.map +1 -0
  67. package/dist/metrics-service.js +58 -0
  68. package/dist/prometheus-service.d.ts +1 -3
  69. package/dist/prometheus-service.d.ts.map +1 -1
  70. package/dist/prometheus-service.js +4 -59
  71. package/dist/types/graph-api.types.d.ts +8 -7
  72. package/dist/types/graph-api.types.d.ts.map +1 -1
  73. package/dist/types/memberships.d.ts +42 -0
  74. package/dist/types/memberships.d.ts.map +1 -0
  75. package/dist/types/memberships.js +1 -0
  76. package/dist/types/scoped-actions-contracts.d.ts +10 -1
  77. package/dist/types/scoped-actions-contracts.d.ts.map +1 -1
  78. package/dist/types/scoped-actions-contracts.js +9 -0
  79. package/dist/utils/api-error-handler.d.ts +2 -0
  80. package/dist/utils/api-error-handler.d.ts.map +1 -0
  81. package/dist/utils/api-error-handler.js +20 -0
  82. package/package.json +5 -2
  83. package/src/attributions-service.ts +93 -0
  84. package/src/authorization-attributes-service.ts +234 -0
  85. package/src/authorization-internal-service.ts +129 -0
  86. package/src/authorization-middleware.ts +51 -0
  87. package/src/authorization-service.ts +356 -0
  88. package/src/clients/graph-api.ts +170 -0
  89. package/src/clients/platform-api.ts +117 -0
  90. package/src/constants/sns.ts +5 -0
  91. package/src/constants.ts +23 -0
  92. package/src/index.ts +63 -0
  93. package/src/memberships.ts +111 -0
  94. package/src/metrics-service.ts +71 -0
  95. package/src/prometheus-service.ts +51 -0
  96. package/src/roles-service.ts +125 -0
  97. package/src/testKit/index.ts +69 -0
  98. package/src/types/authorization-attributes-contracts.ts +33 -0
  99. package/src/types/express.ts +8 -0
  100. package/src/types/general.ts +32 -0
  101. package/src/types/graph-api.types.ts +25 -0
  102. package/src/types/memberships.ts +47 -0
  103. package/src/types/roles.ts +42 -0
  104. package/src/types/scoped-actions-contracts.ts +57 -0
  105. package/src/utils/api-error-handler.ts +25 -0
  106. package/src/utils/authorization.utils.ts +47 -0
  107. package/dist/clients/graph-api.client.d.ts +0 -24
  108. package/dist/clients/graph-api.client.d.ts.map +0 -1
  109. package/dist/clients/platform-api.client.d.ts +0 -31
  110. package/dist/clients/platform-api.client.d.ts.map +0 -1
  111. package/dist/esm/clients/graph-api.client.d.ts +0 -24
  112. package/dist/esm/clients/graph-api.client.d.ts.map +0 -1
  113. package/dist/esm/clients/platform-api.client.d.ts +0 -31
  114. package/dist/esm/clients/platform-api.client.d.ts.map +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { MondayFetchOptions } from '@mondaydotcomorg/monday-fetch';
2
+ import { setPrometheus } from './prometheus-service';
3
+ import { setIgniteClient, setRedisClient, setRequestFetchOptions } from './authorization-service';
4
+ import { initializeMetrics } from './metrics-service';
5
+ import * as TestKit from './testKit';
6
+
7
+ export interface InitOptions {
8
+ prometheus?: any;
9
+ mondayFetchOptions?: MondayFetchOptions;
10
+ redisClient?: any;
11
+ grantedFeatureRedisExpirationInSeconds?: number;
12
+ metrics?: {
13
+ serviceName?: string;
14
+ host?: string;
15
+ port?: number;
16
+ disabled?: boolean;
17
+ };
18
+ }
19
+
20
+ export async function init(options: InitOptions = {}) {
21
+ if (options.prometheus) {
22
+ setPrometheus(options.prometheus);
23
+ }
24
+
25
+ const resolvedDisabled =
26
+ options.metrics?.disabled ?? ['test', 'development'].includes((process.env.NODE_ENV ?? '').toLowerCase());
27
+ initializeMetrics({
28
+ serviceName: options.metrics?.serviceName ?? process.env.APP_NAME ?? 'authorization-sdk',
29
+ host: options.metrics?.host,
30
+ port: options.metrics?.port,
31
+ disabled: resolvedDisabled,
32
+ });
33
+
34
+ if (options.mondayFetchOptions) {
35
+ setRequestFetchOptions(options.mondayFetchOptions);
36
+ }
37
+ if (options.redisClient) {
38
+ setRedisClient(options.redisClient, options.grantedFeatureRedisExpirationInSeconds);
39
+ }
40
+
41
+ // add an ignite client for gradual release features
42
+ await setIgniteClient();
43
+ }
44
+
45
+ export {
46
+ authorizationCheckMiddleware,
47
+ getAuthorizationMiddleware,
48
+ skipAuthorizationMiddleware,
49
+ } from './authorization-middleware';
50
+ export { AuthorizationService, AuthorizeResponse } from './authorization-service';
51
+ export { AuthorizationAttributesService } from './authorization-attributes-service';
52
+ export { RolesService } from './roles-service';
53
+ export { MembershipsService } from './memberships';
54
+ export { AuthorizationObject, Resource, BaseRequest, ResourceGetter, ContextGetter } from './types/general';
55
+ export {
56
+ Translation,
57
+ ScopedAction,
58
+ ScopedActionResponseObject,
59
+ ScopedActionPermit,
60
+ } from './types/scoped-actions-contracts';
61
+ export { CustomRole, BasicRole, RoleType, RoleCreateRequest, RoleUpdateRequest, RolesResponse } from './types/roles';
62
+
63
+ export { TestKit };
@@ -0,0 +1,111 @@
1
+ import { Api, FetcherConfig, HttpClient } from '@mondaydotcomorg/trident-backend-api';
2
+ import { RecursivePartial } from '@mondaydotcomorg/monday-fetch-api';
3
+ import { getAttributionsFromApi } from './attributions-service';
4
+
5
+ import { APP_NAME, DEFAULT_FETCH_OPTIONS, ERROR_MESSAGES } from './constants';
6
+ import {
7
+ MembershipCreateResponse,
8
+ MembershipDeleteResponse,
9
+ MembershipForCreate,
10
+ MembershipForDelete,
11
+ } from 'types/memberships';
12
+ import { handleApiError } from 'utils/api-error-handler';
13
+
14
+ export class MembershipsService {
15
+ private static API_PATHS = {
16
+ UPSERT_RESOURCE_ATTRIBUTES: '/memberships/{accountId}',
17
+ DELETE_RESOURCE_ATTRIBUTES: '/memberships/{accountId}',
18
+ } as const;
19
+ private httpClient: HttpClient;
20
+ private fetchOptions: RecursivePartial<FetcherConfig>;
21
+
22
+ /**
23
+ * Public constructor to create the AuthorizationAttributesService instance.
24
+ * @param httpClient The HTTP client to use for API requests, if not provided, the default HTTP client from Api will be used.
25
+ * @param fetchOptions The fetch options to use for API requests, if not provided, the default fetch options will be used.
26
+ */
27
+ constructor(httpClient?: HttpClient, fetchOptions?: RecursivePartial<FetcherConfig>) {
28
+ if (!httpClient) {
29
+ httpClient = Api.getPart('httpClient');
30
+ if (!httpClient) {
31
+ throw new Error(ERROR_MESSAGES.HTTP_CLIENT_NOT_INITIALIZED);
32
+ }
33
+ }
34
+
35
+ if (!fetchOptions) {
36
+ fetchOptions = DEFAULT_FETCH_OPTIONS;
37
+ } else {
38
+ fetchOptions = {
39
+ ...DEFAULT_FETCH_OPTIONS,
40
+ ...fetchOptions,
41
+ };
42
+ }
43
+ this.httpClient = httpClient;
44
+ this.fetchOptions = fetchOptions;
45
+ }
46
+
47
+ /**
48
+ * Upsert memberships synchronously, performing http call to the authorization MS to assign the given memberships.
49
+ * @param accountId
50
+ * @param memberships - Array of memberships to upsert
51
+ * @returns MembershipCreateResponse - The affected (created and updated) memberships.
52
+ */
53
+ async upsertMemberships(accountId: number, memberships: MembershipForCreate[]): Promise<MembershipCreateResponse> {
54
+ const attributionHeaders = getAttributionsFromApi();
55
+ try {
56
+ return await this.httpClient.fetch<MembershipCreateResponse>(
57
+ {
58
+ url: {
59
+ appName: APP_NAME,
60
+ path: MembershipsService.API_PATHS.UPSERT_RESOURCE_ATTRIBUTES.replace('{accountId}', accountId.toString()),
61
+ },
62
+ method: 'PUT',
63
+ query: {
64
+ useAStyleRoleId: 'true',
65
+ },
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ ...attributionHeaders,
69
+ },
70
+ body: JSON.stringify({ memberships }),
71
+ },
72
+ this.fetchOptions
73
+ );
74
+ } catch (err) {
75
+ return handleApiError(err, 'authorization', 'upsertMemberships');
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Delete memberships synchronously, performing http call to the authorization MS to delete the given memberships.
81
+ * @param accountId
82
+ * @param resource - The resource (resourceType, resourceId) to delete the attributes for.
83
+ * @param attributeKeys - Array of attribute keys to delete for the resource.
84
+ * @returns ResourceAttributeResponse - The affected (deleted) resource attributes assignments in the `attributes` field.
85
+ */
86
+ async deleteMemberships(accountId: number, memberships: MembershipForDelete[]): Promise<MembershipDeleteResponse> {
87
+ const attributionHeaders = getAttributionsFromApi();
88
+ try {
89
+ return await this.httpClient.fetch<MembershipDeleteResponse>(
90
+ {
91
+ url: {
92
+ appName: APP_NAME,
93
+ path: MembershipsService.API_PATHS.DELETE_RESOURCE_ATTRIBUTES.replace('{accountId}', accountId.toString()),
94
+ },
95
+ method: 'DELETE',
96
+ query: {
97
+ useAStyleRoleId: 'true',
98
+ },
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ ...attributionHeaders,
102
+ },
103
+ body: JSON.stringify({ memberships }),
104
+ },
105
+ this.fetchOptions
106
+ );
107
+ } catch (err) {
108
+ return handleApiError(err, 'authorization', 'deleteMemberships');
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,71 @@
1
+ import { Metric } from '@mondaydotcomorg/monday-observability-kit';
2
+ import { logger } from './authorization-internal-service';
3
+
4
+ type ApiType = 'platform' | 'graph' | 'authorization';
5
+
6
+ interface InitializeMetricsOptions {
7
+ serviceName: string;
8
+ host?: string;
9
+ port?: number;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ let initialized = false;
14
+
15
+ export function initializeMetrics(options: InitializeMetricsOptions): void {
16
+ if (initialized) {
17
+ return;
18
+ }
19
+
20
+ const { serviceName } = options;
21
+ if (!serviceName) {
22
+ logger.warn({ tag: 'metrics-service' }, 'Metrics initialization skipped: serviceName is missing');
23
+ return;
24
+ }
25
+
26
+ const resolvedHost = options.host ?? process.env.DOGSTATSD_HOST ?? 'localhost';
27
+ const envPort = process.env.DOGSTATSD_PORT ? Number(process.env.DOGSTATSD_PORT) : undefined;
28
+ const resolvedPort = options.port ?? (Number.isFinite(envPort ?? NaN) ? envPort : undefined) ?? 8125;
29
+ const resolvedDisabled =
30
+ options.disabled ?? ['test', 'development'].includes((process.env.NODE_ENV ?? '').toLowerCase());
31
+
32
+ try {
33
+ Metric.initialize({
34
+ serviceName,
35
+ host: resolvedHost,
36
+ port: resolvedPort,
37
+ disabled: resolvedDisabled,
38
+ });
39
+ initialized = true;
40
+ } catch (error) {
41
+ logger.warn({ tag: 'metrics-service', error }, 'Failed to initialize metrics');
42
+ }
43
+ }
44
+
45
+ export function recordAuthorizationTiming(apiType: ApiType, duration: number, placement: string): void {
46
+ if (!initialized) {
47
+ return;
48
+ }
49
+
50
+ try {
51
+ Metric.distribution(`authorization.authorizationCheck.${apiType}.${placement}.duration`, duration);
52
+ } catch {
53
+ // ignore metric emission failures
54
+ }
55
+ }
56
+
57
+ export function recordAuthorizationError(apiType: ApiType, statusCode: number, placement: string): void {
58
+ if (!initialized) {
59
+ return;
60
+ }
61
+
62
+ try {
63
+ Metric.increment(
64
+ `authorization.authorizationCheck.${apiType}.${placement}.error`,
65
+ { statusCode: String(statusCode) },
66
+ 1
67
+ );
68
+ } catch {
69
+ // ignore metric emission failures
70
+ }
71
+ }
@@ -0,0 +1,51 @@
1
+ import { Action } from './types/general';
2
+
3
+ let prometheus: any = null;
4
+ let authorizationCheckResponseTimeMetric: any = null;
5
+
6
+ export const METRICS = {
7
+ AUTHORIZATION_CHECK: 'authorization_check',
8
+ AUTHORIZATION_CHECKS_PER_REQUEST: 'authorization_checks_per_request',
9
+ AUTHORIZATION_CHECK_RESPONSE_TIME: 'authorization_check_response_time',
10
+ };
11
+
12
+ const authorizationCheckResponseTimeMetricConfig = {
13
+ name: METRICS.AUTHORIZATION_CHECK_RESPONSE_TIME,
14
+ labels: ['resourceType', 'action', 'isAuthorized', 'responseStatus'],
15
+ description: 'Authorization check response time summary',
16
+ };
17
+
18
+ export function setPrometheus(customPrometheus) {
19
+ prometheus = customPrometheus;
20
+ if (!prometheus) {
21
+ return;
22
+ }
23
+ const { METRICS_TYPES } = prometheus;
24
+
25
+ authorizationCheckResponseTimeMetric = getMetricsManager().addMetric(
26
+ METRICS_TYPES.SUMMARY,
27
+ authorizationCheckResponseTimeMetricConfig.name,
28
+ authorizationCheckResponseTimeMetricConfig.labels,
29
+ authorizationCheckResponseTimeMetricConfig.description
30
+ );
31
+ }
32
+
33
+ export function getMetricsManager() {
34
+ return prometheus?.metricsManager;
35
+ }
36
+
37
+ export function sendAuthorizationCheckResponseTimeMetric(
38
+ resourceType: string,
39
+ action: Action,
40
+ isAuthorized: boolean,
41
+ responseStatus: number,
42
+ time: number
43
+ ) {
44
+ try {
45
+ if (authorizationCheckResponseTimeMetric) {
46
+ authorizationCheckResponseTimeMetric.labels(resourceType, action, isAuthorized, responseStatus).observe(time);
47
+ }
48
+ } catch (e) {
49
+ // ignore
50
+ }
51
+ }
@@ -0,0 +1,125 @@
1
+ import { Api, FetcherConfig, HttpClient } from '@mondaydotcomorg/trident-backend-api';
2
+ import { HttpFetcherError, RecursivePartial } from '@mondaydotcomorg/monday-fetch-api';
3
+ import { RoleCreateRequest, RolesResponse, RoleUpdateRequest } from 'types/roles';
4
+ import { getAttributionsFromApi } from 'attributions-service';
5
+ import { APP_NAME, DEFAULT_FETCH_OPTIONS, ERROR_MESSAGES } from './constants';
6
+
7
+ const API_PATH = '/roles/account/{accountId}';
8
+
9
+ export class RolesService {
10
+ private httpClient: HttpClient;
11
+ private fetchOptions: RecursivePartial<FetcherConfig>;
12
+ private attributionHeaders: { [key: string]: string };
13
+
14
+ /**
15
+ * Public constructor to create the AuthorizationAttributesService instance.
16
+ * @param httpClient The HTTP client to use for API requests, if not provided, the default HTTP client from Api will be used.
17
+ * @param fetchOptions The fetch options to use for API requests, if not provided, the default fetch options will be used.
18
+ */
19
+ constructor(httpClient?: HttpClient, fetchOptions?: RecursivePartial<FetcherConfig>) {
20
+ if (!httpClient) {
21
+ httpClient = Api.getPart('httpClient');
22
+ if (!httpClient) {
23
+ throw new Error(ERROR_MESSAGES.HTTP_CLIENT_NOT_INITIALIZED);
24
+ }
25
+ }
26
+
27
+ if (!fetchOptions) {
28
+ fetchOptions = DEFAULT_FETCH_OPTIONS;
29
+ } else {
30
+ fetchOptions = {
31
+ ...DEFAULT_FETCH_OPTIONS,
32
+ ...fetchOptions,
33
+ };
34
+ }
35
+ this.httpClient = httpClient;
36
+ this.fetchOptions = fetchOptions;
37
+ this.attributionHeaders = getAttributionsFromApi();
38
+ }
39
+
40
+ /**
41
+ * Get all roles for an account
42
+ * @param accountId - The account ID
43
+ * @param style - The style of the roles to return, either 'A' or 'B', default is 'A'. 'B' is not deprecated and only available for backward compatibility.
44
+ * @returns - The roles for the account, both basic and custom roles. Note that basic role ids are returned in A style and not B style.
45
+ */
46
+ async getRoles(accountId: number, resourceTypes: string[], style: 'A' | 'B' = 'A'): Promise<RolesResponse> {
47
+ return await this.sendRoleRequest('GET', accountId, {}, { resourceTypes, style });
48
+ }
49
+
50
+ /**
51
+ * Create a custom role for an account
52
+ * @param accountId - The account ID
53
+ * @param roles - The roles to create
54
+ * @returns - The created roles
55
+ * Note that basic role ids should be provided in A style and not in B style.
56
+ */
57
+ async createCustomRole(accountId: number, roles: RoleCreateRequest[]): Promise<RolesResponse> {
58
+ if (roles.length === 0) {
59
+ throw new Error('Roles array cannot be empty');
60
+ }
61
+
62
+ return await this.sendRoleRequest('PUT', accountId, {
63
+ customRoles: roles,
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Delete a custom role for an account
69
+ * @param accountId - The account ID
70
+ * @param roleIds - The ids of the roles to delete
71
+ * @returns - The deleted roles. Note that basic role ids should be provided in A style and not in B style.
72
+ */
73
+ async deleteCustomRole(accountId: number, roleIds: number[]): Promise<RolesResponse> {
74
+ return await this.sendRoleRequest('DELETE', accountId, {
75
+ ids: roleIds,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Update a custom role for an account
81
+ * @param accountId - The account ID
82
+ * @param updateRequests - The requests to update the roles
83
+ * @returns - The updated roles. Note that basic role ids should be provided in A style and not in B style.
84
+ */
85
+ async updateCustomRole(accountId: number, updateRequests: RoleUpdateRequest[]): Promise<RolesResponse> {
86
+ return await this.sendRoleRequest('PATCH', accountId, {
87
+ customRoles: updateRequests,
88
+ });
89
+ }
90
+
91
+ private async sendRoleRequest(
92
+ method: 'PUT' | 'GET' | 'DELETE' | 'PATCH',
93
+ accountId: number,
94
+ body: any,
95
+ additionalQueryParams: { [key: string]: any } = {},
96
+ style: 'A' | 'B' = 'A'
97
+ ): Promise<RolesResponse> {
98
+ try {
99
+ return await this.httpClient.fetch<RolesResponse>(
100
+ {
101
+ url: {
102
+ appName: APP_NAME,
103
+ path: API_PATH.replace('{accountId}', accountId.toString()),
104
+ },
105
+ query: {
106
+ style: style,
107
+ ...additionalQueryParams,
108
+ },
109
+ method,
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ ...this.attributionHeaders,
113
+ },
114
+ body: method === 'GET' ? undefined : body,
115
+ },
116
+ this.fetchOptions
117
+ );
118
+ } catch (err) {
119
+ if (err instanceof HttpFetcherError) {
120
+ throw new Error(ERROR_MESSAGES.REQUEST_FAILED('sendRoleRequest', err.status, err.message));
121
+ }
122
+ throw err;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,69 @@
1
+ import { Action, BaseRequest, BaseResponse, ContextGetter, Resource, ResourceGetter } from '../types/general';
2
+ import { defaultContextGetter } from '../authorization-middleware';
3
+ import { AuthorizationInternalService } from '../authorization-internal-service';
4
+ import type { NextFunction } from 'express';
5
+
6
+ export type TestPermittedAction = {
7
+ accountId: number;
8
+ userId: number;
9
+ resources: Resource[];
10
+ action: Action;
11
+ };
12
+
13
+ let testPermittedActions: TestPermittedAction[] = [];
14
+ export const addTestPermittedAction = (accountId: number, userId: number, resources: Resource[], action: Action) => {
15
+ testPermittedActions.push({ accountId, userId, resources, action });
16
+ };
17
+
18
+ export const clearTestPermittedActions = () => {
19
+ testPermittedActions = [];
20
+ };
21
+
22
+ const isActionAuthorized = (accountId: number, userId: number, resources: Resource[], action: Action) => {
23
+ // If no resources to check, deny access
24
+ if (resources.length === 0) {
25
+ return { isAuthorized: false };
26
+ }
27
+
28
+ return {
29
+ isAuthorized: resources.every(resource => {
30
+ return testPermittedActions.some(combination => {
31
+ return (
32
+ combination.accountId === accountId &&
33
+ combination.userId === userId &&
34
+ combination.action === action &&
35
+ combination.resources.some(combinationResource => {
36
+ return (
37
+ combinationResource.id === resource.id &&
38
+ combinationResource.type === resource.type &&
39
+ JSON.stringify(combinationResource.wrapperData) === JSON.stringify(resource.wrapperData)
40
+ );
41
+ })
42
+ );
43
+ });
44
+ }),
45
+ };
46
+ };
47
+
48
+ export const getTestAuthorizationMiddleware = (
49
+ action: Action,
50
+ resourceGetter: ResourceGetter,
51
+ contextGetter?: ContextGetter
52
+ ) => {
53
+ return async function authorizationMiddleware(
54
+ request: BaseRequest,
55
+ response: BaseResponse,
56
+ next: NextFunction
57
+ ): Promise<void> {
58
+ contextGetter ||= defaultContextGetter;
59
+ const { userId, accountId } = contextGetter(request);
60
+ const resources = resourceGetter(request);
61
+ const { isAuthorized } = isActionAuthorized(accountId, userId, resources, action);
62
+ if (!isAuthorized) {
63
+ response.status(403).json({ message: 'Access denied' });
64
+ return;
65
+ }
66
+ AuthorizationInternalService.markAuthorized(request);
67
+ next();
68
+ };
69
+ };
@@ -0,0 +1,33 @@
1
+ import { Resource } from './general';
2
+
3
+ export interface ResourceAttributeAssignment {
4
+ resourceType: Resource['type'];
5
+ resourceId: Resource['id'];
6
+ key: string;
7
+ value: string;
8
+ }
9
+
10
+ export interface ResourceAttributeResponse {
11
+ attributes: ResourceAttributeAssignment[];
12
+ }
13
+
14
+ export interface ResourceAttributeDelete {
15
+ resourceType: Resource['type'];
16
+ resourceId: Resource['id'];
17
+ key: string;
18
+ }
19
+
20
+ export enum ResourceAttributeOperationEnum {
21
+ UPSERT = 'upsert',
22
+ DELETE = 'delete',
23
+ }
24
+
25
+ interface UpsertResourceAttributeOperation extends ResourceAttributeAssignment {
26
+ operationType: ResourceAttributeOperationEnum.UPSERT;
27
+ }
28
+
29
+ interface DeleteResourceAttributeOperation extends ResourceAttributeDelete {
30
+ operationType: ResourceAttributeOperationEnum.DELETE;
31
+ }
32
+
33
+ export type ResourceAttributesOperation = UpsertResourceAttributeOperation | DeleteResourceAttributeOperation;
@@ -0,0 +1,8 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-unused-vars
2
+ declare namespace Express {
3
+ export interface Request {
4
+ payload: { accountId: number; userId: number };
5
+ authorizationCheckPerformed: boolean;
6
+ authorizationSkipPerformed: boolean;
7
+ }
8
+ }
@@ -0,0 +1,32 @@
1
+ import type { Request, Response } from 'express';
2
+
3
+ export interface Resource {
4
+ id?: number;
5
+ type: string;
6
+ wrapperData?: object;
7
+ }
8
+ export type Action = string;
9
+ export interface Context {
10
+ accountId: number;
11
+ userId: number;
12
+ }
13
+ export interface AuthorizationObject {
14
+ resource_id?: Resource['id'];
15
+ resource_type: Resource['type'];
16
+ wrapper_data?: Resource['wrapperData'];
17
+ action: Action;
18
+ }
19
+ export interface AuthorizationParams {
20
+ authorizationObjects: AuthorizationObject[];
21
+ }
22
+
23
+ type BasicObject = { [key: string]: unknown };
24
+
25
+ export type BaseParameters = BasicObject;
26
+ export type BaseResponseBody = BasicObject;
27
+ export type BaseBodyParameters = BasicObject;
28
+ export type BaseQueryParameters = BasicObject;
29
+ export type BaseRequest = Request<BaseParameters, BaseResponseBody, BaseBodyParameters, BaseQueryParameters>;
30
+ export type BaseResponse = Response<BaseResponseBody>;
31
+ export type ResourceGetter = (request: BaseRequest) => Resource[];
32
+ export type ContextGetter = (request: BaseRequest) => Context;
@@ -0,0 +1,25 @@
1
+ // Graph API related types and interfaces
2
+
3
+ export type ResourceType = string;
4
+ export type ResourceId = number;
5
+ export type ActionName = string;
6
+
7
+ export type GraphIsAllowedDto = Record<ResourceType, Record<ResourceId, ActionName[]>>;
8
+
9
+ export interface GraphPermissionReason {
10
+ key: string;
11
+ additionalOptions?: Record<string, string>;
12
+ technicalReason?: number;
13
+ }
14
+
15
+ export interface GraphPermissionResult {
16
+ can: boolean;
17
+ reason?: GraphPermissionReason;
18
+ }
19
+
20
+ // Permission to its result
21
+ export type GraphPermissionResults = Record<ActionName, GraphPermissionResult>;
22
+
23
+ // Resource type to map of resource ids to their permissions results
24
+ // Note: Resource IDs are string keys in the API response (JSON object keys are always strings)
25
+ export type GraphIsAllowedResponse = Record<ResourceType, Record<string, GraphPermissionResults>>;
@@ -0,0 +1,47 @@
1
+ export interface MembershipCreateRequest {
2
+ memberships: MembershipForCreate[];
3
+ }
4
+
5
+ export interface MembershipDeleteRequest {
6
+ memberships: MembershipForDelete[];
7
+ }
8
+
9
+ export interface MembershipForCreate {
10
+ entityId: number; // Which entity has the role?
11
+ entityType: string; // What type of entity is this?
12
+ resourceId: number; // What resource is this membership for?
13
+ resourceType: string; // What type of resource is this membership for?
14
+ roleId: number; // What role is this membership for?
15
+ roleType?: string; // What type of role is this membership for? (basic/custom)
16
+ addedById: number; // Who added this membership? for logging purposes
17
+ }
18
+
19
+ export interface MembershipForDelete {
20
+ entityId?: number; // Which entity has the role?
21
+ entityType: string; // What type of entity is this?
22
+ resourceId?: number; // What resource is this membership for?
23
+ resourceType: string; // What type of resource is this membership for?
24
+ }
25
+
26
+ export interface MembershipCreateResponse {
27
+ memberships: Membership[];
28
+ }
29
+
30
+ export interface MembershipDeleteResponse {
31
+ memberships: Membership[];
32
+ }
33
+
34
+ export interface Membership {
35
+ id: number;
36
+ entityId: number;
37
+ entityType: string;
38
+ resourceId: number;
39
+ resourceType: string;
40
+ roleId: number;
41
+ roleType: string;
42
+ addedById: null | number | undefined;
43
+ hops: number;
44
+ isNewRecord: boolean;
45
+ previousValues: Partial<Membership>;
46
+ walVersion: number | null | undefined;
47
+ }
@@ -0,0 +1,42 @@
1
+ export enum RoleType {
2
+ CUSTOM = 'custom_role',
3
+ BASIC = 'basic_role',
4
+ }
5
+
6
+ export interface CustomRole {
7
+ id?: number;
8
+ name: string;
9
+ resourceType: string;
10
+ resourceId: number;
11
+ basicRoleId: number;
12
+ basicRoleType: RoleType;
13
+ }
14
+
15
+ export interface BasicRole {
16
+ id: number;
17
+ resourceType: string;
18
+ roleType: string;
19
+ name: string;
20
+ }
21
+
22
+ export interface RolesResponse {
23
+ customRoles: CustomRole[];
24
+ basicRoles?: BasicRole[];
25
+ }
26
+
27
+ export interface RoleCreateRequest {
28
+ name: string;
29
+ resourceType: string;
30
+ resourceId: number;
31
+ sourceRole: {
32
+ id: number;
33
+ type: RoleType;
34
+ };
35
+ }
36
+
37
+ export interface RoleUpdateRequest {
38
+ id: number;
39
+ updateAttributes: {
40
+ name: string;
41
+ };
42
+ }