feat: add core authorization foundation

This commit is contained in:
ekklesia
2026-07-23 10:29:01 -05:00
parent 3fa1c4cdec
commit 72e082ff2f
23 changed files with 3029 additions and 3 deletions
+145
View File
@@ -0,0 +1,145 @@
import { relations, sql } from "drizzle-orm";
import { check, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { ekklesiaCoreUser } from "../identity/schema.js";
/**
* Core-owned authorization schema: grants (explicit or relationship-derived), platform-owner
* status, and an append-only audit log of every security-relevant mutation. Table and index names
* are explicit and namespaced so ownership is unambiguous inside the shared public PostgreSQL
* schema, exactly like `core/src/identity/schema.ts`. Foreign keys to `ekklesia_core_user` are
* safe here (unlike a module referencing another module's tables) because Core owns both sides.
*/
export const ekklesiaCoreGrant = pgTable(
"ekklesia_core_grant",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: text("user_id")
.notNull()
.references(() => ekklesiaCoreUser.id, { onDelete: "cascade" }),
permissionId: text("permission_id").notNull(),
scope: text("scope"),
sourceKind: text("source_kind").notNull(),
sourceRef: text("source_ref"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
createdByActorType: text("created_by_actor_type").notNull(),
createdByActorId: text("created_by_actor_id"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
revokedByActorType: text("revoked_by_actor_type"),
revokedByActorId: text("revoked_by_actor_id"),
},
(table) => [
index("ekklesia_core_grant_user_permission_idx").on(table.userId, table.permissionId),
check(
"ekklesia_core_grant_source_kind_check",
sql`${table.sourceKind} in ('explicit', 'relationship')`,
),
check(
"ekklesia_core_grant_created_actor_type_check",
sql`${table.createdByActorType} in ('user', 'system')`,
),
check(
"ekklesia_core_grant_revoked_actor_type_check",
sql`${table.revokedByActorType} is null or ${table.revokedByActorType} in ('user', 'system')`,
),
],
);
export const ekklesiaCorePlatformOwner = pgTable(
"ekklesia_core_platform_owner",
{
userId: text("user_id")
.primaryKey()
.references(() => ekklesiaCoreUser.id, { onDelete: "cascade" }),
grantedAt: timestamp("granted_at", { withTimezone: true }).defaultNow().notNull(),
grantedByActorType: text("granted_by_actor_type").notNull(),
grantedByActorId: text("granted_by_actor_id"),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
revokedByActorType: text("revoked_by_actor_type"),
revokedByActorId: text("revoked_by_actor_id"),
},
(table) => [
check(
"ekklesia_core_platform_owner_granted_actor_type_check",
sql`${table.grantedByActorType} in ('user', 'system')`,
),
check(
"ekklesia_core_platform_owner_revoked_actor_type_check",
sql`${table.revokedByActorType} is null or ${table.revokedByActorType} in ('user', 'system')`,
),
],
);
/**
* Append-only: no update or delete path is ever exposed by Core's authorization store. `scope`
* intentionally rejects an empty (but non-null) string — defense in depth matching the
* application-layer `AuthorizationScope` contract, which never produces one — so a coding defect
* that slipped an empty scope past that validation fails the whole grant-plus-audit transaction
* atomically instead of recording a meaningless audit entry.
*/
export const ekklesiaCoreAuthorizationAudit = pgTable(
"ekklesia_core_authorization_audit",
{
id: uuid("id").primaryKey().defaultRandom(),
action: text("action").notNull(),
actorType: text("actor_type").notNull(),
actorId: text("actor_id"),
subjectUserId: text("subject_user_id")
.notNull()
.references(() => ekklesiaCoreUser.id, { onDelete: "cascade" }),
permissionId: text("permission_id"),
scope: text("scope"),
grantId: uuid("grant_id"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("ekklesia_core_authorization_audit_subject_idx").on(table.subjectUserId),
check(
"ekklesia_core_authorization_audit_action_check",
sql`${table.action} in ('grant.create', 'grant.revoke', 'platform_owner.grant', 'platform_owner.revoke')`,
),
check(
"ekklesia_core_authorization_audit_actor_type_check",
sql`${table.actorType} in ('user', 'system')`,
),
check(
"ekklesia_core_authorization_audit_scope_check",
sql`${table.scope} is null or length(${table.scope}) > 0`,
),
],
);
export const ekklesiaCoreGrantRelations = relations(ekklesiaCoreGrant, ({ one }) => ({
user: one(ekklesiaCoreUser, {
fields: [ekklesiaCoreGrant.userId],
references: [ekklesiaCoreUser.id],
}),
}));
export const ekklesiaCorePlatformOwnerRelations = relations(
ekklesiaCorePlatformOwner,
({ one }) => ({
user: one(ekklesiaCoreUser, {
fields: [ekklesiaCorePlatformOwner.userId],
references: [ekklesiaCoreUser.id],
}),
}),
);
export const ekklesiaCoreAuthorizationAuditRelations = relations(
ekklesiaCoreAuthorizationAudit,
({ one }) => ({
subject: one(ekklesiaCoreUser, {
fields: [ekklesiaCoreAuthorizationAudit.subjectUserId],
references: [ekklesiaCoreUser.id],
}),
}),
);
/** Explicit map passed to Drizzle, mirroring `identityAdapterSchema`'s explicit-mapping style. */
export const authorizationSchema = {
authorizationAudit: ekklesiaCoreAuthorizationAudit,
grant: ekklesiaCoreGrant,
platformOwner: ekklesiaCorePlatformOwner,
} as const;