@tatara/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/bin.mjs +200 -24
  2. package/package.json +1 -1
  3. package/templates/features/auth-keycloak/files+authz/src/core/auth/tests/keycloak-authz.test.ts +2 -2
  4. package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +5 -5
  5. package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +2 -2
  6. package/templates/features/authz/files/src/core/authz/db/seed.ts +2 -2
  7. package/templates/features/authz/files/src/core/authz/index.ts +9 -0
  8. package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +9 -280
  9. package/templates/features/authz/files/src/core/authz/system-roles.ts +0 -2
  10. package/templates/features/authz/files/src/core/authz/tests/authz.test.ts +1 -2
  11. package/templates/features/authz/files/tests/helpers/authz.ts +6 -8
  12. package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +3 -3
  13. package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +3 -3
  14. package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
  15. package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
  16. package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
  17. package/templates/versions.json +2 -1
  18. package/templates/features/authz/files/src/core/authz/cache.ts +0 -31
  19. package/templates/features/authz/files/src/core/authz/catalog.ts +0 -85
  20. package/templates/features/authz/files/src/core/authz/constants.ts +0 -32
  21. package/templates/features/authz/files/src/core/authz/middleware.ts +0 -21
  22. package/templates/features/authz/files/src/core/authz/permissions.ts +0 -31
  23. package/templates/features/authz/files/src/core/authz/resolver.ts +0 -98
  24. package/templates/features/authz/files/src/core/authz/services/audit.ts +0 -24
  25. package/templates/features/authz/files/src/core/authz/services/grants.ts +0 -29
  26. package/templates/features/authz/files/src/core/authz/services/members.ts +0 -84
  27. package/templates/features/authz/files/src/core/authz/services/roles.ts +0 -73
  28. package/templates/features/authz/files/src/core/authz/services/sync.ts +0 -34
  29. package/templates/features/authz/files/src/core/authz/types.ts +0 -87
  30. package/templates/features/authz/files/src/core/authz/validators.ts +0 -99
