diff --git a/.envrc.template b/.envrc.template index 6765a4d2..703411e4 100644 --- a/.envrc.template +++ b/.envrc.template @@ -14,8 +14,8 @@ export CLOUDSDK_COMPUTE_ZONE= # the GCP zone where a GKE cluster will be created export CLUSTER_NAME= # the name of the GKE cluster which will be created. E.g. cluster-1 # The following variables are required for CyberArk / MachineHub integration tests. -export ARK_SUBDOMAIN= # your CyberArk tenant subdomain +export ARK_SUBDOMAIN= # your CyberArk tenant subdomain e.g. tlskp-test export ARK_USERNAME= # your CyberArk username export ARK_SECRET= # your CyberArk password # OPTIONAL: the URL for the CyberArk Discovery API if not using the production environment -export ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2 +export ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/ diff --git a/pkg/internal/cyberark/client_test.go b/pkg/internal/cyberark/client_test.go index c8fa98ab..e532b037 100644 --- a/pkg/internal/cyberark/client_test.go +++ b/pkg/internal/cyberark/client_test.go @@ -48,7 +48,7 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) { // ARK_SUBDOMAIN should be your tenant subdomain. // // To test against a tenant on the integration platform, also set: -// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2 +// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/ // // To enable verbose request logging: // diff --git a/pkg/internal/cyberark/identity/cmd/testidentity/main.go b/pkg/internal/cyberark/identity/cmd/testidentity/main.go index 45be02ba..181eb2cd 100644 --- a/pkg/internal/cyberark/identity/cmd/testidentity/main.go +++ b/pkg/internal/cyberark/identity/cmd/testidentity/main.go @@ -22,7 +22,7 @@ import ( // the login is successful. // // To test against a tenant on the integration platform, set: -// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2 +// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/ const ( subdomainFlag = "subdomain" usernameFlag = "username" diff --git a/pkg/internal/cyberark/servicediscovery/discovery.go b/pkg/internal/cyberark/servicediscovery/discovery.go index 9aeab674..250f6e47 100644 --- a/pkg/internal/cyberark/servicediscovery/discovery.go +++ b/pkg/internal/cyberark/servicediscovery/discovery.go @@ -8,13 +8,14 @@ import ( "net/http" "net/url" "os" + "path" "github.com/jetstack/preflight/pkg/version" ) const ( // ProdDiscoveryAPIBaseURL is the base URL for the production CyberArk Service Discovery API - ProdDiscoveryAPIBaseURL = "https://platform-discovery.cyberark.cloud/api/v2/" + ProdDiscoveryAPIBaseURL = "https://platform-discovery.cyberark.cloud/" // IdentityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API // We were told to use the identity_administration field, not the identity_user_portal field. @@ -53,32 +54,54 @@ func New(httpClient *http.Client) *Client { return client } +// DiscoveryResponse represents the full JSON response returned by the CyberArk api/tenant-discovery/public API +// The API is documented here https://ca-il-confluence.il.cyber-ark.com/spaces/EV/pages/575618345/Updated+PD+APIs+doc +type DiscoveryResponse struct { + Region string `json:"region"` + DRRegion string `json:"dr_region"` + Subdomain string `json:"subdomain"` + TenantID string `json:"tenant_id"` + PlatformID string `json:"platform_id"` + IdentityID string `json:"identity_id"` + DefaultURL string `json:"default_url"` + TenantFlags map[string]interface{} `json:"tenant_flags"` + Services []Service `json:"services"` +} + +type Service struct { + ServiceName string `json:"service_name"` + ServiceSubdomains []string `json:"service_subdomains"` + Region string `json:"region"` + Endpoints []ServiceEndpoint `json:"endpoints"` +} + // ServiceEndpoint represents a single service endpoint returned by the CyberArk // Service Discovery API. The JSON field names here must match the field names -// returned by the Service Discovery API. Currently, we only care about the -// "api" field. Other fields are intentionally ignored here. +// returned by the Service Discovery API. type ServiceEndpoint struct { - API string `json:"api"` + IsActive bool `json:"is_active"` + Type string `json:"type"` + UI string `json:"ui"` + API string `json:"api"` } -// Services represents the relevant services returned by the CyberArk Service -// Discovery API for a given subdomain. Currently, we only care about the -// Identity API and the Discovery Context API. Other services are intentionally -// ignored here. The JSON field names here must match the field names returned -// by the Service Discovery API. +// This is a convenience struct to hold the two ServiceEndpoints we care about. +// Currently, we only care about the Identity API and the Discovery Context API. type Services struct { - Identity ServiceEndpoint `json:"identity_administration"` - DiscoveryContext ServiceEndpoint `json:"discoverycontext"` + Identity ServiceEndpoint + DiscoveryContext ServiceEndpoint } // 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) { - endpoint, err := url.JoinPath(c.baseURL, "services", "subdomain", subdomain) + u, err := url.Parse(c.baseURL) if err != nil { - return nil, fmt.Errorf("failed to build a valid URL for subdomain %s; possibly an invalid endpoint: %s", subdomain, 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) @@ -104,19 +127,42 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi return nil, fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status) } - var services Services - err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&services) + 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("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err) } + var identityAPI, discoveryContextAPI string + for _, svc := range discoveryResp.Services { + switch svc.ServiceName { + case IdentityServiceName: + for _, ep := range svc.Endpoints { + if ep.Type == "main" && ep.IsActive && ep.API != "" { + identityAPI = ep.API + break + } + } + case DiscoveryContextServiceName: + for _, ep := range svc.Endpoints { + if ep.Type == "main" && ep.IsActive && ep.API != "" { + discoveryContextAPI = ep.API + break + } + } + } + } - if services.Identity.API == "" { - 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) + if identityAPI == "" { + 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, nil + return &Services{ + Identity: ServiceEndpoint{API: identityAPI}, + DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI}, + }, nil } diff --git a/pkg/internal/cyberark/servicediscovery/mock.go b/pkg/internal/cyberark/servicediscovery/mock.go index 12607903..f462a6c5 100644 --- a/pkg/internal/cyberark/servicediscovery/mock.go +++ b/pkg/internal/cyberark/servicediscovery/mock.go @@ -20,10 +20,11 @@ import ( const ( // MockDiscoverySubdomain is the subdomain for which the MockDiscoveryServer will return a success response - MockDiscoverySubdomain = "venafi-test" + MockDiscoverySubdomain = "tlskp-test" mockIdentityAPIURL = "https://ajp5871.id.integration-cyberark.cloud" - mockDiscoveryContextAPIURL = "https://venafi-test.inventory.integration-cyberark.cloud/api" + mockDiscoveryContextAPIURL = "https://venafi-test.inventory.integration-cyberark.cloud/" + prefix = "/api/public/tenant-discovery?bySubdomain=" ) //go:embed testdata/discovery_success.json.template @@ -77,7 +78,7 @@ func (mds *mockDiscoveryServer) ServeHTTP(w http.ResponseWriter, r *http.Request return } - if !strings.HasPrefix(r.URL.String(), "/services/subdomain/") { + if !strings.HasPrefix(r.URL.String(), prefix) { // This was observed by making a request to /api/v2/services/asd // Normally, we'd expect 404 Not Found but we match the observed response here w.WriteHeader(http.StatusForbidden) @@ -97,7 +98,7 @@ func (mds *mockDiscoveryServer) ServeHTTP(w http.ResponseWriter, r *http.Request return } - subdomain := strings.TrimPrefix(r.URL.String(), "/services/subdomain/") + subdomain := strings.TrimPrefix(r.URL.String(), prefix) switch subdomain { case MockDiscoverySubdomain: @@ -105,7 +106,22 @@ func (mds *mockDiscoveryServer) ServeHTTP(w http.ResponseWriter, r *http.Request case "no-identity": // return a snippet of valid service discovery JSON, but don't include the identity service - _, _ = w.Write([]byte(`{"data_privacy": {"ui": "https://ui.dataprivacy.integration-cyberark.cloud/", "api": "https://us-east-1.dataprivacy.integration-cyberark.cloud/api", "bootstrap": "https://venafi-test-data_privacy.integration-cyberark.cloud", "region": "us-east-1"}}`)) + _, _ = w.Write([]byte(`{ + "services": [ + { + "service_name": "data_privacy", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui.dataprivacy.integration-cyberark.cloud/", + "api": "https://us-east-1.dataprivacy.integration-cyberark.cloud/api" + } + ] + } + ] + }`)) case "bad-request": // test how the client handles a random unexpected response diff --git a/pkg/internal/cyberark/servicediscovery/testdata/README.md b/pkg/internal/cyberark/servicediscovery/testdata/README.md index d8fa2b79..d6cf51a8 100644 --- a/pkg/internal/cyberark/servicediscovery/testdata/README.md +++ b/pkg/internal/cyberark/servicediscovery/testdata/README.md @@ -4,8 +4,9 @@ All data in this folder is derived from an unauthenticated endpoint accessible f To get the original data: +NOTE: This API is not implemented yet as of 02.09.2025 but is expected to be finalised by end of PI3 2025. ```bash -curl -fsSL "${ARK_DISCOVERY_API}/services/subdomain/${ARK_SUBDOMAIN}" | jq +curl -fsSL "${ARK_DISCOVERY_API}?bySubdomain=${ARK_SUBDOMAIN}" | jq ``` Then replace `identity_administration.api` with `{{ .Identity.API }}` and diff --git a/pkg/internal/cyberark/servicediscovery/testdata/discovery_success.json.template b/pkg/internal/cyberark/servicediscovery/testdata/discovery_success.json.template index b41c4f1d..c503028c 100644 --- a/pkg/internal/cyberark/servicediscovery/testdata/discovery_success.json.template +++ b/pkg/internal/cyberark/servicediscovery/testdata/discovery_success.json.template @@ -1,98 +1,206 @@ { - "data_privacy": { - "ui": "https://ui.dataprivacy.integration-cyberark.cloud/", - "api": "https://us-east-1.dataprivacy.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-data_privacy.integration-cyberark.cloud", - "region": "us-east-1" + "region": "us-east-1", + "dr_region": "us-east-2", + "subdomain": "venafi-test", + "platform_id": "platform-123", + "identity_id": "identity-456", + "default_url": "https://venafi-test.integration-cyberark.cloud", + "tenant_flags": { + "is_crdr_supported": "true", + "is_crdr_active": "true" + }, + "services": [ + { + "service_name": "data_privacy", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui.dataprivacy.integration-cyberark.cloud/", + "api": "https://us-east-1.dataprivacy.integration-cyberark.cloud/api" + } + ] }, - "secrets_manager": { - "ui": "https://ui.test-conjur.cloud", - "api": "https://venafi-test.secretsmgr.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-secrets_manager.integration-cyberark.cloud", - "region": "us-east-2" + { + "service_name": "secrets_manager", + "region": "us-east-2", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui.test-conjur.cloud", + "api": "https://venafi-test.secretsmgr.integration-cyberark.cloud/api" + } + ] }, - "idaptive_risk_analytics": { - "ui": "https://ajp5871-my.analytics.idaptive.qa", - "api": "https://ajp5871-my.analytics.idaptive.qa", - "bootstrap": "https://venafi-test-idaptive_risk_analytics.integration-cyberark.cloud", - "region": "US-East-Pod" + { + "service_name": "idaptive_risk_analytics", + "region": "US-East-Pod", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ajp5871-my.analytics.idaptive.qa", + "api": "https://ajp5871-my.analytics.idaptive.qa" + } + ] }, - "component_manager": { - "ui": "https://ui-connectormanagement.connectormanagement.integration-cyberark.cloud", - "api": "https://venafi-test.connectormanagement.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-component_manager.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "component_manager", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui-connectormanagement.connectormanagement.integration-cyberark.cloud", + "api": "https://venafi-test.connectormanagement.integration-cyberark.cloud/api" + } + ] }, - "recording": { - "ui": "https://us-east-1.rec-ui.recording.integration-cyberark.cloud", - "api": "https://venafi-test.recording.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-recording.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "recording", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://us-east-1.rec-ui.recording.integration-cyberark.cloud", + "api": "https://venafi-test.recording.integration-cyberark.cloud/api" + } + ] }, - "identity_user_portal": { - "ui": "https://ajp5871.id.integration-cyberark.cloud", - "api": "https://ajp5871.id.integration-cyberark.cloud", - "bootstrap": "https://venafi-test-identity_user_portal.integration-cyberark.cloud/my", - "region": "US-East-Pod" + { + "service_name": "identity_user_portal", + "region": "US-East-Pod", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ajp5871.id.integration-cyberark.cloud", + "api": "https://ajp5871.id.integration-cyberark.cloud" + } + ] }, - "userportal": { - "ui": "https://us-east-1.ui.userportal.integration-cyberark.cloud/", - "api": "https://venafi-test.api.userportal.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-userportal.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "userportal", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://us-east-1.ui.userportal.integration-cyberark.cloud/", + "api": "https://venafi-test.api.userportal.integration-cyberark.cloud/api" + } + ] }, - "cloud_onboarding": { - "ui": "https://ui-cloudonboarding.cloudonboarding.integration-cyberark.cloud/", - "api": "https://venafi-test.cloudonboarding.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-cloud_onboarding.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "cloud_onboarding", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui-cloudonboarding.cloudonboarding.integration-cyberark.cloud/", + "api": "https://venafi-test.cloudonboarding.integration-cyberark.cloud/api" + } + ] }, - "identity_administration": { - "ui": "https://ajp5871.id.integration-cyberark.cloud", - "api": "{{ .Identity.API }}", - "bootstrap": "https://venafi-test-identity_administration.integration-cyberark.cloud/admin", - "region": "US-East-Pod" + { + "service_name": "identity_administration", + "region": "US-East-Pod", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ajp5871.id.integration-cyberark.cloud", + "api": "{{ .Identity.API }}" + } + ] }, - "adminportal": { - "ui": "https://ui-adminportal.adminportal.integration-cyberark.cloud", - "api": "https://venafi-test.adminportal.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-adminportal.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "adminportal", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui-adminportal.adminportal.integration-cyberark.cloud", + "api": "https://venafi-test.adminportal.integration-cyberark.cloud/api" + } + ] }, - "analytics": { - "ui": "https://venafi-test.analytics.integration-cyberark.cloud/", - "api": "https://venafi-test.analytics.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-analytics.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "analytics", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://venafi-test.analytics.integration-cyberark.cloud/", + "api": "https://venafi-test.analytics.integration-cyberark.cloud/api" + } + ] }, - "session_monitoring": { - "ui": "https://us-east-1.sm-ui.sessionmonitoring.integration-cyberark.cloud", - "api": "https://venafi-test.sessionmonitoring.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-session_monitoring.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "session_monitoring", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://us-east-1.sm-ui.sessionmonitoring.integration-cyberark.cloud", + "api": "https://venafi-test.sessionmonitoring.integration-cyberark.cloud/api" + } + ] }, - "audit": { - "ui": "https://ui.audit-ui.integration-cyberark.cloud", - "api": "https://venafi-test.audit.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-audit.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "audit", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui.audit-ui.integration-cyberark.cloud", + "api": "https://venafi-test.audit.integration-cyberark.cloud/api" + } + ] }, - "fmcdp": { - "ui": "https://tagtig.io/", - "api": "https://tagtig.io/api", - "bootstrap": "https://venafi-test-fmcdp.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "fmcdp", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://tagtig.io/", + "api": "https://tagtig.io/api" + } + ] }, - "featureadopt": { - "ui": "https://ui-featureadopt.featureadopt.integration-cyberark.cloud/", - "api": "https://us-east-1-featureadopt.featureadopt.integration-cyberark.cloud/api", - "bootstrap": "https://venafi-test-featureadopt.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "featureadopt", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui-featureadopt.featureadopt.integration-cyberark.cloud/", + "api": "https://us-east-1-featureadopt.featureadopt.integration-cyberark.cloud/api" + } + ] }, - "discoverycontext": { - "ui": "https://ui-inventory.inventory.integration-cyberark.cloud/", - "api": "{{ .DiscoveryContext.API }}", - "bootstrap": "https://venafi-test-discoverycontext.integration-cyberark.cloud", - "region": "us-east-1" + { + "service_name": "discoverycontext", + "region": "us-east-1", + "endpoints": [ + { + "is_active": true, + "type": "main", + "ui": "https://ui-inventory.inventory.integration-cyberark.cloud/", + "api": "{{ .DiscoveryContext.API }}" + } + ] } + ] }