a73x

internal/server/api/principal.go

Ref:   Size: 2.3 KiB   History

package api

import (
	"context"
	"net/http"

	"github.com/a73x/eitri/internal/server/store"
)

// Principal is the authenticated actor attached to every user-API request by
// userAuth. It is the F3 seam: userAuth resolves a PAT or session cookie to the
// credential's tenant; handlers consume the Principal without caring how it was
// resolved. Every credential is scoped to exactly one tenant — there is no
// fleet-wide principal and no fleet-wide user-facing operation (spec §3/§4).
type Principal struct {
	// Tenant is the tenant scope this principal acts within.
	Tenant string
}

// principalKey is the context key for the request principal.
type principalKey struct{}

// withPrincipal returns a request context carrying p.
func withPrincipal(ctx context.Context, p Principal) context.Context {
	return context.WithValue(ctx, principalKey{}, p)
}

// principalFromContext returns the request's principal. A request that never
// passed userAuth yields the zero Principal — no tenant — so every check
// downstream fails closed.
func principalFromContext(r *http.Request) Principal {
	p, _ := r.Context().Value(principalKey{}).(Principal)
	return p
}

// TenantFromContext returns the tenant of the request's authenticated
// principal, or "" if the request never passed UserAuth. It is how a handler
// mounted outside the /api/v1 subtree — /mcp — reads the identity the auth
// middleware resolved.
func TenantFromContext(ctx context.Context) string {
	p, _ := ctx.Value(principalKey{}).(Principal)
	return p.Tenant
}

// mayActAs reports whether p may act on a resource owned by tenant. Strict
// scope-equality; an empty tenant on either side never matches (resources always
// have a tenant; a zero principal must not pair with a malformed resource via
// "" == "").
func mayActAs(p Principal, tenant string) bool {
	return tenant != "" && p.Tenant == tenant
}

func filterVMs(p Principal, vms []store.VM) []store.VM {
	out := make([]store.VM, 0, len(vms))
	for _, vm := range vms {
		if mayActAs(p, vm.Tenant) {
			out = append(out, vm)
		}
	}
	return out
}

// filterHosts is filterVMs for hosts.
func filterHosts(p Principal, hosts []store.Host) []store.Host {
	out := make([]store.Host, 0, len(hosts))
	for _, h := range hosts {
		if mayActAs(p, h.Tenant) {
			out = append(out, h)
		}
	}
	return out
}