Files
zeekay 2b14fd1659 Compute: the module, the binary and the image say so
Cloud's side of this rename landed some time ago — the app is apps/compute,
the env is COMPUTE_URL — and left a note saying the other half moves with
the manifest that names it. This is the half that does not touch the
cluster: the Go module is github.com/hanzoai/compute, the binary it builds
is compute, and the image is ghcr.io/hanzoai/compute. GitHub already
carries the new name.

What is deliberately NOT here is the cutover. The Service universe declares
is still visor, and so are visor-config, visor-kms-sync and visor-db-bucket.
Pointing anything at compute.hanzo.svc before those move resolves to
nothing, so they move together, in universe, not here.
2026-09-07 17:37:50 -07:00

198 lines
7.1 KiB
Go

// Copyright 2024 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 (
"strings"
authz "github.com/hanzoai/authz"
"github.com/hanzoai/authz/model"
stringadapter "github.com/hanzoai/authz/persist/string-adapter"
"github.com/hanzoai/iamsdk/v2/iamsdk"
"github.com/hanzoai/compute/conf"
)
var Enforcer *authz.Enforcer
func InitAuthz() {
var err error
modelText := `
[request_definition]
r = subOwner, subName, method, urlPath, objOwner, objName
[policy_definition]
p = subOwner, subName, method, urlPath, objOwner, objName
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = (r.subOwner == p.subOwner || p.subOwner == "*") && (r.subName == p.subName || p.subName == "*") && \
(r.method == p.method || p.method == "*") && (r.urlPath == p.urlPath || p.urlPath == "*") && \
(r.objOwner == p.objOwner || p.objOwner == "*") && (r.objName == p.objName || p.objName == "*")
`
m, err := model.NewModelFromString(modelText)
if err != nil {
panic(err)
}
// The policy is STATIC (code-defined below) — it lives entirely in memory. No
// database (was a pointless Postgres api_rule persistence sink for hardcoded
// rules; platform rule: no SQL). One source of truth: the ruleText string adapter.
Enforcer, err = authz.NewEnforcer(m)
if err != nil {
panic(err)
}
if true {
ruleText := `
p, built-in, *, *, *, *, *
p, app, *, *, *, *, *
p, *, *, POST, /v1/signin, *, *
p, *, *, POST, /v1/signout, *, *
p, *, *, GET, /v1/get-account, *, *
p, *, *, GET, /v1/sessions/:owner/:name/connection, *, *
p, *, *, POST, /v1/assets/:owner/:name/sessions, *, *
p, *, *, POST, /v1/start-session, *, *
p, *, *, GET, /v1/get-whitelabel, *, *
p, *, *, GET, /v1/regions, *, *
p, *, *, GET, /v1/sizes, *, *
p, *, *, GET, /v1/gpus, *, *
`
sa := stringadapter.NewAdapter(ruleText)
// Load the static rules into the enforcer's memory. There is no DB to save
// them back to — the rules are the source of truth in code.
if err := sa.LoadPolicy(Enforcer.GetModel()); err != nil {
panic(err)
}
}
}
func IsAllowed(user *iamsdk.User, subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if conf.GetConfigBool("IsDemoMode") {
if !isAllowedInDemoMode(method, urlPath) {
return false
}
}
if subOwner == "app" {
return true
}
if user != nil {
if user.IsDeleted {
return false
}
// Resell compute surface (catalog + platform-account machines) is org-scoped
// IN THE CONTROLLER: resolveComputeOrg pins org = user.Owner for real
// users (a client-supplied ?owner is ignored), so a maxpower user can only
// ever list/launch/destroy maxpower's machines. Any authenticated user of
// this brand may therefore reach these routes; cross-org access is
// impossible regardless of objOwner. This is what lets a customer see and
// manage their OWN machines (not just browse the catalog) without the
// request having to carry a matching ?owner for the subOwner==objOwner rule.
if isResellComputePath(method, urlPath) {
return true
}
// A subject may act on its OWN org's objects. There is no second clause:
// this used to also return true whenever the OBJECT's owner was "admin",
// which admitted every authenticated customer to every object of the
// reserved SuperAdmin org — the object's owner is not a statement about the
// subject. A member of the admin org still passes, through this same
// subOwner == objOwner comparison, because that is what membership means.
if subOwner == objOwner {
return true
}
}
res, err := Enforcer.Enforce(subOwner, subName, method, urlPath, objOwner, objName)
if err != nil {
panic(err)
}
return res
}
// isResellComputePath reports whether (method, urlPath) is a resell-compute
// route whose org-scoping is enforced downstream in the controller
// (resolveComputeOrg). These are safe to admit for any authenticated user of the
// brand:
//
// GET /v1/regions|/v1/sizes|/v1/gpus catalog (also public-read)
// GET /v1/machines list the caller org's machines
// POST /v1/machines quote (dryRun) or metered launch
// GET /v1/machines/<org>/<name> get one of the caller org's machines
// DELETE /v1/machines/<org>/<name> destroy one of the caller org's machines
// GET /v1/machines/agents list the caller org's agent bindings
// GET /v1/machines/<org>/<name>/agent read a machine's agent binding
// DELETE /v1/machines/<org>/<name>/agent unbind a machine's agent
// PUT /v1/machines/<org>/<name>/agent bind a cloud Agent to a machine
func isResellComputePath(method string, urlPath string) bool {
switch urlPath {
case "/v1/regions", "/v1/sizes", "/v1/gpus":
return method == "GET"
case "/v1/machines":
// The collection: read it, or add to it. Adding is the launch — a quote
// when dryRun, a metered provision otherwise — which is why the POST that
// spends money is admitted here and every other write is not.
return method == "GET" || method == "POST"
}
// Agent↔machine binding: the bind is the ONE write admitted here, and only as
// PUT on the machine's own agent sub-resource. Every other write under
// /v1/machines/ stays denied — no blanket POST or PUT on the prefix.
if method == "PUT" && strings.HasPrefix(urlPath, "/v1/machines/") && strings.HasSuffix(urlPath, "/agent") {
return true
}
// /v1/machines/<org>/<name>, its /agent, and /v1/machines/agents — read or
// delete one machine, its binding, or the org's binding list.
if strings.HasPrefix(urlPath, "/v1/machines/") {
return method == "GET" || method == "DELETE"
}
return false
}
func isAllowedInDemoMode(method string, urlPath string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/v1/signin") || urlPath == "/v1/signout" || isAssetSessions(urlPath) || urlPath == "/v1/start-session" || urlPath == "/v1/stop-session" {
return true
} else {
return false
}
}
// If method equals GET
return true
}
// isAssetSessions reports whether urlPath is the address that opens a session on
// one asset: /v1/assets/{owner}/{name}/sessions, and nothing else.
//
// The SHAPE is matched, not a suffix. This address carries two parameters, so a
// literal comparison cannot express it — but a suffix can express far more than
// it should: "ends with /sessions" also admits /v1/sessions, which creates a
// session against ANY asset and is a different permission entirely.
func isAssetSessions(urlPath string) bool {
seg := strings.Split(strings.Trim(urlPath, "/"), "/")
return len(seg) == 5 && seg[0] == "v1" && seg[1] == "assets" && seg[4] == "sessions"
}