-- W1 거버넌스/RBAC. 멱등. db/apply-local-ddl.sh 로 실행 DB 비파괴 적용. -- 기존 데이터(전원 role='USER') 호환 — 추가만, 파괴 없음. -- 1) users.role 값집합 확장 (CHECK 멱등 추가) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_role_check') THEN ALTER TABLE "users" ADD CONSTRAINT "users_role_check" CHECK ("role" IN ('ADMIN', 'SUBADMIN', 'USER')); END IF; END $$; -- 2) users.permissions_epoch (세션 권한 전파 버전 스탬프) ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "permissions_epoch" bigint DEFAULT 0 NOT NULL; COMMENT ON COLUMN "users"."permissions_epoch" IS 'RBAC 권한 변경 버전. 변경 시 +1 → 세션 캐시 epoch 와 mismatch 시 권한 재로딩(회수 즉시성, 결정4)'; -- 3) permissions (권한 카탈로그) CREATE SEQUENCE IF NOT EXISTS "permissions_id_seq"; CREATE TABLE IF NOT EXISTS "permissions" ( "id" bigint DEFAULT nextval('permissions_id_seq'::regclass) NOT NULL, "permission_key" character varying(50) NOT NULL, "display_name" character varying(100) NOT NULL, "is_active" boolean DEFAULT true NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "permissions_id_seq" OWNED BY "permissions"."id"; CREATE UNIQUE INDEX IF NOT EXISTS "ux_permissions_key" ON "permissions" ("permission_key"); -- 4) user_permissions (SUBADMIN 개별 권한 join) CREATE SEQUENCE IF NOT EXISTS "user_permissions_id_seq"; CREATE TABLE IF NOT EXISTS "user_permissions" ( "id" bigint DEFAULT nextval('user_permissions_id_seq'::regclass) NOT NULL, "user_id" bigint NOT NULL, "permission_key" character varying(50) NOT NULL, "granted_by" bigint, "created_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "user_permissions_id_seq" OWNED BY "user_permissions"."id"; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_permissions_user_id_fkey') THEN ALTER TABLE "user_permissions" ADD CONSTRAINT "user_permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id"); END IF; END $$; CREATE UNIQUE INDEX IF NOT EXISTS "ux_user_permissions_user_key" ON "user_permissions" ("user_id", "permission_key"); CREATE INDEX IF NOT EXISTS "idx_user_permissions_user" ON "user_permissions" ("user_id"); -- 5) rbac_audit_log (감사로그 — 임명/강등/토글 대칭 기록) CREATE SEQUENCE IF NOT EXISTS "rbac_audit_log_id_seq"; CREATE TABLE IF NOT EXISTS "rbac_audit_log" ( "id" bigint DEFAULT nextval('rbac_audit_log_id_seq'::regclass) NOT NULL, "actor_id" bigint NOT NULL, "target_id" bigint NOT NULL, "action" character varying(30) NOT NULL, "permission_key" character varying(50), "created_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "rbac_audit_log_id_seq" OWNED BY "rbac_audit_log"."id"; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'rbac_audit_log_action_check') THEN ALTER TABLE "rbac_audit_log" ADD CONSTRAINT "rbac_audit_log_action_check" CHECK ("action" IN ('APPOINT', 'DEMOTE', 'GRANT', 'REVOKE')); END IF; END $$;