20260716_10_rbac.py 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. """Add normalized identities and fixed platform roles without copying legacy passwords."""
  2. from alembic import op
  3. revision = "20260716_10"
  4. down_revision = "20260716_03"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. DO $$
  11. BEGIN
  12. IF EXISTS (
  13. SELECT 1 FROM information_schema.columns
  14. WHERE table_schema = 'public' AND table_name = 'users'
  15. AND column_name = 'password'
  16. ) AND NOT EXISTS (
  17. SELECT 1 FROM information_schema.tables
  18. WHERE table_schema = 'public' AND table_name = 'users_legacy'
  19. ) THEN
  20. ALTER TABLE public.users RENAME TO users_legacy;
  21. ALTER INDEX IF EXISTS public.idx_users_username
  22. RENAME TO idx_users_legacy_username;
  23. END IF;
  24. END $$;
  25. CREATE TABLE IF NOT EXISTS public.roles (
  26. id UUID PRIMARY KEY,
  27. name VARCHAR(32) NOT NULL UNIQUE,
  28. description VARCHAR(200) NOT NULL,
  29. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  30. );
  31. CREATE TABLE IF NOT EXISTS public.users (
  32. id UUID PRIMARY KEY,
  33. username VARCHAR(64) NOT NULL UNIQUE,
  34. display_name VARCHAR(100),
  35. password_hash TEXT NOT NULL,
  36. status VARCHAR(20) NOT NULL DEFAULT 'active'
  37. CHECK (status IN ('active', 'disabled')),
  38. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  39. updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  40. last_login_at TIMESTAMPTZ
  41. );
  42. CREATE TABLE IF NOT EXISTS public.user_roles (
  43. user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
  44. role_id UUID NOT NULL REFERENCES public.roles(id) ON DELETE CASCADE,
  45. assigned_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  46. assigned_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
  47. PRIMARY KEY (user_id, role_id)
  48. );
  49. CREATE TABLE IF NOT EXISTS public.auth_audit_events (
  50. id BIGSERIAL PRIMARY KEY,
  51. user_id UUID REFERENCES public.users(id) ON DELETE SET NULL,
  52. username VARCHAR(64),
  53. event_type VARCHAR(40) NOT NULL,
  54. success BOOLEAN NOT NULL,
  55. ip_address VARCHAR(64),
  56. user_agent VARCHAR(300),
  57. detail VARCHAR(500),
  58. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  59. );
  60. CREATE INDEX IF NOT EXISTS idx_auth_audit_created_at
  61. ON public.auth_audit_events(created_at DESC);
  62. CREATE INDEX IF NOT EXISTS idx_auth_audit_username
  63. ON public.auth_audit_events(username);
  64. INSERT INTO public.roles (id, name, description) VALUES
  65. ('01900000-0000-7000-8000-000000000001', 'admin', '平台管理员'),
  66. ('01900000-0000-7000-8000-000000000002', 'editor', '治理编辑者'),
  67. ('01900000-0000-7000-8000-000000000003', 'viewer', '只读查看者')
  68. ON CONFLICT (name) DO NOTHING;
  69. """
  70. )
  71. def downgrade() -> None:
  72. # Identity rollback is intentionally data-preserving.
  73. pass