Skip to content
Merged
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 .envrc.template
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 1 addition & 1 deletion pkg/internal/cyberark/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
Expand Down
2 changes: 1 addition & 1 deletion pkg/internal/cyberark/identity/cmd/testidentity/main.go
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this and it worked well:

$ go run pkg/internal/cyberark/identity/cmd/testidentity/main.go -subdomain $ARK_SUBDOMAIN -username $ARK_USERNAME
I0903 16:45:13.361918 1414535 round_trippers.go:632] "Response" verb="GET" url="https://platform-discovery.integration-cyberark.cloud/api/public/tenant-discovery?bySubdomain=tlskp-test" status="200 OK" milliseconds=354
I0903 16:45:13.905341 1414535 round_trippers.go:632] "Response" verb="POST" url="https://anb5751.id.integration-cyberark.cloud/Security/StartAuthentication" status="200 OK" milliseconds=536
I0903 16:45:13.906041 1414535 identity.go:303] "made successful request to StartAuthentication" source="Identity.doStartAuthentication" summary="NewPackage"
I0903 16:45:14.772488 1414535 round_trippers.go:632] "Response" verb="POST" url="https://anb5751.id.integration-cyberark.cloud/Security/AdvanceAuthentication" status="200 OK" milliseconds=866
I0903 16:45:14.773137 1414535 identity.go:419] "successfully completed AdvanceAuthentication request to CyberArk Identity; login complete" username="<REDACTED>"

Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
86 changes: 66 additions & 20 deletions pkg/internal/cyberark/servicediscovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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 != "" {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not sure if I should check the Type and IsActive but I've added them. I don't understand the difference between Type main and crdr.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either. Let's merge as-is and adapt it if we learn more from the API team.

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?
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We check for identityAPI but not for discoveryContextAPI. Was this intentional?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about adding it, but didn't know what actionable message to return if it's not found.
Thanks for adding the TODO, lets address that in a future PR.


return &services, nil
return &Services{
Identity: ServiceEndpoint{API: identityAPI},
DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI},
}, nil
}
26 changes: 21 additions & 5 deletions pkg/internal/cyberark/servicediscovery/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -97,15 +98,30 @@ 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:
_, _ = w.Write([]byte(mds.successResponse))

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
Expand Down
3 changes: 2 additions & 1 deletion pkg/internal/cyberark/servicediscovery/testdata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading