Skip to content
Open
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
12 changes: 12 additions & 0 deletions go/adbc/driver/bigquery/bigquery_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type databaseImpl struct {
clientID string
clientSecret string
refreshToken string
authScopes []string

impersonateTargetPrincipal string
impersonateDelegates []string
Expand All @@ -57,6 +58,7 @@ func (d *databaseImpl) Open(ctx context.Context) (adbc.Connection, error) {
clientID: d.clientID,
clientSecret: d.clientSecret,
refreshToken: d.refreshToken,
authScopes: d.authScopes,
impersonateTargetPrincipal: d.impersonateTargetPrincipal,
impersonateDelegates: d.impersonateDelegates,
impersonateScopes: d.impersonateScopes,
Expand Down Expand Up @@ -96,6 +98,14 @@ func (d *databaseImpl) GetOption(key string) (string, error) {
return d.clientSecret, nil
case OptionStringAuthRefreshToken:
return d.refreshToken, nil
case OptionStringAuthScopes:
return strings.Join(d.authScopes, ","), nil
case OptionStringImpersonateTargetPrincipal:
return d.impersonateTargetPrincipal, nil
case OptionStringImpersonateDelegates:
return strings.Join(d.impersonateDelegates, ","), nil
case OptionStringImpersonateScopes:
return strings.Join(d.impersonateScopes, ","), nil
case OptionStringLocation:
return d.location, nil
case OptionStringProjectID:
Expand Down Expand Up @@ -158,6 +168,8 @@ func (d *databaseImpl) SetOption(key string, value string) error {
d.clientSecret = value
case OptionStringAuthRefreshToken:
d.refreshToken = value
case OptionStringAuthScopes:
d.authScopes = strings.Split(value, ",")
case OptionStringImpersonateTargetPrincipal:
d.impersonateTargetPrincipal = value
case OptionStringImpersonateDelegates:
Expand Down
16 changes: 16 additions & 0 deletions go/adbc/driver/bigquery/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type connectionImpl struct {
clientID string
clientSecret string
refreshToken string
authScopes []string

impersonateTargetPrincipal string
impersonateDelegates []string
Expand Down Expand Up @@ -468,6 +469,14 @@ func (c *connectionImpl) GetOption(key string) (string, error) {
return c.clientSecret, nil
case OptionStringAuthRefreshToken:
return c.refreshToken, nil
case OptionStringAuthScopes:
return strings.Join(c.authScopes, ","), nil
case OptionStringImpersonateTargetPrincipal:
return c.impersonateTargetPrincipal, nil
case OptionStringImpersonateDelegates:
return strings.Join(c.impersonateDelegates, ","), nil
case OptionStringImpersonateScopes:
return strings.Join(c.impersonateScopes, ","), nil
case OptionStringProjectID:
return c.catalog, nil
case OptionStringDatasetID:
Expand Down Expand Up @@ -500,6 +509,8 @@ func (c *connectionImpl) SetOption(key string, value string) error {
c.clientSecret = value
case OptionStringAuthRefreshToken:
c.refreshToken = value
case OptionStringAuthScopes:
c.authScopes = strings.Split(value, ",")
case OptionStringImpersonateTargetPrincipal:
c.impersonateTargetPrincipal = value
case OptionStringImpersonateDelegates:
Expand Down Expand Up @@ -591,6 +602,11 @@ func (c *connectionImpl) newClient(ctx context.Context) error {
}
}

// Apply regular authentication scopes if specified (for non-impersonated auth)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should error if both are set?

if len(c.authScopes) > 0 && !c.hasImpersonationOptions() {
authOptions = append(authOptions, option.WithScopes(c.authScopes...))
}

// Then, apply impersonation if configured (as a credential transformation layer)
if c.hasImpersonationOptions() {
if c.impersonateTargetPrincipal == "" {
Expand Down
6 changes: 6 additions & 0 deletions go/adbc/driver/bigquery/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ const (
// OptionStringImpersonateLifetime instructs the driver to impersonate for the
// given duration (e.g. "3600s").
OptionStringImpersonateLifetime = "adbc.bigquery.sql.impersonate.lifetime"

// OptionStringAuthScopes instructs the driver to use the given comma-separated
// list of OAuth 2.0 scopes for regular (non-impersonated) authentication.
// This is used with Application Default Credentials, JSON credentials, etc.
// Example: "https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform"
OptionStringAuthScopes = "adbc.bigquery.sql.auth.scopes"
)

var (
Expand Down
228 changes: 228 additions & 0 deletions go/adbc/driver/bigquery/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1589,3 +1589,231 @@ func TestAuthTypeConsolidation(t *testing.T) {
t.Errorf("Expected error message to contain 'unknown database auth type value', got: %v", err)
}
}

// ---- Auth Scopes Tests ----

// TestAuthScopesSetGet tests setting and getting regular auth scopes
func TestAuthScopesSetGet(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
defer mem.AssertSize(t, 0)

drv := driver.NewDriver(mem)
db, err := drv.NewDatabase(nil)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CheckedClose for this instead

_ = db.Close()
}()

// Test setting auth scopes
expectedScopes := "https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform"
err = db.SetOptions(map[string]string{
driver.OptionStringAuthScopes: expectedScopes,
})
if err != nil {
t.Errorf("Failed to set auth scopes: %v", err)
}

// Test getting auth scopes
getSetDB, ok := db.(interface {
GetOption(string) (string, error)
})
if !ok {
t.Fatal("Database does not implement GetOption")
}

retrievedScopes, err := getSetDB.GetOption(driver.OptionStringAuthScopes)
if err != nil {
t.Errorf("Failed to get auth scopes: %v", err)
}

if retrievedScopes != expectedScopes {
t.Errorf("Expected scopes %s, got %s", expectedScopes, retrievedScopes)
}
}

// TestAuthScopesSeparateFromImpersonateScopes verifies that auth scopes and impersonate scopes are separate
func TestAuthScopesSeparateFromImpersonateScopes(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
defer mem.AssertSize(t, 0)

drv := driver.NewDriver(mem)
db, err := drv.NewDatabase(nil)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer func() {
_ = db.Close()
}()

authScopes := "https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform"
impersonateScopes := "https://www.googleapis.com/auth/bigquery,https://www.googleapis.com/auth/devstorage.read_write"

// Set both types of scopes
err = db.SetOptions(map[string]string{
driver.OptionStringAuthScopes: authScopes,
driver.OptionStringImpersonateScopes: impersonateScopes,
})
if err != nil {
t.Errorf("Failed to set scopes: %v", err)
}

getSetDB, ok := db.(interface {
GetOption(string) (string, error)
})
if !ok {
t.Fatal("Database does not implement GetOption")
}

// Verify auth scopes
retrievedAuthScopes, err := getSetDB.GetOption(driver.OptionStringAuthScopes)
if err != nil {
t.Errorf("Failed to get auth scopes: %v", err)
}
if retrievedAuthScopes != authScopes {
t.Errorf("Expected auth scopes %s, got %s", authScopes, retrievedAuthScopes)
}

// Verify impersonate scopes (they should be different)
retrievedImpScopes, err := getSetDB.GetOption(driver.OptionStringImpersonateScopes)
if err != nil {
t.Errorf("Failed to get impersonate scopes: %v", err)
}

// The retrieved impersonate scopes should match what we set
t.Logf("Auth scopes: %s", retrievedAuthScopes)
t.Logf("Impersonate scopes: %s", retrievedImpScopes)

// The key point: they should be stored separately
if retrievedAuthScopes == impersonateScopes {
t.Error("Auth scopes and impersonate scopes should be different but got the same value")
}
}

// TestAuthScopesEmptyByDefault tests that auth scopes default to empty
func TestAuthScopesEmptyByDefault(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
defer mem.AssertSize(t, 0)

drv := driver.NewDriver(mem)
db, err := drv.NewDatabase(nil)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer func() {
_ = db.Close()
}()

getSetDB, ok := db.(interface {
GetOption(string) (string, error)
})
if !ok {
t.Fatal("Database does not implement GetOption")
}

// Get auth scopes without setting them
scopes, err := getSetDB.GetOption(driver.OptionStringAuthScopes)
if err != nil {
t.Errorf("Failed to get auth scopes: %v", err)
}

if scopes != "" {
t.Errorf("Expected empty scopes by default, got %s", scopes)
}
}

// TestAuthScopesCommaSeparated tests that multiple scopes are properly handled
func TestAuthScopesCommaSeparated(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
defer mem.AssertSize(t, 0)

drv := driver.NewDriver(mem)
db, err := drv.NewDatabase(nil)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer func() {
_ = db.Close()
}()

scopes := []string{
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/bigquery",
}
scopesStr := strings.Join(scopes, ",")

err = db.SetOptions(map[string]string{
driver.OptionStringAuthScopes: scopesStr,
})
if err != nil {
t.Errorf("Failed to set auth scopes: %v", err)
}

getSetDB, ok := db.(interface {
GetOption(string) (string, error)
})
if !ok {
t.Fatal("Database does not implement GetOption")
}

retrievedScopes, err := getSetDB.GetOption(driver.OptionStringAuthScopes)
if err != nil {
t.Errorf("Failed to get auth scopes: %v", err)
}

if retrievedScopes != scopesStr {
t.Errorf("Expected scopes %s, got %s", scopesStr, retrievedScopes)
}

// Verify the scopes contain all expected values
retrievedParts := strings.Split(retrievedScopes, ",")
if len(retrievedParts) != len(scopes) {
t.Errorf("Expected %d scopes, got %d", len(scopes), len(retrievedParts))
}
}

// TestAuthScopesPropagatedToConnection tests that scopes are propagated from database to connection
func TestAuthScopesPropagatedToConnection(t *testing.T) {
// Note: This test doesn't actually open a connection (which would require valid GCP credentials)
// but verifies that the scopes are stored in the database impl
mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
defer mem.AssertSize(t, 0)

drv := driver.NewDriver(mem)

scopes := "https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform"

db, err := drv.NewDatabase(map[string]string{
driver.OptionStringProjectID: "test-project",
driver.OptionStringAuthScopes: scopes,
})
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer func() {
_ = db.Close()
}()

// Verify the scopes were set
getSetDB, ok := db.(interface {
GetOption(string) (string, error)
})
if !ok {
t.Fatal("Database does not implement GetOption")
}

retrievedScopes, err := getSetDB.GetOption(driver.OptionStringAuthScopes)
if err != nil {
t.Errorf("Failed to get auth scopes: %v", err)
}

if retrievedScopes != scopes {
t.Errorf("Expected scopes %s, got %s", scopes, retrievedScopes)
}

// Note: We can't test actual connection opening without valid credentials,
// but the struct propagation is tested in the databaseImpl.Open() method
t.Log("Scopes successfully stored in database and ready for connection propagation")
}
Loading