Skip to content

Commit 30c1166

Browse files
committed
refact: move auth services in platform
1 parent 03b641c commit 30c1166

23 files changed

Lines changed: 1327 additions & 834 deletions

packages/uipath-platform/src/uipath/platform/common/_external_application_service.py

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from urllib.parse import urlparse
44

55
import httpx
6-
from httpx import HTTPStatusError, Request
6+
from httpx import HTTPStatusError
77

88
from ..errors import EnrichedException
9-
from ._http_config import get_httpx_client_kwargs
9+
from ..identity import IdentityService
1010
from .auth import TokenData
1111
from .constants import ENV_BASE_URL
1212

@@ -75,7 +75,7 @@ def _extract_environment_from_base_url(self, base_url: str) -> str:
7575
return "cloud"
7676

7777
def get_token_data(
78-
self, client_id: str, client_secret: str, scope: Optional[str] = "OR.Execution"
78+
self, client_id: str, client_secret: str, scope: Optional[str]
7979
) -> TokenData:
8080
"""Authenticate using client credentials flow.
8181
@@ -87,53 +87,45 @@ def get_token_data(
8787
Returns:
8888
Token data if successful
8989
"""
90-
token_url = self.get_token_url()
91-
92-
data = {
93-
"grant_type": "client_credentials",
94-
"client_id": client_id,
95-
"client_secret": client_secret,
96-
"scope": scope,
90+
domain_map = {
91+
"alpha": "https://alpha.uipath.com",
92+
"staging": "https://staging.uipath.com",
9793
}
94+
domain = domain_map.get(self._domain, "https://cloud.uipath.com")
9895

9996
try:
100-
with httpx.Client(**get_httpx_client_kwargs()) as client:
101-
response = client.post(token_url, data=data)
102-
match response.status_code:
103-
case 200:
104-
return TokenData.model_validate(response.json())
105-
case 400:
106-
raise EnrichedException(
107-
HTTPStatusError(
108-
message="Invalid client credentials or request parameters.",
109-
request=Request(
110-
data=data, url=token_url, method="post"
111-
),
112-
response=response,
113-
)
97+
return IdentityService.get_client_credentials_token(
98+
domain=domain,
99+
client_id=client_id,
100+
client_secret=client_secret,
101+
scope=scope,
102+
)
103+
except HTTPStatusError as e:
104+
match e.response.status_code:
105+
case 400:
106+
raise EnrichedException(
107+
HTTPStatusError(
108+
message="Invalid client credentials or request parameters.",
109+
request=e.request,
110+
response=e.response,
114111
)
115-
case 401:
116-
raise EnrichedException(
117-
HTTPStatusError(
118-
message="Unauthorized: Invalid client credentials.",
119-
request=Request(
120-
data=data, url=token_url, method="post"
121-
),
122-
response=response,
123-
)
112+
) from e
113+
case 401:
114+
raise EnrichedException(
115+
HTTPStatusError(
116+
message="Unauthorized: Invalid client credentials.",
117+
request=e.request,
118+
response=e.response,
124119
)
125-
case _:
126-
raise EnrichedException(
127-
HTTPStatusError(
128-
message=f"Authentication failed with unexpected status: {response.status_code}",
129-
request=Request(
130-
data=data, url=token_url, method="post"
131-
),
132-
response=response,
133-
)
120+
) from e
121+
case _:
122+
raise EnrichedException(
123+
HTTPStatusError(
124+
message=f"Authentication failed with unexpected status: {e.response.status_code}",
125+
request=e.request,
126+
response=e.response,
134127
)
135-
except EnrichedException:
136-
raise
128+
) from e
137129
except httpx.RequestError as e:
138130
raise Exception(f"Network error during authentication: {e}") from e
139131
except Exception as e:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""UiPath Identity Service."""
2+
3+
from ._identity_service import IdentityService
4+
5+
__all__ = ["IdentityService"]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Identity service for UiPath authentication token operations."""
2+
3+
from typing import Optional
4+
5+
import httpx
6+
7+
from ..common._http_config import get_httpx_client_kwargs
8+
from ..common.auth import TokenData
9+
10+
11+
class IdentityService:
12+
"""Service for interacting with the UiPath Identity server."""
13+
14+
@staticmethod
15+
def refresh_access_token(
16+
domain: str,
17+
refresh_token: str,
18+
client_id: str,
19+
) -> TokenData:
20+
"""Refresh an access token using a refresh token.
21+
22+
Args:
23+
domain: The base URL of the UiPath identity server (e.g., "https://cloud.uipath.com").
24+
refresh_token: The refresh token to exchange for a new access token.
25+
client_id: The client ID of the application.
26+
27+
Returns:
28+
TokenData containing the new access token and related information.
29+
30+
Raises:
31+
httpx.HTTPStatusError: If the server returns a non-2xx response.
32+
httpx.ConnectError: If there is a network connectivity issue.
33+
"""
34+
url = f"{domain}/identity_/connect/token"
35+
data = {
36+
"grant_type": "refresh_token",
37+
"refresh_token": refresh_token,
38+
"client_id": client_id,
39+
}
40+
41+
with httpx.Client(**get_httpx_client_kwargs()) as client:
42+
response = client.post(url, data=data)
43+
response.raise_for_status()
44+
return TokenData.model_validate(response.json())
45+
46+
@staticmethod
47+
def get_client_credentials_token(
48+
domain: str,
49+
client_id: str,
50+
client_secret: str,
51+
scope: Optional[str] = "OR.Execution",
52+
) -> TokenData:
53+
"""Obtain an access token using client credentials grant.
54+
55+
Args:
56+
domain: The base URL of the UiPath identity server (e.g., "https://cloud.uipath.com").
57+
client_id: The client ID of the application.
58+
client_secret: The client secret of the application.
59+
scope: The requested OAuth scopes (optional, default: "OR.Execution").
60+
61+
Returns:
62+
TokenData containing the access token and related information.
63+
64+
Raises:
65+
httpx.HTTPStatusError: If the server returns a non-2xx response.
66+
httpx.ConnectError: If there is a network connectivity issue.
67+
"""
68+
url = f"{domain}/identity_/connect/token"
69+
data = {
70+
"grant_type": "client_credentials",
71+
"client_id": client_id,
72+
"client_secret": client_secret,
73+
"scope": scope,
74+
}
75+
76+
with httpx.Client(**get_httpx_client_kwargs()) as client:
77+
response = client.post(url, data=data)
78+
response.raise_for_status()
79+
return TokenData.model_validate(response.json())

packages/uipath-platform/src/uipath/platform/orchestrator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ._mcp_service import McpService
1212
from ._processes_service import ProcessesService
1313
from ._queues_service import QueuesService
14+
from ._studio_web_service import StudioWebService
1415
from .assets import Asset, UserAsset
1516
from .attachment import Attachment
1617
from .buckets import Bucket, BucketFile
@@ -34,6 +35,7 @@
3435
"McpService",
3536
"ProcessesService",
3637
"QueuesService",
38+
"StudioWebService",
3739
"Asset",
3840
"UserAsset",
3941
"Attachment",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""StudioWeb service for UiPath Platform."""
2+
3+
import logging
4+
5+
import httpx
6+
7+
from ..common._http_config import get_httpx_client_kwargs
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class StudioWebService:
13+
"""Service for interacting with the UiPath StudioWeb API."""
14+
15+
@staticmethod
16+
def enable_first_run(tenant_url: str, access_token: str) -> None:
17+
"""Fire-and-forget POST requests to enable first run for StudioWeb.
18+
19+
Posts to TryEnableFirstRun and AcquireLicense endpoints.
20+
Does NOT raise on failure — logs warnings instead.
21+
22+
Args:
23+
tenant_url: The tenant base URL (e.g., "https://cloud.uipath.com/org/tenant").
24+
access_token: The Bearer access token for authorization.
25+
"""
26+
urls = [
27+
f"{tenant_url}/orchestrator_/api/StudioWeb/TryEnableFirstRun",
28+
f"{tenant_url}/orchestrator_/api/StudioWeb/AcquireLicense",
29+
]
30+
headers = {"Authorization": f"Bearer {access_token}"}
31+
32+
with httpx.Client(**get_httpx_client_kwargs()) as client:
33+
for url in urls:
34+
try:
35+
response = client.post(url, headers=headers)
36+
if not response.is_success:
37+
logger.warning(
38+
"StudioWeb enable_first_run: POST %s returned %s",
39+
url,
40+
response.status_code,
41+
)
42+
except httpx.HTTPError as exc:
43+
logger.warning(
44+
"StudioWeb enable_first_run: POST %s failed: %s",
45+
url,
46+
exc,
47+
)
48+
49+
@staticmethod
50+
def get_server_version(domain: str) -> str | None:
51+
"""Get the Orchestrator server version.
52+
53+
Args:
54+
domain: The base URL of the UiPath platform (e.g., "https://cloud.uipath.com").
55+
56+
Returns:
57+
The server version string, or None if the request fails for any reason.
58+
"""
59+
url = f"{domain}/orchestrator_/api/status/version"
60+
61+
try:
62+
client_kwargs = get_httpx_client_kwargs()
63+
client_kwargs["timeout"] = 5.0
64+
with httpx.Client(**client_kwargs) as client:
65+
response = client.get(url)
66+
response.raise_for_status()
67+
data = response.json()
68+
return data.get("version")
69+
except Exception:
70+
return None
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""UiPath Portal Service."""
2+
3+
from ._portal_service import PortalService
4+
from .portal import OrganizationInfo, TenantInfo, TenantsAndOrganizationInfoResponse
5+
6+
__all__ = [
7+
"PortalService",
8+
"TenantInfo",
9+
"OrganizationInfo",
10+
"TenantsAndOrganizationInfoResponse",
11+
]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Portal service for UiPath Platform."""
2+
3+
import httpx
4+
5+
from ..common._http_config import get_httpx_client_kwargs
6+
from .portal import TenantsAndOrganizationInfoResponse
7+
8+
9+
class PortalService:
10+
"""Service for interacting with the UiPath Portal API."""
11+
12+
@staticmethod
13+
def get_tenants_and_organizations(
14+
domain: str,
15+
prt_id: str,
16+
access_token: str,
17+
) -> TenantsAndOrganizationInfoResponse:
18+
"""Retrieve tenants and organization info for the given organization.
19+
20+
Args:
21+
domain: The base URL of the UiPath platform (e.g., "https://cloud.uipath.com").
22+
prt_id: The organization/partition ID used in the URL path.
23+
access_token: The Bearer access token for authorization.
24+
25+
Returns:
26+
TenantsAndOrganizationInfoResponse containing tenants and organization info.
27+
28+
Raises:
29+
httpx.HTTPStatusError: If the server returns a non-2xx response.
30+
httpx.ConnectError: If there is a network connectivity issue.
31+
"""
32+
url = f"{domain}/{prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
33+
headers = {"Authorization": f"Bearer {access_token}"}
34+
35+
with httpx.Client(**get_httpx_client_kwargs()) as client:
36+
response = client.get(url, headers=headers)
37+
response.raise_for_status()
38+
return TenantsAndOrganizationInfoResponse.model_validate(response.json())
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Models for UiPath Portal service."""
2+
3+
from pydantic import BaseModel
4+
5+
6+
class TenantInfo(BaseModel):
7+
"""Model representing a tenant."""
8+
9+
name: str
10+
id: str
11+
12+
13+
class OrganizationInfo(BaseModel):
14+
"""Model representing an organization."""
15+
16+
id: str
17+
name: str
18+
19+
20+
class TenantsAndOrganizationInfoResponse(BaseModel):
21+
"""Model representing the tenants and organization info response."""
22+
23+
tenants: list[TenantInfo]
24+
organization: OrganizationInfo

0 commit comments

Comments
 (0)