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 ConnectCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func LinkCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func LoginCookie(value string, secure bool, ttl time.Duration) *http.Cookie
- func NewSessionKey() ([]byte, error)
- func ServiceAccountSubject(namespace, name string) string
- 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 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
Constants ¶
const ( ConnectCookieName = "access_roster_connect" LoginCookieName = "access_roster_login" LinkCookieName = "access_roster_link" )
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.
const CookieName = "access_roster_session"
CookieName is the console's session cookie.
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 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 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 LinkCookie ¶
LinkCookie is the same for linking a GitHub account.
func LoginCookie ¶
LoginCookie is the same for the sign-in flow.
func NewSessionKey ¶
NewSessionKey returns a fresh random session key.
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.
Types ¶
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. 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) 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 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 (*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 (*StateCodec) Issue ¶
func (c *StateCodec) Issue(bind string) (string, error)
Issue returns a signed state bound to a workspace and to nobody.
func (*StateCodec) IssueAs ¶
func (c *StateCodec) IssueAs(b Binding) (string, error)
IssueAs returns a signed state carrying the whole binding.
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) VerifyBinding ¶
func (c *StateCodec) VerifyBinding(state string) (Binding, error)
VerifyBinding checks a state and returns everything it carries.