@transmit-security/rbac 1.0.0-beta

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @transmit-security/rbac
2
+
3
+ RBAC impl of Transmt sec
4
+
5
+ ### Usage
6
+
7
+ ```js
8
+ import { Permission, Action, AppResourceDefinition } from '@transmit-security/rbac';
9
+
10
+ const userPermission = new Permission(Action.read, AppResourceDefinition);
11
+
12
+ if (userPermission.allows([Action.write, AppResourceDefinition.type])) {
13
+ // perform the operation
14
+ } else {
15
+ // return error
16
+ }
17
+
18
+ ```
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Action = void 0;
4
+ var Action;
5
+ (function (Action) {
6
+ /** Create a resource. */
7
+ Action["create"] = "create";
8
+ /** Read a single resource. This should not allow searching or listing similar resources, only reading a specific one.
9
+ for example, this can be used for the `/users/:id` endpoint, but not for `/users` (at least not without additional actions).
10
+ */
11
+ Action["read"] = "read";
12
+ /** Edit a single resource. This should not allow deleting that resource or creating similar resources. */
13
+ Action["edit"] = "edit";
14
+ /** Delete resources. This can be used for bulk-delete as well. */
15
+ Action["delete"] = "delete";
16
+ /** List resources. This should be used for list and search endpoints, where resource IDs or resource details are returned without
17
+ * their IDs being known to the caller. for example, if the `/users` endpoint returns a list of all user IDs, this can be used to
18
+ * guard it. but this should not be used for `/users/:id`.
19
+ *
20
+ * If an endpoint returns not just resource IDs but also some details (e.g search users which returns name and email), then the
21
+ * endpoint should be guarded by the `read` permission as well as `list`.
22
+ */
23
+ Action["list"] = "list";
24
+ /** Execute a resource. Currently the only resources we can execute are FlexId journeys, but perhaps in the future we'll extend
25
+ * this capability to users who complain too much.
26
+ */
27
+ Action["execute"] = "execute";
28
+ /** All actions. This grants the ability to perform any of the other actions. */
29
+ Action["all"] = "*";
30
+ })(Action || (exports.Action = Action = {}));
31
+ //# sourceMappingURL=action.js.map
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdministrationResourceDefinition = void 0;
4
+ exports.AdministrationResourceDefinition = {
5
+ type: 'simple',
6
+ id: 'administration',
7
+ children: {
8
+ adminUsers: { type: 'collection', id: 'admin-users', key: 'adminId' },
9
+ adminRoles: { type: 'collection', id: 'admin-roles', key: 'roleId' },
10
+ },
11
+ };
12
+ //# sourceMappingURL=administration-resources.js.map
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppResourceDefinition = void 0;
4
+ exports.AppResourceDefinition = {
5
+ type: 'collection',
6
+ id: 'apps',
7
+ key: 'appId',
8
+ children: {
9
+ config: {
10
+ type: 'simple',
11
+ id: 'config',
12
+ children: {
13
+ idv: {
14
+ type: 'simple',
15
+ id: 'idv',
16
+ children: {
17
+ identityExperienceManagement: {
18
+ type: 'simple',
19
+ id: 'identity-experience-management',
20
+ children: {
21
+ settings: { type: 'simple', id: 'settings' },
22
+ consent: { type: 'simple', id: 'consent' },
23
+ branding: { type: 'simple', id: 'branding' },
24
+ },
25
+ },
26
+ identityRestrictionCriteria: {
27
+ type: 'simple',
28
+ id: 'identity-restriction-criteria',
29
+ },
30
+ },
31
+ },
32
+ drs: {
33
+ type: 'simple',
34
+ id: 'drs',
35
+ children: {
36
+ privateIdentifier: { type: 'simple', id: 'private-identifier' },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ };
43
+ //# sourceMappingURL=app-resources.js.map
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.drsResourceDefinition = void 0;
4
+ exports.drsResourceDefinition = {
5
+ type: 'simple',
6
+ id: 'drs',
7
+ children: {
8
+ recommendations: {
9
+ type: 'simple',
10
+ id: 'recommendations',
11
+ children: {
12
+ export: { type: 'simple', id: 'export' },
13
+ },
14
+ },
15
+ list: {
16
+ type: 'collection',
17
+ id: 'list',
18
+ key: 'listId',
19
+ children: {
20
+ items: { type: 'collection', id: 'items', key: 'itemId' },
21
+ },
22
+ },
23
+ label: {
24
+ type: 'simple',
25
+ id: 'label',
26
+ children: {
27
+ bulk: { type: 'simple', id: 'bulk' },
28
+ },
29
+ },
30
+ customActionType: { type: 'simple', id: 'custom-action-type' },
31
+ detectionSensitivityConfiguration: {
32
+ type: 'simple',
33
+ id: 'detection-sensitivity-configuration',
34
+ children: {
35
+ preview: { type: 'simple', id: 'preview' },
36
+ production: { type: 'simple', id: 'production' },
37
+ },
38
+ },
39
+ rule: {
40
+ type: 'collection',
41
+ id: 'rule',
42
+ key: 'ruleId',
43
+ children: {
44
+ preview: { type: 'simple', id: 'preview' },
45
+ production: { type: 'simple', id: 'production' },
46
+ },
47
+ },
48
+ preview: { type: 'simple', id: 'preview' },
49
+ securityInsights: { type: 'simple', id: 'security-insights' },
50
+ automatedWorkflows: { type: 'simple', id: 'automated-workflows' },
51
+ },
52
+ };
53
+ //# sourceMappingURL=drs-resources.js.map
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsResourceDefinition = void 0;
4
+ exports.EventsResourceDefinition = {
5
+ type: 'simple',
6
+ id: 'events',
7
+ children: {
8
+ userActivities: {
9
+ type: 'collection',
10
+ id: 'user-activities',
11
+ key: 'userActivityId',
12
+ },
13
+ adminActivities: {
14
+ type: 'collection',
15
+ id: 'admin-activities',
16
+ key: 'adminActivityId',
17
+ },
18
+ },
19
+ };
20
+ //# sourceMappingURL=events-resources.js.map
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.idvResourceDefinition = void 0;
4
+ exports.idvResourceDefinition = {
5
+ type: 'simple',
6
+ id: 'idv',
7
+ children: {
8
+ identityVerifications: {
9
+ type: 'collection',
10
+ id: 'identity-verifications',
11
+ key: 'verificationId',
12
+ children: {
13
+ status: { type: 'simple', id: 'status' },
14
+ images: { type: 'collection', id: 'images', key: 'imageId' },
15
+ result: { type: 'simple', id: 'result' },
16
+ labels: { type: 'simple', id: 'labels' },
17
+ },
18
+ },
19
+ blocklist: {
20
+ type: 'simple',
21
+ id: 'blocklist',
22
+ children: {
23
+ faces: { type: 'collection', id: 'faces', key: 'entryId' },
24
+ },
25
+ },
26
+ analytics: {
27
+ type: 'simple',
28
+ id: 'analytics',
29
+ },
30
+ },
31
+ };
32
+ //# sourceMappingURL=idv-resources.js.map
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oidcResources = exports.serviceProviders = exports.orc = exports.drs = exports.events = exports.idv = exports.users = exports.organization = exports.administration = exports.apps = void 0;
4
+ const resource_class_builder_1 = require("../../infra/resource-class-builder");
5
+ const administration_resources_1 = require("./administration-resources");
6
+ const app_resources_1 = require("./app-resources");
7
+ const organization_resources_1 = require("./organization-resources");
8
+ const user_resources_1 = require("./user-resources");
9
+ const idv_resources_1 = require("./idv-resources");
10
+ const events_resources_1 = require("./events-resources");
11
+ const drs_resources_1 = require("./drs-resources");
12
+ const service_provider_resources_1 = require("./service-provider-resources");
13
+ const oidc_resource_resources_1 = require("./oidc-resource-resources");
14
+ const orc_resources_1 = require("./orc-resources");
15
+ /*
16
+ Resources definition are split into files for better change isolation. as
17
+ this project matures, we can consider merging all definitions into a single
18
+ file for ease of use.
19
+ */
20
+ exports.apps = (0, resource_class_builder_1.buildResourceClass)(app_resources_1.AppResourceDefinition);
21
+ exports.administration = (0, resource_class_builder_1.buildResourceClass)(administration_resources_1.AdministrationResourceDefinition);
22
+ exports.organization = (0, resource_class_builder_1.buildResourceClass)(organization_resources_1.organizationsResourceDefinition);
23
+ exports.users = (0, resource_class_builder_1.buildResourceClass)(user_resources_1.userResourceDefinition);
24
+ exports.idv = (0, resource_class_builder_1.buildResourceClass)(idv_resources_1.idvResourceDefinition);
25
+ exports.events = (0, resource_class_builder_1.buildResourceClass)(events_resources_1.EventsResourceDefinition);
26
+ exports.drs = (0, resource_class_builder_1.buildResourceClass)(drs_resources_1.drsResourceDefinition);
27
+ exports.orc = (0, resource_class_builder_1.buildResourceClass)(orc_resources_1.orcResourceDefinitions);
28
+ exports.serviceProviders = (0, resource_class_builder_1.buildResourceClass)(service_provider_resources_1.serviceProviderResourceDefinition);
29
+ exports.oidcResources = (0, resource_class_builder_1.buildResourceClass)(oidc_resource_resources_1.oidcResourceResourceDefinition);
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oidcResourceResourceDefinition = void 0;
4
+ exports.oidcResourceResourceDefinition = {
5
+ type: 'collection',
6
+ id: 'resources',
7
+ key: 'resourceId',
8
+ };
9
+ //# sourceMappingURL=oidc-resource-resources.js.map
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.orcResourceDefinitions = void 0;
4
+ exports.orcResourceDefinitions = {
5
+ type: 'simple',
6
+ id: 'orc',
7
+ children: {
8
+ journeys: {
9
+ type: 'simple',
10
+ id: 'journeys',
11
+ },
12
+ connections: {
13
+ type: 'simple',
14
+ id: 'connections',
15
+ },
16
+ lists: {
17
+ type: 'simple',
18
+ id: 'lists',
19
+ },
20
+ identities: {
21
+ type: 'simple',
22
+ id: 'identities',
23
+ },
24
+ },
25
+ };
26
+ //# sourceMappingURL=orc-resources.js.map
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.organizationsResourceDefinition = void 0;
4
+ exports.organizationsResourceDefinition = {
5
+ type: 'simple',
6
+ id: 'organizations',
7
+ children: {
8
+ orgs: { type: 'simple', id: 'orgs' },
9
+ roles: { type: 'simple', id: 'roles' },
10
+ },
11
+ };
12
+ //# sourceMappingURL=organization-resources.js.map
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.serviceProviderResourceDefinition = void 0;
4
+ exports.serviceProviderResourceDefinition = {
5
+ type: 'collection',
6
+ id: 'service-providers',
7
+ key: 'serviceProviderId',
8
+ };
9
+ //# sourceMappingURL=service-provider-resources.js.map
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.userResourceDefinition = void 0;
4
+ exports.userResourceDefinition = {
5
+ type: 'collection',
6
+ id: 'users',
7
+ key: 'userId',
8
+ };
9
+ //# sourceMappingURL=user-resources.js.map
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.wildcardPrimitive = exports.wildcard = exports.resourceClassRegistry = exports.build = exports.Resource = exports.placeholder = exports.Permission = exports.Action = exports.permissions = exports.resources = void 0;
27
+ const resources = __importStar(require("./definitions/resources"));
28
+ exports.resources = resources;
29
+ const permission_selector_builder_1 = require("./infra/permission-selector-builder");
30
+ /** A reference to all the actions and resources in the RBAC system. You can
31
+ * use this instead of creating permissions and resource instances
32
+ * manually.
33
+ *
34
+ * example:
35
+ * ```
36
+ * permissions.create.Users()
37
+ * ```*/
38
+ exports.permissions = (0, permission_selector_builder_1.buildPermissionSelector)(resources);
39
+ var action_1 = require("./definitions/action");
40
+ Object.defineProperty(exports, "Action", { enumerable: true, get: function () { return action_1.Action; } });
41
+ var permission_1 = require("./infra/permission");
42
+ Object.defineProperty(exports, "Permission", { enumerable: true, get: function () { return permission_1.Permission; } });
43
+ var placeHolders_1 = require("./infra/placeHolders");
44
+ Object.defineProperty(exports, "placeholder", { enumerable: true, get: function () { return placeHolders_1.placeholder; } });
45
+ var resource_1 = require("./infra/resource");
46
+ Object.defineProperty(exports, "Resource", { enumerable: true, get: function () { return resource_1.Resource; } });
47
+ exports.build = __importStar(require("./infra/resource-class-builder"));
48
+ var resource_registry_1 = require("./infra/resource-registry");
49
+ Object.defineProperty(exports, "resourceClassRegistry", { enumerable: true, get: function () { return resource_registry_1.resourceClassRegistry; } });
50
+ var wildcard_1 = require("./infra/wildcard");
51
+ Object.defineProperty(exports, "wildcard", { enumerable: true, get: function () { return wildcard_1.wildcard; } });
52
+ Object.defineProperty(exports, "wildcardPrimitive", { enumerable: true, get: function () { return wildcard_1.wildcardPrimitive; } });
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPermissionSelector = void 0;
4
+ const action_1 = require("../definitions/action");
5
+ const permission_1 = require("./permission");
6
+ const placeHolders_1 = require("./placeHolders");
7
+ const wildcard_1 = require("./wildcard");
8
+ function buildPermissionTree(resourceClass, action, parentKeys, keyValues) {
9
+ const result = {};
10
+ let childKeys = { ...parentKeys };
11
+ let entityKeys = { ...parentKeys };
12
+ if (resourceClass.definition.type === 'collection') {
13
+ let keyValue = keyValues
14
+ ? keyValues[resourceClass.definition.key]
15
+ : undefined;
16
+ if (keyValue === placeHolders_1.placeholder) {
17
+ keyValue = (0, placeHolders_1.getPlaceHolderString)(resourceClass.definition.key);
18
+ }
19
+ childKeys[resourceClass.definition.key] = keyValue || wildcard_1.wildcard;
20
+ entityKeys[resourceClass.definition.key] = keyValue || undefined;
21
+ }
22
+ Object.getOwnPropertyNames(resourceClass).forEach((propName) => {
23
+ const value = resourceClass[propName];
24
+ if (typeof value === 'function') {
25
+ if ('definition' in value) {
26
+ result[propName] = buildPermissionTree(value, action, childKeys, keyValues);
27
+ }
28
+ }
29
+ });
30
+ childKeys['childPermission'] = resourceClass.definition.id;
31
+ const selectable = {
32
+ permission: new permission_1.Permission(action, new resourceClass(entityKeys)),
33
+ childPermission: new permission_1.Permission(action, new resourceClass(childKeys)),
34
+ };
35
+ Object.assign(result, selectable);
36
+ return result;
37
+ }
38
+ function buildPermissionSelector(resourceTree, keyValues) {
39
+ return Object.entries(action_1.Action).reduce((selector, [actionName, actionValue]) => {
40
+ const resources = Object.entries(resourceTree).reduce((acc, [key, value]) => {
41
+ const tree = buildPermissionTree(value, actionValue, {}, keyValues);
42
+ acc[key] = tree;
43
+ return acc;
44
+ }, {});
45
+ selector[actionName] = resources;
46
+ return selector;
47
+ }, {});
48
+ }
49
+ exports.buildPermissionSelector = buildPermissionSelector;
50
+ //# sourceMappingURL=permission-selector-builder.js.map
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Permission = void 0;
4
+ const action_1 = require("../definitions/action");
5
+ const is_valid_enum_value_1 = require("../utils/is-valid-enum-value");
6
+ const resource_1 = require("./resource");
7
+ class Permission {
8
+ constructor(action, resource) {
9
+ this.action = action;
10
+ this.resource = resource;
11
+ }
12
+ static parse(permissionString) {
13
+ const segments = permissionString.split(':');
14
+ if (segments.length !== 2 || segments.some((s) => s.length === 0))
15
+ return {
16
+ success: false,
17
+ error: `Invalid format - should be 'action:resource'`,
18
+ };
19
+ const action = segments[0];
20
+ if (!(0, is_valid_enum_value_1.isValidEnumValue)(action_1.Action, action))
21
+ return { success: false, error: `Invalid action: '${segments[0]}'` };
22
+ const parseRes = resource_1.Resource.parse(segments[1]);
23
+ if (!parseRes.success)
24
+ return {
25
+ success: false,
26
+ error: `Cannot parse resource: ${parseRes.error}`,
27
+ };
28
+ const { resource } = parseRes;
29
+ return {
30
+ success: true,
31
+ permission: new Permission(action, resource),
32
+ };
33
+ }
34
+ /** Checks if this permission is granted by any of the provided permissions.
35
+ *
36
+ * When permission validation fails, it's recommended to log the required and provided permissions for easy debugging.
37
+ *
38
+ * note: by default, invalid permissions are ignored. you can adjust this behavior using the options parameter, or
39
+ * parse and check the permissions yourself using `Permission.parse`.
40
+ */
41
+ isAllowedBy(providedPermissions, options) {
42
+ for (const permission of providedPermissions) {
43
+ if (typeof permission === 'string') {
44
+ const parseRes = Permission.parse(permission);
45
+ if (!parseRes.success) {
46
+ if (options.onParsingError === 'throw') {
47
+ throw new Error(parseRes.error);
48
+ }
49
+ }
50
+ else if (parseRes.permission.allows(this)) {
51
+ return true;
52
+ }
53
+ }
54
+ else if (permission.allows(this)) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ /** Checks if this permission allows the operation described by the other permission.
61
+ *
62
+ * example:
63
+ * ```
64
+ * if (tokenPermission.allows(requiredPermission)) {
65
+ * // perform the operation
66
+ * } else {
67
+ * // return error
68
+ * }
69
+ * ```
70
+ */
71
+ allows(other) {
72
+ if (this.action !== action_1.Action.all && this.action !== other.action)
73
+ return false;
74
+ if (!this.resource.owns(other.resource))
75
+ return false;
76
+ return true;
77
+ }
78
+ toString() {
79
+ return `${this.action}:${this.resource.toString()}`;
80
+ }
81
+ }
82
+ exports.Permission = Permission;
83
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPlaceHolderString = exports.placeholder = void 0;
4
+ exports.placeholder = 'PLACEHOLDER';
5
+ const getPlaceHolderString = (key) => `[${key}]`;
6
+ exports.getPlaceHolderString = getPlaceHolderString;
7
+ //# sourceMappingURL=placeHolders.js.map
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ /*
3
+
4
+ The purpose of this file is to provide a way to build resource classes, from a
5
+ data structure that's as simple as possible.
6
+
7
+ ## General idea
8
+
9
+ recommended reading:
10
+ https://cloud.google.com/apis/design/resource_names
11
+
12
+ Resource classes are typescript classes which represent a specific type of resources,
13
+ and are used to create instances of those resources for things like permission checks
14
+ or data access. At the lowest level a resource is just a string, but we want to provide
15
+ a move convenient mechanism to work with them.
16
+
17
+ Resource classes have several requirements:
18
+
19
+ 1. provide a string that represents the resource
20
+ 2. ensure all placeholder values are filled in when creating an instance
21
+ e.g if the resource is `apps/$appId/users/$userId`, then when creating an instance,
22
+ both `appId` and `userId` must be provided.
23
+ 3. provide a way to reference a resource class (not instance) via a string
24
+ i.e if a client tells a server "I want to access the `users` resource", the server
25
+ should be able to understand which resource class that is.
26
+ 4. provide a convenient way to create child resources from a parent resource
27
+
28
+
29
+ The end result is a bunch of classes that look kinda like this:
30
+ ```
31
+ class Apps {
32
+ constructor(public appId: string) { }
33
+ toString() { return `apps/${this.appId}` }
34
+
35
+ users(userId: string) { return new AppUsers(this.appId, userId); }
36
+ }
37
+
38
+ class AppUsers {
39
+ constructor(public appId: string, public userId: string) { }
40
+ toString() { return `apps/${this.appId}/users/${this.userId}` }
41
+ }
42
+ ```
43
+
44
+ the ONLY thing that matters is that end result. the implementation of the
45
+ builder is free to change and evolve - we can even generate the classes
46
+ at build time using a code generator, or even remove automatic generation
47
+ entirely and just write the classes manually.
48
+
49
+ ## Implementation
50
+
51
+ The implementation is based on a recursive function that builds the classes
52
+ from a data structure that represents the resource.
53
+
54
+ Due to all the type manipulation, the implementation is a bit complex -
55
+ we have the types which facilitate the type manipulation, and the actual code
56
+ that builds the classes. That's 2 parallel flows of data. We try to use them
57
+ together as much as possible to get type safety, but in some cases it's
58
+ technically impossible to get type info, so we resort to `any` and make sure
59
+ that area covered by tests.
60
+
61
+ The entry point for the type definitions is the `ResourceClassFromDef` type,
62
+ which takes in a `ResourceDefNode` type and builds a class from it, with the
63
+ the collections keys and child resource if any. see the jsdoc on the type
64
+ itself for details.
65
+
66
+ The entry point for the actual code is the `buildResourceClass` function,
67
+ which takes in a `ResourceDefNode` instance and returns a class built from it,
68
+ also with the correct collection keys and child resources. here too, see the
69
+ jsdoc for details.
70
+
71
+ ## Some future ideas
72
+
73
+ ### non-class references
74
+
75
+ there's no real reason that `Apps.Users` (for example) has to be the users
76
+ class - all we need from it is the template, since we can build an instance
77
+ using `new Apps().users()`. Populating those props with something that holds
78
+ a template string and child objects, instead of a class, will make them easier
79
+ to explore since they won't have the other methods that functions have.
80
+
81
+ ### inherently type-checked collection keys
82
+
83
+ when building the type of an anon object (`{a:"str"}`), typescript sets the
84
+ type of primitive properties to their full type even when a fixed value is
85
+ provided. so the example's type would be `{a: string}` and not `{a:"str"}`.
86
+ for this reason, users of the builder have to add `as const` to their
87
+ definitions - without it, collection keys would be just any string and not
88
+ specifically the provided key.
89
+ we could instead change the api to get collection keys as object keys, not as
90
+ strings: `{key: {userId: string}}` instead of `{key: "userId"}`. with this,
91
+ there's no need to `as const` since string fields are not used for type
92
+ inference.
93
+
94
+
95
+ ### id-based resource types
96
+
97
+ instead of simple and collection resource definitions, we can use ids like
98
+ `apps` and `apps/$appId` and automatically determine if it's a collection.
99
+ see https://www.typescriptlang.org/play/?#code/C4TwDgpgBAyglgWzAGwgSQCZQLxQM7ABOcAdgOYBQokUAwgPbKoDGwc9JmAPAHICGCaBAAewCCQx58RUmQA0UANIQQ-QVBFiJUgsXIA+HFAAGAEgDeaiAF8A9KYvLVAm8YruAZgFcSrdiSgyCGAuABUNUXFJOkYWNg5uPhIQBSSQQwAfWEQUdAx9AAoKKBLpQgAuKFCKAEpK8M0oqQYmCD8EjC5SDwhCKCsFbt6lFStDYtKAfihzaghKgCJmWLb4kgWFEhdKgagAa1HtkedBaygJksrzKDnFvBzUDagtwXqzinMLqEJgr0IA8xnPhSNIUazuCjLEgEfAPaC4ILAAoLLx4Xp4BY1Ci2WylPEAPUmkI4MOWrXaAQRwWRqPR9lphEwmOxuIJRIhQA
100
+ this will make the definitions a lot simpler, but is also a bit less discoverable -
101
+ the type system won't tell you that you can create a collection that way.
102
+ can consider this when there are more resources to use as reference.
103
+
104
+ ### fluent builder
105
+
106
+ instead of passing an object to the constructor, we can use a fluent builder.
107
+ this will allow greater flexibility in the future, and will avoid the need for `as const`.
108
+ downside is that it can get pretty verbose, and will require a lot of boilerplate.
109
+
110
+
111
+ */
112
+ Object.defineProperty(exports, "__esModule", { value: true });
113
+ exports.InvalidKeyForCollection = exports.MissingKeyForCollection = exports.buildResourceClass = void 0;
114
+ const resource_1 = require("./resource");
115
+ const resource_registry_1 = require("./resource-registry");
116
+ const wildcard_1 = require("./wildcard");
117
+ function getTemplatePath(res) {
118
+ return res.type === 'simple' ? [res.id] : [res.id, `$${res.key}`];
119
+ }
120
+ function buildResourceClassCommon(source, parentDefinitions) {
121
+ const parentPath = parentDefinitions.map(getTemplatePath).flat();
122
+ const fullPath = [...parentPath, ...getTemplatePath(source)];
123
+ return {
124
+ template: fullPath,
125
+ templateStr: fullPath.join(resource_1.Resource.separator),
126
+ definition: source,
127
+ };
128
+ }
129
+ function buildResourceConstructor(source, parents) {
130
+ switch (source.type) {
131
+ case 'simple':
132
+ return class SimpleResource extends resource_1.Resource {
133
+ constructor(input) {
134
+ super(buildResourcePath([...parents, source], input));
135
+ }
136
+ };
137
+ case 'collection':
138
+ return class CollectionResource extends resource_1.Resource {
139
+ constructor(input) {
140
+ super(buildResourcePath([...parents, source], input));
141
+ }
142
+ };
143
+ }
144
+ }
145
+ /** Builds the properties that provide the child resource.
146
+ *
147
+ * e.g with `new Apps.Users({appId:"123", userId:"456"})` - this function
148
+ * had built the `Users` static property on the `Apps` class.
149
+ *
150
+ * The returned object should be merged with the parent class.
151
+ */
152
+ function buildChildResourceStaticProperties(childClasses) {
153
+ const propMap = {};
154
+ for (const [key, childClass] of Object.entries(childClasses)) {
155
+ propMap[key] = { get: () => childClass };
156
+ }
157
+ return propMap;
158
+ }
159
+ /** Builds the methods that create child resource from a parent resource.
160
+ *
161
+ * e.g with `new Apps("someAppId").users("someUserId")` - this function
162
+ * had built the `users` method on the `Apps` class.
163
+ *
164
+ * The returned object should be merged with the parent class prototype.
165
+ */
166
+ function buildChildResourceInstanceMethods(childClasses, definitions) {
167
+ const propMap = {};
168
+ for (const [key, childClass] of Object.entries(childClasses)) {
169
+ const childDefinition = childClass.definition;
170
+ propMap[key] = {
171
+ get() {
172
+ let input = gatherInput(this, definitions);
173
+ return childDefinition.type == 'simple'
174
+ ? (childResource) => new childClass({ ...input, childResource })
175
+ : (id, childResource) => new childClass({
176
+ ...input,
177
+ [childDefinition.key]: id,
178
+ childResource,
179
+ });
180
+ },
181
+ };
182
+ }
183
+ return propMap;
184
+ }
185
+ function build(source, parentDefinitions) {
186
+ /*
187
+ we're gonna do dynamic stuff here, so to maintain type safety as much as possible,
188
+ we'll create the components of a resource class separately, and then combine them.
189
+ */
190
+ const resourceClass = buildResourceConstructor(source, parentDefinitions);
191
+ Object.assign(resourceClass, buildResourceClassCommon(source, parentDefinitions));
192
+ if (source.children) {
193
+ const childParents = [...parentDefinitions, source];
194
+ const childClassesRecord = Object.entries(source.children).reduce((acc, [key, childDefinition]) => {
195
+ acc[key] = build(childDefinition, childParents);
196
+ return acc;
197
+ }, {});
198
+ Object.defineProperties(resourceClass, buildChildResourceStaticProperties(childClassesRecord));
199
+ Object.defineProperties(resourceClass.prototype, buildChildResourceInstanceMethods(childClassesRecord, childParents));
200
+ }
201
+ resource_registry_1.resourceClassRegistry.register(resourceClass.templateStr);
202
+ return resourceClass;
203
+ }
204
+ /** returns an object with the inputs used to build it and each parent.
205
+ * simple (non-collection) resources are not included in the output since they don't have inputs.
206
+ *
207
+ * example -
208
+ * for resource `apps/123/users/456` (assuming template `apps/$appId/users/$userId`),
209
+ * this will return `{appId: "123", userId: "456"}`
210
+ */
211
+ function gatherInput(source, definitions) {
212
+ const remainingPath = [...source.path];
213
+ return definitions.reduce((acc, def) => {
214
+ remainingPath.shift();
215
+ if (def.type === 'collection') {
216
+ const key = remainingPath.shift();
217
+ if (key === wildcard_1.wildcardPrimitive) {
218
+ acc[def.key] = wildcard_1.wildcard;
219
+ }
220
+ else {
221
+ acc[def.key] = key;
222
+ }
223
+ }
224
+ return acc;
225
+ }, {});
226
+ }
227
+ function buildResourcePath(resources, input = {}) {
228
+ const lastResource = resources[resources.length - 1];
229
+ return resources.reduce((acc, res) => {
230
+ if (res.type === 'simple') {
231
+ if (input.childPermission === res.id ||
232
+ (res.id === lastResource.id && input.childResource)) {
233
+ return [...acc, res.id, wildcard_1.wildcardPrimitive];
234
+ }
235
+ return [...acc, res.id];
236
+ }
237
+ else {
238
+ if (!(res.key in input))
239
+ return [...acc, res.id];
240
+ const key = input[res.key];
241
+ if (key == '' || key == null)
242
+ return [...acc, res.id];
243
+ if (key === wildcard_1.wildcard)
244
+ return [...acc, res.id, wildcard_1.wildcardPrimitive];
245
+ const isValid = (0, resource_1.validateResourceSegment)(key);
246
+ if (!isValid)
247
+ throw new InvalidKeyForCollection(res.key, key);
248
+ if ((res.id === lastResource.id && input.childResource) ||
249
+ input.childPermission === res.id) {
250
+ return [...acc, res.id, key, wildcard_1.wildcardPrimitive];
251
+ }
252
+ return [...acc, res.id, key];
253
+ }
254
+ }, []);
255
+ }
256
+ /** Build a resource class from a definition.
257
+ *
258
+ * - remember to add `as const` to the input object, to get accurate auto-complete.
259
+ * - for resource names, use valid javascript identifiers - i.e adminUsers and not 'admin-users'. those identifies are mapped to properties, so non-standard names would make them harder to access.
260
+ */
261
+ function buildResourceClass(resourceDef) {
262
+ return build(resourceDef, []);
263
+ }
264
+ exports.buildResourceClass = buildResourceClass;
265
+ class MissingKeyForCollection extends Error {
266
+ constructor(keyName) {
267
+ super(`No key for collection; keyName: '${String(keyName)}'`);
268
+ }
269
+ }
270
+ exports.MissingKeyForCollection = MissingKeyForCollection;
271
+ class InvalidKeyForCollection extends Error {
272
+ constructor(keyName, keyValue) {
273
+ super(`Invalid key for collection; keyName: '${String(keyName)}', value: '${keyValue}'`);
274
+ }
275
+ }
276
+ exports.InvalidKeyForCollection = InvalidKeyForCollection;
277
+ //# sourceMappingURL=resource-class-builder.js.map
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resourceClassRegistry = void 0;
4
+ class ResourceClassRegistry {
5
+ constructor() {
6
+ this.templates = [];
7
+ }
8
+ register(name) {
9
+ this.templates.push(name);
10
+ }
11
+ getTemplates() {
12
+ return [...this.templates];
13
+ }
14
+ }
15
+ /* not very DI-like but for this specific use case (library, registry of fixed
16
+ values) it's fine.
17
+
18
+ if we do want to play nice with DI, we'll need to have the registry provided
19
+ to the resource creation externally, and have the DI provide it.
20
+ */
21
+ exports.resourceClassRegistry = new ResourceClassRegistry();
22
+ //# sourceMappingURL=resource-registry.js.map
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Resource = exports.validateResourceSegment = exports.segmentMatcher = void 0;
4
+ const wildcard_1 = require("./wildcard");
5
+ exports.segmentMatcher = /^[a-zA-Z0-9_-]+$/;
6
+ function validateResourceSegment(segment, options) {
7
+ if ((options === null || options === void 0 ? void 0 : options.allowWildcard) && segment === wildcard_1.wildcardPrimitive)
8
+ return true;
9
+ if (segment === undefined)
10
+ return true;
11
+ if (segment.startsWith('[') && segment.endsWith(']')) {
12
+ segment = segment.slice(1, -1); // remove template brackets
13
+ }
14
+ return exports.segmentMatcher.test(segment);
15
+ }
16
+ exports.validateResourceSegment = validateResourceSegment;
17
+ class Resource {
18
+ constructor(path) {
19
+ this.path = path;
20
+ }
21
+ static parse(str) {
22
+ const path = str.split(Resource.separator);
23
+ if (path.length === 0) {
24
+ return { success: false, error: 'Resource path is empty' };
25
+ }
26
+ const emptySegments = path.filter((segment) => segment === '');
27
+ if (emptySegments.length > 0) {
28
+ return { success: false, error: `Resource path contains empty segments` };
29
+ }
30
+ const invalidSegments = path.filter((segment) => !validateResourceSegment(segment, { allowWildcard: true }));
31
+ if (invalidSegments.length > 0) {
32
+ return {
33
+ success: false,
34
+ error: 'Resource path contains invalid segments',
35
+ invalidSegments,
36
+ validationRegex: exports.segmentMatcher,
37
+ };
38
+ }
39
+ return {
40
+ success: true,
41
+ resource: new Resource(path),
42
+ };
43
+ }
44
+ /** checks if this resource owns `other`. i.e if `other` is a subset of this resource.
45
+ *
46
+ * examples:
47
+ * - `apps/123/users` does not own `apps/123/users/456`
48
+ * - `apps/123/users/*` owns `apps/123/users/456`
49
+ * - `apps/123/users` does not own `apps/123`
50
+ * - `apps/123/users` owns `apps/123/users` (itself)
51
+ *
52
+ * it's like `israel/tel-aviv/*` owns `israel/tel-aviv/alenby`, but not the other way around.
53
+ * and `israel/tel-aviv` does not own `israel/tel-aviv/alenby`, it owns only itself.
54
+ */
55
+ owns(other) {
56
+ const maxPathLength = Math.max(this.path.length, other.path.length);
57
+ // safeguard against unintentional global permission. should be explicit `*`
58
+ if (other.path.length === 0 || other.path[0] === '')
59
+ return false;
60
+ for (let i = 0; i < maxPathLength; i++) {
61
+ const thisSegment = this.path[i];
62
+ const otherSegment = other.path[i];
63
+ // e.g this is `apps/123` does not own `apps/123/users`.
64
+ // but 'apps/123/*' owns 'apps/123/users/567' , 'apps/*' owns 'apps/123/users/567' or 'apps/*/users' etc.
65
+ if (!thisSegment) {
66
+ if (this.path[i - 1] === wildcard_1.wildcardPrimitive) {
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ // if `other` ended, but `this` didn't, then `this` doesn't contain `other`.
72
+ if (!otherSegment)
73
+ return false;
74
+ // if this segment is wildcard, no need for additional checks - it's a match.
75
+ // we only check further if it's not.
76
+ if (thisSegment !== wildcard_1.wildcardPrimitive) {
77
+ // if this segment isn't wildcard, but other is, then this can't possibly contain other.
78
+ if (otherSegment === wildcard_1.wildcardPrimitive)
79
+ return false;
80
+ // if the segments don't match, we can stop checking.
81
+ if (thisSegment !== otherSegment)
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+ toString() {
88
+ return this.path.join(Resource.separator);
89
+ }
90
+ }
91
+ exports.Resource = Resource;
92
+ Resource.separator = '/';
93
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wildcard = exports.wildcardPrimitive = void 0;
4
+ exports.wildcardPrimitive = '*';
5
+ exports.wildcard = Symbol('*');
6
+ //# sourceMappingURL=wildcard.js.map
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isValidEnumValue = void 0;
4
+ function isValidEnumValue(enumType, value) {
5
+ return Object.values(enumType).includes(value);
6
+ }
7
+ exports.isValidEnumValue = isValidEnumValue;
8
+ //# sourceMappingURL=is-valid-enum-value.js.map
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@transmit-security/rbac",
3
+ "private": false,
4
+ "description": "RBAC impl of Transmt sec",
5
+ "version": "1.0.0-beta",
6
+ "main": "dist/ui.es.js",
7
+ "module": "dist/ui.es.js",
8
+ "author": "htrs-sec",
9
+ "repository": "https://www.github.com/htrs-sec/@transmit-security/rbac",
10
+ "license": "MIT",
11
+ "files": [
12
+ "dist",
13
+ "scripts"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc"
20
+ },
21
+ "devDependencies": {
22
+ "husky": "9.1.4",
23
+ "lint-staged": "13.1.4",
24
+ "typescript": "^5.6.2"
25
+ },
26
+ "husky": {
27
+ "hooks": {
28
+ "commit-msg": "commitlint .commitlintrc.js -E HUSKY_GIT_PARAMS",
29
+ "pre-commit": "lint-staged"
30
+ }
31
+ },
32
+ "lint-staged": {
33
+ "*": [
34
+ "eden lint format",
35
+ "git add"
36
+ ]
37
+ }
38
+ }