diff --git a/internal/cyberark/client.go b/internal/cyberark/client.go index 3d553142..8593ed51 100644 --- a/internal/cyberark/client.go +++ b/internal/cyberark/client.go @@ -49,7 +49,7 @@ func LoadClientConfigFromEnvironment() (ClientConfig, error) { // NewDatauploadClient initializes and returns a new CyberArk Data Upload client. // It performs service discovery to find the necessary API endpoints and authenticates // using the provided client configuration. -func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMap *servicediscovery.Services, cfg ClientConfig) (*dataupload.CyberArkClient, error) { +func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMap *servicediscovery.Services, tenantUUID string, cfg ClientConfig) (*dataupload.CyberArkClient, error) { identityAPI := serviceMap.Identity.API if identityAPI == "" { return nil, errors.New("service discovery returned an empty identity API") @@ -67,5 +67,5 @@ func NewDatauploadClient(ctx context.Context, httpClient *http.Client, serviceMa return nil, err } - return dataupload.New(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil + return dataupload.New(httpClient, discoveryAPI, tenantUUID, cfg.Username, identityClient.AuthenticateRequest), nil } diff --git a/internal/cyberark/client_test.go b/internal/cyberark/client_test.go index ae7162ae..1c220d2d 100644 --- a/internal/cyberark/client_test.go +++ b/internal/cyberark/client_test.go @@ -34,12 +34,12 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) { discoveryClient := servicediscovery.New(httpClient) - serviceMap, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain) + serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain) if err != nil { t.Fatalf("failed to discover mock services: %v", err) } - cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, cfg) + cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, tenantUUID, cfg) require.NoError(t, err) err = cl.PutSnapshot(ctx, dataupload.Snapshot{ @@ -78,12 +78,12 @@ func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) { discoveryClient := servicediscovery.New(httpClient) - serviceMap, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain) + serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(t.Context(), cfg.Subdomain) if err != nil { t.Fatalf("failed to discover services: %v", err) } - cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, cfg) + cl, err := cyberark.NewDatauploadClient(ctx, httpClient, serviceMap, tenantUUID, cfg) require.NoError(t, err) err = cl.PutSnapshot(ctx, dataupload.Snapshot{ diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index 0d5bcc08..71a2e492 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/url" + "strings" "k8s.io/apimachinery/pkg/runtime" @@ -33,13 +34,21 @@ type CyberArkClient struct { baseURL string httpClient *http.Client + tenantUUID string + username string + authenticateRequest func(req *http.Request) error } -func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient { +// TODO: should probably take a cyberark Identity client directly and query subdomain + username from that +func New(httpClient *http.Client, baseURL string, tenantUUID string, username string, authenticateRequest func(req *http.Request) error) *CyberArkClient { return &CyberArkClient{ - baseURL: baseURL, - httpClient: httpClient, + baseURL: baseURL, + httpClient: httpClient, + + tenantUUID: tenantUUID, + username: username, + authenticateRequest: authenticateRequest, } } @@ -102,13 +111,6 @@ type Snapshot struct { // has been received intact. // Read [Checking object integrity for data uploads in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html), // to learn more. -// -// TODO(wallrj): There is a bug in the AWS backend: -// [S3 Presigned PutObjectCommand URLs ignore Sha256 Hash when uploading](https://github.com/aws/aws-sdk/issues/480) -// ...which means that the `x-amz-checksum-sha256` request header is optional. -// If you omit that header, it is possible to PUT any data. -// There is a work around listed in that issue which we have shared with the -// CyberArk API team. func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) error { if snapshot.ClusterID == "" { return fmt.Errorf("programmer mistake: the snapshot cluster ID cannot be left empty") @@ -133,6 +135,30 @@ func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) err return err } req.Header.Set("X-Amz-Checksum-Sha256", checksumBase64) + + // TODO: this is temporary logic to only enable the extra sigv4 headers on the specific tenant with + // the feature flag enabled + // We'll remove this later. + if c.tenantUUID == "8f08a102-58ca-49cd-960e-debc5e0d3cd4" { + req.Header.Set("X-Amz-Server-Side-Encryption", "AES256") + + q := url.Values{} + + q.Add("agent_version", snapshot.AgentVersion) + q.Add("tenant_id", c.tenantUUID) + q.Add("upload_type", "k8s_snapshot") + q.Add("uploader_id", snapshot.ClusterID) + q.Add("username", c.username) + q.Add("vendor", "k8s") + + // TODO: Remove this hack when urlencoding is fixed on the backend + // MASSIVE HACK: backend is not url-encoding the username, so we need to "decode" it here to match what the backend expects + // Backend has committed to change this soon, but to unbreak tests in the meantime we need to do this hack. + encodedTags := strings.ReplaceAll(q.Encode(), "%40", "@") + + req.Header.Set("X-Amz-Tagging", encodedTags) + } + version.SetUserAgent(req) res, err := c.httpClient.Do(req) diff --git a/internal/cyberark/dataupload/dataupload_test.go b/internal/cyberark/dataupload/dataupload_test.go index f38a0e51..e7402f23 100644 --- a/internal/cyberark/dataupload/dataupload_test.go +++ b/internal/cyberark/dataupload/dataupload_test.go @@ -96,7 +96,7 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) { datauploadAPIBaseURL, httpClient := dataupload.MockDataUploadServer(t) - cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, tc.authenticate) + cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, "test-tenant-uuid", "test-user@example.com", tc.authenticate) err := cyberArkClient.PutSnapshot(ctx, tc.snapshot) tc.requireFn(t, err) diff --git a/internal/cyberark/identity/cmd/testidentity/main.go b/internal/cyberark/identity/cmd/testidentity/main.go index 8729cfbe..916c81ea 100644 --- a/internal/cyberark/identity/cmd/testidentity/main.go +++ b/internal/cyberark/identity/cmd/testidentity/main.go @@ -51,7 +51,7 @@ func run(ctx context.Context) error { httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs) sdClient := servicediscovery.New(httpClient) - services, err := sdClient.DiscoverServices(ctx, subdomain) + services, _, err := sdClient.DiscoverServices(ctx, subdomain) if err != nil { return fmt.Errorf("while performing service discovery: %s", err) } diff --git a/internal/cyberark/identity/identity_test.go b/internal/cyberark/identity/identity_test.go index 732805e7..917ba15d 100644 --- a/internal/cyberark/identity/identity_test.go +++ b/internal/cyberark/identity/identity_test.go @@ -53,7 +53,7 @@ func TestLoginUsernamePassword_RealAPI(t *testing.T) { arktesting.SkipIfNoEnv(t) subdomain := os.Getenv("ARK_SUBDOMAIN") httpClient := http.DefaultClient - services, err := servicediscovery.New(httpClient).DiscoverServices(t.Context(), subdomain) + services, _, err := servicediscovery.New(httpClient).DiscoverServices(t.Context(), subdomain) require.NoError(t, err) loginUsernamePasswordTests(t, func(t testing.TB) inputs { diff --git a/internal/cyberark/servicediscovery/discovery.go b/internal/cyberark/servicediscovery/discovery.go index e838e507..82394ab3 100644 --- a/internal/cyberark/servicediscovery/discovery.go +++ b/internal/cyberark/servicediscovery/discovery.go @@ -95,17 +95,21 @@ type Services struct { // DiscoverServices fetches from the service discovery service for a given subdomain // and parses the CyberArk Identity API URL and Inventory API URL. -func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Services, error) { +// It also returns the Tenant ID UUID corresponding to the subdomain. +func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Services, string, error) { u, err := url.Parse(c.baseURL) if err != nil { - return nil, fmt.Errorf("invalid base URL for service discovery: %w", err) + return nil, "", fmt.Errorf("invalid base URL for service discovery: %w", err) } + u.Path = path.Join(u.Path, "api/public/tenant-discovery") u.RawQuery = url.Values{"bySubdomain": []string{subdomain}}.Encode() + endpoint := u.String() + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { - return nil, fmt.Errorf("failed to initialise request to %s: %s", endpoint, err) + return nil, "", fmt.Errorf("failed to initialise request to %s: %s", endpoint, err) } request.Header.Set("Accept", "application/json") @@ -114,7 +118,7 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi arkapi.SetTelemetryRequestHeader(request) resp, err := c.client.Do(request) if err != nil { - return nil, fmt.Errorf("failed to perform HTTP request: %s", err) + return nil, "", fmt.Errorf("failed to perform HTTP request: %s", err) } defer resp.Body.Close() @@ -123,19 +127,19 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi // a 404 error is returned with an empty JSON body "{}" if the subdomain is unknown; at the time of writing, we haven't observed // any other errors and so we can't special case them if resp.StatusCode == http.StatusNotFound { - return nil, fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", subdomain) + return nil, "", fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", subdomain) } - return nil, fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status) + return nil, "", fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status) } var discoveryResp DiscoveryResponse err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&discoveryResp) if err != nil { if err == io.ErrUnexpectedEOF { - return nil, fmt.Errorf("rejecting JSON response from server as it was too large or was truncated") + return nil, "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated") } - return nil, fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err) + return nil, "", fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err) } var identityAPI, discoveryContextAPI string for _, svc := range discoveryResp.Services { @@ -158,7 +162,7 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi } if identityAPI == "" { - return nil, fmt.Errorf("didn't find %s in service discovery response, "+ + return nil, "", fmt.Errorf("didn't find %s in service discovery response, "+ "which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName) } //TODO: Should add a check for discoveryContextAPI too? @@ -166,5 +170,5 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi return &Services{ Identity: ServiceEndpoint{API: identityAPI}, DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI}, - }, nil + }, discoveryResp.TenantID, nil } diff --git a/internal/cyberark/servicediscovery/discovery_test.go b/internal/cyberark/servicediscovery/discovery_test.go index d1091307..00d0fd58 100644 --- a/internal/cyberark/servicediscovery/discovery_test.go +++ b/internal/cyberark/servicediscovery/discovery_test.go @@ -66,7 +66,7 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) { client := New(httpClient) - services, err := client.DiscoverServices(ctx, testSpec.subdomain) + services, _, err := client.DiscoverServices(ctx, testSpec.subdomain) if testSpec.expectedError != nil { assert.EqualError(t, err, testSpec.expectedError.Error()) assert.Nil(t, services) diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index 735313bd..3c573689 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -67,7 +67,7 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin discoveryClient := servicediscovery.New(o.httpClient) - serviceMap, err := discoveryClient.DiscoverServices(ctx, cfg.Subdomain) + serviceMap, tenantUUID, err := discoveryClient.DiscoverServices(ctx, cfg.Subdomain) if err != nil { return err } @@ -81,7 +81,7 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin // Minimize the snapshot to reduce size and improve privacy minimizeSnapshot(log.V(logs.Debug), &snapshot) - datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, serviceMap, cfg) + datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, serviceMap, tenantUUID, cfg) if err != nil { return fmt.Errorf("while initializing data upload client: %s", err) }