@@ -208,6 +208,36 @@ class PasswordGrantAuthorizer(Authorizer):
208208 """
209209
210210 TOKEN_PATH_URI = "/oauth2/token"
211+ PLATFORM_TOKEN_PATH_URI = "/identity/api/oauth2/token/xpmplatform"
212+
213+ def _detect_server_type (self ):
214+ """Detects if the server is Secret Server or Platform by health check endpoints, using _check_json_response."""
215+ ss_health_url = self .base_url .rstrip ("/" ) + "/api/v1/healthcheck"
216+ platform_health_url = self .base_url .rstrip ("/" ) + "/health"
217+ if self ._check_json_response (ss_health_url ):
218+ self ._server_type = "secret_server"
219+ return
220+ if self ._check_json_response (platform_health_url ):
221+ self ._server_type = "platform"
222+ return
223+ raise SecretServerError ("Unable to detect server type via health check endpoints." )
224+
225+ def _check_json_response (self , url ):
226+ """Python equivalent of Go checkJSONResponse for health check detection."""
227+ try :
228+ resp = requests .get (url , timeout = 60 )
229+ except Exception :
230+ return False
231+ try :
232+ body = resp .content
233+ except Exception :
234+ return False
235+ try :
236+ data = resp .json ()
237+ healthy = data .get ("Healthy" , False )
238+ return healthy
239+ except Exception :
240+ return b"Healthy" in body or b"healthy" in body
211241
212242 @staticmethod
213243 def get_access_grant (token_url , grant_request ):
@@ -241,18 +271,49 @@ def _refresh(self, seconds_of_drift=300):
241271 ):
242272 return
243273 else :
244- self .access_grant = self .get_access_grant (
245- self .token_url , self .grant_request
246- )
247- self .access_grant_refreshed = datetime .now ()
248-
249- def __init__ (self , base_url , username , password , token_path_uri = TOKEN_PATH_URI ):
250- self .token_url = base_url .rstrip ("/" ) + "/" + token_path_uri .strip ("/" )
251- self .grant_request = {
252- "username" : username ,
253- "password" : password ,
254- "grant_type" : "password" ,
255- }
274+ # Detect server type if not already done
275+ if not hasattr (self , "_server_type" ):
276+ self ._detect_server_type ()
277+ # Decide token_path_uri if not provided
278+ if not self .token_path_uri :
279+ if self ._server_type == "secret_server" :
280+ self .token_path_uri = self .TOKEN_PATH_URI
281+ elif self ._server_type == "platform" :
282+ self .token_path_uri = self .PLATFORM_TOKEN_PATH_URI
283+ else :
284+ raise SecretServerError ("Unknown server type for token request." )
285+ if self ._server_type == "secret_server" :
286+ token_url = self .base_url .rstrip ("/" ) + "/" + self .token_path_uri .strip ("/" )
287+ grant_request = {
288+ "username" : self .username ,
289+ "password" : self .password ,
290+ "grant_type" : "password" ,
291+ }
292+ if hasattr (self , "domain" ) and self .domain :
293+ grant_request ["domain" ] = self .domain
294+ self .access_grant = self .get_access_grant (token_url , grant_request )
295+ self .access_grant_refreshed = datetime .now ()
296+ elif self ._server_type == "platform" :
297+ token_url = self .base_url .rstrip ("/" ) + "/" + self .token_path_uri .strip ("/" )
298+ grant_request = {
299+ "client_id" : self .username ,
300+ "client_secret" : self .password ,
301+ "grant_type" : "client_credentials" ,
302+ "scope" : "xpmheadless" ,
303+ }
304+ self .access_grant = self .get_access_grant (token_url , grant_request )
305+ self .access_grant_refreshed = datetime .now ()
306+ else :
307+ raise SecretServerError ("Unknown server type for token request." )
308+
309+ def __init__ (self , base_url , username , password , token_path_uri = None , domain = None ):
310+ self .base_url = base_url .rstrip ("/" )
311+ self .username = username
312+ self .password = password
313+ self .domain = domain
314+ self .token_path_uri = token_path_uri # May be None, will decide in _refresh
315+ self .token_url = None
316+ self .grant_request = None
256317
257318 def get_access_token (self ):
258319 self ._refresh ()
@@ -268,10 +329,9 @@ def __init__(
268329 username ,
269330 domain ,
270331 password ,
271- token_path_uri = PasswordGrantAuthorizer . TOKEN_PATH_URI ,
332+ token_path_uri = None ,
272333 ):
273- super ().__init__ (base_url , username , password , token_path_uri = token_path_uri )
274- self .grant_request ["domain" ] = domain
334+ super ().__init__ (base_url , username , password , token_path_uri = token_path_uri , domain = domain )
275335
276336
277337class SecretServer :
@@ -317,11 +377,11 @@ def headers(self):
317377 return self .authorizer .headers ()
318378
319379 def __init__ (
320- self ,
321- base_url ,
322- authorizer : Authorizer ,
323- api_path_uri = API_PATH_URI ,
324- ):
380+ self ,
381+ base_url ,
382+ authorizer : Authorizer ,
383+ api_path_uri = API_PATH_URI ,
384+ ):
325385 """
326386 :param base_url: The base URL e.g. ``http://localhost/SecretServer``
327387 :type base_url: str
@@ -330,10 +390,40 @@ def __init__(
330390 :param api_path_uri: Defaults to ``/api/v1``
331391 :type api_path_uri: str
332392 """
333-
334393 self .base_url = base_url .rstrip ("/" )
394+ self .platform_url = self .base_url
335395 self .authorizer = authorizer
336- self .api_url = f"{ self .base_url } /{ api_path_uri .strip ('/' )} "
396+ self ._api_path_uri = api_path_uri
397+
398+ @property
399+ def api_url (self ):
400+ return f"{ self .base_url } /{ self ._api_path_uri .strip ('/' )} "
401+
402+ def ensure_vault_url (self ):
403+ """For platform, fetch and set the vault URL before making API calls."""
404+ # Only needed for platform scenario
405+ if hasattr (self .authorizer , "_server_type" ) and self .authorizer ._server_type == "platform" :
406+ if not hasattr (self , "_vault_url_fetched" ) or not self ._vault_url_fetched :
407+ access_token = self .authorizer .get_access_token ()
408+ vaults_endpoint = self .platform_url + "/vaultbroker/api/vaults"
409+ headers = {"Authorization" : f"Bearer { access_token } " }
410+ resp = requests .get (vaults_endpoint , headers = headers , timeout = 60 )
411+ if resp .status_code != 200 :
412+ raise SecretServerError (f"Failed to fetch vault details: HTTP { resp .status_code } - { resp .text } " )
413+ try :
414+ data = resp .json ()
415+ except Exception as ex :
416+ raise SecretServerError (f"Failed to parse vault details: { ex } " )
417+ for vault in data .get ("vaults" , []):
418+ if vault .get ("isDefault" ) and vault .get ("isActive" ):
419+ conn = vault .get ("connection" , {})
420+ url = conn .get ("url" )
421+ if url :
422+ self .base_url = url .rstrip ("/" )
423+ self ._vault_url_fetched = True
424+ return
425+ raise SecretServerError ("No configured default and active vault found in vault details." )
426+
337427
338428 def get_secret_json (self , id , query_params = None ):
339429 """Gets a Secret from Secret Server
@@ -349,18 +439,20 @@ def get_secret_json(self, id, query_params=None):
349439 :raise: :class:`SecretServerError` when the REST API call fails for
350440 any other reason
351441 """
442+ headers = self .headers ()
443+ self .ensure_vault_url ()
352444 endpoint_url = f"{ self .api_url } /secrets/{ id } "
353445
354446 if query_params is None :
355447 return self .process (
356- requests .get (endpoint_url , headers = self . headers () , timeout = 60 )
448+ requests .get (endpoint_url , headers = headers , timeout = 60 )
357449 ).text
358450 else :
359451 return self .process (
360452 requests .get (
361453 endpoint_url ,
362454 params = query_params ,
363- headers = self . headers () ,
455+ headers = headers ,
364456 timeout = 60 ,
365457 )
366458 ).text
@@ -379,19 +471,21 @@ def get_folder_json(self, id, query_params=None, get_all_children=True):
379471 :raise: :class:`SecretServerError` when the REST API call fails for
380472 any other reason
381473 """
474+ headers = self .headers ()
475+ self .ensure_vault_url ()
382476 endpoint_url = f"{ self .api_url } /folders/{ id } "
383477
384478 if get_all_children :
385479 query_params ["getAllChildren" ] = "true"
386480
387481 if query_params is None :
388- return self .process (requests .get (endpoint_url , headers = self . headers () )).text
482+ return self .process (requests .get (endpoint_url , headers = headers )).text
389483 else :
390484 return self .process (
391485 requests .get (
392486 endpoint_url ,
393487 params = query_params ,
394- headers = self . headers () ,
488+ headers = headers ,
395489 )
396490 ).text
397491
@@ -520,18 +614,20 @@ def search_secrets(self, query_params=None):
520614 :raise: :class:`SecretServerError` when the REST API call fails for
521615 any other reason
522616 """
617+ headers = self .headers ()
618+ self .ensure_vault_url ()
523619 endpoint_url = f"{ self .api_url } /secrets"
524620
525621 if query_params is None :
526622 return self .process (
527- requests .get (endpoint_url , headers = self . headers () , timeout = 60 )
623+ requests .get (endpoint_url , headers = headers , timeout = 60 )
528624 ).text
529625 else :
530626 return self .process (
531627 requests .get (
532628 endpoint_url ,
533629 params = query_params ,
534- headers = self . headers () ,
630+ headers = headers ,
535631 timeout = 60 ,
536632 )
537633 ).text
@@ -548,16 +644,18 @@ def lookup_folders(self, query_params=None):
548644 :raise: :class:`SecretServerError` when the REST API call fails for
549645 any other reason
550646 """
647+ headers = self .headers ()
648+ self .ensure_vault_url ()
551649 endpoint_url = f"{ self .api_url } /folders/lookup"
552650
553651 if query_params is None :
554- return self .process (requests .get (endpoint_url , headers = self . headers () )).text
652+ return self .process (requests .get (endpoint_url , headers = headers )).text
555653 else :
556654 return self .process (
557655 requests .get (
558656 endpoint_url ,
559657 params = query_params ,
560- headers = self . headers () ,
658+ headers = headers ,
561659 )
562660 ).text
563661
@@ -573,12 +671,13 @@ def get_secret_ids_by_folderid(self, folder_id):
573671 :raise: :class:`SecretServerError` when the REST API call fails for
574672 any other reason
575673 """
576-
674+ headers = self .headers ()
675+ self .ensure_vault_url ()
577676 params = {"filter.folderId" : folder_id }
578677 endpoint_url = f"{ self .api_url } /secrets/search-total"
579678 params ["take" ] = self .process (
580679 requests .get (
581- endpoint_url , params = params , headers = self . headers () , timeout = 60
680+ endpoint_url , params = params , headers = headers , timeout = 60
582681 )
583682 ).text
584683 response = self .search_secrets (query_params = params )
@@ -605,7 +704,8 @@ def get_child_folder_ids_by_folderid(self, folder_id):
605704 :raise: :class:`SecretServerError` when the REST API call fails for
606705 any other reason
607706 """
608-
707+ headers = self .headers ()
708+ self .ensure_vault_url ()
609709 params = {
610710 "filter.parentFolderId" : folder_id ,
611711 "filter.limitToDirectDescendents" : True ,
@@ -614,7 +714,7 @@ def get_child_folder_ids_by_folderid(self, folder_id):
614714 endpoint_url = f"{ self .api_url } /folders/lookup"
615715
616716 params ["take" ] = self .process (
617- requests .get (endpoint_url , params = params , headers = self . headers () )
717+ requests .get (endpoint_url , params = params , headers = headers )
618718 ).json ()["total" ]
619719 # Handle result of zero child folders
620720 if params ["take" ] != 0 :
@@ -651,12 +751,12 @@ def __init__(
651751 username ,
652752 password ,
653753 api_path_uri = SecretServer .API_PATH_URI ,
654- token_path_uri = PasswordGrantAuthorizer . TOKEN_PATH_URI ,
754+ token_path_uri = None ,
655755 ):
656756 super ().__init__ (
657757 base_url ,
658758 PasswordGrantAuthorizer (
659- f"{ base_url } / { token_path_uri . strip ( '/' ) } " , username , password
759+ f"{ base_url } " , username , password , token_path_uri
660760 ),
661761 api_path_uri ,
662762 )
@@ -676,5 +776,13 @@ class SecretServerCloud(SecretServer):
676776 DEFAULT_TLD = "com"
677777 URL_TEMPLATE = "https://{}.secretservercloud.{}"
678778
679- def __init__ (self , tenant , authorizer : Authorizer , tld = DEFAULT_TLD ):
680- super ().__init__ (self .URL_TEMPLATE .format (tenant , tld ), authorizer )
779+ def __init__ (self , tenant = None , authorizer = None , tld = DEFAULT_TLD , base_url = None ):
780+ if authorizer is None or not isinstance (authorizer , Authorizer ):
781+ raise ValueError ("authorizer must be provided and must be of type Authorizer" )
782+ if tenant :
783+ url = self .URL_TEMPLATE .format (tenant , tld )
784+ elif base_url :
785+ url = base_url .rstrip ("/" )
786+ else :
787+ raise ValueError ("Must provide either tenant or base_url" )
788+ super ().__init__ (url , authorizer )
0 commit comments