UNPKG

@unkey/rbac

Version:
154 lines (152 loc) 3.64 kB
// src/permissions.ts import { z } from "zod"; function buildIdSchema(prefix) { return z.string().refine((s) => { if (s === "*") { return true; } const regex = new RegExp( `^${prefix}_[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{8,32}$` ); return regex.test(s); }); } var apiId = buildIdSchema("api"); var ratelimitNamespaceId = buildIdSchema("rl"); var apiActions = z.enum([ "read_api", "create_api", "delete_api", "update_api", "create_key", "update_key", "delete_key", "encrypt_key", "decrypt_key", "read_key" ]); var ratelimitActions = z.enum([ "limit", "create_namespace", "read_namespace", "update_namespace", "delete_namespace" ]); var unkeyPermissionValidation = z.custom().refine((s) => { z.string().parse(s); if (s === "*") { return true; } const split = s.split("."); if (split.length !== 3) { return false; } const [resource, id, action] = split; switch (resource) { case "api": { return apiId.safeParse(id).success && apiActions.safeParse(action).success; } case "ratelimit": { return ratelimitNamespaceId.safeParse(id).success && ratelimitActions.safeParse(action).success; } default: { return false; } } }); // src/queries.ts import { z as z2 } from "zod"; var permissionQuerySchema = z2.union([ z2.string(), z2.object({ and: z2.array(z2.lazy(() => permissionQuerySchema)).min(1, "provide at least one permission") }), z2.object({ or: z2.array(z2.lazy(() => permissionQuerySchema)).min(1, "provide at least one permission") }) ]); function merge(rule, ...args) { return args.filter(Boolean).reduce( (acc, arg) => { if (typeof acc === "string") { throw new Error("Cannot merge into a string"); } if (!acc[rule]) { acc[rule] = []; } acc[rule].push(arg); return acc; }, {} ); } function or(...args) { return merge("or", ...args); } function and(...args) { return merge("and", ...args); } function buildQuery(fn) { return fn({ or, and }); } var buildUnkeyQuery = buildQuery; // src/rbac.ts import { Err, Ok, SchemaError } from "@unkey/error"; var RBAC = class { evaluatePermissions(q, roles) { return this.evaluateQueryV1(q, roles); } validateQuery(q) { const validQuery = permissionQuerySchema.safeParse(q); if (!validQuery.success) { return Err(SchemaError.fromZod(validQuery.error, q)); } return Ok({ query: validQuery.data }); } evaluateQueryV1(query, roles) { if (typeof query === "string") { if (roles.includes(query)) { return Ok({ valid: true }); } return Ok({ valid: false, message: `Role ${query} not allowed` }); } if (query.and) { const results = query.and.filter(Boolean).map((q) => this.evaluateQueryV1(q, roles)); for (const r of results) { if (r.err) { return r; } if (!r.val.valid) { return r; } } return Ok({ valid: true }); } if (query.or) { for (const q of query.or) { const r = this.evaluateQueryV1(q, roles); if (r.err) { return r; } if (r.val.valid) { return r; } } return Ok({ valid: false, message: "No role matched" }); } return Err(new SchemaError({ message: "reached end of evaluate and no match" })); } }; export { RBAC, and, apiActions, buildIdSchema, buildQuery, buildUnkeyQuery, or, permissionQuerySchema, ratelimitActions, unkeyPermissionValidation }; //# sourceMappingURL=index.js.map