Files
Josh Siviour 0a1d837f40 feat(bedrock): gate Ochre models behind per-account access grants (#741)
* feat(bedrock): gate Ochre models behind per-account access grants

Self-host models were invokable by any authenticated account: tieredCatalog
returned them unconditionally and no runtime router checked the caller.
GetFoundationModel also described models the caller could not use.

Add a bedrock-model-access JetStream KV bucket holding presence-only grants,
resolved through a new AccessResolver. Access is deny-by-default: a gateway
with no store configured falls back to DenyAllAccessResolver, so an
unconfigured deployment serves nothing rather than everything. The system
account bypasses grants, matching how quotas exempt it.

All four runtime routers share one grantedCatalogEntry gate so they cannot
drift on who may invoke what. List and describe agree: an ungranted model is
absent from ListFoundationModels and reported as ResourceNotFoundException by
GetFoundationModel, so the error cannot confirm a model exists.

Administration lands on two surfaces: Grant/Revoke/ListModelAccess actions on
the spinifex gateway service for the UI, and `spx admin ochre access` for
operators and provisioning, which opens the bucket over NATS directly rather
than requiring a SigV4 client the CLI does not have.

* feat(bedrock): seed the admin account's model grants at gateway start

Deny-by-default left a fresh install with a catalog nobody could see, and the
first cut filled that gap from ansible — which is a dev tool, so any install
that never ran it came up unusable.

Seed from the product instead: on start awsgw grants the platform admin
account the full catalog, once, behind a marker key in the same bucket. The
marker makes it a bootstrap step rather than a policy: tenant accounts still
begin with no access, and an operator who revokes a grant does not have it
restored by the next restart. Grants are written before the marker so a crash
midway repeats the idempotent grants rather than leaving a half-seeded
account, and the marker is a conditional create so only the first node to
reach it need win.

Seeding is best-effort: a KV failure logs and leaves the gateway serving,
and the next start retries because the marker is written only on success.

* test(bedrock): cover the model-access CLI and gateway admin actions

Route the access CLI runners through the same loadConfigAndConnectFn /
ochreExit seam the weights runners use, so grant, revoke and list can be
driven against an embedded JetStream server, and add gateway tests for the
grant admin actions including the non-admin denial.
2026-08-06 16:17:10 +10:00

69 lines
2.0 KiB
Go

//go:build e2e
// Package bedrock is the Ochre (AWS-Bedrock) E2E suite. Hermetic API-shape and
// provider-translation parity (Anthropic Messages / vLLM OpenAI wire formats)
// is covered by the gateway_bedrock unit tests; this suite exercises
// ListFoundationModels/GetFoundationModel against any running cluster and
// gates real Converse calls on backend availability (self-host GPU or a live
// Anthropic key), so CI parity holds without requiring either to be present.
//
// Model access is deny-by-default, so the cluster's test account must hold
// grants or every assertion here reduces to "empty catalog, access denied".
// awsgw seeds the platform admin account with the full catalog on first
// start, so a suite running as that account needs no setup; running as a
// tenant account needs
// `spx admin ochre access grant --account-id <id> --all-models` first.
package bedrock
import (
"os"
"sync"
"testing"
"github.com/mulgadc/spinifex/tests/e2e/harness"
)
// Package-scoped singleton fixture, built lazily by requireBedrockFixture and
// torn down by TestMain after the run.
var (
pkgFixOnce sync.Once
pkgFix *Fixture
pkgFixErr error
)
// Fixture carries per-process state shared across every Test* in this package.
type Fixture struct {
Env *harness.Env
AWS *harness.AWSClient
Account string
}
func TestMain(m *testing.M) {
os.Exit(m.Run())
}
// requireBedrockFixture returns the package-scoped Fixture, building it on
// first call. Skips the calling test if SPINIFEX_E2E is unset.
func requireBedrockFixture(t *testing.T) *Fixture {
t.Helper()
pkgFixOnce.Do(func() {
if os.Getenv("SPINIFEX_E2E") == "" {
return
}
env := harness.LoadEnv(t)
awsCli := harness.NewAWSClient(t, env)
pkgFix = &Fixture{
Env: env,
AWS: awsCli,
Account: harness.IAMAccountID(t, awsCli),
}
})
if pkgFixErr != nil {
t.Fatalf("bedrock fixture init failed: %v", pkgFixErr)
}
if pkgFix == nil {
t.Skip("bedrock fixture unavailable (SPINIFEX_E2E unset)")
}
return pkgFix
}