feat: add cerbos access control
This commit is contained in:
19
api/libraries/auth/access.ts
Normal file
19
api/libraries/auth/access.ts
Normal 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>;
|
||||
@@ -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;
|
||||
|
||||
8
api/libraries/auth/cerbos.ts
Normal file
8
api/libraries/auth/cerbos.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { HTTP } from "@cerbos/http";
|
||||
|
||||
export const cerbos = new HTTP("http://localhost:3592", {
|
||||
adminCredentials: {
|
||||
username: "cerbos",
|
||||
password: "cerbosAdmin",
|
||||
},
|
||||
});
|
||||
18
api/libraries/auth/principal.ts
Normal file
18
api/libraries/auth/principal.ts
Normal 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;
|
||||
10
api/libraries/auth/resources.ts
Normal file
10
api/libraries/auth/resources.ts
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}>();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user