Skip to content

Commit 145f5f8

Browse files
authored
Merge pull request #786 from atlanhq/DEVX-299
DEVX-299 : Manage OAuth clients (incl. secrets)
2 parents 967b1c5 + 2497033 commit 145f5f8

14 files changed

Lines changed: 1426 additions & 0 deletions

File tree

pyatlan/client/aio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .file import AsyncFileClient
3636
from .group import AsyncGroupClient
3737
from .impersonate import AsyncImpersonationClient
38+
from .oauth_client import AsyncOAuthClient
3839
from .open_lineage import AsyncOpenLineageClient
3940
from .query import AsyncQueryClient
4041
from .role import AsyncRoleClient
@@ -61,6 +62,7 @@
6162
"AsyncImpersonationClient",
6263
"AsyncIndexSearchResults",
6364
"AsyncLineageListResults",
65+
"AsyncOAuthClient",
6466
"AsyncOpenLineageClient",
6567
"AsyncQueryClient",
6668
"AsyncRoleClient",

pyatlan/client/aio/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from pyatlan.client.aio.group import AsyncGroupClient
4343
from pyatlan.client.aio.impersonate import AsyncImpersonationClient
4444
from pyatlan.client.aio.oauth import AsyncOAuthTokenManager
45+
from pyatlan.client.aio.oauth_client import AsyncOAuthClient
4546
from pyatlan.client.aio.open_lineage import AsyncOpenLineageClient
4647
from pyatlan.client.aio.query import AsyncQueryClient
4748
from pyatlan.client.aio.role import AsyncRoleClient
@@ -113,6 +114,7 @@ class AsyncAtlanClient(AtlanClient):
113114
_async_sso_client: Optional[AsyncSSOClient] = PrivateAttr(default=None)
114115
_async_task_client: Optional[AsyncTaskClient] = PrivateAttr(default=None)
115116
_async_token_client: Optional[AsyncTokenClient] = PrivateAttr(default=None)
117+
_async_oauth_client_client: Optional[AsyncOAuthClient] = PrivateAttr(default=None)
116118
_async_typedef_client: Optional[AsyncTypeDefClient] = PrivateAttr(default=None)
117119
_async_user_client: Optional[AsyncUserClient] = PrivateAttr(default=None)
118120
_async_workflow_client: Optional[AsyncWorkflowClient] = PrivateAttr(default=None)
@@ -362,6 +364,13 @@ def token(self) -> AsyncTokenClient: # type: ignore[override]
362364
self._async_token_client = AsyncTokenClient(self) # type: ignore[arg-type]
363365
return self._async_token_client
364366

367+
@property
368+
def oauth_client(self) -> AsyncOAuthClient: # type: ignore[override]
369+
"""Get async OAuth client client with same API as sync"""
370+
if self._async_oauth_client_client is None:
371+
self._async_oauth_client_client = AsyncOAuthClient(self) # type: ignore[arg-type]
372+
return self._async_oauth_client_client
373+
365374
@property
366375
def typedef(self) -> AsyncTypeDefClient: # type: ignore[override]
367376
"""Get async typedef client with same API as sync"""

pyatlan/client/aio/oauth_client.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Atlan Pte. Ltd.
3+
4+
from __future__ import annotations
5+
6+
from typing import List, Optional
7+
8+
from pydantic.v1 import validate_arguments
9+
10+
from pyatlan.client.common import (
11+
AsyncApiCaller,
12+
OAuthClientCreate,
13+
OAuthClientGet,
14+
OAuthClientGetById,
15+
OAuthClientPurge,
16+
OAuthClientUpdate,
17+
RoleGet,
18+
)
19+
from pyatlan.errors import ErrorCode
20+
from pyatlan.model.aio.oauth_client import AsyncOAuthClientListResponse
21+
from pyatlan.model.oauth_client import OAuthClientCreateResponse, OAuthClientResponse
22+
23+
24+
class AsyncOAuthClient:
25+
"""
26+
Async client for managing OAuth client credentials.
27+
"""
28+
29+
def __init__(self, client: AsyncApiCaller):
30+
if not isinstance(client, AsyncApiCaller):
31+
raise ErrorCode.INVALID_PARAMETER_TYPE.exception_with_parameters(
32+
"client", "AsyncApiCaller"
33+
)
34+
self._client = client
35+
36+
@validate_arguments
37+
async def get(
38+
self,
39+
limit: int = 20,
40+
offset: int = 0,
41+
sort: Optional[str] = None,
42+
) -> AsyncOAuthClientListResponse:
43+
"""
44+
Retrieves OAuth clients defined in Atlan with pagination support.
45+
46+
:param limit: maximum number of results to be returned per page (default: 20)
47+
:param offset: starting point for results to return, for paging
48+
:param sort: property by which to sort the results (e.g., 'createdAt' for descending)
49+
:returns: an AsyncOAuthClientListResponse containing records and pagination info
50+
:raises AtlanError: on any API communication issue
51+
"""
52+
endpoint, query_params = OAuthClientGet.prepare_request(limit, offset, sort)
53+
raw_json = await self._client._call_api(endpoint, query_params)
54+
return AsyncOAuthClientListResponse(
55+
**raw_json,
56+
endpoint=endpoint,
57+
client=self._client,
58+
size=limit,
59+
start=offset,
60+
sort=sort,
61+
)
62+
63+
@validate_arguments
64+
async def get_by_id(self, client_id: str) -> OAuthClientResponse:
65+
"""
66+
Retrieves the OAuth client with the specified client ID.
67+
68+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
69+
:returns: the OAuthClientResponse with the specified client ID
70+
:raises AtlanError: on any API communication issue
71+
"""
72+
endpoint, query_params = OAuthClientGetById.prepare_request(client_id)
73+
raw_json = await self._client._call_api(endpoint, query_params)
74+
return OAuthClientGetById.process_response(raw_json)
75+
76+
@validate_arguments
77+
async def update(
78+
self,
79+
client_id: str,
80+
display_name: Optional[str] = None,
81+
description: Optional[str] = None,
82+
) -> OAuthClientResponse:
83+
"""
84+
Update an existing OAuth client with the provided settings.
85+
86+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
87+
:param display_name: human-readable name for the OAuth client
88+
:param description: optional explanation of the OAuth client
89+
:returns: the updated OAuthClientResponse
90+
:raises AtlanError: on any API communication issue
91+
"""
92+
endpoint, request_obj = OAuthClientUpdate.prepare_request(
93+
client_id, display_name, description
94+
)
95+
raw_json = await self._client._call_api(endpoint, request_obj=request_obj)
96+
return OAuthClientUpdate.process_response(raw_json)
97+
98+
@validate_arguments
99+
async def purge(self, client_id: str) -> None:
100+
"""
101+
Delete (purge) the specified OAuth client.
102+
103+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
104+
:raises AtlanError: on any API communication issue
105+
"""
106+
endpoint, _ = OAuthClientPurge.prepare_request(client_id)
107+
await self._client._call_api(endpoint)
108+
109+
async def _fetch_available_roles(self):
110+
"""
111+
Fetch all available roles (workspace and admin-subrole levels).
112+
113+
:returns: list of AtlanRole objects
114+
"""
115+
filter_str = OAuthClientCreate.build_roles_filter()
116+
endpoint, query_params = RoleGet.prepare_request(
117+
limit=100,
118+
post_filter=filter_str,
119+
)
120+
raw_json = await self._client._call_api(endpoint, query_params)
121+
response = RoleGet.process_response(raw_json)
122+
return response.records or []
123+
124+
@validate_arguments
125+
async def create(
126+
self,
127+
name: str,
128+
role: str,
129+
description: Optional[str] = None,
130+
persona_qualified_names: Optional[List[str]] = None,
131+
) -> OAuthClientCreateResponse:
132+
"""
133+
Create a new OAuth client with the provided settings.
134+
135+
:param name: human-readable name for the OAuth client (displayed in UI)
136+
:param role: role description to assign to the OAuth client (e.g., 'Admin', 'Member').
137+
:param description: optional explanation of the OAuth client
138+
:param persona_qualified_names: qualified names of personas to associate with the OAuth client
139+
:returns: the created OAuthClientCreateResponse (includes client_id and client_secret)
140+
:raises AtlanError: on any API communication issue
141+
:raises NotFoundError: if the specified role description is not found
142+
"""
143+
# Fetch available roles and resolve the user-provided role name
144+
available_roles = await self._fetch_available_roles()
145+
resolved_role = OAuthClientCreate.resolve_role_name(role, available_roles)
146+
147+
# Prepare and execute the request
148+
endpoint, request_obj = OAuthClientCreate.prepare_request(
149+
display_name=name,
150+
role=resolved_role,
151+
description=description,
152+
persona_qualified_names=persona_qualified_names,
153+
)
154+
raw_json = await self._client._call_api(endpoint, request_obj=request_obj)
155+
return OAuthClientCreate.process_response(raw_json)

pyatlan/client/atlan.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from pyatlan.client.group import GroupClient
5050
from pyatlan.client.impersonate import ImpersonationClient
5151
from pyatlan.client.oauth import OAuthTokenManager
52+
from pyatlan.client.oauth_client import OAuthClient
5253
from pyatlan.client.open_lineage import OpenLineageClient
5354
from pyatlan.client.query import QueryClient
5455
from pyatlan.client.role import RoleClient
@@ -151,6 +152,7 @@ class AtlanClient(BaseSettings):
151152
_asset_client: Optional[AssetClient] = PrivateAttr(default=None)
152153
_typedef_client: Optional[TypeDefClient] = PrivateAttr(default=None)
153154
_token_client: Optional[TokenClient] = PrivateAttr(default=None)
155+
_oauth_client_client: Optional[OAuthClient] = PrivateAttr(default=None)
154156
_user_client: Optional[UserClient] = PrivateAttr(default=None)
155157
_impersonate_client: Optional[ImpersonationClient] = PrivateAttr(default=None)
156158
_query_client: Optional[QueryClient] = PrivateAttr(default=None)
@@ -343,6 +345,12 @@ def token(self) -> TokenClient:
343345
self._token_client = TokenClient(client=self)
344346
return self._token_client
345347

348+
@property
349+
def oauth_client(self) -> OAuthClient:
350+
if self._oauth_client_client is None:
351+
self._oauth_client_client = OAuthClient(client=self)
352+
return self._oauth_client_client
353+
346354
@property
347355
def typedef(self) -> TypeDefClient:
348356
if self._typedef_client is None:

pyatlan/client/common/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@
9797
ImpersonateUser,
9898
)
9999

100+
# OAuth client shared logic classes
101+
from .oauth_client import (
102+
OAuthClientCreate,
103+
OAuthClientGet,
104+
OAuthClientGetById,
105+
OAuthClientPurge,
106+
OAuthClientUpdate,
107+
)
108+
100109
# OpenLineage shared logic classes
101110
from .open_lineage import (
102111
OpenLineageCreateConnection,
@@ -255,6 +264,12 @@
255264
"ImpersonateGetClientSecret",
256265
"ImpersonateGetUserId",
257266
"ImpersonateUser",
267+
# OAuth client shared logic classes
268+
"OAuthClientCreate",
269+
"OAuthClientGet",
270+
"OAuthClientGetById",
271+
"OAuthClientPurge",
272+
"OAuthClientUpdate",
258273
# OpenLineage shared logic classes
259274
"OpenLineageCreateConnection",
260275
"OpenLineageCreateCredential",

0 commit comments

Comments
 (0)