api

package
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package api serves Hecate's HTTP API over pkg/ops.

It holds no rules of its own — eligibility, windows, what counts as approved are all pkg/ops' (D32) — and it holds no identity of its own either. A caller presents the Kubernetes credentials they already have, and Hecate asks the Kubernetes API server both who they are and whether they may do the thing.

That is the whole authorisation design. It is not a shortcut: Hecate is a Kubernetes controller whose objects are Kubernetes objects, and a second permission model over them would be a second answer to "may this person promote to production" — with the two disagreeing eventually and silently.

Index

Constants

View Source
const ClusterLabel = "hecate.dev/cluster"

ClusterLabel marks a Secret as holding a remote cluster's kubeconfig.

A label rather than a naming convention: names are chosen by whoever created the Secret and cannot be queried, while a label selector is an indexed server- side filter. It also means an operator can adopt a kubeconfig Secret they created by hand simply by labelling it.

View Source
const SessionCookie = "hecate_session"

SessionCookie holds the caller's ID token between requests.

Variables

View Source
var (
	// ActionRead is reading any of the four resources.
	ActionRead = Action{Verb: "list", Resource: "gates"}
	// ActionPromote is asking a Gate to cross a Bundle: it creates a Passage.
	ActionPromote = Action{Verb: "create", Resource: "passages"}
	// ActionApprove is approving a Bundle for a Gate. A distinct resource from
	// the one promoting writes, which is what makes four-eyes real.
	ActionApprove = Action{Verb: "update", Resource: "bundles/status"}
	// ActionAbort stops a running Passage.
	ActionAbort = Action{Verb: "update", Resource: "passages"}
	// ActionOperateFlux suspends, resumes or reconciles a Flux resource a Gate
	// watches.
	//
	// Its own action, and deliberately not folded into promoting. Suspending a
	// Kustomization stops every future deploy of it and is state git will not
	// restore, so it outlives whoever did it — that is a bigger right than
	// asking a Gate to cross a Bundle, which git can undo and which leaves a
	// Passage saying who asked.
	//
	// Checked against Flux's own resource rather than a Hecate one, because
	// that is what is actually being written: someone who may patch
	// Kustomizations has this right already, and someone who may not should not
	// gain it by having a Hecate role.
	ActionOperateFlux = Action{
		Verb: "patch", Group: "kustomize.toolkit.fluxcd.io", Resource: "kustomizations",
	}

	// ActionBindRole grants someone a Hecate role. Checked against the caller
	// because hecate-api writes with its own ServiceAccount: Kubernetes'
	// built-in escalation prevention compares the *writer's* rights, and the
	// writer here is the server, not the person clicking. Without this check
	// anyone who could reach the API could grant themselves anything the server
	// can grant.
	ActionBindRole = Action{Verb: "create", Resource: "clusterrolebindings", Group: "rbac.authorization.k8s.io"}
	// ActionManageSecrets covers cluster credentials, which are kubeconfigs and
	// therefore the keys to another cluster.
	ActionManageSecrets = Action{Verb: "create", Resource: "secrets", Group: coreGroup}
	// ActionEditGate is changing a Gate's own configuration — the evidence
	// server it trusts, most of all.
	ActionEditGate = Action{Verb: "update", Resource: "gates"}
	// ActionPoll asks a Beacon to look at its sources now. A separate verb
	// from reading, so a CI job's identity can be allowed to poke a Beacon
	// without being able to read every Gate in the namespace — which is the
	// whole grant a webhook needs.
	ActionPoll = Action{Verb: "update", Resource: "beacons"}
)
View Source
var ErrUnauthenticated = errors.New("no valid credentials")

ErrUnauthenticated means no usable credential was presented.

Functions

This section is empty.

Types

type Action

type Action struct {
	Verb     string
	Resource string
	// Group is the API group the resource lives in. Empty means hecate.dev,
	// which is every Action that existed before settings could write anything —
	// so the default keeps those unchanged rather than making each restate the
	// group it always had.
	//
	// The core group is spelled coreGroup rather than "", because "" here would
	// be indistinguishable from "unset" and would silently authorise against
	// hecate.dev instead. Getting that wrong means checking the wrong resource
	// and allowing a write nobody was granted.
	Group string
}

Action is one thing a caller might be allowed to do.

The mapping to Kubernetes verbs is the point rather than an implementation detail: #74 requires that the right to *cross* and the right to *approve* be separable, because an approval a promoter can grant themselves is not an approval. They are different verbs on different resources, so a Role can carry one without the other — and nothing here has to enforce that, because the API server already does.

