Skip to content

Commit d6d80f4

Browse files
authored
Merge pull request #772 from jetstack/sigv4
Use sigv4 for sending data to s3
2 parents d58fc65 + 298d84f commit d6d80f4

14 files changed

Lines changed: 253 additions & 94 deletions

File tree

internal/cyberark/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func LoadClientConfigFromEnvironment() (ClientConfig, error) {
4949
// NewDatauploadClient initializes and returns a new CyberArk Data Upload client.
5050
// It performs service discovery to find the necessary API endpoints and authenticates
5151
// using the provided client configuration.
52-
func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMap *servicediscovery.Services, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
52+
func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMap *servicediscovery.Services, tenantUUID string, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
5353
identityAPI := serviceMap.Identity.API
5454
if identityAPI == "" {
5555
return nil, errors.New("service discovery returned an empty identity API")
@@ -67,5 +67,5 @@ func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMa
6767
return nil, err
6868
}
6969

70-
return dataupload.New(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil
70+
return dataupload.New(httpClient, discoveryAPI, tenantUUID, identityClient.AuthenticateRequest), nil
7171
}

internal/cyberark/client_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
3434

3535
discoveryClient := servicediscovery.New(httpClient)
3636

37-
serviceMap, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain)
37+
serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain)
3838
if err != nil {
3939
t.Fatalf("failed to discover mock services: %v", err)
4040
}
4141

42-
cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, cfg)
42+
cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, tenantUUID, cfg)
4343
require.NoError(t, err)
4444

4545
err = cl.PutSnapshot(ctx, dataupload.Snapshot{
@@ -78,12 +78,12 @@ func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) {
7878

7979
discoveryClient := servicediscovery.New(httpClient)
8080

81-
serviceMap, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain)
81+
serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain)
8282
if err != nil {
8383
t.Fatalf("failed to discover services: %v", err)
8484
}
8585

86-
cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, cfg)
86+
cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, tenantUUID, cfg)
8787
require.NoError(t, err)
8888

