Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/cyberark/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}
8 changes: 4 additions & 4 deletions internal/cyberark/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
46 changes: 36 additions & 10 deletions internal/cyberark/dataupload/dataupload.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"net/http"
"net/url"
"strings"

"k8s.io/apimachinery/pkg/runtime"

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/cyberark/dataupload/dataupload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/cyberark/identity/cmd/testidentity/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cyberark/identity/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 14 additions & 10 deletions internal/cyberark/servicediscovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -158,13 +162,13 @@ 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?

return &Services{
Identity: ServiceEndpoint{API: identityAPI},
DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI},
}, nil
}, discoveryResp.TenantID, nil
}
2 changes: 1 addition & 1 deletion internal/cyberark/servicediscovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pkg/client/client_cyberark.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down