Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v0.5.5 Latest Latest
Warning

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

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

Documentation

Overview

Package auth provides JWT authentication, password hashing, the RBAC permission model, and login rate limiting for the control plane (ADR 0008).

Index

Constants

View Source
const (
	AgentTokenExpired       = "expired"
	AgentTokenWrongAudience = "wrong_audience"
	AgentTokenBadSignature  = "bad_signature"
	AgentTokenWrongIssuer   = "wrong_issuer"
	AgentTokenNotYetValid   = "not_yet_valid"
	AgentTokenMalformed     = "malformed"
	AgentTokenInvalid       = "invalid"
)

Reasons an agent bearer token is rejected (#1125). They are a closed set so a log line or a metric label built from one never carries anything derived from the token itself. Each points the operator at a different fault:

  • expired: the credential lapsed. A running attempt renews on every heartbeat, so this means the agent could not reach the control plane for a whole TTL, or its pod started after the credential minted at dispatch had already run out.
  • wrong_audience: a token minted for another surface (a user token) reached the agent service: a wiring bug.
  • bad_signature: the token was signed with another key (key rotation, or a token from another install) or with a method the server does not accept.
  • wrong_issuer, not_yet_valid, malformed: the token is not one this control plane minted as it stands.
  • invalid: any other verification failure.
View Source
const (
	// ScopeRead grants the read routes.
	ScopeRead = "dexaflow:read"
	// ScopeRun grants run-state writes: trigger, clear, mark, pause.
	ScopeRun = "dexaflow:run"
	// ScopeDeploy grants registering a DAG version.
	ScopeDeploy = "dexaflow:deploy"
)

The scopes a trusted-issuer bearer token may carry (ADR 0067). No scope implies another.

View Source
const APITokenPrefix = "dxf_"

APITokenPrefix starts every API token (ADR 0074). It tells an API token apart from a JWT without parsing it, and lets secret scanners recognize a leaked one.

View Source
const CISessionPrefix = "dxci_"

CISessionPrefix starts every CI OIDC session token (ADR 0092). It tells one apart from a user JWT and an API token without parsing it.

View Source
const DefaultAdminEmail = "admin@dexaflow.local"

DefaultAdminEmail is the email of the bootstrap admin a new install creates: a fresh Lite setup, and a server booted with a bootstrap password and no bootstrap email.

View Source
const DevTokenSubject = "leoflow-dev"

DevTokenSubject is the subject of the in-process token that `dexaflow lite` mints for its admin. It intentionally has no user row, so Authenticate trusts its signed claims as the ONLY subject exempt from the per-request DB authz reload.

View Source
const LegacyAdminEmail = "admin@leoflow.local"

LegacyAdminEmail is the bootstrap admin email of installs created before the rename to Dexaflow. Those installs keep their admin under this address; see ResolveAdminEmail.

View Source
const ScopeWarmWorker = "warm-worker"

ScopeWarmWorker marks an agent token that authorizes ONLY the warm-worker control channel — Register + AwaitAssignment — and NOTHING that resolves secrets or a task (ADR 0058 D2). It is the value of AgentIdentity.Scope for a warm worker's bootstrap credential; the empty scope is the default task credential, which resolves secrets exactly as before. The control plane gates every secret/task RPC on this value (see agentrpc.requireAttemptToken).

Variables

View Source
var (
	// ErrDeviceSignInsFull: the cluster holds as many sign-ins in progress
	// as it allows.
	ErrDeviceSignInsFull = errors.New("too many device sign-ins are in progress")
	// ErrDeviceSignInsFromRequester: the starting address holds as many
	// sign-ins in progress as one address may.
	ErrDeviceSignInsFromRequester = errors.New("too many device sign-ins are in progress from this address")
	// ErrDeviceStartsBusy: the start could not take its turn among
	// concurrent starts in time, so it was not recorded.
	ErrDeviceStartsBusy = errors.New("device sign-in starts are busy")
)

The refusals of a start over a cap.

View Source
var ErrAPITokenNotFound = errors.New("api token not found")

ErrAPITokenNotFound is returned by APITokenStore.FindAPITokenByHash when no token has the given hash.

View Source
var ErrInvalidCredentials = errors.New("invalid credentials")

ErrInvalidCredentials is returned when a username/password pair is rejected.

View Source
var ErrInvalidToken = errors.New("invalid token")

ErrInvalidToken is returned when a token is malformed, expired, or unsigned by us.

View Source
var ErrRoleNotFound = errors.New("role not found")

ErrRoleNotFound is returned by RolePermissionStore.RolePermissions when the tenant has no role of that name, or there is no such tenant.

View Source
var ErrTenantlessToken = errors.New("token names no tenant")

ErrTenantlessToken is joined with ErrInvalidToken when a token is trusted from its signed claims but names no tenant: such a principal cannot be scoped to any tenant's data, so it is refused rather than given a default one.

View Source
var ErrUserNotFound = errors.New("user not found")

ErrUserNotFound is returned by UserStore.FindUserByID when no user has the given id. Authenticate treats it as a signal to trust the token's signed claims (the in-process minting path has no backing row), distinct from a store failure, which fails closed.

Functions

func APITokenScopes added in v0.5.4

func APITokenScopes(scope string) ([]string, bool)

APITokenScopes returns the scopes an API token created for scope carries, and false when scope is not one a token may be created for.

func AddressBlock added in v0.5.4

func AddressBlock(ip string, v6Bits int) string

AddressBlock is AddressBucket with the IPv6 prefix length v6Bits: an IPv4 address as is, an IPv6 one by its /v6Bits, anything else as is.

func AddressBucket added in v0.5.4

func AddressBucket(ip string) string

AddressBucket is the rate-limit key of a client address: an IPv4 address (or an IPv4-mapped IPv6 one) as is, and an IPv6 address by its /64, the smallest block one subscriber is usually given, so rotating addresses within it does not buy a fresh budget. Anything that does not parse is its own key.

func HashAPIToken added in v0.5.4

func HashAPIToken(token string) string

HashAPIToken returns the hex SHA-256 of token, the only form of it the server keeps. A fast hash is enough: the secret is 256 random bits, so there is no dictionary to try, and the hash is the lookup key.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a plaintext password with bcrypt.

func IsAPIToken added in v0.5.4

func IsAPIToken(token string) bool

IsAPIToken reports whether token has the API token form.

func IsCISession added in v0.5.5

func IsCISession(token string) bool

IsCISession reports whether token has the CI session form.

func MintCISession added in v0.5.5

func MintCISession(secret string, ttl time.Duration, now time.Time, s CISession) (string, time.Time, error)

MintCISession signs a CI session valid for ttl from now with the engine secret, and returns it with its expiry (whole seconds, as in the token).

func MintUserToken

func MintUserToken(secret string, ttl time.Duration, user User) (string, error)

MintUserToken signs a user JWT directly, without checking credentials against a store. It is for trusted in-process callers only — notably `dexaflow lite`, which runs its own control plane and must register DAGs without a login round-trip. The token validates under Authenticate using the same secret.

func NewAPIToken added in v0.5.4

func NewAPIToken() (token, hash string, err error)

NewAPIToken returns a fresh API token and the hash to store for it. The token is shown to its owner once and never stored.

func VerifyPassword

func VerifyPassword(hash, password string) bool

VerifyPassword reports whether password matches the stored bcrypt hash.

Types

type APIToken added in v0.5.4

type APIToken struct {
	ID string
	// TenantID is the tenant name of the owner, as in User.TenantID.
	TenantID   string
	UserID     string
	UserEmail  string
	Name       string
	Scopes     []string
	CreatedAt  time.Time
	ExpiresAt  time.Time
	LastUsedAt *time.Time
	RevokedAt  *time.Time
}

APIToken is the stored record of a long-lived API token (ADR 0074). It never holds the token itself, only what describes it.

type APITokenAuthenticator added in v0.5.4

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

APITokenAuthenticator authenticates API tokens and hands every other token to the next Authenticator (ADR 0074). An API token resolves to its owner, reloaded from the store on every request like a JWT subject, narrowed to the token's scopes.

func NewAPITokenAuthenticator added in v0.5.4

func NewAPITokenAuthenticator(next Authenticator, tokens APITokenStore, users UserStore) *APITokenAuthenticator

NewAPITokenAuthenticator wraps next so that API tokens are checked against tokens and their owners reloaded from users.

func (*APITokenAuthenticator) Authenticate added in v0.5.4

func (a *APITokenAuthenticator) Authenticate(ctx context.Context, token string) (*User, error)

Authenticate resolves an API token to its scoped owner, or delegates any other token. A token that is unknown, revoked or expired, or whose owner is missing, inactive or in another tenant, is ErrInvalidToken. A store failure is returned as it is, so the request answers 503 rather than 401.

func (*APITokenAuthenticator) IssueToken added in v0.5.4

func (a *APITokenAuthenticator) IssueToken(ctx context.Context, creds Credentials) (string, error)

IssueToken delegates to the next authenticator: API tokens are created through the API by a signed-in user, never from a password.

type APITokenStore added in v0.5.4

type APITokenStore interface {
	// FindAPITokenByHash returns the token whose secret hashes to hash, revoked
	// and expired ones included, or ErrAPITokenNotFound.
	FindAPITokenByHash(ctx context.Context, hash string) (*APIToken, error)
	// TouchAPIToken records that the token was used at at, unless its last
	// recorded use is less than five minutes before at, and reports whether
	// it wrote. The store decides, so concurrent requests write once.
	TouchAPIToken(ctx context.Context, id string, at time.Time) (bool, error)
}

APITokenStore resolves API tokens by the hash of their secret and records their use. storage implements it.

type AdminLookup added in v0.5.4

type AdminLookup func(ctx context.Context, email string) (AdminState, error)

AdminLookup reports the AdminState of an email address in the tenant the caller is bootstrapping.

type AdminResolution added in v0.5.4

type AdminResolution struct {
	// Email is the address the bootstrap admin lives under.
	Email string
	// KeptLegacy reports that Email is LegacyAdminEmail, chosen in place of the
	// default address because the install predates the rename.
	KeptLegacy bool
	// BothExist reports that users exist under both addresses, which the
	// caller should warn about: only Email is bootstrapped.
	BothExist bool
}

AdminResolution is the address ResolveAdminEmail chose and why.

func ResolveAdminEmail added in v0.5.4

func ResolveAdminEmail(ctx context.Context, configured string, lookup AdminLookup) (AdminResolution, error)

ResolveAdminEmail returns the address the bootstrap admin lives under.

An empty configured email means DefaultAdminEmail. Any address other than DefaultAdminEmail is returned unchanged, with no lookup: an operator who named an address gets exactly that address. For DefaultAdminEmail the user holding the admin role wins, the new address first, so an install created before the rename (or a reinstall over its kept database) keeps the admin it has instead of gaining a second one, and the bootstrap never writes a password to a non-admin account at the new address while the legacy admin exists. Without an admin at either address, an existing user wins, the new address first; with no user at all, the new address is created. The existing user is never renamed.

type AdminState added in v0.5.4

type AdminState int

AdminState is what the user store knows about one email address.

const (
	// UserAbsent means no user has the address.
	UserAbsent AdminState = iota
	// UserWithoutAdminRole means a user has the address but not the admin role.
	UserWithoutAdminRole
	// UserIsAdmin means a user has the address and holds the admin role.
	UserIsAdmin
)

type AgentIdentity

type AgentIdentity struct {
	TaskInstanceID string
	TenantID       string
	DagID          string
	RunID          string
	TaskID         string
	TryNumber      int
	// AttemptEpoch identifies which execution of TryNumber this credential
	// belongs to (ADR 0051 amendment): an infra re-place, a reschedule poke or a
	// repeated dispatch reuses the try but never the epoch. It is meaningful
	// only when HasAttemptEpoch is set.
	AttemptEpoch int
	// HasAttemptEpoch is the presence bit for AttemptEpoch. It is false for a
	// token minted before the epoch existed (a legacy token, which the fence
	// treats as epoch 0) and for a warm-worker credential, so "absent" stays
	// distinguishable from epoch 0.
	HasAttemptEpoch bool
	// Scope is "" for a task credential (the default, byte-compatible with every
	// token minted before this field existed) or ScopeWarmWorker for a warm
	// worker's control-channel-only bootstrap credential.
	Scope string
	// DagVersionID names the warm pool a warm-worker credential serves. Empty on a
	// task credential.
	DagVersionID string
	// WorkerID is the warm worker's stable identifier (its pod name), minted as the
	// token Subject for a warm-worker credential. Empty on a task credential (whose
	// Subject is TaskInstanceID instead).
	WorkerID string
}

AgentIdentity is the identity a verified agent token represents. By default (Scope == "") it is a single task instance — the task-scoped credential that resolves secrets. When Scope == ScopeWarmWorker it is instead a warm worker's bootstrap credential, which names its dag_version pool (DagVersionID) and its worker id (WorkerID, the token Subject) and carries NO task claims.

type AgentTokenRejection added in v0.5.5

type AgentTokenRejection struct {
	// Reason is one of the AgentToken* reason constants.
	Reason string
	// Identity is the attempt an authentic but out-of-window token belonged to,
	// or nil.
	Identity *AgentIdentity
	// ExpiredAt is when an expired token lapsed; zero for any other reason.
	ExpiredAt time.Time
}

AgentTokenRejection describes why AuthenticateAgent refused a bearer token. AuthenticateAgent joins it into its error (alongside ErrInvalidToken), so a caller recovers it with errors.As.

It never holds the token or any part of it. Identity is set only when the token's signature, issuer and audience all verified and the token failed on time alone (expired or not yet valid): its claims are then authentic and name the attempt the credential belonged to. For any other reason the claims are unauthenticated or belong to another surface, so they are not surfaced.

func (*AgentTokenRejection) Error added in v0.5.5

func (r *AgentTokenRejection) Error() string

Error names the reason only.

type Authenticator

type Authenticator interface {
	Authenticate(ctx context.Context, token string) (*User, error)
	IssueToken(ctx context.Context, creds Credentials) (string, error)
}

Authenticator issues and validates authentication tokens. The MVP ships a JWT implementation; the interface keeps OIDC/LDAP pluggable (ADR 0008).

type CIIssuerFingerprints added in v0.5.5

type CIIssuerFingerprints interface {
	Fingerprint(name string) (string, bool)
}

CIIssuerFingerprints answers the configuration fingerprint of a CI issuer entry by name, false when the entry is not configured. cioidc.Verifier implements it.

type CISession added in v0.5.5

type CISession struct {
	// Issuer is the name of the CI issuer entry that verified the CI token.
	Issuer string
	// Fingerprint is that entry's configuration fingerprint when the session
	// was minted; a changed entry refuses the session.
	Fingerprint string
	// Tenant is the entry's tenant.
	Tenant string
	// Subject is the CI token's sub, which names the job.
	Subject string
	// Role and Scopes are the matching rule's.
	Role   string
	Scopes []string
}

CISession is what a CI OIDC session acts as: the role a rule of a trusted CI issuer named, in that issuer's tenant (ADR 0092).

type CISessionAuthenticator added in v0.5.5

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

CISessionAuthenticator authenticates CI sessions and hands every other token to the next Authenticator (ADR 0092). A session resolves to the role it names in its tenant, read from the store on every request, narrowed to its scopes.

func NewCISessionAuthenticator added in v0.5.5

func NewCISessionAuthenticator(next Authenticator, secret string, issuers CIIssuerFingerprints, roles RolePermissionStore) *CISessionAuthenticator

NewCISessionAuthenticator wraps next so that CI sessions signed with secret are checked against the configured issuers and the tenant's roles.

func (*CISessionAuthenticator) Authenticate added in v0.5.5

func (a *CISessionAuthenticator) Authenticate(ctx context.Context, token string) (*User, error)

Authenticate resolves a CI session, or delegates any other token. A session that does not verify, is expired, names an entry that is gone or changed, names no tenant, or names a role its tenant does not have is ErrInvalidToken. A store failure is returned as it is (503).

func (*CISessionAuthenticator) IssueToken added in v0.5.5

func (a *CISessionAuthenticator) IssueToken(ctx context.Context, creds Credentials) (string, error)

IssueToken delegates to the next authenticator: a CI session comes only from the exchange.

type Credentials

type Credentials struct {
	Tenant   string
	Username string
	Password string
}

Credentials are the inputs to token issuance.

type DeviceAuthorization added in v0.5.4

type DeviceAuthorization struct {
	// ID names the pending sign-in between the verification page, the IdP
	// round trip and the consent page.
	ID string
	// CreatedAt is when the CLI started the sign-in.
	CreatedAt time.Time
	// ExpiresAt is when the sign-in can no longer be approved or redeemed.
	ExpiresAt time.Time
	// RequesterAddr is the address that started the sign-in, as the server
	// saw it, shown on the consent page.
	RequesterAddr string
}

DeviceAuthorization is a device sign-in of the CLI waiting for a person to approve it (`dexaflow auth login --device`, ADR 0081), as storage returns it. Neither of its codes is here: storage keeps only their keyed hashes.

type DeviceAuthorizationRequest added in v0.5.4

type DeviceAuthorizationRequest struct {
	DeviceCodeHash string
	UserCodeHash   string
	// RequesterHash is the keyed hash of the starting address (its /56 for
	// IPv6), which MaxLivePerRequester counts by.
	RequesterHash string
	RequesterAddr string
	TTL           time.Duration
	Interval      time.Duration
	// MaxLive caps the sign-ins in progress in all, and
	// MaxLivePerRequester those from one requester.
	MaxLive             int
	MaxLivePerRequester int
}

DeviceAuthorizationRequest is what starting a device sign-in stores: the keyed hashes of its codes and of the address that started it, that address for the consent page, and the limits it starts under.

type DevicePoll added in v0.5.4

type DevicePoll struct {
	Outcome DevicePollOutcome
	User    User
}

DevicePoll is the result of one poll: its outcome and, for an approved sign-in, the user it was approved as, with the roles the sign-in resolved.

type DevicePollOutcome added in v0.5.4

type DevicePollOutcome int

DevicePollOutcome is what one poll of a device sign-in found.

const (
	// DevicePollUnknown is a device code nobody issued, or one already
	// redeemed or refused: invalid_grant.
	DevicePollUnknown DevicePollOutcome = iota
	// DevicePollPending is a sign-in nobody approved yet:
	// authorization_pending.
	DevicePollPending
	// DevicePollSlowDown is a poll that came sooner than the interval: the
	// interval grew by five seconds, slow_down.
	DevicePollSlowDown
	// DevicePollExpired is a sign-in past its expiry: expired_token.
	DevicePollExpired
	// DevicePollDenied is a sign-in the person canceled: access_denied. It
	// is answered once; later polls find nothing.
	DevicePollDenied
	// DevicePollApproved is an approved sign-in, redeemed by this poll: the
	// token is minted for User. Later polls find nothing.
	DevicePollApproved
)

The outcomes of a poll. Each maps to one answer of the token route (RFC 8628 section 3.5).

type JWTAuthenticator

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

JWTAuthenticator issues and validates HS256 JWTs against a UserStore.

func NewJWTAuthenticator

func NewJWTAuthenticator(store UserStore, secret string, ttl time.Duration) *JWTAuthenticator

NewJWTAuthenticator builds a JWTAuthenticator with the given user store, HS256 secret, and token lifetime.

func (*JWTAuthenticator) Authenticate

func (a *JWTAuthenticator) Authenticate(ctx context.Context, token string) (*User, error)

Authenticate validates a bearer token and resolves the current principal. After the signature and registered claims check out, it reloads the user's roles, permissions, and active flag from the store keyed by the token subject (the user id). The store — not the token — is the source of truth for authorization: this is what makes non-admin role grants gate anything (the token carries roles but never permissions) and lets deactivating a user revoke their live tokens within the token TTL.

Two cases fall back to the signed claims instead of the reload: a nil store (no data plane bound — the trusted in-process minting context) and a subject with no backing row (a directly-minted token, e.g. `dexaflow lite`). Any other store failure fails closed, so a flaky database cannot silently disable revocation.

func (*JWTAuthenticator) AuthenticateAgent

func (a *JWTAuthenticator) AuthenticateAgent(token string) (*AgentIdentity, error)

AuthenticateAgent validates an agent bearer token and returns the task instance it identifies. A rejection's error matches ErrInvalidToken and carries an *AgentTokenRejection saying why (#1125).

func (*JWTAuthenticator) IssueAgentToken

func (a *JWTAuthenticator) IssueAgentToken(id AgentIdentity, ttl time.Duration) (string, error)

IssueAgentToken mints a signed token that identifies a single task instance, valid for the given TTL. The control plane passes it to the worker pod.

The token's origin (oiat) is set to the mint time — this is a fresh dispatch. Renewal (RenewAgentToken) preserves that origin instead of resetting it.

func (*JWTAuthenticator) IssueToken

func (a *JWTAuthenticator) IssueToken(ctx context.Context, creds Credentials) (string, error)

IssueToken validates the credentials against the store and returns a signed JWT.

func (*JWTAuthenticator) RenewAgentToken

func (a *JWTAuthenticator) RenewAgentToken(token string, ttl, maxLifetime time.Duration) (renewed string, ok bool, err error)

RenewAgentToken validates an in-flight agent bearer token and re-mints it for the SAME task instance with a fresh short TTL, refreshing the live window without ever changing what AuthenticateAgent verifies (signature, issuer, leoflow-agent audience, HS256). It is the heartbeat-driven half of the short-TTL-plus-renewal design (ADR 0055 Fix #4): the short TTL bounds a stolen or finished token, while renewal keeps a genuinely live task's credential working.

The attempt's original dispatch time is preserved across every renewal (the oiat claim). maxLifetime is a hard ceiling on that total age: once the attempt has been alive longer than maxLifetime since first dispatch, renewal is refused (ok=false, empty token) so a runaway attempt's credential lapses instead of being kept alive forever. A non-positive maxLifetime disables the ceiling. exp is always now+ttl — never accumulated onto the previous exp.

An invalid incoming token (bad signature, wrong audience, expired) returns an error and is never re-minted.

func (*JWTAuthenticator) RenewUserToken

func (a *JWTAuthenticator) RenewUserToken(ctx context.Context, token string, ttl, maxLifetime time.Duration) (renewed string, ok bool, err error)

RenewUserToken validates a still-valid user bearer token and re-mints it for the SAME principal (subject, tenant, email, roles) with a fresh short TTL, without ever changing what Authenticate verifies (signature, issuer, leoflow-user audience, HS256). It is the server half of transparent CLI token renewal (EKS validation aresta #5): the short access-token TTL still bounds a stolen token, while renewal keeps a genuinely live session working so a long dev session never has to `dexaflow auth login` again on the hour. It is modeled directly on RenewAgentToken.

The session's original login time is preserved across every renewal (the oiat claim, falling back to iat for a token minted before that claim existed). maxLifetime is a hard ceiling on that total age: once the session has been alive longer than maxLifetime since first login, renewal is refused (ok=false, empty token, no error) so the user must re-authenticate. A non-positive maxLifetime disables the ceiling. exp is always now+ttl — never accumulated.

An invalid incoming token (bad signature, wrong audience, expired) returns an error and is never re-minted.

Renewal RE-PROVES the principal: after the claims check and the ceiling check it reloads the user from the store, exactly as Authenticate does, and refuses (ErrInvalidToken) when the account is inactive or its row is gone. Issuing a credential is not weaker than using one, so a deactivated user must stop receiving fresh tokens, not merely stop being able to spend them. The same two carve-outs Authenticate makes apply verbatim: a nil store (no data plane bound — the trusted in-process minting context) and the dev-token subject, which has no backing row by design. Any other store failure fails closed; that adds no failure mode, because a database blip that refuses this renewal would refuse the following request anyway, and the client's refresh is best-effort. Roles on the re-minted token come from the reload, so a role revoked mid-session is reflected at once rather than carried forward from the old claims.

type Permission

type Permission struct {
	Action   string `json:"action"`
	Resource string `json:"resource"`
}

Permission is an action on a resource (e.g. {Action: "read", Resource: "dag"}).

type RateLimiter

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

RateLimiter is a per-key fixed-window limiter used to throttle failed logins per client IP (ADR 0008).

func NewRateLimiter

func NewRateLimiter(limit int, window time.Duration) *RateLimiter

NewRateLimiter builds a limiter allowing limit events per window per key.

func (*RateLimiter) Allow

func (r *RateLimiter) Allow(key string) bool

Allow records an event for key and reports whether it is within the limit. The check and the spend are one step under the lock, so concurrent callers of a key never get more than the limit between them: a caller that must bound concurrent work spends with Allow before the work, not after a Blocked peek, and gives the event back with Refund when it should not count.

func (*RateLimiter) Blocked

func (r *RateLimiter) Blocked(key string) bool

Blocked reports whether key has already reached its limit in the current window, WITHOUT recording an attempt (a peek). The login handler uses it to reject an over-limit caller up front while calling Allow only for actual failures — so a successful login never consumes the budget and a user who mistypes a few times is not locked out the moment they finally get it right.

func (*RateLimiter) Refund added in v0.5.4

func (r *RateLimiter) Refund(key string)

Refund gives back one event of key's window, never going below zero. A window that ended needs none: the next Allow starts a fresh one.

type RolePermissionStore added in v0.5.5

type RolePermissionStore interface {
	// RolePermissions returns the role's grants, or ErrRoleNotFound.
	RolePermissions(ctx context.Context, tenant, role string) ([]Permission, error)
}

RolePermissionStore reads the grants of one role of one tenant. storage implements it.

type User

type User struct {
	ID          string
	TenantID    string
	Email       string
	Roles       []string
	Permissions []Permission
	// Scoped marks a principal authenticated by a client token that carries
	// OAuth scopes (ADR 0067): the scopes then narrow what its roles allow.
	// Engine tokens and sessions are not scoped; only their roles decide.
	Scoped bool
	// Scopes are the scopes a Scoped principal's token carries.
	Scopes []string
	// DeployAudience marks a principal authenticated by a trusted-issuer
	// bearer minted for auth.trusted_issuer.deploy_audience (#1773). When that
	// setting is on, only such a principal may deploy. Only the issuer bearer
	// sets it; no other credential can.
	DeployAudience bool
}

User is an authenticated principal with its tenant, roles, and permissions.

func (*User) HasPermission

func (u *User) HasPermission(action, resource string) bool

HasPermission reports whether the user may perform action on resource. The admin role, or an admin action / wildcard resource permission, grants access.

func (*User) HasScope added in v0.5.3

func (u *User) HasScope(scope string) bool

HasScope reports whether the principal's token carries scope.

type UserStore

type UserStore interface {
	FindUserByLogin(ctx context.Context, tenant, username string) (user *User, passwordHash string, err error)
	// FindUserByID reloads a user's current authorization state (tenant, roles,
	// permissions) by id, along with whether the account is active. It is the
	// per-request source of truth for token validation: it makes non-admin role
	// grants take effect and lets deactivating a user revoke live tokens within
	// the token TTL. It returns ErrUserNotFound when no user has the id.
	FindUserByID(ctx context.Context, id string) (user *User, isActive bool, err error)
}

UserStore loads users for authentication. storage implements it.

Jump to

Keyboard shortcuts

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