server

package
v1.76.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 66 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 BackupModule added in v1.75.0

type BackupModule interface {
	Status(ctx context.Context) (job.Status, error)
	List(ctx context.Context, limit int) ([]job.Info, error)
	RestoreStatus(ctx context.Context) (restorejob.Status, error)
}

BackupModule is the backup module and the restore function as the console reads them (*rpc.Client of internal/backup/rpc, which calls them as the caller class `console`). It only reads: the console starts neither a backup nor a restore, and the modules would refuse it.

type ClientVerifier

type ClientVerifier = googleconsole.ClientVerifier

ClientVerifier is a connector that can say whether the registered client would be accepted, without a person.

type CloudflareModule added in v1.76.0

type CloudflareModule = cfrpc.Admin

CloudflareModule is the Cloudflare module as the console uses it: the minter's own methods when this process holds one (cfrpc.Local), or the module in another process (cfrpc.Client) when `cloudflare.remote` is set. Either way the console reaches Cloudflare through the module and holds no minter credential reference. An interface so the handlers are tested with a fake module.

On-demand minting shares its code path with the token exchange (internal/issuer): both end at the minter's MintFor, which checks the grant and the lifetime and audits. What differs is only how the caller is established (a console session here, a verified proof there), which is why each builds its own cfrpc.Caller.

type Connector

type Connector = googleconsole.Connector

Connector starts and finishes an admin-consent flow for one backend.

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. The module knows which kind an id is, and who may operate it.

func (*Console) BeginGitHubCatalogueAppConnect

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

func (*Console) BeginGitHubConnect

BeginGitHubConnect starts connecting an organisation: the module decides who may and what is made, and this signs the state that pins the flow to this browser, which the module never holds the key for.

func (*Console) BeginGitHubLinkAppConnect

BeginGitHubLinkAppConnect starts creating the link App.

func (*Console) BeginGitHubRunnerAppConnect

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

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 carrying signed state; the header pins the flow to this browser.

func (*Console) ChangeGitHubOrganisationOwner

ChangeGitHubOrganisationOwner changes the directory recorded as a connected organisation's owner, or removes it: in the module.

func (*Console) ChangeSlackWorkspaceOwner

ChangeSlackWorkspaceOwner changes the directory recorded as a connected workspace's owner, or removes it.

func (*Console) CheckGitHubApp

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

func (*Console) CheckGitHubCatalogueApp

CheckGitHubCatalogueApp asks GitHub again about one catalogue App.

func (*Console) ConfirmGitHubRemovals

ConfirmGitHubRemovals lets exactly the removal set the operator saw go ahead.

func (*Console) ConfirmSlackRemovals

ConfirmSlackRemovals lets exactly the removal set the operator saw go ahead.

func (*Console) CreateSlackApp

CreateSlackApp creates a catalogue App in Slack.

func (*Console) CreateSlackChannel

CreateSlackChannel puts a channel under management.

func (*Console) CreateSlackSharedChannel

CreateSlackSharedChannel defines a Slack Connect channel.

func (*Console) DeleteSlackChannel

DeleteSlackChannel forgets a console channel's record, and archives the channel when asked.

func (*Console) DeleteSlackSharedChannel

DeleteSlackSharedChannel forgets a Slack Connect channel's record.

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.

func (*Console) DisconnectGitHubLinkApp

DisconnectGitHubLinkApp forgets the link App and makes every link made with it unverifiable.

func (*Console) DisconnectGitHubOrganisation

DisconnectGitHubOrganisation revokes, then forgets: in the module.

func (*Console) DisconnectGitHubRunnerApp

DisconnectGitHubRunnerApp uninstalls a runner App, then forgets it.

func (*Console) DisconnectSlackWorkspace

DisconnectSlackWorkspace revokes the bot token and forgets the connection.

func (*Console) Explain

Explain implements the operator contract. It discloses what a proof would effectively get, so it is for two callers only: an operator, whose every look-up is recorded, and the roster controllers, which are workloads (a ServiceAccount or an AWS role) the policy places in policy.GroupRosterControllers, which ask in bulk and are not recorded. Any other workload, whatever role it holds, is treated as a person: an operator is served and recorded. Anybody else, a person asking about themselves included, is refused with a fixed text and the refusal is recorded.

func (*Console) GetBackupStatus added in v1.75.0

GetBackupStatus implements the administrator's contract.

func (*Console) GetCloudflareCredential added in v1.74.0

GetCloudflareCredential implements the person contract. The grant is the minter's to check, and it records the refusal.

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 reads one App.

func (*Console) GetGitHubStatus

GetGitHubStatus implements the GitHub page.

func (*Console) GetPolicy

GetPolicy implements the operator contract.

func (*Console) GetRestoreStatus added in v1.75.0

GetRestoreStatus implements the administrator's 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.

ImportGitHubLinks adopts approved pairings as links.

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) ListBackups added in v1.75.0

ListBackups implements the administrator's contract.

func (*Console) ListCloudflare added in v1.74.0

ListCloudflare implements the administrator's contract.

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 lists the last installation tokens asked of an App.

The ring of them is written where the decision to mint is made, which is the issuer, and read here. A GitHub module in another process mints but keeps no ring (the issuer asks it for each token and records the request itself), so there is nothing to forward the question to: the module is asked only which App this is and whether the person may operate it, and the ring answers.

func (*Console) ListGitHubApps

ListGitHubApps lists 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) ListMyCloudflarePresets added in v1.74.0

ListMyCloudflarePresets implements the person contract.

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 Slack Connect 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: in the module, which records the request and ticks itself.

func (*Console) RequestSlackPass

RequestSlackPass asks the Slack controller to pass over a workspace now.

func (*Console) ResolveDirectoryGroups

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

func (*Console) RevokeCloudflareToken added in v1.74.0

RevokeCloudflareToken implements the operator contract.

func (*Console) RotateCloudflarePreset added in v1.74.0

RotateCloudflarePreset implements the operator contract.

func (*Console) RotateGitHubAppWebhook added in v1.74.0

RotateGitHubAppWebhook gives an App's webhook a new secret.

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 a console channel's mode, ignore list and sources.

func (*Console) UpdateSlackSharedChannel

UpdateSlackSharedChannel changes a Slack Connect channel's guests, sources and visibility.

func (*Console) UploadKey

UploadKey implements the operator contract. The key passes through this handler in memory on its way to the module, which is the only holder of it: it is bounded here at 64 KB, and no log line or error carries it.

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
	// GitHubOrgAppName is the name the organisation's App is created with,
	// empty for the default (<org>-access-roster). Nil is all defaults.
	GitHubOrgAppName func(org string) string
	// GitHubLinkAppName is the name the link App is created with, empty for
	// the default (<owner>-access-roster-link).
	GitHubLinkAppName string
	// 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 *ghconsole.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 *slackconsole.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 []slackconsole.APIOption
	// 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 *ghconsole.MintRing
	// GitHubHTTP makes the calls to GitHub that connecting and
	// disconnecting need. Nil is a client with a short timeout.
	GitHubHTTP *http.Client
	// Cloudflare is the minter of Cloudflare credentials. Nil is a
	// deployment with no `cloudflare` section and no Cloudflare page; it is
	// connected late, by [ConsoleServer.UseCloudflare].
	Cloudflare CloudflareModule
	// Backup is the backup module and the restore function, read through
	// modcall as the caller class `console`. Nil is a deployment with neither
	// and no Backups page; it is connected late, by [ConsoleServer.UseBackup].
	Backup BackupModule
	// WebhookHTTP sends the signed ping a webhook rotation checks a new
	// target with. Nil is a client with a short timeout that follows no
	// redirect, so a target that moves counts as one that refused.
	WebhookHTTP *http.Client
	// WebhookPingWindow is how long a rotation waits for the new target to
	// accept the new secret, and WebhookPingEvery how often it asks. Zero is
	// two minutes and five seconds.
	WebhookPingWindow, WebhookPingEvery time.Duration
	// 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/concepts/sluis/ports.md). Nil
	// notifies nobody.
	Trigger port.Trigger
	// Modules is how the console reaches the provider modules for the
	// operations that move into them (docs/decisions/0071, v1.76 D-v176-1 b),
	// each as the caller class `console`. A nil caller is the in-process
	// servers, [modcall.Local]; the wiring sets one per module it split out.
	Modules consolecall.Callers
}

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) UseBackup added in v1.75.0

func (s *ConsoleServer) UseBackup(m BackupModule)

UseBackup connects the Backups page to the modules, with the same timing as ConsoleServer.UseCloudflare. Without it the console has no Backups page.

func (*ConsoleServer) UseCloudflare added in v1.74.0

func (s *ConsoleServer) UseCloudflare(m CloudflareModule)

UseCloudflare connects the Cloudflare page to the module, with the same timing as ConsoleServer.UseAuditQuery: the module is reached after the console is made. Without it the console has no Cloudflare page.

