server

package
v1.57.1 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 61 Imported by: 0

Documentation

Overview

Package server maps the hub onto its two listeners: DirectoryService for consumers on the API port, and the operator services plus the console on the console port.

Handlers here are thin on purpose — authorize, call the hub, map the result — because everything worth testing lives in the hub and in the rules engine.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotAdmitted = errors.New("not admitted to the audit trail")

ErrNotAdmitted is a person the audit installation's audience does not admit: they may use the console and may not read the trail.

View Source
var ErrRecoveryRefused = errors.New("server: recovery refused")

ErrRecoveryRefused is returned for a proof that does not check out. It is one error for every reason: the caller is unauthenticated, and telling it which would help it guess.

View Source
var ErrRecoveryThrottled = errors.New("server: too many attempts")

ErrRecoveryThrottled is returned when too many proofs have been refused recently. It is distinct because it is not a statement about the proof.

Functions

func AuditRequests

func AuditRequests(trustedHops int, next http.Handler) http.Handler

AuditRequests puts what an audit record keeps of each request into the request's context, where the emitter finds it: where it came from, what sent it, the gateway's id for it, and the trace it belongs to.

Once, at the outermost handler, rather than at every place that records. The events a request causes are recorded in places that never see the request — the token endpoint's storage is handed a context by an OpenID library and nothing else — and a context is the one thing all of them share. A handler already under it does not read the request again: the merged service mounts the console, which does this for itself, inside the issuer, which does it first.

trustedHops is how many proxies of the deployment's own append to X-Forwarded-For in front of the service; zero takes the peer and ignores the header. See [clientAddress].

func IdentityFrom

func IdentityFrom(ctx context.Context) (access.Identity, bool)

IdentityFrom returns the identity a request carries.

func WithGrant

func WithGrant(ctx context.Context, grant *Grant) context.Context

WithGrant carries one request's grant. Nil means full read, which is also what an unguarded listener has.

func WithIdentity

func WithIdentity(ctx context.Context, id access.Identity) context.Context

WithIdentity puts an authorized identity in the context. The console's middleware does it once per request; handlers only read.

Types

type AuditQuery

type AuditQuery struct {
	// URL is the query service.
	URL *url.URL
	// Token mints a token the query service accepts, for the person: the
	// issuer beside this console, for the installation's audience. An error
	// wrapping ErrNotAdmitted is somebody the audience does not admit.
	Token func(ctx context.Context, id access.Identity) (token string, expires time.Time, err error)
	// Transport reaches the query service; default http.DefaultTransport.
	Transport http.RoundTripper
}

AuditQuery is the audit installation's query service, and how the console reaches it as the person signed in.

The console's Audit page is the installation's own view, and it reads the trail through this console rather than from the browser: the browser keeps the console's session, and the query service gets a token for the person, minted here, that never reaches the browser. So there is no second sign-in, no bearer in the page, and no cross-origin call, and what the person may read is decided where the trail is kept, by its grants.

type ClientVerifier

type ClientVerifier interface {
	VerifyClient(ctx context.Context) error
}

ClientVerifier is a connector that can say whether this installation's registered client would be accepted, without a person. A connector that cannot simply does not implement it, and the flow starts unchecked.

type Connector

type Connector interface {
	// Kind names the backend, matching backend.Backend.Kind.
	Kind() string
	// AuthURL is where the browser goes to consent. It fails when this
	// deployment cannot start a consent yet — most often because nobody
	// has registered an OAuth client — because a button that navigates
	// nowhere is worse than one that says why.
	AuthURL(state string) (string, error)
	// Exchange turns the callback's code into a workspace and the backend
	// that reads it. Bind carries the workspace being reconnected, or is
	// empty for a new connection.
	Exchange(ctx context.Context, code, bind string) (hub.Workspace, backend.Backend, error)
}

Connector starts and finishes an admin-consent flow for one backend. The browser is in the middle of it, which is why the callback is a plain HTTP route rather than an RPC.

type Console

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

Console serves WorkspaceService, SettingsService and AccessService on the console listener.

func NewConsole

func NewConsole(_ context.Context, deps ConsoleDeps) (*Console, error)

NewConsole returns the operator services.

It used to install a console layer of the policy here, so that a membership added before a restart was in force after it. There is no console layer any more: who is in which internal group is the policy, rendered from the installation's own access model and reviewed in git. A console that could disagree with git was a second source of truth and a merge to reconcile them.

func (*Console) BeginConnect

BeginConnect implements the operator contract: it returns the consent URL and sets the state cookie the callback checks.

func (*Console) BeginGitHubAppConnect

BeginGitHubAppConnect starts creating any App, or finishing installing one created before.

It hands the work to the same code the per-kind calls run, so the signed state, the cookie it is pinned to and the callback that finishes the flow are the ones that kind of App has always used: a browser part-way through a flow is unaffected by which call started it.

func (*Console) BeginGitHubCatalogueAppConnect

BeginGitHubCatalogueAppConnect starts creating a catalogue App, or finishing installing one created before.

func (*Console) BeginGitHubConnect

BeginGitHubConnect starts connecting an organisation.

Only one the policy binds. Connecting an organisation nothing will manage installs an App with members:write for no reason, and the likeliest cause is a typo in the login — which GitHub would answer with a 404 on its create page, after the operator has left this one.

func (*Console) BeginGitHubLinkAppConnect

BeginGitHubLinkAppConnect starts creating the link App.

func (*Console) BeginGitHubRunnerAppConnect

BeginGitHubRunnerAppConnect starts creating a runner App, or finishing installing one created before.

Only in an organisation the policy binds, for the same reason as an organisation's App: a typo in the login would otherwise meet GitHub's 404 after the operator has left this page.

func (*Console) BeginSlackWorkspaceConnect

BeginSlackWorkspaceConnect creates the roster's App in a workspace, or prepares the reinstall of one created earlier, and returns Slack's authorize URL.

The configuration token is read from the request into one local, used for one call, and dropped: it is not written to the Secret or a record, not put into the signed state, not in any log line or audit record, and not in any error returned (errors from Slack never carry it).

func (*Console) ChangeGitHubOrganisationOwner

ChangeGitHubOrganisationOwner changes the directory recorded as a connected organisation's owner, or removes it. The installation-wide operator alone: an owner that could hand its organisation to another, or take one, would make "who may operate this" a thing its operators decide.

func (*Console) ChangeSlackWorkspaceOwner

ChangeSlackWorkspaceOwner changes the directory recorded as a connected workspace's owner, or removes it. The installation-wide operator alone: an owner that could hand its workspace to another, or take one, would make "who may operate this" a thing its operators decide.

func (*Console) CheckGitHubApp

CheckGitHubApp asks GitHub again about one App, whichever kind it is.

func (*Console) CheckGitHubCatalogueApp

CheckGitHubCatalogueApp asks GitHub again about one App.

func (*Console) ConfirmGitHubRemovals

ConfirmGitHubRemovals lets exactly the removal set the operator saw go ahead. The fingerprint must be the one the organisation's latest report shows: confirming a set that has since changed would confirm people nobody looked at.

func (*Console) ConfirmSlackRemovals

ConfirmSlackRemovals lets exactly the removal set the operator saw go ahead. The fingerprint must be the one the latest report shows for that gate — the workspace's, or the named channel's: confirming a set that has since changed would confirm people nobody looked at.

func (*Console) CreateSlackApp

CreateSlackApp creates a catalogue App in Slack.

The configuration token is read from the request into one local, used for one call, and dropped: it is not written to the Secret or a record, not put into the signed state, not in any log line or audit record, and not in any error returned (errors from Slack never carry it).

func (*Console) CreateSlackChannel

CreateSlackChannel puts a channel under management: operator of the workspace's owner, or of the installation.

func (*Console) CreateSlackSharedChannel

CreateSlackSharedChannel defines a channel: operator of the host workspace's owner, or of the installation.

func (*Console) DeleteSlackChannel

DeleteSlackChannel forgets the record. The channel stays in Slack, unless the request asks for it to be archived too.

func (*Console) DeleteSlackSharedChannel

DeleteSlackSharedChannel forgets the record. The channel stays in Slack.

func (*Console) Disconnect

Disconnect implements the operator contract.

func (*Console) DisconnectGitHubApp

DisconnectGitHubApp uninstalls any App, then forgets it.

func (*Console) DisconnectGitHubCatalogueApp

DisconnectGitHubCatalogueApp uninstalls a catalogue App, then forgets it. A failed uninstall still forgets, and says what is left to do by hand. The App stays on GitHub: the API cannot delete one.

func (*Console) DisconnectGitHubLinkApp

DisconnectGitHubLinkApp forgets the link App, and makes every link made with it unverifiable — before forgetting, so that no pass can ever check one of those tokens with some later App's credentials and read GitHub's "unknown token" as a revocation.

func (*Console) DisconnectGitHubOrganisation

DisconnectGitHubOrganisation revokes, then forgets.

The revoke is uninstalling: GitHub's API cannot delete an App, so the registration stays for its owner to delete, and the response says where. Once uninstalled and forgotten nothing can act through it — nobody else ever held its key. A failed uninstall still forgets: the operator asked for this organisation to stop being managed, and the detail says what is left to do by hand.

func (*Console) DisconnectGitHubRunnerApp

DisconnectGitHubRunnerApp uninstalls a runner App, then forgets it. A failed uninstall still forgets, and says what is left to do by hand.

func (*Console) DisconnectSlackWorkspace

DisconnectSlackWorkspace revokes the bot token and forgets the connection.

func (*Console) Explain

Explain implements the operator contract. An empty address explains the caller, which anyone signed in may ask; explaining somebody else discloses their access, so that needs operator.

func (*Console) GetDirectoryGroup

GetDirectoryGroup implements the operator contract: one directory group, its members as the directory reports them, and the internal groups it feeds. The feeds are the memberships table read backwards.

func (*Console) GetGitHubApp

GetGitHubApp implements one App's page.

func (*Console) GetGitHubStatus

GetGitHubStatus implements the GitHub page: every organisation the policy binds or the controller reports on, its bindings beside its report.

The join happens here rather than in the browser so that the page has one answer to render. Bindings come from the policy in force and reports from the controller's ConfigMap; neither is authoritative for the other, and the page shows where they disagree — a bound team the controller has not reported, a report for a team the policy no longer binds — rather than hiding either side.

A viewer of some directory sees the organisations that directory owns (the owner recorded when each was connected) and no others; the installation-wide viewer sees all. A report names the members of every bound team, which is more than a scoped viewer may see of another company. The link App and every link are people's across all organisations, so they are the installation-wide viewer's alone.

func (*Console) GetPolicy

GetPolicy implements the operator contract.

func (*Console) GetSettings

GetSettings implements the operator contract. It never returns the client secret.

func (*Console) GetSlackStatus

GetSlackStatus is every Slack workspace the caller may view: the ones the policy declares, and any the controller reports on or a connection exists for that it no longer does (which only the installation-wide role sees, for want of an owner). A workspace's owner and team are the ones its connection recorded.

ImportGitHubLinks adopts approved pairings as links, after three checks each: somebody approved the pairing, an address is a live account the directory vouches for, and the account is a member of a connected organisation. A link the person made is never displaced.

func (*Console) InstallSlackApp

