From bb1d09fc90dcdffb3f9485a0f2946cef57069707 Mon Sep 17 00:00:00 2001 From: edwardsb Date: Fri, 5 Jun 2026 10:04:35 -0400 Subject: [PATCH] Add GCS IAM authentication for S3-compatible storage (#40303) (#40374) Closes #40303 ### Summary Adds support for Google Application Default Credentials (ADC) bearer token authentication when using GCS's S3-compatible endpoint. This allows Fleet deployments on GCP to use workload identity instead of static HMAC keys. Changes - Add `s3_software_installers_gcs_iam_auth` config option for software installer storage - Add `s3_carves_gcs_iam_auth` config option for file carving storage - Implement OAuth2 bearer token auth in S3 client via middleware (removes AWS SigV4 signing) - Add validation to ensure GCS IAM auth requires endpoint URL containing `storage.googleapis.com` - Add Helm chart values and deployment env vars for both options - Add documentation for new configuration options - Add tests for GCS IAM auth validation and integration ### Usage Enable GCS IAM auth by setting the endpoint URL to Google's S3-compatible endpoint and enabling the IAM auth flag: ```yaml s3: software_installers_endpoint_url: https://storage.googleapis.com software_installers_gcs_iam_auth: true software_installers_bucket: my-bucket software_installers_force_s3_path_style: true ``` Or via environment variables: ``` FLEET_S3_SOFTWARE_INSTALLERS_ENDPOINT_URL=https://storage.googleapis.com FLEET_S3_SOFTWARE_INSTALLERS_GCS_IAM_AUTH=true FLEET_S3_SOFTWARE_INSTALLERS_BUCKET=my-bucket FLEET_S3_SOFTWARE_INSTALLERS_FORCE_S3_PATH_STYLE=true ``` ### Testing - Unit tests validate configuration requirements (GCS endpoint, no HMAC keys, no STS role) - Integration test verifies bearer token is correctly injected into requests **Related issue:** Resolves # ## Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [X] Added/updated automated tests - [TODO] QA'd all new/changed functionality manually ## New Fleet configuration settings - [X] Setting(s) is/are explicitly excluded from GitOps > [!NOTE] These are infrastructure-level server settings (env vars/config file), not app-level settings managed via GitOps YAML. ## Summary by CodeRabbit * **New Features** * Added Google Cloud Storage (GCS) IAM authentication support for file carving and software installer storage using Google Application Default Credentials * **Configuration** * New authentication configuration option available for both carving and software installer S3 storage in Helm deployments and configuration files [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/40374) Co-authored-by: Carlo <1778532+cdcme@users.noreply.github.com> --- changes/40303-gcs-iam-auth | 2 + charts/fleet/templates/deployment.yaml | 6 +- charts/fleet/values.yaml | 4 +- server/config/config.go | 9 ++ server/datastore/s3/s3.go | 111 ++++++++++++-- server/datastore/s3/s3_test.go | 194 +++++++++++++++++++++++++ 6 files changed, 311 insertions(+), 15 deletions(-) create mode 100644 changes/40303-gcs-iam-auth create mode 100644 server/datastore/s3/s3_test.go diff --git a/changes/40303-gcs-iam-auth b/changes/40303-gcs-iam-auth new file mode 100644 index 0000000000..a8d54975ce --- /dev/null +++ b/changes/40303-gcs-iam-auth @@ -0,0 +1,2 @@ +- Added GCS IAM authentication support for software installers S3 storage using Google Application Default Credentials (ADC) bearer tokens instead of S3 HMAC keys. Configurable via `s3_software_installers_gcs_iam_auth`. +- Added GCS IAM authentication support for file carving S3 storage. Configurable via `s3_carves_gcs_iam_auth`. diff --git a/charts/fleet/templates/deployment.yaml b/charts/fleet/templates/deployment.yaml index 2d081de9a6..562bd0a554 100644 --- a/charts/fleet/templates/deployment.yaml +++ b/charts/fleet/templates/deployment.yaml @@ -146,6 +146,8 @@ spec: value: "{{ .Values.fleet.carving.s3.endpointURL }}" - name: FLEET_S3_FORCE_S3_PATH_STYLE value: "{{ .Values.fleet.carving.s3.forceS3PathStyle }}" + - name: FLEET_S3_CARVES_GCS_IAM_AUTH + value: "{{ .Values.fleet.carving.s3.gcsIAMAuth }}" - name: FLEET_S3_CARVES_REGION value: "{{ .Values.fleet.carving.s3.region }}" {{- if ne .Values.fleet.carving.s3.accessKeyID "" }} @@ -171,6 +173,8 @@ spec: value: "{{ .Values.fleet.softwareInstallers.s3.endpointURL }}" - name: FLEET_S3_SOFTWARE_INSTALLERS_FORCE_S3_PATH_STYLE value: "{{ .Values.fleet.softwareInstallers.s3.forceS3PathStyle }}" + - name: FLEET_S3_SOFTWARE_INSTALLERS_GCS_IAM_AUTH + value: "{{ .Values.fleet.softwareInstallers.s3.gcsIAMAuth }}" - name: FLEET_S3_SOFTWARE_INSTALLERS_REGION value: "{{ .Values.fleet.softwareInstallers.s3.region }}" {{- if ne .Values.fleet.softwareInstallers.s3.accessKeyID "" }} @@ -598,4 +602,4 @@ spec: {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 46f95719f1..adc49c396e 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -113,6 +113,7 @@ fleet: region: "" endpointURL: "" forceS3PathStyle: false + gcsIAMAuth: false stsAssumeRoleARN: "" softwareInstallers: s3: @@ -123,6 +124,7 @@ fleet: region: "" endpointURL: "" forceS3PathStyle: false + gcsIAMAuth: false stsAssumeRoleARN: "" license: secretName: "" @@ -136,7 +138,7 @@ fleet: runAsUser: 3333 runAsGroup: 3333 # Add additional CA's to the Fleet container truststore - # To add CA's, set enabled: true + # To add CA's, set enabled: true # Supports adding CA's from Config maps and Secrets # configMaps: # - name: fleet-ca-config diff --git a/server/config/config.go b/server/config/config.go index a7c23ab699..e2ed6333a8 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -481,6 +481,7 @@ type S3Config struct { CarvesStsExternalID string `yaml:"carves_sts_external_id"` CarvesDisableSSL bool `yaml:"carves_disable_ssl"` CarvesForceS3PathStyle bool `yaml:"carves_force_s3_path_style"` + CarvesGCSIAMAuth bool `yaml:"carves_gcs_iam_auth"` SoftwareInstallersBucket string `yaml:"software_installers_bucket"` SoftwareInstallersPrefix string `yaml:"software_installers_prefix"` @@ -492,6 +493,7 @@ type S3Config struct { SoftwareInstallersStsExternalID string `yaml:"software_installers_sts_external_id"` SoftwareInstallersDisableSSL bool `yaml:"software_installers_disable_ssl"` SoftwareInstallersForceS3PathStyle bool `yaml:"software_installers_force_s3_path_style"` + SoftwareInstallersGCSIAMAuth bool `yaml:"software_installers_gcs_iam_auth"` SoftwareInstallersCloudFrontURL string `yaml:"software_installers_cloudfront_url"` SoftwareInstallersCloudFrontURLSigningPublicKeyID string `yaml:"software_installers_cloudfront_url_signing_public_key_id"` SoftwareInstallersCloudFrontURLSigningPrivateKey string `yaml:"software_installers_cloudfront_url_signing_private_key"` @@ -553,6 +555,7 @@ func (s S3Config) SoftwareInstallersToInternalCfg() S3ConfigInternal { StsExternalID: s.SoftwareInstallersStsExternalID, DisableSSL: s.SoftwareInstallersDisableSSL, ForceS3PathStyle: s.SoftwareInstallersForceS3PathStyle, + GCSIAMAuth: s.SoftwareInstallersGCSIAMAuth, } if s.SoftwareInstallersCloudFrontSigner != nil { configInternal.CloudFrontConfig = &S3CloudFrontConfig{ @@ -609,6 +612,7 @@ func (s S3Config) CarvesToInternalCfg() S3ConfigInternal { if !s.CarvesForceS3PathStyle { internal.ForceS3PathStyle = s.ForceS3PathStyle } + internal.GCSIAMAuth = s.CarvesGCSIAMAuth return internal } @@ -625,6 +629,7 @@ type S3ConfigInternal struct { StsExternalID string DisableSSL bool ForceS3PathStyle bool + GCSIAMAuth bool CloudFrontConfig *S3CloudFrontConfig } @@ -1579,6 +1584,7 @@ func (man Manager) addConfigs() { man.addConfigString("s3.carves_sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.") man.addConfigBool("s3.carves_disable_ssl", false, "Disable SSL (typically for local testing)") man.addConfigBool("s3.carves_force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") + man.addConfigBool("s3.carves_gcs_iam_auth", false, "Use Google ADC bearer tokens for GCS endpoint authentication instead of S3 HMAC keys") // S3 for software installers man.addConfigString("s3.software_installers_bucket", "", "Bucket where to store uploaded software installers") @@ -1591,6 +1597,7 @@ func (man Manager) addConfigs() { man.addConfigString("s3.software_installers_sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.") man.addConfigBool("s3.software_installers_disable_ssl", false, "Disable SSL (typically for local testing)") man.addConfigBool("s3.software_installers_force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`") + man.addConfigBool("s3.software_installers_gcs_iam_auth", false, "Use Google ADC bearer tokens for GCS endpoint authentication instead of S3 HMAC keys") man.addConfigString("s3.software_installers_cloudfront_url", "", "CloudFront URL for software installers") man.addConfigString("s3.software_installers_cloudfront_url_signing_public_key_id", "", "CloudFront public key ID for URL signing") man.addConfigString("s3.software_installers_cloudfront_url_signing_private_key", "", "CloudFront private key for URL signing") @@ -2117,6 +2124,7 @@ func (man Manager) loadS3Config() S3Config { CarvesStsExternalID: man.getConfigString("s3.carves_sts_external_id"), CarvesDisableSSL: man.getConfigBool("s3.carves_disable_ssl"), CarvesForceS3PathStyle: man.getConfigBool("s3.carves_force_s3_path_style"), + CarvesGCSIAMAuth: man.getConfigBool("s3.carves_gcs_iam_auth"), Bucket: man.getConfigString("s3.bucket"), Prefix: man.getConfigString("s3.prefix"), @@ -2139,6 +2147,7 @@ func (man Manager) loadS3Config() S3Config { SoftwareInstallersStsExternalID: man.getConfigString("s3.software_installers_sts_external_id"), SoftwareInstallersDisableSSL: man.getConfigBool("s3.software_installers_disable_ssl"), SoftwareInstallersForceS3PathStyle: man.getConfigBool("s3.software_installers_force_s3_path_style"), + SoftwareInstallersGCSIAMAuth: man.getConfigBool("s3.software_installers_gcs_iam_auth"), SoftwareInstallersCloudFrontURL: man.getConfigString("s3.software_installers_cloudfront_url"), SoftwareInstallersCloudFrontURLSigningPublicKeyID: man.getConfigString("s3.software_installers_cloudfront_url_signing_public_key_id"), SoftwareInstallersCloudFrontURLSigningPrivateKey: man.getConfigString("s3.software_installers_cloudfront_url_signing_private_key"), diff --git a/server/datastore/s3/s3.go b/server/datastore/s3/s3.go index 01b948b57d..5afac1e078 100644 --- a/server/datastore/s3/s3.go +++ b/server/datastore/s3/s3.go @@ -21,9 +21,17 @@ import ( types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go/middleware" smithyhttp "github.com/aws/smithy-go/transport/http" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" ) -const awsRegionHint = "us-east-1" +const ( + awsRegionHint = "us-east-1" + gcsReadWriteScope = "https://www.googleapis.com/auth/devstorage.read_write" + signingMiddlewareID = "Signing" +) + +var findDefaultGoogleCredentials = google.FindDefaultCredentials type s3store struct { s3Client *s3.Client @@ -47,6 +55,38 @@ func (p installerNotFoundError) IsNotFound() bool { // newS3Store initializes an S3 Datastore. func newS3Store(cfg config.S3ConfigInternal) (*s3store, error) { var opts []func(*aws_config.LoadOptions) error + gcsEndpoint := cfg.EndpointURL != "" && isGCS(cfg.EndpointURL) + + if cfg.GCSIAMAuth { + switch { + case cfg.EndpointURL == "": + return nil, errors.New("gcs iam auth requires endpoint_url to be set (e.g. https://storage.googleapis.com)") + case !gcsEndpoint: + return nil, fmt.Errorf("gcs iam auth requires endpoint_url to contain storage.googleapis.com (got %q)", cfg.EndpointURL) + } + if cfg.AccessKeyID != "" || cfg.SecretAccessKey != "" { + return nil, errors.New("gcs iam auth cannot be used with access key credentials") + } + if cfg.StsAssumeRoleArn != "" { + return nil, errors.New("gcs iam auth cannot be used with sts assume role") + } + } + + var gcsTokenSource oauth2.TokenSource + if cfg.GCSIAMAuth { + creds, err := findDefaultGoogleCredentials(context.Background(), gcsReadWriteScope) + if err != nil { + return nil, fmt.Errorf("finding default google credentials: %w", err) + } + gcsTokenSource = creds.TokenSource + // Even with SigV4 middleware removed, AWS SDK may still resolve credentials. + // Set a local static provider to avoid IMDS/network credential lookups. + opts = append(opts, aws_config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + "gcs-iam-auth", + "gcs-iam-auth", + "", + ))) + } // The service endpoint is deprecated in AWS, but required for S3 workalikes elsewhere if cfg.EndpointURL != "" { @@ -78,18 +118,23 @@ func newS3Store(cfg config.S3ConfigInternal) (*s3store, error) { } if cfg.Region == "" { - // Attempt to deduce region from bucket. - conf, err := aws_config.LoadDefaultConfig(context.Background(), - append(opts, aws_config.WithRegion(awsRegionHint))..., - ) - if err != nil { - return nil, fmt.Errorf("failed to create default config to get bucket region: %w", err) + if cfg.GCSIAMAuth { + // GCS doesn't expose AWS region APIs. Keep AWS SDK happy with a fixed hint. + cfg.Region = awsRegionHint + } else { + // Attempt to deduce region from bucket. + conf, err := aws_config.LoadDefaultConfig(context.Background(), + append(opts, aws_config.WithRegion(awsRegionHint))..., + ) + if err != nil { + return nil, fmt.Errorf("failed to create default config to get bucket region: %w", err) + } + bucketRegion, err := manager.GetBucketRegion(context.Background(), s3.NewFromConfig(conf), cfg.Bucket) + if err != nil { + return nil, fmt.Errorf("get bucket region: %w", err) + } + cfg.Region = bucketRegion } - bucketRegion, err := manager.GetBucketRegion(context.Background(), s3.NewFromConfig(conf), cfg.Bucket) - if err != nil { - return nil, fmt.Errorf("get bucket region: %w", err) - } - cfg.Region = bucketRegion } opts = append(opts, aws_config.WithRegion(cfg.Region)) @@ -111,12 +156,17 @@ func newS3Store(cfg config.S3ConfigInternal) (*s3store, error) { // Apply workaround if using Google Cloud Storage (GCS) endpoint // This fixes signature issues with AWS SDK v2 when using GCS // See: https://github.com/aws/aws-sdk-go-v2/issues/1816#issuecomment-1927281540 - if cfg.EndpointURL != "" && isGCS(cfg.EndpointURL) { + if gcsEndpoint && !cfg.GCSIAMAuth { // GCS alters the Accept-Encoding header which breaks the request signature ignoreSigningHeaders(o, []string{"Accept-Encoding"}) + } + if gcsEndpoint { // GCS also has issues with trailing checksums in UploadPart and PutObject operations disableTrailingChecksumForGCS(o) } + if cfg.GCSIAMAuth { + useGCSBearerAuth(o, gcsTokenSource) + } }) return &s3store{ @@ -127,6 +177,41 @@ func newS3Store(cfg config.S3ConfigInternal) (*s3store, error) { }, nil } +func useGCSBearerAuth(o *s3.Options, tokenSource oauth2.TokenSource) { + o.APIOptions = append(o.APIOptions, func(stack *middleware.Stack) error { + if tokenSource == nil { + return errors.New("gcs bearer auth requested but no google token source was configured") + } + + // Remove SigV4 signing. GCS IAM auth uses OAuth bearer tokens. + if _, err := stack.Finalize.Remove(signingMiddlewareID); err != nil { + return fmt.Errorf("removing signing middleware: %w", err) + } + + return stack.Finalize.Add(gcsBearerTokenAuth(tokenSource), middleware.After) + }) +} + +func gcsBearerTokenAuth(tokenSource oauth2.TokenSource) middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "GCSBearerTokenAuth", + func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (out middleware.FinalizeOutput, metadata middleware.Metadata, err error) { + req, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, fmt.Errorf("(gcsBearerTokenAuth) unexpected request middleware type %T", in.Request) + } + + token, err := tokenSource.Token() + if err != nil { + return out, metadata, fmt.Errorf("getting google access token: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token.AccessToken) + return next.HandleFinalize(ctx, in) + }, + ) +} + // CreateTestBucket creates a bucket with the provided name and a default // bucket config. Only recommended for local testing. func (s *s3store) CreateTestBucket(ctx context.Context, name string) error { diff --git a/server/datastore/s3/s3_test.go b/server/datastore/s3/s3_test.go new file mode 100644 index 0000000000..b1d9ede6a1 --- /dev/null +++ b/server/datastore/s3/s3_test.go @@ -0,0 +1,194 @@ +package s3 + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/config" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" +) + +func TestNewS3StoreGCSIAMAuthRequiresGCSEndpoint(t *testing.T) { + _, err := newS3Store(config.S3ConfigInternal{ + Bucket: "bucket", + Prefix: "prefix", + Region: "us-east-1", + EndpointURL: "https://s3.amazonaws.com", + GCSIAMAuth: true, + }) + require.ErrorContains(t, err, "gcs iam auth requires endpoint_url to contain storage.googleapis.com") + require.ErrorContains(t, err, `"https://s3.amazonaws.com"`) +} + +func TestNewS3StoreGCSIAMAuthRequiresEndpointSet(t *testing.T) { + _, err := newS3Store(config.S3ConfigInternal{ + Bucket: "bucket", + Prefix: "prefix", + Region: "us-east-1", + GCSIAMAuth: true, + }) + require.ErrorContains(t, err, "gcs iam auth requires endpoint_url to be set") +} + +func TestNewS3StoreGCSIAMAuthDisallowsStaticKeys(t *testing.T) { + _, err := newS3Store(config.S3ConfigInternal{ + Bucket: "bucket", + Prefix: "prefix", + Region: "us-east-1", + EndpointURL: "https://storage.googleapis.com", + GCSIAMAuth: true, + AccessKeyID: "id", + SecretAccessKey: "secret", + }) + require.ErrorContains(t, err, "gcs iam auth cannot be used with access key credentials") +} + +func TestNewS3StoreGCSIAMAuthDisallowsSTSRole(t *testing.T) { + _, err := newS3Store(config.S3ConfigInternal{ + Bucket: "bucket", + Prefix: "prefix", + Region: "us-east-1", + EndpointURL: "https://storage.googleapis.com", + GCSIAMAuth: true, + StsAssumeRoleArn: "arn:aws:iam::123456789012:role/test", + }) + require.ErrorContains(t, err, "gcs iam auth cannot be used with sts assume role") +} + +func TestNewS3StoreGCSIAMAuthCredentialLookupError(t *testing.T) { + originalFindDefaultGoogleCredentials := findDefaultGoogleCredentials + t.Cleanup(func() { + findDefaultGoogleCredentials = originalFindDefaultGoogleCredentials + }) + + findDefaultGoogleCredentials = func(context.Context, ...string) (*google.Credentials, error) { + return nil, errors.New("lookup failed") + } + + _, err := newS3Store(config.S3ConfigInternal{ + Bucket: "bucket", + Prefix: "prefix", + Region: "us-east-1", + EndpointURL: "https://storage.googleapis.com", + GCSIAMAuth: true, + }) + require.ErrorContains(t, err, "finding default google credentials") + require.ErrorContains(t, err, "lookup failed") +} + +func TestSoftwareInstallerStoreGCSIAMAuthUsesBearerToken(t *testing.T) { + type requestInfo struct { + AuthHeader string + Method string + Path string + } + + reqCh := make(chan requestInfo, 1) + testSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqCh <- requestInfo{ + AuthHeader: r.Header.Get("Authorization"), + Method: r.Method, + Path: r.URL.Path, + } + w.WriteHeader(http.StatusOK) + })) + defer testSrv.Close() + + originalFindDefaultGoogleCredentials := findDefaultGoogleCredentials + t.Cleanup(func() { + findDefaultGoogleCredentials = originalFindDefaultGoogleCredentials + }) + + const token = "test-bearer-token" + findDefaultGoogleCredentials = func(context.Context, ...string) (*google.Credentials, error) { + return &google.Credentials{ + TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}), + }, nil + } + + store, err := NewSoftwareInstallerStore(config.S3Config{ + SoftwareInstallersBucket: "bucket", + SoftwareInstallersPrefix: "prefix", + SoftwareInstallersEndpointURL: testSrv.URL + "/storage.googleapis.com", + SoftwareInstallersForceS3PathStyle: true, + SoftwareInstallersGCSIAMAuth: true, + }) + require.NoError(t, err) + + exists, err := store.Exists(context.Background(), "installer-id") + require.NoError(t, err) + require.True(t, exists) + + select { + case req := <-reqCh: + require.Equal(t, http.MethodHead, req.Method) + require.Equal(t, "Bearer "+token, req.AuthHeader) + require.NotContains(t, req.AuthHeader, "AWS4-HMAC-SHA256") + require.Contains(t, req.Path, "/bucket/", "expected bucket in request path, got %s", req.Path) + case <-time.After(2 * time.Second): + t.Fatal("did not receive request to test server") + } +} + +func TestCarveStoreGCSIAMAuthUsesBearerToken(t *testing.T) { + type requestInfo struct { + AuthHeader string + Method string + Path string + } + + reqCh := make(chan requestInfo, 1) + testSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case reqCh <- requestInfo{ + AuthHeader: r.Header.Get("Authorization"), + Method: r.Method, + Path: r.URL.Path, + }: + default: + } + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`false`)) + })) + defer testSrv.Close() + + originalFindDefaultGoogleCredentials := findDefaultGoogleCredentials + t.Cleanup(func() { + findDefaultGoogleCredentials = originalFindDefaultGoogleCredentials + }) + + const token = "carves-bearer-token" + findDefaultGoogleCredentials = func(context.Context, ...string) (*google.Credentials, error) { + return &google.Credentials{ + TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}), + }, nil + } + + store, err := NewCarveStore(config.S3Config{ + CarvesBucket: "carves-bucket", + CarvesPrefix: "carves-prefix", + CarvesEndpointURL: testSrv.URL + "/storage.googleapis.com", + CarvesForceS3PathStyle: true, + CarvesGCSIAMAuth: true, + }, nil) + require.NoError(t, err) + + _, err = store.listS3Carves(context.Background(), "", 10) + require.NoError(t, err) + + select { + case req := <-reqCh: + require.Equal(t, "Bearer "+token, req.AuthHeader) + require.NotContains(t, req.AuthHeader, "AWS4-HMAC-SHA256") + require.Contains(t, req.Path, "/carves-bucket", "expected carves bucket in request path, got %s", req.Path) + case <-time.After(2 * time.Second): + t.Fatal("did not receive request to test server") + } +}