@@ -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.
106125type 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
0 commit comments