Skip to content

Commit

Permalink
schema builder
Browse files Browse the repository at this point in the history
  • Loading branch information
ramonsnir committed Oct 24, 2022
1 parent 19db68d commit 1c9b8d4
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 160 deletions.
127 changes: 44 additions & 83 deletions examples/simpleMembershipWithAdmin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Schema } from '../src';
import { Schema, SchemaBuilder } from '../src';

export class User {
constructor(public id: string, public admin: boolean) {}
Expand Down Expand Up @@ -29,91 +29,52 @@ const nodes = [
{ node: Membership, tableName: 'memberships', primaryColumn: 'id' },
] as const;

const edges = [
{
source: Project,
sourceColumn: 'owner_id',
sourceRelation: 'owner',
target: User,
targetColumn: 'id',
},
{
source: Membership,
sourceColumn: 'user_id',
sourcePreloadedProperty: 'user',
sourceRelation: 'user',
target: User,
targetColumn: 'id',
},
{
source: Membership,
sourceColumn: 'project_id',
sourceRelation: 'project',
target: Project,
targetColumn: 'id',
targetPreloadedProperty: 'memberships',
targetRelation: 'memberships',
},
] as const;

type Relation = 'owner' | 'user' | 'project' | 'memberships';

type Role = 'Stranger' | 'Member' | 'Owner' | 'Admin';

type Permission = 'view' | 'edit' | 'manage';

export const simpleMembershipWithAdminSchema: Schema<typeof nodes[number]['node'], Relation, Role, Permission> = {
nodes,
grants: new Map([
[
Project,
{
view: ['Stranger', 'Member', 'Owner'],
edit: ['Member', 'Owner'],
manage: ['Owner', 'Admin'],
},
],
]),
edges: [
{
kind: '1:n',
source: Project,
sourceColumn: 'owner_id',
sourceRelation: 'owner',
target: User,
targetColumn: 'id',
},
{
kind: '1:n',
source: Membership,
sourceColumn: 'user_id',
sourcePreloadedProperty: 'user',
sourceRelation: 'user',
target: User,
targetColumn: 'id',
},
{
kind: '1:n',
source: Membership,
sourceColumn: 'project_id',
sourceRelation: 'project',
target: Project,
targetColumn: 'id',
targetPreloadedProperty: 'memberships',
targetRelation: 'memberships',
},
],
rules: [
{
kind: 'direct',
node: Project,
role: 'Stranger',
actorNode: User,
},
{
kind: 'direct',
node: Project,
role: 'Owner',
actorNode: User,
throughRelation: 'owner',
},
{
kind: 'direct',
node: Membership,
role: 'Member',
actorNode: User,
throughRelation: 'user',
},
{
kind: 'extension',
node: Project,
role: 'Member',
actorNode: User,
extend: {
linkNode: Membership,
linkRole: 'Member',
throughRelation: 'memberships',
},
},
{
kind: 'direct',
node: Project,
role: 'Admin',
actorNode: User,
actorFilters: [{ column: 'admin', condition: { kind: '=', value: true } }],
},
],
};
export const simpleMembershipWithAdminSchema: Schema<typeof nodes[number]['node'], Relation, Role, Permission> =
new SchemaBuilder<typeof nodes[number]['node'], Relation, Role, Permission>(nodes, edges)
.grant(Project, 'Owner', ['view', 'edit', 'manage'])
.grant(Project, 'Admin', 'manage')
.grant(Project, 'Member', ['view', 'edit'])
.grant(Project, 'Stranger', 'view')
.assignDirectly(Project, [], User, [], 'Stranger')
.assignDirectly(Project, [], User, [{ column: 'admin', condition: { kind: '=', value: true } }], 'Admin')
.assignThroughRelation(Project, [], 'owner', User, [], 'Owner')
.assignThroughRelation(Membership, [], 'user', User, [], 'Member')
.assignByExtension(Project, [], User, [], 'Member', {
linkNode: Membership,
linkRole: 'Member',
throughRelation: 'memberships',
})
.finalize();
58 changes: 19 additions & 39 deletions examples/simpleOwnership.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Schema } from '../src';
import { Schema, SchemaBuilder } from '../src';

