Files
Carlo DiCelico a5101d796f Address review feedback for GCS presigned downloads
- config: require an https GCS endpoint and HMAC credentials when signed URLs
  are enabled, and reject combining them with STS assume role (alongside the
  existing GCS IAM auth check).
- s3 store: build the presign client once and reuse it across Sign() calls.
- changes: note bootstrap package downloads are covered too.
- tests: assert the presigned URL shape and cover the STS assume-role rejection.
2026-08-06 11:56:05 -04:00

85 lines
2.9 KiB
Go

package s3
import (
"context"
"net/url"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)
// TestSignGCSPresignedURL verifies the GCS presigned-URL download path added on
// top of the upstream CloudFront-only Sign(). It runs fully offline:
// PresignGetObject computes the URL locally without contacting the bucket, and
// a non-empty region avoids the GetBucketRegion network lookup in newS3Store.
func TestSignGCSPresignedURL(t *testing.T) {
baseCfg := func() config.S3Config {
return config.S3Config{
SoftwareInstallersBucket: "test-bucket",
SoftwareInstallersRegion: "auto",
SoftwareInstallersEndpointURL: "https://storage.googleapis.com",
SoftwareInstallersAccessKeyID: "GOOG-test",
SoftwareInstallersSecretAccessKey: "secret",
SoftwareInstallersForceS3PathStyle: true,
}
}
t.Run("signed url enabled returns GCS presigned URL", func(t *testing.T) {
cfg := baseCfg()
cfg.SoftwareInstallersSignedURL = true
store, err := NewSoftwareInstallerStore(cfg)
require.NoError(t, err)
signed, err := store.Sign(context.Background(), "abc123", 15*time.Minute)
require.NoError(t, err)
u, err := url.Parse(signed)
require.NoError(t, err)
require.Equal(t, "https", u.Scheme)
require.Equal(t, "storage.googleapis.com", u.Host)
// Path-style addressing puts the bucket and key in the path.
require.Contains(t, u.Path, "test-bucket")
require.Contains(t, u.Path, "abc123")
q := u.Query()
require.True(t,
q.Get("X-Amz-Signature") != "" || q.Get("X-Goog-Signature") != "",
"expected a presigned signature query param, got %s", signed)
require.NotEmpty(t, q.Get("X-Amz-Algorithm"))
require.Equal(t, "900", q.Get("X-Amz-Expires")) // 15 minutes
})
t.Run("signed url disabled and no cloudfront returns ErrNotConfigured", func(t *testing.T) {
store, err := NewSoftwareInstallerStore(baseCfg())
require.NoError(t, err)
_, err = store.Sign(context.Background(), "abc123", 15*time.Minute)
require.ErrorIs(t, err, fleet.ErrNotConfigured)
})
t.Run("signed url with gcs iam auth is rejected", func(t *testing.T) {
// GCS IAM (bearer) auth is incompatible with SigV4 presigning, so store
// initialization must fail rather than hand out unusable signed URLs.
cfg := baseCfg()
cfg.SoftwareInstallersSignedURL = true
cfg.SoftwareInstallersGCSIAMAuth = true
_, err := NewSoftwareInstallerStore(cfg)
require.ErrorContains(t, err, "gcs iam auth")
})
t.Run("signed url with sts assume role is rejected", func(t *testing.T) {
// STS assume-role swaps the HMAC credentials presigning needs for
// temporary AWS credentials GCS can't verify, so store init must fail.
cfg := baseCfg()
cfg.SoftwareInstallersSignedURL = true
cfg.SoftwareInstallersStsAssumeRoleArn = "arn:aws:iam::123456789012:role/test"
_, err := NewSoftwareInstallerStore(cfg)
require.ErrorContains(t, err, "sts assume role")
})
}