Console services, roles, routes and errors. The proto files under proto/ are the source of truth.
Connect also speaks JSON over HTTP, so curl works. The proto package is sluis.v1; the legacy directoryroster.v1 and accessissuer.v1 are still served. Tables write <package> for either package.
WorkspaceService, SettingsService, AccessService, GitHubService, SlackService, SlackChannelService, SlackSharedChannelService, SlackAppService, CloudflareService, BackupService, OutlineService, the SPA, and the audit QueryService forwarded under /audit/
the console, same origin under console.mount; a workload with its own ServiceAccount token; sluisctl's bearer token, for BackupService only
/<package>.*/
sluis.v1.SessionService
a browser at the issuer's host (SSO cookie); a person signed in at sluisctl or the console, by bearer
id, backend, sync_groups, discovered_groups, domains with authoritative/conflict/served/owned flags and reason, admin, credential type, connected_by/at, health, snapshot_at, declared
BeginConnect
operator
backend
consent_url
sets the state cookie; state names the operator
Reconnect
operator
workspace_id
consent_url
callback requires the same tenant, then replaces the credential
UploadKey
operator
backend, key (bytes), admin
workspace
domain-wide-delegation service-account key; creates or re-credentials
SetServedDomains
operator
workspace_id, domains[]
workspace
empty means all, including later ones; undiscovered domain is InvalidArgument; declared workspace is FailedPrecondition; excluded accounts leave the snapshot at once; the re-read is detached
SetSyncedGroups
operator
workspace_id, groups[]
workspace
empty keeps every group in served domains; unknown group is InvalidArgument; declared workspace is FailedPrecondition
Probe
operator
workspace_id
health, domains[]
credential check now, domains re-read
Refresh
operator
workspace_id
snapshot_at
full snapshot now
GET /connect/google/callback?code&state
route
verifies state cookie, exchanges code, discovers tenant and domains, probes, stores, redirects
Disconnect
operator
workspace_id
none
revokes at the backend, deletes the Secret and record; failed_precondition for a declared workspace
never the client secret; intervals are chart values; no SetOAuthClient exists; setup lists two redirect URIs per backend, and a client missing one fails on that flow; key_connectors also accept an uploaded key
identity{email, subject, source, role, groups[], given_name, family_name}, version
groups are internal groups from the policy
Explain
operator, or a workload in the group all:sluis:roster-controller (the roster controllers, which the policy's service_account or aws matchers place there); anyone else gets not authorised. An operator's look-up is audited
one proof: email?, github{repository, owner, ref, workflow, environment, visibility}? or service_account{cluster, namespace, name}?
a client names its Secret, never the secret; github_grants is read from the catalogue and makes no GitHub call; absent last_minted means not remembered, not unused
effective is false once recovery is turned off, and while the switch cannot be read; superseded_* is an earlier turn-off that a new recovery.generation overrode; can_disable is the server's own answer for this caller
DisableRecovery
operator, signed in through the identity provider: a recovery sign-in, a workload, an address-less identity, an address from an unverified gateway header and a scoped operator are refused with permission_denied
the only switch: nothing turns recovery on. Idempotent. failed_precondition when recovery is not enabled or the State cannot keep the record. Recorded as roster.recovery.disabled; the switch stands when the trail is down and audit_pending says so (ADR 0075)
absence is not evidence: an unreadable snapshot contributes no accounts, so confirm with Explain before removing access; a group the policy lacks has no holders
nested groups expanded, each once, to at most 8 levels and 500 groups; truncated forbids removing on absence; a directory the caller cannot view returns found=false
last requests, newest first, never the token; memory of this replica, lost on restart; failed_precondition for an App that mints none; unavailable is not "nothing asked"
BeginGitHubAppConnect
operator
id, owner, owner_directory
url, manifest
create or finish installing any App kind; owner applies to the link App only
forgets record and key even if uninstall fails; invalidated counts links, link App only
CheckGitHubApp
operator
id
app
asks GitHub again, bypassing the one-minute cache
RotateGitHubAppWebhook
operator
id
app
catalogue App only; waits for the new target to accept a signed ping; a failure before that restores the old secret
BeginGitHubConnect
operator
org, owner_directory
url, manifest
sets the state cookie; with manifest, POST it as form field manifest to url, else url is the install page; failed_precondition for an unbound or already installed organisation, or no Kubernetes state
RequestGitHubPass
owner operator or installation-wide
org
requested_at
writes _pass.<org>.json, noticed within 30 seconds; failed_precondition if the App is not installed; resource_exhausted within a minute of the last; audit roster.github_org.pass_requested
ChangeGitHubOrganisationOwner
installation-wide operator
org, owner_directory
none
empty removes; new owner must be a connected directory; audit roster.github_org.owner_changed
DisconnectGitHubOrganisation
operator
org
uninstalled, detail, app_settings_url
forgets record and key even if uninstall fails; the owner deletes the App at app_settings_url
BeginGitHubLinkAppConnect
operator
owner
url, manifest
public App, emails: read only, installed nowhere; failed_precondition if one exists or no Kubernetes state
DisconnectGitHubLinkApp
operator
none
invalidated, app_settings_url
self-links become unverifiable
BeginGitHubRunnerAppConnect
operator
org, tier
url, manifest
invalid_argument for a tier not in githubRunnerApps.tiers; failed_precondition for an unbound organisation or installed App
DisconnectGitHubRunnerApp
operator
org, tier
uninstalled, detail, app_settings_url
runners registered with it stop getting jobs
BeginGitHubCatalogueAppConnect
operator
id
url, manifest
invalid_argument for an undeclared id; failed_precondition if installed or no Kubernetes state.
DisconnectGitHubCatalogueApp
operator
id
uninstalled, detail, app_settings_url
works for an App the catalogue no longer declares; the App stays on GitHub
CheckGitHubCatalogueApp
operator
id
app
asks GitHub again, bypassing the one-minute cache
ConfirmGitHubRemovals
operator
org, fingerprint
none
failed_precondition if the report shows a different set; lapses after a day
at most 500 records; each must be approved, vouched and live in the directory, and a member of a connected organisation; never displaces a person's link
every App is checked against its declaration; the link App is not (no key), and reason says so; secret is empty without Kubernetes state
Report
ConfigMap <release>-github-status, key <login>.json per organisation; the service creates it, the controller replaces data; reports_available=false only without Kubernetes state
never a client secret or bot token; a scoped viewer sees its own workspaces; declared=false marks a connected workspace the policy dropped
BeginSlackWorkspaceConnect
operator
workspace, configuration_token?, owner?
url
creates the Slack App from its manifest or prepares a reinstall; the token is used once, never stored or logged; needed to create and when scopes grew; sets a cookie pinning the browser
RequestSlackPass
owner operator or installation-wide
workspace
requested_at
writes _pass.<workspace>.json, noticed within 30 seconds; resource_exhausted within a minute; no audit action
revokes the bot token, forgets the connection; forget_anyway forgets even if Slack refuses revocation, and the token stays valid until the App is deleted in Slack; without it a refused revocation is unavailable and keeps the connection; channel records stay
ConfirmSlackRemovals
operator
workspace, channel?, fingerprint
none
empty channel is the workspace breaker; one confirmation covers every gate of that fingerprint; lapses after a day
{name, id, private, mode, shared, host, state, reason, members[], breaker, removal_confirmation, console, sources[]}; console marks a console channel, sources the internal groups of a git channel
Report: ConfigMap <release>-slack-status, one key per workspace.
state is not_reported, pending, active, held, invalid; discovered is capped per workspace
CreateSlackChannel
owner operator or installation-wide
channel{...}
channel
record _channel.<workspace>.<name>.json; needs sources or members; modeextend (default) or strict (private only); already_exists for a name or channel id already managed, here or as Slack Connect; invalid_argument for an undiscovered id, a visibility mismatch, a person as a group or a group as a person; failed_precondition for a workspace with no owner; audit roster.slack_console_channel.created
UpdateSlackChannel
same
channel{...}
channel
workspace, name, channel_id, private are immutable; audit roster.slack_console_channel.updated
DeleteSlackChannel
same
workspace, name, archive?
note, archived
forgets the record, channel stays; archive pre-checks and calls conversations.archive; FailedPrecondition for dry-run, shared or invisible channels; Unavailable if Slack is down; audit roster.slack_console_channel.deleted, roster.slack_channel.archived
every declared workspace of the Outline controller; report_json is its status document, version 1; no message carries the API key back
ConnectOutlineWorkspace
operator
workspace, url, api_key
key_user, key_user_ignored
asks Outline auth.info; refuses a key that is not an admin's, or of another team than the one connected before; audit roster.outline_workspace.connected
DisconnectOutlineWorkspace
operator
workspace
none
forgets the record, the key, the confirmations and the pass request; audit roster.outline_workspace.disconnected
RequestOutlinePass
operator
workspace
kept, last
at most once a minute
ConfirmOutlineBreaker
operator
workspace, scope, fingerprint
none
only the latest report's set; lapses after 24 hours; audit roster.outline_breaker.confirmed
restore.status; no RPC starts or resumes a restore
RunBackup
a group in the policy's backup.runGroups
resume?
available, outcome (started, contended, idle), started
starts backup.run as console without waiting, with by the caller's address, audited as that person; read the run with GetBackupStatus; contended while a run goes or after this person's last start; refused in maintenance; Unavailable when the module does not answer
newest first; how is HOW_CODE, HOW_DEVICE, HOW_EXCHANGE; page_size default 50, cap 500; sign_ins on page one only; operator also for client-only, global and contains listings
invalid_argument for an empty or unparseable address; internal for faults that are not a backend read; a failed backend read is a non-authoritative answer
WorkspaceService
permission_denied; not_found unknown id; failed_precondition operation refused for the workspace kind; invalid_argument unparseable key or admin without domain
AccessService
unauthenticated without a session; permission_denied; not_found undeclared group; failed_precondition deployment-owned state
SlackChannelService, SlackSharedChannelService
aborted when another edit changed the records first: reload and retry; failed_precondition with no Kubernetes state
SessionService
unauthenticated without a cookie or such a token, and for an ID token, another client's token, a workload's exchange or an ended session; permission_denied for another identity, or a client-only, global or contains listing, without operator; invalid_argument for an empty identity, or every_identity with anything but client_id
sluis sends records with its projected ServiceAccount token
audit.v1.QueryService
the console forwards it under <mount>/audit/ with a token minted for the signed-in person; only POST to its methods passes, else 404; 401 signed out; 403 when the audit audience does not admit the person; 502 when unreachable
RFC 8693 exchange on /token. It applies when requested_token_type is the type below and audience starts github-app:.
POST /token, application/x-www-form-urlencoded:
Parameter
Value
grant_type
urn:ietf:params:oauth:grant-type:token-exchange
requested_token_type
urn:sluis:params:oauth:token-type:github-installation-token (Go: tokens.TypeSluisGitHubInstallationToken); the old spelling urn:access-roster:params:oauth:token-type:github-installation-token (tokens.TypeGitHubInstallationToken) is still accepted. The response's issued_token_type repeats the one asked
audience
github-app:<catalogue id>, one
subject_token
GitHub Actions token for the issuer URL, federated ServiceAccount token, or sign-in access token
GitHub's installation token: Authorization: Bearer, or x-access-token as git password
expires_in
from GitHub's expires_at
repositories, permissions
what GitHub granted, not what was asked; repositories is absent when not narrowed
Decision
the first grant in catalogue order, for the proof's groups, that covers all of the request; named repositories must all match it; none requires a ["*"] grant; GitHub gets the narrowing explicitly
Errors are RFC 6749 JSON, {"error": "...", "error_description": "..."}:
error
Status
When
invalid_request
400
malformed parameters, or actor_token
invalid_client
401
a presented client other than the audience failed to authenticate
invalid_grant
400
the subject token is not a proof
invalid_target
400
App undeclared, not created, not installed or uninstalled; or no grant names a group the proof holds
invalid_scope
400
wider than any one grant, or GitHub refused the narrowing (422)
server_error
500
GitHub or the App's key failed
Each request is one roster.github_token.minted record (fields), never with the token.
# The projected ServiceAccount token, minted for config.exchange.audience, is the bearer.TOKEN=$(cat/var/run/secrets/sluis/token)# WhoAmI over GET (Connect's idempotent-GET encoding)
curl-s-H"authorization: Bearer $TOKEN"\'http://sluis.<namespace>.svc:8080/console/<package>.AccessService/WhoAmI?encoding=json&message=%7B%7D'# ListHolders over POST
curl-s-H"authorization: Bearer $TOKEN"-H'content-type: application/json'\-d'{"group":"prod:k8s:viewer"}'\http://sluis.<namespace>.svc:8080/console/<package>.AccessService/ListHolders