Documentation
¶
Overview ¶
Package console is the Google module's half of the console: the Workspace service (the directories an operator connects), the consent and the key upload that connect one, and the sign-in's Identify. The console in the issuer authenticates a person and asks this package, as the caller class `console` (consolecall); the module holds the OAuth client secret, the service-account keys, the workspace records and the refresh writer, and the issuer holds none of them (docs/decisions/0071, v1.76 D-v176-1 b).
The sign-in is the one method the issuer itself calls (`google.identify`, caller class `issuer`): it needs the client secret, once per login.
Index ¶
- Constants
- Variables
- func BackendEnum(kind string) directoryrosterv1.Backend
- func BackendKind(b directoryrosterv1.Backend) string
- func ConnectError(err error) error
- func DomainsProto(domains []hub.DomainStanding) []*directoryrosterv1.WorkspaceDomain
- func ProviderName(kind string) string
- func Register(s *modcall.Server, svc *Service)
- func RegisterLazy(s *modcall.Server, log *slog.Logger, ...)
- func Stamp(t time.Time) *timestamppb.Timestamp
- func WorkspaceProto(v *hub.WorkspaceView) *directoryrosterv1.Workspace
- type BeginRequest
- type BeginResponse
- type Client
- func (c *Client) BeginConnect(ctx context.Context, caller consolecall.Caller, kind, state string) (string, error)
- func (c *Client) CompleteConsent(ctx context.Context, caller consolecall.Caller, req CompleteRequest) (CompleteResponse, error)
- func (c *Client) Disconnect(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.DisconnectResponse, error)
- func (c *Client) ListWorkspaces(ctx context.Context, caller consolecall.Caller) (*directoryrosterv1.ListWorkspacesResponse, error)
- func (c *Client) Probe(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.ProbeResponse, error)
- func (c *Client) Reconnect(ctx context.Context, caller consolecall.Caller, workspace, state string) (string, error)
- func (c *Client) Refresh(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.RefreshResponse, error)
- func (c *Client) SetServedDomains(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.SetServedDomainsResponse, error)
- func (c *Client) SetSyncedGroups(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.SetSyncedGroupsResponse, error)
- func (c *Client) UploadKey(ctx context.Context, caller consolecall.Caller, ...) (*directoryrosterv1.UploadKeyResponse, error)
- type ClientVerifier
- type CompleteRequest
- type CompleteResponse
- type Connector
- type CredentialReopener
- type KeyConnector
- type RemoteConnector
- func (*RemoteConnector) AuthURL(string) (string, error)
- func (*RemoteConnector) Exchange(context.Context, string, string) (hub.Workspace, backendpkg.Backend, error)
- func (*RemoteConnector) FromKey(context.Context, []byte, string) (hub.Workspace, backendpkg.Backend, error)
- func (r *RemoteConnector) Identify(ctx context.Context, code string) (string, error)
- func (*RemoteConnector) Kind() string
- func (r *RemoteConnector) SignInURL(state string) (string, error)
- type Service
- func (s *Service) BeginConnect(ctx context.Context, c consolecall.Caller, req BeginRequest) (BeginResponse, error)
- func (s *Service) CompleteConsent(ctx context.Context, c consolecall.Caller, req CompleteRequest) (CompleteResponse, error)
- func (s *Service) Disconnect(ctx context.Context, c consolecall.Caller, ...) (*directoryrosterv1.DisconnectResponse, error)
- func (s *Service) Identify(ctx context.Context, code string) (string, error)
- func (s *Service) ListWorkspaces(ctx context.Context, c consolecall.Caller, ...) (*directoryrosterv1.ListWorkspacesResponse, error)
- func (s *Service) Probe(ctx context.Context, c consolecall.Caller, req *directoryrosterv1.ProbeRequest) (*directoryrosterv1.ProbeResponse, error)
- func (s *Service) Reconnect(ctx context.Context, c consolecall.Caller, req BeginRequest) (BeginResponse, error)
- func (s *Service) Refresh(ctx context.Context, c consolecall.Caller, ...) (*directoryrosterv1.RefreshResponse, error)
- func (s *Service) Server() *modcall.Server
- func (s *Service) SetServedDomains(ctx context.Context, c consolecall.Caller, ...) (*directoryrosterv1.SetServedDomainsResponse, error)
- func (s *Service) SetSyncedGroups(ctx context.Context, c consolecall.Caller, ...) (*directoryrosterv1.SetSyncedGroupsResponse, error)
- func (s *Service) SignInURL(state string) (string, error)
- func (s *Service) UploadKey(ctx context.Context, c consolecall.Caller, ...) (*directoryrosterv1.UploadKeyResponse, error)
- type SignInConnector
Constants ¶
const ( MethodListWorkspaces = "console.list_workspaces" MethodBeginConnect = "console.begin_connect" MethodReconnect = "console.reconnect" MethodCompleteConsent = "console.complete_consent" MethodUploadKey = "console.upload_key" MethodSetServedDomains = "console.set_served_domains" MethodSetSyncedGroups = "console.set_synced_groups" MethodProbe = "console.probe" MethodRefresh = "console.refresh" MethodDisconnect = "console.disconnect" // MethodIdentify turns a sign-in's code into the address that authenticated. // It answers the caller class `issuer` and no other: it needs the OAuth // client secret, and the console has no business with a sign-in. MethodIdentify = "identify" // MethodSignInURL is where the browser goes to sign in. It answers the // issuer alone, like [MethodIdentify]: the module holds the OAuth client, so // the issuer holds nothing to build the address from. MethodSignInURL = "sign_in_url" )
The console methods, answered to the caller class `console` alone (consolecall.MethodPrefix), and the two the issuer calls.
const ( CodeInvalidArgument = "invalid_argument" CodeNotFound = "not_found" CodeFailedPrecondition = "failed_precondition" CodeUnimplemented = "unimplemented" CodeUnauthenticated = "unauthenticated" )
The codes the module answers with besides modcall's own: each is a refusal of the person's request, and its message is safe to show them.
const CallerIssuer = "issuer"
CallerIssuer is the caller class of the issuer, the only one that signs somebody in.
const CompleteDeadline = 25 * time.Second
CompleteDeadline bounds the consent's completion: the exchange with Google and the first read of the directory happen in one call, and the browser is waiting on it.
const MaxKeyBytes = 64 << 10
MaxKeyBytes bounds a service-account key uploaded through the console. A key file is about 2 KB; the bound is generous and exists so that nothing the issuer forwards can be large.
const Module = consolecall.ModuleGoogle
Module is the module the methods belong to.
const RefusalText = "not authorised"
RefusalText is what a caller who may not is told, whatever the reason. The reason is the module's log's.
Variables ¶
var ConsentCauses = []string{
"The directory API is not enabled in the cloud project that owns this hub's OAuth client. " +
"The message above names the project and the page that enables it.",
"The API was enabled moments ago. Enabling propagates over a few minutes; try again.",
"The account that consented is not an administrator of the workspace, or lacks the " +
"privileges to read users and groups.",
"The consent was granted with a personal account, or with an account in a different " +
"workspace than the one intended. The account chooser remembers the last one used.",
}
ConsentCauses is what to check when a directory refuses the credential its own consent screen has just granted. Ordered by how often each one is the answer, because a list read top-down is a list whose order is a claim about likelihood.
var ConsentRedirects = map[string]string{ "google": google.CallbackPath, }
ConsentRedirects is where a backend's CONSENT flow comes back: always the console, because connecting a directory is the console's job -- but at the HOST ROOT, never under route.pathPrefix. /connect is a bootstrap path, served on its own HTTPRoute precisely so the operator connecting the FIRST directory -- who by definition no directory can vouch for yet -- is not sent through whatever gates the console's own path. A redirect URI registered under the prefix would be one Google never returns to.
Read from the backend for the same reason the scopes are: a copy would drift, and the drift surfaces in a cloud console's own words, much later, naming nothing useful.
var ErrRemote = errors.New("google: this belongs to the Google module, which the console reaches by a call")
ErrRemote is what a connector method that belongs to the module answers.
var Scopes = map[string][]string{ "google": google.Scopes, }
Scopes is what each backend is asked for, all read-only. It reads the list from the backend rather than repeating it: a copy would drift, and the console would tell an operator to grant one set while the hub asked for another.
var SignInRedirects = map[string]string{ "google": google.SignInCallbackPath, }
SignInRedirects is where a backend's SIGN-IN flow comes back, which is a different service. With one shared OAuth client -- one client for the hub and the issuer, so there is one secret to rotate -- the sign-in redirect belongs to the ISSUER's hostname, not the console's.
Functions ¶
func BackendEnum ¶
func BackendEnum(kind string) directoryrosterv1.Backend
BackendEnum is BackendKind's inverse.
func BackendKind ¶
func BackendKind(b directoryrosterv1.Backend) string
BackendKind turns the contract's enum into a backend's own name.
func ConnectError ¶
ConnectError is an error of the module as the console's Connect handler returns it. The module's refusals keep their fixed words; a transport failure says the module could not be reached and nothing else, since what went wrong is the log's.
func DomainsProto ¶
func DomainsProto(domains []hub.DomainStanding) []*directoryrosterv1.WorkspaceDomain
DomainsProto is a workspace's domains as the contract tells them.
func ProviderName ¶
ProviderName is a backend as a person calls it.
func Register ¶
Register makes the service answer on s: the console methods to the caller class `console` alone, and the two the issuer calls to the class `issuer` alone. A method that failed with something the person should read keeps its words; any other failure is the transport's, with no detail.
func RegisterLazy ¶
func RegisterLazy(s *modcall.Server, log *slog.Logger, open func(context.Context) (*Service, error))
RegisterLazy is Register for a function that opens the service when the first call arrives: a function whose other invocations (a scheduled refresh) do not need the hub, the connectors or the secrets this assembles. An open that fails is told as the module being unavailable, and tried again by the next call. log is where a failed open is written.
func Stamp ¶
func Stamp(t time.Time) *timestamppb.Timestamp
Stamp is a time as the contract carries it: the zero time is nil.
func WorkspaceProto ¶
func WorkspaceProto(v *hub.WorkspaceView) *directoryrosterv1.Workspace
WorkspaceProto is a connected workspace as the contract tells it.
Types ¶
type BeginRequest ¶
type BeginRequest struct {
Backend string `json:"backend,omitempty"`
Bind string `json:"bind,omitempty"`
State string `json:"state"`
}
BeginRequest starts a consent. State is the issuer's signed state, which the provider hands back at the callback; the module never signs one. Backend names the directory for a new connection; Bind names the workspace being reconnected (and Backend is then the one it was connected with).
type BeginResponse ¶
type BeginResponse struct {
URL string `json:"url"`
}
BeginResponse is where the browser goes to consent.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the console's view of the module: one method per console method, each sent as the caller class `console`.
func NewClient ¶
NewClient is the console's calls to the Google module through c, which sends as the class `console`.
func (*Client) BeginConnect ¶
func (c *Client) BeginConnect(ctx context.Context, caller consolecall.Caller, kind, state string) (string, error)
BeginConnect asks where the browser goes to consent to a new connection.
func (*Client) CompleteConsent ¶
func (c *Client) CompleteConsent(ctx context.Context, caller consolecall.Caller, req CompleteRequest) (CompleteResponse, error)
CompleteConsent forwards the callback's code. It is never retried: the code is single-use, so a second send after a lost answer would find it spent and report a failure for a connection that was made. It is bounded by CompleteDeadline.
func (*Client) Disconnect ¶
func (c *Client) Disconnect( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.DisconnectRequest, ) (*directoryrosterv1.DisconnectResponse, error)
Disconnect implements the console's call of the same name.
func (*Client) ListWorkspaces ¶
func (c *Client) ListWorkspaces(ctx context.Context, caller consolecall.Caller) (*directoryrosterv1.ListWorkspacesResponse, error)
ListWorkspaces implements the console's call of the same name.
func (*Client) Probe ¶
func (c *Client) Probe(ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.ProbeRequest) (*directoryrosterv1.ProbeResponse, error)
Probe implements the console's call of the same name.
func (*Client) Reconnect ¶
func (c *Client) Reconnect(ctx context.Context, caller consolecall.Caller, workspace, state string) (string, error)
Reconnect asks the same for an existing workspace.
func (*Client) Refresh ¶
func (c *Client) Refresh(ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.RefreshRequest) (*directoryrosterv1.RefreshResponse, error)
Refresh implements the console's call of the same name.
func (*Client) SetServedDomains ¶
func (c *Client) SetServedDomains( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.SetServedDomainsRequest, ) (*directoryrosterv1.SetServedDomainsResponse, error)
SetServedDomains implements the console's call of the same name.
func (*Client) SetSyncedGroups ¶
func (c *Client) SetSyncedGroups( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.SetSyncedGroupsRequest, ) (*directoryrosterv1.SetSyncedGroupsResponse, error)
SetSyncedGroups implements the console's call of the same name.
func (*Client) UploadKey ¶
func (c *Client) UploadKey( ctx context.Context, caller consolecall.Caller, req *directoryrosterv1.UploadKeyRequest, ) (*directoryrosterv1.UploadKeyResponse, error)
UploadKey forwards a service-account key. The key is in memory for the length of the call and is not written anywhere here; more than MaxKeyBytes is refused before it is sent.
type ClientVerifier ¶
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 CompleteRequest ¶
type CompleteRequest struct {
Backend string `json:"backend"`
Code string `json:"code"`
Bind string `json:"bind,omitempty"`
}
CompleteRequest is the consent's callback: the provider's code, which is single-use, and the workspace being reconnected, if any.
type CompleteResponse ¶
type CompleteResponse struct {
Workspace string `json:"workspace"`
Backend string `json:"backend"`
}
CompleteResponse is the workspace the consent connected.
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 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 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 RemoteConnector ¶
type RemoteConnector struct {
// contains filtered or unexported fields
}
RemoteConnector is the Google connector as the issuer holds it when the module is another function: it offers the kind, and the sign-in through the module (`google.sign_in_url`, `google.identify`). The consent and the key upload are the module's, reached through Client; this connector never holds a client secret or a key, and its Exchange and FromKey refuse.
func NewRemoteConnector ¶
func NewRemoteConnector(c modcall.Caller) *RemoteConnector
NewRemoteConnector is the connector over c, which sends as the caller class `issuer`.
func (*RemoteConnector) AuthURL ¶
func (*RemoteConnector) AuthURL(string) (string, error)
AuthURL implements Connector: the consent is begun at the module.
func (*RemoteConnector) Exchange ¶
func (*RemoteConnector) Exchange(context.Context, string, string) (hub.Workspace, backendpkg.Backend, error)
Exchange implements Connector: the code is forwarded to the module.
func (*RemoteConnector) FromKey ¶
func (*RemoteConnector) FromKey(context.Context, []byte, string) (hub.Workspace, backendpkg.Backend, error)
FromKey implements KeyConnector: a key is forwarded to the module.
func (*RemoteConnector) Identify ¶
Identify implements SignInConnector with `google.identify`. A module that is cold answers late and one that is gone answers not at all, so the error is the caller's to show: a sign-in cannot be completed without it.
func (*RemoteConnector) SignInURL ¶
func (r *RemoteConnector) SignInURL(state string) (string, error)
SignInURL implements SignInConnector: the module holds the client the address is built from.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the Workspace service on the module's side: the hub that holds the workspaces, the connectors that open them, and the trail it records what it did to. It acts on the caller snapshot of the console, never on a session.
func NewService ¶
NewService is the Workspace service over a hub and its connectors. A nil recorder records nothing, a nil logger is the default one.
func (*Service) BeginConnect ¶
func (s *Service) BeginConnect(ctx context.Context, c consolecall.Caller, req BeginRequest) (BeginResponse, error)
BeginConnect returns where the browser goes to consent to a new connection.
func (*Service) CompleteConsent ¶
func (s *Service) CompleteConsent(ctx context.Context, c consolecall.Caller, req CompleteRequest) (CompleteResponse, error)
CompleteConsent finishes a consent: it exchanges the provider's code, checks it is the tenant being reconnected if one is, and adopts the workspace. The code is single-use, so the issuer never retries this call. What a person should read when it fails is a Problem, not an error.
func (*Service) Disconnect ¶
func (s *Service) Disconnect( ctx context.Context, c consolecall.Caller, req *directoryrosterv1.DisconnectRequest, ) (*directoryrosterv1.DisconnectResponse, error)
Disconnect removes a workspace.
func (*Service) Identify ¶
Identify turns the sign-in's code into the address that authenticated. It answers the issuer alone (Register); what that address may do is decided by the directory and the policy, not here.
func (*Service) ListWorkspaces ¶
func (s *Service) ListWorkspaces( ctx context.Context, c consolecall.Caller, _ *directoryrosterv1.ListWorkspacesRequest, ) (*directoryrosterv1.ListWorkspacesResponse, error)
ListWorkspaces lists the workspaces the caller may see.
func (*Service) Probe ¶
func (s *Service) Probe( ctx context.Context, c consolecall.Caller, req *directoryrosterv1.ProbeRequest, ) (*directoryrosterv1.ProbeResponse, error)
Probe checks a workspace's credential now.
func (*Service) Reconnect ¶
func (s *Service) Reconnect(ctx context.Context, c consolecall.Caller, req BeginRequest) (BeginResponse, error)
Reconnect is the same flow, bound to an existing workspace.
func (*Service) Refresh ¶
func (s *Service) Refresh( ctx context.Context, c consolecall.Caller, req *directoryrosterv1.RefreshRequest, ) (*directoryrosterv1.RefreshResponse, error)
Refresh reads a workspace's directory now, whatever the age of its snapshot.
func (*Service) SetServedDomains ¶
func (s *Service) SetServedDomains( ctx context.Context, c consolecall.Caller, req *directoryrosterv1.SetServedDomainsRequest, ) (*directoryrosterv1.SetServedDomainsResponse, error)
SetServedDomains sets which of a tenant's domains this hub answers for.
func (*Service) SetSyncedGroups ¶
func (s *Service) SetSyncedGroups( ctx context.Context, c consolecall.Caller, req *directoryrosterv1.SetSyncedGroupsRequest, ) (*directoryrosterv1.SetSyncedGroupsResponse, error)
SetSyncedGroups sets which of a tenant's groups this hub keeps.
func (*Service) UploadKey ¶
func (s *Service) UploadKey( ctx context.Context, c consolecall.Caller, req *directoryrosterv1.UploadKeyRequest, ) (*directoryrosterv1.UploadKeyResponse, error)
UploadKey connects a workspace with a service-account key. The key is in this call's memory and the credential store's, nowhere else: it is never logged, and a failure names the cause without it.
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.