feat: add core authorization foundation
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user