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

@@ -0,0 +1,19 @@
import { cerbos } from "./cerbos.ts";
import type { Principal } from "./principal.ts";
import { Resource } from "./resources.ts";
export function access(principal: Principal) {
return {
isAllowed(resource: Resource, action: string) {
return cerbos.isAllowed({ principal, resource, action });
},
checkResource(resource: Resource, actions: string[]) {
return cerbos.checkResource({ principal, resource, actions });
},
checkResources(resources: { resource: Resource; actions: string[] }[]) {
return cerbos.checkResources({ principal, resources });
},
};
}
export type Access = ReturnType<typeof access>;

View File

@@ -1,82 +1,21 @@
import { Auth, ResolvedSession } from "@valkyr/auth";
import z from "zod";
import { db } from "~stores/read-store/database.ts";
import { Auth } from "@valkyr/auth";
import { access } from "./access.ts";
import { config } from "./config.ts";
import { principal } from "./principal.ts";
import { resources } from "./resources.ts";
export const auth = new Auth(
{
settings: {
algorithm: "RS256",
privateKey: config.privateKey,
publicKey: config.publicKey,
issuer: "http://localhost",
audience: "http://localhost",
},
session: z.object({
accountId: z.string(),
}),
permissions: {} as const,
guards: [],
export const auth = new Auth({
principal,
resources,
access,
jwt: {
algorithm: "RS256",
privateKey: config.privateKey,
publicKey: config.publicKey,
issuer: "http://localhost",
audience: "http://localhost",
},
{
roles: {
async add(role) {
await db.collection("roles").insertOne(role);
},
});
async getById(id) {
const role = await db.collection("roles").findOne({ id });
if (role === null) {
return undefined;
}
return role;
},
async getBySession({ accountId }) {
const account = await db.collection("accounts").findOne({ id: accountId });
if (account === null) {
return [];
}
return db
.collection("roles")
.find({ id: { $in: account.roles } })
.toArray();
},
async setPermissions() {
throw new Error("MongoRolesProvider > .setPermissions is managed by Role aggregate projections");
},
async delete(id) {
await db.collection("roles").deleteOne({ id });
},
async assignAccount(roleId: string, accountId: string): Promise<void> {
await db.collection("accounts").updateOne(
{ id: accountId },
{
$push: {
roles: roleId,
},
},
);
},
async removeAccount(roleId: string, accountId: string): Promise<void> {
await db.collection("roles").updateOne(
{ id: accountId },
{
$pull: {
roles: roleId,
},
},
);
},
},
},
);
export type Session = ResolvedSession<typeof auth>;
export type Permissions = (typeof auth)["$permissions"];
export type Session = typeof auth.$session;

View File

@@ -0,0 +1,8 @@
import { HTTP } from "@cerbos/http";
export const cerbos = new HTTP("http://localhost:3592", {
adminCredentials: {
username: "cerbos",
password: "cerbosAdmin",
},
});

View File

@@ -0,0 +1,18 @@
import { RoleSchema } from "@spec/schemas/account/role.ts";
import { PrincipalProvider } from "@valkyr/auth";
import { db } from "~stores/read-store/database.ts";
export const principal = new PrincipalProvider(RoleSchema, {}, async function (id: string) {
const account = await db.collection("accounts").findOne({ id });
if (account === null) {
return undefined;
}
return {
id,
roles: account.roles,
attributes: {},
};
});
export type Principal = typeof principal.$principal;

View File

@@ -0,0 +1,10 @@
import { ResourceRegistry } from "@valkyr/auth";
export const resources = new ResourceRegistry([
{
kind: "account",
attributes: {},
},
] as const);
export type Resource = typeof resources.$resource;

View File

@@ -2,9 +2,9 @@ import { parseArgs } from "@std/cli";
import { Parser, toString } from "./parsers.ts";
export function getArgsVariable(key: string, fallback?: string): string;
export function getArgsVariable<T extends Parser>(key: string, parse: T, fallback?: string): ReturnType<T>;
export function getArgsVariable<T extends Parser>(key: string, parse?: T, fallback?: string): ReturnType<T> {
export function getArgsVariable(key: string, fallback?: any): string;
export function getArgsVariable<T extends Parser>(key: string, parse: T, fallback?: any): ReturnType<T>;
export function getArgsVariable<T extends Parser>(key: string, parse?: T, fallback?: any): ReturnType<T> {
if (typeof parse === "string") {
fallback = parse;
parse = undefined;
@@ -17,5 +17,5 @@ export function getArgsVariable<T extends Parser>(key: string, parse?: T, fallba
}
throw new Error(`Config Exception: Missing ${key} variable in arguments`);
}
return parse ? parse(value) : toString(value);
return parse ? parse(value) : (toString(value) as any);
}

View File

@@ -11,9 +11,9 @@ const env = await load();
* @param key - Environment key to resolve.
* @param parse - Parser function to convert the value to the desired type. Default: `string`.
*/
export function getEnvironmentVariable(key: string, fallback?: string): string;
export function getEnvironmentVariable<T extends Parser>(key: string, parse: T, fallback?: string): ReturnType<T>;
export function getEnvironmentVariable<T extends Parser>(key: string, parse?: T, fallback?: string): ReturnType<T> {
export function getEnvironmentVariable(key: string, fallback?: any): string;
export function getEnvironmentVariable<T extends Parser>(key: string, parse: T, fallback?: any): ReturnType<T>;
export function getEnvironmentVariable<T extends Parser>(key: string, parse?: T, fallback?: any): ReturnType<T> {
if (typeof parse === "string") {
fallback = parse;
parse = undefined;
@@ -25,7 +25,7 @@ export function getEnvironmentVariable<T extends Parser>(key: string, parse?: T,
}
throw new Error(`Config Exception: Missing ${key} variable in configuration`);
}
return parse ? parse(value) : toString(value);
return parse ? parse(value) : (toString(value) as any);
}
/**

View File

@@ -166,7 +166,7 @@ export class Api {
);
}
if (route.state.access === "session" && req.isAuthenticated === false) {
if (route.state.access === "authenticated" && req.isAuthenticated === false) {
return toResponse(new UnauthorizedError(), request);
}

View File

@@ -2,7 +2,9 @@ import { ServerContext } from "@spec/relay";
import type { Sockets } from "~libraries/socket/sockets.ts";
import { Access } from "../auth/access.ts";
import { Session } from "../auth/auth.ts";
import { Principal } from "../auth/principal.ts";
import { req } from "./request.ts";
declare module "@spec/relay" {
@@ -17,17 +19,21 @@ declare module "@spec/relay" {
*/
isAuthenticated: boolean;
/**
* Get account id from session, throws an error if the request
* does not have a valid session.
*/
accountId: string;
/**
* Get request session instance.
*/
session: Session;
/**
* Get request principal.
*/
principal: Principal;
/**
* Get access control session.
*/
access: Access;
/**
* Sockets instance attached to the server.
*/
@@ -43,14 +49,18 @@ export function getRequestContext(request: Request): ServerContext {
return req.isAuthenticated;
},
get accountId() {
return this.session.accountId;
},
get session(): Session {
return req.session;
},
get principal(): Principal {
return req.session.principal;
},
get access(): Access {
return req.session.access;
},
get sockets(): Sockets {
return req.sockets;
},

View File

@@ -1,11 +1,11 @@
import { InternalServerError, UnauthorizedError } from "@spec/relay";
import { Session } from "../auth/auth.ts";
import { asyncLocalStorage } from "./storage.ts";
import { storage } from "./storage.ts";
export const req = {
get store() {
const store = asyncLocalStorage.getStore();
const store = storage.getStore();
if (store === undefined) {
throw new InternalServerError("AsyncLocalStorage not defined.");
}
@@ -55,7 +55,7 @@ export const req = {
* Typically used when utility functions might run in and out of request scope.
*/
getStore() {
return asyncLocalStorage.getStore();
return storage.getStore();
},
} as const;

View File

@@ -3,7 +3,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
import type { Session } from "~libraries/auth/mod.ts";
import type { Sockets } from "~libraries/socket/sockets.ts";
export const asyncLocalStorage = new AsyncLocalStorage<{
export const storage = new AsyncLocalStorage<Storage>();
export type Storage = {
session?: Session;
info: {
method: string;
@@ -14,4 +16,4 @@ export const asyncLocalStorage = new AsyncLocalStorage<{
response: {
headers: Headers;
};
}>();
};