type Authenticator

type Authenticator struct {
	// Client is Hecate's own client, used to create TokenReviews and
	// SubjectAccessReviews. It needs `create` on those two and nothing more:
	// Hecate asks whether the caller may act, it does not act as them.
	Client client.Client
}

Authenticator turns a request into a Subject, and answers whether that Subject may perform an Action.

func (*Authenticator) Authenticate

func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (Subject, error)

Authenticate identifies the bearer of a request.

func (*Authenticator) Authorize

func (a *Authenticator) Authorize(ctx context.Context, s Subject, act Action, namespace string) error

Authorize asks the Kubernetes API server whether the subject may act.

Asked per request rather than cached. A cache here would mean a revoked permission still worked for its lifetime, and the whole point of deferring to Kubernetes is that its answer is the current one.

type BadRequest added in v0.2.0

type BadRequest struct{ Reason string }

writeOpsError maps an operations failure onto a status code.

A refusal is not a malfunction: "this Bundle has not cleared staging" is an answer, and a client should be able to tell it from a server that broke. BadRequest is a request the caller can fix by sending different input.

Distinct from ops.IsRefused, which means the request was fine and the state of the system said no. Conflating them tells someone to change their input when the input was never the problem.

func (*BadRequest) Error added in v0.2.0

func (e *BadRequest) Error() string

type ClusterTarget added in v0.2.0

type ClusterTarget struct {
	// Secret holding the kubeconfig, as "namespace/name".
	Secret string `json:"secret"`
	// Gates using it. Empty is not an error — a cluster can be connected before
	// any Gate references it, and saying so is more useful than hiding it,
	// which is what listing only Gate-referenced clusters used to do: you could
	// store a kubeconfig and watch nothing appear.
	Gates []string `json:"gates"`
	// Reachable is whether the credentials in the Secret actually answer.
	//
	// Checked here rather than left until a promotion needs it. A kubeconfig
	// that has expired, or names an endpoint this cluster cannot route to,
	// looks identical to a working one right up to the moment a Gate is waiting
	// on it — and that is the moment when nobody wants to be debugging
	// credentials.
	Reachable bool `json:"reachable"`
	// Detail says why when Reachable is false.
	Detail string `json:"detail,omitempty"`
}

ClusterTarget is a remote cluster this installation knows about.

type FidesTarget added in v0.2.0

type FidesTarget struct {
	ServerURL string `json:"serverURL"`
	// Gates naming this server, as "namespace/name".
	Gates []string `json:"gates"`
	// Environments this server is asked about, as UUIDs — the same value that
	// appears in the Fides UI, so the two can be lined up.
	Environments []string `json:"environments,omitempty"`
	// Reachable is the result of actually asking, not of the URL looking
	// plausible. A misconfigured evidence server is indistinguishable from a
	// working one until a promotion needs it, which is the worst moment to find
	// out.
	Reachable bool `json:"reachable"`
	// Detail says why when Reachable is false.
	Detail string `json:"detail,omitempty"`
}

FidesTarget is one evidence server and the Gates that use it.

type Forbidden

type Forbidden struct {
	Subject   Subject
	Action    Action
	Namespace string
	Reason    string
}

Forbidden is a caller who is known but not permitted.

func (*Forbidden) Error

func (f *Forbidden) Error() string

type HomeCluster added in v0.5.0

type HomeCluster struct {
	// InCluster is whether Hecate is running inside Kubernetes at all, rather
	// than against a kubeconfig on someone's laptop.
	InCluster bool `json:"inCluster"`
	// Server is the API server address, as this process reaches it. Inside a
	// cluster that is the service address rather than the public endpoint,
	// which is honest — it is what Hecate uses.
	Server string `json:"server,omitempty"`
	// Namespace is where Hecate itself is installed.
	Namespace string `json:"namespace,omitempty"`
}

HomeCluster is the cluster this process is running in.

type Identity added in v0.2.0

type Identity struct {
	Name   string   `json:"name"`
	Groups []string `json:"groups,omitempty"`
}

Identity is the authenticated caller.

type Login

type Login struct {
	// contains filtered or unexported fields
}

Login runs the OIDC authorization-code flow and puts the resulting ID token in a session cookie.

