diff --git a/pkg/block/blocktest/adapter.go b/pkg/block/blocktest/adapter.go index f6c9db88121..d37d595cb19 100644 --- a/pkg/block/blocktest/adapter.go +++ b/pkg/block/blocktest/adapter.go @@ -230,10 +230,6 @@ func getPresignedURLBasicTest(t *testing.T, adapter block.Adapter, storageNamesp if errors.Is(err, block.ErrOperationNotSupported) { t.Skip("GetPreSignedURL not supported") } - // Google storage returns an error if no credentials are found, and we can't sign the URL - if err != nil && strings.Contains(err.Error(), "no credentials found") { - t.Skip("GetPreSignedURL no credentials found") - } require.NoError(t, err) return preSignedURL, &exp } diff --git a/pkg/block/gs/adapter.go b/pkg/block/gs/adapter.go index 7e502ba072e..2adb39f9d42 100644 --- a/pkg/block/gs/adapter.go +++ b/pkg/block/gs/adapter.go @@ -45,6 +45,11 @@ type Adapter struct { disablePreSignedUI bool ServerSideEncryptionCustomerSupplied []byte ServerSideEncryptionKmsKeyID string + nowFactory func() time.Time + // presignedGoogleAccessID and presignedPrivateKey are used for testing with fake-gcs-server + // which requires explicit signing credentials since the test client has no credentials + presignedGoogleAccessID string + presignedPrivateKey []byte } func WithPreSignedExpiry(v time.Duration) func(a *Adapter) { @@ -73,12 +78,28 @@ func WithDisablePreSignedUI(b bool) func(a *Adapter) { } } +func WithNowFactory(f func() time.Time) func(a *Adapter) { + return func(a *Adapter) { + a.nowFactory = f + } +} + +// WithPresignedCredentials sets the Google Access ID and private key for signing URLs. +// This is primarily used for testing with fake-gcs-server where the client has no credentials. +func WithPresignedCredentials(googleAccessID string, privateKey []byte) func(a *Adapter) { + return func(a *Adapter) { + a.presignedGoogleAccessID = googleAccessID + a.presignedPrivateKey = privateKey + } +} + type AdapterOption func(a *Adapter) func NewAdapter(client *storage.Client, opts ...AdapterOption) *Adapter { a := &Adapter{ client: client, preSignedExpiry: block.DefaultPreSignExpiryDuration, + nowFactory: time.Now, // current time function can be mocked out via injection for testing purposes } for _, opt := range opts { opt(a) @@ -103,7 +124,7 @@ func (a *Adapter) log(ctx context.Context) logging.Logger { } func (a *Adapter) newPreSignedTime() time.Time { - return time.Now().UTC().Add(a.preSignedExpiry) + return a.nowFactory().UTC().Add(a.preSignedExpiry) } // withReadHandle returns a corresponding handle for reading object based on the encryption settings. @@ -243,6 +264,12 @@ func (a *Adapter) GetPreSignedURL(ctx context.Context, obj block.ObjectPointer, Expires: a.newPreSignedTime(), } + // Use explicit signing credentials if provided (for testing with fake-gcs-server) + if a.presignedGoogleAccessID != "" && len(a.presignedPrivateKey) > 0 { + opts.GoogleAccessID = a.presignedGoogleAccessID + opts.PrivateKey = a.presignedPrivateKey + } + // Add content-disposition if filename provided if mode == block.PreSignModeRead && filename != "" { contentDisposition := mime.FormatMediaType("attachment", map[string]string{ diff --git a/pkg/block/gs/adapter_test.go b/pkg/block/gs/adapter_test.go index e70c110065f..b87056aafbb 100644 --- a/pkg/block/gs/adapter_test.go +++ b/pkg/block/gs/adapter_test.go @@ -16,8 +16,42 @@ import ( "github.com/treeverse/lakefs/pkg/config" ) -func newAdapter() *gs.Adapter { - return gs.NewAdapter(client) +// testGoogleAccessID is a fake Google Access ID used for testing signed URLs +const testGoogleAccessID = "fake@test-project.iam.gserviceaccount.com" + +// testPrivateKey is a PEM-encoded RSA private key for testing signed URLs with fake-gcs-server. +// This is a test-only key generated with: `openssl genrsa 2048` +var testPrivateKey = []byte(`-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0LBKd6cf913PB +GIsh9qfrBT2limGijI8ctSQCH7CrbEjCGI4gtyUhgxKaFMV9hKeAAca9Kilck+xH +rjBM2kjzTHlkVgWa3TNy3VM2v26DsK9Q8v6p2enMLE6ofqWTyyNaaSDXuXVfNKGg +vzahyIUp7kZG1f8HKuBIybZk74gTCubYF4wNZQ/asJuq+o7QGTQpK6v4SfdQtwxM +6w0bRDc737M7WJLNn0r2dF4hOytQW7cO9vX02GrW94P3j+N5tT2ktrULo4XQxTZO +uA6DawWGRg2jf1hsZ2aiOJdUU71FBx9iU7Z9tL4QyB29TOPxi4OugK3NMWlMhv/G +93/feUHRAgMBAAECggEAB3KWcEC2ilhJJ0YEKKVf49EZxHbjyg1gyY/1zaDWeQGP +AOH1DGZnAnt+7c+rvwsNmvbyf9rcg+sz7khwTpmhKsMiS0rbLLTV1dAkX/waCFd0 +fxu/pJEBecsqPbYNfV5dZzUhsnUkDvP3oJPNi/K5tBs5ZwpybNm21MoXcBTu2qhB +RvQ3IvhcQj2PUfPw9S7kx5bCtW9aLcn5u7ySv6WNRBPbaEiMQhCEpFNQuG9w3keR +tJiQUzgvUZlutnyczMU+EZchmSHMUQMyE6ah+QStiyyUtOC1vtV95ZvLboEc6NJk +sPmx8ESugdj3Tx8PSciRD5T4C2RmktVJpxYaGEHauQKBgQDysPnIHEPsV7OjviZu +UfblYWGiWol/MRHj8/TKIGYU/Ss1qXMsICLudq/sghBmYA0h6vN1i44RR1w1HtOU +8urv7u90CCFigfu28UzPl2ajTkLbbg9wQwQ6qundcMSScSVEu9xRUrVkJToy5zuM +itl5yOSavusdOvWwZlFaqiTQeQKBgQC+DWvaE4z+reVPghVyD4Ob76DW81eGoq0Y +6JIMKnZqETVNEzWjWcsPF5dE747gmxtapHRvQrjrz0hfNttQgp7VcPbyLVx6MXrK +qL/wqLpZpCx8yJ5MQUHe6a+DJGSJeFH1nEZ2Bw6aOjoOD2GvdPp6flDytwrDMXZM +9cVbIwqWGQKBgQDn/jVIDX0AmHWouUSTgNa7PvPN9y4o4AdyGOqPrZjnx3teuLTY +IYBC5EIXm92Bf6AOJELGwrjz23tRbD5lzDC5W3abPIptWEP/BXufleMPiOhwSi2H +6whH7MnSXNIMCwzNP6fENYQgT1XrAw/xsWli+Z9OLeMi9hGWprhuKuc2QQKBgQCM +RnO4fn2u7MM4MBeMHI9TZUcd4HZV1XRV0jMZ761/FDx3KxqH+xq5hPwN0ZNvjIxg +Fsop5OGAi3orbN3rSr3ZZIugrIJ5XlP3iR5CjwccauS7JYhRWEk6MtlsvkvGe5xi +4HnRW9wXUarP/eJoErtd9iXhP+EduUBMBYspfW+u4QKBgF/kF/fucq2QkMCpDf+H +ITIxEup3Tg81lZAbtnZpfTU7TcPvgBRLWtg3gEKJfTIGL79hl7lPhdqUG9w5egl8 +KOGyY30wiwFQ4Lu1MX+BZTKgQG9FVDtOQ43JQOLCbYdcYeKU9tJi7FUgvZmdZRaL +sGXLHjxoneUJGohAIXVzlIGW +-----END PRIVATE KEY-----`) + +func newAdapter(opts ...gs.AdapterOption) *gs.Adapter { + return gs.NewAdapter(client, opts...) } func TestAdapter(t *testing.T) { @@ -28,7 +62,10 @@ func TestAdapter(t *testing.T) { externalPath, err := url.JoinPath(basePath, "external") require.NoError(t, err) - adapter := newAdapter() + adapter := newAdapter( + gs.WithNowFactory(blocktest.NowMockDefault), + gs.WithPresignedCredentials(testGoogleAccessID, testPrivateKey), + ) defer func() { require.NoError(t, adapter.Close()) }()