refactor(delivery): remove legacy metadata migrations

This commit is contained in:
2026-08-06 10:21:01 +08:00
parent b383cf7a4b
commit 8201448029
6 changed files with 0 additions and 247 deletions
-13
View File
@@ -90,19 +90,6 @@ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().d
Keep keys stable for the lifetime of encrypted metadata. Rotating a key requires
decrypting with the old key and re-encrypting with the new key.
## Metadata Schema Patches
Apply metadata patches in order:
1. `resources/sql/004_metadata_auth_management.sql`
2. `resources/sql/005_metadata_project_configuration.sql`
3. `resources/sql/006_metadata_rbac_roles.sql`
`004` creates Keycloak-backed metadata users and project memberships. `005`
creates project and project database routing tables with uniqueness, role/type,
and pool-size constraints. `006` extends existing membership constraints with
the fixed delivery roles.
## Frontend System Management
`/system-admin` is shown only when `GET /api/v1/access/context` returns
@@ -1,70 +0,0 @@
-- Metadata auth management schema patch.
-- Keycloak owns login credentials; TJWater stores only business identity and access.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
DO $$
DECLARE
users_id_type text;
BEGIN
SELECT data_type INTO users_id_type
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'users'
AND column_name = 'id';
IF users_id_type IS NULL THEN
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
keycloak_id UUID UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
role VARCHAR(20) DEFAULT 'user' NOT NULL,
is_active BOOLEAN DEFAULT TRUE NOT NULL,
is_superuser BOOLEAN DEFAULT FALSE NOT NULL,
attributes JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
last_login_at TIMESTAMP WITH TIME ZONE
);
ELSIF users_id_type <> 'uuid' THEN
RAISE EXCEPTION
'Existing public.users.id is %, not uuid. Export old local users, create Keycloak accounts, then migrate to metadata UUID users before applying this patch.',
users_id_type;
END IF;
END $$;
ALTER TABLE users
ADD COLUMN IF NOT EXISTS keycloak_id UUID,
ADD COLUMN IF NOT EXISTS attributes JSONB,
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE users
ALTER COLUMN role SET DEFAULT 'user';
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_role_check;
ALTER TABLE users
ADD CONSTRAINT users_role_check
CHECK (role IN ('admin', 'user'));
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_keycloak_id ON users(keycloak_id);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
CREATE TABLE IF NOT EXISTS user_project_membership (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
project_id UUID NOT NULL,
project_role VARCHAR(20) DEFAULT 'viewer' NOT NULL,
CONSTRAINT user_project_membership_role_check
CHECK (
project_role IN ('member', 'viewer')
),
CONSTRAINT user_project_membership_unique UNIQUE (user_id, project_id)
);
CREATE INDEX IF NOT EXISTS idx_user_project_membership_user_id
ON user_project_membership(user_id);
CREATE INDEX IF NOT EXISTS idx_user_project_membership_project_id
ON user_project_membership(project_id);
@@ -1,55 +0,0 @@
-- Metadata project configuration schema patch.
-- Admin APIs write these tables; operators should not hand-edit encrypted values.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
code VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
gs_workspace VARCHAR(100) UNIQUE NOT NULL,
map_extent JSONB,
status VARCHAR(20) DEFAULT 'active' NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT projects_status_check CHECK (status IN ('active', 'inactive', 'archived'))
);
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
CREATE INDEX IF NOT EXISTS idx_projects_code ON projects(code);
DROP TRIGGER IF EXISTS update_projects_updated_at ON projects;
CREATE TRIGGER update_projects_updated_at
BEFORE UPDATE ON projects
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TABLE IF NOT EXISTS project_databases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
db_role VARCHAR(20) NOT NULL,
db_type VARCHAR(20) NOT NULL,
dsn_encrypted TEXT NOT NULL,
pool_min_size INTEGER DEFAULT 2 NOT NULL,
pool_max_size INTEGER DEFAULT 10 NOT NULL,
CONSTRAINT project_databases_unique_role UNIQUE (project_id, db_role),
CONSTRAINT project_databases_role_check CHECK (db_role IN ('biz_data', 'iot_data')),
CONSTRAINT project_databases_type_check CHECK (db_type IN ('postgresql', 'timescaledb')),
CONSTRAINT project_databases_pool_check CHECK (
pool_min_size >= 1 AND pool_max_size >= pool_min_size
)
);
CREATE INDEX IF NOT EXISTS idx_project_databases_project_id
ON project_databases(project_id);
CREATE INDEX IF NOT EXISTS idx_project_databases_role
ON project_databases(db_role);
-32
View File
@@ -1,32 +0,0 @@
-- Normalize existing roles to the Web authorization model.
-- This migration is intentionally re-runnable.
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_role_check;
UPDATE users
SET role = 'user'
WHERE role NOT IN ('admin', 'user');
ALTER TABLE users
ADD CONSTRAINT users_role_check
CHECK (role IN ('admin', 'user'));
ALTER TABLE user_project_membership
DROP CONSTRAINT IF EXISTS user_project_membership_role_check;
UPDATE user_project_membership
SET project_role = CASE
WHEN project_role IN (
'owner',
'admin',
'modeler',
'dispatcher'
) THEN 'member'
ELSE 'viewer'
END
WHERE project_role NOT IN ('member', 'viewer');
ALTER TABLE user_project_membership
ADD CONSTRAINT user_project_membership_role_check
CHECK (project_role IN ('member', 'viewer'));
@@ -1,63 +0,0 @@
#!/usr/bin/env python3
"""Build metadata user sync payloads from an old-user to Keycloak mapping CSV."""
from __future__ import annotations
import argparse
import csv
import json
from pathlib import Path
REQUIRED_COLUMNS = {"keycloak_id", "username", "email"}
def parse_bool(value: str | None) -> bool:
if value is None or value == "":
return True
return value.strip().lower() not in {"0", "false", "no", "n", "disabled"}
def build_payload(mapping_csv: Path) -> dict:
with mapping_csv.open(newline="", encoding="utf-8") as handle:
reader = csv.DictReader(handle)
missing = REQUIRED_COLUMNS.difference(reader.fieldnames or [])
if missing:
raise SystemExit(f"missing required CSV columns: {', '.join(sorted(missing))}")
users = []
for row in reader:
users.append(
{
"keycloak_id": row["keycloak_id"].strip(),
"username": row["username"].strip(),
"email": row["email"].strip(),
"role": (row.get("role") or "user").strip().lower(),
"is_active": parse_bool(row.get("is_active")),
}
)
return {"users": users}
def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Convert old local user mappings into a JSON body for "
"POST /api/v1/admin/users/sync/batch. Passwords are never migrated."
)
)
parser.add_argument("mapping_csv", type=Path)
parser.add_argument("-o", "--output", type=Path)
args = parser.parse_args()
payload = build_payload(args.mapping_csv)
content = json.dumps(payload, ensure_ascii=False, indent=2)
if args.output:
args.output.write_text(content + "\n", encoding="utf-8")
else:
print(content)
if __name__ == "__main__":
main()
-14
View File
@@ -1,14 +0,0 @@
from pathlib import Path
def test_rbac_migration_normalizes_legacy_roles():
sql = Path("resources/sql/006_metadata_rbac_roles.sql").read_text(
encoding="utf-8"
)
assert "role IN ('admin', 'user')" in sql
assert "project_role IN ('member', 'viewer')" in sql
assert "'modeler'," in sql
assert "'dispatcher'" in sql
assert "THEN 'member'" in sql
assert "ELSE 'viewer'" in sql