parent
6e29d73572
commit
a9c58b6cc3
@ -1,54 +0,0 @@
|
||||
# perm-authz-ts
|
||||
|
||||
Implementación base de un sistema de autorización tipado para aplicaciones TypeScript con dos runtimes:
|
||||
|
||||
- `@perm/core`: tipos, schema, builders e IR. No depende de DB, DOM ni framework.
|
||||
- `@perm/server`: runtime autoritativo para backend: `check`, `assert`, `explain`, `filter`, adapters y endpoints HTTP.
|
||||
- `@perm/client`: cliente remoto para SPA/SSR: snapshot, cache, batch checks, invalidación.
|
||||
- `@perm/svelte`: integración Svelte/SvelteKit: context, stores y componente `<Can>`.
|
||||
|
||||
La regla de seguridad es: el servidor decide; el cliente refleja. El cliente sirve para UX y reactividad, no como frontera de seguridad.
|
||||
|
||||
## Ejemplo rápido
|
||||
|
||||
```ts
|
||||
import { allow, and, attr, definePolicies, definePermSchema, rel, actor } from "@perm/core";
|
||||
import { createPermissions } from "@perm/server";
|
||||
|
||||
const schema = definePermSchema({
|
||||
resources: {
|
||||
post: { actions: ["read", "update", "delete"] }
|
||||
},
|
||||
actors: {
|
||||
user: { actions: [] }
|
||||
},
|
||||
relations: {
|
||||
"post.owner": { from: "post", to: "user" }
|
||||
}
|
||||
});
|
||||
|
||||
const policies = definePolicies(schema, [
|
||||
allow("post.read").when(attr("post.visibility").eq("public")),
|
||||
allow("post.update").when(rel("post.owner").is(actor())),
|
||||
allow("post.delete").when(and(rel("post.owner").is(actor()), attr("actor.role").eq("admin")))
|
||||
]);
|
||||
|
||||
const Perm = createPermissions({
|
||||
schema,
|
||||
policies,
|
||||
providers: {
|
||||
relations: {
|
||||
async hasRelation({ relation, resource, subject }) {
|
||||
if (relation === "post.owner") return resource.ownerId === subject.id;
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Perm.assert({ actor: { type: "user", id: "u1" }, action: "post.update", resource: post });
|
||||
```
|
||||
|
||||
## Estado
|
||||
|
||||
Es una primera implementación funcional de arquitectura, no una librería productiva cerrada. Incluye el core, el evaluador, el combinador deny-overrides, cliente remoto, integración Svelte y compilers base. Faltan adapters productivos para Prisma/Drizzle/OpenFGA/SpiceDB, persistencia de auditoría y hardening completo de seguridad.
|
||||
@ -1,91 +0,0 @@
|
||||
import { actor, allow, and, attr, audit, definePermSchema, definePolicies, deny, rel } from "@perm/core";
|
||||
import { createPermissions, createSqlCompiler } from "@perm/server";
|
||||
|
||||
export const schema = definePermSchema({
|
||||
actors: {
|
||||
user: { attributes: { status: "string", role: "string" } }
|
||||
},
|
||||
resources: {
|
||||
post: {
|
||||
actions: ["read", "update", "delete", "publish"],
|
||||
attributes: {
|
||||
visibility: "string",
|
||||
ownerId: "string",
|
||||
teamId: "string",
|
||||
status: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
relations: {
|
||||
"post.owner": { from: "post", to: "user" },
|
||||
"post.team.member": { from: "post", to: "user" },
|
||||
"post.team.admin": { from: "post", to: "user" }
|
||||
},
|
||||
context: {
|
||||
risk: { mfa: "boolean" }
|
||||
}
|
||||
});
|
||||
|
||||
export const policies = definePolicies(schema, [
|
||||
deny("post.*")
|
||||
.id("post.deny.suspended")
|
||||
.priority(1000)
|
||||
.when(attr("actor.status").eq("suspended"))
|
||||
.because("Suspended users cannot access posts", "user_suspended"),
|
||||
|
||||
allow("post.read")
|
||||
.id("post.read.public-or-member")
|
||||
.when(
|
||||
// A minimal starter. In production, keep relations backed by DB/OpenFGA/SpiceDB.
|
||||
attr("post.visibility").eq("public")
|
||||
),
|
||||
|
||||
allow("post.update")
|
||||
.id("post.update.owner")
|
||||
.when(
|
||||
and(
|
||||
rel("post.owner").is(actor()),
|
||||
attr("post.status").notEq("archived")
|
||||
)
|
||||
),
|
||||
|
||||
allow("post.publish")
|
||||
.id("post.publish.admin-with-mfa")
|
||||
.when(
|
||||
and(
|
||||
rel("post.team.admin").has(actor()),
|
||||
attr("context.risk.mfa").eq(true)
|
||||
)
|
||||
)
|
||||
.oblige(audit("post.publish", "medium"))
|
||||
]);
|
||||
|
||||
export const Perm = createPermissions({
|
||||
schema,
|
||||
policies,
|
||||
providers: {
|
||||
relations: {
|
||||
async hasRelation({ relation, resource, subject }) {
|
||||
if (relation === "post.owner") return resource.ownerId === subject.id;
|
||||
if (relation === "post.team.admin") return subject.role === "admin";
|
||||
if (relation === "post.team.member") return Array.isArray(subject.teamIds) && subject.teamIds.includes(resource.teamId);
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
},
|
||||
compilers: [
|
||||
createSqlCompiler({
|
||||
resourceAlias: "post",
|
||||
relation({ relation, resourceAlias, actor, param }) {
|
||||
if (relation === "post.owner") return `${resourceAlias}.owner_id = ${param(actor.id)}`;
|
||||
if (relation === "post.team.member") {
|
||||
return `EXISTS (SELECT 1 FROM team_members tm WHERE tm.team_id = ${resourceAlias}.team_id AND tm.user_id = ${param(actor.id)})`;
|
||||
}
|
||||
if (relation === "post.team.admin") {
|
||||
return `EXISTS (SELECT 1 FROM team_members tm WHERE tm.team_id = ${resourceAlias}.team_id AND tm.user_id = ${param(actor.id)} AND tm.role = 'admin')`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { Perm } from "./permissions.js";
|
||||
|
||||
export async function updatePostRoute({ user, post, body }: { user: any; post: any; body: any }) {
|
||||
await Perm.assert({ actor: user, action: "post.update", resource: post });
|
||||
// db.posts.update(post.id, body)
|
||||
return { ok: true, updated: body };
|
||||
}
|
||||
|
||||
export async function listPostsRoute({ user }: { user: any }) {
|
||||
const plan = await Perm.filter("post.read").for(user).resource("post").toPlan("sql");
|
||||
// db.select().from(posts).where(plan.predicate.sql, plan.predicate.params)
|
||||
return plan;
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Can from "@perm/svelte/Can.svelte";
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<Can action="post.update" resource={post}>
|
||||
<button>Edit</button>
|
||||
<svelte:fragment slot="fallback">
|
||||
<button disabled>Edit</button>
|
||||
</svelte:fragment>
|
||||
</Can>
|
||||
|
||||
<Can action="post.delete" resource={post}>
|
||||
<button>Delete</button>
|
||||
</Can>
|
||||
@ -1,6 +0,0 @@
|
||||
import { createSveltePermissions } from "@perm/svelte";
|
||||
|
||||
export const perm = createSveltePermissions({
|
||||
endpoint: "/api/authz",
|
||||
cacheTtlMs: 30_000
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "perm-authz-ts-workspace",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Typed authorization core + server runtime + SPA/SSR clients.",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
],
|
||||
"scripts": {
|
||||
"typecheck": "tsc -b packages/core packages/server packages/client packages/svelte",
|
||||
"build": "tsc -b packages/core packages/server packages/client packages/svelte"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import type { PermissionClient, PermissionClientOptions } from "./types.js";
|
||||
export declare function createPermissionClient(options: PermissionClientOptions): PermissionClient;
|
||||
//# sourceMappingURL=createPermissionClient.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"createPermissionClient.d.ts","sourceRoot":"","sources":["../src/createPermissionClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsC,gBAAgB,EAAE,uBAAuB,EAAsB,MAAM,YAAY,CAAC;AA8BpI,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CA2IzF"}
|
||||
@ -1,156 +0,0 @@
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
function joinUrl(base, path) {
|
||||
return `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
||||
}
|
||||
async function postJson(fetcher, url, body) {
|
||||
const response = await fetcher(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Authorization request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
export function createPermissionClient(options) {
|
||||
const fetcher = options.fetcher ?? fetch.bind(globalThis);
|
||||
const cacheTtlMs = options.cacheTtlMs ?? 30_000;
|
||||
const cache = new Map();
|
||||
const listeners = new Set();
|
||||
let currentSnapshot = options.initialSnapshot ?? { decisions: {} };
|
||||
function emit() {
|
||||
for (const listener of listeners)
|
||||
listener(currentSnapshot);
|
||||
}
|
||||
function decisionKey(input) {
|
||||
const resource = input.resource ? `${input.resource.type}:${input.resource.id ?? "none"}` : "global";
|
||||
const contextKey = input.context ? JSON.stringify(input.context) : "";
|
||||
return `${resource}:${input.action}:${contextKey}`;
|
||||
}
|
||||
function readSnapshotDecision(input) {
|
||||
const key = decisionKey(input);
|
||||
const direct = currentSnapshot.decisions?.[key];
|
||||
if (direct)
|
||||
return direct;
|
||||
const global = currentSnapshot.global?.[input.action];
|
||||
if (typeof global === "boolean")
|
||||
return global ? { effect: "allow", policy: "snapshot.global" } : { effect: "deny", code: "snapshot_denied", reason: "Denied by snapshot" };
|
||||
return global;
|
||||
}
|
||||
function setCached(input, decision) {
|
||||
const key = decisionKey(input);
|
||||
cache.set(key, { decision, expiresAt: now() + cacheTtlMs });
|
||||
currentSnapshot = {
|
||||
...currentSnapshot,
|
||||
decisions: {
|
||||
...(currentSnapshot.decisions ?? {}),
|
||||
[key]: decision
|
||||
}
|
||||
};
|
||||
emit();
|
||||
}
|
||||
async function check(input) {
|
||||
const key = decisionKey(input);
|
||||
const cached = cache.get(key);
|
||||
if (cached && cached.expiresAt > now())
|
||||
return cached.decision;
|
||||
const snapshotDecision = readSnapshotDecision(input);
|
||||
if (snapshotDecision) {
|
||||
cache.set(key, { decision: snapshotDecision, expiresAt: now() + cacheTtlMs });
|
||||
return snapshotDecision;
|
||||
}
|
||||
try {
|
||||
const decision = await postJson(fetcher, joinUrl(options.endpoint, "/check"), input);
|
||||
setCached(input, decision);
|
||||
return decision;
|
||||
}
|
||||
catch (error) {
|
||||
options.onError?.(error);
|
||||
return { effect: "indeterminate", reason: "Remote authorization check failed", fallback: "deny", errors: [error] };
|
||||
}
|
||||
}
|
||||
async function can(input) {
|
||||
return (await check(input)).effect === "allow";
|
||||
}
|
||||
async function batch(input) {
|
||||
try {
|
||||
const result = await postJson(fetcher, joinUrl(options.endpoint, "/batch"), input);
|
||||
for (const item of input.checks) {
|
||||
const key = decisionKey(item);
|
||||
const resourceKey = item.resource ? `${item.resource.type}:${item.resource.id ?? "none"}:${item.action}` : `resource:none:${item.action}`;
|
||||
const decision = result.decisions[key] ?? result.decisions[resourceKey];
|
||||
if (decision)
|
||||
setCached(item, decision);
|
||||
}
|
||||
return result.decisions;
|
||||
}
|
||||
catch (error) {
|
||||
options.onError?.(error);
|
||||
const decisions = {};
|
||||
for (const item of input.checks) {
|
||||
decisions[decisionKey(item)] = { effect: "indeterminate", reason: "Remote batch authorization failed", fallback: "deny", errors: [error] };
|
||||
}
|
||||
return decisions;
|
||||
}
|
||||
}
|
||||
async function what(input) {
|
||||
try {
|
||||
const result = await postJson(fetcher, joinUrl(options.endpoint, "/what"), input);
|
||||
for (const [action, decision] of Object.entries(result.actions)) {
|
||||
setCached({ action, resource: input.resource, context: input.context }, decision);
|
||||
}
|
||||
return result.actions;
|
||||
}
|
||||
catch (error) {
|
||||
options.onError?.(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
function hydrate(snapshot) {
|
||||
currentSnapshot = snapshot;
|
||||
cache.clear();
|
||||
for (const [key, decision] of Object.entries(snapshot.decisions ?? {})) {
|
||||
cache.set(key, { decision, expiresAt: now() + cacheTtlMs });
|
||||
}
|
||||
emit();
|
||||
}
|
||||
function invalidate(scope) {
|
||||
if (!scope) {
|
||||
cache.clear();
|
||||
currentSnapshot = { ...currentSnapshot, decisions: {} };
|
||||
emit();
|
||||
return;
|
||||
}
|
||||
for (const key of [...cache.keys()]) {
|
||||
if (key.includes(scope))
|
||||
cache.delete(key);
|
||||
}
|
||||
const decisions = { ...(currentSnapshot.decisions ?? {}) };
|
||||
for (const key of Object.keys(decisions)) {
|
||||
if (key.includes(scope))
|
||||
delete decisions[key];
|
||||
}
|
||||
currentSnapshot = { ...currentSnapshot, decisions };
|
||||
emit();
|
||||
}
|
||||
return {
|
||||
check,
|
||||
can,
|
||||
batch,
|
||||
what,
|
||||
hydrate,
|
||||
snapshot: () => currentSnapshot,
|
||||
invalidate,
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(currentSnapshot);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
decisionKey
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=createPermissionClient.js.map
|
||||
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./createPermissionClient.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC"}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./createPermissionClient.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC"}
|
||||
@ -1,39 +0,0 @@
|
||||
import type { PermissionDecision, ResourceRef, SubjectRef } from "@perm/core";
|
||||
export interface PermissionSnapshot {
|
||||
actor?: SubjectRef;
|
||||
version?: string;
|
||||
decisions?: Record<string, PermissionDecision>;
|
||||
global?: Record<string, boolean | PermissionDecision>;
|
||||
expiresAt?: string;
|
||||
}
|
||||
export interface PermissionClientOptions {
|
||||
endpoint: string;
|
||||
fetcher?: typeof fetch;
|
||||
initialSnapshot?: PermissionSnapshot;
|
||||
cacheTtlMs?: number;
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
export interface ClientCheckInput {
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
export interface ClientBatchInput {
|
||||
checks: ClientCheckInput[];
|
||||
}
|
||||
export interface PermissionClient {
|
||||
check(input: ClientCheckInput): Promise<PermissionDecision>;
|
||||
can(input: ClientCheckInput): Promise<boolean>;
|
||||
batch(input: ClientBatchInput): Promise<Record<string, PermissionDecision>>;
|
||||
what(input: {
|
||||
resource?: ResourceRef;
|
||||
actions?: string[];
|
||||
context?: Record<string, unknown>;
|
||||
}): Promise<Record<string, PermissionDecision>>;
|
||||
hydrate(snapshot: PermissionSnapshot): void;
|
||||
snapshot(): PermissionSnapshot;
|
||||
invalidate(scope?: string): void;
|
||||
subscribe(listener: (snapshot: PermissionSnapshot) => void): () => void;
|
||||
decisionKey(input: ClientCheckInput): string;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,kBAAkB,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,eAAe,CAAC,EAAE,kBAAkB,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC5D,GAAG,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,KAAK,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC5E,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,WAAW,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC5I,OAAO,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC5C,QAAQ,IAAI,kBAAkB,CAAC;IAC/B,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAAC;CAC9C"}
|
||||
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "@perm/client",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"typecheck": "tsc -b --pretty"
|
||||
},
|
||||
"dependencies": {
|
||||
"@perm/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@ -1,171 +0,0 @@
|
||||
import type { PermissionDecision } from "@perm/core";
|
||||
import type { ClientBatchInput, ClientCheckInput, PermissionClient, PermissionClientOptions, PermissionSnapshot } from "./types.js";
|
||||
|
||||
interface CacheEntry {
|
||||
decision: PermissionDecision;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
function now(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function joinUrl(base: string, path: string): string {
|
||||
return `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
||||
}
|
||||
|
||||
async function postJson<T>(fetcher: typeof fetch, url: string, body: unknown): Promise<T> {
|
||||
const response = await fetcher(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Authorization request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json() as T;
|
||||
}
|
||||
|
||||
export function createPermissionClient(options: PermissionClientOptions): PermissionClient {
|
||||
const fetcher = options.fetcher ?? fetch.bind(globalThis);
|
||||
const cacheTtlMs = options.cacheTtlMs ?? 30_000;
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
const listeners = new Set<(snapshot: PermissionSnapshot) => void>();
|
||||
let currentSnapshot: PermissionSnapshot = options.initialSnapshot ?? { decisions: {} };
|
||||
|
||||
function emit(): void {
|
||||
for (const listener of listeners) listener(currentSnapshot);
|
||||
}
|
||||
|
||||
function decisionKey(input: ClientCheckInput): string {
|
||||
const resource = input.resource ? `${input.resource.type}:${input.resource.id ?? "none"}` : "global";
|
||||
const contextKey = input.context ? JSON.stringify(input.context) : "";
|
||||
return `${resource}:${input.action}:${contextKey}`;
|
||||
}
|
||||
|
||||
function readSnapshotDecision(input: ClientCheckInput): PermissionDecision | undefined {
|
||||
const key = decisionKey(input);
|
||||
const direct = currentSnapshot.decisions?.[key];
|
||||
if (direct) return direct;
|
||||
const global = currentSnapshot.global?.[input.action];
|
||||
if (typeof global === "boolean") return global ? { effect: "allow", policy: "snapshot.global" } : { effect: "deny", code: "snapshot_denied", reason: "Denied by snapshot" };
|
||||
return global;
|
||||
}
|
||||
|
||||
function setCached(input: ClientCheckInput, decision: PermissionDecision): void {
|
||||
const key = decisionKey(input);
|
||||
cache.set(key, { decision, expiresAt: now() + cacheTtlMs });
|
||||
currentSnapshot = {
|
||||
...currentSnapshot,
|
||||
decisions: {
|
||||
...(currentSnapshot.decisions ?? {}),
|
||||
[key]: decision
|
||||
}
|
||||
};
|
||||
emit();
|
||||
}
|
||||
|
||||
async function check(input: ClientCheckInput): Promise<PermissionDecision> {
|
||||
const key = decisionKey(input);
|
||||
const cached = cache.get(key);
|
||||
if (cached && cached.expiresAt > now()) return cached.decision;
|
||||
|
||||
const snapshotDecision = readSnapshotDecision(input);
|
||||
if (snapshotDecision) {
|
||||
cache.set(key, { decision: snapshotDecision, expiresAt: now() + cacheTtlMs });
|
||||
return snapshotDecision;
|
||||
}
|
||||
|
||||
try {
|
||||
const decision = await postJson<PermissionDecision>(fetcher, joinUrl(options.endpoint, "/check"), input);
|
||||
setCached(input, decision);
|
||||
return decision;
|
||||
} catch (error) {
|
||||
options.onError?.(error);
|
||||
return { effect: "indeterminate", reason: "Remote authorization check failed", fallback: "deny", errors: [error] };
|
||||
}
|
||||
}
|
||||
|
||||
async function can(input: ClientCheckInput): Promise<boolean> {
|
||||
return (await check(input)).effect === "allow";
|
||||
}
|
||||
|
||||
async function batch(input: ClientBatchInput): Promise<Record<string, PermissionDecision>> {
|
||||
try {
|
||||
const result = await postJson<{ decisions: Record<string, PermissionDecision> }>(fetcher, joinUrl(options.endpoint, "/batch"), input);
|
||||
for (const item of input.checks) {
|
||||
const key = decisionKey(item);
|
||||
const resourceKey = item.resource ? `${item.resource.type}:${item.resource.id ?? "none"}:${item.action}` : `resource:none:${item.action}`;
|
||||
const decision = result.decisions[key] ?? result.decisions[resourceKey];
|
||||
if (decision) setCached(item, decision);
|
||||
}
|
||||
return result.decisions;
|
||||
} catch (error) {
|
||||
options.onError?.(error);
|
||||
const decisions: Record<string, PermissionDecision> = {};
|
||||
for (const item of input.checks) {
|
||||
decisions[decisionKey(item)] = { effect: "indeterminate", reason: "Remote batch authorization failed", fallback: "deny", errors: [error] };
|
||||
}
|
||||
return decisions;
|
||||
}
|
||||
}
|
||||
|
||||
async function what(input: { resource?: ClientCheckInput["resource"]; actions?: string[]; context?: ClientCheckInput["context"] }): Promise<Record<string, PermissionDecision>> {
|
||||
try {
|
||||
const result = await postJson<{ actions: Record<string, PermissionDecision> }>(fetcher, joinUrl(options.endpoint, "/what"), input);
|
||||
for (const [action, decision] of Object.entries(result.actions)) {
|
||||
setCached({ action, resource: input.resource, context: input.context }, decision);
|
||||
}
|
||||
return result.actions;
|
||||
} catch (error) {
|
||||
options.onError?.(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function hydrate(snapshot: PermissionSnapshot): void {
|
||||
currentSnapshot = snapshot;
|
||||
cache.clear();
|
||||
for (const [key, decision] of Object.entries(snapshot.decisions ?? {})) {
|
||||
cache.set(key, { decision, expiresAt: now() + cacheTtlMs });
|
||||
}
|
||||
emit();
|
||||
}
|
||||
|
||||
function invalidate(scope?: string): void {
|
||||
if (!scope) {
|
||||
cache.clear();
|
||||
currentSnapshot = { ...currentSnapshot, decisions: {} };
|
||||
emit();
|
||||
return;
|
||||
}
|
||||
for (const key of [...cache.keys()]) {
|
||||
if (key.includes(scope)) cache.delete(key);
|
||||
}
|
||||
const decisions = { ...(currentSnapshot.decisions ?? {}) };
|
||||
for (const key of Object.keys(decisions)) {
|
||||
if (key.includes(scope)) delete decisions[key];
|
||||
}
|
||||
currentSnapshot = { ...currentSnapshot, decisions };
|
||||
emit();
|
||||
}
|
||||
|
||||
return {
|
||||
check,
|
||||
can,
|
||||
batch,
|
||||
what,
|
||||
hydrate,
|
||||
snapshot: () => currentSnapshot,
|
||||
invalidate,
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(currentSnapshot);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
decisionKey
|
||||
};
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./createPermissionClient.js";
|
||||
@ -1,39 +0,0 @@
|
||||
import type { PermissionDecision, ResourceRef, SubjectRef } from "@perm/core";
|
||||
|
||||
export interface PermissionSnapshot {
|
||||
actor?: SubjectRef;
|
||||
version?: string;
|
||||
decisions?: Record<string, PermissionDecision>;
|
||||
global?: Record<string, boolean | PermissionDecision>;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface PermissionClientOptions {
|
||||
endpoint: string;
|
||||
fetcher?: typeof fetch;
|
||||
initialSnapshot?: PermissionSnapshot;
|
||||
cacheTtlMs?: number;
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export interface ClientCheckInput {
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ClientBatchInput {
|
||||
checks: ClientCheckInput[];
|
||||
}
|
||||
|
||||
export interface PermissionClient {
|
||||
check(input: ClientCheckInput): Promise<PermissionDecision>;
|
||||
can(input: ClientCheckInput): Promise<boolean>;
|
||||
batch(input: ClientBatchInput): Promise<Record<string, PermissionDecision>>;
|
||||
what(input: { resource?: ResourceRef; actions?: string[]; context?: Record<string, unknown> }): Promise<Record<string, PermissionDecision>>;
|
||||
hydrate(snapshot: PermissionSnapshot): void;
|
||||
snapshot(): PermissionSnapshot;
|
||||
invalidate(scope?: string): void;
|
||||
subscribe(listener: (snapshot: PermissionSnapshot) => void): () => void;
|
||||
decisionKey(input: ClientCheckInput): string;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
],
|
||||
"include": ["src"]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -1,60 +0,0 @@
|
||||
import type { AdviceIR, ExprIR, ObligationIR, PermSchema, PolicyIR } from "./types.js";
|
||||
type ExprInput = ExprBuilder | ExprIR | string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
|
||||
export declare class ExprBuilder {
|
||||
readonly ir: ExprIR;
|
||||
constructor(ir: ExprIR);
|
||||
eq(value: ExprInput): ExprBuilder;
|
||||
notEq(value: ExprInput): ExprBuilder;
|
||||
gt(value: ExprInput): ExprBuilder;
|
||||
gte(value: ExprInput): ExprBuilder;
|
||||
lt(value: ExprInput): ExprBuilder;
|
||||
lte(value: ExprInput): ExprBuilder;
|
||||
in(set: ExprInput): ExprBuilder;
|
||||
contains(value: ExprInput): ExprBuilder;
|
||||
not(): ExprBuilder;
|
||||
}
|
||||
export declare class RelationBuilder {
|
||||
private readonly path;
|
||||
constructor(path: string);
|
||||
has(subject: ExprInput): ExprBuilder;
|
||||
is(subject: ExprInput): ExprBuilder;
|
||||
}
|
||||
export declare class PolicyBuilder {
|
||||
private readonly policyEffect;
|
||||
private readonly policyAction;
|
||||
private conditionIR;
|
||||
private policyId;
|
||||
private policyPriority;
|
||||
private policyReason;
|
||||
private policyCode;
|
||||
private policyObligations;
|
||||
private policyAdvice;
|
||||
private policyMetadata;
|
||||
constructor(policyEffect: "allow" | "deny", policyAction: string);
|
||||
id(id: string): this;
|
||||
priority(priority: number): this;
|
||||
when(expr: ExprInput): this;
|
||||
because(reason: string, code?: string): this;
|
||||
oblige(...obligations: ObligationIR[]): this;
|
||||
advise(...advice: AdviceIR[]): this;
|
||||
meta(metadata: Record<string, unknown>): this;
|
||||
build(): PolicyIR;
|
||||
}
|
||||
export declare function allow(action: string): PolicyBuilder;
|
||||
export declare function deny(action: string): PolicyBuilder;
|
||||
export declare function attr(path: string): ExprBuilder;
|
||||
export declare function ctx(path: string): ExprBuilder;
|
||||
export declare function actor(path?: string): ExprBuilder;
|
||||
export declare function resource(path?: string): ExprBuilder;
|
||||
export declare function val(value: unknown): ExprBuilder;
|
||||
export declare function rel(path: string): RelationBuilder;
|
||||
export declare function and(...args: ExprInput[]): ExprBuilder;
|
||||
export declare function or(...args: ExprInput[]): ExprBuilder;
|
||||
export declare function not(expr: ExprInput): ExprBuilder;
|
||||
export declare function mask(field: string, mode?: "full" | "partial"): ObligationIR;
|
||||
export declare function redact(field: string): ObligationIR;
|
||||
export declare function requireMfa(reason?: string): ObligationIR;
|
||||
export declare function audit(event: string, severity?: "low" | "medium" | "high"): ObligationIR;
|
||||
export declare function definePolicies(_schema: PermSchema, policies: Array<PolicyBuilder | PolicyIR>): PolicyIR[];
|
||||
export {};
|
||||
//# sourceMappingURL=builder.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,183 +0,0 @@
|
||||
import { stripResourcePrefix } from "./path.js";
|
||||
let autoPolicyCounter = 0;
|
||||
function toIR(input) {
|
||||
if (input instanceof ExprBuilder)
|
||||
return input.ir;
|
||||
if (isExprIR(input))
|
||||
return input;
|
||||
return { op: "const", value: input };
|
||||
}
|
||||
function isExprIR(input) {
|
||||
return Boolean(input && typeof input === "object" && "op" in input);
|
||||
}
|
||||
function refForAttribute(path) {
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
if (parts[0] === "actor")
|
||||
return { op: "ref", root: "actor", path: parts.slice(1).join(".") };
|
||||
if (parts[0] === "context")
|
||||
return { op: "ref", root: "context", path: parts.slice(1).join(".") };
|
||||
if (parts[0] === "resource")
|
||||
return { op: "ref", root: "resource", path: parts.slice(1).join(".") };
|
||||
return { op: "ref", root: "resource", path: stripResourcePrefix(path) };
|
||||
}
|
||||
export class ExprBuilder {
|
||||
ir;
|
||||
constructor(ir) {
|
||||
this.ir = ir;
|
||||
}
|
||||
eq(value) {
|
||||
return new ExprBuilder({ op: "eq", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
notEq(value) {
|
||||
return new ExprBuilder({ op: "neq", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
gt(value) {
|
||||
return new ExprBuilder({ op: "gt", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
gte(value) {
|
||||
return new ExprBuilder({ op: "gte", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
lt(value) {
|
||||
return new ExprBuilder({ op: "lt", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
lte(value) {
|
||||
return new ExprBuilder({ op: "lte", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
in(set) {
|
||||
return new ExprBuilder({ op: "in", value: this.ir, set: toIR(set) });
|
||||
}
|
||||
contains(value) {
|
||||
return new ExprBuilder({ op: "contains", set: this.ir, value: toIR(value) });
|
||||
}
|
||||
not() {
|
||||
return new ExprBuilder({ op: "not", expr: this.ir });
|
||||
}
|
||||
}
|
||||
export class RelationBuilder {
|
||||
path;
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
has(subject) {
|
||||
return new ExprBuilder({ op: "rel", path: this.path, subject: toIR(subject) });
|
||||
}
|
||||
is(subject) {
|
||||
return this.has(subject);
|
||||
}
|
||||
}
|
||||
export class PolicyBuilder {
|
||||
policyEffect;
|
||||
policyAction;
|
||||
conditionIR = { op: "const", value: true };
|
||||
policyId;
|
||||
policyPriority = 0;
|
||||
policyReason;
|
||||
policyCode;
|
||||
policyObligations = [];
|
||||
policyAdvice = [];
|
||||
policyMetadata;
|
||||
constructor(policyEffect, policyAction) {
|
||||
this.policyEffect = policyEffect;
|
||||
this.policyAction = policyAction;
|
||||
}
|
||||
id(id) {
|
||||
this.policyId = id;
|
||||
return this;
|
||||
}
|
||||
priority(priority) {
|
||||
this.policyPriority = priority;
|
||||
return this;
|
||||
}
|
||||
when(expr) {
|
||||
this.conditionIR = toIR(expr);
|
||||
return this;
|
||||
}
|
||||
because(reason, code) {
|
||||
this.policyReason = reason;
|
||||
this.policyCode = code;
|
||||
return this;
|
||||
}
|
||||
oblige(...obligations) {
|
||||
this.policyObligations.push(...obligations);
|
||||
return this;
|
||||
}
|
||||
advise(...advice) {
|
||||
this.policyAdvice.push(...advice);
|
||||
return this;
|
||||
}
|
||||
meta(metadata) {
|
||||
this.policyMetadata = metadata;
|
||||
return this;
|
||||
}
|
||||
build() {
|
||||
const resource = this.policyAction.includes(".") ? this.policyAction.split(".")[0] : undefined;
|
||||
const id = this.policyId ?? `${this.policyEffect}.${this.policyAction}.${++autoPolicyCounter}`;
|
||||
const policy = {
|
||||
id,
|
||||
effect: this.policyEffect,
|
||||
priority: this.policyPriority,
|
||||
target: resource ? { action: this.policyAction, resource } : { action: this.policyAction },
|
||||
condition: this.conditionIR
|
||||
};
|
||||
if (this.policyReason !== undefined)
|
||||
policy.reason = this.policyReason;
|
||||
if (this.policyCode !== undefined)
|
||||
policy.code = this.policyCode;
|
||||
if (this.policyObligations.length)
|
||||
policy.obligations = this.policyObligations;
|
||||
if (this.policyAdvice.length)
|
||||
policy.advice = this.policyAdvice;
|
||||
if (this.policyMetadata !== undefined)
|
||||
policy.metadata = this.policyMetadata;
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
export function allow(action) {
|
||||
return new PolicyBuilder("allow", action);
|
||||
}
|
||||
export function deny(action) {
|
||||
return new PolicyBuilder("deny", action);
|
||||
}
|
||||
export function attr(path) {
|
||||
return new ExprBuilder(refForAttribute(path));
|
||||
}
|
||||
export function ctx(path) {
|
||||
return new ExprBuilder({ op: "ref", root: "context", path });
|
||||
}
|
||||
export function actor(path = "") {
|
||||
return new ExprBuilder({ op: "ref", root: "actor", path });
|
||||
}
|
||||
export function resource(path = "") {
|
||||
return new ExprBuilder({ op: "ref", root: "resource", path });
|
||||
}
|
||||
export function val(value) {
|
||||
return new ExprBuilder({ op: "const", value });
|
||||
}
|
||||
export function rel(path) {
|
||||
return new RelationBuilder(path);
|
||||
}
|
||||
export function and(...args) {
|
||||
return new ExprBuilder({ op: "and", args: args.map(toIR) });
|
||||
}
|
||||
export function or(...args) {
|
||||
return new ExprBuilder({ op: "or", args: args.map(toIR) });
|
||||
}
|
||||
export function not(expr) {
|
||||
return new ExprBuilder({ op: "not", expr: toIR(expr) });
|
||||
}
|
||||
export function mask(field, mode = "full") {
|
||||
return { type: "mask", field, mode };
|
||||
}
|
||||
export function redact(field) {
|
||||
return { type: "redact", field };
|
||||
}
|
||||
export function requireMfa(reason = "MFA required") {
|
||||
return { type: "require_mfa", reason };
|
||||
}
|
||||
export function audit(event, severity = "low") {
|
||||
return { type: "audit", event, severity };
|
||||
}
|
||||
export function definePolicies(_schema, policies) {
|
||||
return policies.map((policy) => policy instanceof PolicyBuilder ? policy.build() : policy);
|
||||
}
|
||||
//# sourceMappingURL=builder.js.map
|
||||
File diff suppressed because one or more lines are too long
@ -1,5 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./schema.js";
|
||||
export * from "./builder.js";
|
||||
export * from "./path.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
|
||||
@ -1,5 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./schema.js";
|
||||
export * from "./builder.js";
|
||||
export * from "./path.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
|
||||
@ -1,4 +0,0 @@
|
||||
export declare function getPath(input: unknown, path: string): unknown;
|
||||
export declare function setPath(input: Record<string, unknown>, path: string, value: unknown): void;
|
||||
export declare function stripResourcePrefix(path: string): string;
|
||||
//# sourceMappingURL=path.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAEA,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAS7D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAY1F;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOxD"}
|
||||
@ -1,36 +0,0 @@
|
||||
export function getPath(input, path) {
|
||||
if (path === "" || path === ".")
|
||||
return input;
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
let current = input;
|
||||
for (const part of parts) {
|
||||
if (current == null || typeof current !== "object")
|
||||
return undefined;
|
||||
current = current[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
export function setPath(input, path, value) {
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
if (parts.length === 0)
|
||||
return;
|
||||
let current = input;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
const next = current[part];
|
||||
if (!next || typeof next !== "object") {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
}
|
||||
export function stripResourcePrefix(path) {
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
if (parts.length <= 1)
|
||||
return path;
|
||||
if (parts[0] === "actor" || parts[0] === "resource" || parts[0] === "context") {
|
||||
return parts.slice(1).join(".");
|
||||
}
|
||||
return parts.slice(1).join(".");
|
||||
}
|
||||
//# sourceMappingURL=path.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,OAAO,CAAC,KAAc,EAAE,IAAY;IAClD,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,OAAO,GAAY,KAAK,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACrE,OAAO,GAAI,OAAgB,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAA8B,EAAE,IAAY,EAAE,KAAc;IAClF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC/B,IAAI,OAAO,GAA4B,KAAK,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACrB,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,IAAI,CAA4B,CAAC;IACrD,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,GAAG,KAAK,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAC9E,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC"}
|
||||
@ -1,6 +0,0 @@
|
||||
import type { Action, PermSchema, ResourceRef } from "./types.js";
|
||||
export declare function definePermSchema<const T extends PermSchema>(schema: T): T;
|
||||
export declare function validateSchema(schema: PermSchema): void;
|
||||
export declare function actionResource(action: Action): string | undefined;
|
||||
export declare function resourceKey(resource?: ResourceRef | string): string;
|
||||
//# sourceMappingURL=schema.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElE,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAGzE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAqBvD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,CAInE"}
|
||||
@ -1,36 +0,0 @@
|
||||
export function definePermSchema(schema) {
|
||||
validateSchema(schema);
|
||||
return schema;
|
||||
}
|
||||
export function validateSchema(schema) {
|
||||
if (!schema.resources || Object.keys(schema.resources).length === 0) {
|
||||
throw new Error("perm schema requires at least one resource");
|
||||
}
|
||||
for (const [resource, def] of Object.entries(schema.resources)) {
|
||||
if (!Array.isArray(def.actions) || def.actions.length === 0) {
|
||||
throw new Error(`resource ${resource} requires actions[]`);
|
||||
}
|
||||
}
|
||||
for (const [relation, def] of Object.entries(schema.relations ?? {})) {
|
||||
if (!schema.resources[def.from]) {
|
||||
throw new Error(`relation ${relation} points from unknown resource ${def.from}`);
|
||||
}
|
||||
const toIsResource = Boolean(schema.resources[def.to]);
|
||||
const toIsActor = Boolean(schema.actors?.[def.to]);
|
||||
if (!toIsResource && !toIsActor) {
|
||||
throw new Error(`relation ${relation} points to unknown actor/resource ${def.to}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export function actionResource(action) {
|
||||
const [resource] = String(action).split(".");
|
||||
return resource || undefined;
|
||||
}
|
||||
export function resourceKey(resource) {
|
||||
if (!resource)
|
||||
return "none";
|
||||
if (typeof resource === "string")
|
||||
return resource;
|
||||
return `${resource.type}:${resource.id ?? "unknown"}`;
|
||||
}
|
||||
//# sourceMappingURL=schema.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAA6B,MAAS;IACpE,cAAc,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAkB;IAC/C,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5D,MAAM,IAAI,KAAK,CAAC,YAAY,QAAQ,qBAAqB,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC;QACrE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,YAAY,QAAQ,iCAAiC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACnF,CAAC;QACD,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACvD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,YAAY,IAAI,CAAC,SAAS,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,YAAY,QAAQ,qCAAqC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7C,OAAO,QAAQ,IAAI,SAAS,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAA+B;IACzD,IAAI,CAAC,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC7B,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAClD,OAAO,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,EAAE,IAAI,SAAS,EAAE,CAAC;AACxD,CAAC"}
|
||||
@ -1,173 +0,0 @@
|
||||
export type Primitive = string | number | boolean | null;
|
||||
export type JsonValue = Primitive | JsonValue[] | {
|
||||
[key: string]: JsonValue;
|
||||
};
|
||||
export type Dict<T = unknown> = Record<string, T>;
|
||||
export type ResourceType = string;
|
||||
export type ActorType = string;
|
||||
export type Action = `${string}.${string}` | `${string}.*` | string;
|
||||
export interface SubjectRef {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface ResourceRef {
|
||||
type: ResourceType;
|
||||
id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface RequestContext {
|
||||
actor: SubjectRef;
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
context?: Dict;
|
||||
requestId?: string;
|
||||
}
|
||||
export type DecisionEffect = "allow" | "deny" | "indeterminate" | "not_applicable";
|
||||
export interface ObligationIR {
|
||||
type: string;
|
||||
field?: string;
|
||||
mode?: string;
|
||||
event?: string;
|
||||
severity?: "low" | "medium" | "high";
|
||||
reason?: string;
|
||||
meta?: Dict;
|
||||
}
|
||||
export interface AdviceIR {
|
||||
type: string;
|
||||
message?: string;
|
||||
meta?: Dict;
|
||||
}
|
||||
export type PermissionDecision = {
|
||||
effect: "allow";
|
||||
policy: string;
|
||||
reason?: string;
|
||||
obligations?: ObligationIR[];
|
||||
advice?: AdviceIR[];
|
||||
ttl?: number;
|
||||
} | {
|
||||
effect: "deny";
|
||||
policy?: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
advice?: AdviceIR[];
|
||||
} | {
|
||||
effect: "indeterminate";
|
||||
reason: string;
|
||||
fallback: "deny" | "allow";
|
||||
errors?: unknown[];
|
||||
} | {
|
||||
effect: "not_applicable";
|
||||
reason?: string;
|
||||
};
|
||||
export type ExprIR = {
|
||||
op: "const";
|
||||
value: unknown;
|
||||
} | {
|
||||
op: "ref";
|
||||
root: "actor" | "resource" | "context";
|
||||
path: string;
|
||||
} | {
|
||||
op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte";
|
||||
left: ExprIR;
|
||||
right: ExprIR;
|
||||
} | {
|
||||
op: "in";
|
||||
value: ExprIR;
|
||||
set: ExprIR;
|
||||
} | {
|
||||
op: "contains";
|
||||
set: ExprIR;
|
||||
value: ExprIR;
|
||||
} | {
|
||||
op: "and" | "or";
|
||||
args: ExprIR[];
|
||||
} | {
|
||||
op: "not";
|
||||
expr: ExprIR;
|
||||
} | {
|
||||
op: "rel";
|
||||
path: string;
|
||||
subject: ExprIR;
|
||||
resource?: ExprIR;
|
||||
} | {
|
||||
op: "exists";
|
||||
relation: string;
|
||||
where?: ExprIR;
|
||||
};
|
||||
export interface PolicyTargetIR {
|
||||
action: string;
|
||||
resource?: string;
|
||||
}
|
||||
export interface PolicyIR {
|
||||
id: string;
|
||||
effect: "allow" | "deny";
|
||||
priority: number;
|
||||
target: PolicyTargetIR;
|
||||
condition: ExprIR;
|
||||
reason?: string;
|
||||
code?: string;
|
||||
obligations?: ObligationIR[];
|
||||
advice?: AdviceIR[];
|
||||
metadata?: Dict;
|
||||
}
|
||||
export interface TraceEntry {
|
||||
policy: string;
|
||||
effect: "allow" | "deny";
|
||||
targetMatched: boolean;
|
||||
condition?: ExprIR;
|
||||
result?: EvalValue;
|
||||
reason?: string;
|
||||
dependencies: DependencyKey[];
|
||||
}
|
||||
export interface ExplainResult {
|
||||
actor: SubjectRef;
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
decision: PermissionDecision;
|
||||
trace: TraceEntry[];
|
||||
dependencies: DependencyKey[];
|
||||
}
|
||||
export type EvalValue = true | false | "unknown" | "error";
|
||||
export interface EvalResult {
|
||||
value: EvalValue;
|
||||
reason?: string;
|
||||
error?: unknown;
|
||||
dependencies: DependencyKey[];
|
||||
}
|
||||
export type DependencyKind = "actor" | "resource" | "context" | "relation";
|
||||
export type DependencyKey = `${DependencyKind}:${string}`;
|
||||
export interface ResourceSchema {
|
||||
actions: readonly string[];
|
||||
attributes?: Dict;
|
||||
}
|
||||
export interface ActorSchema {
|
||||
actions?: readonly string[];
|
||||
attributes?: Dict;
|
||||
}
|
||||
export interface RelationSchema {
|
||||
from: string;
|
||||
to: string;
|
||||
via?: string;
|
||||
table?: string;
|
||||
metadata?: Dict;
|
||||
}
|
||||
export interface PermSchema {
|
||||
actors?: Record<string, ActorSchema>;
|
||||
resources: Record<string, ResourceSchema>;
|
||||
relations?: Record<string, RelationSchema>;
|
||||
context?: Dict;
|
||||
}
|
||||
export interface QueryPlan {
|
||||
strategy: "compiled" | "partial" | "not_compilable";
|
||||
target: string;
|
||||
predicate?: unknown;
|
||||
residualPolicies?: PolicyIR[];
|
||||
dependencies?: DependencyKey[];
|
||||
warnings?: string[];
|
||||
}
|
||||
export declare class PermissionDeniedError extends Error {
|
||||
readonly decision: PermissionDecision;
|
||||
constructor(decision: PermissionDecision);
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,9 +0,0 @@
|
||||
export class PermissionDeniedError extends Error {
|
||||
decision;
|
||||
constructor(decision) {
|
||||
super(decision.effect === "deny" ? decision.reason : `Permission denied: ${decision.effect}`);
|
||||
this.name = "PermissionDeniedError";
|
||||
this.decision = decision;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=types.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAwKA,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IACrC,QAAQ,CAAqB;IACtC,YAAY,QAA4B;QACtC,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,sBAAsB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9F,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF"}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "@perm/core",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"typecheck": "tsc -b --pretty"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@ -1,210 +0,0 @@
|
||||
import type { AdviceIR, ExprIR, ObligationIR, PermSchema, PolicyIR } from "./types.js";
|
||||
import { stripResourcePrefix } from "./path.js";
|
||||
|
||||
let autoPolicyCounter = 0;
|
||||
|
||||
type ExprInput = ExprBuilder | ExprIR | string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
|
||||
|
||||
function toIR(input: ExprInput): ExprIR {
|
||||
if (input instanceof ExprBuilder) return input.ir;
|
||||
if (isExprIR(input)) return input;
|
||||
return { op: "const", value: input };
|
||||
}
|
||||
|
||||
function isExprIR(input: unknown): input is ExprIR {
|
||||
return Boolean(input && typeof input === "object" && "op" in input);
|
||||
}
|
||||
|
||||
function refForAttribute(path: string): ExprIR {
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
if (parts[0] === "actor") return { op: "ref", root: "actor", path: parts.slice(1).join(".") };
|
||||
if (parts[0] === "context") return { op: "ref", root: "context", path: parts.slice(1).join(".") };
|
||||
if (parts[0] === "resource") return { op: "ref", root: "resource", path: parts.slice(1).join(".") };
|
||||
return { op: "ref", root: "resource", path: stripResourcePrefix(path) };
|
||||
}
|
||||
|
||||
export class ExprBuilder {
|
||||
constructor(readonly ir: ExprIR) {}
|
||||
|
||||
eq(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "eq", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
|
||||
notEq(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "neq", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
|
||||
gt(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "gt", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
|
||||
gte(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "gte", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
|
||||
lt(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "lt", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
|
||||
lte(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "lte", left: this.ir, right: toIR(value) });
|
||||
}
|
||||
|
||||
in(set: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "in", value: this.ir, set: toIR(set) });
|
||||
}
|
||||
|
||||
contains(value: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "contains", set: this.ir, value: toIR(value) });
|
||||
}
|
||||
|
||||
not(): ExprBuilder {
|
||||
return new ExprBuilder({ op: "not", expr: this.ir });
|
||||
}
|
||||
}
|
||||
|
||||
export class RelationBuilder {
|
||||
constructor(private readonly path: string) {}
|
||||
|
||||
has(subject: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "rel", path: this.path, subject: toIR(subject) });
|
||||
}
|
||||
|
||||
is(subject: ExprInput): ExprBuilder {
|
||||
return this.has(subject);
|
||||
}
|
||||
}
|
||||
|
||||
export class PolicyBuilder {
|
||||
private conditionIR: ExprIR = { op: "const", value: true };
|
||||
private policyId: string | undefined;
|
||||
private policyPriority = 0;
|
||||
private policyReason: string | undefined;
|
||||
private policyCode: string | undefined;
|
||||
private policyObligations: ObligationIR[] = [];
|
||||
private policyAdvice: AdviceIR[] = [];
|
||||
private policyMetadata: Record<string, unknown> | undefined;
|
||||
|
||||
constructor(private readonly policyEffect: "allow" | "deny", private readonly policyAction: string) {}
|
||||
|
||||
id(id: string): this {
|
||||
this.policyId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
priority(priority: number): this {
|
||||
this.policyPriority = priority;
|
||||
return this;
|
||||
}
|
||||
|
||||
when(expr: ExprInput): this {
|
||||
this.conditionIR = toIR(expr);
|
||||
return this;
|
||||
}
|
||||
|
||||
because(reason: string, code?: string): this {
|
||||
this.policyReason = reason;
|
||||
this.policyCode = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
oblige(...obligations: ObligationIR[]): this {
|
||||
this.policyObligations.push(...obligations);
|
||||
return this;
|
||||
}
|
||||
|
||||
advise(...advice: AdviceIR[]): this {
|
||||
this.policyAdvice.push(...advice);
|
||||
return this;
|
||||
}
|
||||
|
||||
meta(metadata: Record<string, unknown>): this {
|
||||
this.policyMetadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): PolicyIR {
|
||||
const resource = this.policyAction.includes(".") ? this.policyAction.split(".")[0] : undefined;
|
||||
const id = this.policyId ?? `${this.policyEffect}.${this.policyAction}.${++autoPolicyCounter}`;
|
||||
|
||||
const policy: PolicyIR = {
|
||||
id,
|
||||
effect: this.policyEffect,
|
||||
priority: this.policyPriority,
|
||||
target: resource ? { action: this.policyAction, resource } : { action: this.policyAction },
|
||||
condition: this.conditionIR
|
||||
};
|
||||
|
||||
if (this.policyReason !== undefined) policy.reason = this.policyReason;
|
||||
if (this.policyCode !== undefined) policy.code = this.policyCode;
|
||||
if (this.policyObligations.length) policy.obligations = this.policyObligations;
|
||||
if (this.policyAdvice.length) policy.advice = this.policyAdvice;
|
||||
if (this.policyMetadata !== undefined) policy.metadata = this.policyMetadata;
|
||||
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
|
||||
export function allow(action: string): PolicyBuilder {
|
||||
return new PolicyBuilder("allow", action);
|
||||
}
|
||||
|
||||
export function deny(action: string): PolicyBuilder {
|
||||
return new PolicyBuilder("deny", action);
|
||||
}
|
||||
|
||||
export function attr(path: string): ExprBuilder {
|
||||
return new ExprBuilder(refForAttribute(path));
|
||||
}
|
||||
|
||||
export function ctx(path: string): ExprBuilder {
|
||||
return new ExprBuilder({ op: "ref", root: "context", path });
|
||||
}
|
||||
|
||||
export function actor(path = ""): ExprBuilder {
|
||||
return new ExprBuilder({ op: "ref", root: "actor", path });
|
||||
}
|
||||
|
||||
export function resource(path = ""): ExprBuilder {
|
||||
return new ExprBuilder({ op: "ref", root: "resource", path });
|
||||
}
|
||||
|
||||
export function val(value: unknown): ExprBuilder {
|
||||
return new ExprBuilder({ op: "const", value });
|
||||
}
|
||||
|
||||
export function rel(path: string): RelationBuilder {
|
||||
return new RelationBuilder(path);
|
||||
}
|
||||
|
||||
export function and(...args: ExprInput[]): ExprBuilder {
|
||||
return new ExprBuilder({ op: "and", args: args.map(toIR) });
|
||||
}
|
||||
|
||||
export function or(...args: ExprInput[]): ExprBuilder {
|
||||
return new ExprBuilder({ op: "or", args: args.map(toIR) });
|
||||
}
|
||||
|
||||
export function not(expr: ExprInput): ExprBuilder {
|
||||
return new ExprBuilder({ op: "not", expr: toIR(expr) });
|
||||
}
|
||||
|
||||
export function mask(field: string, mode: "full" | "partial" = "full"): ObligationIR {
|
||||
return { type: "mask", field, mode };
|
||||
}
|
||||
|
||||
export function redact(field: string): ObligationIR {
|
||||
return { type: "redact", field };
|
||||
}
|
||||
|
||||
export function requireMfa(reason = "MFA required"): ObligationIR {
|
||||
return { type: "require_mfa", reason };
|
||||
}
|
||||
|
||||
export function audit(event: string, severity: "low" | "medium" | "high" = "low"): ObligationIR {
|
||||
return { type: "audit", event, severity };
|
||||
}
|
||||
|
||||
export function definePolicies(_schema: PermSchema, policies: Array<PolicyBuilder | PolicyIR>): PolicyIR[] {
|
||||
return policies.map((policy) => policy instanceof PolicyBuilder ? policy.build() : policy);
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./schema.js";
|
||||
export * from "./builder.js";
|
||||
export * from "./path.js";
|
||||
@ -1,35 +0,0 @@
|
||||
import type { Dict } from "./types.js";
|
||||
|
||||
export function getPath(input: unknown, path: string): unknown {
|
||||
if (path === "" || path === ".") return input;
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
let current: unknown = input;
|
||||
for (const part of parts) {
|
||||
if (current == null || typeof current !== "object") return undefined;
|
||||
current = (current as Dict)[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export function setPath(input: Record<string, unknown>, path: string, value: unknown): void {
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
if (parts.length === 0) return;
|
||||
let current: Record<string, unknown> = input;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
const next = current[part];
|
||||
if (!next || typeof next !== "object") {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part] as Record<string, unknown>;
|
||||
}
|
||||
current[parts[parts.length - 1]!] = value;
|
||||
}
|
||||
|
||||
export function stripResourcePrefix(path: string): string {
|
||||
const parts = path.split(".").filter(Boolean);
|
||||
if (parts.length <= 1) return path;
|
||||
if (parts[0] === "actor" || parts[0] === "resource" || parts[0] === "context") {
|
||||
return parts.slice(1).join(".");
|
||||
}
|
||||
return parts.slice(1).join(".");
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import type { Action, PermSchema, ResourceRef } from "./types.js";
|
||||
|
||||
export function definePermSchema<const T extends PermSchema>(schema: T): T {
|
||||
validateSchema(schema);
|
||||
return schema;
|
||||
}
|
||||
|
||||
export function validateSchema(schema: PermSchema): void {
|
||||
if (!schema.resources || Object.keys(schema.resources).length === 0) {
|
||||
throw new Error("perm schema requires at least one resource");
|
||||
}
|
||||
|
||||
for (const [resource, def] of Object.entries(schema.resources)) {
|
||||
if (!Array.isArray(def.actions) || def.actions.length === 0) {
|
||||
throw new Error(`resource ${resource} requires actions[]`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [relation, def] of Object.entries(schema.relations ?? {})) {
|
||||
if (!schema.resources[def.from]) {
|
||||
throw new Error(`relation ${relation} points from unknown resource ${def.from}`);
|
||||
}
|
||||
const toIsResource = Boolean(schema.resources[def.to]);
|
||||
const toIsActor = Boolean(schema.actors?.[def.to]);
|
||||
if (!toIsResource && !toIsActor) {
|
||||
throw new Error(`relation ${relation} points to unknown actor/resource ${def.to}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function actionResource(action: Action): string | undefined {
|
||||
const [resource] = String(action).split(".");
|
||||
return resource || undefined;
|
||||
}
|
||||
|
||||
export function resourceKey(resource?: ResourceRef | string): string {
|
||||
if (!resource) return "none";
|
||||
if (typeof resource === "string") return resource;
|
||||
return `${resource.type}:${resource.id ?? "unknown"}`;
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
export type Primitive = string | number | boolean | null;
|
||||
export type JsonValue = Primitive | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
export type Dict<T = unknown> = Record<string, T>;
|
||||
|
||||
export type ResourceType = string;
|
||||
export type ActorType = string;
|
||||
export type Action = `${string}.${string}` | `${string}.*` | string;
|
||||
|
||||
export interface SubjectRef {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ResourceRef {
|
||||
type: ResourceType;
|
||||
id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface RequestContext {
|
||||
actor: SubjectRef;
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
context?: Dict;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export type DecisionEffect = "allow" | "deny" | "indeterminate" | "not_applicable";
|
||||
|
||||
export interface ObligationIR {
|
||||
type: string;
|
||||
field?: string;
|
||||
mode?: string;
|
||||
event?: string;
|
||||
severity?: "low" | "medium" | "high";
|
||||
reason?: string;
|
||||
meta?: Dict;
|
||||
}
|
||||
|
||||
export interface AdviceIR {
|
||||
type: string;
|
||||
message?: string;
|
||||
meta?: Dict;
|
||||
}
|
||||
|
||||
export type PermissionDecision =
|
||||
| {
|
||||
effect: "allow";
|
||||
policy: string;
|
||||
reason?: string;
|
||||
obligations?: ObligationIR[];
|
||||
advice?: AdviceIR[];
|
||||
ttl?: number;
|
||||
}
|
||||
| {
|
||||
effect: "deny";
|
||||
policy?: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
advice?: AdviceIR[];
|
||||
}
|
||||
| {
|
||||
effect: "indeterminate";
|
||||
reason: string;
|
||||
fallback: "deny" | "allow";
|
||||
errors?: unknown[];
|
||||
}
|
||||
| {
|
||||
effect: "not_applicable";
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type ExprIR =
|
||||
| { op: "const"; value: unknown }
|
||||
| { op: "ref"; root: "actor" | "resource" | "context"; path: string }
|
||||
| { op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte"; left: ExprIR; right: ExprIR }
|
||||
| { op: "in"; value: ExprIR; set: ExprIR }
|
||||
| { op: "contains"; set: ExprIR; value: ExprIR }
|
||||
| { op: "and" | "or"; args: ExprIR[] }
|
||||
| { op: "not"; expr: ExprIR }
|
||||
| { op: "rel"; path: string; subject: ExprIR; resource?: ExprIR }
|
||||
| { op: "exists"; relation: string; where?: ExprIR };
|
||||
|
||||
export interface PolicyTargetIR {
|
||||
action: string;
|
||||
resource?: string;
|
||||
}
|
||||
|
||||
export interface PolicyIR {
|
||||
id: string;
|
||||
effect: "allow" | "deny";
|
||||
priority: number;
|
||||
target: PolicyTargetIR;
|
||||
condition: ExprIR;
|
||||
reason?: string;
|
||||
code?: string;
|
||||
obligations?: ObligationIR[];
|
||||
advice?: AdviceIR[];
|
||||
metadata?: Dict;
|
||||
}
|
||||
|
||||
export interface TraceEntry {
|
||||
policy: string;
|
||||
effect: "allow" | "deny";
|
||||
targetMatched: boolean;
|
||||
condition?: ExprIR;
|
||||
result?: EvalValue;
|
||||
reason?: string;
|
||||
dependencies: DependencyKey[];
|
||||
}
|
||||
|
||||
export interface ExplainResult {
|
||||
actor: SubjectRef;
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
decision: PermissionDecision;
|
||||
trace: TraceEntry[];
|
||||
dependencies: DependencyKey[];
|
||||
}
|
||||
|
||||
export type EvalValue = true | false | "unknown" | "error";
|
||||
|
||||
export interface EvalResult {
|
||||
value: EvalValue;
|
||||
reason?: string;
|
||||
error?: unknown;
|
||||
dependencies: DependencyKey[];
|
||||
}
|
||||
|
||||
export type DependencyKind = "actor" | "resource" | "context" | "relation";
|
||||
export type DependencyKey = `${DependencyKind}:${string}`;
|
||||
|
||||
export interface ResourceSchema {
|
||||
actions: readonly string[];
|
||||
attributes?: Dict;
|
||||
}
|
||||
|
||||
export interface ActorSchema {
|
||||
actions?: readonly string[];
|
||||
attributes?: Dict;
|
||||
}
|
||||
|
||||
export interface RelationSchema {
|
||||
from: string;
|
||||
to: string;
|
||||
via?: string;
|
||||
table?: string;
|
||||
metadata?: Dict;
|
||||
}
|
||||
|
||||
export interface PermSchema {
|
||||
actors?: Record<string, ActorSchema>;
|
||||
resources: Record<string, ResourceSchema>;
|
||||
relations?: Record<string, RelationSchema>;
|
||||
context?: Dict;
|
||||
}
|
||||
|
||||
export interface QueryPlan {
|
||||
strategy: "compiled" | "partial" | "not_compilable";
|
||||
target: string;
|
||||
predicate?: unknown;
|
||||
residualPolicies?: PolicyIR[];
|
||||
dependencies?: DependencyKey[];
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export class PermissionDeniedError extends Error {
|
||||
readonly decision: PermissionDecision;
|
||||
constructor(decision: PermissionDecision) {
|
||||
super(decision.effect === "deny" ? decision.reason : `Permission denied: ${decision.effect}`);
|
||||
this.name = "PermissionDeniedError";
|
||||
this.decision = decision;
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -1,9 +0,0 @@
|
||||
import type { EvalResult, PermissionDecision, PolicyIR, TraceEntry } from "@perm/core";
|
||||
export interface EvaluatedPolicy {
|
||||
policy: PolicyIR;
|
||||
targetMatched: boolean;
|
||||
evaluation?: EvalResult;
|
||||
}
|
||||
export declare function combineEvaluatedPolicies(evaluated: EvaluatedPolicy[], defaultFallback?: "deny" | "allow"): PermissionDecision;
|
||||
export declare function toTrace(evaluated: EvaluatedPolicy[]): TraceEntry[];
|
||||
//# sourceMappingURL=combiner.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"combiner.d.ts","sourceRoot":"","sources":["../src/combiner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,QAAQ,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,eAAe,GAAE,MAAM,GAAG,OAAgB,GAAG,kBAAkB,CAqDrI;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,UAAU,EAAE,CAUlE"}
|
||||
@ -1,60 +0,0 @@
|
||||
export function combineEvaluatedPolicies(evaluated, defaultFallback = "deny") {
|
||||
const applicable = evaluated.filter((entry) => entry.targetMatched && entry.evaluation?.value === true);
|
||||
const deny = applicable
|
||||
.filter((entry) => entry.policy.effect === "deny")
|
||||
.sort((a, b) => b.policy.priority - a.policy.priority)[0];
|
||||
if (deny) {
|
||||
return {
|
||||
effect: "deny",
|
||||
policy: deny.policy.id,
|
||||
code: deny.policy.code ?? "permission_denied",
|
||||
reason: deny.policy.reason ?? `Denied by policy ${deny.policy.id}`,
|
||||
advice: deny.policy.advice
|
||||
};
|
||||
}
|
||||
const allow = applicable
|
||||
.filter((entry) => entry.policy.effect === "allow")
|
||||
.sort((a, b) => b.policy.priority - a.policy.priority)[0];
|
||||
if (allow) {
|
||||
return {
|
||||
effect: "allow",
|
||||
policy: allow.policy.id,
|
||||
reason: allow.policy.reason,
|
||||
obligations: allow.policy.obligations,
|
||||
advice: allow.policy.advice
|
||||
};
|
||||
}
|
||||
const errored = evaluated.find((entry) => entry.targetMatched && entry.evaluation?.value === "error");
|
||||
if (errored?.evaluation) {
|
||||
return {
|
||||
effect: "indeterminate",
|
||||
reason: errored.evaluation.reason ?? "Authorization policy evaluation failed",
|
||||
fallback: defaultFallback,
|
||||
errors: errored.evaluation.error ? [errored.evaluation.error] : undefined
|
||||
};
|
||||
}
|
||||
const unknown = evaluated.find((entry) => entry.targetMatched && entry.evaluation?.value === "unknown");
|
||||
if (unknown?.evaluation) {
|
||||
return {
|
||||
effect: "indeterminate",
|
||||
reason: unknown.evaluation.reason ?? "Authorization policy evaluation is unknown",
|
||||
fallback: defaultFallback
|
||||
};
|
||||
}
|
||||
return {
|
||||
effect: "not_applicable",
|
||||
reason: "No matching policy allowed or denied the request"
|
||||
};
|
||||
}
|
||||
export function toTrace(evaluated) {
|
||||
return evaluated.map((entry) => ({
|
||||
policy: entry.policy.id,
|
||||
effect: entry.policy.effect,
|
||||
targetMatched: entry.targetMatched,
|
||||
condition: entry.targetMatched ? entry.policy.condition : undefined,
|
||||
result: entry.evaluation?.value,
|
||||
reason: entry.evaluation?.reason,
|
||||
dependencies: entry.evaluation?.dependencies ?? []
|
||||
}));
|
||||
}
|
||||
//# sourceMappingURL=combiner.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"combiner.js","sourceRoot":"","sources":["../src/combiner.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,20 +0,0 @@
|
||||
import type { SubjectRef } from "@perm/core";
|
||||
import type { QueryCompiler } from "../types.js";
|
||||
export interface SqlCompileResult {
|
||||
sql: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
export interface SqlRelationCompilerInput {
|
||||
relation: string;
|
||||
resourceAlias: string;
|
||||
actor: SubjectRef;
|
||||
param(value: unknown): string;
|
||||
}
|
||||
export type SqlRelationCompiler = (input: SqlRelationCompilerInput) => string | undefined;
|
||||
export interface CreateSqlCompilerOptions {
|
||||
resourceAlias?: string;
|
||||
relation?: SqlRelationCompiler;
|
||||
columnName?: (path: string) => string;
|
||||
}
|
||||
export declare function createSqlCompiler(options?: CreateSqlCompilerOptions): QueryCompiler;
|
||||
//# sourceMappingURL=sql.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/compilers/sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+B,UAAU,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,wBAAwB,KAAK,MAAM,GAAG,SAAS,CAAC;AAE1F,MAAM,WAAW,wBAAwB;IACvC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAaD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,aAAa,CA4IvF"}
|
||||
@ -1,147 +0,0 @@
|
||||
import { actionMatches } from "../match.js";
|
||||
function defaultColumnName(path) {
|
||||
return path.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
||||
}
|
||||
function relevantPolicies(policies, action, resourceType) {
|
||||
return policies.filter((policy) => {
|
||||
const resourceMatches = !policy.target.resource || policy.target.resource === resourceType;
|
||||
return resourceMatches && actionMatches(policy.target.action, action);
|
||||
});
|
||||
}
|
||||
export function createSqlCompiler(options = {}) {
|
||||
const alias = options.resourceAlias ?? "resource";
|
||||
const columnName = options.columnName ?? defaultColumnName;
|
||||
return {
|
||||
target: "sql",
|
||||
compile(input) {
|
||||
const policies = relevantPolicies(input.policies, input.action, input.resourceType);
|
||||
const allowPolicies = policies.filter((policy) => policy.effect === "allow");
|
||||
const denyPolicies = policies.filter((policy) => policy.effect === "deny");
|
||||
const params = {};
|
||||
let counter = 0;
|
||||
const warnings = [];
|
||||
const residualPolicies = [];
|
||||
const param = (value) => {
|
||||
const key = `p${++counter}`;
|
||||
params[key] = value;
|
||||
return `:${key}`;
|
||||
};
|
||||
const compileExpr = (expr) => {
|
||||
switch (expr.op) {
|
||||
case "const":
|
||||
if (typeof expr.value === "boolean")
|
||||
return expr.value ? "TRUE" : "FALSE";
|
||||
return param(expr.value);
|
||||
case "ref":
|
||||
if (expr.root === "resource")
|
||||
return `${alias}.${columnName(expr.path)}`;
|
||||
if (expr.root === "actor") {
|
||||
const value = expr.path ? input.actor[expr.path] : input.actor;
|
||||
return param(value);
|
||||
}
|
||||
if (expr.root === "context") {
|
||||
const value = expr.path ? input.context?.[expr.path] : input.context;
|
||||
return param(value);
|
||||
}
|
||||
return undefined;
|
||||
case "eq":
|
||||
case "neq":
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte": {
|
||||
const left = compileExpr(expr.left);
|
||||
const right = compileExpr(expr.right);
|
||||
if (!left || !right)
|
||||
return undefined;
|
||||
const op = expr.op === "eq" ? "=" : expr.op === "neq" ? "<>" : expr.op === "gt" ? ">" : expr.op === "gte" ? ">=" : expr.op === "lt" ? "<" : "<=";
|
||||
return `(${left} ${op} ${right})`;
|
||||
}
|
||||
case "and": {
|
||||
const parts = expr.args.map(compileExpr);
|
||||
if (parts.some((part) => !part))
|
||||
return undefined;
|
||||
return `(${parts.join(" AND ")})`;
|
||||
}
|
||||
case "or": {
|
||||
const parts = expr.args.map(compileExpr);
|
||||
if (parts.some((part) => !part))
|
||||
return undefined;
|
||||
return `(${parts.join(" OR ")})`;
|
||||
}
|
||||
case "not": {
|
||||
const inner = compileExpr(expr.expr);
|
||||
return inner ? `(NOT ${inner})` : undefined;
|
||||
}
|
||||
case "in": {
|
||||
const value = compileExpr(expr.value);
|
||||
if (!value)
|
||||
return undefined;
|
||||
if (expr.set.op === "const" && Array.isArray(expr.set.value)) {
|
||||
const list = expr.set.value.map(param).join(", ");
|
||||
return `(${value} IN (${list}))`;
|
||||
}
|
||||
const set = compileExpr(expr.set);
|
||||
return set ? `(${value} IN ${set})` : undefined;
|
||||
}
|
||||
case "contains": {
|
||||
const value = compileExpr(expr.value);
|
||||
if (!value)
|
||||
return undefined;
|
||||
if (expr.set.op === "const" && Array.isArray(expr.set.value)) {
|
||||
const list = expr.set.value.map(param).join(", ");
|
||||
return `(${value} IN (${list}))`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
case "rel": {
|
||||
const sql = options.relation?.({ relation: expr.path, resourceAlias: alias, actor: input.actor, param });
|
||||
if (!sql)
|
||||
return undefined;
|
||||
return `(${sql})`;
|
||||
}
|
||||
case "exists":
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const allowSql = [];
|
||||
for (const policy of allowPolicies) {
|
||||
const compiled = compileExpr(policy.condition);
|
||||
if (compiled)
|
||||
allowSql.push(compiled);
|
||||
else
|
||||
residualPolicies.push(policy);
|
||||
}
|
||||
const denySql = [];
|
||||
for (const policy of denyPolicies) {
|
||||
const compiled = compileExpr(policy.condition);
|
||||
if (compiled)
|
||||
denySql.push(compiled);
|
||||
else
|
||||
residualPolicies.push(policy);
|
||||
}
|
||||
if (allowSql.length === 0 && residualPolicies.length > 0) {
|
||||
return {
|
||||
strategy: "not_compilable",
|
||||
target: "sql",
|
||||
residualPolicies,
|
||||
warnings: ["No allow policy could be compiled to SQL."]
|
||||
};
|
||||
}
|
||||
let sql = allowSql.length > 0 ? `(${allowSql.join(" OR ")})` : "FALSE";
|
||||
if (denySql.length > 0)
|
||||
sql = `(${sql}) AND NOT (${denySql.join(" OR ")})`;
|
||||
if (residualPolicies.length > 0) {
|
||||
warnings.push("Some policies were not compilable and require residual checks.");
|
||||
}
|
||||
return {
|
||||
strategy: residualPolicies.length > 0 ? "partial" : "compiled",
|
||||
target: "sql",
|
||||
predicate: { sql, params },
|
||||
residualPolicies: residualPolicies.length ? residualPolicies : undefined,
|
||||
warnings: warnings.length ? warnings : undefined
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=sql.js.map
|
||||
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@
|
||||
import type { CreatePermissionsOptions, PermissionsRuntime } from "./types.js";
|
||||
export declare function createPermissions(options: CreatePermissionsOptions): PermissionsRuntime;
|
||||
//# sourceMappingURL=createPermissions.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"createPermissions.d.ts","sourceRoot":"","sources":["../src/createPermissions.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAEV,wBAAwB,EAExB,kBAAkB,EAGnB,MAAM,YAAY,CAAC;AAgBpB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,kBAAkB,CA8IvF"}
|
||||
@ -1,145 +0,0 @@
|
||||
import { PermissionDeniedError, actionResource, resourceKey } from "@perm/core";
|
||||
import { DefaultEvaluator } from "./evaluator.js";
|
||||
import { combineEvaluatedPolicies, toTrace } from "./combiner.js";
|
||||
import { actionsForResource, targetMatches } from "./match.js";
|
||||
import { reversePolicies } from "./reverse.js";
|
||||
function dedupe(values) {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
function buildContext(input) {
|
||||
return {
|
||||
actor: input.actor,
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
context: input.context,
|
||||
requestId: input.requestId
|
||||
};
|
||||
}
|
||||
export function createPermissions(options) {
|
||||
const evaluator = new DefaultEvaluator(options.providers);
|
||||
const defaultFallback = options.defaultFallback ?? "deny";
|
||||
async function evaluatePolicies(context) {
|
||||
const ordered = [...options.policies].sort((a, b) => b.priority - a.priority);
|
||||
const entries = [];
|
||||
for (const policy of ordered) {
|
||||
const matched = targetMatches(policy, context);
|
||||
if (!matched) {
|
||||
entries.push({ policy, targetMatched: false });
|
||||
continue;
|
||||
}
|
||||
const evaluation = await evaluator.evaluate(policy.condition, context);
|
||||
entries.push({ policy, targetMatched: true, evaluation });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
async function explain(input) {
|
||||
const context = buildContext(input);
|
||||
const evaluated = await evaluatePolicies(context);
|
||||
const decision = combineEvaluatedPolicies(evaluated, defaultFallback);
|
||||
const trace = toTrace(evaluated);
|
||||
const dependencies = dedupe(trace.flatMap((entry) => entry.dependencies));
|
||||
return {
|
||||
actor: input.actor,
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
decision,
|
||||
trace,
|
||||
dependencies
|
||||
};
|
||||
}
|
||||
async function check(input) {
|
||||
return (await explain(input)).decision;
|
||||
}
|
||||
async function can(input) {
|
||||
const decision = await check(input);
|
||||
return decision.effect === "allow";
|
||||
}
|
||||
async function assertAllowed(input) {
|
||||
const decision = await check(input);
|
||||
if (decision.effect !== "allow")
|
||||
throw new PermissionDeniedError(decision);
|
||||
}
|
||||
async function what(input) {
|
||||
const resourceType = input.resource?.type;
|
||||
const actions = input.actions ?? (resourceType && options.schema.resources[resourceType]
|
||||
? actionsForResource(resourceType, options.schema.resources[resourceType].actions)
|
||||
: dedupe(options.policies.map((policy) => policy.target.action).filter((action) => !action.endsWith(".*"))));
|
||||
const decisions = {};
|
||||
for (const action of actions) {
|
||||
decisions[action] = await check({ ...input, action });
|
||||
}
|
||||
return { resource: input.resource, actions: decisions };
|
||||
}
|
||||
async function who(input) {
|
||||
const context = buildContext(input);
|
||||
const matchingPolicies = options.policies.filter((policy) => targetMatches(policy, context));
|
||||
return reversePolicies({
|
||||
policies: matchingPolicies,
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
subjectType: input.subjectType ?? "user"
|
||||
});
|
||||
}
|
||||
function filter(action) {
|
||||
let actor;
|
||||
let resourceType = actionResource(action) ?? "resource";
|
||||
let filterContext;
|
||||
const builder = {
|
||||
for(nextActor) {
|
||||
actor = nextActor;
|
||||
return builder;
|
||||
},
|
||||
resource(nextResourceType) {
|
||||
resourceType = nextResourceType;
|
||||
return builder;
|
||||
},
|
||||
context(nextContext) {
|
||||
filterContext = nextContext;
|
||||
return builder;
|
||||
},
|
||||
async toPlan(target = "memory") {
|
||||
if (!actor)
|
||||
throw new Error("filter().for(actor) is required before toPlan()");
|
||||
if (target === "memory") {
|
||||
return {
|
||||
strategy: "compiled",
|
||||
target: "memory",
|
||||
predicate: "async (resource) => Perm.can({ actor, action, resource })"
|
||||
};
|
||||
}
|
||||
const compiler = options.compilers?.find((candidate) => candidate.target === target);
|
||||
if (!compiler) {
|
||||
return {
|
||||
strategy: "not_compilable",
|
||||
target,
|
||||
warnings: [`No ${target} compiler configured`]
|
||||
};
|
||||
}
|
||||
return compiler.compile({
|
||||
schema: options.schema,
|
||||
policies: options.policies,
|
||||
action,
|
||||
actor,
|
||||
resourceType,
|
||||
context: filterContext
|
||||
});
|
||||
},
|
||||
async toPredicate() {
|
||||
if (!actor)
|
||||
throw new Error("filter().for(actor) is required before toPredicate()");
|
||||
return async (resource) => can({ actor: actor, action, resource, context: filterContext });
|
||||
}
|
||||
};
|
||||
return builder;
|
||||
}
|
||||
return {
|
||||
check,
|
||||
can,
|
||||
assert: assertAllowed,
|
||||
explain,
|
||||
what,
|
||||
who,
|
||||
filter
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=createPermissions.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"createPermissions.js","sourceRoot":"","sources":["../src/createPermissions.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,11 +0,0 @@
|
||||
import type { EvalResult, ExprIR, RequestContext } from "@perm/core";
|
||||
import type { PermissionProviders } from "./types.js";
|
||||
export declare class DefaultEvaluator {
|
||||
private readonly providers;
|
||||
constructor(providers?: PermissionProviders);
|
||||
evaluate(expr: ExprIR, context: RequestContext): Promise<EvalResult>;
|
||||
private value;
|
||||
private rawValue;
|
||||
private evaluateInternal;
|
||||
}
|
||||
//# sourceMappingURL=evaluator.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../src/evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,UAAU,EAAa,MAAM,EAAE,cAAc,EAA2B,MAAM,YAAY,CAAC;AAExH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoCtD,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,GAAE,mBAAwB;IAE1D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC;YAQ5D,KAAK;YAQL,QAAQ;YA6BR,gBAAgB;CA2G/B"}
|
||||
@ -1,189 +0,0 @@
|
||||
import { getPath } from "@perm/core";
|
||||
function ok(value, dependencies = [], reason, error) {
|
||||
const result = { value, dependencies };
|
||||
if (reason !== undefined)
|
||||
result.reason = reason;
|
||||
if (error !== undefined)
|
||||
result.error = error;
|
||||
return result;
|
||||
}
|
||||
function dep(kind, key) {
|
||||
return `${kind}:${key}`;
|
||||
}
|
||||
function dedupeDeps(deps) {
|
||||
return [...new Set(deps)];
|
||||
}
|
||||
function compare(op, left, right) {
|
||||
switch (op) {
|
||||
case "eq":
|
||||
return left === right;
|
||||
case "neq":
|
||||
return left !== right;
|
||||
case "gt":
|
||||
return typeof left === "number" && typeof right === "number" ? left > right : "unknown";
|
||||
case "gte":
|
||||
return typeof left === "number" && typeof right === "number" ? left >= right : "unknown";
|
||||
case "lt":
|
||||
return typeof left === "number" && typeof right === "number" ? left < right : "unknown";
|
||||
case "lte":
|
||||
return typeof left === "number" && typeof right === "number" ? left <= right : "unknown";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
export class DefaultEvaluator {
|
||||
providers;
|
||||
constructor(providers = {}) {
|
||||
this.providers = providers;
|
||||
}
|
||||
async evaluate(expr, context) {
|
||||
try {
|
||||
return await this.evaluateInternal(expr, context);
|
||||
}
|
||||
catch (error) {
|
||||
return ok("error", [], "Expression evaluation failed", error);
|
||||
}
|
||||
}
|
||||
async value(expr, context) {
|
||||
const result = await this.evaluateInternal(expr, context);
|
||||
if (result.value === "unknown" || result.value === "error") {
|
||||
return { value: undefined, dependencies: result.dependencies, unknown: result.reason ?? String(result.value) };
|
||||
}
|
||||
return { value: result.value, dependencies: result.dependencies };
|
||||
}
|
||||
async rawValue(expr, context) {
|
||||
if (expr.op === "const")
|
||||
return { value: expr.value, dependencies: [] };
|
||||
if (expr.op === "ref") {
|
||||
const key = expr.path ? `${expr.root}.${expr.path}` : expr.root;
|
||||
const dependency = dep(expr.root, key);
|
||||
if (this.providers.attributes) {
|
||||
const provided = await this.providers.attributes.getAttribute({ root: expr.root, path: expr.path, context });
|
||||
return { value: provided, dependencies: [dependency] };
|
||||
}
|
||||
const root = expr.root === "actor" ? context.actor : expr.root === "resource" ? context.resource : context.context;
|
||||
return { value: getPath(root, expr.path), dependencies: [dependency] };
|
||||
}
|
||||
if (expr.op === "rel") {
|
||||
const evaluated = await this.evaluateInternal(expr, context);
|
||||
const out = { value: evaluated.value === true, dependencies: evaluated.dependencies };
|
||||
if (evaluated.value === "unknown" && evaluated.reason !== undefined)
|
||||
out.unknown = evaluated.reason;
|
||||
return out;
|
||||
}
|
||||
const evaluated = await this.evaluateInternal(expr, context);
|
||||
if (evaluated.value === "unknown" || evaluated.value === "error") {
|
||||
return { value: undefined, dependencies: evaluated.dependencies, unknown: evaluated.reason ?? String(evaluated.value) };
|
||||
}
|
||||
return { value: evaluated.value, dependencies: evaluated.dependencies };
|
||||
}
|
||||
async evaluateInternal(expr, context) {
|
||||
switch (expr.op) {
|
||||
case "const":
|
||||
return ok(Boolean(expr.value));
|
||||
case "ref": {
|
||||
const { value, dependencies } = await this.rawValue(expr, context);
|
||||
return ok(Boolean(value), dependencies);
|
||||
}
|
||||
case "eq":
|
||||
case "neq":
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte": {
|
||||
const left = await this.rawValue(expr.left, context);
|
||||
const right = await this.rawValue(expr.right, context);
|
||||
const dependencies = dedupeDeps([...left.dependencies, ...right.dependencies]);
|
||||
if (left.unknown || right.unknown)
|
||||
return ok("unknown", dependencies, left.unknown ?? right.unknown);
|
||||
const value = compare(expr.op, left.value, right.value);
|
||||
return value === "unknown" ? ok("unknown", dependencies, `Cannot compare values using ${expr.op}`) : ok(value, dependencies);
|
||||
}
|
||||
case "in": {
|
||||
const value = await this.rawValue(expr.value, context);
|
||||
const set = await this.rawValue(expr.set, context);
|
||||
const dependencies = dedupeDeps([...value.dependencies, ...set.dependencies]);
|
||||
if (value.unknown || set.unknown)
|
||||
return ok("unknown", dependencies, value.unknown ?? set.unknown);
|
||||
if (!Array.isArray(set.value))
|
||||
return ok("unknown", dependencies, "Right side of in() is not an array");
|
||||
return ok(set.value.includes(value.value), dependencies);
|
||||
}
|
||||
case "contains": {
|
||||
const set = await this.rawValue(expr.set, context);
|
||||
const value = await this.rawValue(expr.value, context);
|
||||
const dependencies = dedupeDeps([...value.dependencies, ...set.dependencies]);
|
||||
if (value.unknown || set.unknown)
|
||||
return ok("unknown", dependencies, value.unknown ?? set.unknown);
|
||||
if (!Array.isArray(set.value))
|
||||
return ok("unknown", dependencies, "contains() target is not an array");
|
||||
return ok(set.value.includes(value.value), dependencies);
|
||||
}
|
||||
case "and": {
|
||||
const dependencies = [];
|
||||
let unknownReason;
|
||||
for (const arg of expr.args) {
|
||||
const result = await this.evaluateInternal(arg, context);
|
||||
dependencies.push(...result.dependencies);
|
||||
if (result.value === false)
|
||||
return ok(false, dedupeDeps(dependencies));
|
||||
if (result.value === "error")
|
||||
return ok("error", dedupeDeps(dependencies), result.reason, result.error);
|
||||
if (result.value === "unknown")
|
||||
unknownReason = unknownReason ?? result.reason;
|
||||
}
|
||||
return unknownReason ? ok("unknown", dedupeDeps(dependencies), unknownReason) : ok(true, dedupeDeps(dependencies));
|
||||
}
|
||||
case "or": {
|
||||
const dependencies = [];
|
||||
let unknownReason;
|
||||
for (const arg of expr.args) {
|
||||
const result = await this.evaluateInternal(arg, context);
|
||||
dependencies.push(...result.dependencies);
|
||||
if (result.value === true)
|
||||
return ok(true, dedupeDeps(dependencies));
|
||||
if (result.value === "error")
|
||||
return ok("error", dedupeDeps(dependencies), result.reason, result.error);
|
||||
if (result.value === "unknown")
|
||||
unknownReason = unknownReason ?? result.reason;
|
||||
}
|
||||
return unknownReason ? ok("unknown", dedupeDeps(dependencies), unknownReason) : ok(false, dedupeDeps(dependencies));
|
||||
}
|
||||
case "not": {
|
||||
const result = await this.evaluateInternal(expr.expr, context);
|
||||
if (result.value === true)
|
||||
return ok(false, result.dependencies);
|
||||
if (result.value === false)
|
||||
return ok(true, result.dependencies);
|
||||
return result;
|
||||
}
|
||||
case "rel": {
|
||||
const relationKey = dep("relation", expr.path);
|
||||
const resourceValue = expr.resource ? await this.rawValue(expr.resource, context) : { value: context.resource, dependencies: [] };
|
||||
const subjectValue = await this.rawValue(expr.subject, context);
|
||||
const dependencies = dedupeDeps([relationKey, ...resourceValue.dependencies, ...subjectValue.dependencies]);
|
||||
if (resourceValue.unknown || subjectValue.unknown) {
|
||||
return ok("unknown", dependencies, resourceValue.unknown ?? subjectValue.unknown);
|
||||
}
|
||||
const resource = resourceValue.value;
|
||||
const subject = subjectValue.value;
|
||||
if (!resource || typeof resource !== "object" || !resource.type) {
|
||||
return ok("unknown", dependencies, `Relation ${expr.path} requires a resource`);
|
||||
}
|
||||
if (!subject || typeof subject !== "object" || !subject.type || !subject.id) {
|
||||
return ok("unknown", dependencies, `Relation ${expr.path} requires a subject`);
|
||||
}
|
||||
if (!this.providers.relations) {
|
||||
return ok("unknown", dependencies, `No relation provider configured for ${expr.path}`);
|
||||
}
|
||||
const relationResult = await this.providers.relations.hasRelation({ relation: expr.path, resource, subject, context });
|
||||
if (relationResult === "unknown")
|
||||
return ok("unknown", dependencies, `Relation ${expr.path} is unknown`);
|
||||
return ok(relationResult, dependencies);
|
||||
}
|
||||
case "exists":
|
||||
return ok("unknown", [dep("relation", expr.relation)], "exists() requires a query-capable provider");
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=evaluator.js.map
|
||||
File diff suppressed because one or more lines are too long
@ -1,18 +0,0 @@
|
||||
import type { PermissionsRuntime, CheckInput } from "./types.js";
|
||||
export interface AuthzHttpRequestLike {
|
||||
method: string;
|
||||
url?: string;
|
||||
json(): Promise<unknown>;
|
||||
}
|
||||
export interface AuthzHttpResponse {
|
||||
status: number;
|
||||
body: unknown;
|
||||
}
|
||||
export type ActorResolver = (request: AuthzHttpRequestLike, body: unknown) => Promise<CheckInput["actor"]> | CheckInput["actor"];
|
||||
export declare function createAuthzHttpHandlers(runtime: PermissionsRuntime, resolveActor: ActorResolver): {
|
||||
check(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse>;
|
||||
batch(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse>;
|
||||
what(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse>;
|
||||
explain(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse>;
|
||||
};
|
||||
//# sourceMappingURL=http.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;AAMjI,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa;mBAEvE,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC;mBAajD,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC;kBAiBlD,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC;qBAa9C,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC;EAa3E"}
|
||||
@ -1,59 +0,0 @@
|
||||
function assertBodyObject(body) {
|
||||
if (!body || typeof body !== "object")
|
||||
throw new Error("Request body must be an object");
|
||||
}
|
||||
export function createAuthzHttpHandlers(runtime, resolveActor) {
|
||||
return {
|
||||
async check(request) {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const decision = await runtime.check({
|
||||
actor,
|
||||
action: String(body.action),
|
||||
resource: body.resource,
|
||||
context: body.context
|
||||
});
|
||||
return { status: 200, body: decision };
|
||||
},
|
||||
async batch(request) {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const checks = Array.isArray(body.checks) ? body.checks : [];
|
||||
const decisions = {};
|
||||
for (const check of checks) {
|
||||
const resource = check.resource;
|
||||
const action = String(check.action);
|
||||
const key = `${resource?.type ?? "resource"}:${resource?.id ?? "none"}:${action}`;
|
||||
decisions[key] = await runtime.check({ actor, action, resource, context: check.context });
|
||||
}
|
||||
return { status: 200, body: { decisions } };
|
||||
},
|
||||
async what(request) {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const result = await runtime.what({
|
||||
actor,
|
||||
resource: body.resource,
|
||||
context: body.context,
|
||||
actions: Array.isArray(body.actions) ? body.actions.map(String) : undefined
|
||||
});
|
||||
return { status: 200, body: result };
|
||||
},
|
||||
async explain(request) {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const result = await runtime.explain({
|
||||
actor,
|
||||
action: String(body.action),
|
||||
resource: body.resource,
|
||||
context: body.context
|
||||
});
|
||||
return { status: 200, body: result };
|
||||
}
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=http.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,9 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./evaluator.js";
|
||||
export * from "./combiner.js";
|
||||
export * from "./match.js";
|
||||
export * from "./reverse.js";
|
||||
export * from "./createPermissions.js";
|
||||
export * from "./http.js";
|
||||
export * from "./compilers/sql.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC"}
|
||||
@ -1,9 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./evaluator.js";
|
||||
export * from "./combiner.js";
|
||||
export * from "./match.js";
|
||||
export * from "./reverse.js";
|
||||
export * from "./createPermissions.js";
|
||||
export * from "./http.js";
|
||||
export * from "./compilers/sql.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC"}
|
||||
@ -1,5 +0,0 @@
|
||||
import type { PolicyIR, RequestContext } from "@perm/core";
|
||||
export declare function actionMatches(pattern: string, action: string): boolean;
|
||||
export declare function targetMatches(policy: PolicyIR, context: RequestContext): boolean;
|
||||
export declare function actionsForResource(resourceType: string, actions: readonly string[]): string[];
|
||||
//# sourceMappingURL=match.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG3D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOtE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAMhF;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAE7F"}
|
||||
@ -1,23 +0,0 @@
|
||||
import { actionResource } from "@perm/core";
|
||||
export function actionMatches(pattern, action) {
|
||||
if (pattern === action)
|
||||
return true;
|
||||
if (pattern.endsWith(".*")) {
|
||||
const prefix = pattern.slice(0, -1);
|
||||
return action.startsWith(prefix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function targetMatches(policy, context) {
|
||||
if (!actionMatches(policy.target.action, context.action))
|
||||
return false;
|
||||
const policyResource = policy.target.resource ?? actionResource(policy.target.action);
|
||||
const requestResource = context.resource?.type ?? actionResource(context.action);
|
||||
if (!policyResource || !requestResource)
|
||||
return true;
|
||||
return policyResource === requestResource;
|
||||
}
|
||||
export function actionsForResource(resourceType, actions) {
|
||||
return actions.map((action) => action.includes(".") ? action : `${resourceType}.${action}`);
|
||||
}
|
||||
//# sourceMappingURL=match.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"match.js","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,MAAc;IAC3D,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACpC,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAgB,EAAE,OAAuB;IACrE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACvE,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtF,MAAM,eAAe,GAAG,OAAO,CAAC,QAAQ,EAAE,IAAI,IAAI,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACjF,IAAI,CAAC,cAAc,IAAI,CAAC,eAAe;QAAE,OAAO,IAAI,CAAC;IACrD,OAAO,cAAc,KAAK,eAAe,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,YAAoB,EAAE,OAA0B;IACjF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,MAAM,EAAE,CAAC,CAAC;AAC9F,CAAC"}
|
||||
@ -1,9 +0,0 @@
|
||||
import type { PolicyIR, ResourceRef } from "@perm/core";
|
||||
import type { ReverseQueryResult } from "./types.js";
|
||||
export declare function reversePolicies(input: {
|
||||
policies: PolicyIR[];
|
||||
action: string;
|
||||
resource: ResourceRef;
|
||||
subjectType: string;
|
||||
}): ReverseQueryResult;
|
||||
//# sourceMappingURL=reverse.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"reverse.d.ts","sourceRoot":"","sources":["../src/reverse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA8CrD,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,kBAAkB,CAoBrB"}
|
||||
@ -1,61 +0,0 @@
|
||||
function collect(expr, result, policy, resource) {
|
||||
switch (expr.op) {
|
||||
case "rel":
|
||||
result.sources.push({
|
||||
type: expr.path.includes(".") ? "relationExpansion" : "relation",
|
||||
path: expr.path,
|
||||
resource
|
||||
});
|
||||
return;
|
||||
case "and":
|
||||
case "or":
|
||||
for (const arg of expr.args)
|
||||
collect(arg, result, policy, resource);
|
||||
return;
|
||||
case "not":
|
||||
collect(expr.expr, result, policy, resource);
|
||||
return;
|
||||
case "eq":
|
||||
case "neq":
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte": {
|
||||
const left = expr.left;
|
||||
const right = expr.right;
|
||||
if (left.op === "ref" && left.root === "context" && right.op === "const") {
|
||||
result.constraints.push({ type: "context", path: left.path, required: right.value, policy: policy.id });
|
||||
}
|
||||
else if (left.op === "ref" && right.op === "const") {
|
||||
result.constraints.push({ type: "attribute", path: `${left.root}.${left.path}`, required: right.value, policy: policy.id });
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "in":
|
||||
case "contains":
|
||||
case "exists":
|
||||
case "const":
|
||||
case "ref":
|
||||
return;
|
||||
}
|
||||
}
|
||||
export function reversePolicies(input) {
|
||||
const result = {
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
subjectType: input.subjectType,
|
||||
sources: [],
|
||||
constraints: [],
|
||||
warnings: []
|
||||
};
|
||||
for (const policy of input.policies) {
|
||||
if (policy.effect !== "allow")
|
||||
continue;
|
||||
collect(policy.condition, result, policy, input.resource);
|
||||
}
|
||||
if (result.sources.length === 0) {
|
||||
result.warnings.push("No invertible relation was found. The policy may depend only on attributes or custom predicates.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
//# sourceMappingURL=reverse.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"reverse.js","sourceRoot":"","sources":["../src/reverse.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,112 +0,0 @@
|
||||
import type { DependencyKey, EvalResult, ExprIR, ExplainResult, PermSchema, PermissionDecision, PolicyIR, QueryPlan, RequestContext, ResourceRef, SubjectRef } from "@perm/core";
|
||||
export type RelationResult = boolean | "unknown";
|
||||
export interface RelationProvider {
|
||||
hasRelation(input: {
|
||||
relation: string;
|
||||
resource: ResourceRef;
|
||||
subject: SubjectRef;
|
||||
context: RequestContext;
|
||||
}): Promise<RelationResult> | RelationResult;
|
||||
listSubjects?(input: {
|
||||
relation: string;
|
||||
resource: ResourceRef;
|
||||
subjectType?: string;
|
||||
context: RequestContext;
|
||||
}): Promise<SubjectRef[]> | SubjectRef[];
|
||||
listResources?(input: {
|
||||
relation: string;
|
||||
subject: SubjectRef;
|
||||
resourceType: string;
|
||||
context: RequestContext;
|
||||
}): Promise<ResourceRef[]> | ResourceRef[];
|
||||
}
|
||||
export interface AttributeProvider {
|
||||
getAttribute(input: {
|
||||
root: "actor" | "resource" | "context";
|
||||
path: string;
|
||||
context: RequestContext;
|
||||
}): Promise<unknown> | unknown;
|
||||
}
|
||||
export interface SubscriptionProvider {
|
||||
subscribe(dependencies: DependencyKey[], cb: () => void): () => void;
|
||||
}
|
||||
export interface QueryCompiler {
|
||||
target: string;
|
||||
compile(input: {
|
||||
schema: PermSchema;
|
||||
policies: PolicyIR[];
|
||||
action: string;
|
||||
actor: SubjectRef;
|
||||
resourceType: string;
|
||||
context?: Record<string, unknown>;
|
||||
}): Promise<QueryPlan> | QueryPlan;
|
||||
}
|
||||
export interface PermissionProviders {
|
||||
attributes?: AttributeProvider;
|
||||
relations?: RelationProvider;
|
||||
subscriptions?: SubscriptionProvider;
|
||||
}
|
||||
export interface CreatePermissionsOptions {
|
||||
schema: PermSchema;
|
||||
policies: PolicyIR[];
|
||||
providers?: PermissionProviders;
|
||||
compilers?: QueryCompiler[];
|
||||
defaultFallback?: "deny" | "allow";
|
||||
}
|
||||
export interface EvaluateOptions {
|
||||
providers?: PermissionProviders;
|
||||
}
|
||||
export interface Evaluator {
|
||||
evaluate(expr: ExprIR, context: RequestContext): Promise<EvalResult>;
|
||||
}
|
||||
export interface CheckInput {
|
||||
actor: SubjectRef;
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
context?: Record<string, unknown>;
|
||||
requestId?: string;
|
||||
}
|
||||
export interface WhatResult {
|
||||
resource?: ResourceRef;
|
||||
actions: Record<string, PermissionDecision>;
|
||||
}
|
||||
export interface ReverseQueryResult {
|
||||
action: string;
|
||||
resource: ResourceRef;
|
||||
subjectType: string;
|
||||
sources: Array<{
|
||||
type: "relation" | "relationExpansion" | "policy";
|
||||
path?: string;
|
||||
policy?: string;
|
||||
resource?: ResourceRef;
|
||||
}>;
|
||||
constraints: Array<{
|
||||
type: "context" | "attribute" | "unknown";
|
||||
path?: string;
|
||||
required?: unknown;
|
||||
policy?: string;
|
||||
}>;
|
||||
warnings: string[];
|
||||
}
|
||||
export interface PermissionsRuntime {
|
||||
check(input: CheckInput): Promise<PermissionDecision>;
|
||||
can(input: CheckInput): Promise<boolean>;
|
||||
assert(input: CheckInput): Promise<void>;
|
||||
explain(input: CheckInput): Promise<ExplainResult>;
|
||||
what(input: Omit<CheckInput, "action"> & {
|
||||
actions?: string[];
|
||||
}): Promise<WhatResult>;
|
||||
who(input: CheckInput & {
|
||||
resource: ResourceRef;
|
||||
subjectType?: string;
|
||||
}): Promise<ReverseQueryResult>;
|
||||
filter(action: string): FilterBuilder;
|
||||
}
|
||||
export interface FilterBuilder {
|
||||
for(actor: SubjectRef): FilterBuilder;
|
||||
resource(resourceType: string): FilterBuilder;
|
||||
context(context: Record<string, unknown>): FilterBuilder;
|
||||
toPlan(target?: string): Promise<QueryPlan>;
|
||||
toPredicate(): Promise<(resource: ResourceRef) => Promise<boolean>>;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
||||
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "@perm/server",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"typecheck": "tsc -b --pretty"
|
||||
},
|
||||
"dependencies": {
|
||||
"@perm/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import type { EvalResult, PermissionDecision, PolicyIR, TraceEntry } from "@perm/core";
|
||||
|
||||
export interface EvaluatedPolicy {
|
||||
policy: PolicyIR;
|
||||
targetMatched: boolean;
|
||||
evaluation?: EvalResult;
|
||||
}
|
||||
|
||||
export function combineEvaluatedPolicies(evaluated: EvaluatedPolicy[], defaultFallback: "deny" | "allow" = "deny"): PermissionDecision {
|
||||
const applicable = evaluated.filter((entry) => entry.targetMatched && entry.evaluation?.value === true);
|
||||
const deny = applicable
|
||||
.filter((entry) => entry.policy.effect === "deny")
|
||||
.sort((a, b) => b.policy.priority - a.policy.priority)[0];
|
||||
|
||||
if (deny) {
|
||||
return {
|
||||
effect: "deny",
|
||||
policy: deny.policy.id,
|
||||
code: deny.policy.code ?? "permission_denied",
|
||||
reason: deny.policy.reason ?? `Denied by policy ${deny.policy.id}`,
|
||||
advice: deny.policy.advice
|
||||
};
|
||||
}
|
||||
|
||||
const allow = applicable
|
||||
.filter((entry) => entry.policy.effect === "allow")
|
||||
.sort((a, b) => b.policy.priority - a.policy.priority)[0];
|
||||
|
||||
if (allow) {
|
||||
return {
|
||||
effect: "allow",
|
||||
policy: allow.policy.id,
|
||||
reason: allow.policy.reason,
|
||||
obligations: allow.policy.obligations,
|
||||
advice: allow.policy.advice
|
||||
};
|
||||
}
|
||||
|
||||
const errored = evaluated.find((entry) => entry.targetMatched && entry.evaluation?.value === "error");
|
||||
if (errored?.evaluation) {
|
||||
return {
|
||||
effect: "indeterminate",
|
||||
reason: errored.evaluation.reason ?? "Authorization policy evaluation failed",
|
||||
fallback: defaultFallback,
|
||||
errors: errored.evaluation.error ? [errored.evaluation.error] : undefined
|
||||
};
|
||||
}
|
||||
|
||||
const unknown = evaluated.find((entry) => entry.targetMatched && entry.evaluation?.value === "unknown");
|
||||
if (unknown?.evaluation) {
|
||||
return {
|
||||
effect: "indeterminate",
|
||||
reason: unknown.evaluation.reason ?? "Authorization policy evaluation is unknown",
|
||||
fallback: defaultFallback
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
effect: "not_applicable",
|
||||
reason: "No matching policy allowed or denied the request"
|
||||
};
|
||||
}
|
||||
|
||||
export function toTrace(evaluated: EvaluatedPolicy[]): TraceEntry[] {
|
||||
return evaluated.map((entry) => ({
|
||||
policy: entry.policy.id,
|
||||
effect: entry.policy.effect,
|
||||
targetMatched: entry.targetMatched,
|
||||
condition: entry.targetMatched ? entry.policy.condition : undefined,
|
||||
result: entry.evaluation?.value,
|
||||
reason: entry.evaluation?.reason,
|
||||
dependencies: entry.evaluation?.dependencies ?? []
|
||||
}));
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
import type { ExprIR, PolicyIR, QueryPlan, SubjectRef } from "@perm/core";
|
||||
import type { QueryCompiler } from "../types.js";
|
||||
import { actionMatches } from "../match.js";
|
||||
|
||||
export interface SqlCompileResult {
|
||||
sql: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SqlRelationCompilerInput {
|
||||
relation: string;
|
||||
resourceAlias: string;
|
||||
actor: SubjectRef;
|
||||
param(value: unknown): string;
|
||||
}
|
||||
|
||||
export type SqlRelationCompiler = (input: SqlRelationCompilerInput) => string | undefined;
|
||||
|
||||
export interface CreateSqlCompilerOptions {
|
||||
resourceAlias?: string;
|
||||
relation?: SqlRelationCompiler;
|
||||
columnName?: (path: string) => string;
|
||||
}
|
||||
|
||||
function defaultColumnName(path: string): string {
|
||||
return path.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function relevantPolicies(policies: PolicyIR[], action: string, resourceType: string): PolicyIR[] {
|
||||
return policies.filter((policy) => {
|
||||
const resourceMatches = !policy.target.resource || policy.target.resource === resourceType;
|
||||
return resourceMatches && actionMatches(policy.target.action, action);
|
||||
});
|
||||
}
|
||||
|
||||
export function createSqlCompiler(options: CreateSqlCompilerOptions = {}): QueryCompiler {
|
||||
const alias = options.resourceAlias ?? "resource";
|
||||
const columnName = options.columnName ?? defaultColumnName;
|
||||
|
||||
return {
|
||||
target: "sql",
|
||||
compile(input): QueryPlan {
|
||||
const policies = relevantPolicies(input.policies, input.action, input.resourceType);
|
||||
const allowPolicies = policies.filter((policy) => policy.effect === "allow");
|
||||
const denyPolicies = policies.filter((policy) => policy.effect === "deny");
|
||||
const params: Record<string, unknown> = {};
|
||||
let counter = 0;
|
||||
const warnings: string[] = [];
|
||||
const residualPolicies: PolicyIR[] = [];
|
||||
|
||||
const param = (value: unknown): string => {
|
||||
const key = `p${++counter}`;
|
||||
params[key] = value;
|
||||
return `:${key}`;
|
||||
};
|
||||
|
||||
const compileExpr = (expr: ExprIR): string | undefined => {
|
||||
switch (expr.op) {
|
||||
case "const":
|
||||
if (typeof expr.value === "boolean") return expr.value ? "TRUE" : "FALSE";
|
||||
return param(expr.value);
|
||||
|
||||
case "ref":
|
||||
if (expr.root === "resource") return `${alias}.${columnName(expr.path)}`;
|
||||
if (expr.root === "actor") {
|
||||
const value = expr.path ? (input.actor as Record<string, unknown>)[expr.path] : input.actor;
|
||||
return param(value);
|
||||
}
|
||||
if (expr.root === "context") {
|
||||
const value = expr.path ? input.context?.[expr.path] : input.context;
|
||||
return param(value);
|
||||
}
|
||||
return undefined;
|
||||
|
||||
case "eq":
|
||||
case "neq":
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte": {
|
||||
const left = compileExpr(expr.left);
|
||||
const right = compileExpr(expr.right);
|
||||
if (!left || !right) return undefined;
|
||||
const op = expr.op === "eq" ? "=" : expr.op === "neq" ? "<>" : expr.op === "gt" ? ">" : expr.op === "gte" ? ">=" : expr.op === "lt" ? "<" : "<=";
|
||||
return `(${left} ${op} ${right})`;
|
||||
}
|
||||
|
||||
case "and": {
|
||||
const parts = expr.args.map(compileExpr);
|
||||
if (parts.some((part) => !part)) return undefined;
|
||||
return `(${parts.join(" AND ")})`;
|
||||
}
|
||||
|
||||
case "or": {
|
||||
const parts = expr.args.map(compileExpr);
|
||||
if (parts.some((part) => !part)) return undefined;
|
||||
return `(${parts.join(" OR ")})`;
|
||||
}
|
||||
|
||||
case "not": {
|
||||
const inner = compileExpr(expr.expr);
|
||||
return inner ? `(NOT ${inner})` : undefined;
|
||||
}
|
||||
|
||||
case "in": {
|
||||
const value = compileExpr(expr.value);
|
||||
if (!value) return undefined;
|
||||
if (expr.set.op === "const" && Array.isArray(expr.set.value)) {
|
||||
const list = expr.set.value.map(param).join(", ");
|
||||
return `(${value} IN (${list}))`;
|
||||
}
|
||||
const set = compileExpr(expr.set);
|
||||
return set ? `(${value} IN ${set})` : undefined;
|
||||
}
|
||||
|
||||
case "contains": {
|
||||
const value = compileExpr(expr.value);
|
||||
if (!value) return undefined;
|
||||
if (expr.set.op === "const" && Array.isArray(expr.set.value)) {
|
||||
const list = expr.set.value.map(param).join(", ");
|
||||
return `(${value} IN (${list}))`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
case "rel": {
|
||||
const sql = options.relation?.({ relation: expr.path, resourceAlias: alias, actor: input.actor, param });
|
||||
if (!sql) return undefined;
|
||||
return `(${sql})`;
|
||||
}
|
||||
|
||||
case "exists":
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const allowSql: string[] = [];
|
||||
for (const policy of allowPolicies) {
|
||||
const compiled = compileExpr(policy.condition);
|
||||
if (compiled) allowSql.push(compiled);
|
||||
else residualPolicies.push(policy);
|
||||
}
|
||||
|
||||
const denySql: string[] = [];
|
||||
for (const policy of denyPolicies) {
|
||||
const compiled = compileExpr(policy.condition);
|
||||
if (compiled) denySql.push(compiled);
|
||||
else residualPolicies.push(policy);
|
||||
}
|
||||
|
||||
if (allowSql.length === 0 && residualPolicies.length > 0) {
|
||||
return {
|
||||
strategy: "not_compilable",
|
||||
target: "sql",
|
||||
residualPolicies,
|
||||
warnings: ["No allow policy could be compiled to SQL."]
|
||||
};
|
||||
}
|
||||
|
||||
let sql = allowSql.length > 0 ? `(${allowSql.join(" OR ")})` : "FALSE";
|
||||
if (denySql.length > 0) sql = `(${sql}) AND NOT (${denySql.join(" OR ")})`;
|
||||
|
||||
if (residualPolicies.length > 0) {
|
||||
warnings.push("Some policies were not compilable and require residual checks.");
|
||||
}
|
||||
|
||||
return {
|
||||
strategy: residualPolicies.length > 0 ? "partial" : "compiled",
|
||||
target: "sql",
|
||||
predicate: { sql, params } satisfies SqlCompileResult,
|
||||
residualPolicies: residualPolicies.length ? residualPolicies : undefined,
|
||||
warnings: warnings.length ? warnings : undefined
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
import type {
|
||||
DependencyKey,
|
||||
ExplainResult,
|
||||
PermissionDecision,
|
||||
PolicyIR,
|
||||
QueryPlan,
|
||||
RequestContext,
|
||||
ResourceRef,
|
||||
SubjectRef
|
||||
} from "@perm/core";
|
||||
import { PermissionDeniedError, actionResource, resourceKey } from "@perm/core";
|
||||
import { DefaultEvaluator } from "./evaluator.js";
|
||||
import { combineEvaluatedPolicies, toTrace, type EvaluatedPolicy } from "./combiner.js";
|
||||
import { actionsForResource, targetMatches } from "./match.js";
|
||||
import { reversePolicies } from "./reverse.js";
|
||||
import type {
|
||||
CheckInput,
|
||||
CreatePermissionsOptions,
|
||||
FilterBuilder,
|
||||
PermissionsRuntime,
|
||||
ReverseQueryResult,
|
||||
WhatResult
|
||||
} from "./types.js";
|
||||
|
||||
function dedupe<T>(values: T[]): T[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function buildContext(input: CheckInput): RequestContext {
|
||||
return {
|
||||
actor: input.actor,
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
context: input.context,
|
||||
requestId: input.requestId
|
||||
};
|
||||
}
|
||||
|
||||
export function createPermissions(options: CreatePermissionsOptions): PermissionsRuntime {
|
||||
const evaluator = new DefaultEvaluator(options.providers);
|
||||
const defaultFallback = options.defaultFallback ?? "deny";
|
||||
|
||||
async function evaluatePolicies(context: RequestContext): Promise<EvaluatedPolicy[]> {
|
||||
const ordered = [...options.policies].sort((a, b) => b.priority - a.priority);
|
||||
const entries: EvaluatedPolicy[] = [];
|
||||
|
||||
for (const policy of ordered) {
|
||||
const matched = targetMatches(policy, context);
|
||||
if (!matched) {
|
||||
entries.push({ policy, targetMatched: false });
|
||||
continue;
|
||||
}
|
||||
const evaluation = await evaluator.evaluate(policy.condition, context);
|
||||
entries.push({ policy, targetMatched: true, evaluation });
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function explain(input: CheckInput): Promise<ExplainResult> {
|
||||
const context = buildContext(input);
|
||||
const evaluated = await evaluatePolicies(context);
|
||||
const decision = combineEvaluatedPolicies(evaluated, defaultFallback);
|
||||
const trace = toTrace(evaluated);
|
||||
const dependencies = dedupe(trace.flatMap((entry) => entry.dependencies));
|
||||
|
||||
return {
|
||||
actor: input.actor,
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
decision,
|
||||
trace,
|
||||
dependencies
|
||||
};
|
||||
}
|
||||
|
||||
async function check(input: CheckInput): Promise<PermissionDecision> {
|
||||
return (await explain(input)).decision;
|
||||
}
|
||||
|
||||
async function can(input: CheckInput): Promise<boolean> {
|
||||
const decision = await check(input);
|
||||
return decision.effect === "allow";
|
||||
}
|
||||
|
||||
async function assertAllowed(input: CheckInput): Promise<void> {
|
||||
const decision = await check(input);
|
||||
if (decision.effect !== "allow") throw new PermissionDeniedError(decision);
|
||||
}
|
||||
|
||||
async function what(input: Omit<CheckInput, "action"> & { actions?: string[] }): Promise<WhatResult> {
|
||||
const resourceType = input.resource?.type;
|
||||
const actions = input.actions ?? (resourceType && options.schema.resources[resourceType]
|
||||
? actionsForResource(resourceType, options.schema.resources[resourceType]!.actions)
|
||||
: dedupe(options.policies.map((policy) => policy.target.action).filter((action) => !action.endsWith(".*"))));
|
||||
|
||||
const decisions: Record<string, PermissionDecision> = {};
|
||||
for (const action of actions) {
|
||||
decisions[action] = await check({ ...input, action });
|
||||
}
|
||||
return { resource: input.resource, actions: decisions };
|
||||
}
|
||||
|
||||
async function who(input: CheckInput & { resource: ResourceRef; subjectType?: string }): Promise<ReverseQueryResult> {
|
||||
const context = buildContext(input);
|
||||
const matchingPolicies = options.policies.filter((policy) => targetMatches(policy, context));
|
||||
return reversePolicies({
|
||||
policies: matchingPolicies,
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
subjectType: input.subjectType ?? "user"
|
||||
});
|
||||
}
|
||||
|
||||
function filter(action: string): FilterBuilder {
|
||||
let actor: SubjectRef | undefined;
|
||||
let resourceType = actionResource(action) ?? "resource";
|
||||
let filterContext: Record<string, unknown> | undefined;
|
||||
|
||||
const builder: FilterBuilder = {
|
||||
for(nextActor: SubjectRef) {
|
||||
actor = nextActor;
|
||||
return builder;
|
||||
},
|
||||
|
||||
resource(nextResourceType: string) {
|
||||
resourceType = nextResourceType;
|
||||
return builder;
|
||||
},
|
||||
|
||||
context(nextContext: Record<string, unknown>) {
|
||||
filterContext = nextContext;
|
||||
return builder;
|
||||
},
|
||||
|
||||
async toPlan(target = "memory"): Promise<QueryPlan> {
|
||||
if (!actor) throw new Error("filter().for(actor) is required before toPlan()");
|
||||
if (target === "memory") {
|
||||
return {
|
||||
strategy: "compiled",
|
||||
target: "memory",
|
||||
predicate: "async (resource) => Perm.can({ actor, action, resource })"
|
||||
};
|
||||
}
|
||||
const compiler = options.compilers?.find((candidate) => candidate.target === target);
|
||||
if (!compiler) {
|
||||
return {
|
||||
strategy: "not_compilable",
|
||||
target,
|
||||
warnings: [`No ${target} compiler configured`]
|
||||
};
|
||||
}
|
||||
return compiler.compile({
|
||||
schema: options.schema,
|
||||
policies: options.policies,
|
||||
action,
|
||||
actor,
|
||||
resourceType,
|
||||
context: filterContext
|
||||
});
|
||||
},
|
||||
|
||||
async toPredicate(): Promise<(resource: ResourceRef) => Promise<boolean>> {
|
||||
if (!actor) throw new Error("filter().for(actor) is required before toPredicate()");
|
||||
return async (resource: ResourceRef) => can({ actor: actor!, action, resource, context: filterContext });
|
||||
}
|
||||
};
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
return {
|
||||
check,
|
||||
can,
|
||||
assert: assertAllowed,
|
||||
explain,
|
||||
what,
|
||||
who,
|
||||
filter
|
||||
};
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
import type { DependencyKey, EvalResult, EvalValue, ExprIR, RequestContext, ResourceRef, SubjectRef } from "@perm/core";
|
||||
import { getPath } from "@perm/core";
|
||||
import type { PermissionProviders } from "./types.js";
|
||||
|
||||
function ok(value: EvalValue, dependencies: DependencyKey[] = [], reason?: string, error?: unknown): EvalResult {
|
||||
const result: EvalResult = { value, dependencies };
|
||||
if (reason !== undefined) result.reason = reason;
|
||||
if (error !== undefined) result.error = error;
|
||||
return result;
|
||||
}
|
||||
|
||||
function dep(kind: "actor" | "resource" | "context" | "relation", key: string): DependencyKey {
|
||||
return `${kind}:${key}`;
|
||||
}
|
||||
|
||||
function dedupeDeps(deps: DependencyKey[]): DependencyKey[] {
|
||||
return [...new Set(deps)];
|
||||
}
|
||||
|
||||
function compare(op: ExprIR extends { op: infer O } ? O : never, left: unknown, right: unknown): boolean | "unknown" {
|
||||
switch (op) {
|
||||
case "eq":
|
||||
return left === right;
|
||||
case "neq":
|
||||
return left !== right;
|
||||
case "gt":
|
||||
return typeof left === "number" && typeof right === "number" ? left > right : "unknown";
|
||||
case "gte":
|
||||
return typeof left === "number" && typeof right === "number" ? left >= right : "unknown";
|
||||
case "lt":
|
||||
return typeof left === "number" && typeof right === "number" ? left < right : "unknown";
|
||||
case "lte":
|
||||
return typeof left === "number" && typeof right === "number" ? left <= right : "unknown";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultEvaluator {
|
||||
constructor(private readonly providers: PermissionProviders = {}) {}
|
||||
|
||||
async evaluate(expr: ExprIR, context: RequestContext): Promise<EvalResult> {
|
||||
try {
|
||||
return await this.evaluateInternal(expr, context);
|
||||
} catch (error) {
|
||||
return ok("error", [], "Expression evaluation failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async value(expr: ExprIR, context: RequestContext): Promise<{ value: unknown; dependencies: DependencyKey[]; unknown?: string }> {
|
||||
const result = await this.evaluateInternal(expr, context);
|
||||
if (result.value === "unknown" || result.value === "error") {
|
||||
return { value: undefined, dependencies: result.dependencies, unknown: result.reason ?? String(result.value) };
|
||||
}
|
||||
return { value: result.value, dependencies: result.dependencies };
|
||||
}
|
||||
|
||||
private async rawValue(expr: ExprIR, context: RequestContext): Promise<{ value: unknown; dependencies: DependencyKey[]; unknown?: string }> {
|
||||
if (expr.op === "const") return { value: expr.value, dependencies: [] };
|
||||
|
||||
if (expr.op === "ref") {
|
||||
const key = expr.path ? `${expr.root}.${expr.path}` : expr.root;
|
||||
const dependency = dep(expr.root, key);
|
||||
if (this.providers.attributes) {
|
||||
const provided = await this.providers.attributes.getAttribute({ root: expr.root, path: expr.path, context });
|
||||
return { value: provided, dependencies: [dependency] };
|
||||
}
|
||||
|
||||
const root = expr.root === "actor" ? context.actor : expr.root === "resource" ? context.resource : context.context;
|
||||
return { value: getPath(root, expr.path), dependencies: [dependency] };
|
||||
}
|
||||
|
||||
if (expr.op === "rel") {
|
||||
const evaluated = await this.evaluateInternal(expr, context);
|
||||
const out: { value: unknown; dependencies: DependencyKey[]; unknown?: string } = { value: evaluated.value === true, dependencies: evaluated.dependencies };
|
||||
if (evaluated.value === "unknown" && evaluated.reason !== undefined) out.unknown = evaluated.reason;
|
||||
return out;
|
||||
}
|
||||
|
||||
const evaluated = await this.evaluateInternal(expr, context);
|
||||
if (evaluated.value === "unknown" || evaluated.value === "error") {
|
||||
return { value: undefined, dependencies: evaluated.dependencies, unknown: evaluated.reason ?? String(evaluated.value) };
|
||||
}
|
||||
return { value: evaluated.value, dependencies: evaluated.dependencies };
|
||||
}
|
||||
|
||||
private async evaluateInternal(expr: ExprIR, context: RequestContext): Promise<EvalResult> {
|
||||
switch (expr.op) {
|
||||
case "const":
|
||||
return ok(Boolean(expr.value));
|
||||
|
||||
case "ref": {
|
||||
const { value, dependencies } = await this.rawValue(expr, context);
|
||||
return ok(Boolean(value), dependencies);
|
||||
}
|
||||
|
||||
case "eq":
|
||||
case "neq":
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte": {
|
||||
const left = await this.rawValue(expr.left, context);
|
||||
const right = await this.rawValue(expr.right, context);
|
||||
const dependencies = dedupeDeps([...left.dependencies, ...right.dependencies]);
|
||||
if (left.unknown || right.unknown) return ok("unknown", dependencies, left.unknown ?? right.unknown);
|
||||
const value = compare(expr.op, left.value, right.value);
|
||||
return value === "unknown" ? ok("unknown", dependencies, `Cannot compare values using ${expr.op}`) : ok(value, dependencies);
|
||||
}
|
||||
|
||||
case "in": {
|
||||
const value = await this.rawValue(expr.value, context);
|
||||
const set = await this.rawValue(expr.set, context);
|
||||
const dependencies = dedupeDeps([...value.dependencies, ...set.dependencies]);
|
||||
if (value.unknown || set.unknown) return ok("unknown", dependencies, value.unknown ?? set.unknown);
|
||||
if (!Array.isArray(set.value)) return ok("unknown", dependencies, "Right side of in() is not an array");
|
||||
return ok(set.value.includes(value.value), dependencies);
|
||||
}
|
||||
|
||||
case "contains": {
|
||||
const set = await this.rawValue(expr.set, context);
|
||||
const value = await this.rawValue(expr.value, context);
|
||||
const dependencies = dedupeDeps([...value.dependencies, ...set.dependencies]);
|
||||
if (value.unknown || set.unknown) return ok("unknown", dependencies, value.unknown ?? set.unknown);
|
||||
if (!Array.isArray(set.value)) return ok("unknown", dependencies, "contains() target is not an array");
|
||||
return ok(set.value.includes(value.value), dependencies);
|
||||
}
|
||||
|
||||
case "and": {
|
||||
const dependencies: DependencyKey[] = [];
|
||||
let unknownReason: string | undefined;
|
||||
for (const arg of expr.args) {
|
||||
const result = await this.evaluateInternal(arg, context);
|
||||
dependencies.push(...result.dependencies);
|
||||
if (result.value === false) return ok(false, dedupeDeps(dependencies));
|
||||
if (result.value === "error") return ok("error", dedupeDeps(dependencies), result.reason, result.error);
|
||||
if (result.value === "unknown") unknownReason = unknownReason ?? result.reason;
|
||||
}
|
||||
return unknownReason ? ok("unknown", dedupeDeps(dependencies), unknownReason) : ok(true, dedupeDeps(dependencies));
|
||||
}
|
||||
|
||||
case "or": {
|
||||
const dependencies: DependencyKey[] = [];
|
||||
let unknownReason: string | undefined;
|
||||
for (const arg of expr.args) {
|
||||
const result = await this.evaluateInternal(arg, context);
|
||||
dependencies.push(...result.dependencies);
|
||||
if (result.value === true) return ok(true, dedupeDeps(dependencies));
|
||||
if (result.value === "error") return ok("error", dedupeDeps(dependencies), result.reason, result.error);
|
||||
if (result.value === "unknown") unknownReason = unknownReason ?? result.reason;
|
||||
}
|
||||
return unknownReason ? ok("unknown", dedupeDeps(dependencies), unknownReason) : ok(false, dedupeDeps(dependencies));
|
||||
}
|
||||
|
||||
case "not": {
|
||||
const result = await this.evaluateInternal(expr.expr, context);
|
||||
if (result.value === true) return ok(false, result.dependencies);
|
||||
if (result.value === false) return ok(true, result.dependencies);
|
||||
return result;
|
||||
}
|
||||
|
||||
case "rel": {
|
||||
const relationKey = dep("relation", expr.path);
|
||||
const resourceValue = expr.resource ? await this.rawValue(expr.resource, context) : { value: context.resource, dependencies: [] };
|
||||
const subjectValue = await this.rawValue(expr.subject, context);
|
||||
const dependencies = dedupeDeps([relationKey, ...resourceValue.dependencies, ...subjectValue.dependencies]);
|
||||
|
||||
if (resourceValue.unknown || subjectValue.unknown) {
|
||||
return ok("unknown", dependencies, resourceValue.unknown ?? subjectValue.unknown);
|
||||
}
|
||||
|
||||
const resource = resourceValue.value as ResourceRef | undefined;
|
||||
const subject = subjectValue.value as SubjectRef | undefined;
|
||||
|
||||
if (!resource || typeof resource !== "object" || !resource.type) {
|
||||
return ok("unknown", dependencies, `Relation ${expr.path} requires a resource`);
|
||||
}
|
||||
if (!subject || typeof subject !== "object" || !subject.type || !subject.id) {
|
||||
return ok("unknown", dependencies, `Relation ${expr.path} requires a subject`);
|
||||
}
|
||||
if (!this.providers.relations) {
|
||||
return ok("unknown", dependencies, `No relation provider configured for ${expr.path}`);
|
||||
}
|
||||
|
||||
const relationResult = await this.providers.relations.hasRelation({ relation: expr.path, resource, subject, context });
|
||||
if (relationResult === "unknown") return ok("unknown", dependencies, `Relation ${expr.path} is unknown`);
|
||||
return ok(relationResult, dependencies);
|
||||
}
|
||||
|
||||
case "exists":
|
||||
return ok("unknown", [dep("relation", expr.relation)], "exists() requires a query-capable provider");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
import type { PermissionsRuntime, CheckInput } from "./types.js";
|
||||
|
||||
export interface AuthzHttpRequestLike {
|
||||
method: string;
|
||||
url?: string;
|
||||
json(): Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface AuthzHttpResponse {
|
||||
status: number;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
export type ActorResolver = (request: AuthzHttpRequestLike, body: unknown) => Promise<CheckInput["actor"]> | CheckInput["actor"];
|
||||
|
||||
function assertBodyObject(body: unknown): asserts body is Record<string, unknown> {
|
||||
if (!body || typeof body !== "object") throw new Error("Request body must be an object");
|
||||
}
|
||||
|
||||
export function createAuthzHttpHandlers(runtime: PermissionsRuntime, resolveActor: ActorResolver) {
|
||||
return {
|
||||
async check(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse> {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const decision = await runtime.check({
|
||||
actor,
|
||||
action: String(body.action),
|
||||
resource: body.resource as CheckInput["resource"],
|
||||
context: body.context as CheckInput["context"]
|
||||
});
|
||||
return { status: 200, body: decision };
|
||||
},
|
||||
|
||||
async batch(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse> {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const checks = Array.isArray(body.checks) ? body.checks as Array<Record<string, unknown>> : [];
|
||||
const decisions: Record<string, unknown> = {};
|
||||
|
||||
for (const check of checks) {
|
||||
const resource = check.resource as CheckInput["resource"];
|
||||
const action = String(check.action);
|
||||
const key = `${resource?.type ?? "resource"}:${resource?.id ?? "none"}:${action}`;
|
||||
decisions[key] = await runtime.check({ actor, action, resource, context: check.context as CheckInput["context"] });
|
||||
}
|
||||
|
||||
return { status: 200, body: { decisions } };
|
||||
},
|
||||
|
||||
async what(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse> {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const result = await runtime.what({
|
||||
actor,
|
||||
resource: body.resource as CheckInput["resource"],
|
||||
context: body.context as CheckInput["context"],
|
||||
actions: Array.isArray(body.actions) ? body.actions.map(String) : undefined
|
||||
});
|
||||
return { status: 200, body: result };
|
||||
},
|
||||
|
||||
async explain(request: AuthzHttpRequestLike): Promise<AuthzHttpResponse> {
|
||||
const body = await request.json();
|
||||
assertBodyObject(body);
|
||||
const actor = await resolveActor(request, body);
|
||||
const result = await runtime.explain({
|
||||
actor,
|
||||
action: String(body.action),
|
||||
resource: body.resource as CheckInput["resource"],
|
||||
context: body.context as CheckInput["context"]
|
||||
});
|
||||
return { status: 200, body: result };
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
export * from "./types.js";
|
||||
export * from "./evaluator.js";
|
||||
export * from "./combiner.js";
|
||||
export * from "./match.js";
|
||||
export * from "./reverse.js";
|
||||
export * from "./createPermissions.js";
|
||||
export * from "./http.js";
|
||||
export * from "./compilers/sql.js";
|
||||
@ -1,23 +0,0 @@
|
||||
import type { PolicyIR, RequestContext } from "@perm/core";
|
||||
import { actionResource } from "@perm/core";
|
||||
|
||||
export function actionMatches(pattern: string, action: string): boolean {
|
||||
if (pattern === action) return true;
|
||||
if (pattern.endsWith(".*")) {
|
||||
const prefix = pattern.slice(0, -1);
|
||||
return action.startsWith(prefix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function targetMatches(policy: PolicyIR, context: RequestContext): boolean {
|
||||
if (!actionMatches(policy.target.action, context.action)) return false;
|
||||
const policyResource = policy.target.resource ?? actionResource(policy.target.action);
|
||||
const requestResource = context.resource?.type ?? actionResource(context.action);
|
||||
if (!policyResource || !requestResource) return true;
|
||||
return policyResource === requestResource;
|
||||
}
|
||||
|
||||
export function actionsForResource(resourceType: string, actions: readonly string[]): string[] {
|
||||
return actions.map((action) => action.includes(".") ? action : `${resourceType}.${action}`);
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
import type { ExprIR, PolicyIR, ResourceRef } from "@perm/core";
|
||||
import type { ReverseQueryResult } from "./types.js";
|
||||
|
||||
function collect(expr: ExprIR, result: ReverseQueryResult, policy: PolicyIR, resource: ResourceRef): void {
|
||||
switch (expr.op) {
|
||||
case "rel":
|
||||
result.sources.push({
|
||||
type: expr.path.includes(".") ? "relationExpansion" : "relation",
|
||||
path: expr.path,
|
||||
resource
|
||||
});
|
||||
return;
|
||||
|
||||
case "and":
|
||||
case "or":
|
||||
for (const arg of expr.args) collect(arg, result, policy, resource);
|
||||
return;
|
||||
|
||||
case "not":
|
||||
collect(expr.expr, result, policy, resource);
|
||||
return;
|
||||
|
||||
case "eq":
|
||||
case "neq":
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte": {
|
||||
const left = expr.left;
|
||||
const right = expr.right;
|
||||
if (left.op === "ref" && left.root === "context" && right.op === "const") {
|
||||
result.constraints.push({ type: "context", path: left.path, required: right.value, policy: policy.id });
|
||||
} else if (left.op === "ref" && right.op === "const") {
|
||||
result.constraints.push({ type: "attribute", path: `${left.root}.${left.path}`, required: right.value, policy: policy.id });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case "in":
|
||||
case "contains":
|
||||
case "exists":
|
||||
case "const":
|
||||
case "ref":
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function reversePolicies(input: {
|
||||
policies: PolicyIR[];
|
||||
action: string;
|
||||
resource: ResourceRef;
|
||||
subjectType: string;
|
||||
}): ReverseQueryResult {
|
||||
const result: ReverseQueryResult = {
|
||||
action: input.action,
|
||||
resource: input.resource,
|
||||
subjectType: input.subjectType,
|
||||
sources: [],
|
||||
constraints: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
for (const policy of input.policies) {
|
||||
if (policy.effect !== "allow") continue;
|
||||
collect(policy.condition, result, policy, input.resource);
|
||||
}
|
||||
|
||||
if (result.sources.length === 0) {
|
||||
result.warnings.push("No invertible relation was found. The policy may depend only on attributes or custom predicates.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
import type {
|
||||
DependencyKey,
|
||||
EvalResult,
|
||||
ExprIR,
|
||||
ExplainResult,
|
||||
PermSchema,
|
||||
PermissionDecision,
|
||||
PolicyIR,
|
||||
QueryPlan,
|
||||
RequestContext,
|
||||
ResourceRef,
|
||||
SubjectRef
|
||||
} from "@perm/core";
|
||||
|
||||
export type RelationResult = boolean | "unknown";
|
||||
|
||||
export interface RelationProvider {
|
||||
hasRelation(input: {
|
||||
relation: string;
|
||||
resource: ResourceRef;
|
||||
subject: SubjectRef;
|
||||
context: RequestContext;
|
||||
}): Promise<RelationResult> | RelationResult;
|
||||
|
||||
listSubjects?(input: {
|
||||
relation: string;
|
||||
resource: ResourceRef;
|
||||
subjectType?: string;
|
||||
context: RequestContext;
|
||||
}): Promise<SubjectRef[]> | SubjectRef[];
|
||||
|
||||
listResources?(input: {
|
||||
relation: string;
|
||||
subject: SubjectRef;
|
||||
resourceType: string;
|
||||
context: RequestContext;
|
||||
}): Promise<ResourceRef[]> | ResourceRef[];
|
||||
}
|
||||
|
||||
export interface AttributeProvider {
|
||||
getAttribute(input: {
|
||||
root: "actor" | "resource" | "context";
|
||||
path: string;
|
||||
context: RequestContext;
|
||||
}): Promise<unknown> | unknown;
|
||||
}
|
||||
|
||||
export interface SubscriptionProvider {
|
||||
subscribe(dependencies: DependencyKey[], cb: () => void): () => void;
|
||||
}
|
||||
|
||||
export interface QueryCompiler {
|
||||
target: string;
|
||||
compile(input: {
|
||||
schema: PermSchema;
|
||||
policies: PolicyIR[];
|
||||
action: string;
|
||||
actor: SubjectRef;
|
||||
resourceType: string;
|
||||
context?: Record<string, unknown>;
|
||||
}): Promise<QueryPlan> | QueryPlan;
|
||||
}
|
||||
|
||||
export interface PermissionProviders {
|
||||
attributes?: AttributeProvider;
|
||||
relations?: RelationProvider;
|
||||
subscriptions?: SubscriptionProvider;
|
||||
}
|
||||
|
||||
export interface CreatePermissionsOptions {
|
||||
schema: PermSchema;
|
||||
policies: PolicyIR[];
|
||||
providers?: PermissionProviders;
|
||||
compilers?: QueryCompiler[];
|
||||
defaultFallback?: "deny" | "allow";
|
||||
}
|
||||
|
||||
export interface EvaluateOptions {
|
||||
providers?: PermissionProviders;
|
||||
}
|
||||
|
||||
export interface Evaluator {
|
||||
evaluate(expr: ExprIR, context: RequestContext): Promise<EvalResult>;
|
||||
}
|
||||
|
||||
export interface CheckInput {
|
||||
actor: SubjectRef;
|
||||
action: string;
|
||||
resource?: ResourceRef;
|
||||
context?: Record<string, unknown>;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface WhatResult {
|
||||
resource?: ResourceRef;
|
||||
actions: Record<string, PermissionDecision>;
|
||||
}
|
||||
|
||||
export interface ReverseQueryResult {
|
||||
action: string;
|
||||
resource: ResourceRef;
|
||||
subjectType: string;
|
||||
sources: Array<{
|
||||
type: "relation" | "relationExpansion" | "policy";
|
||||
path?: string;
|
||||
policy?: string;
|
||||
resource?: ResourceRef;
|
||||
}>;
|
||||
constraints: Array<{
|
||||
type: "context" | "attribute" | "unknown";
|
||||
path?: string;
|
||||
required?: unknown;
|
||||
policy?: string;
|
||||
}>;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface PermissionsRuntime {
|
||||
check(input: CheckInput): Promise<PermissionDecision>;
|
||||
can(input: CheckInput): Promise<boolean>;
|
||||
assert(input: CheckInput): Promise<void>;
|
||||
explain(input: CheckInput): Promise<ExplainResult>;
|
||||
what(input: Omit<CheckInput, "action"> & { actions?: string[] }): Promise<WhatResult>;
|
||||
who(input: CheckInput & { resource: ResourceRef; subjectType?: string }): Promise<ReverseQueryResult>;
|
||||
filter(action: string): FilterBuilder;
|
||||
}
|
||||
|
||||
export interface FilterBuilder {
|
||||
for(actor: SubjectRef): FilterBuilder;
|
||||
resource(resourceType: string): FilterBuilder;
|
||||
context(context: Record<string, unknown>): FilterBuilder;
|
||||
toPlan(target?: string): Promise<QueryPlan>;
|
||||
toPredicate(): Promise<(resource: ResourceRef) => Promise<boolean>>;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
],
|
||||
"include": ["src"]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue