feat: identity cerbos implementation
This commit is contained in:
17
modules/identity/cerbos/client.ts
Normal file
17
modules/identity/cerbos/client.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { HTTP } from "@cerbos/http";
|
||||
import { getEnvironmentVariable } from "@platform/config/environment.ts";
|
||||
import z from "zod";
|
||||
|
||||
export const cerbos = new HTTP(
|
||||
getEnvironmentVariable({
|
||||
key: "CERBOS_URL",
|
||||
type: z.string(),
|
||||
fallback: "http://localhost:3592",
|
||||
}),
|
||||
{
|
||||
adminCredentials: {
|
||||
username: "cerbos",
|
||||
password: "cerbosAdmin",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,17 +1,34 @@
|
||||
import { CheckResourcesResponse } from "@cerbos/core";
|
||||
import { HttpAdapter, makeClient } from "@platform/relay";
|
||||
|
||||
import { config } from "./config.ts";
|
||||
import checkResource from "./routes/access/check-resource/spec.ts";
|
||||
import checkResources from "./routes/access/check-resources/spec.ts";
|
||||
import isAllowed from "./routes/access/is-allowed/spec.ts";
|
||||
import getById from "./routes/identities/get/spec.ts";
|
||||
import loginByPassword from "./routes/login/code/spec.ts";
|
||||
import loginByEmail from "./routes/login/email/spec.ts";
|
||||
import loginByCode from "./routes/login/password/spec.ts";
|
||||
import me from "./routes/me/spec.ts";
|
||||
|
||||
const adapter = new HttpAdapter({
|
||||
url: config.url,
|
||||
});
|
||||
|
||||
const access = makeClient(
|
||||
{
|
||||
adapter,
|
||||
},
|
||||
{
|
||||
isAllowed,
|
||||
checkResource,
|
||||
checkResources,
|
||||
},
|
||||
);
|
||||
|
||||
export const identity = makeClient(
|
||||
{
|
||||
adapter: new HttpAdapter({
|
||||
url: config.url,
|
||||
}),
|
||||
adapter,
|
||||
},
|
||||
{
|
||||
/**
|
||||
@@ -43,5 +60,104 @@ export const identity = makeClient(
|
||||
*/
|
||||
code: loginByCode,
|
||||
},
|
||||
|
||||
access: {
|
||||
/**
|
||||
* Check if a principal is allowed to perform an action on a resource.
|
||||
*
|
||||
* @param resource - Resource which we are validating.
|
||||
* @param action - Action which we are validating.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* await access.isAllowed(
|
||||
* {
|
||||
* kind: "document",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* "view"
|
||||
* ); // => true
|
||||
*/
|
||||
isAllowed: async (resource: Resource, action: string) => {
|
||||
const response = await access.isAllowed({ body: { resource, action } });
|
||||
if ("error" in response) {
|
||||
throw response.error;
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check a principal's permissions on a resource.
|
||||
*
|
||||
* @param resource - Resource which we are validating.
|
||||
* @param actions - Actions which we are validating.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const decision = await access.checkResource(
|
||||
* {
|
||||
* kind: "document",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* ["view", "edit"],
|
||||
* );
|
||||
*
|
||||
* decision.isAllowed("view"); // => true
|
||||
*/
|
||||
checkResource: async (resource: Resource, actions: string[]) => {
|
||||
const response = await access.checkResource({ body: { resource, actions } });
|
||||
if ("error" in response) {
|
||||
throw response.error;
|
||||
}
|
||||
return new CheckResourcesResponse(response.data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check a principal's permissions on a set of resources.
|
||||
*
|
||||
* @param resources - Resources which we are validating.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const decision = await access.checkResources([
|
||||
* {
|
||||
* resource: {
|
||||
* kind: "document",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* actions: ["view", "edit"],
|
||||
* },
|
||||
* {
|
||||
* resource: {
|
||||
* kind: "image",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* actions: ["delete"],
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* decision.isAllowed({
|
||||
* resource: { kind: "document", id: "1" },
|
||||
* action: "view",
|
||||
* }); // => true
|
||||
*/
|
||||
checkResources: async (resources: { resource: Resource; actions: string[] }[]) => {
|
||||
const response = await access.checkResources({ body: resources });
|
||||
if ("error" in response) {
|
||||
throw response.error;
|
||||
}
|
||||
return new CheckResourcesResponse(response.data);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type Resource = {
|
||||
kind: string;
|
||||
id: string;
|
||||
attr: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"./server.ts": "./server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@platform/cerbos": "workspace:*",
|
||||
"@cerbos/core": "0.24.1",
|
||||
"@cerbos/http": "0.23.1",
|
||||
"@platform/config": "workspace:*",
|
||||
"@platform/logger": "workspace:*",
|
||||
"@platform/relay": "workspace:*",
|
||||
|
||||
6
modules/identity/routes/access/check-resource/handle.ts
Normal file
6
modules/identity/routes/access/check-resource/handle.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { cerbos } from "../../../cerbos/client.ts";
|
||||
import route from "./spec.ts";
|
||||
|
||||
export default route.access("session").handle(async ({ body: { resource, actions } }, { principal }) => {
|
||||
return cerbos.checkResource({ principal, resource, actions });
|
||||
});
|
||||
16
modules/identity/routes/access/check-resource/spec.ts
Normal file
16
modules/identity/routes/access/check-resource/spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { route } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
export default route
|
||||
.post("/api/v1/identity/access/check-resource")
|
||||
.body(
|
||||
z.strictObject({
|
||||
resource: z.strictObject({
|
||||
kind: z.string(),
|
||||
id: z.string(),
|
||||
attr: z.record(z.string(), z.any()),
|
||||
}),
|
||||
actions: z.array(z.string()),
|
||||
}),
|
||||
)
|
||||
.response(z.any());
|
||||
6
modules/identity/routes/access/check-resources/handle.ts
Normal file
6
modules/identity/routes/access/check-resources/handle.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { cerbos } from "../../../cerbos/client.ts";
|
||||
import route from "./spec.ts";
|
||||
|
||||
export default route.access("session").handle(async ({ body: resources }, { principal }) => {
|
||||
return cerbos.checkResources({ principal, resources });
|
||||
});
|
||||
18
modules/identity/routes/access/check-resources/spec.ts
Normal file
18
modules/identity/routes/access/check-resources/spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { route } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
export default route
|
||||
.post("/api/v1/identity/access/check-resources")
|
||||
.body(
|
||||
z.array(
|
||||
z.strictObject({
|
||||
resource: z.strictObject({
|
||||
kind: z.string(),
|
||||
id: z.string(),
|
||||
attr: z.record(z.string(), z.any()),
|
||||
}),
|
||||
actions: z.array(z.string()),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.response(z.any());
|
||||
6
modules/identity/routes/access/is-allowed/handle.ts
Normal file
6
modules/identity/routes/access/is-allowed/handle.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { cerbos } from "../../../cerbos/client.ts";
|
||||
import route from "./spec.ts";
|
||||
|
||||
export default route.access("session").handle(async ({ body: { resource, action } }, { principal }) => {
|
||||
return cerbos.isAllowed({ principal, resource, action });
|
||||
});
|
||||
16
modules/identity/routes/access/is-allowed/spec.ts
Normal file
16
modules/identity/routes/access/is-allowed/spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { route } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
export default route
|
||||
.post("/api/v1/identity/access/is-allowed")
|
||||
.body(
|
||||
z.strictObject({
|
||||
resource: z.strictObject({
|
||||
kind: z.string(),
|
||||
id: z.string(),
|
||||
attr: z.record(z.string(), z.any()),
|
||||
}),
|
||||
action: z.string(),
|
||||
}),
|
||||
)
|
||||
.response(z.boolean());
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ForbiddenError, NotFoundError } from "@platform/relay";
|
||||
|
||||
import { getPrincipalById } from "../../../services/database.ts";
|
||||
import route from "./spec.ts";
|
||||
|
||||
export default route.access("session").handle(async () => {
|
||||
// const user = await getUserById(id);
|
||||
// if (user === undefined) {
|
||||
// return new NotFoundError("Identity does not exist, or has been removed.");
|
||||
// }
|
||||
// const decision = await access.isAllowed({ kind: "identity", id: user.id, attr: {} }, "read");
|
||||
// if (decision === false) {
|
||||
// return new ForbiddenError("You do not have permission to view this identity.");
|
||||
// }
|
||||
// return {
|
||||
// id: user.id,
|
||||
// roles: await getPrincipalRoles(id),
|
||||
// attr: await getPrincipalAttributes(id),
|
||||
// };
|
||||
export default route.access("session").handle(async ({ params: { id } }, { access }) => {
|
||||
const principal = await getPrincipalById(id);
|
||||
if (principal === undefined) {
|
||||
return new NotFoundError("Identity does not exist, or has been removed.");
|
||||
}
|
||||
const decision = await access.isAllowed({ kind: "identity", id, attr: {} }, "read");
|
||||
if (decision === false) {
|
||||
return new ForbiddenError("You do not have permission to view this identity.");
|
||||
}
|
||||
return principal;
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import resolve from "./routes/session/resolve/spec.ts";
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export const identity = makeClient(
|
||||
const identity = makeClient(
|
||||
{
|
||||
adapter: new HttpAdapter({
|
||||
url: config.url,
|
||||
@@ -22,13 +22,19 @@ export const identity = makeClient(
|
||||
},
|
||||
);
|
||||
|
||||
export async function getPrincipalSession(payload: { headers: Headers }) {
|
||||
const response = await identity.resolve(payload);
|
||||
if ("data" in response) {
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Server Exports
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export * from "./services/access.ts";
|
||||
export * from "./services/session.ts";
|
||||
export * from "./types.ts";
|
||||
|
||||
@@ -49,5 +55,8 @@ export default {
|
||||
(await import("./routes/me/handle.ts")).default,
|
||||
(await import("./routes/roles/handle.ts")).default,
|
||||
(await import("./routes/session/resolve/handle.ts")).default,
|
||||
(await import("./routes/access/is-allowed/handle.ts")).default,
|
||||
(await import("./routes/access/check-resource/handle.ts")).default,
|
||||
(await import("./routes/access/check-resources/handle.ts")).default,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { cerbos } from "@platform/cerbos";
|
||||
|
||||
import type { Principal } from "../models/principal.ts";
|
||||
|
||||
export function getAccessControlMethods(principal: Principal) {
|
||||
return {
|
||||
/**
|
||||
* Check if a principal is allowed to perform an action on a resource.
|
||||
*
|
||||
* @param resource - Resource which we are validating.
|
||||
* @param action - Action which we are validating.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* await access.isAllowed(
|
||||
* {
|
||||
* kind: "document",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* "view"
|
||||
* ); // => true
|
||||
*/
|
||||
isAllowed(resource: any, action: string) {
|
||||
return cerbos.isAllowed({ principal, resource, action });
|
||||
},
|
||||
|
||||
/**
|
||||
* Check a principal's permissions on a resource.
|
||||
*
|
||||
* @param resource - Resource which we are validating.
|
||||
* @param actions - Actions which we are validating.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const decision = await access.checkResource(
|
||||
* {
|
||||
* kind: "document",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* ["view", "edit"],
|
||||
* );
|
||||
*
|
||||
* decision.isAllowed("view"); // => true
|
||||
*/
|
||||
checkResource(resource: any, actions: string[]) {
|
||||
return cerbos.checkResource({ principal, resource, actions });
|
||||
},
|
||||
|
||||
/**
|
||||
* Check a principal's permissions on a set of resources.
|
||||
*
|
||||
* @param resources - Resources which we are validating.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const decision = await access.checkResources([
|
||||
* {
|
||||
* resource: {
|
||||
* kind: "document",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* actions: ["view", "edit"],
|
||||
* },
|
||||
* {
|
||||
* resource: {
|
||||
* kind: "image",
|
||||
* id: "1",
|
||||
* attr: { owner: "user@example.com" },
|
||||
* },
|
||||
* actions: ["delete"],
|
||||
* },
|
||||
* ]);
|
||||
*
|
||||
* decision.isAllowed({
|
||||
* resource: { kind: "document", id: "1" },
|
||||
* action: "view",
|
||||
* }); // => true
|
||||
*/
|
||||
checkResources(resources: { resource: any; actions: string[] }[]) {
|
||||
return cerbos.checkResources({ principal, resources });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type AccessControlMethods = ReturnType<typeof getAccessControlMethods>;
|
||||
@@ -3,8 +3,8 @@ import "@platform/storage";
|
||||
|
||||
import type { Session } from "better-auth";
|
||||
|
||||
import type { AccessControlMethods } from "./access.ts";
|
||||
import type { Principal } from "./principal.ts";
|
||||
import type { identity } from "./client.ts";
|
||||
import type { Principal } from "./models/principal.ts";
|
||||
|
||||
declare module "@platform/storage" {
|
||||
interface StorageContext {
|
||||
@@ -21,7 +21,7 @@ declare module "@platform/storage" {
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
access?: AccessControlMethods;
|
||||
access?: typeof identity.access;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,6 @@ declare module "@platform/relay" {
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
access: AccessControlMethods;
|
||||
access: typeof identity.access;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user