InstallSlackApp starts installing, or reinstalling, a created App: the response is Slack's authorize URL carrying signed state, and the header pins the flow to this browser.

func (*Console) ListDirectoryGroups

ListDirectoryGroups implements the operator contract: the groups the hub has snapshotted, so that a membership is a click rather than a typed address.

func (*Console) ListGitHubAppTokens

ListGitHubAppTokens implements an App's *Recent tokens*: the last installation tokens asked of it, minted or refused, newest first.

From this service's own memory of them rather than from the audit trail. The trail is the record and holds every request, but narrowing it by kind and target scans object after object of every hour in turn: measured against a real bucket the call was still running when the gateway gave up on it, and the section on the page was a sentence apologising. What it costs to keep the answer to this one question beside the process that answers it is a bounded ring per App.

Operator, not viewer: a request names who asked for it, as the Audit page does.

func (*Console) ListGitHubApps

ListGitHubApps implements the Apps list: every App in one shape.

func (*Console) ListHolders

ListHolders implements the operator contract: who holds an internal group, or who reaches a client, right now.

func (*Console) ListServedDomains

ListServedDomains implements the controllers' read: for each connected directory the caller may view, the domains the hub serves for it now.

func (*Console) ListSlackApps

ListSlackApps is every declared App, and every App created from an entry no longer declared, with where each stands.

func (*Console) ListSlackChannels

ListSlackChannels is every record the caller may see, the channels the bots see that nothing manages, and the choices a form offers.

func (*Console) ListSlackSharedChannels

ListSlackSharedChannels is every record the caller may see, with where the controller has each.

func (*Console) ListWorkspaces

ListWorkspaces implements the operator contract.

func (*Console) Probe

Probe implements the operator contract.

func (*Console) Reconnect

Reconnect implements the operator contract: the same flow, bound to an existing workspace so the callback can refuse a different tenant.

func (*Console) Refresh

Refresh implements the operator contract: the operator's max_age of zero.

func (*Console) RequestGitHubPass

