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 ¶
- func LoopbackUsable() bool
- func ProxyMayBlockLoopback() bool
- type AssistedLogin
- func (a *AssistedLogin) Cancel()
- func (a *AssistedLogin) Cancelled() bool
- func (a *AssistedLogin) Deliver(pasted string) error
- func (a *AssistedLogin) Done() <-chan struct{}
- func (a *AssistedLogin) Err() error
- func (a *AssistedLogin) Output() string
- func (a *AssistedLogin) Project() string
- func (a *AssistedLogin) URL() string
- type LoginAttempt
- type LoginDiagnosis
- type Manager
- func (m *Manager) ADCPath(p config.Project) string
- func (m *Manager) Available() error
- func (m *Manager) Check(ctx context.Context, p config.Project) Status
- func (m *Manager) ClientOptions(p config.Project) []option.ClientOption
- func (m *Manager) ConfigDir(p config.Project) string
- func (m *Manager) GcloudCmd(p config.Project, args ...string) *exec.Cmd
- func (m *Manager) LoginCmd(p config.Project, noBrowser bool) (*exec.Cmd, error)
- func (m *Manager) ManagesAnyCredentials(cfg *config.Config) bool
- func (m *Manager) ManagesCredentials(p config.Project) bool
- func (m *Manager) StartAssistedLogin(p config.Project) (*AssistedLogin, error)
- type State
- type Status
- type TailBuffer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
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 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) 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 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.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager resolves credentials for projects.
func (*Manager) ADCPath ¶
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 ¶
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 ¶
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) GcloudCmd ¶
GcloudCmd builds an arbitrary gcloud invocation scoped to a project's credentials, for actions like `compute ssh`.
func (*Manager) LoginCmd ¶
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
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
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 )
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.
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