Skip to content

Add opt-in ADC reauth preflight (--reauth=off|auto) - #93

Merged
apstndb merged 5 commits into
mainfrom
feat/auto-reauth
Sep 8, 2026
Merged

apstndb merged 5 commits into
mainfrom
feat/auto-reauth

Conversation

@apstndb

@apstndb apstndb commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add --reauth=off|auto (default off, env EXECSPANSQL_REAUTH) so Workspace session-policy refresh failures (invalid_grant + invalid_rapt / rapt_required) can be classified before any Spanner RPC.
  • off keeps today's client construction (no DetectDefault, no WithAuthCredentials) and only appends a google-auth-style hint on classified or message-only reauth errors.
  • auto is explicit consent for one interactive gcloud auth application-default login during a preflight token fetch, then ADC reload and a second token check, then client creation with the reloaded credentials.

Flag contract

Value Behavior
off (default) Hint only. No gcloud process. Client options identical to today.
auto At most one login per process when a typed reauth error is observed and every applicability check passes. No second confirmation.

--timeout starts after authentication. Login is human-paced and does not consume or extend the query deadline. SIGINT still cancels the login and later execution.

auto means continue as whoever completes the login. The previous ADC principal is not compared.

Safety boundaries / what is never retried

  • No mid-flight retry and no runCLI loop after a failed RPC.
  • DML, Partitioned DML, and any statement that has already been dispatched are never replayed.
  • A reauth-looking Unauthenticated during a long-running statement is reported with the hint and a non-zero exit.
  • Substring matches on invalid_rapt / rapt_required are hint-only; they never trigger login.
  • Tokens, refresh tokens, ADC JSON, and raw OAuth bodies are never logged.
  • gcloud is invoked with a fixed argv through exec.CommandContext (stdout attached to stderr). No shell string is built.

Applicability conditions

Automatic login runs only when all of the following hold:

  • --reauth=auto
  • SPANNER_EMULATOR_HOST unset
  • GOOGLE_APPLICATION_CREDENTIALS unset
  • CLOUDSDK_CONFIG unset (gcloud honors it; the Go auth library always reads the well-known ADC path)
  • well-known ADC file exists, is writable, and "type" is authorized_user
  • stdin and stderr are terminals (golang.org/x/term.IsTerminal; stdout may be a pipe)
  • gcloud is found by exec.LookPath
  • no injected test/emulator client options (any non-empty runCLI client-option list skips preflight)

If CLOUDSDK_CONFIG or GOOGLE_APPLICATION_CREDENTIALS is set, the hint names that variable.

Tests run

Command Exit
go build ./... 0
go test ./params/... ./jqresult/... ./resultset/... 0
go test -count=1 -run 'Reauth|Auth' . (Colima emulator env) 0
golangci-lint run 2 (host toolchain go1.27 vs linter built with go1.26)
GOTOOLCHAIN=go1.25.13 golangci-lint run 0
DOCKER_HOST=unix://$HOME/.colima/default/docker.sock TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock go test ./... 0
go run . --help 0 (--reauth="off" and $EXECSPANSQL_REAUTH shown)
go mod tidy no extra diff beyond the intended direct deps (cloud.google.com/go/auth, golang.org/x/oauth2, golang.org/x/term) and the golang.org/x/sys indirect bump required by x/term

Coverage includes typed classifier cases (auth.Error, oauth2.RetrieveError, revoked invalid_grant, non-JSON body, hint-only gRPC Unauthenticated), orchestration fakes (off/auto, login failure, unchanged/wrong-type/unreadable reload, second-fetch failure, spent budget, quota-project change), applicability via t.Setenv and temp HOME, a PATH-first fake gcloud that must not write to process stdout, and a transport test that --reauth=auto plus injected client options skips DetectDefault and leaves ExecuteSql count at 1.

Known limitations

  • Slice C is deferred: no wrapping auth.TokenProvider and no read-only re-attempt after an RPC has started. Mid-run expiry after the preflight remains hint-only.
  • No prompt value; default stays off.
  • Windows gcloud.cmd launch is untested; the fake-gcloud script test is POSIX-only.
  • After login, identity is not verified (no extra identity scopes). Quota project changes are reported on stderr and do not fail the command.
  • Manual check against a Workspace short-session account is still the only way to exercise Google's token endpoint.

Detect invalid_grant with error_subtype invalid_rapt or rapt_required on
a token fetch before creating the Spanner client. --reauth=off (default)
only appends a google-auth-style hint. --reauth=auto may run gcloud
application-default login once when user ADC is applicable, then reload
credentials.

Never replay SQL, DML, or already-started RPCs. Skip automatic login when
emulator/GAC/CLOUDSDK_CONFIG is set, ADC is not a writable authorized_user
file, stdin/stderr are not terminals, gcloud is missing, or tests inject
client options. Login is human-paced and does not consume --timeout.

Validation: go build, package tests, go test -run Reauth|Auth, full
emulator suite, and golangci-lint with GOTOOLCHAIN=go1.25.13.
signal.NotifyContext keeps capturing SIGINT until stop() is called, so a
second Ctrl-C would be swallowed while a cancelled login or query is
still unwinding. Call stop() as soon as ctx is cancelled so the second
interrupt terminates the process as before.
@apstndb apstndb added the enhancement New feature or request label Sep 8, 2026
A review suggested that passing the resolved gcloud.cmd path to
exec.CommandContext cannot start the batch wrapper on Windows. It can:
CreateProcess launches .cmd/.bat files through cmd.exe implicitly, which
is why exec.LookPath resolves PATHEXT batch extensions and why os/exec
documents cmd.exe quoting as a caveat rather than a limitation. Record
that reasoning at the call site, together with why the cmd.exe unquoting
differences are irrelevant for this fixed argument vector, so future
reviews do not repeat the concern. Windows runtime behavior remains
unverified in CI.
mergedParams and GenerateParams ran after maybeAuthPreflight, so a parameter
file edited while the user completed a browser login could change the
executed statement. Build the spanner.Statement (SQL and parameters) before
any interactive step, as the design note's frozen execution spec requires.
Behavior is unchanged when --reauth=off.
Resolve the runCLI overlap with the split-output change: freeze SQL and
parameters, open the output sinks (destination validation and temp files),
then run the reauth preflight and start the query timeout. go.mod/go.sum
re-tidied on top of main (x/term v0.43.0; x/sys stays at main's v0.45.0).
@apstndb
apstndb merged commit ff4f7d1 into main Sep 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant