fix: enforce durable migration ownership and readiness
This commit is contained in:
parent
14befd0b50
commit
826874a47e
5 changed files with 52 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ from alembic import context
|
|||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from hub_core.models import Base
|
||||
from hub_core.migrations.roles import migration_role_statement
|
||||
|
||||
config = context.config
|
||||
|
||||
|
|
@ -38,6 +39,8 @@ def run_migrations_online() -> None:
|
|||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
if statement := migration_role_statement(os.environ.get("HUB_CORE_MIGRATION_ROLE")):
|
||||
connection.exec_driver_sql(statement)
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
|
|
|||
12
hub_core/migrations/roles.py
Normal file
12
hub_core/migrations/roles.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def migration_role_statement(role: str | None) -> str | None:
|
||||
"""Return a safely quoted SET ROLE statement for an admitted owner role."""
|
||||
if not role:
|
||||
return None
|
||||
if not re.fullmatch(r"[a-z_][a-z0-9_]{0,62}", role):
|
||||
raise ValueError("HUB_CORE_MIGRATION_ROLE is not a safe PostgreSQL role name")
|
||||
return f'SET ROLE "{role}"'
|
||||
|
|
@ -22,6 +22,8 @@ from hub_core.runtime.models import (
|
|||
RegistryRegistration,
|
||||
)
|
||||
from hub_core.runtime.tables import (
|
||||
compat_api_keys,
|
||||
compat_hubs,
|
||||
runtime_audit_ledger,
|
||||
runtime_interaction_events,
|
||||
runtime_messages,
|
||||
|
|
@ -47,6 +49,13 @@ class PostgresPortStore:
|
|||
try:
|
||||
async with self.engine.connect() as connection:
|
||||
await connection.execute(sa.text("SELECT 1"))
|
||||
# A live connection alone can hide a mis-granted runtime lease.
|
||||
# Exercise the durable port, compatibility, and authorization
|
||||
# tables that protected traffic actually depends on.
|
||||
for table in (runtime_audit_ledger, compat_hubs, compat_api_keys):
|
||||
await connection.execute(
|
||||
sa.select(sa.literal(1)).select_from(table).limit(1)
|
||||
)
|
||||
except Exception:
|
||||
return {"database": "unavailable"}
|
||||
return {"database": "ok"}
|
||||
|
|
|
|||
12
tests/test_migration_environment.py
Normal file
12
tests/test_migration_environment.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from hub_core.migrations.roles import migration_role_statement
|
||||
|
||||
|
||||
def test_migration_role_is_quoted_and_validated() -> None:
|
||||
assert migration_role_statement("hub_runtime_owner") == 'SET ROLE "hub_runtime_owner"'
|
||||
assert migration_role_statement(None) is None
|
||||
with pytest.raises(ValueError, match="safe PostgreSQL role"):
|
||||
migration_role_statement('owner"; DROP SCHEMA public; --')
|
||||
|
|
@ -89,6 +89,22 @@ def test_postgresql_readiness_fails_when_database_is_unavailable() -> None:
|
|||
asyncio.run(store.aclose())
|
||||
|
||||
|
||||
def test_postgresql_readiness_fails_when_runtime_tables_are_unavailable(tmp_path) -> None:
|
||||
database_url = f"sqlite+aiosqlite:///{tmp_path / 'empty.db'}"
|
||||
store = PostgresPortStore.from_url(database_url)
|
||||
settings = RuntimeSettings(
|
||||
environment="production",
|
||||
backend="postgresql",
|
||||
allow_ephemeral=False,
|
||||
database_url=database_url,
|
||||
)
|
||||
response = TestClient(create_app(settings=settings, port_store=store)).get("/readyz")
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json()["checks"]["database"] == "unavailable"
|
||||
asyncio.run(store.aclose())
|
||||
|
||||
|
||||
def _event(event_type: str) -> dict:
|
||||
return {
|
||||
"schema_version": "0.1.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue