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
5 changes: 4 additions & 1 deletion internal/cyberark/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
discoveryContextAPI, _ := dataupload.MockDataUploadServer(t)

// Unused by the Conjur path, but service discovery requires it to be set.
const identitySrv = "https://identity.example.invalid"
// Never dialed, so a loopback address is fine — see pkg/testutil/envtest.go's
// identical const for why this is preferred over a real, resolvable
// CyberArk zone name.
const identitySrv = "https://127.0.0.1:1"

httpClient := servicediscovery.MockDiscoveryServer(t, servicediscovery.Services{
Identity: servicediscovery.ServiceEndpoint{
Expand Down
17 changes: 11 additions & 6 deletions internal/cyberark/conjur/conjur.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,21 @@ func (c *Client) exchange(ctx context.Context) (string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// Conjur returns a JSON error body with the actual reason; include a
// bounded prefix so the operator doesn't have to go read Conjur's own
// audit log to find out why. 401 here most often means the SA token
// audience != authenticator audience=conjur.
// Conjur returns a JSON error body with the actual reason. It can
// contain policy structure, service IDs and host identities, and this
// error is surfaced all the way up to a Kubernetes Pod Event
// (pkg/agent/run.go's PushingErr notification), which anyone with
// `get events` in the namespace can read — so the body is logged at
// V(2) for an operator to go find, not embedded in the returned
// error. 401 here most often means the SA token audience != the
// authenticator's audience=conjur.
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4*1024))
// Drain the rest so the connection can be reused, bounded so a
// misbehaving server can't make this read unboundedly.
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1024*1024))
return "", fmt.Errorf("authn-jwt exchange rejected (%d): %s; verify service_id, the authenticator is enabled, and the SA token audience is 'conjur'",
resp.StatusCode, strings.TrimSpace(string(errBody)))
klog.FromContext(ctx).V(2).Info("authn-jwt exchange rejected", "statusCode", resp.StatusCode, "body", strings.TrimSpace(string(errBody)))
return "", fmt.Errorf("authn-jwt exchange rejected (%d); verify service_id, the authenticator is enabled, and the SA token audience is 'conjur' (run with -v=2 to see Conjur's response body)",
resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
Expand Down
12 changes: 10 additions & 2 deletions internal/cyberark/conjur/conjur_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,21 @@ func TestInvalidate_ForcesReexchange(t *testing.T) {
require.Equal(t, 2, exchanges, "Invalidate must force the next AuthenticateRequest call to re-exchange")
}

func TestAuthenticateRequest_ExchangeErrorIncludesConjurResponseBody(t *testing.T) {
// This error propagates all the way up to a Kubernetes Pod Event
// (pkg/agent/run.go's PushingErr notification), readable by anyone with `get
// events` in the namespace — so Conjur's response body (which can contain
// policy structure, service IDs and host identities) must not appear in it.
// The body is still logged at V(2) for an operator to go find, but that's
// exercised via the "authn-jwt exchange rejected" log line, not asserted
// here — ktesting has no easy log-buffer assertion in this codebase.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dispute this last clause — the codebase already has the helper.

pkg/agent/config_test.go:1040:

func recordLogs(t *testing.T) (logr.Logger, ktesting.Buffer) {
	log := ktesting.NewLogger(t, ktesting.NewConfig(ktesting.BufferLogs(true)))
	testingLogger, ok := log.GetSink().(ktesting.Underlier)
	require.True(t, ok)
	return log, testingLogger.GetBuffer()
}

Six tests in that file already assert against the buffer with gotLogs.String(). It is three lines, not a gap in the tooling.

I wrote the assertion for this test to check I was not talking nonsense, and it passes:

logger := ktesting.NewLogger(t, ktesting.NewConfig(ktesting.BufferLogs(true), ktesting.Verbosity(2)))
buf := logger.GetSink().(ktesting.Underlier).GetBuffer()

ctx := klog.NewContext(t.Context(), logger)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com/x", nil)
_, err := c.AuthenticateRequest(req)
require.Error(t, err)

require.NotContains(t, err.Error(), "CONJ00001E Invalid JWT token")
require.Contains(t, buf.String(), "authn-jwt exchange rejected")
require.Contains(t, buf.String(), "CONJ00001E Invalid JWT token")
--- PASS: TestScratch_BodyIsLoggedAtV2 (0.01s)
    captured buffer:
    INFO authn-jwt exchange rejected statusCode=401 body="{\"error\":{\"message\":\"CONJ00001E Invalid JWT token\"}}"

Two things that are genuinely non-obvious, and are probably what you hit:

  • conjur.go:100 logs via klog.FromContext(ctx), so the logger has to reach it through the request context — klog.NewContext(t.Context(), logger). Passing it any other way silently captures nothing and the buffer just comes back empty.
  • You need ktesting.Verbosity(2), otherwise the V(2) line is filtered out and, again, you get an empty buffer rather than a failure that tells you why.

Worth doing, because it closes the actual loop: right now the test proves the body is absent from the error, but nothing proves it is still present in the log. Someone deleting the klog line to "fix" a leak would keep this test green and quietly remove the operator's only way to see Conjur's response.

If you would rather not, that is fine — but then please reword rather than leave the claim, since the next person will believe it:

Suggested change
// here — ktesting has no easy log-buffer assertion in this codebase.
// here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified — wrong. pkg/agent/config_test.go:1040's recordLogs helper does exactly this, six tests already use it. Added the same pattern here: TestAuthenticateRequest_ExchangeErrorOmitsConjurResponseBody now asserts the body is still present in the V(2) log line via ktesting.BufferLogs, not just absent from the returned error. Reworded the comment to state that rather than the false claim.

func TestAuthenticateRequest_ExchangeErrorOmitsConjurResponseBody(t *testing.T) {
srv, httpClient := MockConjurExchangeServerStatusBody(t, http.StatusUnauthorized, []byte(`{"error":{"message":"CONJ00001E Invalid JWT token"}}`))
defer srv.Close()

c := New(httpClient, srv.URL, "dev-cluster", "conjur", staticSource{tok: "the-jwt"})
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://example.com/x", nil)
_, err := c.AuthenticateRequest(req)
require.Error(t, err)
require.Contains(t, err.Error(), "CONJ00001E Invalid JWT token")
require.NotContains(t, err.Error(), "CONJ00001E Invalid JWT token")
require.Contains(t, err.Error(), "authn-jwt exchange rejected (401)")
}
119 changes: 117 additions & 2 deletions internal/cyberark/servicediscovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import (
"net/url"
"os"
"path"
"strings"
"sync"
"time"

"k8s.io/klog/v2"

arkapi "github.com/jetstack/preflight/internal/cyberark/api"
"github.com/jetstack/preflight/pkg/version"
)
Expand Down Expand Up @@ -40,6 +43,101 @@ const (
maxDiscoverBodySize = 2 * 1024 * 1024
)

// allowedRootDomains are the only root domains a discovery response is
// allowed to point us at for identity/discoverycontext/secrets_manager.
// Without this, mainActiveAPI's ep.API is trusted verbatim from the response
// body and handed straight to the Conjur/Identity clients, which then POST
// the agent's SA token (or username/password) to it — an SSRF-shaped hole if
// the response is ever tampered with. Mirrors the per-env ROOT_DOMAIN
// allowlist already enforced on the discoverycontext-regional-resources side
// (token.py, for the JWT `iss` host) — copied by value here since these
// domains rarely change and the agent has no access to that env-keyed map.
//
// Source of truth is the `everest_env_utils` package's ROOT_DOMAIN map
// (published to Artifactory as everest_env_utils_cyberark, v2.0.117 as of
// 2026-09-03), not the Lambda's local commercial-only clone — that clone
// omits the GOV_* environments entirely, which would have made this
// allowlist silently break every gov-cloud tenant's agent.
var allowedRootDomains = []string{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Which attacker does this stop? I can only make it bind against one, and it is not the one the description implies.

The discovery call is already HTTPS against system roots, so work through who can actually change what comes back:

  1. Network attacker with no trusted certificate. They cannot alter the discovery response at all — TLS stops them. The allowlist has nothing to defend.
  2. Network attacker who can pass TLS for the discovery host (misissued certificate, an extra CA in the pod trust store, an intercepting proxy). They tamper with the response — and they can equally intercept the connection to whichever host the allowlist permits. They name a genuine *.cyberark.cloud host and intercept that too. The allowlist buys nothing.
  3. The discovery service itself returns a host it should not — compromised, or buggy, or reflecting input someone else influenced. TLS is intact everywhere; the only lever is the response body. Here, and only here, the allowlist binds.

So this is defence-in-depth against our own service misbehaving. That is a legitimate thing to want, and worth keeping. But it is not a fix for a broken trust chain at the network layer, and the description reads as though it is.

Two consequences worth being explicit about:

Requiring HTTPS does not substitute for the allowlist. Anyone can get a publicly trusted certificate for a domain they own, so in case 3 https://evil.example/ is accepted by TLS and the agent POSTs its SA token there. The allowlist is the only thing standing in the way. Conversely, in case 2, HTTPS does not help either. The two controls are not layers on the same threat — they address different ones, and neither covers case 2.

In case 3, cyberark.cloud admits every tenant's host. A compromised discovery service does not need an outside domain; it names another tenant's id or secretsmgr host. That makes the tenant-scoping check the load-bearing control and the root-domain list the weaker one — which is the opposite of the emphasis here. I appreciate why you shipped it warn-only, and I am not arguing with that call.

Separately, on the loopback shape the comment below cites: an attacker who can set ARK_DISCOVERY_API on the agent already controls the pod's environment, and the token is /var/run/secrets/tokens/jwt in that same pod (jwtsource.DefaultTokenPath). They can read the file; they have no need to make the agent post it to them. Requiring HTTPS is still worth having — it stops a plain-HTTP dev configuration reaching production — but it should be described as protecting against misconfiguration, not against that attacker.

None of this needs a code change. What I would like is for the description and this comment to say "constrains a misbehaving discovery service" rather than implying it closes a network-level gap, so that nobody later reads the allowlist as a reason not to do the pinning work.

@roeezis roeezis Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed with the threat model — walked through the same three cases and land in the same place: TLS already stops case 1, case 2 gains nothing from this check (an attacker who can defeat the discovery host's TLS can equally intercept whichever host the allowlist permits), and case 3 (our own discovery service misbehaving) is the only place it binds. Reworded the doc comment to say exactly that rather than "SSRF-shaped hole"/"tampered with", and made the tenant-scoping gap explicit there too (cyberark.cloud admits every tenant's host; hostLeadingLabelMatchesSubdomain is warn-only, not a fix for it). Also corrected the "loopback-attacker" framing in both PR descriptions to "loopback-misconfiguration" — same point applies to ARK_DISCOVERY_API: someone who can set it already has the pod's token file directly.

"cyberark.cloud",
"cyberark-everest-dev.com",
"cyberark-everest-test.com",
"cyberark-everest-stage.com",
"sandbox-cyberark.cloud",
"integration-cyberark.cloud",
"pt-cyberark.cloud",
"cyberark-everest-integdev.cloud",
"cyberark-everest-preinteg.cloud",
"cyberark-everest-perf.cloud",
"cyberark-everest-pre-prod.cloud",
// Gov-cloud environments.
"dev-cyberarkgov.com",
"test-cyberarkgov.com",
"stage-cyberarkgov.com",
"integdev-cyberarkgov.cloud",
"integration-cyberarkgov.cloud",
"preprod-cyberarkgov.cloud",
"dum-preprod-cyberarkgov.cloud",
"cyberarkgov.cloud",
}

// isAllowedServiceHost reports whether host is, or is a subdomain of, one of
// allowedRootDomains — or is exactly discoveryHost, the host the discovery
// request was addressed to. The latter matters for ARK_DISCOVERY_API-
// overridden (dev/CI/test) discovery endpoints: whatever host that override
// already points at is at least as trusted as the discovery call itself.
//
// This comparison is hostname-only: discoveryHost carries no port, and
// ARK_DISCOVERY_API is not guaranteed to be HTTPS or to be the host the
// request actually landed on after redirects — the follow-up in CP-26002
// (validating ARK_DISCOVERY_API itself against allowedRootDomains, removing
// this whole escape hatch) is the actual fix for both gaps; a host:port
// comparison here alone would break every test that currently relies on
// same-host-different-port mocks without that same rework.
func isAllowedServiceHost(host, discoveryHost string) bool {
// DNS is case-insensitive and net/url doesn't normalise host case (it
// lowercases the scheme but not the host), so a discovery response with
// any uppercase in a legitimate hostname must still match here.
if strings.EqualFold(host, discoveryHost) {
return true
}
host = strings.ToLower(host)
for _, root := range allowedRootDomains {
if host == root || strings.HasSuffix(host, "."+root) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The host comparison is case-sensitive, but net/url does not normalise host case.

url.Parse lowercases the scheme but leaves the host exactly as written — confirmed:

url.Parse("HTTPS://Tenant.CyberArk.Cloud/api") -> scheme="https" hostname="Tenant.CyberArk.Cloud"

So a discovery response carrying any uppercase in the hostname fails both host == root and strings.HasSuffix(host, "."+root) and is dropped. DNS is case-insensitive, so such a response is entirely legitimate. For identity_administration the result is a hard startup failure with the misleading "suspended tenant" error.

It fails closed rather than open, so this is availability rather than a bypass, but it is a one-line fix.

Suggested change
if host == root || strings.HasSuffix(host, "."+root) {
for _, root := range allowedRootDomains {
if strings.EqualFold(host, root) || strings.HasSuffix(strings.ToLower(host), "."+root) {
return true
}
}

(the same normalisation should be applied to the host == discoveryHost comparison above)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed: url.Parse lowercases the scheme but not the host. Fixed in 6fea542 — strings.EqualFold for the host==discoveryHost case, lowercased comparison against allowedRootDomains for the rest. Added a test with a mixed-case identity host.

return true
}
}
return false
}

// sanitizeServiceAPI returns rawAPI unchanged if its scheme is https and its
// host is allowed, or "" (treated the same as "service not present in the
// response") if not.
func sanitizeServiceAPI(ctx context.Context, serviceName, rawAPI, discoveryHost string) string {
if rawAPI == "" {
return ""
}
u, err := url.Parse(rawAPI)
if err != nil || u.Hostname() == "" {
klog.FromContext(ctx).Info("dropping unparseable service discovery API URL", "service", serviceName, "api", rawAPI)
return ""
}
if u.Scheme != "https" {
// Rejecting plain HTTP also closes the loopback-attacker shape of
// the isAllowedServiceHost "same host as discoveryHost" case: a
// same-host rogue endpoint (e.g. an attacker-controlled
// ARK_DISCOVERY_API pointing at 127.0.0.1) can't present a
// certificate this client's TLS verification will accept.
klog.FromContext(ctx).Info("dropping non-HTTPS service discovery API URL", "service", serviceName, "scheme", u.Scheme)
return ""
}
if !isAllowedServiceHost(u.Hostname(), discoveryHost) {
klog.FromContext(ctx).Info("dropping service discovery API URL outside the allowed CyberArk domains", "service", serviceName, "host", u.Hostname())
return ""
}
return rawAPI
}

// Client is a Golang client for interacting with the CyberArk Discovery Service. It allows
// users to fetch URLs for various APIs available in CyberArk. This client is specialised to
// fetch only API endpoints, since only API endpoints are required by the Venafi Kubernetes Agent currently.
Expand Down Expand Up @@ -195,13 +293,30 @@ func (c *Client) DiscoverServices(ctx context.Context) (*Services, string, error
}
}

// Drop any of the three API URLs whose host isn't one of the CyberArk
// domains we actually trust, before anything downstream authenticates
// against it. A dropped URL is treated exactly like one absent from the
// response — see the required/optional distinction below.
rawIdentityAPI := identityAPI
identityAPI = sanitizeServiceAPI(ctx, IdentityServiceName, identityAPI, u.Hostname())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A rejected identity_administration host is fatal, and the error the operator sees says the wrong thing.

When sanitizeServiceAPI drops the identity URL, identityAPI becomes "" and the check below returns didn't find identity_administration in service discovery response, which may indicate a suspended tenant. The tenant is fine; the agent simply does not recognise the root domain.

This matters because the allowlist is a hardcoded copy of a CyberArk-internal ROOT_DOMAIN map baked into a customer-deployed binary. The moment a new environment root domain is added, or a tenant's identity API moves, every agent in that environment fails at startup and the only accurate signal is a separate Info line. The response already demonstrates that CyberArk emits service hosts outside these roots — testdata/discovery_success.json.template has idaptive_risk_analytics on *.analytics.idaptive.qa and fmcdp on tagtig.io.

At minimum, distinguish the two cases so the error names the real cause. Consider also whether dropping identity should be fatal at all, or whether an escape hatch (an extra-allowed-domain env var) is warranted given the agent cannot be updated on CyberArk's release cadence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6fea542. Split the fatal-identity error into two cases: absent from the response (kept the existing "suspended tenant" message) vs. present but rejected by the allowlist (new message naming the rejected host explicitly). Confirmed the testdata fixture does carry out-of-root-domain services today (idaptive.qa, tagtig.io) — didn't spot that when I wrote the allowlist. Not adding an escape-hatch env var for extra allowed domains without discussing it first; that's a real widening of the trust boundary, not a mechanical fix.

discoveryContextAPI = sanitizeServiceAPI(ctx, DiscoveryContextServiceName, discoveryContextAPI, u.Hostname())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping discoverycontext makes an already-poor downstream failure newly reachable.

The comment below correctly notes that keyfetch does not check DiscoveryContext.API. Previously an empty value only happened for an absent or inactive service; now a live-but-unrecognised host produces the same empty string. The resulting failure in keyfetch.FetchKey is opaque — verified:

url.JoinPath("", "discovery-context/jwks") -> "discovery-context/jwks", err=nil
http.NewRequest(...)                        -> err=nil
client.Do(...)                              -> Get "discovery-context/jwks": unsupported protocol scheme ""

An operator sees unsupported protocol scheme "" with nothing tying it back to a dropped discovery host. Adding the missing empty check in keyfetch.FetchKey (matching NewDatauploadClient's) would make the new failure mode self-explanatory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed the failure mode (unsupported protocol scheme ""). Fixed in 6fea542 -- FetchKey now checks services.DiscoveryContext.API == "" explicitly, mirroring NewDatauploadClient's existing check on the same field, with a new test covering it.

secretsManagerAPI = sanitizeServiceAPI(ctx, SecretsManagerServiceName, secretsManagerAPI, u.Hostname())

// identityAPI is required unconditionally, unlike discoveryContextAPI and
// secretsManagerAPI below: it's present and active for every healthy
// tenant, so callers may rely on it being non-empty without checking it
// themselves again.
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)
if rawIdentityAPI == "" {
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)
}
// The response did name an identity_administration endpoint, but its
// host isn't on our allowlist — a distinct, more actionable failure
// than "suspended tenant" (see sanitizeServiceAPI's Info log for
// which host was rejected and why).
return nil, "", fmt.Errorf("%s endpoint %q is not on an allowed CyberArk domain over HTTPS; refusing to use it",
IdentityServiceName, rawIdentityAPI)
}
// discoveryContextAPI and secretsManagerAPI are deliberately not required
// here, unlike identityAPI above: not every caller needs both, and
Expand Down
143 changes: 143 additions & 0 deletions internal/cyberark/servicediscovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,149 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
},
}

