Template
1
0

feat: add cerbos access control

This commit is contained in:
2025-09-19 03:28:00 +02:00
parent d322138502
commit 74a9426bcc
41 changed files with 999 additions and 821 deletions

View File

@@ -1,4 +1,5 @@
import { toAccountDocument } from "@spec/schemas/account/account.ts";
import { Role } from "@spec/schemas/account/role.ts";
import { Strategy } from "@spec/schemas/account/strategies.ts";
import { Avatar } from "@spec/schemas/avatar.ts";
import { Contact } from "@spec/schemas/contact.ts";
@@ -22,6 +23,7 @@ export class Account extends AggregateRoot<EventStoreFactory> {
emails: [],
};
strategies: Strategy[] = [];
roles: Role[] = [];
createdAt!: Date;
updatedAt!: Date;
@@ -51,6 +53,11 @@ export class Account extends AggregateRoot<EventStoreFactory> {
this.updatedAt = getDate(event.created);
break;
}
case "account:role:added": {
this.roles.push(event.data);
this.updatedAt = getDate(event.created);
break;
}
case "strategy:email:added": {
this.strategies.push({ type: "email", value: event.data });
this.updatedAt = getDate(event.created);
@@ -103,11 +110,11 @@ export class Account extends AggregateRoot<EventStoreFactory> {
});
}
addRole(roleId: string, meta: Auditor = systemAuditor): this {
addRole(role: Role, meta: Auditor = systemAuditor): this {
return this.push({
stream: this.id,
type: "account:role:added",
data: roleId,
data: role,
meta,
});
}
@@ -194,8 +201,8 @@ projector.on("account:email:added", async ({ stream: id, data: email }) => {
await db.collection("accounts").updateOne({ id }, { $push: { "contact.emails": email } });
});
projector.on("account:role:added", async ({ stream: id, data: roleId }) => {
await db.collection("accounts").updateOne({ id }, { $push: { roles: roleId } });
projector.on("account:role:added", async ({ stream: id, data: role }) => {
await db.collection("accounts").updateOne({ id }, { $push: { roles: role } });
});
projector.on("strategy:email:added", async ({ stream: id, data: email }) => {

View File

@@ -1,118 +0,0 @@
import { AggregateRoot, getDate, makeAggregateReducer } from "@valkyr/event-store";
import { db } from "~libraries/read-store/database.ts";
import type { Auditor } from "../events/auditor.ts";
import { EventStoreFactory } from "../events/mod.ts";
import type { RoleCreatedData, RolePermissionOperation } from "../events/role.ts";
import { projector } from "../projector.ts";
export class Role extends AggregateRoot<EventStoreFactory> {
static override readonly name = "role";
id!: string;
name!: string;
permissions: { [resource: string]: Set<string> } = {};
createdAt!: Date;
updatedAt!: Date;
// -------------------------------------------------------------------------
// Factories
// -------------------------------------------------------------------------
static #reducer = makeAggregateReducer(Role);
static create(data: RoleCreatedData, meta: Auditor): Role {
return new Role().push({
type: "role:created",
data,
meta,
});
}
static async getById(stream: string): Promise<Role | undefined> {
return this.$store.reduce({ name: "role", stream, reducer: this.#reducer });
}
// -------------------------------------------------------------------------
// Reducer
// -------------------------------------------------------------------------
override with(event: EventStoreFactory["$events"][number]["$record"]): void {
switch (event.type) {
case "role:created": {
this.id = event.stream;
this.createdAt = getDate(event.created);
this.updatedAt = getDate(event.created);
break;
}
case "role:name-set": {
this.name = event.data;
this.updatedAt = getDate(event.created);
break;
}
case "role:permissions-set": {
for (const operation of event.data) {
if (operation.type === "grant") {
if (this.permissions[operation.resource] === undefined) {
this.permissions[operation.resource] = new Set();
}
this.permissions[operation.resource].add(operation.action);
}
if (operation.type === "deny") {
if (operation.action === undefined) {
delete this.permissions[operation.resource];
} else {
this.permissions[operation.resource]?.delete(operation.action);
}
}
}
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
setName(name: string, meta: Auditor): this {
return this.push({
type: "role:name-set",
stream: this.id,
data: name,
meta,
});
}
setPermissions(operations: RolePermissionOperation[], meta: Auditor): this {
return this.push({
type: "role:permissions-set",
stream: this.id,
data: operations,
meta,
});
}
}
/*
|--------------------------------------------------------------------------------
| Projectors
|--------------------------------------------------------------------------------
*/
projector.on("role:created", async ({ stream, data: { name, permissions } }) => {
await db.collection("roles").insertOne({
id: stream,
name,
permissions: permissions.reduce(
(map, permission) => {
map[permission.resource] = permission.actions;
return map;
},
{} as Record<string, string[]>,
),
});
});