8989
err = cl.PutSnapshot(ctx, dataupload.Snapshot{

internal/cyberark/dataupload/dataupload.go

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"k8s.io/apimachinery/pkg/runtime"
1616

1717
arkapi "github.com/jetstack/preflight/internal/cyberark/api"
18+
"github.com/jetstack/preflight/internal/cyberark/identity"
1819
"github.com/jetstack/preflight/pkg/version"
1920
)
2021

@@ -33,13 +34,19 @@ type CyberArkClient struct {
3334
baseURL string
3435
httpClient *http.Client
3536

36-
authenticateRequest func(req *http.Request) error
37+
tenantUUID string
38+
39+
authenticateRequest identity.RequestAuthenticator
3740
}
3841

39-
func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient {
42+
// New creates a new CyberArkClient. The tenant UUID is best sourced from service discovery along with the base URL.
43+
func New(httpClient *http.Client, baseURL string, tenantUUID string, authenticateRequest identity.RequestAuthenticator) *CyberArkClient {
4044
return &CyberArkClient{
41-
baseURL: baseURL,
42-
httpClient: httpClient,
45+
baseURL: baseURL,
46+
httpClient: httpClient,
47+
48+
tenantUUID: tenantUUID,
49+
4350
authenticateRequest: authenticateRequest,
4451
}
4552
}
@@ -102,13 +109,6 @@ type Snapshot struct {
102109
// has been received intact.
103110
// Read [Checking object integrity for data uploads in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html),
104111
// to learn more.
105-
//
106-
// TODO(wallrj): There is a bug in the AWS backend:
107-
// [S3 Presigned PutObjectCommand URLs ignore Sha256 Hash when uploading](https://github.com/aws/aws-sdk/issues/480)
108-
// ...which means that the `x-amz-checksum-sha256` request header is optional.
109-
// If you omit that header, it is possible to PUT any data.
110-
// There is a work around listed in that issue which we have shared with the
111-
// CyberArk API team.
112112
func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) error {
113113
if snapshot.ClusterID == "" {
114114
return fmt.Errorf("programmer mistake: the snapshot cluster ID cannot be left empty")
@@ -119,10 +119,12 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err
119119
if err := json.NewEncoder(io.MultiWriter(encodedBody, hash)).Encode(snapshot); err != nil {
120120
return err
121121
}
122+
122123
checksum := hash.Sum(nil)
123124
checksumHex := hex.EncodeToString(checksum)
124125
checksumBase64 := base64.StdEncoding.EncodeToString(checksum)
125-
presignedUploadURL, err := c.retrievePresignedUploadURL(ctx, checksumHex, snapshot.ClusterID)
126+
127+
presignedUploadURL, username, err := c.retrievePresignedUploadURL(ctx, checksumHex, snapshot.ClusterID, int64(encodedBody.Len()))
126128
if err != nil {
127129
return fmt.Errorf("while retrieving snapshot upload URL: %s", err)
128130
}
@@ -132,7 +134,21 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err
132134
if err != nil {
133135
return err
134136
}
137+
135138
req.Header.Set("X-Amz-Checksum-Sha256", checksumBase64)
139+
req.Header.Set("X-Amz-Server-Side-Encryption", "AES256")
140+
141+
q := url.Values{}
142+
143+
q.Add("agent_version", snapshot.AgentVersion)
144+
q.Add("tenant_id", c.tenantUUID)
145+
q.Add("upload_type", "k8s_snapshot")
146+
q.Add("uploader_id", snapshot.ClusterID)
147+
q.Add("username", username)
148+
q.Add("vendor", "k8s")
149+
150+
req.Header.Set("X-Amz-Tagging", q.Encode())
151+
136152
version.SetUserAgent(req)
137153

138154
res, err := c.httpClient.Do(req)
@@ -152,44 +168,57 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err
152168
return nil
153169
}
154170

155-
func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, clusterID string) (string, error) {
171+
// RetrievePresignedUploadURLRequest is the JSON body sent to the inventory API to request a presigned upload URL.
172+
type RetrievePresignedUploadURLRequest struct {
173+
ClusterID string `json:"cluster_id"`
174+
Checksum string `json:"checksum_sha256"`
175+
176+
// AgentVersion is the v-prefixed version of the agent uploading the snapshot.
177+
// Note that the backend relies on this version being v-prefixed semver.
178+
AgentVersion string `json:"agent_version"`
179+
180+
// FileSize is the size of the data we'll upload in bytes
181+
FileSize int64 `json:"file_size"`
182+
}
183+
184+
func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, clusterID string, fileSize int64) (string, string, error) {
156185
uploadURL, err := url.JoinPath(c.baseURL, apiPathSnapshotLinks)
157186
if err != nil {
158-
return "", err
187+
return "", "", err
159188
}
160189

161-
request := struct {
162-
ClusterID string `json:"cluster_id"`
163-
Checksum string `json:"checksum_sha256"`
164-
AgentVersion string `json:"agent_version"`
165-
}{
190+
request := RetrievePresignedUploadURLRequest{
166191
ClusterID: clusterID,
167192
Checksum: checksum,
168193
AgentVersion: version.PreflightVersion,
194+
FileSize: fileSize,
169195
}
170196

171197
encodedBody := &bytes.Buffer{}
172198
if err := json.NewEncoder(encodedBody).Encode(request); err != nil {
173-
return "", err
199+
return "", "", err
174200
}
175201

176202
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, encodedBody)
177203
if err != nil {
178-
return "", err
204+
return "", "", err
179205
}
180206

181207
req.Header.Set("Content-Type", "application/json")
182-
if err := c.authenticateRequest(req); err != nil {
183-
return "", fmt.Errorf("failed to authenticate request: %s", err)
208+
209+
username, err := c.authenticateRequest(req)
210+
if err != nil {
211+
return "", "", fmt.Errorf("failed to authenticate request: %s", err)
184212
}
213+
185214
version.SetUserAgent(req)
186215

187216
// Add telemetry headers
188217
arkapi.SetTelemetryRequestHeader(req)
189218

190219
res, err := c.httpClient.Do(req)
191220
if err != nil {
192-
return "", err
221+
return "", "", err
193222
}
194223
defer res.Body.Close()
195224

@@ -198,7 +227,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
198227
if len(body) == 0 {
199228
body = []byte(`<empty body>`)
200229
}
201-
return "", fmt.Errorf("received response with status code %d: %s", code, bytes.TrimSpace(body))
230+
return "", "", fmt.Errorf("received response with status code %d: %s", code, bytes.TrimSpace(body))
202231
}
203232

204233
response := struct {
@@ -207,11 +236,11 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
207236

208237
if err := json.NewDecoder(io.LimitReader(res.Body, maxRetrievePresignedUploadURLBodySize)).Decode(&response); err != nil {
209238
if err == io.ErrUnexpectedEOF {
210-
return "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
239+
return "", "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
211240
}
212241

213-
return "", fmt.Errorf("failed to parse JSON from otherwise successful request to start data upload: %s", err)
242+
return "", "", fmt.Errorf("failed to parse JSON from otherwise successful request to start data upload: %s", err)
214243
}
215244

216-
return response.URL, nil
245+
return response.URL, username, nil
217246
}

internal/cyberark/dataupload/dataupload_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"k8s.io/klog/v2/ktesting"
1111

1212
"github.com/jetstack/preflight/internal/cyberark/dataupload"
13+
"github.com/jetstack/preflight/internal/cyberark/identity"
1314
"github.com/jetstack/preflight/pkg/version"
1415

1516
_ "k8s.io/klog/v2/ktesting/init"
@@ -19,17 +20,17 @@ import (
1920
// mock API server. The mock server is configured to return different responses
2021
// based on the cluster ID and bearer token used in the request.
2122
func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
22-
setToken := func(token string) func(*http.Request) error {
23-
return func(req *http.Request) error {
23+
setToken := func(token string) identity.RequestAuthenticator {
24+
return func(req *http.Request) (string, error) {
2425
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
25-
return nil
26+
return "foo@example.com", nil // set a dummy username for testing purposes; the actual value is not important for these tests
2627
}
2728
}
2829

2930
tests := []struct {
3031
name string
3132
snapshot dataupload.Snapshot
32-
authenticate func(req *http.Request) error
33+
authenticate identity.RequestAuthenticator
3334
requireFn func(t *testing.T, err error)
3435
}{
3536
{
@@ -96,7 +97,7 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
9697

9798
datauploadAPIBaseURL, httpClient := dataupload.MockDataUploadServer(t)
9899

99-
cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, tc.authenticate)
100+
cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, "test-tenant-uuid", tc.authenticate)
100101

101102
err := cyberArkClient.PutSnapshot(ctx, tc.snapshot)
102103
tc.requireFn(t, err)

0 commit comments

Comments
 (0)