export class Person {
constructor(public id: string) {}
Expand All @@ -13,47 +13,27 @@ const nodes = [
{ node: Car, tableName: 'cars', primaryColumn: 'id' },
] as const;

const edges = [
{
source: Car,
sourceColumn: 'owner_id',
sourcePreloadedProperty: 'owner',
sourceRelation: 'owner',
target: Person,
targetColumn: 'id',
},
] as const;

type Relation = 'owner';

type Role = 'Stranger' | 'Owner';

type Permission = 'view' | 'drive';

export const simpleOwnershipSchema: Schema<typeof nodes[number]['node'], Relation, Role, Permission> = {
nodes,
grants: new Map([
[
Car,
{
view: ['Stranger', 'Owner'],
drive: ['Owner'],
},
],
]),
edges: [
{
kind: '1:n',
source: Car,
sourceColumn: 'owner_id',
sourcePreloadedProperty: 'owner',
sourceRelation: 'owner',
target: Person,
targetColumn: 'id',
},
],
rules: [
{
kind: 'direct',
node: Car,
role: 'Stranger',
actorNode: Person,
},
{
kind: 'direct',
node: Car,
role: 'Owner',
actorNode: Person,
throughRelation: 'owner',
},
],
};
export const simpleOwnershipSchema: Schema<typeof nodes[number]['node'], Relation, Role, Permission> =
new SchemaBuilder<typeof nodes[number]['node'], Relation, Role, Permission>(nodes, edges)
.grant(Car, 'Owner', ['view', 'drive'])
.grant(Car, 'Stranger', 'view')
.assignDirectly(Car, [], Person, [], 'Stranger')
.assignThroughRelation(Car, [], 'owner', Person, [], 'Owner')
.finalize();
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sunbear",
"version": "0.0.8",
"version": "0.1.0",
"description": "Authorization library for SQL",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down
96 changes: 95 additions & 1 deletion src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export interface Edge<Node, Relation> {
kind: '1:1' | '1:n' | 'n:1' | 'n:n';
source: Node;
sourceColumn: string;
/** Set to #this for a self-referencing relation. */
Expand Down Expand Up @@ -120,3 +119,98 @@ export function getNodeDefinition<Node, Relation, Role, Permission extends strin
// right: Right,
// leftRelation: string,
// ) {}

export class SchemaBuilder<Node, Relation, Role extends string, Permission extends string> {
grants = new Map<Node, Partial<Record<Permission, Role[]>>>();
rules: Rule<Node, Relation, Role>[] = [];

constructor(public nodes: readonly NodeDefinition<Node>[], public edges: readonly Edge<Node, Relation>[]) {}

finalize(): Schema<Node, Relation, Role, Permission> {
return {
nodes: this.nodes,
grants: this.grants,
edges: this.edges,
rules: this.rules,
};
}

grant(node: Node, role: Role, permission: Permission): this;
grant(node: Node, role: Role, permissions: readonly Permission[]): this;
grant(node: Node, roles: readonly Role[], permission: Permission): this;
grant(node: Node, roles: Role | readonly Role[], permissions: Permission | readonly Permission[]): this {
let grant = this.grants.get(node);
if (!grant) {
grant = {};
this.grants.set(node, grant);
}
for (const permission of [permissions as Permission[]].flat()) {
let grantees = grant[permission];
if (!grantees) {
grantees = [];
grant[permission] = grantees;
}
for (const role of [roles as Role[]].flat()) {
if (!grantees.includes(role)) {
grantees.push(role);
}
}
}
return this;
}

assignDirectly(
node: Node,
filters: readonly RuleFilter[],
actorNode: Node,
actorFilters: readonly RuleFilter[],
role: Role,
): this {
this.rules.push({ kind: 'direct', node, filters, role, actorNode, actorFilters });
return this;
}

assignThroughRelation(
node: Node,
filters: readonly RuleFilter[],
throughRelation: Relation,
actorNode: Node,
actorFilters: readonly RuleFilter[],
role: Role,
): this {
this.rules.push({ kind: 'direct', node, filters, role, actorNode, actorFilters, throughRelation });
return this;
}

assignByExtension(
node: Node,
filters: readonly RuleFilter[],
actorNode: Node,
actorFilters: readonly RuleFilter[],
role: Role,
extend: RuleExtension<Node, Relation, Role>['extend'],
): this {
this.rules.push({
kind: 'extension',
node,
filters,
role,
actorNode,
actorFilters,
extend,
});
return this;
}

assignByOtherRole(
node: Node,
filters: readonly RuleFilter[],
otherRole: Role,
actorNode: Node,
actorFilters: readonly RuleFilter[],
role: Role,
): this {
this.rules.push({ kind: 'superset', node, filters, role, actorNode, actorFilters, ofRole: otherRole });
return this;
}
}
Loading

0 comments on commit 1c9b8d4

Please sign in to comment.