Skip to content

Commit 173ba28

Browse files
Merge pull request #820 from roeezis/split/03-servicediscovery-secretsmanager
fix(agent): resolve secrets_manager alongside identity_administration
2 parents 896ad95 + ee1907b commit 173ba28

5 files changed

Lines changed: 57 additions & 19 deletions

File tree

internal/cyberark/servicediscovery/discovery.go

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ const (
2828
// in responses from the Service Discovery API.
2929
DiscoveryContextServiceName = "discoverycontext"
3030

31+
// SecretsManagerServiceName is the name of the Secrets Manager (Conjur
32+
// Cloud) API in responses from the Service Discovery API. This is the host
33+
// that serves `authn-jwt/<service-id>/<account>/authenticate` — NOT the
34+
// identity_administration host. The server that validates the resulting
35+
// token resolves this same service name.
36+
SecretsManagerServiceName = "secrets_manager"
37+
3138
// maxDiscoverBodySize is the maximum allowed size for a response body from the CyberArk Service Discovery subdomain endpoint
3239
// As of 2025-04-16, a response from the integration environment is ~4kB
3340
maxDiscoverBodySize = 2 * 1024 * 1024
@@ -47,6 +54,17 @@ type Client struct {
4754
cachedResponseMutex sync.Mutex
4855
}
4956

57+
// mainActiveAPI returns the API URL of the first active "main" endpoint in
58+
// eps, or "" if there isn't one.
59+
func mainActiveAPI(eps []ServiceEndpoint) string {
60+
for _, ep := range eps {
61+
if ep.Type == "main" && ep.IsActive && ep.API != "" {
62+
return ep.API
63+
}
64+
}
65+
return ""
66+
}
67+
5068
// New creates a new CyberArk Service Discovery client. If the ARK_DISCOVERY_API
5169
// environment variable is set, it is used as the base URL for the service
5270
// discovery API. Otherwise, the production URL is used.
@@ -101,11 +119,13 @@ type ServiceEndpoint struct {
101119
API string `json:"api"`
102120
}
103121

104-
// This is a convenience struct to hold the two ServiceEndpoints we care about.
105-
// Currently, we only care about the Identity API and the Discovery Context API.
122+
// This is a convenience struct to hold the ServiceEndpoints we care about:
123+
// the Identity API, the Discovery Context API, and the Secrets Manager
124+
// (Conjur Cloud) API used for the authn-jwt token exchange.
106125
type Services struct {
107126
Identity ServiceEndpoint
108127
DiscoveryContext ServiceEndpoint
128+
SecretsManager ServiceEndpoint
109129
}
110130

111131
// DiscoverServices fetches from the service discovery service for the configured subdomain
@@ -163,35 +183,41 @@ func (c *Client) DiscoverServices(ctx context.Context) (*Services, string, error
163183
}
164184
return nil, "", fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err)
165185
}
166-
var identityAPI, discoveryContextAPI string
186+
var identityAPI, discoveryContextAPI, secretsManagerAPI string
167187
for _, svc := range discoveryResp.Services {
168188
switch svc.ServiceName {
169189
case IdentityServiceName:
170-
for _, ep := range svc.Endpoints {
171-
if ep.Type == "main" && ep.IsActive && ep.API != "" {
172-
identityAPI = ep.API
173-
break
174-
}
175-
}
190+
identityAPI = mainActiveAPI(svc.Endpoints)
176191
case DiscoveryContextServiceName:
177-
for _, ep := range svc.Endpoints {
178-
if ep.Type == "main" && ep.IsActive && ep.API != "" {
179-
discoveryContextAPI = ep.API
180-
break
181-
}
182-
}
192+
discoveryContextAPI = mainActiveAPI(svc.Endpoints)
193+
case SecretsManagerServiceName:
194+
secretsManagerAPI = mainActiveAPI(svc.Endpoints)
183195
}
184196
}
185197

