Compare commits
4 Commits
main
...
feat/adc-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
054573598b | ||
|
|
0b31ab8d3b | ||
|
|
96a084af7f | ||
|
|
e86e2f57e7 |
@ -5,6 +5,7 @@
|
||||
### Added
|
||||
- Admin: add Workspace Admin Directory commands for users and groups, including user list/get/create/suspend and group membership list/add/remove. (#403) — thanks @dl-alexandre.
|
||||
- Auth: add `--access-token` / `GOG_ACCESS_TOKEN` for direct access-token auth in headless or CI flows, bypassing stored refresh tokens. (#419) — thanks @mmkal.
|
||||
- Auth: add Application Default Credentials mode via `GOG_AUTH_MODE=adc` for Workload Identity, Cloud Run, and local `gcloud` ADC flows without stored OAuth refresh tokens. (#357) — thanks @tengis617.
|
||||
- Chat: add `chat messages reactions create|list|delete` to manage emoji reactions on messages; `react` and `reaction` are aliases for the reactions command group. (#426) — thanks @fernandopps.
|
||||
- Sheets: add named range management (`sheets named-ranges`) and let range-based Sheets commands accept named range names where GridRange-backed operations are needed. (#278) — thanks @TheCrazyLex.
|
||||
- Sheets: add `add-tab`, `rename-tab`, and `delete-tab` commands for managing spreadsheet tabs, with delete dry-run/confirmation guardrails. (#309) — thanks @JulienMalige.
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/steipete/gogcli/internal/config"
|
||||
"github.com/steipete/gogcli/internal/googleapi"
|
||||
"github.com/steipete/gogcli/internal/secrets"
|
||||
)
|
||||
|
||||
@ -22,6 +23,19 @@ const (
|
||||
)
|
||||
|
||||
func requireAccount(flags *RootFlags) (string, error) {
|
||||
// In ADC mode the service account authenticates as itself — no user email
|
||||
// or keyring lookup is needed. We still accept --account/GOG_ACCOUNT as an
|
||||
// optional label (e.g. for logging), but it is not required.
|
||||
if googleapi.IsADCMode() {
|
||||
if v := strings.TrimSpace(flags.Account); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv("GOG_ACCOUNT")); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
return "adc", nil
|
||||
}
|
||||
|
||||
client := config.DefaultClientName
|
||||
var err error
|
||||
if flags != nil {
|
||||
|
||||
@ -6,9 +6,11 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"github.com/steipete/gogcli/internal/googleauth"
|
||||
@ -28,6 +30,8 @@ const (
|
||||
tokenExchangeTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
var newADCTokenSource = google.DefaultTokenSource
|
||||
|
||||
func optionsForAccount(ctx context.Context, service googleauth.Service, email string) ([]option.ClientOption, error) {
|
||||
scopes, err := googleauth.Scopes(service)
|
||||
if err != nil {
|
||||
@ -37,12 +41,36 @@ func optionsForAccount(ctx context.Context, service googleauth.Service, email st
|
||||
return optionsForAccountScopes(ctx, string(service), email, scopes)
|
||||
}
|
||||
|
||||
// IsADCMode reports whether Application Default Credentials mode is active.
|
||||
// When GOG_AUTH_MODE=adc, the CLI authenticates using the ambient credentials
|
||||
// (e.g. GKE Workload Identity, GOOGLE_APPLICATION_CREDENTIALS, or gcloud ADC)
|
||||
// instead of the keyring-based OAuth flow. The service account accesses only
|
||||
// resources explicitly shared with it — no domain-wide delegation needed.
|
||||
func IsADCMode() bool {
|
||||
return os.Getenv("GOG_AUTH_MODE") == "adc"
|
||||
}
|
||||
|
||||
func optionsForAccountScopes(ctx context.Context, serviceLabel string, email string, scopes []string) ([]option.ClientOption, error) {
|
||||
slog.Debug("creating client options with custom scopes", "serviceLabel", serviceLabel, "email", email)
|
||||
|
||||
ts, err := tokenSourceForAvailableAccountAuth(ctx, serviceLabel, email, scopes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var ts oauth2.TokenSource
|
||||
|
||||
if IsADCMode() {
|
||||
slog.Debug("using Application Default Credentials (GOG_AUTH_MODE=adc)", "serviceLabel", serviceLabel)
|
||||
|
||||
adcTS, err := newADCTokenSource(ctx, scopes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ADC token source: %w", err)
|
||||
}
|
||||
|
||||
ts = adcTS
|
||||
} else {
|
||||
var err error
|
||||
|
||||
ts, err = tokenSourceForAvailableAccountAuth(ctx, serviceLabel, email, scopes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
baseTransport := newBaseTransport()
|
||||
|
||||
@ -399,6 +399,76 @@ func TestOptionsForAccountScopes_ServiceAccountPreferred(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsADCMode(t *testing.T) {
|
||||
t.Setenv("GOG_AUTH_MODE", "")
|
||||
|
||||
if IsADCMode() {
|
||||
t.Fatalf("expected false when GOG_AUTH_MODE is empty")
|
||||
}
|
||||
|
||||
t.Setenv("GOG_AUTH_MODE", "adc")
|
||||
|
||||
if !IsADCMode() {
|
||||
t.Fatalf("expected true when GOG_AUTH_MODE=adc")
|
||||
}
|
||||
|
||||
t.Setenv("GOG_AUTH_MODE", "oauth")
|
||||
|
||||
if IsADCMode() {
|
||||
t.Fatalf("expected false when GOG_AUTH_MODE=oauth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsForAccountScopes_ADCMode(t *testing.T) {
|
||||
t.Setenv("GOG_AUTH_MODE", "adc")
|
||||
|
||||
origADC := newADCTokenSource
|
||||
origRead := readClientCredentials
|
||||
origOpen := openSecretsStore
|
||||
|
||||
t.Cleanup(func() {
|
||||
newADCTokenSource = origADC
|
||||
readClientCredentials = origRead
|
||||
openSecretsStore = origOpen
|
||||
})
|
||||
|
||||
called := false
|
||||
newADCTokenSource = func(_ context.Context, scopes ...string) (oauth2.TokenSource, error) {
|
||||
called = true
|
||||
|
||||
if len(scopes) != 1 || scopes[0] != "https://www.googleapis.com/auth/spreadsheets.readonly" {
|
||||
t.Fatalf("unexpected scopes: %v", scopes)
|
||||
}
|
||||
|
||||
return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "adc-token"}), nil
|
||||
}
|
||||
|
||||
// Should NOT call keyring or readClientCredentials.
|
||||
readClientCredentials = func(string) (config.ClientCredentials, error) {
|
||||
t.Fatalf("readClientCredentials should not be called in ADC mode")
|
||||
return config.ClientCredentials{}, nil
|
||||
}
|
||||
openSecretsStore = func() (secrets.Store, error) {
|
||||
t.Fatalf("openSecretsStore should not be called in ADC mode")
|
||||
return nil, errBoom
|
||||
}
|
||||
|
||||
opts, err := optionsForAccountScopes(context.Background(), "sheets", "adc", []string{
|
||||
"https://www.googleapis.com/auth/spreadsheets.readonly",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
|
||||
if !called {
|
||||
t.Fatalf("expected ADC token source to be called")
|
||||
}
|
||||
|
||||
if len(opts) == 0 {
|
||||
t.Fatalf("expected client options")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBaseTransport_RespectsProxyAndTLSMinimum(t *testing.T) {
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:8888")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user