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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/40374) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Carlo <1778532+cdcme@users.noreply.github.com>
This commit is contained in:
@@ -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`.
|
||||
@@ -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 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+98
-13
@@ -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 {
|
||||
|
||||
@@ -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(`<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><IsTruncated>false</IsTruncated></ListBucketResult>`))
|
||||
}))
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user