198+
// identityAPI is required unconditionally, unlike discoveryContextAPI and
199+
// secretsManagerAPI below: it's present and active for every healthy
200+
// tenant, so callers may rely on it being non-empty without checking it
201+
// themselves again.
186202
if identityAPI == "" {
187203
return nil, "", fmt.Errorf("didn't find %s in service discovery response, "+
188204
"which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName)
189205
}
190-
//TODO: Should add a check for discoveryContextAPI too?
206+
// discoveryContextAPI and secretsManagerAPI are deliberately not required
207+
// here, unlike identityAPI above: not every caller needs both, and
208+
// requiring secretsManagerAPI would break every existing
209+
// username/password install on a tenant not yet onboarded to Conjur.
210+
// Each caller that needs one validates it itself — e.g.
211+
// cyberark.NewDatauploadClient rejects an empty discoveryContextAPI, and
212+
// cyberark.selectAuthenticator rejects an empty secretsManagerAPI on the
213+
// Conjur JWT path. Not every caller does this yet (keyfetch's client
214+
// doesn't check discoveryContextAPI), so an empty value can still surface
215+
// downstream as a less obvious error.
191216

192217
services := &Services{
193218
Identity: ServiceEndpoint{API: identityAPI},
194219
DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI},
220+
SecretsManager: ServiceEndpoint{API: secretsManagerAPI},
195221
}
196222

197223
c.cachedResponse = services

internal/cyberark/servicediscovery/discovery_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
6262
DiscoveryContext: ServiceEndpoint{
6363
API: mockDiscoveryContextAPIURL,
6464
},
65+
SecretsManager: ServiceEndpoint{
66+
API: mockSecretsManagerAPIURL,
67+
},
6568
})
6669

6770
client := New(httpClient, testSpec.subdomain)
@@ -76,6 +79,13 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
7679
if services.Identity.API != testSpec.expectedURL {
7780
t.Errorf("expected API URL=%s\nobserved API URL=%s", testSpec.expectedURL, services.Identity.API)
7881
}
82+
// The Conjur authn-jwt exchange is served by secrets_manager, not
83+
// by identity_administration. Parsing it into the wrong field means
84+
// every live token exchange 404s/401s, which the Conjur unit tests
85+
// cannot catch because they point their mock at whichever field the
86+
// code reads.
87+
assert.Equal(t, mockSecretsManagerAPIURL, services.SecretsManager.API)
88+
assert.NotEqual(t, services.Identity.API, services.SecretsManager.API)
7989
})
8090
}
8191
}

internal/cyberark/servicediscovery/mock.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525

2626
mockIdentityAPIURL = "https://ajp5871.id.integration-cyberark.cloud"
2727
mockDiscoveryContextAPIURL = "https://venafi-test.inventory.integration-cyberark.cloud/"
28+
mockSecretsManagerAPIURL = "https://venafi-test.secretsmgr.integration-cyberark.cloud/api"
2829
prefix = "/api/public/tenant-discovery?bySubdomain="
2930
)
3031

internal/cyberark/servicediscovery/testdata/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ NOTE: This API is not implemented yet as of 02.09.2025 but is expected to be fin
99
curl -fsSL "${ARK_DISCOVERY_API}?bySubdomain=${ARK_SUBDOMAIN}" | jq
1010
```
1111

12-
Then replace `identity_administration.api` with `{{ .Identity.API }}` and
13-
`discoverycontext.api` with `{{ .DiscoveryContext.API }}`. Those Go template
12+
Then replace `identity_administration.api` with `{{ .Identity.API }}`,
13+
`discoverycontext.api` with `{{ .DiscoveryContext.API }}`, and
14+
`secrets_manager.api` with `{{ .SecretsManager.API }}`. Those Go template
1415
fields will be substituted in the tests.

internal/cyberark/servicediscovery/testdata/discovery_success.json.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"is_active": true,
3232
"type": "main",
3333
"ui": "https://ui.test-conjur.cloud",
34-
"api": "https://venafi-test.secretsmgr.integration-cyberark.cloud/api"
34+
"api": "{{ .SecretsManager.API }}"
3535
}
3636
]
3737
},

0 commit comments

Comments
 (0)