RequestGitHubPass asks the GitHub controller to pass over now. The request is a marker the controller notices within its poll (see the controller's watch); a second one under a minute after the first is refused, so the button cannot be leaned on. It is recorded as a configuration change.

func (*Console) RequestSlackPass

RequestSlackPass asks the Slack controller to pass over a workspace now. The request is a marker the controller notices within its poll (see the controller's watch); a second one under a minute after the first is refused, so the button cannot be leaned on.

func (*Console) ResolveDirectoryGroups

ResolveDirectoryGroups implements the operator contract: the members of each directory group asked, nested groups expanded.

func (*Console) SearchPeople

SearchPeople implements the operator contract.

func (*Console) SetServedDomains

SetServedDomains implements the operator contract: which of a tenant's domains this hub answers for.

func (*Console) SetSyncedGroups

SetSyncedGroups implements the operator contract: which of a tenant's groups this hub keeps.

func (*Console) UpdateSlackChannel

UpdateSlackChannel changes mode, ignore and sources. The caller's role is asked about the STORED workspace, which a request cannot change.

func (*Console) UpdateSlackSharedChannel

UpdateSlackSharedChannel changes with, from (directory groups) and private. The caller's role is asked about the STORED host, which a request cannot change.

func (*Console) UploadKey

UploadKey implements the operator contract.

func (*Console) WhoAmI

WhoAmI implements the operator contract.

type ConsoleDeps

type ConsoleDeps struct {
	// Log is where the console logs what is not an audit record; nil is the
	// default logger.
	Log        *slog.Logger
	Hub        *hub.Hub
	Authorizer *access.Authorizer
	Settings   settings.Store
	State      *access.StateCodec
	Connectors []Connector
	// Recovery is the way in when the ordinary one is broken; nil is a
	// deployment with none. The console reports its shape, because only
	// a stored password is a standing credential worth a banner.
	Recovery     Recovery
	LoginSources []string
	CacheBackend string
	SecureCookie bool
	// PublicURL is where a browser reaches this console,
	// including route.pathPrefix, when one is set, because this is the
	// console's own address and the console may sit under a path.
	//
	// It is what makes the redirect URI reportable: a runbook can only
	// say "your hostname plus this path", and an operator retyping a
	// hostname into a cloud console is exactly where a day-one setup goes
	// wrong.
	PublicURL string
	// RootURL is the host's ROOT, never carrying route.pathPrefix even
	// when PublicURL does. The bootstrap surface (route.bootstrapPaths:
	// /login, /connect) is deliberately never moved under the console's
	// path -- it is what the FIRST operator reaches before there is a
	// console session to authenticate, on its own HTTPRoute, and the
	// registered OAuth redirect URIs for it are at the domain root. Empty
	// falls back to PublicURL, which is exactly right wherever no prefix
	// is configured: the two are then the same address.
	RootURL string
	// IssuerURL is the token service this console is behind, when it is
	// behind one. With a shared OAuth client the sign-in redirect belongs
	// to that host rather than this one, and only this side knows it.
	IssuerURL string
	// SignIn reports whether this console signs people in itself, which
	// is the other place a sign-in redirect can land.
	SignIn bool
	// GitHub is where the GitHub controller's reports are read. Nil is a
	// deployment keeping no state in Kubernetes, which has nowhere for a
	// controller to report to; the GitHub page then shows the bindings
	// alone and says why.
	GitHub GitHubReports
	// GitHubOrgs is where connected organisations are kept. Nil is a
	// deployment keeping no state in Kubernetes, which can connect none.
	GitHubOrgs GitHubConnections
	// GitHubLinkApp is where the link App is kept. Nil is a deployment
	// where nobody can link an account.
	GitHubLinkApp GitHubLinkApp
	// GitHubLinks is where people's links are kept. Nil, likewise.
	GitHubLinks GitHubLinks
	// GitHubConfirmations is where operators confirm removal sets over the
	// limit. Nil confirms nothing.
	GitHubConfirmations GitHubConfirmations
	// GitHubRunnerApps is where runner Apps are kept. Nil is a deployment
	// that keeps none.
	GitHubRunnerApps GitHubRunnerApps
	// GitHubRunnerTiers are the runner tiers an operator may create an App
	// for. Empty keeps no runner Apps, whatever the store.
	GitHubRunnerTiers []string
	// GitHubCatalogue is every App the deployment declares. Nil declares
	// none.
	GitHubCatalogue *catalogue.Catalogue
	// GitHubCatalogueApps is where catalogue Apps are kept. Nil is a
	// deployment keeping no state in Kubernetes, which can create none.
	GitHubCatalogueApps GitHubCatalogueApps
	// SlackCatalogue is every Slack App the deployment declares. Nil
	// declares none.
	SlackCatalogue *slackcatalogue.Catalogue
	// SlackCatalogueApps is where catalogue Slack Apps are kept. Nil is a
	// deployment keeping no state in Kubernetes, which can create none.
	SlackCatalogueApps SlackCatalogueApps
	// SlackShared is where Slack Connect channel definitions are kept. Nil is
	// a deployment keeping no state in Kubernetes, which can define none.
	SlackShared SlackSharedRecords
	// SlackChannels is where console channels' records are kept. Nil is a
	// deployment keeping no state in Kubernetes, which can manage none.
	SlackChannels SlackChannelRecords
	// SlackStatus is what the Slack controller last reported. Nil shows every
	// channel as not reported.
	SlackStatus SlackStatusReports
	// SlackWorkspaces is where connected Slack workspaces and operators'
	// confirmations are kept. Nil connects and confirms nothing. The
	// controller's report is SlackStatus.
	SlackWorkspaces SlackWorkspaces
	// SlackAPI configures the calls to Slack that creating and installing
	// an App make: a test points them at a fake. Nil is Slack.
	SlackAPI []slackapp.Option
	// GitHubMints is the last installation tokens asked of each App, kept
	// by the half of this service that mints them. Nil is a deployment
	// that mints none, or one whose issuer is another process; an App's
	// page then says it keeps none rather than showing an empty table.
	GitHubMints *mints.Ring
	// GitHubHTTP makes the calls to GitHub that connecting and
	// disconnecting need. Nil is a client with a short timeout.
	GitHubHTTP *http.Client
	// Audit records what an identity did through the console. Nil records
	// nothing.
	Audit audit.Recorder
	// Trigger is told which target a write of the console concerns, so its
	// tick runs soon (docs/decisions/0029). With the legacy adapter the
	// trigger is in this process, and the controllers are other processes: the
	// notification reaches nobody there, and the controller notices the same
	// write through the records it mounts (see docs/design/ports.md). Nil
	// notifies nobody.
	Trigger port.Trigger
}

ConsoleDeps is everything the operator services need.

type ConsoleServer

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

ConsoleServer is the console listener: the operator services, the login routes, the consent callback and the identity middleware that feeds them.

func NewConsoleServer

func NewConsoleServer(deps ConsoleServerDeps) *ConsoleServer

NewConsoleServer assembles the console listener.

func (*ConsoleServer) Handler

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

Handler returns the console listener's HTTP handler.

func (*ConsoleServer) UseAuditQuery

func (s *ConsoleServer) UseAuditQuery(q *AuditQuery)

UseAuditQuery connects the Audit page to an audit installation's query service, for the same reason and with the same timing as ConsoleServer.UseSignedIn: its tokens are minted by the issuer, which does not exist yet when this console is built. Without it the console has no Audit page.

func (*ConsoleServer) UseIssuerURL

func (s *ConsoleServer) UseIssuerURL(url string)

UseIssuerURL supplies the issuer this console shares an origin with.

Without it the console reported only the FORWARDED bearer's issuer, which a proxy in front used to configure. On the merged service there is no proxy, so that was empty and the console concluded it had no issuer to talk to -- hiding the Sessions page and the sessions section of a person's page, on precisely the deployment where they work best, because the SessionService is now a same-origin call carrying the browser's own SSO cookie.

func (*ConsoleServer) UseSignInEntry

func (s *ConsoleServer) UseSignInEntry(where func() string)

UseSignInEntry supplies where to send somebody who has no session, for the same reason and with the same timing as ConsoleServer.UseSignedIn.

func (*ConsoleServer) UseSignedIn

func (s *ConsoleServer) UseSignedIn(read func(*http.Request) (access.Principal, bool))

UseSignedIn supplies the issuer's own browser session as an identity source, after construction.

After, because in the merged service the directory is assembled first — the issuer is handed a door to it — so the issuer does not exist yet when this console is built. It must be called before anything is served; [ConsoleServer.principal] reads it per request.

func (*ConsoleServer) UseWorkloads

func (s *ConsoleServer) UseWorkloads(read func(*http.Request) (access.Principal, bool))

UseWorkloads supplies a reader of ServiceAccount bearers from the clusters the issuer federates, for the same reason and with the same timing as ConsoleServer.UseSignedIn: the key sets live with the issuer, which does not exist yet when this console is built.

It is how a controller in the same cluster reads this console's API. It presents its own projected token and nothing else — no exchange in front of a same-cluster call, which would have the issuer verify that very token and re-sign it — and what it may then do is whatever the policy's `service_account` matchers make it.

type ConsoleServerDeps

type ConsoleServerDeps struct {
	Console    *Console
	Authorizer *access.Authorizer
	Sessions   *access.Sessions
	State      *access.StateCodec
	Connectors []Connector
	Hub        *hub.Hub
	// Recovery is the way in when the ordinary one is broken. Nil is a
	// deployment with no recovery path at all.
	Recovery Recovery
	// SignIn offers the hub's own sign-in page. A console reached only
	// through a gateway that has already run the login wants one door,
	// not two. It does not affect connecting a directory, which is an
	// operator granting this hub access rather than a way in.
	SignIn bool
	// SignOutURL is where the console's sign-out control goes. Empty is
	// this hub's own `/logout`, which is right only where this hub's own
	// cookie is what signed the person in. Behind a proxy it is the
	// proxy's sign-out path.
	SignOutURL string
	Forwarded  ForwardedIdentity
	// SignInEntry is where an unauthenticated browser is sent to GET a
	// session: the issuer's authorization endpoint, with this console as
	// the client.
	//
	// [SignedIn] reads a session somebody already has; this is how they
	// come by one. Both are needed and neither replaces the other. Nil
	// falls back to this console's own sign-in page, which is the split
	// deployment's shape and the second door an installation with a
	// gateway turns off.
	SignInEntry func() string
	// SignedIn reads the ISSUER's own browser session, when the console
	// is served by the same process on the same origin.
	//
	// It is what lets a person who already signed in — at any console
	// behind this issuer — reach this one with no second session and no
	// proxy in front of it. A function rather than the type, because this
	// package answers questions about a directory and should not have to
	// know what an issuer is.
	//
	// Nil is the split deployment, where a gateway forwards a bearer.
	SignedIn func(*http.Request) (access.Principal, bool)
	// Mount is where this console sits on its origin: "/console" when it
	// shares the issuer's hostname, empty when it has an origin of its
	// own. It is not a route — the prefix is stripped before any of these
	// handlers run, whether by the gateway or by the merged process — it
	// is what every link and redirect they EMIT has to be prefixed with,
	// because a browser resolves "/login" against the origin and would
	// land on the issuer's page instead of this one's.
	Mount string
	// ForwardedForTrustedHops is how many of the deployment's own proxies
	// append to X-Forwarded-For in front of the service. Zero records the
	// peer as an audit event's client address; otherwise the entry just
	// left of those hops is the client.
	ForwardedForTrustedHops int
	Log                     *slog.Logger
	// UI is the built console. Nil serves no UI, which is what a
	// deployment that only wants the API does.
	UI fs.FS
}

ConsoleServerDeps is what the console listener needs.

type Consumer

type Consumer struct {
	// Namespace and ServiceAccount name the caller as the deployment
	// knows it; the subject the API server returns is derived from them.
	Namespace      string `yaml:"namespace"`
	ServiceAccount string `yaml:"serviceAccount"`
	// Workspaces, Domains, Groups and Reads are the grant. All empty is
	// full read, which is what an entry that names only a consumer
	// means -- and what every entry meant before grants existed.
	Workspaces []string `yaml:"workspaces,omitempty"`
	Domains    []string `yaml:"domains,omitempty"`
	Groups     []string `yaml:"groups,omitempty"`
	Reads      []Read   `yaml:"reads,omitempty"`
}

Consumer is one declared caller. The grant is inline, so the declaration reads as one row per consumer rather than two tables that have to be joined by eye.

func (Consumer) Grant

func (c Consumer) Grant() *Grant

Grant is this consumer's, or nil for full read. Nil rather than an empty struct so that the whole request path can test one thing.

type ConsumerFile

type ConsumerFile struct {
	Consumers []Consumer `yaml:"consumers"`
}

ConsumerFile is the mounted document.

func LoadConsumers

func LoadConsumers(path string) (*ConsumerFile, error)

LoadConsumers reads the declared consumers. An empty path is no file, which is not the same as an empty file: the first leaves the environment list in force, the second declares that nobody may call.

type Consumers

type Consumers struct {
	// Review is [kube.Client.ReviewToken]. Nil means the hub is not
	// running in a cluster and cannot check anything, which is a
	// different situation from an empty list — see [Consumers.Middleware].
	Review func(ctx context.Context, token string, audiences []string) (string, error)
	// Audience the token must have been minted for.
	Audience string
	// Allowed subjects, as the API server spells them.
	Allowed []string
	// Grants is what each admitted subject may ask, keyed the same way
	// as Allowed. A subject with no entry keeps full read, so
	// a deployment that declares consumers and no grants behaves exactly
	// as it did.
	Grants map[string]*Grant
	Log    *slog.Logger
}

Consumers is who may call the API listener.

The listener answers the two directory questions for every workspace this hub reads — every account, every group, every membership of every company it serves — so "reachable" must not be the same as "allowed". A NetworkPolicy is a second layer and a good one; it is not an answer to "which workload is this", and it is one misapplied label away from admitting a namespace nobody meant to.

The proof is a projected ServiceAccount token with this hub's audience, verified by the API server. The hub therefore trusts no signature of its own and keeps no shared secret: it asks the cluster who is calling, and compares the answer to a list the deployment states.

func (*Consumers) Middleware

func (c *Consumers) Middleware(next http.Handler) http.Handler

Middleware guards a handler.

Two situations that look alike and are not. **No reviewer** is a hub outside a cluster — a local run, the demonstration — where there is nothing to verify a token against; the listener is open and the caller is told so at start, loudly, because that is a development posture and never a deployed one. **A reviewer and an empty list** is a deployment that named no consumers, and it admits nobody: a hub that answered everyone by default would be one forgotten value away from serving a company's directory to the cluster.

type CredentialReopener

type CredentialReopener interface {
	OpenStored(ctx context.Context, cred backend.Credential) (backend.Backend, error)
}

CredentialReopener is a connector that can turn a stored credential back into a reader -- the counterpart of Exchange and FromKey for a process that starts already holding the secret, rather than mid-flow with a browser. A connector that cannot simply does not implement it, and a kind with no such connector is refused by name after a restart, the same as it would be refused at the console.

type Directory

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

Directory serves directory.v1.DirectoryService.

func NewDirectory

func NewDirectory(h *hub.Hub) *Directory

NewDirectory returns the handler over a hub.

func (*Directory) Describe

Describe implements the contract's Describe.

func (*Directory) GetAccount

GetAccount implements the contract's GetAccount.

func (*Directory) GetGroup

GetGroup implements the contract's GetGroup.

func (*Directory) ListGroups

ListGroups implements the contract's ListGroups.

func (*Directory) Probe

Probe implements the contract's Probe.

func (*Directory) ResolveAccounts

ResolveAccounts implements the contract's ResolveAccounts.

func (*Directory) ResolveUser

ResolveUser implements the contract's ResolveUser.

type ForwardedIdentity

type ForwardedIdentity struct {
	// Issuer the bearer is verified against, as a URL. Set with Audience,
	// this turns on the verified path.
	Issuer string
	// Audience the token must name -- the console's client id at the
	// issuer. Empty accepts any audience the issuer mints, which makes
	// every other service it serves a way in here.
	Audience string
	// EmailHeader is the header the proxy puts the caller's address in.
	// Empty turns the trusted-header path off, which is the right setting
	// wherever the verified path is configured.
	EmailHeader string
}

ForwardedIdentity configures how a bearer forwarded by an authenticating gateway becomes a principal.

Two paths, and they are not equals. VERIFYING the bearer against the issuer's published keys is the one to use: it answers "who signed this" rather than "who can reach this port". TRUSTING a header is the fallback for a gateway that forwards no token, and it is only ever as good as the promise that nothing else can reach the listener -- a promise one NetworkPolicy edit, one port-forward or one sidecar away from false.

type GitHubCatalogueApps

type GitHubCatalogueApps interface {
	Put(ctx context.Context, record catalogueapp.Record, privateKey string) error
	List(ctx context.Context) ([]catalogueapp.Record, error)
	// Get is one App's record and its key, installed or pending.
	Get(ctx context.Context, id string) (catalogueapp.Record, string, bool, error)
	Delete(ctx context.Context, id string) error
}

GitHubCatalogueApps is where catalogue Apps are kept: every App's record and key, by its catalogue id.

type GitHubConfirmations

type GitHubConfirmations interface {
	PutConfirmation(ctx context.Context, confirmation connection.Confirmation) error
	Confirmations(ctx context.Context) (map[string]connection.Confirmation, error)
}

GitHubConfirmations is where operators' confirmations of removal sets are kept.

type GitHubConnections

type GitHubConnections interface {
	Put(ctx context.Context, record connection.Record, credential connection.Credential) error
	List(ctx context.Context) ([]connection.Record, error)
	Credential(ctx context.Context, org string) (connection.Credential, bool, error)
	// SetOwner changes only a record's owner, under the object's version;
	// found is false when the organisation has no record.
	SetOwner(ctx context.Context, org, owner string) (previous string, found bool, err error)
	Delete(ctx context.Context, org string) error
	// RequestPass keeps an operator's request for a pass now; kept is false,
	// with when the last one was, if that is under connection.PassGap old.
	RequestPass(ctx context.Context, r connection.PassRequest) (kept bool, last time.Time, err error)
	// PassRequests reads every organisation's last request for a pass.
	PassRequests(ctx context.Context) (map[string]connection.PassRequest, error)
}

GitHubConnections is where connected organisations are kept: a record the console shows, a credential only the controller acts with.

type GitHubLinkApp

type GitHubLinkApp interface {
	PutLinkApp(ctx context.Context, record link.App, credential link.AppCredential) error
	LinkApp(ctx context.Context) (link.App, bool, error)
	LinkAppCredential(ctx context.Context) (link.AppCredential, bool, error)
	DeleteLinkApp(ctx context.Context) error
}

GitHubLinkApp is where the link App is kept: its record, which the console shows, and its credential, which redeems a person's authorization.

type GitHubLinks interface {
	List(ctx context.Context) ([]link.Link, error)
	Claim(ctx context.Context, claimed link.Link, now time.Time) ([]link.Link, error)
	Adopt(ctx context.Context, candidates []link.Link) ([]link.Link, map[int64]string, error)
	Invalidate(ctx context.Context, reason string, now time.Time) (int, error)
}

GitHubLinks is where people's links are kept.

type GitHubReports

type GitHubReports interface {
	Reports(ctx context.Context) (map[string]string, error)
}

GitHubReports is where the console reads what the GitHub controller last reported: the documents of the status contract, keyed as written.

type GitHubRunnerApps

type GitHubRunnerApps interface {
	Put(ctx context.Context, record runnerapp.Record, privateKey string) error
	List(ctx context.Context) ([]runnerapp.Record, error)
	PrivateKey(ctx context.Context, tier, org string) (string, bool, error)
	Delete(ctx context.Context, tier, org string) error
}

GitHubRunnerApps is where runner Apps are kept: every App's record and key, which the deployment hands to its runners.

type Grant

type Grant struct {
	// Consumer is namespace/serviceaccount, for the log. The subject the
	// API server returns is the identity; this is what an operator
	// reading the declaration recognises.
	Consumer string `yaml:"consumer" json:"consumer"`
	// Workspaces this consumer may ask about. Empty is every workspace.
	Workspaces []string `yaml:"workspaces,omitempty" json:"workspaces,omitempty"`
	// Domains this consumer may ask about, which is the API's own
	// routing key. Empty is every domain. Named alongside Workspaces
	// rather than instead of it because a consumer that cares about a
	// tenant should say the tenant, and one that cares about a mail
	// domain should say the domain; either resolves to the same test.
	Domains []string `yaml:"domains,omitempty" json:"domains,omitempty"`
	// Groups this consumer may see, by exact address or by a `*` suffix.
	// Empty is every group. A group outside the grant is absent from
	// ListGroups and not found by GetGroup, and does not appear in the
	// groups an address resolves to.
	Groups []string `yaml:"groups,omitempty" json:"groups,omitempty"`
	// Reads this consumer may perform. Empty is every read.
	Reads []Read `yaml:"reads,omitempty" json:"reads,omitempty"`
}

Grant is one consumer's, as the deployment declares it.

func GrantOf

func GrantOf(ctx context.Context) *Grant

GrantOf reads the grant back. Absent is full read: a handler reached without passing the guard is a local run, and the guard's own absence is already said out loud at start.

func (*Grant) Allows

func (g *Grant) Allows(read Read) bool

Allows reports whether a read class is granted.

func (*Grant) AllowsDomain

func (g *Grant) AllowsDomain(domain string, routing map[string]string) bool

AllowsDomain reports whether this consumer may ask about a domain.

routing maps domain to workspace and may be nil, in which case a grant written in workspaces admits nothing -- the fail-closed direction, and the honest one: without the map the hub cannot tell whether the domain belongs to a workspace this consumer holds.

func (*Grant) AllowsGroup

func (g *Grant) AllowsGroup(email string) bool

AllowsGroup reports whether a group address is inside the grant.

func (*Grant) AllowsWorkspace

func (g *Grant) AllowsWorkspace(workspace string, routing map[string]string) bool

AllowsWorkspace reports whether this consumer may ask about a workspace, for the calls that carry one directly.

func (*Grant) Everything

func (g *Grant) Everything() bool

Everything reports whether this grant withholds nothing, which is what a consumer declared with no grant gets and what the issuer has.

func (*Grant) KeepGroups

func (g *Grant) KeepGroups(emails []string) []string

KeepGroups filters a list of group addresses to the granted ones.

func (*Grant) NeedsRouting

func (g *Grant) NeedsRouting() bool

NeedsRouting reports whether enforcing this grant needs the hub's domain-to-workspace map. Only a grant written in workspaces does: one written in domains is already in the key every point lookup carries.

func (*Grant) ScopedToDirectories

func (g *Grant) ScopedToDirectories() bool

ScopedToDirectories reports whether this grant names particular directories. When it does not, no domain has to be resolved and the request path stays exactly as short as it is today.

func (*Grant) Validate

func (g *Grant) Validate() error

Validate refuses a grant that cannot mean what it says.

type KeyConnector

type KeyConnector interface {
	FromKey(ctx context.Context, key []byte, admin string) (hub.Workspace, backend.Backend, error)
}

KeyConnector is the second way in: a service-account key uploaded instead of a consent flow. A connector that cannot do it simply does not implement this.

type PasswordRecovery

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

PasswordRecovery is the shape for a hub outside Kubernetes: a generated password, kept only as an Argon2id digest with a random salt.

It carries a lock, so it is created once and used through a pointer; the lock serialises verification, which keeps the memory cost of one attempt from becoming the cost of as many as anyone cares to send.

func NewPasswordRecovery

func NewPasswordRecovery(password string) *PasswordRecovery

NewPasswordRecovery returns the password shape.

func (*PasswordRecovery) Kind

func (p *PasswordRecovery) Kind() string

Kind implements Recovery.

func (*PasswordRecovery) Prompt

func (p *PasswordRecovery) Prompt() Prompt

Prompt implements Recovery.

func (*PasswordRecovery) Verify

func (p *PasswordRecovery) Verify(_ context.Context, proof string) (string, error)

Verify implements Recovery.

type Prompt

type Prompt struct {
	// Label names the field.
	Label string
	// Intro is the sentence before it.
	Intro string
	// Command, when set, is shown as a command to run. It carries this
	// installation's own names rather than placeholders.
	Command string
	// Caution is the sentence after it, and is the reason showing any of
	// this is safe to do on a page anyone may load.
	Caution string
}

Prompt is what the sign-in page shows above the recovery field.

type Read

type Read string

Read is one class of question, and the unit a grant is written in. The classes group calls that leak the same thing: whether an address exists and what it holds, versus what the directory contains, versus what this hub serves at all.

const (
	// ReadResolve is ResolveUser, GetAccount and ResolveAccounts: what
	// one address, already known to the caller, resolves to.
	ReadResolve Read = "resolve"
	// ReadGroups is GetGroup and ListGroups: what the directory
	// contains, which is the enumeration a resolve-only consumer must
	// not have.
	ReadGroups Read = "groups"
	// ReadDescribe is Describe: which domains this hub serves. Discovery
	// itself is scoped, so a consumer granted one directory is not told
	// the others exist.
	ReadDescribe Read = "describe"
	// ReadProbe is Probe: whether a workspace's last read succeeded.
	ReadProbe Read = "probe"
)

type Recovery

type Recovery interface {
	// Kind is "token" or "password": what the recovery page asks for.
	Kind() string
	// Prompt is how the sign-in page explains this shape. Each shape
	// carries its own words, because the two are not variations on one
	// sentence: one is a command to run, the other a secret to have kept.
	Prompt() Prompt
	// Verify returns the identity the proof establishes.
	Verify(ctx context.Context, proof string) (string, error)
}

Recovery is the way in when the ordinary one is broken: nobody is in the operators group, the group was renamed, the directory will not answer.

It has two shapes because the honest answer depends on where the hub runs. In a cluster there is already an authority that says who is trusted — the API server — so recovery proves access to it and the hub stores no credential at all. Anywhere else there is nothing to prove access to, so a generated password is the only thing left.

type SignInConnector

type SignInConnector interface {
	Connector
	// SignInURL is where the browser goes to prove who somebody is.
	SignInURL(state string) (string, error)
	// Identify turns the callback's code into the address that
	// authenticated. Nothing else is taken from it: what that address may
	// do is decided by the directory and the policy.
	Identify(ctx context.Context, code string) (string, error)
}

SignInConnector is a connector that can also say who somebody is.

It is a different flow from Connector, not a parameter of it: consent is an administrator granting this hub read access to a company, and asks for the directory scopes; sign-in is a person proving who they are, and asks for nothing but their address. They return to different endpoints because the two endpoints have opposite authorisation — one adopts a workspace and demands an operator, the other is how a person becomes anyone at all.

type SlackCatalogueApps

type SlackCatalogueApps interface {
	Put(ctx context.Context, record catalogueapp.Record, credentials catalogueapp.Credentials) error
	List(ctx context.Context) ([]catalogueapp.Record, error)
	// Get is one App's record and credentials, installed or not.
	Get(ctx context.Context, id string) (catalogueapp.Record, catalogueapp.Credentials, bool, error)
}

SlackCatalogueApps is where catalogue Slack Apps are kept: every App's record, client credentials and bot token, by its catalogue id.

type SlackChannelRecords

type SlackChannelRecords interface {
	List(ctx context.Context) ([]connection.ChannelRecord, error)
	// Apply reads one record (nil when there is none), asks decide what it
	// becomes (nil deletes it) and writes that under the object's version.
	// decide is given every record as read, for a check across records.
	Apply(ctx context.Context, workspace, name string,
		decide func(current *reconcile.ConsoleChannel, all []connection.ChannelRecord) (*reconcile.ConsoleChannel, error)) error
}

SlackChannelRecords is where console channels' records are kept: the records the Slack controller reads.

type SlackSharedRecords

type SlackSharedRecords interface {
	List(ctx context.Context) ([]connection.SharedRecord, error)
	// Apply reads one record (nil when there is none), asks decide what it
	// becomes (nil deletes it) and writes that under the object's version.
	Apply(ctx context.Context, name string, decide func(current *reconcile.SharedChannel) (*reconcile.SharedChannel, error)) error
}

SlackSharedRecords is where Slack Connect channel definitions are kept: the records the Slack controller reads.

type SlackStatusReports

type SlackStatusReports interface {
	Reports(ctx context.Context) (map[string]string, error)
}

SlackStatusReports is what the Slack controller last reported, one document per workspace.

type SlackWorkspaces

type SlackWorkspaces interface {
	// Put writes a record and its credential.
	Put(ctx context.Context, record connection.Record, credential connection.Credential) error
	List(ctx context.Context) ([]connection.Record, error)
	// Get is one workspace's record and credential; found is false when
	// there is no credential.
	Get(ctx context.Context, workspace string) (connection.Record, connection.Credential, bool, error)
	// SetOwner changes only a record's owner, under the object's version;
	// found is false when the workspace has no record.
	SetOwner(ctx context.Context, workspace, owner string) (previous string, found bool, err error)
	// RequestPass keeps an operator's request for a pass now; kept is false,
	// with the time of the last request, when that was under a minute ago.
	RequestPass(ctx context.Context, request connection.PassRequest) (kept bool, last time.Time, err error)
	// PassRequests are the last request for each workspace.
	PassRequests(ctx context.Context) (map[string]connection.PassRequest, error)
	// Delete forgets a workspace and what belongs to it.
	Delete(ctx context.Context, workspace string) error
	PutConfirmation(ctx context.Context, confirmation connection.Confirmation) error
	// Confirmations are every confirmation by connection.ConfirmationKey.
	Confirmations(ctx context.Context) (map[string]connection.Confirmation, error)
}

SlackWorkspaces is where connected Slack workspaces are kept: each one's record, which the console shows, and credential, which only the Slack controller acts with. It also keeps the operators' confirmations of removal sets.

type TokenRecovery

type TokenRecovery struct {
	// Review is [kube.Client.ReviewToken].
	Review func(ctx context.Context, token string, audiences []string) (string, error)
	// Namespace and Account are what the sign-in page tells a person to
	// mint a token for. They are object names, not secrets: they are
	// visible to anyone who may read the namespace, the chart that
	// creates them is public, and none of it helps without the RBAC to
	// create a token — which is itself enough to reach the hub by other
	// means. Printing the real ones beats making somebody guess a release
	// name during an outage.
	Namespace string
	Account   string
	// Audience the token must have been minted for.
	Audience string
	// Subjects that may recover, as the API server spells them.
	Subjects []string
}

TokenRecovery proves access to the cluster the hub runs in.

Nothing is stored: no password, no digest, no Secret to rotate or to find in a backup. What may be presented is a ServiceAccount token minted for one audience and a few minutes, so the authority is the cluster's own RBAC — who may create a token for that account — which is where cluster privilege is supposed to be visible, is revocable by removing a binding, and lands in the cluster's audit log.

It also names who recovered. A shared password makes every recovery look like the same person.

func (*TokenRecovery) Kind

func (t *TokenRecovery) Kind() string

Kind implements Recovery.

func (*TokenRecovery) Prompt

func (t *TokenRecovery) Prompt() Prompt

Prompt implements Recovery: the command that mints a proof, with this installation's own names in it.

func (*TokenRecovery) Verify

func (t *TokenRecovery) Verify(ctx context.Context, proof string) (string, error)

Verify implements Recovery.

Jump to

Keyboard shortcuts

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