Skip to content

Commit 8ba0934

Browse files
committed
Added platform token support
1 parent 67cdae7 commit 8ba0934

1 file changed

Lines changed: 146 additions & 38 deletions

File tree

delinea/secrets/server.py

Lines changed: 146 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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

277337
class 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

Comments
 (0)