Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/nemo-insights/testbed/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion plugins/nemo-insights/tests/testbed/test_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 0 additions & 60 deletions services/core/auth/src/nmp/core/auth/assets/static-authz.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 0 additions & 33 deletions services/core/auth/tests/test_embedded_pdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion services/intake/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
21 changes: 0 additions & 21 deletions services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
35 changes: 0 additions & 35 deletions services/intake/tests/integration/test_experiments_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
Loading