t.Run("identity API host outside the allowed CyberArk domains is rejected", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)

httpClient := MockDiscoveryServer(t, Services{
Identity: ServiceEndpoint{
API: "https://ajp5871.id.attacker.example",
},
DiscoveryContext: ServiceEndpoint{
API: mockDiscoveryContextAPIURL,
},
SecretsManager: ServiceEndpoint{
API: mockSecretsManagerAPIURL,
},
})

client := New(httpClient, MockDiscoverySubdomain)
services, _, err := client.DiscoverServices(ctx)
require.Error(t, err)
assert.Nil(t, services)
// The error must say the host was rejected, not the unrelated
// "suspended tenant" message reserved for a genuinely absent
// identity_administration entry (see the no-identity-in-response
// case in the tests map above, which still gets that message).
assert.Contains(t, err.Error(), "not on an allowed CyberArk domain")
})

t.Run("secrets_manager and discovery_context hosts outside the allowed CyberArk domains are dropped, not fatal", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)

httpClient := MockDiscoveryServer(t, Services{
Identity: ServiceEndpoint{
API: mockIdentityAPIURL,
},
DiscoveryContext: ServiceEndpoint{
API: "https://venafi-test.inventory.attacker.example",
},
SecretsManager: ServiceEndpoint{
API: "https://venafi-test.secretsmgr.attacker.example",
},
})