**It adds no identity model, and that is the entire design.** Kubernetes can itself be configured to trust an OIDC issuer, and when it is, an ID token from that issuer *is* a valid Kubernetes bearer token. So Hecate does not verify who you are and then decide what you may do — it obtains a credential the cluster already understands and keeps asking the API server exactly the questions it asked before (see the package comment, and D32).

**The consequence, which has to be said plainly: the cluster must trust the same issuer.** If `kube-apiserver` is not configured with this issuer, every login will succeed and every request afterwards will be rejected — the token is real, and the cluster has no reason to believe it. The startup check below cannot detect that, because whether the API server trusts an issuer is not something it exposes. Hecate says so at startup rather than letting it be discovered one confused user at a time.

func NewLogin

func NewLogin(ctx context.Context, cfg LoginConfig) (*Login, error)

NewLogin performs OIDC discovery and returns a configured login flow.

Discovery at startup rather than on first use: an unreachable or misspelt issuer should fail the rollout, where it is obvious, and not the first login, where it looks like the user's fault.

func (*Login) Routes

func (l *Login) Routes(mux *http.ServeMux)

Routes registers the login endpoints on a mux.

type LoginConfig

type LoginConfig struct {
	// Issuer is the OIDC provider, e.g. https://login.example.com. Discovery
	// happens at startup, so a wrong one fails the rollout rather than the
	// first login.
	Issuer string
	// ClientID and ClientSecret identify Hecate to the provider.
	ClientID     string
	ClientSecret string
	// RedirectURL is where the provider sends the browser back. It must be the
	// public URL of this server plus /auth/callback.
	RedirectURL string
	// Scopes beyond openid. `profile` and `email` are usual; `groups` is often
	// what carries the claim Kubernetes maps to groups.
	Scopes []string
	// Insecure allows the session cookie over plain HTTP. For local
	// development only — it is the difference between a token the network can
	// read and one it cannot.
	Insecure bool
}

LoginConfig is what an OIDC login needs.

type Server

type Server struct {
	Ops  *ops.Ops
	Auth *Authenticator
	// Version is reported at /healthz, so an operator can tell which build is
	// answering without reading the deployment.
	Version string
	// Login is the browser sign-in flow. Nil serves the API to callers who
	// already hold a Kubernetes token — which is every CLI and script, and is
	// why this is optional rather than required.
	Login *Login
}

Server is Hecate's HTTP API.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the routes.

Every route is authenticated, and every route that changes something is authorised for its own action — the reads and the writes do not share a permission, and promoting and approving do not share one either (#74).

type Settings added in v0.2.0

type Settings struct {
	// Version is the running build, so a bug report can name it.
	Version string `json:"version"`
	// Identity is who the API thinks you are, straight from the token. The most
	// common sign-in complaint is "it says I cannot do this and I should be
	// able to", and the first useful question is which account you arrived as.
	Identity Identity `json:"identity"`
	// Fides is every evidence server the visible Gates point at, with whether
	// it answers.
	Fides []FidesTarget `json:"fides"`
	// Clusters are the remote clusters Gates watch (#22).
	Clusters []ClusterTarget `json:"clusters"`
	// Telemetry is where traces go, if anywhere.
	Telemetry Telemetry `json:"telemetry"`
	// Home is the cluster Hecate itself runs in.
	//
	// Reported because the screen listing "connected clusters" was read three
	// times as saying no cluster was connected, when Hecate was running inside
	// one and promoting into it. A panel that can only ever show the extra
	// clusters, and is empty on the installation everyone actually has, is a
	// panel that says "none" to the question people are asking.
	Home HomeCluster `json:"home"`
}

Settings is what the settings screen shows.

**Derived from cluster state, not from configuration.** The obvious alternative is to plumb the chart's values through to this process and report them, and it would be wrong: values say what someone intended, and a Gate says what is actually in force. When they disagree — a Gate overriding serverURL, a chart upgraded but not applied — the honest answer is the one the controller will act on.

type Subject

type Subject struct {
	Name   string
	Groups []string
}

Subject is who is making a request.

func (Subject) String

func (s Subject) String() string

type Telemetry added in v0.2.0

type Telemetry struct {
	Endpoint   string `json:"endpoint,omitempty"`
	Configured bool   `json:"configured"`
}

Telemetry is the OpenTelemetry export configuration.

Hecate exports spans and stores none, so there is nothing here to browse. Reporting where they go is the useful thing a settings page can do: it turns "is tracing on?" into a question with an answer, and gives the address to open the collector's own UI at.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL