Documentation
¶
Overview ¶
Package auth closes the Brevis interface behind an operator credential.
It exists for a concrete reason: in dev, an anonymous `POST /workflows/<slug>/trigger` answered 303 and started the pipeline. Anyone on the internet could run a `dbt build` that MERGEs into the warehouse. An orchestration interface is a remote control for the warehouse -- leaving it open is the same as publishing the terminal.
The scope is deliberately small: ONE operator credential, from the configuration. There is no user registry, no roles and no multi-user, because none of that exists in the product yet and inventing it here would be building the floor before the wall. What does exist has to be right: a slow derivation hash, a signed session, constant-time comparison.
All stdlib. `crypto/pbkdf2` landed in the standard library in Go 1.24, which removes the need for `x/crypto` for the one piece that was missing.
Index ¶
- Constants
- func Anonymous(ctx context.Context) bool
- func CheckPassword(hash, password string) bool
- func ClientKey(r *http.Request, header string) string
- func GenerateHash(password string) (string, error)
- func IntoContext(ctx context.Context, user string) context.Context
- func OperatorOf(ctx context.Context) string
- func Target(raw string) string
- type Credential
- type Gate
- func (p *Gate) Attempt(ctx context.Context, w http.ResponseWriter, source, user, password string) (Outcome, time.Duration)
- func (p *Gate) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (p *Gate) SignIn(w http.ResponseWriter, user, password string) bool
- func (p *Gate) SignOut(w http.ResponseWriter)
- type Limiter
- type Outcome
Constants ¶
const CodeSignInRequired = "sign_in_required"
CodeSignInRequired is the error code of every 401 the gate answers: a session is missing or has expired. The server speaks codes and never sentences; the app says it in the reader's language and goes to its login.
const CookieName = "brevis_session"
CookieName is the session cookie's name.
const LegacyCookieName = "brevis_sessao"
LegacyCookieName is what releases before 0.43 called the session cookie ("sessao" is Portuguese for "session"). It is only READ: a session signed in under it stays valid until its own expiry, so upgrading signs nobody out, and every new session is written under CookieName. shortcut: one session lifetime (SessionLifetime) after 0.43 every such cookie has expired; delete this and the two fallbacks below in the next release.
const NextParam = "next"
NextParam is the query parameter carrying where the operator was going.
It is a constant because it has two ends -- the redirect that writes it, in this file, and the login screen that reads it, in internal/api -- and they were once different words.
const SessionLifetime = 12 * time.Hour
SessionLifetime is how long a login lasts. A working shift: short enough that a tab forgotten on a laptop does not become permanent access, long enough not to ask for a password in the middle of an investigation.
const ( // (no trusted client-IP header): a guesser must not be able to keep the // operator out of the console for 15 minutes at a time. SharedKeyMaxLock = 30 * time.Second )
Variables ¶
This section is empty.
Functions ¶
func Anonymous ¶ added in v0.43.0
Anonymous reports that the console has a credential and this request has no valid session, on a path the Gate lets through without one. False on an open console (nobody is asked to sign in, so nobody is a stranger) and on every request that did sign in.
func CheckPassword ¶
CheckPassword compares the password against the hash in constant time.
Returns false -- and not an error -- for a malformed hash: the caller is on a login path, and the only safe answer there is "did not get in". The configuration error is caught at boot, by Credential.Validate.
func ClientKey ¶
ClientKey is the key an attempt is counted under: the address of the client, IPv6 reduced to its /64 (one subscriber holds a whole /64, so rotating inside it must not reset the counters). With header set (BREVIS_AUTH_CLIENT_IP_HEADER, for a deployment behind a proxy we trust) it is the LAST entry of that header -- the one our proxy appended; the first is whatever the client sent. Absent or invalid, it falls back to the connection's own address.
func GenerateHash ¶
GenerateHash produces the text that goes into the configuration, as `pbkdf2-sha256$<iterations>$<salt>$<key>`.
The format carries the iteration count with it because that number will change: when we double the cost a few years from now, old hashes have to keep verifying. A format that stores only the digest forces invalidating everyone.
func IntoContext ¶
IntoContext stores the request's operator. The layout uses it to decide whether to show the sign-out button -- an installation with no credential should not display a button that does nothing.
func OperatorOf ¶ added in v0.43.0
OperatorOf returns the request's operator, or empty when there is no session.
Types ¶
type Credential ¶
type Credential struct {
User string
Hash string
// Secret signs the session cookie. Changing it drops every session, which
// is the emergency lever when a leak is suspected.
Secret []byte
// ClientIPHeader names the header a trusted proxy fills with the client
// address (see ClientKey); empty uses the connection's address.
ClientIPHeader string
}
Credential is the installation's single operator, coming from the configuration.
func (Credential) Enabled ¶
func (c Credential) Enabled() bool
Enabled says whether a credential is configured.
func (Credential) Validate ¶
func (c Credential) Validate() error
Validate refuses a half-finished configuration.
Half configured is worse than nothing: whoever filled in the username believes they closed the door. Failing at boot is the only way that belief does not last until the incident.
type Gate ¶
type Gate struct {
Cred Credential
Next http.Handler
Login http.Handler // renders the login screen
Insecure bool // plain http: sends the cookie without the Secure flag
// Limit throttles Attempt; nil gets a default on first use.
Limit *Limiter
// Check verifies a password against the hash; nil is CheckPassword.
// Injectable so a test can count the hashes that run.
Check func(hash, password string) bool
// contains filtered or unexported fields
}
Gate wraps a handler, requiring a valid session.
The routes that need no session are few and explicit. Kubernetes probes are on that list out of necessity -- a /health that asks for a password kills the pod.
func (*Gate) Attempt ¶
func (p *Gate) Attempt(ctx context.Context, w http.ResponseWriter, source, user, password string) (Outcome, time.Duration)
Attempt is SignIn behind the Limiter, keyed by source (the client address). A Locked or Busy answer did not run the hash.
func (*Gate) SignIn ¶
func (p *Gate) SignIn(w http.ResponseWriter, user, password string) bool
SignIn checks the credential and writes the cookie. Returns false if it did not match.
func (*Gate) SignOut ¶
func (p *Gate) SignOut(w http.ResponseWriter)
SignOut deletes the cookie, under both names: a session that was signed in under the legacy one must end too.
type Limiter ¶
type Limiter struct {
// MaxLock caps one lockout; zero is 15 minutes.
MaxLock time.Duration
// contains filtered or unexported fields
}
Limiter bounds login attempts: a few hashes at a time, one per source, and an exponential lockout per source after repeated failures. In memory and per process; shortcut: with several replicas each keeps its own counters, so the effective limit is multiplied by the replica count -- move to a shared store if Brevis ever runs more than a couple.
func NewLimiter ¶
func NewLimiter() *Limiter
func (*Limiter) Do ¶
func (l *Limiter) Do(ctx context.Context, source string, check func() bool) (out Outcome, retry time.Duration)
Do runs check (the expensive hash) for source unless the source is locked, already has a hash running, or no slot frees up within slotWait, and records the result. retry is how long a Locked source must wait.