Publish curated ITWorx-ModelForge source from 990401737f96c8d46a9f7b0d84994516cadcb6a0
This commit is contained in:
1 parent
7082ab955a
commit
848f0d8f8a
434 files changed
+67298
-4315
No files matched your search
@@ -0,0 +1,252 @@
|
||||
"""Read-only, bounded audit timeline query contract regressions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import SecretStr
|
||||
from sqlalchemy import create_engine, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from modelforge_api.db import get_session
|
||||
from modelforge_api.main import app
|
||||
from modelforge_api.persistence.models import AuditEvent, Base
|
||||
from modelforge_api.services.audit import AuditContext, AuditWriter
|
||||
from modelforge_api.settings import Settings, get_settings
|
||||
|
||||
TIMELINE_PATH = "/api/v1/admin/audit/events"
|
||||
ADMIN_CREDENTIAL = "audit-timeline-test-admin"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session() -> Iterator[Session]:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as value:
|
||||
yield value
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_overrides() -> Iterator[None]:
|
||||
yield
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def _client(session: Session, role: str = "admin") -> tuple[TestClient, dict[str, str]]:
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
if role == "admin":
|
||||
settings = Settings(_env_file=None, operator_api_key=SecretStr(ADMIN_CREDENTIAL)) # type: ignore[call-arg]
|
||||
token = ADMIN_CREDENTIAL
|
||||
else:
|
||||
token = "mfop_" + "A" * 43
|
||||
identities = [
|
||||
{
|
||||
"credential_id": f"audit-{role}",
|
||||
"subject": f"audit-{role}",
|
||||
"role": role,
|
||||
"token_sha256": hashlib.sha256(token.encode()).hexdigest(),
|
||||
"expires_at": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
|
||||
}
|
||||
]
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
operator_identities=SecretStr(json.dumps(identities)),
|
||||
) # type: ignore[call-arg]
|
||||
app.dependency_overrides[get_settings] = lambda: settings
|
||||
return TestClient(app), {"X-ModelForge-Admin-Token": token}
|
||||
|
||||
|
||||
def _append(
|
||||
session: Session,
|
||||
*,
|
||||
principal: str = "alice",
|
||||
action: str = "MODEL_UPDATED",
|
||||
resource_type: str = "model",
|
||||
resource_id: str = "model-1",
|
||||
outcome: str = "success",
|
||||
correlation_id: str = "correlation-1",
|
||||
details: dict[str, object] | None = None,
|
||||
) -> AuditEvent:
|
||||
event = AuditWriter(
|
||||
session,
|
||||
context=AuditContext("operator", principal, correlation_id),
|
||||
).write(
|
||||
action,
|
||||
resource_type,
|
||||
resource_id,
|
||||
details or {"changed_fields": ["display_name"]},
|
||||
outcome=outcome,
|
||||
)
|
||||
session.commit()
|
||||
return event
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["viewer", "operator"])
|
||||
def test_timeline_requires_admin_human_authentication(session: Session, role: str) -> None:
|
||||
client, headers = _client(session, role)
|
||||
|
||||
assert client.get(TIMELINE_PATH).status_code == 401
|
||||
assert client.get(TIMELINE_PATH, headers=headers).status_code == 403
|
||||
assert (
|
||||
client.get(
|
||||
TIMELINE_PATH,
|
||||
headers={"Authorization": "Bearer mfsvc_project-scoped"},
|
||||
).status_code
|
||||
== 401
|
||||
)
|
||||
assert (
|
||||
client.get(
|
||||
TIMELINE_PATH,
|
||||
headers={"Authorization": "Bearer mfnode_node-scoped"},
|
||||
).status_code
|
||||
== 401
|
||||
)
|
||||
|
||||
|
||||
def test_timeline_applies_typed_filters_and_omits_untrusted_details(session: Session) -> None:
|
||||
_append(
|
||||
session,
|
||||
principal="alice-operator",
|
||||
action="NODE_ENROLLMENT_COMPLETED",
|
||||
resource_type="compute_node",
|
||||
resource_id="gpu_node-node-42",
|
||||
outcome="failure",
|
||||
correlation_id="corr-audit-42",
|
||||
details={
|
||||
"access_token": "mfpw_NEVER_RETURN_THIS",
|
||||
"nested": {"api-key": "NEVER_RETURN_THIS_EITHER", "changed": True},
|
||||
"comment": "authorization: Bearer mfpw_NEVER_RETURN_THIS",
|
||||
"operator_note": "opaque-credential-value-do-not-return-7f93",
|
||||
},
|
||||
)
|
||||
_append(
|
||||
session,
|
||||
principal="bob-operator",
|
||||
action="NODE_ENROLLMENT_COMPLETED",
|
||||
resource_type="compute_node",
|
||||
resource_id="gpu_node-node-43",
|
||||
outcome="success",
|
||||
correlation_id="corr-audit-43",
|
||||
)
|
||||
client, headers = _client(session)
|
||||
moment = datetime.now(UTC)
|
||||
response = client.get(
|
||||
TIMELINE_PATH,
|
||||
headers=headers,
|
||||
params={
|
||||
"principal": "alice",
|
||||
"action": "enrollment",
|
||||
"resource": "node-42",
|
||||
"outcome": "failure",
|
||||
"correlation_id": "corr-audit-42",
|
||||
"occurred_after": (moment - timedelta(minutes=1)).isoformat(),
|
||||
"occurred_before": (moment + timedelta(minutes=1)).isoformat(),
|
||||
"limit": 10,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert len(payload["events"]) == 1
|
||||
assert payload["events"][0]["actor_id"] == "alice-operator"
|
||||
assert payload["events"][0]["action"] == "NODE_ENROLLMENT_COMPLETED"
|
||||
assert "details" not in payload["events"][0]
|
||||
for secret_like in (
|
||||
"NEVER_RETURN_THIS",
|
||||
"NEVER_RETURN_THIS_EITHER",
|
||||
"opaque-credential-value-do-not-return-7f93",
|
||||
):
|
||||
assert secret_like not in response.text
|
||||
|
||||
|
||||
def test_timeline_cursor_pages_stably_when_new_events_are_appended(session: Session) -> None:
|
||||
seeded = [_append(session, principal=f"operator-{index}") for index in range(3)]
|
||||
client, headers = _client(session)
|
||||
|
||||
first = client.get(TIMELINE_PATH, headers=headers, params={"limit": 2})
|
||||
assert first.status_code == 200
|
||||
first_payload = first.json()
|
||||
assert [item["sequence"] for item in first_payload["events"]] == [
|
||||
seeded[2].sequence,
|
||||
seeded[1].sequence,
|
||||
]
|
||||
cursor = first_payload["next_cursor"]
|
||||
assert isinstance(cursor, str) and cursor
|
||||
|
||||
_append(session, principal="operator-new")
|
||||
second = client.get(
|
||||
TIMELINE_PATH,
|
||||
headers=headers,
|
||||
params={"limit": 2, "cursor": cursor},
|
||||
)
|
||||
assert second.status_code == 200
|
||||
second_payload = second.json()
|
||||
assert [item["sequence"] for item in second_payload["events"]] == [seeded[0].sequence]
|
||||
assert second_payload["next_cursor"] is None
|
||||
assert not (
|
||||
{item["id"] for item in first_payload["events"]}
|
||||
& {item["id"] for item in second_payload["events"]}
|
||||
)
|
||||
|
||||
mismatched_filters = client.get(
|
||||
TIMELINE_PATH,
|
||||
headers=headers,
|
||||
params={"limit": 2, "cursor": cursor, "principal": "another-principal"},
|
||||
)
|
||||
assert mismatched_filters.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
{"limit": 101},
|
||||
{"cursor": "not-a-valid-cursor"},
|
||||
{"principal": "p" * 256},
|
||||
{"action": "a" * 129},
|
||||
{"resource": "r" * 129},
|
||||
{"outcome": "o" * 33},
|
||||
{"correlation_id": "c" * 65},
|
||||
{"occurred_after": "2026-01-01T00:00:00Z"},
|
||||
{"occurred_after": "not-a-time", "occurred_before": "2026-01-02T00:00:00Z"},
|
||||
{
|
||||
"occurred_after": "2024-01-01T00:00:00Z",
|
||||
"occurred_before": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
"occurred_after": "2026-02-01T00:00:00Z",
|
||||
"occurred_before": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
{"unexpected_filter": "not-accepted"},
|
||||
],
|
||||
)
|
||||
def test_timeline_rejects_malformed_or_unbounded_query_input(
|
||||
session: Session, params: dict[str, str | int]
|
||||
) -> None:
|
||||
client, headers = _client(session)
|
||||
|
||||
response = client.get(TIMELINE_PATH, headers=headers, params=params)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_timeline_returns_a_typed_empty_page_and_does_not_append_audit_events(
|
||||
session: Session,
|
||||
) -> None:
|
||||
client, headers = _client(session)
|
||||
|
||||
response = client.get(TIMELINE_PATH, headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"events": [], "next_cursor": None, "limit": 50}
|
||||
assert session.scalar(select(func.count()).select_from(AuditEvent)) == 0
|
||||
Reference in new issue
Block a user