Skip to content
Closed
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
5 changes: 5 additions & 0 deletions scripts/spark-authz-e2e-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
config.json
venv/
.pytest_cache/
__pycache__/
*.pyc
84 changes: 84 additions & 0 deletions scripts/spark-authz-e2e-tests/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Spark AuthZ E2E tests for scale-agentex.
#
# Target conventions
# ------------------
# Targets are grouped so you can run a single test file, all tests for a
# resource, all tests for a logical category, or the whole suite. Future
# resources slot into one of these groups by adding test files and
# updating the matching aggregate target — no new flat targets unless
# the resource is novel.
#
# test all tests, every group
# test-direct-resources everything with its own SpiceDB type
# (agent, task, api_key, …)
# test-sub-resources everything that delegates to a parent
# (event, task_state, message, tracker, checkpoint)
# test-<resource> all cases for one resource
# (e.g. test-api-key, test-event)
# test-<resource>-<case> one case for one resource
# (e.g. test-api-key-create)
#
# When a parent grouping makes sense (e.g. "all task sub-resources"), add
# a test-<parent>-sub-resources target alongside the resource targets.

VENV ?= venv
PYTHON ?= python3
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest

# Test-file globs. Update these (NOT individual case targets) when adding
# a new test file for an existing resource.
EVENT_TESTS := tests/test_event_authz.py

# Aggregate groupings — extend these as resources are added.
# Direct-resource targets land with their own PRs (e.g. api_key).
SUB_RESOURCE_TESTS := $(EVENT_TESTS)
# Future sub-resource additions go here:
# SUB_RESOURCE_TESTS += $(STATE_TESTS) $(MESSAGE_TESTS) $(TRACKER_TESTS) $(CHECKPOINT_TESTS)

.PHONY: install test \
test-sub-resources \
test-event \
clean help

help:
@echo "scale-agentex Spark AuthZ E2E tests"
@echo ""
@echo "Setup:"
@echo " make install venv + deps (one-time)"
@echo ""
@echo "Run everything:"
@echo " make test all tests"
@echo ""
@echo "Run a logical group:"
@echo " make test-sub-resources resources that delegate to a parent"
@echo ""
@echo "Run one resource:"
@echo " make test-event all AGX1-331 cases"

install:
$(PYTHON) -m venv $(VENV)
$(PIP) install -r requirements.txt

test:
$(PYTEST) tests/ -v

# ---------------------------------------------------------------------------
# Logical groupings
# ---------------------------------------------------------------------------

test-sub-resources:
$(PYTEST) $(SUB_RESOURCE_TESTS) -v

# ---------------------------------------------------------------------------
# Sub-resources (delegate authz to a parent)
# ---------------------------------------------------------------------------

# AGX1-331 — events delegate to parent agent
test-event:
$(PYTEST) $(EVENT_TESTS) -v

clean:
rm -rf $(VENV) .pytest_cache __pycache__
find . -name __pycache__ -type d -exec rm -rf {} +
find . -name '*.pyc' -delete
183 changes: 183 additions & 0 deletions scripts/spark-authz-e2e-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Spark AuthZ E2E tests

End-to-end tests for FGAC on `scale-agentex` routes. Black-box: every test
hits the real `scale-agentex` HTTP API as one identity, then verifies the
resulting state in SpiceDB via a separate Spark-AuthZ client.