@@ -1,281 +1,10 @@
1
- import { createRoute, z } from "@hono/zod-openapi";
2
- import { currentPrincipal, requireAuth } from "@tatara/auth";
3
- import { requirePermission } from "#core/authz/middleware";
4
- import { effectivePermissions, orgOf } from "#core/authz/resolver";
5
- import { listAudit } from "#core/authz/services/audit";
6
- import { addOverlay, assignRole, getMember, removeOverlay, revokeRole } from "#core/authz/services/members";
7
- import { createRole, deleteRole, listRoles, updateRole } from "#core/authz/services/roles";
8
- import type { OverlayRow } from "#core/authz/types";
9
- import {
10
- auditEntrySchema,
11
- createRoleSchema,
12
- idParams,
13
- memberSchema,
14
- myPermissionsSchema,
15
- overlayInputSchema,
16
- roleSchema,
17
- updateRoleSchema,
18
- userOverlayParams,
19
- userParams,
20
- userRoleParams,
21
- } from "#core/authz/validators";
22
- import { errorResponses, jsonBody, jsonContent, success, successSchema } from "@tatara/http";
23
- import { createRouter, defineRoutes } from "#core/http/routes";
24
- import { catalog } from "#generated/permissions";
25
-
26
- const router = createRouter();
27
- const tags = ["Authorization"];
28
- const toOverlay = (o: OverlayRow) => ({
29
- id: o.id,
30
- resource: o.resource,
31
- action: o.action,
32
- scope: o.scope,
33
- effect: o.effect,
34
- reason: o.reason,
35
- expiresAt: o.expiresAt ? o.expiresAt.toISOString() : null,
1
+ import { authorizationRoutes } from "@tatara/authz";
2
+ import { authz } from "#core/authz/index";
3
+ import type { AppEnv } from "#core/http/context";
4
+ import { defineRoutes } from "#core/http/routes";
5
+
6
+ export default defineRoutes({
7
+ app: "api",
8
+ basePath: "/authorization",
9
+ router: authorizationRoutes<AppEnv>(authz),
36
10
  });
37
-
38
- router.openapi(
39
- createRoute({
40
- method: "get",
41
- path: "/catalog",
42
- tags,
43
- summary: "Every permission the API knows",
44
- middleware: [requireAuth()] as const,
45
- security: [{ bearerAuth: [] }],
46
- responses: {
47
- 200: jsonContent(
48
- successSchema(z.object({ keys: z.array(z.string()), resources: z.record(z.string(), z.unknown()) })),
49
- "Permission catalog",
50
- ),
51
- ...errorResponses(401),
52
- },
53
- }),
54
- (c) => c.json(success({ keys: catalog.keys, resources: catalog.resources }), 200),
55
- );
56
-
57
- router.openapi(
58
- createRoute({
59
- method: "get",
60
- path: "/me",
61
- tags,
62
- summary: "The caller's effective permissions in their org",
63
- middleware: [requireAuth()] as const,
64
- security: [{ bearerAuth: [] }],
65
- responses: { 200: jsonContent(successSchema(myPermissionsSchema), "Effective permissions"), ...errorResponses(401) },
66
- }),
67
- async (c) => {
68
- const principal = currentPrincipal(c);
69
- const permissions = await effectivePermissions(principal);
70
- return c.json(
71
- success({
72
- orgId: orgOf(principal),
73
- roles: permissions.roles,
74
- grants: permissions.grants,
75
- denies: [...permissions.denies],
76
- }),
77
- 200,
78
- );
79
- },
80
- );
81
-
82
- router.openapi(
83
- createRoute({
84
- method: "get",
85
- path: "/roles",
86
- tags,
87
- summary: "Built-in roles and the org's custom roles",
88
- middleware: [requirePermission("role:read")] as const,
89
- security: [{ bearerAuth: ["role:read"] }],
90
- responses: { 200: jsonContent(successSchema(z.array(roleSchema)), "Roles"), ...errorResponses(401, 403) },
91
- }),
92
- async (c) => c.json(success(await listRoles(orgOf(currentPrincipal(c)))), 200),
93
- );
94
-
95
- router.openapi(
96
- createRoute({
97
- method: "post",
98
- path: "/roles",
99
- tags,
100
- summary: "Create a custom role",
101
- middleware: [requirePermission("role:create")] as const,
102
- security: [{ bearerAuth: ["role:create"] }],
103
- request: { body: jsonBody(createRoleSchema, "The role") },
104
- responses: {
105
- 201: jsonContent(successSchema(roleSchema), "Created"),
106
- ...errorResponses(400, 409),
107
- ...errorResponses(401, 403),
108
- },
109
- }),
110
- async (c) => {
111
- const principal = currentPrincipal(c);
112
- const role = await createRole(principal.userId, orgOf(principal), c.req.valid("json"));
113
- return c.json(success(role), 201);
114
- },
115
- );
116
-
117
- router.openapi(
118
- createRoute({
119
- method: "patch",
120
- path: "/roles/{id}",
121
- tags,
122
- summary: "Update a custom role",
123
- middleware: [requirePermission("role:update")] as const,
124
- security: [{ bearerAuth: ["role:update"] }],
125
- request: { params: idParams, body: jsonBody(updateRoleSchema, "Fields to change") },
126
- responses: {
127
- 200: jsonContent(successSchema(roleSchema), "Updated"),
128
- ...errorResponses(400, 404),
129
- ...errorResponses(401, 403),
130
- },
131
- }),
132
- async (c) => {
133
- const principal = currentPrincipal(c);
134
- const role = await updateRole(principal.userId, orgOf(principal), c.req.valid("param").id, c.req.valid("json"));
135
- return c.json(success(role), 200);
136
- },
137
- );
138
-
139
- router.openapi(
140
- createRoute({
141
- method: "delete",
142
- path: "/roles/{id}",
143
- tags,
144
- summary: "Delete a custom role that no member holds",
145
- middleware: [requirePermission("role:delete")] as const,
146
- security: [{ bearerAuth: ["role:delete"] }],
147
- request: { params: idParams },
148
- responses: { 204: { description: "Deleted" }, ...errorResponses(400, 404, 409), ...errorResponses(401, 403) },
149
- }),
150
- async (c) => {
151
- const principal = currentPrincipal(c);
152
- await deleteRole(principal.userId, orgOf(principal), c.req.valid("param").id);
153
- return c.body(null, 204);
154
- },
155
- );
156
-
157
- router.openapi(
158
- createRoute({
159
- method: "get",
160
- path: "/members/{userId}",
161
- tags,
162
- summary: "A member's roles and overlays",
163
- middleware: [requirePermission("member:read")] as const,
164
- security: [{ bearerAuth: ["member:read"] }],
165
- request: { params: userParams },
166
- responses: { 200: jsonContent(successSchema(memberSchema), "Member"), ...errorResponses(401, 403) },
167
- }),
168
- async (c) => {
169
- const member = await getMember(orgOf(currentPrincipal(c)), c.req.valid("param").userId);
170
- return c.json(success({ ...member, overlays: member.overlays.map(toOverlay) }), 200);
171
- },
172
- );
173
-
174
- router.openapi(
175
- createRoute({
176
- method: "post",
177
- path: "/members/{userId}/roles",
178
- tags,
179
- summary: "Assign a role to a member",
180
- middleware: [requirePermission("member:update")] as const,
181
- security: [{ bearerAuth: ["member:update"] }],
182
- request: { params: userParams, body: jsonBody(z.object({ roleId: z.string() }), "Role") },
183
- responses: {
184
- 200: jsonContent(successSchema(z.object({ assigned: z.boolean() })), "Assigned"),
185
- ...errorResponses(404),
186
- ...errorResponses(401, 403),
187
- },
188
- }),
189
- async (c) => {
190
- const principal = currentPrincipal(c);
191
- const assigned = await assignRole(
192
- principal.userId,
193
- orgOf(principal),
194
- c.req.valid("param").userId,
195
- c.req.valid("json").roleId,
196
- );
197
- return c.json(success({ assigned }), 200);
198
- },
199
- );
200
-
201
- router.openapi(
202
- createRoute({
203
- method: "delete",
204
- path: "/members/{userId}/roles/{roleId}",
205
- tags,
206
- summary: "Revoke a role from a member",
207
- middleware: [requirePermission("member:update")] as const,
208
- security: [{ bearerAuth: ["member:update"] }],
209
- request: { params: userRoleParams },
210
- responses: { 204: { description: "Revoked" }, ...errorResponses(404), ...errorResponses(401, 403) },
211
- }),
212
- async (c) => {
213
- const principal = currentPrincipal(c);
214
- const { userId, roleId } = c.req.valid("param");
215
- await revokeRole(principal.userId, orgOf(principal), userId, roleId);
216
- return c.body(null, 204);
217
- },
218
- );
219
-
220
- router.openapi(
221
- createRoute({
222
- method: "post",
223
- path: "/members/{userId}/overlays",
224
- tags,
225
- summary: "Grant or deny one permission to a member",
226
- middleware: [requirePermission("member:update")] as const,
227
- security: [{ bearerAuth: ["member:update"] }],
228
- request: { params: userParams, body: jsonBody(overlayInputSchema, "Overlay") },
229
- responses: {
230
- 201: jsonContent(successSchema(z.object({ id: z.string() })), "Created"),
231
- ...errorResponses(400),
232
- ...errorResponses(401, 403),
233
- },
234
- }),
235
- async (c) => {
236
- const principal = currentPrincipal(c);
237
- const body = c.req.valid("json");
238
- const id = await addOverlay(principal.userId, orgOf(principal), c.req.valid("param").userId, {
239
- ...body,
240
- expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
241
- });
242
- return c.json(success({ id }), 201);
243
- },
244
- );
245
-
246
- router.openapi(
247
- createRoute({
248
- method: "delete",
249
- path: "/members/{userId}/overlays/{overlayId}",
250
- tags,
251
- summary: "Remove an overlay",
252
- middleware: [requirePermission("member:update")] as const,
253
- security: [{ bearerAuth: ["member:update"] }],
254
- request: { params: userOverlayParams },
255
- responses: { 204: { description: "Removed" }, ...errorResponses(404), ...errorResponses(401, 403) },
256
- }),
257
- async (c) => {
258
- const principal = currentPrincipal(c);
259
- const { userId, overlayId } = c.req.valid("param");
260
- await removeOverlay(principal.userId, orgOf(principal), userId, overlayId);
261
- return c.body(null, 204);
262
- },
263
- );
264
-
265
- router.openapi(
266
- createRoute({
267
- method: "get",
268
- path: "/audit",
269
- tags,
270
- summary: "Recent authorization changes in the org",
271
- middleware: [requirePermission("audit_log:read")] as const,
272
- security: [{ bearerAuth: ["audit_log:read"] }],
273
- responses: { 200: jsonContent(successSchema(z.array(auditEntrySchema)), "Audit log"), ...errorResponses(401, 403) },
274
- }),
275
- async (c) => {
276
- const rows = await listAudit(orgOf(currentPrincipal(c)));
277
- return c.json(success(rows.map((r) => ({ ...r, at: r.at.toISOString() }))), 200);
278
- },
279
- );
280
-
281
- export default defineRoutes({ app: "api", basePath: "/authorization", router });
@@ -5,5 +5,3 @@ export const SYSTEM_ROLES = [
5
5
  ] as const;
6
6
 
7
7
  export type SystemRoleKey = (typeof SYSTEM_ROLES)[number]["key"];
8
-
9
- export const SYSTEM_ROLE_KEYS: ReadonlySet<string> = new Set(SYSTEM_ROLES.map((r) => r.key));
@@ -1,6 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { allows } from "#core/authz/resolver";
3
- import type { EffectivePermissions } from "#core/authz/types";
2
+ import { allows, type EffectivePermissions } from "@tatara/authz";
4
3
  import { api } from "#tests/helpers/api";
5
4
  import { asUser, authenticate } from "#tests/helpers/auth";
6
5
  import { applyTestAccess } from "#tests/helpers/authz";
@@ -1,7 +1,5 @@
1
- import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "#core/authz/constants";
2
- import { store } from "#core/authz/db/store";
3
- import { addOverlay, assignRole } from "#core/authz/services/members";
4
- import { syncSystemRoles } from "#core/authz/services/sync";
1
+ import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "@tatara/authz";
2
+ import { authz } from "#core/authz/index";
5
3
  import type { SystemRoleKey } from "#core/authz/system-roles";
6
4
  import type { PermissionKey } from "#generated/permissions";
7
5
 
@@ -17,14 +15,14 @@ export async function applyTestAccess(userId: string, access: TestAccess): Promi
17
15
  const orgId = access.orgId ?? DEFAULT_ORG_ID;
18
16
  for (const key of access.permissions ?? []) {
19
17
  const [resource = "", action = ""] = key.split(":");
20
- await addOverlay(null, orgId, userId, { resource, action, effect: "grant" });
18
+ await authz.addOverlay(null, orgId, userId, { resource, action, effect: "grant" });
21
19
  }
22
20
  if (access.systemRoles?.length) {
23
- await syncSystemRoles();
21
+ await authz.syncSystemRoles();
24
22
  for (const roleKey of access.systemRoles) {
25
- const role = await store.findRoleByKey(SYSTEM_ORG_ID, roleKey);
23
+ const role = await authz.store.findRoleByKey(SYSTEM_ORG_ID, roleKey);
26
24
  if (!role) throw new Error(`Unknown built-in role ${roleKey}`);
27
- await assignRole(null, orgId, userId, role.id);
25
+ await authz.assignRole(null, orgId, userId, role.id);
28
26
  }
29
27
  }
30
28
  }
@@ -1,6 +1,6 @@
1
1
  import { and, count, desc, eq, gt, inArray, isNull, or } from "drizzle-orm";
2
- import type { OverlayEffect, Scope } from "#core/authz/constants";
3
- import { SYSTEM_ORG_ID } from "#core/authz/constants";
2
+ import type { OverlayEffect, Scope } from "@tatara/authz";
3
+ import { SYSTEM_ORG_ID } from "@tatara/authz";
4
4
  import {
5
5
  authzAuditLog,
6
6
  authzMemberRoles,
@@ -8,7 +8,7 @@ import {
8
8
  authzRolePermissions,
9
9
  authzRoles,
10
10
  } from "#core/authz/db/authz.schema";
11
- import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "#core/authz/types";
11
+ import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "@tatara/authz";
12
12
  import { db } from "#core/db/client";
13
13
  import { newId } from "@tatara/db";
14
14
 
@@ -1,6 +1,6 @@
1
- import type { OverlayEffect, Scope } from "#core/authz/constants";
2
- import { SYSTEM_ORG_ID } from "#core/authz/constants";
3
- import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "#core/authz/types";
1
+ import type { OverlayEffect, Scope } from "@tatara/authz";
2
+ import { SYSTEM_ORG_ID } from "@tatara/authz";
3
+ import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "@tatara/authz";
4
4
  import { db } from "#core/db/client";
5
5
  import { newId } from "@tatara/db";
6
6
 
@@ -1,4 +1,4 @@
1
- import { definePermissions } from "#core/authz/catalog";
1
+ import { definePermissions } from "@tatara/authz";
2
2
 
3
3
  export default definePermissions({
4
4
  resources: {
@@ -1,4 +1,4 @@
1
- import { definePermissions } from "#core/authz/catalog";
1
+ import { definePermissions } from "@tatara/authz";
2
2
 
3
3
  export default definePermissions({
4
4
  resources: {
@@ -1,4 +1,4 @@
1
- import { definePermissions } from "#core/authz/catalog";
1
+ import { definePermissions } from "@tatara/authz";
2
2
 
3
3
  export default definePermissions({
4
4
  resources: {
@@ -36,5 +36,6 @@
36
36
  "@tatara/db": "0.1.0",
37
37
  "@tatara/testing": "0.1.0",
38
38
  "@tatara/auth": "0.1.0",
39
- "@tatara/keycloak": "0.1.0"
39
+ "@tatara/keycloak": "0.1.0",
40
+ "@tatara/authz": "0.1.0"
40
41
  }
@@ -1,31 +0,0 @@
1
- import { CACHE_TTL_MS } from "#core/authz/constants";
2
- import type { EffectivePermissions } from "#core/authz/types";
3
-
4
- const entries = new Map<string, { value: EffectivePermissions; expires: number }>();
5
-
6
- const key = (orgId: string, userId: string, roles: string[]) =>
7
- `${orgId}:${userId}:${[...roles].sort().join(",")}`;
8
-
9
- export function getCached(orgId: string, userId: string, roles: string[]) {
10
- const entry = entries.get(key(orgId, userId, roles));
11
- if (!entry || entry.expires < Date.now()) return undefined;
12
- return entry.value;
13
- }
14
-
15
- export function setCached(orgId: string, userId: string, roles: string[], value: EffectivePermissions) {
16
- if (entries.size > 10_000) entries.clear();
17
- entries.set(key(orgId, userId, roles), { value, expires: Date.now() + CACHE_TTL_MS });
18
- }
19
-
20
- /** Drops cached sets for one member (any org when orgId is omitted). */
21
- export function invalidateMember(userId: string, orgId?: string) {
22
- for (const k of entries.keys()) {
23
- const [o, u] = k.split(":");
24
- if (u === userId && (!orgId || o === orgId)) entries.delete(k);
25
- }
26
- }
27
-
28
- /** Role definitions changed: every cached set may be stale. */
29
- export function invalidateAll() {
30
- entries.clear();
31
- }
@@ -1,85 +0,0 @@
1
- import { SCOPES, type Scope } from "#core/authz/constants";
2
- import type { SystemRoleKey } from "#core/authz/system-roles";
3
-
4
- export interface ActionSpec {
5
- /** scoped actions are checked at own | team | org */
6
- readonly scoped: boolean;
7
- }
8
-
9
- export type ResourceSpecs = Record<string, Record<string, ActionSpec>>;
10
-
11
- export type GrantTuple = readonly [resource: string, action: string, scope?: Scope];
12
-
13
- export interface PermissionSlice<R extends ResourceSpecs = ResourceSpecs> {
14
- /** resources this module owns and the actions on them */
15
- readonly resources: R;
16
- /** grants this module adds to each built-in role, for its own resources */
17
- readonly roleGrants?: Partial<Record<SystemRoleKey, readonly GrantTuple[]>>;
18
- }
19
-
20
- /** "resource:action" keys of one slice, for typed `requirePermission(...)` calls. */
21
- export type PermissionKeysOf<S> = S extends PermissionSlice<infer R>
22
- ? { [K in keyof R & string]: `${K}:${keyof R[K] & string}` }[keyof R & string]
23
- : never;
24
-
25
- /** Default export of every `permissions/*.permissions.ts`; merged by `tatara sync`. */
26
- export function definePermissions<const R extends ResourceSpecs>(
27
- slice: PermissionSlice<R>,
28
- ): PermissionSlice<R> {
29
- return slice;
30
- }
31
-
32
- export interface Catalog {
33
- resources: ResourceSpecs;
34
- keys: string[];
35
- roleGrants: Partial<Record<SystemRoleKey, GrantTuple[]>>;
36
- }
37
-
38
- export function mergeCatalogs(...slices: PermissionSlice[]): Catalog {
39
- const resources: Record<string, Record<string, ActionSpec>> = {};
40
- const roleGrants: Partial<Record<SystemRoleKey, GrantTuple[]>> = {};
41
- for (const slice of slices) {
42
- for (const [resource, actions] of Object.entries(slice.resources)) {
43
- if (resources[resource]) throw new Error(`Permission resource "${resource}" is declared twice`);
44
- resources[resource] = actions;
45
- }
46
- for (const [role, grants] of Object.entries(slice.roleGrants ?? {})) {
47
- const key = role as SystemRoleKey;
48
- roleGrants[key] = [...(roleGrants[key] ?? []), ...(grants ?? [])];
49
- }
50
- }
51
- const keys = Object.entries(resources).flatMap(([resource, actions]) =>
52
- Object.keys(actions).map((action) => `${resource}:${action}`),
53
- );
54
- return { resources, keys, roleGrants };
55
- }
56
-
57
- /** Resources only built-in roles may grant: authorization itself. */
58
- export const META_RESOURCES: ReadonlySet<string> = new Set(["role", "member", "audit_log"]);
59
-
60
- export class InvalidGrantError extends Error {}
61
-
62
- /** Throws unless (resource, action, scope) exists in the catalog with a matching scope shape. */
63
- export function assertGrantValid(
64
- catalog: Catalog,
65
- grant: { resource: string; action: string; scope?: string | null },
66
- ): void {
67
- const spec = catalog.resources[grant.resource]?.[grant.action];
68
- if (!spec) throw new InvalidGrantError(`Unknown permission ${grant.resource}:${grant.action}`);
69
- const scope = grant.scope ?? null;
70
- if (spec.scoped && !scope) {
71
- throw new InvalidGrantError(
72
- `${grant.resource}:${grant.action} is scoped; pass one of ${SCOPES.join(", ")}`,
73
- );
74
- }
75
- if (spec.scoped && scope && !(SCOPES as readonly string[]).includes(scope)) {
76
- throw new InvalidGrantError(`Scope "${scope}" must be one of ${SCOPES.join(", ")}`);
77
- }
78
- if (!spec.scoped && scope) {
79
- throw new InvalidGrantError(`${grant.resource}:${grant.action} is not scoped; drop the scope`);
80
- }
81
- }
82
-
83
- export function isScoped(catalog: Catalog, resource: string, action: string): boolean {
84
- return catalog.resources[resource]?.[action]?.scoped === true;
85
- }
@@ -1,32 +0,0 @@
1
- /** Scope hierarchy: a grant at a higher rank satisfies a check at a lower one (org > team > own). */
2
- export const SCOPES = ["own", "team", "org"] as const;
3
- export type Scope = (typeof SCOPES)[number];
4
-
5
- export const SCOPE_RANK: Record<Scope, number> = { own: 1, team: 2, org: 3 };
6
-
7
- export const OVERLAY_EFFECTS = ["grant", "deny"] as const;
8
- export type OverlayEffect = (typeof OVERLAY_EFFECTS)[number];
9
-
10
- export const AUDIT_CHANGE_TYPES = [
11
- "role_created",
12
- "role_updated",
13
- "role_deleted",
14
- "member_role_assigned",
15
- "member_role_revoked",
16
- "overlay_granted",
17
- "overlay_revoked",
18
- "member_removed",
19
- ] as const;
20
- export type AuditChangeType = (typeof AUDIT_CHANGE_TYPES)[number];
21
-
22
- /** Audit actor for changes made by the system rather than a user (e.g. org membership hooks). */
23
- export const SYSTEM_ACTOR = "system";
24
-
25
- /** Org used when the principal has none (single-tenant apps, users without an active org). */
26
- export const DEFAULT_ORG_ID = "default";
27
-
28
- /** Org id stored on system (built-in) roles; they apply to every org. */
29
- export const SYSTEM_ORG_ID = "";
30
-
31
- /** How long a resolved permission set is cached in-process. */
32
- export const CACHE_TTL_MS = 30_000;
@@ -1,21 +0,0 @@
1
- import { createMiddleware } from "hono/factory";
2
- import type { Scope } from "#core/authz/constants";
3
- import { checkPermission } from "#core/authz/resolver";
4
- import type { AppEnv } from "#core/http/context";
5
- import { ForbiddenError, UnauthorizedError } from "@tatara/http";
6
- import type { PermissionKey } from "#generated/permissions";
7
-
8
- /**
9
- * 401 without a principal, 403 unless it holds `resource:action` (at `scope` or wider).
10
- * Pair with `security: [{ bearerAuth: ["resource:action"] }]` so the route documents it.
11
- */
12
- export function requirePermission(permission: PermissionKey, scope?: Scope) {
13
- return createMiddleware<AppEnv>(async (c, next) => {
14
- const principal = c.get("principal");
15
- if (!principal) throw new UnauthorizedError();
16
- if (!(await checkPermission(principal, permission, scope))) {
17
- throw new ForbiddenError(`Missing permission ${permission}${scope ? ` (${scope})` : ""}`);
18
- }
19
- await next();
20
- });
21
- }
@@ -1,31 +0,0 @@
1
- import { definePermissions } from "#core/authz/catalog";
2
-
3
- /** Authorization's own resources; only built-in roles may grant them. */
4
- export default definePermissions({
5
- resources: {
6
- role: {
7
- read: { scoped: false },
8
- create: { scoped: false },
9
- update: { scoped: false },
10
- delete: { scoped: false },
11
- },
12
- member: {
13
- read: { scoped: false },
14
- update: { scoped: false },
15
- },
16
- audit_log: {
17
- read: { scoped: false },
18
- },
19
- },
20
- roleGrants: {
21
- admin: [
22
- ["role", "read"],
23
- ["role", "create"],
24
- ["role", "update"],
25
- ["role", "delete"],
26
- ["member", "read"],
27
- ["member", "update"],
28
- ["audit_log", "read"],
29
- ],
30
- },
31
- });