auth

package
v0.1.42 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package auth manages per-project GCP credentials.

Every project gets its own gcloud configuration directory, so credentials for ten projects never share mutable global state and switching projects is just a matter of pointing at a different directory. The password itself never passes through g9s: `gcloud auth application-default login` owns the terminal during login and the browser handles the IdP prompt and MFA.

Index

Constants

View Source
const EgressTimeout = 6 * time.Second

EgressTimeout bounds the reachability probe.

Short, and short on purpose. This runs in front of an interactive login, so it is worth a couple of seconds to save a sign-in and an MFA prompt that were always going to end in a crash — and not worth more than that.

View Source
const TokenEndpoint = "https://oauth2.googleapis.com/token"

TokenEndpoint is where an authorization code is redeemed for a token.

The one host that has to be reachable for any login to complete, whichever flow is used — and the one that fails invisibly. The browser prints "You are now authenticated with the gcloud CLI!" before gcloud has called it at all, so on a machine that cannot reach it the sign-in looks like a success and gcloud crashes afterwards.

Variables

This section is empty.

Functions

func IsTLSTrustFailure added in v0.1.40

func IsTLSTrustFailure(err error) bool

IsTLSTrustFailure reports whether a request failed because the certificate was not trusted, rather than because the connection was never made.

The distinction decides the remedy, and the two remedies share nothing: one needs a proxy address, the other needs a CA bundle.

func LoopbackUsable added in v0.1.3

func LoopbackUsable() bool

LoopbackUsable reports whether the browser gcloud sends you to could reach a server running on this machine.

This decides whether the ordinary login can finish at all. gcloud's flow starts an HTTP server on 127.0.0.1 and points the browser at Google with redirect_uri=http://localhost:<port>/; signing in only completes when the browser's request for that URL arrives back here. Over SSH it arrives at the laptop you are sitting at instead, and gcloud waits forever with the sign-in already done — which reads as a hang with no explanation, because from the browser's side everything worked.

X or Wayland forwarding is the exception worth encoding: the browser process still runs on this machine even though its pixels do not, so the redirect comes back here as normal.

func ProxyAddress added in v0.1.39

func ProxyAddress() string

ProxyAddress returns the proxy configured for this shell, or "".

Reported back to the user when a connection to Google fails, because "no proxy is set" and "this proxy did not work" are different problems with different next steps, and the shell is the one place g9s can actually see.

Any credentials in the URL are stripped: this string goes on screen, and a proxy password belongs there no more than any other secret does.

func ProxyMayBlockLoopback added in v0.1.3

func ProxyMayBlockLoopback() bool

ProxyMayBlockLoopback reports whether an HTTP proxy is configured that does not exempt loopback addresses.

The other way the redirect goes missing: a browser told to send everything through a corporate proxy will send http://localhost:<port>/ there too, and the proxy cannot route it back to this machine. The browser's proxy settings are what actually matter and g9s cannot read them, but a proxy in this environment with no loopback exemption is a strong hint that the browser is configured the same way — enough to name the likely cause instead of leaving the user staring at a URL.

Types

type AssistedLogin added in v0.1.20

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

AssistedLogin is gcloud's browser login run as a piped child instead of a terminal handover, so the flow can be rescued when the browser cannot reach loopback.

This exists for one machine: a corporate laptop whose browser sends http://localhost through the proxy. gcloud's flow ends with the browser fetching http://localhost:<port>/ to hand the authorization code back; a proxied browser never delivers it, the sign-in succeeds, and gcloud waits forever. The code is not lost — it is sitting in the address bar of the browser tab that failed to load. Everything after that is local: the user pastes that address into g9s, and Deliver performs the loopback request the browser could not, with a client that never uses a proxy.

The security posture is unchanged. gcloud still runs the OAuth flow and still holds the PKCE code verifier; the authorization code that passes through g9s is single-use and useless without that verifier. g9s sees no password, no token, and writes no credential — it forwards one local HTTP request that the browser was supposed to make.

func StubAssistedLogin added in v0.1.21

func StubAssistedLogin(project, rawURL, port string) *AssistedLogin

StubAssistedLogin builds an AssistedLogin that can only render: it carries a URL and port but runs no process. It exists for fixtures — the login screen in the generated README screenshots, and UI tests that need the screen populated without a gcloud. Deliver on a stub knocks on whatever the port says, so fixtures should use a port nothing listens on.

func (*AssistedLogin) Cancel added in v0.1.20

func (a *AssistedLogin) Cancel()

Cancel kills gcloud. Done closes shortly after.

func (*AssistedLogin) Cancelled added in v0.1.20

func (a *AssistedLogin) Cancelled() bool

Cancelled reports whether Cancel ended this login.

func (*AssistedLogin) Deliver added in v0.1.20

func (a *AssistedLogin) Deliver(pasted string) (carriedCode bool, err error)

Deliver performs the loopback request the browser could not.

