hts/packages/rbac/src/permissions.ts

77 lines
1.7 KiB
TypeScript

/**
* The database takes care of isolating roles between workspaces.
* That's why we can assume the highest scope of a role is an `api` or later `gateway`
*
* role identifiers can look like this:
* - `api_id.xxx`
* - `gateway_id.xxx`
*
*/
import { z } from "zod";
import { Flatten } from "./types";
export function buildIdSchema(prefix: string) {
return z.string().refine((s) => {
if (s === "*") {
return true;
}
const regex = new RegExp(
`^${prefix}_[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{8,32}$`,
);
return regex.test(s);
});
}
const apiId = buildIdSchema("api");
const ratelimitNamespaceId = buildIdSchema("rl");
export const apiActions = z.enum([
"read_api",
"create_api",
"delete_api",
"update_api",
"create_key",
"update_key",
"delete_key",
"read_key",
]);
export const ratelimitActions = z.enum(["limit"]);
export type Resources = {
[resourceId in `api.${z.infer<typeof apiId>}`]: z.infer<typeof apiActions>;
} & {
[resourceId in `ratelimit.${z.infer<typeof ratelimitNamespaceId>}`]: z.infer<
typeof ratelimitActions
>;
};
export type UnkeyPermission = Flatten<Resources> | "*";
/**
* Validation for roles used for our root keys
*/
export const unkeyPermissionValidation = z.custom<UnkeyPermission>().refine((s) => {
z.string().parse(s);
if (s === "*") {
/**
* This is a legacy role granting access to everything
*/
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;
}
default: {
return false;
}
}
});