client := New(httpClient, MockDiscoverySubdomain)
services, _, err := client.DiscoverServices(ctx)
require.NoError(t, err)
assert.Equal(t, mockIdentityAPIURL, services.Identity.API)
assert.Equal(t, "", services.DiscoveryContext.API)
assert.Equal(t, "", services.SecretsManager.API)
})

t.Run("plain-HTTP identity host is rejected even though the hostname is allowlisted", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)

httpClient := MockDiscoveryServer(t, Services{
Identity: ServiceEndpoint{
API: "http://ajp5871.id.integration-cyberark.cloud",
},
DiscoveryContext: ServiceEndpoint{
API: mockDiscoveryContextAPIURL,
},
SecretsManager: ServiceEndpoint{
API: mockSecretsManagerAPIURL,
},
})

client := New(httpClient, MockDiscoverySubdomain)
services, _, err := client.DiscoverServices(ctx)
require.Error(t, err)
assert.Nil(t, services)
})

t.Run("plain-HTTP secrets_manager and discovery_context hosts are dropped, not fatal", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)

httpClient := MockDiscoveryServer(t, Services{
Identity: ServiceEndpoint{
API: mockIdentityAPIURL,
},
DiscoveryContext: ServiceEndpoint{
API: "http://venafi-test.inventory.integration-cyberark.cloud",
},
SecretsManager: ServiceEndpoint{
API: "http://venafi-test.secretsmgr.integration-cyberark.cloud",
},
})

