Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions internal/storage/dolt/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
42 changes: 42 additions & 0 deletions internal/storage/dolt/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
15 changes: 15 additions & 0 deletions internal/storage/dolt/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading