Files
curriculum-project-hub/hub/prisma/migrations/20260806120000_filelib_audit_log/migration.sql
T

93 lines
4.8 KiB
PL/PgSQL

-- 文件库审计日志(横切模块)。语义锚点:ADR-0039。
--
-- 防篡改由三件事共同保证(ADR-0039「不可篡改三道」):
-- 1. 服务路径只 INSERT/SELECT(应用层约束);
-- 2. BEFORE UPDATE OR DELETE 触发器无条件抛异常 —— 应用层写错也拦得住,
-- 归档只允许改 archivedAt 这一列(保留策略是唯一合法出场方式);
-- 3. entryHash 哈希链,org 内按 seq 单调串联,可离线全量校验。
CREATE TYPE "FileLibAuditObjectType" AS ENUM ('FOLDER', 'PROJECT', 'FILE', 'GRANT', 'EXPORT_JOB', 'GROUP', 'SYSTEM');
CREATE TYPE "FileLibAuditResult" AS ENUM ('SUCCESS', 'FAILURE');
CREATE TABLE "FileLibAuditLog" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"seq" BIGINT NOT NULL,
"occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"action" TEXT NOT NULL,
"result" "FileLibAuditResult" NOT NULL DEFAULT 'SUCCESS',
"failureReason" TEXT,
"actorUserId" TEXT NOT NULL,
"actorName" TEXT NOT NULL,
"actorIsAdmin" BOOLEAN NOT NULL DEFAULT false,
"objectType" "FileLibAuditObjectType" NOT NULL,
"objectId" TEXT NOT NULL,
"objectName" TEXT NOT NULL,
"objectPath" TEXT NOT NULL,
"beforeValue" JSONB,
"afterValue" JSONB,
"context" JSONB,
"clientIp" TEXT,
"userAgent" TEXT,
"entryHash" TEXT NOT NULL,
"prevHash" TEXT,
"archivedAt" TIMESTAMP(3),
CONSTRAINT "FileLibAuditLog_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "FileLibAuditLog_organizationId_seq_key" ON "FileLibAuditLog"("organizationId", "seq");
CREATE INDEX "FileLibAuditLog_organizationId_occurredAt_idx" ON "FileLibAuditLog"("organizationId", "occurredAt");
CREATE INDEX "FileLibAuditLog_organizationId_actorUserId_occurredAt_idx" ON "FileLibAuditLog"("organizationId", "actorUserId", "occurredAt");
CREATE INDEX "FileLibAuditLog_organizationId_action_occurredAt_idx" ON "FileLibAuditLog"("organizationId", "action", "occurredAt");
CREATE INDEX "FileLibAuditLog_organizationId_objectType_objectId_idx" ON "FileLibAuditLog"("organizationId", "objectType", "objectId");
CREATE INDEX "FileLibAuditLog_organizationId_objectPath_idx" ON "FileLibAuditLog"("organizationId", "objectPath");
CREATE INDEX "FileLibAuditLog_organizationId_archivedAt_idx" ON "FileLibAuditLog"("organizationId", "archivedAt");
ALTER TABLE "FileLibAuditLog"
ADD CONSTRAINT "FileLibAuditLog_organizationId_fkey"
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
-- 只追加写入:UPDATE 仅放行 archivedAt 单列变更(保留策略归档),其余全拒。
CREATE OR REPLACE FUNCTION "filelib_audit_log_append_only"() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'FileLibAuditLog is append-only: DELETE is forbidden (id=%)', OLD."id";
END IF;
-- 归档打标之外的任何列变化都是篡改。逐列比对而非整行比对,
-- 这样 archivedAt 之外的字段一旦被动过就立刻炸。
IF ROW(NEW."id", NEW."organizationId", NEW."seq", NEW."occurredAt", NEW."action",
NEW."result", NEW."failureReason", NEW."actorUserId", NEW."actorName",
NEW."actorIsAdmin", NEW."objectType", NEW."objectId", NEW."objectName",
NEW."objectPath", NEW."beforeValue", NEW."afterValue", NEW."context",
NEW."clientIp", NEW."userAgent", NEW."entryHash", NEW."prevHash")
IS DISTINCT FROM
ROW(OLD."id", OLD."organizationId", OLD."seq", OLD."occurredAt", OLD."action",
OLD."result", OLD."failureReason", OLD."actorUserId", OLD."actorName",
OLD."actorIsAdmin", OLD."objectType", OLD."objectId", OLD."objectName",
OLD."objectPath", OLD."beforeValue", OLD."afterValue", OLD."context",
OLD."clientIp", OLD."userAgent", OLD."entryHash", OLD."prevHash")
THEN
RAISE EXCEPTION 'FileLibAuditLog is immutable: only archivedAt may change (id=%)', OLD."id";
END IF;
-- 归档不可撤销(取消归档等同于把已出场的日志拉回,是策略绕过)。
IF OLD."archivedAt" IS NOT NULL AND NEW."archivedAt" IS DISTINCT FROM OLD."archivedAt" THEN
RAISE EXCEPTION 'FileLibAuditLog archive stamp is write-once (id=%)', OLD."id";
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "filelib_audit_log_append_only"
BEFORE UPDATE OR DELETE ON "FileLibAuditLog"
FOR EACH ROW EXECUTE FUNCTION "filelib_audit_log_append_only"();
-- 刻意**不**装 BEFORE TRUNCATE 触发器。TRUNCATE 需要表属主权限,能 TRUNCATE
-- 的角色同样能 DROP TRIGGER —— 挡它防不住有意篡改,却会拦下集成测试按 schema
-- 全量重置的固定装置。本触发器防的是应用层写错(UPDATE/DELETE 走行触发器),
-- 有意篡改由哈希链兜底:绕过服务改数据仍会在 /audit/verify 中断链暴露。