Skip to content

Commit 224e094

Browse files
committed
APP-7824: Added support for AtlanClient creation via api token guid
1 parent 32206e4 commit 224e094

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

pyatlan/client/atlan.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import copy
88
import json
99
import logging
10+
import os
1011
import shutil
1112
import uuid
1213
from contextvars import ContextVar
@@ -41,7 +42,7 @@
4142
from pyatlan.client.asset import A, AssetClient, IndexSearchResults, LineageListResults
4243
from pyatlan.client.audit import AuditClient
4344
from pyatlan.client.common import CONNECTION_RETRY, HTTP_PREFIX, HTTPS_PREFIX
44-
from pyatlan.client.constants import EVENT_STREAM, PARSE_QUERY, UPLOAD_IMAGE
45+
from pyatlan.client.constants import EVENT_STREAM, GET_TOKEN, PARSE_QUERY, UPLOAD_IMAGE
4546
from pyatlan.client.contract import ContractClient
4647
from pyatlan.client.credential import CredentialClient
4748
from pyatlan.client.file import FileClient
@@ -75,7 +76,7 @@
7576
from pyatlan.model.group import AtlanGroup, CreateGroupResponse, GroupResponse
7677
from pyatlan.model.lineage import LineageListRequest
7778
from pyatlan.model.query import ParsedQuery, QueryParserRequest
78-
from pyatlan.model.response import AssetMutationResponse
79+
from pyatlan.model.response import AccessTokenResponse, AssetMutationResponse
7980
from pyatlan.model.role import RoleResponse
8081
from pyatlan.model.search import IndexSearchRequest
8182
from pyatlan.model.typedef import TypeDef, TypeDefResponse
@@ -346,6 +347,70 @@ def source_tag_cache(self) -> SourceTagCache:
346347
self._source_tag_cache = SourceTagCache(client=self)
347348
return self._source_tag_cache
348349

350+
@classmethod
351+
def from_token_guid(cls, guid: str) -> AtlanClient:
352+
"""
353+
Create an AtlanClient instance using an API token GUID.
354+
355+
This method performs a multi-step authentication flow:
356+
1. Obtains Atlan-Argo (superuser) access token
357+
2. Uses Argo token to retrieve the API token's client credentials
358+
3. Exchanges those credentials for an access token
359+
4. Returns a new AtlanClient authenticated with the resolved token
360+
361+
:param guid: API token GUID to resolve
362+
:returns: a new client instance authenticated with the resolved token
363+
:raises: ErrorCode.UNABLE_TO_ESCALATE_WITH_PARAM: If any step in the token resolution fails
364+
"""
365+
base_url = os.environ.get("ATLAN_BASE_URL", "INTERNAL")
366+
367+
# Step 1: Initialize base client and get Atlan-Argo credentials
368+
# Note: Using empty api_key as we're bootstrapping authentication
369+
client = AtlanClient(base_url=base_url, api_key="")
370+
client_info = client.impersonate._get_client_info()
371+
372+
# Prepare credentials for Atlan-Argo token request
373+
argo_credentials = {
374+
"grant_type": "client_credentials",
375+
"client_id": client_info.client_id,
376+
"client_secret": client_info.client_secret,
377+
"scope": "openid",
378+
}
379+
380+
# Step 2: Obtain Atlan-Argo (superuser) access token
381+
try:
382+
raw_json = client._call_api(GET_TOKEN, request_obj=argo_credentials)
383+
argo_token = AccessTokenResponse(**raw_json).access_token
384+
temp_argo_client = AtlanClient(base_url=base_url, api_key=argo_token)
385+
except AtlanError as atlan_err:
386+
raise ErrorCode.UNABLE_TO_ESCALATE_WITH_PARAM.exception_with_parameters(
387+
"Failed to obtain Atlan-Argo token"
388+
) from atlan_err
389+
390+
# Step 3: Use Argo client to retrieve API token's credentials
391+
# Both endpoints require authentication, hence using the Argo token
392+
token_secret = temp_argo_client.impersonate.get_client_secret(client_guid=guid)
393+
token_client_id = temp_argo_client.token.get_by_guid(guid=guid).client_id
394+
395+
# Step 4: Exchange API token credentials for access token
396+
token_credentials = {
397+
"grant_type": "client_credentials",
398+
"client_id": token_client_id,
399+
"client_secret": token_secret,
400+
"scope": "openid",
401+
}
402+
403+
try:
404+
raw_json = client._call_api(GET_TOKEN, request_obj=token_credentials)
405+
token_api_key = AccessTokenResponse(**raw_json).access_token
406+
407+
# Step 5: Create and return the authenticated client
408+
return AtlanClient(base_url=base_url, api_key=token_api_key)
409+
except AtlanError as atlan_err:
410+
raise ErrorCode.UNABLE_TO_ESCALATE_WITH_PARAM.exception_with_parameters(
411+
"Failed to obtain access token for API token"
412+
) from atlan_err
413+
349414
def update_headers(self, header: Dict[str, str]):
350415
self._session.headers.update(header)
351416

pyatlan/errors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,13 @@ class ErrorCode(Enum):
714714
"Check the details of your configured privileged credentials and the user you requested to impersonate.",
715715
PermissionError,
716716
)
717+
UNABLE_TO_ESCALATE_WITH_PARAM = (
718+
403,
719+
"ATLAN-PYTHON-403-003",
720+
"Unable to escalate to a privileged user: {0}",
721+
"Check the details of your configured privileged credentials.",
722+
PermissionError,
723+
)
717724
NOT_FOUND_PASSTHROUGH = (
718725
404,
719726
"ATLAN-PYTHON-404-000",

0 commit comments

Comments
 (0)