Files
zeekay 6b39afc596 One machines collection, and pools read the address they are served on
There were two machine collections. One read a table visor rebuilds from the
organization's own provider credentials; the other read the cloud API on the
house account. They were never two kinds of thing: object.Machine and
service.Machine both key on (Owner, Name) and both carry Id as the provider's
id, so only the SOURCE differed — which is a property of a machine, not a
reason for a second address. A caller wanting "the machines I have" had to ask
both and join them, and one that asked once got a partial answer without being
told. GET /v1/machines now answers from both, tagged by source, registry
winning a collision.

The item is at /v1/machines/{owner}/{name}; adding to the collection is POST.
Six verb addresses and /v1/machines/launch retire to 410.

The node-pool family had the reverse problem: the routes moved to item
addresses in an earlier commit and the handlers kept reading the pool from the
body and the query, so delete and scale answered for whatever the body said.
Both now take the name from the address and the owner from the token, and scale
resolves the provider, cluster and upstream pool id from the row visor already
keeps — a caller that has to repeat them can contradict them.

AddMachine is deleted with the console button that called it: it inserted a row
no provider sync owns, and its address now means launch, which spends money.
GetMachines and LaunchMachine were unrouted.

Also here, because the tests found them:

- isResellComputePath keyed on /v1/machines/launch, so POST /v1/machines would
  have been denied for every authenticated user.
- A doc block for a handler that moved stayed behind and described the next
  function down, publishing UpdateMachine as GET /get-machine.
- The tenant and pool tests registered a shape routers.Route no longer serves,
  so they exercised the query fallback rather than the path the router uses.
  They now register what production registers and name the victim's org in the
  ADDRESS, which is the stronger form of the same attack.
- Two request helpers fixed the method, so an item table answered 404 and a 404
  reads as a refusal. One helper, method as a parameter.
- The launch tenant test proved its point through an error message that named
  an org; the handler now refuses earlier, for an unrelated reason. The subject
  is the resolver itself, which is the one place that decides, with a control
  covering both the query and the newer path vector.
- The unauthenticated control named an address that retires to 410, so it held
  no matter what the filter did.
- pkg/visor's bind raced its own TempDir: Close returns before the transport
  goroutine stops writing socket files. It now waits for the name to leave the
  plane.
2026-08-22 14:21:55 -07:00

52 lines
2.4 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
// What visor says when it will not answer.
//
// Each of these was written out at several call sites, so each was several
// slightly different sentences for one fact. They are values now, said the same
// way everywhere.
//
// A refusal names the MISSING FACT and where it comes from. "not configured"
// answers none of the three questions a caller actually has — is my request
// wrong, is my credential wrong, or is this deployment missing something? — and
// a caller who cannot tell those apart retries the one thing that will never
// work.
const (
// The request named no organization. Both ways of naming one are given,
// because which is right depends on who is asking: a person's token carries
// its own, a service's token does not and must say.
refuseNoOrg = "no organization in this request: send a bearer token whose subject carries one, or name it with ?owner="
// The DEPLOYMENT has no compute. Nothing the caller sends fixes this, which
// is why it does not read like a request error.
refuseNoCompute = "compute is not configured on this deployment: no cloud account is installed, so there is nothing to launch on"
// The ORGANIZATION has no cloud. This one the caller CAN fix, and the
// address that fixes it is named.
refuseNoProvider = "this organization has no cloud provider: add one at /v1/providers, then launch"
// A required parameter, with the values it takes.
refuseNoPool = "the pool is named by the address: /v1/pools/{owner}/{name}"
refuseNoProviderName = "name the provider: ?provider=<name>, one of the providers this organization has added"
// No credential at all.
refuseNotSignedIn = "not signed in: send a bearer token"
)