@transmit-security/rbac 1.0.0-beta

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.

Potentially problematic release.


This version of @transmit-security/rbac might be problematic. Click here for more details.

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
+ }