google

package
v1.65.1 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package google reads a Google Workspace through the Admin SDK.

It is the only place in the hub that holds a directory credential and the only place that knows the Admin SDK exists. Everything above it works in the vocabulary of backend.Account and backend.Group, which is what lets a second directory kind arrive without touching the hub.

Index

Constants

View Source
const (
	CallbackPath       = "/connect/google/callback"
	SignInCallbackPath = "/login/google/callback"
)

The two paths a Google flow returns to, appended to the console's own base URL. They are fixed because an operator types the whole redirect URI into a cloud console by hand, and the one thing that must not vary between installations is the part nobody can look up.

Two, not one, because the two flows are answered by endpoints with opposite authorisation: the consent callback adopts a workspace and so demands an operator, while the sign-in callback is how a person becomes anyone at all and must be reachable by nobody. Sharing a path would mean one endpoint deciding which of those it was, from a parameter, at the moment it matters most.

Variables

View Source
var ConsentScopes = append([]string{"openid", "email"}, Scopes...)

ConsentScopes are what admin consent asks for: the four the hub reads with, plus the two that say who consented.

The identity pair is not decoration. A refresh token acts as whoever granted it, and a console that cannot name that account can only show "someone at this company" beside a credential with directory-wide read — which is exactly the fact an operator needs when deciding whether it is still the right one. They are standard scopes: no consent-screen entry, no verification, nothing to justify.

Scopes are the four read-only scopes the hub asks for. They are the same four the connect runbook tells an operator to grant and the same four the console shows for pasting; if this list and that one drift, an operator grants the wrong thing and the failure arrives later, at the first read, as a 403 that names nothing useful.

The drift that actually happened was not between two lists but between this list and a CALL: Tenant read the customer through Customers.Get, which needs a fifth scope nobody had granted. Every read below must be covered by exactly these four — adding a scope is not a code change, it is asking every administrator who has already consented to consent again.

View Source
var SignInScopes = []string{"openid", "email", "profile"}

SignInScopes are what signing a person in asks for: who they are, and nothing else. The directory scopes belong to the consent flow, which an administrator grants once per company; a person signing in grants nothing on their company's behalf.

Functions

func Identify

func Identify(ctx context.Context, client OAuthClient, code string) (string, error)

Identify turns a sign-in callback's code into the address that authenticated. It is the whole of what the flow is for: the hub decides everything else from the address, through the directory it already reads.

func VerifyClient

func VerifyClient(ctx context.Context, client OAuthClient) error

VerifyClient reports whether this OAuth client's id and secret are the ones Google holds, without needing a person.

It exists because the alternative is finding out after an administrator has already consented: a wrong secret fails at the exchange, which is the step AFTER the consent screen, so a real Super Admin has granted a real credential to an installation that cannot collect it.

The method is the one OAuth actually offers. There is no "check my client" endpoint, so this presents a code that cannot be valid and reads which complaint comes back: `invalid_client` is about the client, and anything else — `invalid_grant`, for the code — means the client itself was accepted. Nothing is minted either way.

Everything inconclusive passes. A refusal here stops an operator from connecting, so it is made only when Google has named the client as the problem; a timeout, a proxy, an unexpected shape are all "proceed".

Types

type Backend

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

Backend reads one Google Workspace.

func Consent(ctx context.Context, client OAuthClient, code string) (*Backend, error)

Consent turns a callback code into an open backend.

The returned backend acts as the administrator who consented, and carries the refresh token as its credential so that a store can write it down.

func Open

func Open(ctx context.Context, keyJSON []byte, admin string) (*Backend, error)

Open returns a backend reading the Workspace the key belongs to, acting as admin.

The impersonation is the whole of the authorisation story: a service-account key grants nothing by itself, and only becomes able to read a directory once that Workspace's administrator has granted the key's client id these scopes. So a key that works against one tenant says nothing about another, which is exactly the property that lets one installation serve several companies without any of them trusting each other.

func OpenWithToken

func OpenWithToken(ctx context.Context, client OAuthClient, refreshToken, admin string) (*Backend, error)

OpenWithToken returns a backend reading the Workspace whose admin consented, acting as that admin.

