diff --git a/internal/storage/dolt/credentials.go b/internal/storage/dolt/credentials.go index 43abe80d16..ba1a5ecf45 100644 --- a/internal/storage/dolt/credentials.go +++ b/internal/storage/dolt/credentials.go @@ -537,3 +537,49 @@ func (s *DoltStore) shouldUseCLIForCredentials(_ context.Context) bool { // to wrong directory — FindCLIRemote returns "" in those cases. return doltutil.FindCLIRemote(cliDir, s.remote) != "" } + +// cloudAuthEnvPrefixes lists environment variable prefixes used by cloud +// storage providers for authentication. When any of these are set and the +// store is in server mode, push/pull must route through a CLI subprocess +// so the dolt process inherits the current env vars. The SQL path +// (CALL DOLT_PUSH/PULL) executes inside the dolt-sql-server, which only +// has env vars from when it was started — not from the current shell. +var cloudAuthEnvPrefixes = []string{ + "AZURE_STORAGE_", // Azure Blob Storage (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, AZURE_STORAGE_SAS_TOKEN) + "AWS_", // AWS S3 (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION) + "GOOGLE_", // GCS (GOOGLE_APPLICATION_CREDENTIALS) + "GCS_", // GCS alternate (GCS_CREDENTIALS_FILE) + "OCI_", // Oracle Cloud Infrastructure + "DOLT_REMOTE_", // Dolt-specific remote credentials +} + +// shouldUseCLIForCloudAuth returns true when CLI subprocess routing should +// be used for push/pull because cloud storage credentials are present in the +// environment and the store is using an external dolt-sql-server. +// +// When bd connects to an external dolt-sql-server (server mode), CALL +// DOLT_PUSH/PULL executes inside the server process. That process only has +// the env vars it inherited at startup. If cloud credentials were set (or +// changed) after the server started, the SQL path silently fails to +// authenticate. Routing through a CLI subprocess (dolt push/pull) ensures +// the child process inherits the current environment (GH#6). +func (s *DoltStore) shouldUseCLIForCloudAuth() bool { + if !s.serverMode { + return false // embedded mode: env vars are in-process + } + cliDir := s.CLIDir() + if cliDir == "" { + return false + } + if doltutil.FindCLIRemote(cliDir, s.remote) == "" { + return false + } + for _, e := range os.Environ() { + for _, prefix := range cloudAuthEnvPrefixes { + if strings.HasPrefix(e, prefix) { + return true + } + } + } + return false +} diff --git a/internal/storage/dolt/credentials_test.go b/internal/storage/dolt/credentials_test.go index 3d05ea551c..c813085e3d 100644 --- a/internal/storage/dolt/credentials_test.go +++ b/internal/storage/dolt/credentials_test.go @@ -438,3 +438,45 @@ func TestFederationCredentialCLIRouting(t *testing.T) { }) } } + +func TestCloudAuthCLIRouting(t *testing.T) { + if _, err := exec.LookPath("dolt"); err != nil { + t.Skip("dolt not installed") + } + + tests := []struct { + name string + serverMode bool + setupRemote bool + envKey string // env var to set (empty = none) + envValue string + wantCLI bool + }{ + // Positive: cloud env + server mode + remote configured → CLI + {"azure storage account", true, true, "AZURE_STORAGE_ACCOUNT", "myaccount", true}, + {"azure storage key", true, true, "AZURE_STORAGE_KEY", "mykey", true}, + {"aws access key", true, true, "AWS_ACCESS_KEY_ID", "AKID", true}, + {"aws secret key", true, true, "AWS_SECRET_ACCESS_KEY", "secret", true}, + {"google creds", true, true, "GOOGLE_APPLICATION_CREDENTIALS", "/path/to/creds.json", true}, + {"gcs creds file", true, true, "GCS_CREDENTIALS_FILE", "/path/to/creds.json", true}, + {"oci var", true, true, "OCI_TENANCY", "ocid1.tenancy", true}, + {"dolt remote user", true, true, "DOLT_REMOTE_USER", "admin", true}, + // Negative: missing conditions → SQL fallback + {"no cloud env", true, true, "", "", false}, + {"embedded mode", false, true, "AZURE_STORAGE_ACCOUNT", "myaccount", false}, + {"no CLI remote", true, false, "AZURE_STORAGE_ACCOUNT", "myaccount", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Use a clean store (no remoteUser/remotePassword — cloud auth uses env vars) + store := setupCredentialTestStore(t, "", "", tt.serverMode, tt.setupRemote) + if tt.envKey != "" { + t.Setenv(tt.envKey, tt.envValue) + } + got := store.shouldUseCLIForCloudAuth() + if got != tt.wantCLI { + t.Errorf("shouldUseCLIForCloudAuth() = %v, want %v", got, tt.wantCLI) + } + }) + } +} diff --git a/internal/storage/dolt/store.go b/internal/storage/dolt/store.go index f27d078188..4787b44533 100644 --- a/internal/storage/dolt/store.go +++ b/internal/storage/dolt/store.go @@ -1813,6 +1813,13 @@ func (s *DoltStore) Push(ctx context.Context) (retErr error) { if s.shouldUseCLIForCredentials(ctx) { return s.doltCLIPush(ctx, false, creds) } + // Cloud auth CLI routing: when cloud storage env vars (AZURE_*, AWS_*, + // etc.) are set and we're in server mode, route through CLI so the dolt + // subprocess inherits the current env. The SQL server may not have these + // vars if it was started in a different context (GH#6). + if s.shouldUseCLIForCloudAuth() { + return s.doltCLIPush(ctx, false, creds) + } if s.remoteUser != "" { return withEnvCredentials(creds, func() error { if err := s.execWithLongTimeout(ctx, "CALL DOLT_PUSH('--user', ?, ?, ?)", s.remoteUser, s.remote, s.branch); err != nil { @@ -1854,6 +1861,10 @@ func (s *DoltStore) ForcePush(ctx context.Context) (retErr error) { if s.shouldUseCLIForCredentials(ctx) { return s.doltCLIPush(ctx, true, creds) } + // Cloud auth CLI routing (GH#6). + if s.shouldUseCLIForCloudAuth() { + return s.doltCLIPush(ctx, true, creds) + } if s.remoteUser != "" { return withEnvCredentials(creds, func() error { if err := s.execWithLongTimeout(ctx, "CALL DOLT_PUSH('--force', '--user', ?, ?, ?)", s.remoteUser, s.remote, s.branch); err != nil { @@ -1919,6 +1930,10 @@ func (s *DoltStore) Pull(ctx context.Context) (retErr error) { } return nil } + // Cloud auth CLI routing (GH#6). + if s.shouldUseCLIForCloudAuth() { + return s.doltCLIPull(ctx, creds) + } if s.remoteUser != "" { return withEnvCredentials(creds, func() error { if err := s.pullWithAutoResolve(ctx, "CALL DOLT_PULL('--user', ?, ?, ?)", s.remoteUser, s.remote, s.branch); err != nil {