Documentation
¶
Overview ¶
Package access decides who is at the console and what they may do.
It never proves who anyone is. A principal arrives already authenticated — by a bearer an authenticating gateway forwarded, by a sign-in the hub delegated to a connected directory or an external issuer, or by the break-glass admin account — and this package turns that principal into an identity, by asking the directory which groups the account is in and running the policy.
The hub holds no role vocabulary of its own: an identity is an operator because the policy puts it in the operators group, exactly as any other relying party's roles work.
Index ¶
- Constants
- Variables
- func AgentConsentActor(subject, signIn string) string
- func AgentConsentCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func ConnectCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func CookieNameFor(base string, secure bool) string
- func LinkCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func LoginCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func LoginStartedHere(r *http.Request, state string, secure bool) bool
- func NewSessionKey() ([]byte, error)
- func ReadCookie(r *http.Request, base string, secure bool) (*http.Cookie, error)
- func RecoveryCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func RecoveryStartedHere(r *http.Request, state string, secure bool) bool
- func ServiceAccountSubject(namespace, name string) string
- func SetCookie(w http.ResponseWriter, cookie *http.Cookie)
- type AgentConsent
- type Authorizer
- func (a *Authorizer) Authorize(ctx context.Context, p Principal) (Identity, error)
- func (a *Authorizer) Explain(ctx context.Context, proof Proof) (Explanation, error)
- func (a *Authorizer) HoldersOf(people []hub.Person, group, client string) []Holder
- func (a *Authorizer) Policy() *policy.Set
- func (a *Authorizer) SetClock(now func() time.Time)
- type Binding
- type ClientAdmission
- type Directory
- type Explanation
- type Holder
- type Identity
- type KeyFunc
- type Principal
- type Proof
- type Role
- type Sessions
- func (s *Sessions) Clear(w http.ResponseWriter)
- func (s *Sessions) Issue(w http.ResponseWriter, p Principal) error
- func (s *Sessions) Lifetime() time.Duration
- func (s *Sessions) Read(r *http.Request) (Principal, error)
- func (s *Sessions) Secure() bool
- func (s *Sessions) SetClock(now func() time.Time)
- type Source
- type StateCodec
- func (c *StateCodec) Issue(bind string) (string, error)
- func (c *StateCodec) IssueAgentConsent(request, actor string) (string, error)
- func (c *StateCodec) IssueAs(b Binding) (string, error)
- func (c *StateCodec) Request(consent AgentConsent) string
- func (c *StateCodec) SetClock(now func() time.Time)
- func (c *StateCodec) Verify(state string) (string, error)
- func (c *StateCodec) VerifyAgentConsent(consent AgentConsent, request, actor string) error
- func (c *StateCodec) VerifyBinding(state string) (Binding, error)
Constants ¶
const ( ConnectCookieName = "sluis_connect" LoginCookieName = "sluis_login" LinkCookieName = "sluis_link" RecoveryCookieName = "sluis_recovery" )
The two flows' state cookies. Each carries its flow's state alongside the URL, so that a callback proves it belongs to the browser that started the flow.
Two names, because the flows are different in the way that matters: consent is an operator granting this hub access to a company, sign-in is a person proving who they are. One cookie would let a callback finish a flow the browser did not start.
A third for a person linking a GitHub account, for the same reason: an operator connecting an organisation in one tab and linking their own account in another must not have either flow finish the other.
A fourth for a recovery form, which shared the sign-in's at first: the issuer and a console mounted on its host both set it, so opening any sign-in page -- either one's, or a provider button on it -- replaced the state a recovery form already open in another tab was bound to, and a cookie holding a recovery state could be presented to a provider callback, and the other way round. Its own name keeps each flow's state in a cookie only that flow sets and reads.
const AgentConsentCookieName = "sluis_agent_consent"
AgentConsentCookieName is the cookie an agent-consent page's acceptance token travels in: a fifth flow cookie, for the reason the others are separate. The login state a provider round trip sets and a recovery form's state carry the same authorization request, and neither may be presented where an acceptance is asked for (docs/decisions/0040-agent-class-sessions.md, decision 6).
const AgentConsentFormLimit = 8 << 10
AgentConsentFormLimit is the most an acceptance post may carry: one state, a few hundred bytes.
const AgentConsentPurpose = "agent-consent"
AgentConsentPurpose is what an acceptance token carries as its Binding.Owner. A provider round trip's state carries none, a legacy four-part state reads back with none, and a recovery form's carries RecoveryPurpose, so none of them is an acceptance.
const CookieName = "sluis_session"
CookieName is the console's session cookie.
const HostPrefix = "__Host-"
HostPrefix makes a Secure cookie a host-locked one.
const RecoveryFormLimit = 16 << 10
RecoveryFormLimit is the most a recovery form post may carry. A proof is a ServiceAccount token or a password, a few KiB at most; the standard library would otherwise read up to 10 MiB of form, or 32 MiB of multipart, from anybody before anything is checked.
const RecoveryPurpose = "recovery"
RecoveryPurpose is what a recovery form's state carries as its Binding.Owner, and a provider round trip's never does, so that each door refuses the other's state as well as the other's cookie.
const SSOCookieName = "sluis_sso"
SSOCookieName is the issuer's browser-session cookie. It is named here, with the others, so that the legacy-name table can hold it.
const SessionKeyBytes = 32
SessionKeyBytes is the length of a session-signing key.
Variables ¶
var ErrBadState = errors.New("access: state is not valid")
ErrBadState is returned for a state that is forged, stale or malformed.
var ErrNoAgentConsent = errors.New("access: the agent connection was not accepted in this browser")
ErrNoAgentConsent is an acceptance that does not verify: absent, forged, stale, for another request or another person, or not from the browser it was shown in.
var ErrNoSession = errors.New("access: no session")
ErrNoSession is returned when a request carries no readable session.
var ErrSuspended = errors.New("access: the account is not live")
ErrSuspended is returned when the directory says, authoritatively, that the account signing in is not live.
Functions ¶
func AgentConsentActor ¶ added in v1.69.0
AgentConsentActor is the actor an acceptance token is bound to: the person being completed and the browser sign-in they were shown the page under. Both, so that a token minted for one person, or for one sign-in of theirs, accepts nothing for another.
func AgentConsentCookie ¶ added in v1.69.0
AgentConsentCookie builds the acceptance cookie, and with an empty value the one that clears it: `__Host-` prefixed when cookies are secure, HttpOnly, SameSite=Lax, Path=/, for the flow's lifetime.
func ConnectCookie ¶
ConnectCookie builds the consent flow's state cookie, and — with an empty value — the one that clears it.
Both come from here because they were built in two places and drifted: the cookie was set Secure and cleared without it. Attributes are not part of a cookie's identity, so the clearing still worked, but a browser being asked to store a cookie over plain HTTP on a site that only ever speaks HTTPS is the kind of difference that stops being harmless the moment someone copies it.
func CookieNameFor ¶ added in v1.66.0
CookieNameFor is the name a cookie is written, read and cleared under.
With secure cookies it carries the __Host- prefix, which a browser honours only for a cookie that is Secure, has Path=/ and no Domain: so a sibling host in the same zone cannot plant (toss) a cookie this host will then read. Without them (plain-HTTP development) the base name is kept, since a browser refuses a __Host- cookie that is not Secure. Every cookie this service sets takes its name from here, so a write, a read and a clear cannot disagree.
func LinkCookie ¶
LinkCookie is the same for linking a GitHub account.
func LoginCookie ¶
LoginCookie is the same for the sign-in flow.
func LoginStartedHere ¶ added in v1.68.0
LoginStartedHere reports whether the request carries the login cookie bound to state: whether this is the browser that started the provider round trip, rather than one somebody else's page redirected into it.
func NewSessionKey ¶
NewSessionKey returns a fresh random session key.
func ReadCookie ¶ added in v1.75.0
ReadCookie returns the cookie a request carries under base, written under its current name, or else under the name it had before the rename. The current name wins when a browser holds both.
func RecoveryCookie ¶ added in v1.68.0
RecoveryCookie is the same for a recovery form.
func RecoveryStartedHere ¶ added in v1.68.0
RecoveryStartedHere is LoginStartedHere for a recovery form: whether the post comes from the browser the form was served to.
func ServiceAccountSubject ¶ added in v1.58.0
ServiceAccountSubject is the username a Kubernetes API server gives a ServiceAccount: what a policy's service_account matcher and a recovery account are compared against. It lives here, with no cluster client behind it, so the Lambda build can name a subject without linking one.
func SetCookie ¶ added in v1.75.0
func SetCookie(w http.ResponseWriter, cookie *http.Cookie)
SetCookie writes cookie, and with it a clearing of the cookie's old name, so that setting, replacing or clearing a cookie leaves the browser with the current name only. A cookie with no old name is written as is.
Types ¶
type AgentConsent ¶ added in v1.69.0
type AgentConsent struct {
// contains filtered or unexported fields
}
AgentConsent is an acceptance as a browser presented it: the token from the form and the value of the acceptance cookie. Only this package makes one (AgentConsentPresented); its zero value is no acceptance at all, so there is no boolean a caller could set instead. It is verified by whoever completes the request (StateCodec.VerifyAgentConsent), not by the page that received it.
func AgentConsentPresented ¶ added in v1.69.0
func AgentConsentPresented(r *http.Request, state string, secure bool) AgentConsent
AgentConsentPresented reads an acceptance off a request: the token the form posted, and the acceptance cookie the browser sent with it.
type Authorizer ¶
type Authorizer struct {
// contains filtered or unexported fields
}
Authorizer turns principals into identities.
func NewAuthorizer ¶
NewAuthorizer returns an authorizer over a policy and a directory.
func (*Authorizer) Authorize ¶
Authorize resolves a principal into an identity.
A recovery sign-in is an operator by construction: it exists for the day the policy or the directory is what is broken, and it was already authorised — by the cluster's RBAC, or by holding the one generated password — before it reached here. Everyone else is resolved through the directory and the policy, and an authoritative "not live" is a refusal rather than an empty role — a suspended account must not reach the console at all.
func (*Authorizer) Explain ¶
func (a *Authorizer) Explain(ctx context.Context, proof Proof) (Explanation, error)
Explain answers what a proof would effectively get. Unlike Authorize it never refuses: a suspended account is reported as suspended, which is the whole point of looking.
func (*Authorizer) HoldersOf ¶
func (a *Authorizer) HoldersOf(people []hub.Person, group, client string) []Holder
HoldersOf resolves an internal group, or a client, to the people who hold it right now.
The policy says which directory groups count; only the directory knows who is in them. Answering that is what makes an access review possible at all, and it is a read over snapshots already in memory.
A suspended account still appears, marked: seeing that a leaver is still counted somewhere is the whole point of looking.
func (*Authorizer) Policy ¶
func (a *Authorizer) Policy() *policy.Set
Policy returns the policy in force.
func (*Authorizer) SetClock ¶
func (a *Authorizer) SetClock(now func() time.Time)
SetClock replaces the clock. For tests.
type Binding ¶
type Binding struct {
// Bind is the workspace being reconnected, empty for a new one.
Bind string
// Actor is the identity that was authorised when the flow began.
//
// It is here because a third-party callback is the one request in a
// flow whose identity cannot be assumed: it arrives as a redirect from
// Google, and the route it lands on need not be the one the gateway
// authenticates. The state is signed by this hub and pinned to the
// browser by a cookie the callback checks, so it can say who started
// the flow when nothing else in the request can.
//
// The authorisation itself still happens at the start, where an
// operator asked for the consent. This only carries the answer.
Actor string
// Owner is the directory workspace id the flow will record as the
// owner of what it connects, empty for none. A recovery form's state
// carries [RecoveryPurpose] here instead, and is never presented to a
// flow that connects anything (its cookie is its own). It is chosen where the
// flow begins, under the role question asked then, and read back by the
// callback that creates the record: signed, so a browser cannot change
// it on the way.
Owner string
}
Binding is what a state carries from the start of a flow to its callback.
type ClientAdmission ¶
type ClientAdmission struct {
ID string
Kind string
Requires []string
Admitted bool
Lifetime time.Duration
}
ClientAdmission is one relying party and whether a proof reaches it.
type Directory ¶
type Directory interface {
ResolveUser(ctx context.Context, email string, maxAge *time.Duration) (hub.UserResult, error)
}
Directory is the part of the hub this package needs: the groups an address is in, whether the account is live, and whether that answer may be acted on.
type Explanation ¶
type Explanation struct {
Email string
Workspace string
GivenName string
FamilyName string
InDomain bool
Found bool
Suspended bool
Authoritative bool
DirectoryGroups []string
Result policy.Result
Role Role
Clients []ClientAdmission
}
Explanation is what a proof effectively gets, and why.
type Holder ¶
type Holder struct {
Email string
GivenName string
FamilyName string
Live bool
Authoritative bool
Via []string
Lifetime time.Duration
}
Holder is one account that holds a group or reaches a client, and why.
type Identity ¶
type Identity struct {
Email string
Subject string
GivenName string
FamilyName string
Source Source
Role Role
// Groups are the internal groups held.
Groups []string
// Held carries the same with the reason for each.
Held []policy.Held
// Claims is what a token for this identity would carry.
Claims map[string]any
// Lifetime is how long such a token would live.
Lifetime time.Duration
// Scopes are roles held over ONE workspace each, keyed by workspace
// id. They exist because the hub is built for several companies and
// the roles above are not: without them every tenant's administrator
// administers every other tenant's directory, including disconnecting
// it and reading its credential state.
//
// A scope never widens the global role and never narrows it. An
// identity with a global role may act on every workspace; an identity
// with only scopes may act on the ones it names.
Scopes map[string]Role
}
Identity is an authorized caller: a principal, the internal groups the policy puts it in, and what those groups grant.
func (Identity) Can ¶
Can reports whether the identity holds at least the given role over the whole installation. Scopes do not count: they are about one workspace, and everything that asks this is about all of them — the policy, the OAuth client, connecting a directory that does not exist yet.
func (Identity) CanAnywhere ¶
CanAnywhere reports whether the identity holds the role over the installation or over any single workspace. It is the gate on a page that then shows only what the identity may see.
func (Identity) CanFor ¶
CanFor reports whether the identity may act on one workspace: because it holds the role over the installation, or because it holds it there.
func (Identity) IsRosterController ¶ added in v1.76.0
IsRosterController reports whether the identity is one of the roster controllers' own: a workload the policy placed in policy.GroupRosterControllers. A workload holding viewer, or operator, is not one unless it is in that group.
func (Identity) Name ¶
Name is the person's name as the directory has it, or who they are when it has none.
func (Identity) Who ¶
Who is the identity as it should be written down: the address where there is one, the subject where there is not.
A recovery sign-in completes as a ServiceAccount and has NO address — so every line that reached for Email alone recorded a blank for exactly the sign-in whose actions most need a name against them.
func (Identity) Workspaces ¶
Workspaces returns the workspace ids this identity may act on at the given role, or nil when it may act on all of them. Nil is "everything", not "nothing": a global role is not a list.
type KeyFunc ¶ added in v1.75.0
KeyFunc yields the key a codec signs with. It is called for every signature, so a function over a secret caches it (see internal/lazy).
type Principal ¶
type Principal struct {
Email string
Subject string
Source Source
Issuer string
Claims map[string][]string
// ServiceAccount is set when the forwarded token names a cluster
// workload rather than a person -- a recovery sign-in at the issuer,
// which completes as the account and not as an address. What it is
// entitled to is the policy's `service_account` matchers, the same
// table that answers for a workload exchanging a token.
ServiceAccount *policy.ServiceAccountRef
// AWS is set when the bearer is an AWS IAM role's outbound identity
// federation token (a Lambda controller reading the console). What it is
// entitled to is the policy's `aws` matchers, the same table that answers
// for a role exchanging a token.
AWS *policy.AWSRole
}
Principal is an authenticated caller, before the policy has run.
type Proof ¶
type Proof struct {
Email string
GitHub *policy.GitHubClaims
ServiceAccount *policy.ServiceAccountRef
AWS *policy.AWSRole
}
Proof is what to explain: a person by address, a CI job, or a cluster workload. Exactly the three kinds the policy can resolve, so the same page answers what a pipeline is entitled to as well as what a person is.
type Role ¶
type Role string
Role is what an identity may do in this console.
The roles. Operator implies viewer.
type Sessions ¶
type Sessions struct {
// contains filtered or unexported fields
}
Sessions issues and reads the console's session cookie.
The cookie is the principal, signed with the hub's session key and short-lived. It is deliberately stateless: there is no session table to keep, and revoking everyone at once is rotating the key. What that cannot do — ending one person's session before it expires — is the gateway's job in an installation that has one, and the reason the lifetime is short in one that does not.
func NewSessions ¶
NewSessions returns a session codec. The key must be SessionKeyBytes long; secure marks the cookie so, which a plain-HTTP development run turns off.
func NewSessionsWith ¶ added in v1.75.0
NewSessionsWith is NewSessions over a key that is read when a cookie is first signed or checked, and read again as the function decides, so that opening the codec reads no secret. The function's key must be SessionKeyBytes long.
func (*Sessions) Issue ¶
func (s *Sessions) Issue(w http.ResponseWriter, p Principal) error
Issue signs a principal into the response's cookie.
type Source ¶
type Source string
Source says how a principal was established.
const ( // SourceForwarded is a bearer an authenticating gateway forwarded — // the normal path where a proxy fronts every console. SourceForwarded Source = "forwarded" // SourceDirectory is the hub's own sign-in with a connected directory, // for a standalone installation. SourceDirectory Source = "directory" // SourceOIDC is the hub's own sign-in against an external issuer. SourceOIDC Source = "oidc" // SourceRecovery is the way in for the day the ordinary one is // broken: proven cluster access, or a generated password outside a // cluster. SourceRecovery Source = "recovery" // SourceWorkload is a workload presenting its own ServiceAccount // token as the bearer, verified against its cluster's published key // set — a controller calling the console's API, not a person at it. // // It is NOT recovery, and the difference is the whole reason it is a // source of its own. Recovery is an operator by construction; a // workload is whatever the policy's `service_account` matchers make // it, which for a controller that only reads is a viewer and nothing // more. SourceWorkload Source = "workload" )
The sources, in the order an installation usually meets them.
type StateCodec ¶
type StateCodec struct {
// contains filtered or unexported fields
}
StateCodec signs the opaque state an OAuth flow carries. It keeps nothing: the state is its own record, and the cookie beside it is what makes the callback the same browser's.
func NewStateCodec ¶
func NewStateCodec(key []byte, ttl time.Duration) *StateCodec
NewStateCodec returns a codec over the hub's session key.
func NewStateCodecWith ¶ added in v1.75.0
func NewStateCodecWith(key KeyFunc, ttl time.Duration) *StateCodec
NewStateCodecWith is NewStateCodec over a key read when a state is first signed or checked, so that opening the codec reads no secret.
func (*StateCodec) Issue ¶
func (c *StateCodec) Issue(bind string) (string, error)
Issue returns a signed state bound to a workspace and to nobody.
func (*StateCodec) IssueAgentConsent ¶ added in v1.69.0
func (c *StateCodec) IssueAgentConsent(request, actor string) (string, error)
IssueAgentConsent mints an acceptance token for one authorization request and one actor (AgentConsentActor). It is what the consent page puts in its form and in AgentConsentCookie, and the only place one is made.
func (*StateCodec) IssueAs ¶
func (c *StateCodec) IssueAs(b Binding) (string, error)
IssueAs returns a signed state carrying the whole binding.
func (*StateCodec) Request ¶ added in v1.69.0
func (c *StateCodec) Request(consent AgentConsent) string
Request is the authorization request the presented token names, read without deciding anything: the page that received the post needs it to find the request, and StateCodec.VerifyAgentConsent is what decides. Empty for a token that does not verify or is not an acceptance.
func (*StateCodec) SetClock ¶
func (c *StateCodec) SetClock(now func() time.Time)
SetClock replaces the clock. For tests.
func (*StateCodec) Verify ¶
func (c *StateCodec) Verify(state string) (string, error)
Verify checks a state and returns what it was bound to.
func (*StateCodec) VerifyAgentConsent ¶ added in v1.69.0
func (c *StateCodec) VerifyAgentConsent(consent AgentConsent, request, actor string) error
VerifyAgentConsent checks an acceptance for the request being completed and the actor completing it (AgentConsentActor):
- the cookie carries the very token the form posted, compared in constant time, so the post comes from the browser the page was shown in (another site can neither read the HttpOnly cookie nor make a cross-site POST carry the SameSite=Lax one);
- the token is this codec's, unexpired, and an acceptance (AgentConsentPurpose): never the login state a provider round trip sets, a recovery form's, or a legacy four-part state with no owner;
- it is bound to this request and to this actor.
func (*StateCodec) VerifyBinding ¶
func (c *StateCodec) VerifyBinding(state string) (Binding, error)
VerifyBinding checks a state and returns everything it carries.