Files
zooqueenandHanzo Dev e89a58f2ec A request naming an org the bearer is not a member of is refused, and a refused bearer never falls back to ?owner (red round 2, item 6).
The tenant was the named org when the signed membership held it and the home org otherwise, so a request aimed at another org was quietly answered from the caller's own. It now resolves to no org, and principal fails closed on any bearer that does not establish a user rather than taking the ?owner the caller named; only a call with no bearer reaches the service branch.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 05:57:49 -07:00

145 lines
6.0 KiB
Go

// Copyright 2023 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.
// Copyright 2026 Hanzo AI, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
package controllers
import (
"net/http"
"strings"
"testing"
"time"
"github.com/zap-proto/zip"
"github.com/hanzoai/compute/object"
)
// machineWire stands the machine collection up exactly as routers.Route does.
// The shape is the subject as much as the handlers are: the owner is a PATH
// segment on the item and a query on the collection, and reading it from the
// wrong one is the defect these tests are for.
func machineWire(t *testing.T) *zip.App {
t.Helper()
app := zip.New(zip.Config{ReadBufferSize: 16384})
h := func(fn func(*ApiController)) zip.Handler {
return func(c *zip.Ctx) error { fn(New(c)); return nil }
}
app.Get("/v1/machines", h((*ApiController).ListMachines))
app.Get("/v1/machines/:owner/:name", h((*ApiController).GetMachine))
return app
}
func storedMachine(t *testing.T, owner, name string) *object.Machine {
t.Helper()
now := time.Now().Format(time.RFC3339)
m := &object.Machine{
Owner: owner, Name: name, Id: "drop-" + name, Provider: "digitalocean",
Region: "sfo3", Size: "s-2vcpu-4gb", State: "running",
CreatedTime: now, DisplayName: name,
}
if _, err := object.AddMachine(m); err != nil {
t.Fatalf("AddMachine(%s/%s): %v", owner, name, err)
}
return m
}
// THE REGISTRY IS A MIRROR, NOT A STORE, and this is the test that says so.
//
// Measured: a read runs object.SyncMachinesCloud, which DELETES the org's rows
// and re-adds whatever its providers report. So an org with no active provider
// reads empty no matter what the table held a moment earlier — two rows written
// directly, as here, are gone by the time the answer is composed.
//
// That is worth a test rather than a surprise. It is also why "source" exists:
// the registry half is whatever the org's OWN credentials can see, and calling
// it a separate collection was always a description of where the rows came
// from, never of a second kind of machine.
func TestReadingRebuildsTheRegistryFromTheOrgsProviders(t *testing.T) {
mint := signer(t, "https://test.id")
app := machineWire(t)
storedMachine(t, "sourceorg", "web-1")
storedMachine(t, "sourceorg", "web-2")
body := get(t, app, "/v1/machines?owner=sourceorg", mint("sourceorg"))
if strings.Contains(body, "web-1") || strings.Contains(body, "web-2") {
t.Fatalf("a row survived a read with no provider to justify it: %s", body)
}
if !strings.Contains(body, `"status":"ok"`) {
t.Fatalf("an org with nothing to list is an empty answer, not an error: %s", body)
}
// Nothing may be tagged live either: there is no house account here, and a
// row appearing under that source would mean the tag was a default.
if strings.Contains(body, `"source":"live"`) {
t.Errorf("tagged a row live with no house account configured: %s", body)
}
}
// A COLLECTION WITH NO TENANT IS REFUSED rather than answered from whatever the
// store happens to hold. The item address cannot be in this state — it names an
// org — so the collection is the only place the question arises.
func TestListMachinesFailsClosedWithoutAnOrg(t *testing.T) {
if body := get(t, machineWire(t), "/v1/machines", ""); !strings.Contains(body, refuseNoOrg) {
t.Fatalf("a tenant-less list must be refused, got %s", body)
}
}
// THE ADDRESS NAMES WHICH MACHINE, AND ONLY AN ORG THE TOKEN'S MEMBERSHIP
// INCLUDES. A signed caller aiming the path at another org is refused outright —
// never the victim's row, and never quietly answered from its own org instead —
// while the same caller addressing its own org is looked up there.
func TestGetMachineIsScopedToTheCaller(t *testing.T) {
mint := signer(t, "https://test.id")
app := machineWire(t)
storedMachine(t, "victimread", "secret-box")
storedMachine(t, "attackerread", "own-box")
one := get(t, app, "/v1/machines/victimread/secret-box", mint("attackerread"))
if !strings.Contains(one, refuseNoOrg) || strings.Contains(one, "victimread") || strings.Contains(one, "drop-secret-box") {
t.Errorf("another org's machine was not refused: %s", one)
}
// A successful read is not available as a control here — the registry is
// rebuilt on read and this org has no provider — so the org NAMED in the
// refusal is what shows the lookup ran in the caller's own org.
own := get(t, app, "/v1/machines/attackerread/own-box", mint("attackerread"))
if !strings.Contains(own, "attackerread") || strings.Contains(own, "victimread") {
t.Errorf("its own machine was not looked up in its own org: %s", own)
}
}
// A MACHINE THAT IS NOT THERE SAYS SO, naming what was asked for and where it
// was looked for. "no machine x in y" is a sentence a caller can act on; an
// empty object is one it has to guess at.
func TestGetMachineSaysWhatIsMissing(t *testing.T) {
mint := signer(t, "https://test.id")
body := get(t, machineWire(t), "/v1/machines/absentorg/ghost", mint("absentorg"))
if !strings.Contains(body, "no machine ghost in absentorg") {
t.Fatalf("want a refusal naming the machine and the org, got %s", body)
}
}
// The collection is a GET and so is the item — one address shape, learned once.
// A POST reaching either read is a routing mistake, not a create.
func TestMachineReadsAreGets(t *testing.T) {
app := machineWire(t)
for _, path := range []string{"/v1/machines", "/v1/machines/acme/web-1"} {
if status, _ := ask(t, app, http.MethodPost, path, "", ""); status != http.StatusMethodNotAllowed &&
status != http.StatusNotFound {
t.Errorf("POST %s answered %d; the reads are GET only", path, status)
}
}
}