pasted is the address bar of the browser tab that got stuck — the full http://localhost:<port>/?state=...&code=... redirect. Everything about it is validated against what gcloud itself announced before a single byte is sent: the scheme must be plain http, the host must be a loopback name, and the port must be the one from gcloud's own redirect_uri. The request is then pinned to 127.0.0.1 with a client that has no proxy and follows no redirects, so a hostile paste cannot turn this into a request to anywhere else. The worst a bad paste can do is knock on gcloud's own listener, which rejects a wrong state token itself.

func (*AssistedLogin) Done added in v0.1.20

func (a *AssistedLogin) Done() <-chan struct{}

Done is closed when gcloud exits, successfully or not.

func (*AssistedLogin) Err added in v0.1.20

func (a *AssistedLogin) Err() error

Err is how gcloud exited. Only meaningful after Done is closed.

func (*AssistedLogin) Output added in v0.1.20

func (a *AssistedLogin) Output() string

Output is the tail of everything gcloud wrote.

func (*AssistedLogin) Project added in v0.1.20

func (a *AssistedLogin) Project() string

Project names the project this login is for.

func (*AssistedLogin) URL added in v0.1.20

func (a *AssistedLogin) URL() string

URL is the authorization link to open in a browser.

type EgressResult added in v0.1.40

type EgressResult struct {
	State EgressState
	// Err is what went wrong, for the message. Nil when State is EgressOK.
	Err error
}

EgressResult is the probe's finding.

func CheckEgress added in v0.1.40

func CheckEgress(ctx context.Context) EgressResult

CheckEgress reports whether this machine can reach Google's token endpoint.

A real request rather than a DNS lookup or a bare TCP dial, because the three fail differently and only the full request tells them apart: a proxy that resolves, accepts connections, and then refuses CONNECT to Google looks perfectly healthy to anything cheaper.

HEAD, which the endpoint will refuse — that is the point. A refusal is proof the request arrived, which is the entire question.

func (EgressResult) OK added in v0.1.40

func (r EgressResult) OK() bool

OK reports whether a login can be expected to complete.

type EgressState added in v0.1.40

type EgressState int

EgressState is what a probe of the token endpoint found.

const (
	// EgressOK means Google answered.
	EgressOK EgressState = iota
	// EgressUnreachable means the connection could not be made. Almost always
	// a proxy that outbound HTTPS has to go through and that nothing here has
	// been told about.
	EgressUnreachable
	// EgressUntrusted means the connection was made and the certificate was
	// not one this machine trusts — a proxy terminating TLS and re-signing
	// with the organization's own CA. A different problem with an opposite
	// remedy: the route is fine and the trust is missing.
	EgressUntrusted
)

type LoginAttempt added in v0.1.20

type LoginAttempt struct {
	// Output is what gcloud wrote before it stopped.
	Output string
	// NoBrowser is whether the --no-browser flow was used.
	NoBrowser bool
	// Err is how the process ended.
	Err error
}

LoginAttempt is everything known about a login that did not produce a credential.

type LoginDiagnosis added in v0.1.20

type LoginDiagnosis struct {
	// Summary is one line naming the cause.
	Summary string
	// Remedy is what to actually do, in the order to try it.
	Remedy []string
}

LoginDiagnosis is a recognised login failure and what to do about it.

func DiagnoseLogin added in v0.1.20

func DiagnoseLogin(a LoginAttempt) (LoginDiagnosis, bool)

DiagnoseLogin turns a failed login into a cause and a remedy.

gcloud reports these accurately, but it reports them as an OAuth error from Google's side — "invalid_request", "access_denied" — which reads like a problem with the account rather than with how the flow was driven. On a corporate machine the cause is almost always local: a proxy in front of loopback, an org policy on the OAuth client, or the --no-browser command being used as though it were a URL.

The two that matter most do not produce an error at all. Both end with gcloud waiting: the browser flow waits for a redirect a proxied browser never delivers, and the --no-browser flow waits at a paste prompt for output the user cannot produce because they opened the URL instead of running the command. Neither times out, so the only way out is ctrl+c — which is why a cancelled login that had already shown an authorization URL is treated as a distinct diagnosis rather than as "you changed your mind".

Returning false means the failure is not one of the known shapes and should be shown as-is rather than guessed at.

func EgressDiagnosis added in v0.1.40

func EgressDiagnosis(r EgressResult) (LoginDiagnosis, bool)

EgressDiagnosis turns a failed probe into the same diagnosis a failed login would produce.

Shared deliberately. Someone who hits this before signing in and someone who hits it after must read the same words, or the second one spends the afternoon deciding they are two different problems.

type Manager

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

Manager resolves credentials for projects.

func NewManager

func NewManager(cfg *config.Config) (*Manager, error)

func (*Manager) ADCPath

func (m *Manager) ADCPath(p config.Project) string

ADCPath is the credentials file g9s reads for a project.

A configured credentials_file wins: everything downstream — the validity check and the client options every lister uses — goes through here, so pointing at an existing file is all it takes for g9s to use credentials it did not create.

func (*Manager) Available

func (m *Manager) Available() error

Available reports whether the configured gcloud binary can be found. Checked at startup so the failure is a clear message rather than a login that dies.

func (*Manager) Check

func (m *Manager) Check(ctx context.Context, p config.Project) Status

Check reports whether the project's credentials can currently mint a token.

This deliberately performs a live token exchange rather than reading an expiry field. A refresh token that the IdP has invalidated looks perfectly healthy on disk; the only honest test is to use it.

func (*Manager) ClientOptions

func (m *Manager) ClientOptions(p config.Project) []option.ClientOption

ClientOptions are the options every GCP client for this project must use.

WithQuotaProject matters: application default credentials minted from a user account have no project of their own, and most APIs reject the call outright without a billing/quota project attached.

func (*Manager) ConfigDir

func (m *Manager) ConfigDir(p config.Project) string

ConfigDir is the CLOUDSDK_CONFIG directory for a project.

func (*Manager) GcloudCmd

func (m *Manager) GcloudCmd(p config.Project, args ...string) *exec.Cmd

GcloudCmd builds an arbitrary gcloud invocation scoped to a project's credentials, for actions like `compute ssh`.

func (*Manager) InvalidateCredentials added in v0.1.28

func (m *Manager) InvalidateCredentials(projectName string)

InvalidateCredentials drops the cached token source for a project.

Called after a login, which rewrites the ADC file. Without it the shared source would go on minting tokens for the identity that was just replaced — the cache turning a fixed credential into a stale one.

func (*Manager) LoginCmd

func (m *Manager) LoginCmd(p config.Project, noBrowser bool) (*exec.Cmd, error)

LoginCmd builds the interactive login command for a project.

The caller is expected to hand the terminal to this process (bubbletea's ExecProcess does exactly that) so gcloud can print its URL, and so the user can paste the authorization response back. g9s never sees the password.

func (*Manager) ManagesAnyCredentials added in v0.1.5

func (m *Manager) ManagesAnyCredentials(cfg *config.Config) bool

ManagesAnyCredentials reports whether any project needs g9s to log it in.

False when every project reads a credentials_file, which is the setup for a machine where the login handshake cannot complete. gcloud is then not needed to start — only for SSH, which says so when it is used.

func (*Manager) ManagesCredentials added in v0.1.5

func (m *Manager) ManagesCredentials(p config.Project) bool

ManagesCredentials reports whether g9s is the one that logs this project in.

False when a credentials_file is configured. g9s then only reads that file, and refreshing it is the user's business — running gcloud against an isolated config directory would write somewhere the file is not.

func (*Manager) StartAssistedLogin added in v0.1.20

func (m *Manager) StartAssistedLogin(p config.Project) (*AssistedLogin, error)

StartAssistedLogin launches gcloud and returns once it has printed the authorization URL.

--no-launch-browser rather than the plain flow: the plain flow opens the browser itself from inside gcloud, and capturing the URL requires it to be printed. The listener behaviour is identical — gcloud still serves the loopback redirect — so when the browser can reach localhost the login completes with no help, exactly like the flow it replaces.

type State

type State int

State describes whether a project is usable right now.

const (
	// StateUnknown means the check has not run yet.
	StateUnknown State = iota
	// StateMissing means the project has never been logged in on this machine.
	StateMissing
	// StateExpired means credentials exist but no longer mint a token. With a
	// federated IdP this is the normal daily case: the session policy expired
	// and the PAM password has to be checked out again.
	StateExpired
	// StateValid means a token was minted successfully.
	StateValid
	// StateWrongAccount means the credential is live but belongs to a
	// different identity from the account configured for this project.
	StateWrongAccount
)

func (State) String

func (s State) String() string

type Status

type Status struct {
	State State
	// ExpectedAccount is the identity configured for this project, when one
	// was specified. It is kept beside Account so a mismatch can name both.
	ExpectedAccount string
	// Account is the identity recorded in the credential file, when present.
	Account string
	// Expiry is when the current access token stops working. The refresh
	// token's own lifetime is set by the IdP session policy and is not
	// discoverable from the file, so this is a floor, not a guarantee.
	Expiry time.Time
	// Err carries the underlying failure for StateExpired.
	Err error
	// CheckedAt lets callers cache the result.
	CheckedAt time.Time
}

Status is the result of checking a project's credentials.

func (Status) Summary

func (s Status) Summary() string

Summary renders the status for a footer or picker row.

func (Status) Valid

func (s Status) Valid() bool

type TailBuffer added in v0.1.20

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

TailBuffer keeps the tail of a stream.

Bounded because an interactive gcloud session can print a lot and only the end is ever the reason it failed; unbounded capture would hold megabytes for a message that is a few lines long. Safe for concurrent writes because a command's stdout and stderr can be separate goroutines writing the same buffer.

func NewTailBuffer added in v0.1.20

func NewTailBuffer(limit int) *TailBuffer

NewTailBuffer returns a buffer that keeps the last limit bytes written.

func (*TailBuffer) String added in v0.1.20

func (t *TailBuffer) String() string

func (*TailBuffer) Write added in v0.1.20

func (t *TailBuffer) Write(p []byte) (int, error)

Jump to

Keyboard shortcuts

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