Comparing version 0.0.2 to 0.0.3
# @a38/core | ||
## 0.0.3 | ||
### Patch Changes | ||
- 405a971: Add support for async assertions | ||
- 68c334b: Improve permission serialization | ||
## 0.0.2 | ||
@@ -4,0 +11,0 @@ |
@@ -137,3 +137,3 @@ "use strict"; | ||
} | ||
resolve(roleOrRoleId, resourceOrResourceId, privilege) { | ||
async resolve(roleOrRoleId, resourceOrResourceId, privilege) { | ||
const role = typeof roleOrRoleId === "string" ? new Role(roleOrRoleId) : roleOrRoleId; | ||
@@ -145,3 +145,3 @@ const resource = typeof resourceOrResourceId === "string" ? new Resource(resourceOrResourceId) : resourceOrResourceId; | ||
for (const rule of rules) { | ||
if (rule.match(this, role, resource, privilege)) { | ||
if (await rule.match(this, role, resource, privilege)) { | ||
return rule.type; | ||
@@ -152,7 +152,7 @@ } | ||
} | ||
isAllowed(roleOrRoleId, resourceOrResourceId, privilege = null) { | ||
return this.resolve(roleOrRoleId, resourceOrResourceId, privilege) === "allow"; | ||
async isAllowed(roleOrRoleId, resourceOrResourceId, privilege = null) { | ||
return await this.resolve(roleOrRoleId, resourceOrResourceId, privilege) === "allow"; | ||
} | ||
isDenied(roleOrRoleId, resourceOrResourceId, privilege = null) { | ||
return this.resolve(roleOrRoleId, resourceOrResourceId, privilege) === "deny"; | ||
async isDenied(roleOrRoleId, resourceOrResourceId, privilege = null) { | ||
return await this.resolve(roleOrRoleId, resourceOrResourceId, privilege) === "deny"; | ||
} | ||
@@ -169,6 +169,6 @@ }; | ||
static fromJSON(json) { | ||
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "allow" && json.type !== "deny" || !("privileges" in json) || !Array.isArray(json.privileges) && json.privileges !== null) { | ||
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "allow" && json.type !== "deny" || "privileges" in json && !Array.isArray(json.privileges) && json.privileges !== null) { | ||
throw new Error(`Invalid serialize [Rule]: ${JSON.stringify(json)}`); | ||
} | ||
return new _Rule(json.type, json.privileges && new Set(json.privileges)); | ||
return new _Rule(json.type, "privileges" in json && json.privileges ? new Set(json.privileges) : null); | ||
} | ||
@@ -242,3 +242,7 @@ match(hrbac, role, resource, privilege) { | ||
toJSON() { | ||
return [...this.map.entries()].map(([resource, rrMap]) => [resource, rrMap.toJSON()]); | ||
return [...this.map.entries()].flatMap(([role, rrMap]) => { | ||
return [...rrMap].flatMap(([resource, rules]) => { | ||
return rules.map((rule) => [role, resource, rule.toJSON()]); | ||
}); | ||
}); | ||
} | ||
@@ -250,10 +254,10 @@ importJSON(json) { | ||
for (const entry of json) { | ||
if (!Array.isArray(entry) || entry.length !== 2) { | ||
if (!Array.isArray(entry) || entry.length !== 3) { | ||
throw new Error(`Invalid serialize [RoleResourceRuleMap] entry: ${JSON.stringify(entry)}`); | ||
} | ||
const [role, rrMap] = entry; | ||
if (role !== null && typeof role !== "string") { | ||
const [role, resource, rule] = entry; | ||
if (role !== null && typeof role !== "string" || resource !== null && typeof resource !== "string") { | ||
throw new Error(`Invalid serialize [RoleResourceRuleMap] entry: ${JSON.stringify(entry)}`); | ||
} | ||
this.get(role).importJSON(rrMap); | ||
this.get(role).add(resource, Rule.fromJSON(rule)); | ||
} | ||
@@ -260,0 +264,0 @@ return this; |
{ | ||
"name": "@a38/core", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"exports": { | ||
@@ -28,5 +28,5 @@ ".": { | ||
"@a38/typedoc": "0.0.1", | ||
"@types/node": "20.14.9", | ||
"typescript": "5.4.5" | ||
"@types/node": "20.14.15", | ||
"typescript": "5.5.4" | ||
} | ||
} |
# @a38/core | ||
Core of the A38 hierarchical RBAC library | ||
## Installation | ||
```bash | ||
npm install @a38/core # for NPM | ||
yarn add @a38/core # for Yarn | ||
pnpm add @a38/core # for PNPM | ||
``` | ||
## Usage | ||
```typescript | ||
import { HRBAC, PermissionManager, ResourceManager, RoleManager } from '@a38/core'; | ||
const roleManager = new RoleManager(); | ||
roleManager.setParents('guest', []); // optional | ||
roleManager.setParents('user', ['guest']); // role 'user' extends role 'guest' | ||
roleManager.setParents('admin', ['user']); // role 'admin' extends role 'user' | ||
const resourceManager = new ResourceManager(); | ||
resourceManager.setParents('dashboard', []); // optional | ||
resourceManager.setParents('login', []); // optional | ||
resourceManager.setParents('profile', []); // optional | ||
resourceManager.setParents('admin', []); // optional | ||
const permissionManager = new PermissionManager(); | ||
permissionManager.allow('guest', 'dashboard'); // allow 'guest' access to 'dashboard' | ||
permissionManager.allow('guest', 'login'); // allow 'guest' access to 'dashboard' | ||
permissionManager.allow('user', 'profile'); // allow 'user' access to 'profile' | ||
permissionManager.deny('user', 'login'); // deny 'user' access to login | ||
permissionManager.allow('admin'); // allow 'admin' access to everything | ||
permissionManager.deny('admin', 'login'); // deny 'admin' access to login | ||
const hrbac = new HRBAC(roleManager, resourceManager, permissionManager); | ||
hrbac.isAllowed('guest', 'dashboard'); // -> true | ||
hrbac.isAllowed('guest', 'login'); // -> true | ||
hrbac.isAllowed('guest', 'profile'); // -> false | ||
hrbac.isAllowed('guest', 'admin'); // -> false | ||
hrbac.isAllowed('user', 'login'); // -> false | ||
hrbac.isAllowed('user', 'profile'); // -> true | ||
hrbac.isAllowed('admin', 'login'); // -> false | ||
hrbac.isAllowed('admin', 'profile'); // -> true | ||
hrbac.isAllowed('admin', 'admin'); // -> true | ||
``` | ||
## Docs | ||
See the [documentation](https://neoskop.github.io/a38/modules/_a38_core.html) | ||
## License | ||
@a38/core is licensed under the MIT License, See the [LICENSE](../../LICENSE) file for more details | ||
## Sponsoring | ||
The project development and maintenance is sponsored by [Neoskop](https://neoskop.de). |
export type SerializedChildNodes = [id: string, parents: string[]][]; | ||
export abstract class ChildNode<T> { | ||
export abstract class ChildNode<T, S extends SerializedChildNodes> { | ||
protected parents = new Map<string, string[]>(); | ||
@@ -55,7 +55,7 @@ | ||
toJSON(): SerializedChildNodes { | ||
return [...this.parents.entries()]; | ||
toJSON(): S { | ||
return [...this.parents.entries()] as S; | ||
} | ||
importJSON(json: SerializedChildNodes | unknown) { | ||
importJSON(json: S | unknown) { | ||
if (!Array.isArray(json)) { | ||
@@ -62,0 +62,0 @@ throw new Error(`Invalid serialize [${Object.getPrototypeOf(this).constructor.name}]: ${JSON.stringify(json)}`); |
@@ -78,59 +78,59 @@ import { beforeEach, describe, expect, it } from 'bun:test'; | ||
describe('permissions', () => { | ||
it('guest', () => { | ||
expect(hrbac.isAllowed('guest', documentA, 'read')).toBeTrue(); | ||
expect(hrbac.isDenied('guest', documentA, 'read')).toBeFalse(); | ||
expect(hrbac.isAllowed('guest', documentA, 'update')).toBeFalse(); | ||
expect(hrbac.isDenied('guest', documentA, 'update')).toBeTrue(); | ||
it('guest', async () => { | ||
expect(await hrbac.isAllowed('guest', documentA, 'read')).toBeTrue(); | ||
expect(await hrbac.isDenied('guest', documentA, 'read')).toBeFalse(); | ||
expect(await hrbac.isAllowed('guest', documentA, 'update')).toBeFalse(); | ||
expect(await hrbac.isDenied('guest', documentA, 'update')).toBeTrue(); | ||
}); | ||
it('admin', () => { | ||
expect(hrbac.isAllowed(admin, 'settings')).toBeTrue(); | ||
expect(hrbac.isDenied(admin, 'settings')).toBeFalse(); | ||
it('admin', async () => { | ||
expect(await hrbac.isAllowed(admin, 'settings')).toBeTrue(); | ||
expect(await hrbac.isDenied(admin, 'settings')).toBeFalse(); | ||
}); | ||
it('user', () => { | ||
expect(hrbac.isAllowed(user, documentA, 'read')).toBeTrue(); | ||
expect(hrbac.isDenied(user, documentA, 'read')).toBeFalse(); | ||
expect(hrbac.isAllowed(user, documentA, 'list')).toBeTrue(); | ||
expect(hrbac.isDenied(user, documentA, 'list')).toBeFalse(); | ||
expect(hrbac.isAllowed(user, documentA, 'update')).toBeFalse(); | ||
expect(hrbac.isDenied(user, documentA, 'update')).toBeTrue(); | ||
it('user', async () => { | ||
expect(await hrbac.isAllowed(user, documentA, 'read')).toBeTrue(); | ||
expect(await hrbac.isDenied(user, documentA, 'read')).toBeFalse(); | ||
expect(await hrbac.isAllowed(user, documentA, 'list')).toBeTrue(); | ||
expect(await hrbac.isDenied(user, documentA, 'list')).toBeFalse(); | ||
expect(await hrbac.isAllowed(user, documentA, 'update')).toBeFalse(); | ||
expect(await hrbac.isDenied(user, documentA, 'update')).toBeTrue(); | ||
expect(hrbac.isAllowed(user, 'ffa')).toBeTrue(); | ||
expect(hrbac.isAllowed(userV, 'ffa')).toBeTrue(); | ||
expect(await hrbac.isAllowed(user, 'ffa')).toBeTrue(); | ||
expect(await hrbac.isAllowed(userV, 'ffa')).toBeTrue(); | ||
expect(hrbac.isAllowed(user, profileU)).toBeTrue(); | ||
expect(hrbac.isAllowed(user, profileV)).toBeFalse(); | ||
expect(await hrbac.isAllowed(user, profileU)).toBeTrue(); | ||
expect(await hrbac.isAllowed(user, profileV)).toBeFalse(); | ||
expect(hrbac.isAllowed(user, documentA)).toBeFalse(); | ||
expect(await hrbac.isAllowed(user, documentA)).toBeFalse(); | ||
}); | ||
it('editor', () => { | ||
expect(hrbac.isAllowed(editor, documentA, 'read')).toBeTrue(); | ||
expect(hrbac.isDenied(editor, documentA, 'read')).toBeFalse(); | ||
expect(hrbac.isAllowed(editor, documentA, 'list')).toBeTrue(); | ||
expect(hrbac.isDenied(editor, documentA, 'list')).toBeFalse(); | ||
expect(hrbac.isAllowed(editor, documentA, 'update')).toBeTrue(); | ||
expect(hrbac.isDenied(editor, documentA, 'update')).toBeFalse(); | ||
expect(hrbac.isAllowed(editor, documentA, 'create')).toBeFalse(); | ||
expect(hrbac.isDenied(editor, documentA, 'create')).toBeTrue(); | ||
expect(hrbac.isAllowed(editor, documentA, 'remove')).toBeFalse(); | ||
expect(hrbac.isDenied(editor, documentA, 'remove')).toBeTrue(); | ||
it('editor', async () => { | ||
expect(await hrbac.isAllowed(editor, documentA, 'read')).toBeTrue(); | ||
expect(await hrbac.isDenied(editor, documentA, 'read')).toBeFalse(); | ||
expect(await hrbac.isAllowed(editor, documentA, 'list')).toBeTrue(); | ||
expect(await hrbac.isDenied(editor, documentA, 'list')).toBeFalse(); | ||
expect(await hrbac.isAllowed(editor, documentA, 'update')).toBeTrue(); | ||
expect(await hrbac.isDenied(editor, documentA, 'update')).toBeFalse(); | ||
expect(await hrbac.isAllowed(editor, documentA, 'create')).toBeFalse(); | ||
expect(await hrbac.isDenied(editor, documentA, 'create')).toBeTrue(); | ||
expect(await hrbac.isAllowed(editor, documentA, 'remove')).toBeFalse(); | ||
expect(await hrbac.isDenied(editor, documentA, 'remove')).toBeTrue(); | ||
}); | ||
it('author', () => { | ||
expect(hrbac.isAllowed(authorA, documentA, 'read')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorA, documentA, 'list')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorA, documentA, 'update')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorA, documentA, 'create')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorA, documentA, 'remove')).toBeFalse(); | ||
it('author', async () => { | ||
expect(await hrbac.isAllowed(authorA, documentA, 'read')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorA, documentA, 'list')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorA, documentA, 'update')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorA, documentA, 'create')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorA, documentA, 'remove')).toBeFalse(); | ||
expect(hrbac.isAllowed(authorB, documentA, 'read')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorB, documentA, 'list')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorB, documentA, 'update')).toBeFalse(); | ||
expect(hrbac.isAllowed(authorB, documentA, 'create')).toBeTrue(); | ||
expect(hrbac.isAllowed(authorB, documentA, 'remove')).toBeFalse(); | ||
expect(await hrbac.isAllowed(authorB, documentA, 'read')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorB, documentA, 'list')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorB, documentA, 'update')).toBeFalse(); | ||
expect(await hrbac.isAllowed(authorB, documentA, 'create')).toBeTrue(); | ||
expect(await hrbac.isAllowed(authorB, documentA, 'remove')).toBeFalse(); | ||
}); | ||
}); | ||
it('should support resource inheritance', () => { | ||
it('should support resource inheritance', async () => { | ||
hrbac = new HRBAC(new RoleManager(), new ResourceManager(), new PermissionManager()); | ||
@@ -141,4 +141,4 @@ hrbac.getResourceManager().addParents('child', ['parent']); | ||
expect(hrbac.isAllowed('role', 'child')).toBeTrue(); | ||
expect(await hrbac.isAllowed('role', 'child')).toBeTrue(); | ||
}); | ||
}); |
@@ -24,3 +24,7 @@ import type { PermissionManager, Type } from './permission-manager.js'; | ||
protected resolve(roleOrRoleId: Role | string, resourceOrResourceId: Resource | string, privilege: string | null): Type | null { | ||
protected async resolve( | ||
roleOrRoleId: Role | string, | ||
resourceOrResourceId: Resource | string, | ||
privilege: string | null | ||
): Promise<Type | null> { | ||
const role = typeof roleOrRoleId === 'string' ? new Role(roleOrRoleId) : roleOrRoleId; | ||
@@ -34,3 +38,3 @@ const resource = typeof resourceOrResourceId === 'string' ? new Resource(resourceOrResourceId) : resourceOrResourceId; | ||
for (const rule of rules) { | ||
if (rule.match(this, role, resource, privilege)) { | ||
if (await rule.match(this, role, resource, privilege)) { | ||
return rule.type; | ||
@@ -43,9 +47,17 @@ } | ||
isAllowed(roleOrRoleId: Role | string, resourceOrResourceId: Resource | string, privilege: string | null = null): boolean { | ||
return this.resolve(roleOrRoleId, resourceOrResourceId, privilege) === 'allow'; | ||
async isAllowed( | ||
roleOrRoleId: Role | string, | ||
resourceOrResourceId: Resource | string, | ||
privilege: string | null = null | ||
): Promise<boolean> { | ||
return (await this.resolve(roleOrRoleId, resourceOrResourceId, privilege)) === 'allow'; | ||
} | ||
isDenied(roleOrRoleId: Role | string, resourceOrResourceId: Resource | string, privilege: string | null = null): boolean { | ||
return this.resolve(roleOrRoleId, resourceOrResourceId, privilege) === 'deny'; | ||
async isDenied( | ||
roleOrRoleId: Role | string, | ||
resourceOrResourceId: Resource | string, | ||
privilege: string | null = null | ||
): Promise<boolean> { | ||
return (await this.resolve(roleOrRoleId, resourceOrResourceId, privilege)) === 'deny'; | ||
} | ||
} |
@@ -34,6 +34,6 @@ import { beforeEach, describe, expect, it } from 'bun:test'; | ||
expect(json).toEqual([ | ||
['roleA', [['resource', [{ type: 'allow', privileges: ['privilegeA'] }]]]], | ||
['roleB', [['resource', [{ type: 'deny', privileges: ['privilegeB', 'privilegeC'] }]]]], | ||
['roleC', [['resource', [{ type: 'allow', privileges: null }]]]], | ||
['roleD', [[null, [{ type: 'allow', privileges: null }]]]] | ||
['roleA', 'resource', { type: 'allow', privileges: ['privilegeA'] }], | ||
['roleB', 'resource', { type: 'deny', privileges: ['privilegeB', 'privilegeC'] }], | ||
['roleC', 'resource', { type: 'allow', privileges: null }], | ||
['roleD', null, { type: 'allow', privileges: null }] | ||
]); | ||
@@ -40,0 +40,0 @@ }); |
@@ -16,3 +16,3 @@ import type { HRBAC } from './hrbac.js'; | ||
type: Type; | ||
privileges: string[] | null; | ||
privileges?: string[] | null; | ||
} | ||
@@ -27,8 +27,7 @@ | ||
(json.type !== 'allow' && json.type !== 'deny') || | ||
!('privileges' in json) || | ||
(!Array.isArray(json.privileges) && json.privileges !== null) | ||
('privileges' in json && !Array.isArray(json.privileges) && json.privileges !== null) | ||
) { | ||
throw new Error(`Invalid serialize [Rule]: ${JSON.stringify(json)}`); | ||
} | ||
return new Rule(json.type, json.privileges && new Set(json.privileges)); | ||
return new Rule(json.type, 'privileges' in json && json.privileges ? new Set(json.privileges as string[]) : null); | ||
} | ||
@@ -108,4 +107,6 @@ | ||
export type SerializedRoleResourceRuleMap = [roleId: string | null, rrmap: SerializedResourceRuleMap][]; | ||
// export type SerializedRoleResourceRuleMap = [roleId: string | null, rrmap: SerializedResourceRuleMap][]; | ||
export type SerializedPermissions = (readonly [roleId: string | null, resourceId: string | null, rule: SerializedRule])[]; | ||
export class RoleResourceRuleMap implements Iterable<[string | null, ResourceRuleMap]> { | ||
@@ -132,7 +133,11 @@ private map = new Map<string | null, ResourceRuleMap>(); | ||
toJSON(): SerializedRoleResourceRuleMap { | ||
return [...this.map.entries()].map(([resource, rrMap]) => [resource, rrMap.toJSON()]); | ||
toJSON(): SerializedPermissions { | ||
return [...this.map.entries()].flatMap(([role, rrMap]) => { | ||
return [...rrMap].flatMap(([resource, rules]) => { | ||
return rules.map(rule => [role, resource, rule.toJSON()] as const); | ||
}); | ||
}); | ||
} | ||
importJSON(json: SerializedRoleResourceRuleMap | unknown) { | ||
importJSON(json: SerializedPermissions | unknown) { | ||
if (!Array.isArray(json)) { | ||
@@ -142,11 +147,11 @@ throw new Error(`Invalid serialize [RoleResourceRuleMap]: ${JSON.stringify(json)}`); | ||
for (const entry of json) { | ||
if (!Array.isArray(entry) || entry.length !== 2) { | ||
if (!Array.isArray(entry) || entry.length !== 3) { | ||
throw new Error(`Invalid serialize [RoleResourceRuleMap] entry: ${JSON.stringify(entry)}`); | ||
} | ||
const [role, rrMap] = entry as [unknown, unknown]; | ||
if (role !== null && typeof role !== 'string') { | ||
const [role, resource, rule] = entry as [unknown, unknown, unknown]; | ||
if ((role !== null && typeof role !== 'string') || (resource !== null && typeof resource !== 'string')) { | ||
throw new Error(`Invalid serialize [RoleResourceRuleMap] entry: ${JSON.stringify(entry)}`); | ||
} | ||
this.get(role).importJSON(rrMap); | ||
this.get(role).add(resource, Rule.fromJSON(rule)); //importJSON(rrMap); | ||
} | ||
@@ -203,7 +208,7 @@ return this; | ||
toJSON(): SerializedRoleResourceRuleMap { | ||
toJSON(): SerializedPermissions { | ||
return this.rrrm.toJSON(); | ||
} | ||
importJSON(json: SerializedRoleResourceRuleMap | unknown): this { | ||
importJSON(json: SerializedPermissions | unknown): this { | ||
this.rrrm.importJSON(json); | ||
@@ -210,0 +215,0 @@ return this; |
import { ChildNode } from './child-node.js'; | ||
export type SerializeResources = [resource: string, parents: string[]][]; | ||
export type SerializedResources = [resource: string, parents: string[]][]; | ||
export class Resource { | ||
@@ -12,4 +12,4 @@ constructor(public readonly resourceId: string) {} | ||
export class ResourceManager extends ChildNode<Resource> { | ||
export class ResourceManager extends ChildNode<Resource, SerializedResources> { | ||
protected assertEntryId = assertResourceId; | ||
} |
import { ChildNode } from './child-node.js'; | ||
export type SerializeRoles = [role: string, parents: string[]][]; | ||
export type SerializedRoles = [role: string, parents: string[]][]; | ||
export class Role { | ||
@@ -12,4 +12,4 @@ constructor(public readonly roleId: string) {} | ||
export class RoleManager extends ChildNode<Role> { | ||
export class RoleManager extends ChildNode<Role, SerializedRoles> { | ||
protected assertEntryId = assertRoleId; | ||
} |
{ | ||
"extends": ["@a38/typedoc"], | ||
"entryPoints": ["src/index.ts", "src/awaitable.ts"] | ||
"entryPoints": ["src/index.ts"] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
77708
1104
65