Files
compute/authz/authz_test.go
zeekayandHanzo Dev 3664ba419a a machine's agent is one noun at one address, and it is typed
Four routes spelled one resource three ways: POST .../bind-agent to create,
GET and DELETE .../agent-binding to read and remove, GET /v1/agent-bindings to
list. A caller had to learn that create lived at a different path from its own
read, and every projection published three names for one thing. A machine hosts
at most one agent, so it is a to-one sub-resource — singular at the member,
plural at the collection, the method carrying the verb:

	PUT    /v1/machines/:id/agent    bind (idempotent, so PUT)
	GET    /v1/machines/:id/agent    read, reconciled; 404 when unbound
	DELETE /v1/machines/:id/agent    unbind; 204, the machine stays
	GET    /v1/machines/agents       the caller org's bindings

That is the address hanzoai/cloud already publishes to its own callers, so the
translation it was keeping in bots.go disappears rather than moving.

All four are TYPED ops. Only a typed op is in the registry that OpenAPI, MCP,
the CLI and the by-name call plane read; an untyped route is invisible to every
one of them. Typing them drops the casibase envelope with it — the answer IS
the value and the status IS the outcome — which is a wire break, so it lands
with its cloud caller in the same change.

An op declares the identity it reads: header:"Authorization" and url:"owner" on
the embedded caller, because a typed handler is given a context and no request
to reach into. principal() is now the ONE rule turning those two into an org,
and resolveComputeOrg calls it instead of restating it.

Not landed, and worth saying: the comment claiming a literal must be registered
ahead of its :param sibling is folklore. Moving registerAgent below the :id
routes leaves /v1/machines/agents still reaching ListAgents — fiber prefers the
static segment whatever the order. The test pins the outcome instead.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 00:28:15 -07:00

65 lines
2.7 KiB
Go

// Copyright 2025 Hanzo Industries Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package authz
import "testing"
// isResellComputePath decides which routes an authenticated brand user may reach
// (org-scoping is enforced downstream in the controller). These cases pin the
// exact resell-compute surface so a future edit can't silently widen it.
func TestIsResellComputePath(t *testing.T) {
allow := []struct{ method, path string }{
{"GET", "/v1/regions"},
{"GET", "/v1/sizes"},
{"GET", "/v1/gpus"},
{"GET", "/v1/machines"},
{"POST", "/v1/machines/launch"},
{"GET", "/v1/machines/abc-123"},
{"DELETE", "/v1/machines/abc-123"},
// A machine's agent — one address, the method carrying the verb.
{"GET", "/v1/machines/agents"},
{"GET", "/v1/machines/abc-123/agent"},
{"DELETE", "/v1/machines/abc-123/agent"},
{"PUT", "/v1/machines/abc-123/agent"},
}
for _, c := range allow {
if !isResellComputePath(c.method, c.path) {
t.Errorf("expected ALLOW for %s %s", c.method, c.path)
}
}
deny := []struct{ method, path string }{
{"POST", "/v1/regions"}, // catalog is read-only
{"POST", "/v1/machines"}, // no bulk create on the collection
{"DELETE", "/v1/machines/launch"}, // launch is POST-only
{"GET", "/v1/plans"}, // not a resell-compute route
{"POST", "/v1/start-session"}, // legacy surface, gated separately
{"GET", "/v1/get-account"},
{"PUT", "/v1/machines/abc-123"}, // only GET/DELETE by id
{"POST", "/v1/machines/abc-123"}, // no blanket POST by id
{"POST", "/v1/machines/abc-123/agent"}, // the bind is PUT, and only PUT
{"PUT", "/v1/machines/abc-123/tag"}, // PUT is admitted for /agent alone
{"POST", "/v1/machines/agents"}, // the binding list is read-only
{"PUT", "/v1/machines/agents"}, // …and is not a bind target
{"GET", "/v1/agent-bindings"}, // the old list address is gone
{"POST", "/v1/machines/abc-123/bind-agent"}, // the old bind address is gone
}
for _, c := range deny {
if isResellComputePath(c.method, c.path) {
t.Errorf("expected DENY for %s %s", c.method, c.path)
}
}
}