Modeled after the equivalent KB suite in
`scaleapi/packages/egp-api-backend/scripts/spark-authz-e2e-tests/` (PR
[#142983](https://github.com/scaleapi/scaleapi/pull/142983)).

## Scope

This PR establishes the e2e test scaffolding (clients, conftest, factories,
cleanup) and ships the first resource: **events**. Subsequent PRs add new
resource test files on top of this infrastructure (e.g. AGX1-325 api_keys
in a follow-up).

### AGX1-331 — `events` (read-only, parent-agent-delegated)

Routes: `GET /events/{id}` and `GET /events?task_id=...&agent_id=...`.

- Events have **no SpiceDB type of their own** — the check goes against the
parent `agent`.
- No public `POST /events` → the happy-path tests are skipped (see note in
`tests/test_event_authz.py`); only the denied paths are exercised, which
is what the ticket asks for.

### Scaffolding shipped with this PR

- `clients/agentex_client.py` — HTTP client for `scale-agentex` routes
(agent + api_key + event surfaces; api_key methods land here ahead of
AGX1-325's tests so the client doesn't grow in two places).
- `clients/spark_authz_client.py` — direct SpiceDB-state client (HTTP-
transcoded). Copied verbatim from the EGP suite — repo-agnostic.
- `conftest.py` — config loader, identity credentials, two `AgentexClient`
instances (user_a / user_b), a `SparkAuthzClient`, an `authz_reachable`
probe with graceful-skip semantics, `parent_agent` fixture that falls
back to a pre-existing `agentex.agent_id` when the test user lacks
`agent.create` on the tenant, function-scoped `cleanup` tracker.
- `helpers/cleanup.py` + `helpers/factories.py` — LIFO teardown +
unique-name generators.

## Setup

The suite does not spin up any services itself — it assumes the relevant
backends are already running (same model as the EGP suite this is mirrored
from). Three terminals + the test runner.

### Terminal 1 — `spark-authz` (authz server + Identity Service + SpiceDB)

```bash
cd ~/spark-authz
docker compose up
```

This brings up everything the authz layer needs in one shot: Postgres,
SpiceDB, Redis, schema migration, dev seed data, the `authz` server on
gRPC `50052` + HTTP `8090`, and `identity-service` on the port its compose
file binds. The suite talks to `localhost:8090` (HTTP-transcoded) for all
direct authz assertions.

### Terminal 2 — `agentex-auth` (the principal-resolution proxy)

```bash
cd ~/agentex/agentex-auth
# Start command depends on the repo's own dev-loop — see its README.
# Must be configured with IDENTITY_SERVICE_URL pointing at the one from
# Terminal 1, and SPARK_AUTHZ_URL pointing at localhost:8090.
```

`scale-agentex` forwards every request's headers to this service to resolve
the principal context (`user_id`, `service_account_id`, `account_id`).
Without it, `scale-agentex` 401s every request.

### Terminal 3 — `scale-agentex` itself

```bash
cd ~/scale-agentex/agentex
# uv run uvicorn ... — see agentex/Makefile for the exact dev target.
# Must be started with AGENTEX_AUTH_URL pointing at the agentex-auth from
# Terminal 2 (otherwise auth is bypassed and the assertions in this suite
# become meaningless).
```

### Terminal 4 — run the suite

```bash
cd ~/scale-agentex/scripts/spark-authz-e2e-tests
make install # one-time: venv + deps
cp config.json.example config.json # one-time
# Edit config.json — fill in real headers + identity_ids + account_id
# (see "Auth model" below for what those need to be).
make test # all tests
# See `make help` for the full list of targets, including logical groups
# (test-sub-resources) and per-resource targets.
```

### Minting credentials

The two `users` in `config.json` need to exist in Identity Service AND be
known to `agentex-auth`. The suite does **not** create them — they're
minted out-of-band, same as the EGP suite's `ssk_is_…` keys. Two paths:

- **Dev cluster**: grab existing dev API keys / bearer tokens for two real
users in the same account and paste them in. Easiest if you have them.
- **Local stack**: use the seed identities that `spark-authz`'s
`authz-dev-seed` container creates, or mint fresh ones via the local
Identity Service after Terminal 1 comes up.

`user_b` must **not** be pre-granted access to `user_a`'s resources — the
negative-path tests depend on user_b having no role on user_a's agent /
api_key by default.

## Run

Targets are grouped so you can run a single test file, all tests for one
resource, all tests in a logical category, or the whole suite.

```bash
# Everything
make test

# Logical groups
make test-sub-resources # resources that delegate to a parent (event, …)

# One resource (all cases)
make test-event # AGX1-331 — all event cases
```

Adding a new resource? Add a `<RESOURCE>_TESTS` variable in the Makefile,
append it to `DIRECT_RESOURCE_TESTS` or `SUB_RESOURCE_TESTS`, and add a
`test-<resource>` target. See the existing entries for the shape.

## When spark-authz isn't reachable

Some environments (e.g. `sgp-pubsec-dev`) run scale-agentex without the
spark-authz HTTP frontend — there's raw SpiceDB but no `:8090` REST surface
for direct permission assertions. The suite degrades gracefully:

- Tests that **only hit scale-agentex HTTP routes** (most of the suite) run
normally and assert on response codes + bodies.
- Tests that **assert directly against SpiceDB** skip with a clear reason
when `spark_authz.host` doesn't answer `/healthz` with 2xx. The event
suite this PR ships doesn't have any SpiceDB-asserting tests, but the
scaffolding is here so future resource PRs land cleanly.
- The factory cleanup falls back to REST-only when spark-authz isn't
reachable (no SpiceDB delete-resource call). Tuples may leak in this
mode, but routes are the unit under test.

To run the SpiceDB-asserting tests, either point `spark_authz.host` at a
reachable spark-authz instance or set up a port-forward to one.

## Layout

```
clients/
agentex_client.py # httpx wrapper for /agents + /agent_api_keys
spark_authz_client.py # httpx wrapper for spark-authz REST
helpers/
cleanup.py # LIFO cleanup tracker honoring config knobs
factories.py # unique_agent_name, unique_api_key_name
tests/
test_event_authz.py # AGX1-331: GET /events/{id} + /events denied paths
conftest.py # config, identities, clients, factories, cleanup
config.json.example # template — copy to config.json and fill in
```

## Auth model

`scale-agentex`'s middleware forwards request headers to `agentex-auth`,
which resolves them to a principal context (user_id, service_account_id,
account_id). The test client doesn't care what flavor of header the target
environment requires — drop whatever `agentex-auth` accepts into
`users.<key>.headers` in `config.json` and the client passes it through.

## Cleanup model

Every factory registers a teardown that **first** tries `DELETE` via the REST
route (exercises the dual-write deregister) and **then** issues
`SparkAuthzClient.delete_resource` as a fallback. The second call is
idempotent — `NOT_FOUND` is swallowed server-side — so it's safe to always
run, and it prevents owner-tuple leaks if the REST dual-write deregister
silently fails between tests.
Empty file.
Loading
Loading