This is the other half of the same authorisation story. Where a service-account key is granted its scopes by an administrator inside the Workspace, a refresh token *is* an administrator's own grant: it was minted at a consent screen the admin saw and can withdraw. The hub therefore holds no standing power over a tenant that has not knowingly given it, which is the property that lets one installation serve companies that do not trust each other.

The client id and secret belong to the installation and are needed to use the token at all; the token alone is not enough to read anything.

func (*Backend) Account

func (b *Backend) Account(ctx context.Context, email string) (backend.Account, bool, error)

Account implements backend.Backend. A not-found is authoritative and is reported as found=false with no error; anything else is an error, from which nothing may be concluded.

func (*Backend) Accounts

func (b *Backend) Accounts(ctx context.Context) ([]backend.Account, error)

Accounts implements backend.Backend: every account in the tenant.

func (*Backend) Admin

func (b *Backend) Admin() string

Admin is the account this backend acts as.

func (*Backend) Credential

func (b *Backend) Credential() backend.Credential

Credential implements backend.Portable: what a store writes down so that this backend can be opened again after a restart.

func (*Backend) Groups

func (b *Backend) Groups(ctx context.Context) ([]backend.Group, error)

Groups implements backend.Backend: every group with its members.

func (*Backend) GroupsOf

func (b *Backend) GroupsOf(ctx context.Context, email string) ([]string, error)

GroupsOf implements backend.Backend: the groups one account is in.

func (*Backend) Kind

func (b *Backend) Kind() string

Kind implements backend.Backend.

func (*Backend) Probe

func (b *Backend) Probe(ctx context.Context) error

Probe implements backend.Backend: exercise the credential now.

One page of one user is enough and is deliberately the cheapest call that proves the whole chain — the key parses, the impersonation is granted, the scopes are present and the tenant answers.

func (*Backend) Revoke

func (b *Backend) Revoke(ctx context.Context) error

Revoke implements backend.Backend.

A refresh token can be handed back, and is: the grant an administrator made is the grant this hub gives up, in one call, at disconnect. A service-account key cannot be revoked from here — it belongs to a cloud project, and what makes it able to read this Workspace is a grant in that Workspace's admin console — so that case says so rather than pretending, and the hub reports it as "deleted locally, retire the credential yourself".

func (*Backend) Tenant

func (b *Backend) Tenant(ctx context.Context) (backend.Tenant, error)

Tenant implements backend.Backend: the customer id and every domain the Workspace owns.

Both are read rather than configured. The domains especially: they are what the hub routes on, and a hand-maintained list is a list that is wrong the day a domain is added and silently answers "no opinion" about everyone in it.

type OAuthClient

type OAuthClient struct {
	ID     string
	Secret string
	// BaseURL is where a browser reaches this console, with no trailing
	// slash. Both redirect URIs are derived from it rather than given
	// separately, so the two cannot disagree — and a mismatched redirect
	// is the failure that arrives late, in a cloud console's own words,
	// naming nothing useful.
	BaseURL string
}

OAuthClient is the client an installation registered once with Google. One per installation, never one per company: the tenants a hub reads are the ones whose administrators consented to this client.

func (OAuthClient) AuthURL

func (c OAuthClient) AuthURL(state string) string

AuthURL is where the browser goes to consent.

Offline access is what makes a refresh token arrive at all, and forcing the screen is what makes one arrive *every* time: Google returns a refresh token only on a grant the administrator actually saw, so a second consent that silently reuses the first would hand back an access token good for an hour and nothing to renew it with. Reconnecting a workspace is precisely the case where that would be a silent failure.

func (OAuthClient) ConsentRedirect

func (c OAuthClient) ConsentRedirect() string

ConsentRedirect is where the consent flow returns; one of the two URIs to register with the client.

func (OAuthClient) SignInRedirect

func (c OAuthClient) SignInRedirect() string

SignInRedirect is where the sign-in flow returns; the other one.

func (OAuthClient) SignInURL

func (c OAuthClient) SignInURL(state string) string

SignInURL is where the browser goes to prove who somebody is.

No offline access and no forced screen: nothing here is stored, so there is no refresh token to want, and a person who signed in a minute ago should not be asked again. The account chooser is Google's, which is why this hub asks nobody for an address first.

Jump to

Keyboard shortcuts

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