diff --git a/plugins/nemo-insights/testbed/ingest.py b/plugins/nemo-insights/testbed/ingest.py index cfaa2a7171..b956f59292 100644 --- a/plugins/nemo-insights/testbed/ingest.py +++ b/plugins/nemo-insights/testbed/ingest.py @@ -127,7 +127,7 @@ def create_experiment( 409 here is a genuine collision and is surfaced as ``RuntimeError`` along with every other non-2xx. No auth header. """ - url = f"{base_url.rstrip('/')}/apis/intake/v2/workspaces/{workspace}/experiments" + url = f"{base_url.rstrip('/')}/apis/intake/v2/workspaces/{workspace}/evaluations" body = { "name": name, "experiment_group_id": experiment_group_id, diff --git a/plugins/nemo-insights/tests/testbed/test_ingest.py b/plugins/nemo-insights/tests/testbed/test_ingest.py index 8ad4bb2c41..cefd4fd3d8 100644 --- a/plugins/nemo-insights/tests/testbed/test_ingest.py +++ b/plugins/nemo-insights/tests/testbed/test_ingest.py @@ -185,7 +185,7 @@ def test_create_experiment_posts_full_body(): assert stub.calls == [ ( "POST", - "http://x/apis/intake/v2/workspaces/ws/experiments", + "http://x/apis/intake/v2/workspaces/ws/evaluations", { "name": "tau2-airline-20260626-000000-abcd", "experiment_group_id": "grp-1", diff --git a/services/core/auth/src/nmp/core/auth/assets/static-authz.yaml b/services/core/auth/src/nmp/core/auth/assets/static-authz.yaml index 3ccf30203e..13133a13e8 100644 --- a/services/core/auth/src/nmp/core/auth/assets/static-authz.yaml +++ b/services/core/auth/src/nmp/core/auth/assets/static-authz.yaml @@ -1003,66 +1003,6 @@ authz: scopes: - intake:write - platform:write - /apis/intake/v2/workspaces/{workspace}/experiments: - get: - permissions: - - intake.evaluations.read - scopes: - - intake:read - - platform:read - x-not-in-openapi: true - post: - permissions: - - intake.evaluations.create - scopes: - - intake:write - - platform:write - x-not-in-openapi: true - /apis/intake/v2/workspaces/{workspace}/experiments/{name}: - delete: - permissions: - - intake.evaluations.delete - scopes: - - intake:write - - platform:write - x-not-in-openapi: true - get: - permissions: - - intake.evaluations.read - scopes: - - intake:read - - platform:read - x-not-in-openapi: true - put: - permissions: - - intake.evaluations.update - scopes: - - intake:write - - platform:write - x-not-in-openapi: true - /apis/intake/v2/workspaces/{workspace}/experiments/{name}/pin: - delete: - permissions: - - intake.evaluations.update - scopes: - - intake:write - - platform:write - x-not-in-openapi: true - post: - permissions: - - intake.evaluations.update - scopes: - - intake:write - - platform:write - x-not-in-openapi: true - /apis/intake/v2/workspaces/{workspace}/experiments/{name}/sessions: - get: - permissions: - - intake.evaluations.read - scopes: - - intake:read - - platform:read - x-not-in-openapi: true /apis/intake/v2/workspaces/{workspace}/ingest/atif: post: permissions: diff --git a/services/core/auth/tests/test_embedded_pdp.py b/services/core/auth/tests/test_embedded_pdp.py index 17bcba7e40..b6c1d0cc7a 100644 --- a/services/core/auth/tests/test_embedded_pdp.py +++ b/services/core/auth/tests/test_embedded_pdp.py @@ -192,39 +192,6 @@ def test_authenticated_user_with_permission(self, minimal_authz_data): ) assert result["allowed"] is True - def test_legacy_experiments_url_alias_authorized_through_pdp(self, static_authz_data): - """The deprecated /experiments alias must authorize through the real bundle (not just - FastAPI's router): a normal user with intake.evaluations.* is allowed, exactly like - /evaluations, and a read-only user is denied — proving it's real authz, not a bypass. - - This guards the class of bug where hidden router aliases are absent from the - OpenAPI-derived auth bundle and therefore fail-closed for non-service principals. - """ - static_authz_data["authz"]["principals"] = { - "editor@example.com": {"workspaces": {"ws1": ["Editor"]}}, - "viewer@example.com": {"workspaces": {"ws1": ["Viewer"]}}, - } - set_policy_data(static_authz_data) - - # Editor (has intake.evaluations.create) may POST to both the canonical and the alias URL. - for path in ( - "/apis/intake/v2/workspaces/ws1/evaluations", - "/apis/intake/v2/workspaces/ws1/experiments", - ): - result = evaluate("allow", {"principal_id": "editor@example.com", "path": path, "method": "POST"}) - assert result["allowed"] is True, f"editor should be allowed to POST {path}" - - # Viewer (read-only) is denied POST to the alias — real permission check, not a blanket allow. - denied = evaluate( - "allow", - { - "principal_id": "viewer@example.com", - "path": "/apis/intake/v2/workspaces/ws1/experiments", - "method": "POST", - }, - ) - assert denied["allowed"] is False - def test_service_principal_bypass(self, minimal_authz_data): set_policy_data(minimal_authz_data) result = evaluate( diff --git a/services/intake/README.md b/services/intake/README.md index 84369d6136..fc02964f24 100644 --- a/services/intake/README.md +++ b/services/intake/README.md @@ -104,7 +104,7 @@ Seed an Experiment rollup and read it back: ```bash uv run services/intake/scripts/spans/seed_experiment_rollup_data.py -curl -s "http://127.0.0.1:8000/apis/intake/v2/workspaces/default/experiments/rollup-smoke-exp" | jq +curl -s "http://127.0.0.1:8000/apis/intake/v2/workspaces/default/evaluations/rollup-smoke-exp" | jq # Optional larger local workload. uv run services/intake/scripts/spans/seed_experiment_rollup_data.py \ diff --git a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py index 96d9ecfeb0..193692d026 100644 --- a/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py +++ b/services/intake/src/nmp/intake/api/v2/experiments/endpoints.py @@ -18,7 +18,6 @@ from typing import Annotated, Any, Literal, NamedTuple, TypeVar from fastapi import APIRouter, Depends, HTTPException, Query, Request, status -from fastapi.routing import APIRoute from nmp.common.api.common import Page, PaginationData from nmp.common.api.filter import ComparisonOperation, FilterOperation, FilterOperator, LogicalOperation from nmp.common.api.parsed_filter import ParsedFilter, make_filter_dep @@ -1423,23 +1422,3 @@ def _aggregate(rollup: ScoreRollup) -> EvaluatorAggregate: p99=rollup.p99, count=rollup.count, ) - - -# --------------------------------------------------------------------------- -# Backwards-compatible URL aliases (TEMPORARY — remove in a follow-up PR) -# -# The child endpoints moved from `/experiments` to `/evaluations`. Register the old -# `/experiments...` paths as hidden aliases (``include_in_schema=False``) that point at the -# same handlers, so existing callers keep working until they migrate to `/evaluations`. -# --------------------------------------------------------------------------- -for _legacy_route in list(router.routes): - if isinstance(_legacy_route, APIRoute) and "/evaluations" in _legacy_route.path: - router.add_api_route( - _legacy_route.path.replace("/evaluations", "/experiments", 1), - _legacy_route.endpoint, - methods=sorted(_legacy_route.methods), - response_model=_legacy_route.response_model, - status_code=_legacy_route.status_code, - include_in_schema=False, - name=f"{_legacy_route.name}_experiments_alias", - ) diff --git a/services/intake/tests/integration/test_experiments_crud.py b/services/intake/tests/integration/test_experiments_crud.py index b9a0aca778..d0951e3a9e 100644 --- a/services/intake/tests/integration/test_experiments_crud.py +++ b/services/intake/tests/integration/test_experiments_crud.py @@ -13,8 +13,6 @@ GROUPS = "/apis/intake/v2/workspaces/default/experiment-groups" EVALUATIONS = "/apis/intake/v2/workspaces/default/evaluations" -# Deprecated URL alias: the child endpoints moved /experiments -> /evaluations. -EXPERIMENTS_ALIAS = "/apis/intake/v2/workspaces/default/experiments" def _evaluation_body(*, experiment_group_id: str, **overrides: Any) -> dict: @@ -220,39 +218,6 @@ def test_delete_group_cascades_to_evaluations(client: TestClient) -> None: assert missing.status_code == 404 -def test_legacy_experiments_url_alias_still_works(client: TestClient) -> None: - """The child endpoints moved /experiments -> /evaluations; the old paths remain as hidden aliases.""" - group = _create_group(client, name="legacy-alias-group") - - # Create via the deprecated /experiments URL. - created = client.post( - EXPERIMENTS_ALIAS, - json=_evaluation_body(name="legacy-alias-eval", experiment_group_id=group["id"]), - ) - assert created.status_code == 201, created.text - assert created.json()["name"] == "legacy-alias-eval" - - # The deprecated URL and the canonical URL resolve to the same entity. - via_legacy = client.get(f"{EXPERIMENTS_ALIAS}/legacy-alias-eval") - via_canonical = client.get(f"{EVALUATIONS}/legacy-alias-eval") - assert via_legacy.status_code == 200, via_legacy.text - assert via_canonical.status_code == 200 - assert via_legacy.json() == via_canonical.json() - - -def test_legacy_experiments_url_aliases_are_hidden_from_schema() -> None: - from fastapi.routing import APIRoute - from nmp.intake.api.v2.experiments.endpoints import router - - alias_routes = [ - route - for route in router.routes - if isinstance(route, APIRoute) and "/experiments" in route.path and "/experiment-groups" not in route.path - ] - assert alias_routes, "expected legacy /experiments alias routes to be registered" - assert all(route.include_in_schema is False for route in alias_routes) - - def test_deprecated_field_aliases_are_backwards_compatible(client: TestClient) -> None: """Renamed fields keep deprecated aliases: requests accept the old name, responses return both.""" group = _create_group(client, name="alias-compat-group")