func (*ConsoleServer) UseGitHub added in v1.76.0

func (s *ConsoleServer) UseGitHub(c modcall.Caller)

UseGitHub connects the console's GitHub operations to the module in another process, with the same timing as ConsoleServer.UseCloudflare. Without it the module is served in this process.

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) UseSessions added in v1.76.0

func (s *ConsoleServer) UseSessions(sessions *SessionsService)

UseSessions connects the console to the issuer's session service in this process, with the same timing as ConsoleServer.UseSignedIn: the issuer does not exist yet when the console is built.

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.ResponseWriter, *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) UseSlack added in v1.76.0

func (s *ConsoleServer) UseSlack(m modcall.Caller)

UseSlack connects the Slack pages to the module in another process, called as the caller class `console`. Without it they call the module in this one.

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
	// Maintenance are the gates over the maintenance flag of the modules whose
	// records the console writes. A write of a module under maintenance is
	// refused, a read is served, and whoami says so. Nil serves everything.
	Maintenance maintenance.Set
	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.
	//
	// It is handed the response as well as the request because a sign-in
	// can be ENDED by asking -- past the absolute limit, or for a person
	// the directory no longer admits -- and the browser's cookie is then
	// cleared on the way out, as the issuer's own pages clear it.
	//
	// Nil is the split deployment, where a gateway forwards a bearer.
	SignedIn func(http.ResponseWriter, *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 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 = googleconsole.CredentialReopener

CredentialReopener is a connector that can turn a stored credential back into a reader.

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 = catalogueapp.Store

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

type GitHubConfirmations

type GitHubConfirmations = connection.ConfirmationStore

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

type GitHubConnections

type GitHubConnections = connection.Store

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

type GitHubLinkApp

type GitHubLinkApp = link.AppStore

GitHubLinkApp is where the link App is kept.

type GitHubLinks = link.Links

GitHubLinks is where people's links are kept.

type GitHubReports

type GitHubReports = status.ReportReader

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 = runnerapp.Store

GitHubRunnerApps is where runner Apps are kept.

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 = googleconsole.KeyConnector

KeyConnector is a connector that takes a service-account key instead of a consent flow.

type PasswordRecovery

type PasswordRecovery struct {
	// Where is where the password is kept, as the secrets source describes it
	// ("ssm /sluis/<instance>/internal/config/recovery/password"): the console
	// shows the operator this installation's own location. Empty for a
	// generated password.
	Where string
	// 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 NewLazyPasswordRecovery added in v1.75.0

func NewLazyPasswordRecovery(ttl time.Duration, load func(context.Context) (string, error)) *PasswordRecovery

NewLazyPasswordRecovery returns the password shape over a password that is read when the first proof arrives, and read again after ttl, so that opening it reads no secret and a rotated password takes effect without a restart. A password that cannot be read, or is empty, refuses every proof.

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(ctx 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 SessionsService added in v1.76.0

type SessionsService struct {
	// Handler serves the session service at /<package>.SessionService/<method>.
	Handler http.Handler
	// As returns ctx carrying the caller the service is to take the request
	// for: identity, and whether they may act on anybody's sessions.
	As func(ctx context.Context, identity string, operator bool) context.Context
}

SessionsService is the issuer's session service as the console reaches it: in this process, as the person signed in to the console.

The console's Sessions page and the sessions on a person's or a client's page used to call the issuer from the browser with the issuer's own session cookie. A sign-in that never touched the issuer -- the console's recovery sign-in -- has no such cookie, so every one of those calls was refused. The console has already established who is signed in to it, so it passes that on itself and the browser needs no second session.

type SignInConnector

type SignInConnector = googleconsole.SignInConnector

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

type SlackCatalogueApps

type SlackCatalogueApps = slackconsole.CatalogueApps

SlackCatalogueApps is where catalogue Slack Apps are kept.

type SlackChannelRecords

type SlackChannelRecords = slackconsole.ChannelRecords

SlackChannelRecords is where console channels' records are kept.

type SlackSharedRecords

type SlackSharedRecords = slackconsole.SharedRecords

SlackSharedRecords is where Slack Connect channel definitions are kept.

type SlackStatusReports

type SlackStatusReports = slackconsole.StatusReports

SlackStatusReports is what the Slack controller last reported.

type SlackWorkspaces

type SlackWorkspaces = slackconsole.Workspaces

SlackWorkspaces is where connected Slack workspaces are kept.

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