forked from EduCraft/curriculum-project-hub
26523d1b54
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
4.8 KiB
PL/PgSQL
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 中断链暴露。
|