195 lines
6.6 KiB
Python
195 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
from services.control_api.persistence import create_engine_from_url, session_factory
|
|
from services.control_api.persistence.models import (
|
|
AuditEventRow,
|
|
EnvironmentRow,
|
|
ProjectRow,
|
|
WorkspaceRow,
|
|
)
|
|
from tests.test_wp230_rbac import ManagementRole, RoleSession
|
|
|
|
pytest_plugins = ("tests.test_wp230_rbac",)
|
|
|
|
|
|
def test_resource_search_matches_connector_slug(
|
|
role_sessions: dict[ManagementRole, RoleSession],
|
|
) -> None:
|
|
response = role_sessions[ManagementRole.VIEWER].get(
|
|
"/api/connectors", params={"q": "knowledge-default"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert [row["id"] for row in body["rows"]] == ["knowledge-default"]
|
|
|
|
|
|
def test_project_scoped_connector_search_uses_bindings_and_environment(
|
|
role_sessions: dict[ManagementRole, RoleSession],
|
|
) -> None:
|
|
viewer = role_sessions[ManagementRole.VIEWER]
|
|
response = viewer.get(
|
|
"/api/connectors",
|
|
params={
|
|
"project": "ludarium",
|
|
"environment": "prod",
|
|
"q": "knowledge-default",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
rows = response.json()["rows"]
|
|
assert len(rows) == 1
|
|
assert rows[0]["id"] == "knowledge-default"
|
|
assert rows[0]["project"] == "ludarium"
|
|
assert rows[0]["environment"] == "prod"
|
|
assert not any("url" in key.casefold() for key in rows[0])
|
|
|
|
unbound_project = viewer.get(
|
|
"/api/connectors",
|
|
params={
|
|
"project": "mobilityops",
|
|
"environment": "prod",
|
|
"q": "knowledge-default",
|
|
},
|
|
)
|
|
assert unbound_project.status_code == 200
|
|
assert unbound_project.json()["rows"] == []
|
|
|
|
|
|
def test_resource_search_matches_tool_canonical_name(
|
|
role_sessions: dict[ManagementRole, RoleSession],
|
|
) -> None:
|
|
response = role_sessions[ManagementRole.VIEWER].get(
|
|
"/api/tools", params={"q": "deployment.project.status"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert [row["canonicalName"] for row in body["rows"]] == ["deployment.project.status"]
|
|
|
|
|
|
def test_project_scoped_tool_search_uses_bindings_and_capability_packs(
|
|
role_sessions: dict[ManagementRole, RoleSession],
|
|
) -> None:
|
|
viewer = role_sessions[ManagementRole.VIEWER]
|
|
|
|
ludarium_tools = viewer.get(
|
|
"/api/tools",
|
|
params={"project": "ludarium", "limit": 100},
|
|
)
|
|
assert ludarium_tools.status_code == 200
|
|
ludarium_body = ludarium_tools.json()
|
|
ludarium_rows = ludarium_body["rows"]
|
|
assert ludarium_body["page"]["total"] == len(ludarium_rows)
|
|
ludarium_names = {row["canonicalName"] for row in ludarium_rows}
|
|
assert "deployment.project.status" in ludarium_names
|
|
assert "gitea.branch.delete" not in ludarium_names
|
|
assert all(row["project"] == "ludarium" for row in ludarium_rows)
|
|
assert all(row["environment"] == "prod" for row in ludarium_rows)
|
|
|
|
unbound_connector = viewer.get(
|
|
"/api/tools",
|
|
params={
|
|
"project": "mobilityops",
|
|
"environment": "prod",
|
|
"q": "deployment.project.status",
|
|
},
|
|
)
|
|
assert unbound_connector.status_code == 200
|
|
assert unbound_connector.json()["rows"] == []
|
|
|
|
environment_without_server = viewer.get(
|
|
"/api/tools",
|
|
params={
|
|
"project": "forgeflow",
|
|
"environment": "dev",
|
|
"q": "deployment.project.status",
|
|
},
|
|
)
|
|
assert environment_without_server.status_code == 200
|
|
assert environment_without_server.json()["rows"] == []
|
|
|
|
|
|
def test_audit_search_matches_trace_id_without_exposing_search_to_viewers(
|
|
role_sessions: dict[ManagementRole, RoleSession], db_path: Path
|
|
) -> None:
|
|
trace_id = f"operator-search-{uuid4()}"
|
|
engine = create_engine_from_url(f"sqlite:///{db_path}")
|
|
sessions = session_factory(engine)
|
|
with sessions() as session, session.begin():
|
|
workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one()
|
|
project = session.query(ProjectRow).filter_by(slug="ludarium").one()
|
|
environment = (
|
|
session.query(EnvironmentRow).filter_by(project_id=project.id, name="prod").one()
|
|
)
|
|
session.add(
|
|
AuditEventRow(
|
|
trace_id=trace_id,
|
|
workspace_id=workspace.id,
|
|
project_id=project.id,
|
|
environment_id=environment.id,
|
|
event_type="operator.search.fixture",
|
|
tool_name="operator.search.fixture",
|
|
policy_decision="must-not-be-a-search-field",
|
|
outcome="success",
|
|
)
|
|
)
|
|
engine.dispose()
|
|
|
|
auditor = role_sessions[ManagementRole.AUDITOR]
|
|
matching = auditor.get("/api/audit", params={"q": trace_id})
|
|
assert matching.status_code == 200
|
|
assert [row["traceId"] for row in matching.json()["rows"]] == [trace_id]
|
|
|
|
excluded_field = auditor.get("/api/audit", params={"q": "must-not-be-a-search-field"})
|
|
assert excluded_field.status_code == 200
|
|
assert excluded_field.json()["rows"] == []
|
|
|
|
denied = role_sessions[ManagementRole.VIEWER].get("/api/audit", params={"q": trace_id})
|
|
assert denied.status_code == 403
|
|
|
|
|
|
def test_servers_list_virtual_server_identities_and_apply_project_environment_scope(
|
|
role_sessions: dict[ManagementRole, RoleSession],
|
|
) -> None:
|
|
viewer = role_sessions[ManagementRole.VIEWER]
|
|
|
|
scoped = viewer.get(
|
|
"/api/servers",
|
|
params={"project": "forgeflow", "environment": "prod", "limit": 1},
|
|
)
|
|
assert scoped.status_code == 200
|
|
scoped_body = scoped.json()
|
|
assert scoped_body["page"]["total"] == 1
|
|
assert scoped_body["rows"] == [
|
|
{
|
|
"id": "forgeflow-prod-readonly",
|
|
"name": "forgeflow-prod-readonly",
|
|
"status": "unknown",
|
|
"summary": "Catalog registered; live runtime health has not been observed.",
|
|
"project": "forgeflow",
|
|
"environment": "prod",
|
|
"risk": None,
|
|
"canonicalName": None,
|
|
}
|
|
]
|
|
|
|
wrong_environment = viewer.get(
|
|
"/api/servers", params={"project": "forgeflow", "environment": "dev"}
|
|
)
|
|
assert wrong_environment.status_code == 200
|
|
assert wrong_environment.json()["page"]["total"] == 0
|
|
assert wrong_environment.json()["rows"] == []
|
|
|
|
wrong_project = viewer.get(
|
|
"/api/servers",
|
|
params={"project": "mobilityops", "environment": "prod", "q": "forgeflow-prod-readonly"},
|
|
)
|
|
assert wrong_project.status_code == 200
|
|
assert wrong_project.json()["page"]["total"] == 0
|
|
assert wrong_project.json()["rows"] == []
|