diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 339656483..4cfcfa540 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features and Improvements +- Add support for OIDC ID token authentication using a file + ([PR #1213](https://github.com/databricks/databricks-sdk-go/pull/1213)). - Add support for OIDC ID token authentication using an environment variable ([PR #1215](https://github.com/databricks/databricks-sdk-go/pull/1215)). diff --git a/config/auth_default.go b/config/auth_default.go index 15083ec02..a53665bc4 100644 --- a/config/auth_default.go +++ b/config/auth_default.go @@ -29,6 +29,10 @@ func buildOidcTokenCredentialStrategies(cfg *Config) []CredentialsStrategy { return oidc.NewEnvIDTokenSource(v) }(), }, + { + name: "file-oidc", + tokenSource: oidc.NewFileTokenSource(cfg.OIDCTokenFilepath), + }, { name: "github-oidc", tokenSource: &githubIDTokenSource{ diff --git a/config/config.go b/config/config.go index 912fac5c0..a31508517 100644 --- a/config/config.go +++ b/config/config.go @@ -108,8 +108,11 @@ type Config struct { // specified by this argument. This argument also holds currently selected auth. AuthType string `name:"auth_type" env:"DATABRICKS_AUTH_TYPE" auth:"-"` + // Path to the file containing an OIDC ID token. + OIDCTokenFilepath string `name:"databricks_id_token_filepath" env:"DATABRICKS_OIDC_TOKEN_FILEPATH" auth:"file-oidc"` + // Environment variable name that contains an OIDC ID token. - OIDCTokenEnv string `name:"oidc_token_env" env:"DATABRICKS_OIDC_TOKEN_ENV" auth:"-"` + OIDCTokenEnv string `name:"oidc_token_env" env:"DATABRICKS_OIDC_TOKEN_ENV" auth:"env-oidc"` // Skip SSL certificate verification for HTTP calls. // Use at your own risk or for unit testing purposes. diff --git a/config/experimental/auth/oidc/oidc.go b/config/experimental/auth/oidc/oidc.go index ff671b8dc..b2f35bf54 100644 --- a/config/experimental/auth/oidc/oidc.go +++ b/config/experimental/auth/oidc/oidc.go @@ -34,7 +34,7 @@ func (fn IDTokenSourceFn) IDToken(ctx context.Context, audience string) (*IDToke return fn(ctx, audience) } -// NewEnvIDTokenSource returns an IDTokenSource that reads the token from +// NewEnvIDTokenSource returns an IDTokenSource that reads the IDtoken from // environment variable env. // // Note that the IDTokenSource does not cache the token and will read the token @@ -48,3 +48,24 @@ func NewEnvIDTokenSource(env string) IDTokenSource { return &IDToken{Value: t}, nil }) } + +// NewFileTokenSource returns an IDTokenSource that reads the ID token from a +// file. The file should contain a single line with the token. +func NewFileTokenSource(path string) IDTokenSource { + return IDTokenSourceFn(func(ctx context.Context, _ string) (*IDToken, error) { + if path == "" { + return nil, fmt.Errorf("missing path") + } + t, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("file %q does not exist", path) + } + return nil, err + } + if len(t) == 0 { + return nil, fmt.Errorf("file %q is empty", path) + } + return &IDToken{Value: string(t)}, nil + }) +} diff --git a/config/experimental/auth/oidc/oidc_test.go b/config/experimental/auth/oidc/oidc_test.go index 9c34174cc..0949aac8b 100644 --- a/config/experimental/auth/oidc/oidc_test.go +++ b/config/experimental/auth/oidc/oidc_test.go @@ -3,6 +3,8 @@ package oidc import ( "context" "fmt" + "os" + "path/filepath" "testing" "github.com/google/go-cmp/cmp" @@ -39,50 +41,37 @@ func TestNewEnvIDTokenSource(t *testing.T) { desc string envName string envValue string - audience string want *IDToken wantErr bool }{ { - desc: "Success - variable set", + desc: "success", envName: "OIDC_TEST_TOKEN_SUCCESS", envValue: "test-token-123", - audience: "test-audience-1", want: &IDToken{Value: "test-token-123"}, wantErr: false, }, { - desc: "Failure - variable not set", + desc: "missing env var", envName: "OIDC_TEST_TOKEN_MISSING", envValue: "", - audience: "test-audience-2", want: nil, wantErr: true, }, { - desc: "Failure - variable set to empty string", + desc: "empty env var", envName: "OIDC_TEST_TOKEN_EMPTY", envValue: "", - audience: "test-audience-3", want: nil, wantErr: true, }, { - desc: "Success - different variable name", + desc: "different variable name", envName: "ANOTHER_OIDC_TOKEN", envValue: "another-token-456", - audience: "test-audience-4", want: &IDToken{Value: "another-token-456"}, wantErr: false, }, - { - desc: "Success - empty audience string", - envName: "OIDC_TEST_TOKEN_NO_AUDIENCE", - envValue: "token-no-audience", - audience: "", - want: &IDToken{Value: "token-no-audience"}, - wantErr: false, - }, } for _, tc := range testCases { @@ -90,7 +79,80 @@ func TestNewEnvIDTokenSource(t *testing.T) { t.Setenv(tc.envName, tc.envValue) ts := NewEnvIDTokenSource(tc.envName) - got, gotErr := ts.IDToken(context.Background(), tc.audience) + got, gotErr := ts.IDToken(context.Background(), "") + + if tc.wantErr && gotErr == nil { + t.Fatalf("IDToken() want error, got none") + } + if !tc.wantErr && gotErr != nil { + t.Fatalf("IDToken() want no error, got error: %v", gotErr) + } + if !cmp.Equal(got, tc.want) { + t.Errorf("IDToken() token = %v, want %v", got, tc.want) + } + }) + } +} + +type testFile struct { + filename string + filecontent string +} + +func TestNewFileTokenSource(t *testing.T) { + testCases := []struct { + desc string + file *testFile // file to create + filepath string + want *IDToken + wantErr bool + }{ + { + desc: "missing filepath", + file: &testFile{filename: "token", filecontent: "content"}, + filepath: "", + wantErr: true, + }, + { + desc: "empty file", + file: &testFile{filename: "token", filecontent: ""}, + filepath: "token", + wantErr: true, + }, + { + desc: "file does not exist", + filepath: "nonexistent-file", + wantErr: true, + }, + { + desc: "file exists", + file: &testFile{filename: "token", filecontent: "content"}, + filepath: "token", + want: &IDToken{Value: "content"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tmpDir := t.TempDir() + + // Create the test file if any is given. + if tc.file != nil { + tokenFile := filepath.Join(tmpDir, tc.file.filename) + if err := os.WriteFile(tokenFile, []byte(tc.file.filecontent), 0644); err != nil { + t.Fatalf("failed to create token file: %v", err) + } + } + + // Only compute the fully qualified filepath if the relative + // filepath is given. + fp := tc.filepath + if tc.filepath != "" { + fp = filepath.Join(tmpDir, tc.filepath) + } + + ts := NewFileTokenSource(fp) + got, gotErr := ts.IDToken(context.Background(), "") if tc.wantErr && gotErr == nil { t.Fatalf("IDToken() want error, got none")