client := New(httpClient, MockDiscoverySubdomain)
services, _, err := client.DiscoverServices(ctx)
require.NoError(t, err)
assert.Equal(t, mockIdentityAPIURL, services.Identity.API)
assert.Equal(t, "", services.DiscoveryContext.API)
assert.Equal(t, "", services.SecretsManager.API)
})

t.Run("gov-cloud root domains are accepted", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)

httpClient := MockDiscoveryServer(t, Services{
Identity: ServiceEndpoint{
API: "https://ajp5871.id.cyberarkgov.cloud",
},
DiscoveryContext: ServiceEndpoint{
API: "https://venafi-test.inventory.integration-cyberarkgov.cloud",
},
SecretsManager: ServiceEndpoint{
API: "https://venafi-test.secretsmgr.dev-cyberarkgov.com",
},
})

client := New(httpClient, MockDiscoverySubdomain)
services, _, err := client.DiscoverServices(ctx)
require.NoError(t, err)
assert.Equal(t, "https://ajp5871.id.cyberarkgov.cloud", services.Identity.API)
assert.Equal(t, "https://venafi-test.inventory.integration-cyberarkgov.cloud", services.DiscoveryContext.API)
assert.Equal(t, "https://venafi-test.secretsmgr.dev-cyberarkgov.com", services.SecretsManager.API)
})

t.Run("uppercase host is still accepted (net/url doesn't lowercase the host)", func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
ctx := klog.NewContext(t.Context(), logger)

httpClient := MockDiscoveryServer(t, Services{
Identity: ServiceEndpoint{
API: "https://AJP5871.ID.Integration-CyberArk.Cloud",
},
DiscoveryContext: ServiceEndpoint{
API: mockDiscoveryContextAPIURL,
},
SecretsManager: ServiceEndpoint{
API: mockSecretsManagerAPIURL,
},
})

client := New(httpClient, MockDiscoverySubdomain)
services, _, err := client.DiscoverServices(ctx)
require.NoError(t, err)
assert.Equal(t, "https://AJP5871.ID.Integration-CyberArk.Cloud", services.Identity.API)
})

for name, testSpec := range tests {
t.Run(name, func(t *testing.T) {
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
Expand Down
Loading
Loading