Skip to content

Commit ae6090b

Browse files
committed
refactor: move auth services in platform
1 parent 164bd12 commit ae6090b

26 files changed

Lines changed: 1300 additions & 1338 deletions

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.0.23"
3+
version = "0.0.24"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

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

Lines changed: 35 additions & 53 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

@@ -22,16 +22,6 @@ def __init__(self, base_url: Optional[str]):
2222
self._base_url = resolved_base_url
2323
self._domain = self._extract_environment_from_base_url(self._base_url)
2424

25-
def get_token_url(self) -> str:
26-
"""Get the token URL for the specified domain."""
27-
match self._domain:
28-
case "alpha":
29-
return "https://alpha.uipath.com/identity_/connect/token"
30-
case "staging":
31-
return "https://staging.uipath.com/identity_/connect/token"
32-
case _: # cloud (default)
33-
return "https://cloud.uipath.com/identity_/connect/token"
34-
3525
def _is_valid_domain_or_subdomain(self, hostname: str, domain: str) -> bool:
3626
"""Check if hostname is either an exact match or a valid subdomain of the domain.
3727
@@ -87,53 +77,45 @@ def get_token_data(
8777
Returns:
8878
Token data if successful
8979
"""
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,
80+
domain_map = {
81+
"alpha": "https://alpha.uipath.com",
82+
"staging": "https://staging.uipath.com",
9783
}
84+
domain = domain_map.get(self._domain, "https://cloud.uipath.com")
9885

9986
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-
)
87+
return IdentityService.get_client_credentials_token(
88+
domain=domain,
89+
client_id=client_id,
90+
client_secret=client_secret,
91+
scope=scope,
92+
)
93+
except HTTPStatusError as e:
94+
match e.response.status_code:
95+
case 400:
96+
raise EnrichedException(
97+
HTTPStatusError(
98+
message="Invalid client credentials or request parameters.",
99+
request=e.request,
100+
response=e.response,
114101
)
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-
)
102+
) from e
103+
case 401:
104+
raise EnrichedException(
105+
HTTPStatusError(
106+
message="Unauthorized: Invalid client credentials.",
107+
request=e.request,
108+
response=e.response,
124109
)
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-
)
110+
) from e
111+
case _:
112+
raise EnrichedException(
113+
HTTPStatusError(
114+
message=f"Authentication failed with unexpected status: {e.response.status_code}",
115+
request=e.request,
116+
response=e.response,
134117
)
135-
except EnrichedException:
136-
raise
118+
) from e
137119
except httpx.RequestError as e:
138120
raise Exception(f"Network error during authentication: {e}") from e
139121
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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
scope = scope or "OR.Execution"
69+
url = f"{domain}/identity_/connect/token"
70+
data = {
71+
"grant_type": "client_credentials",
72+
"client_id": client_id,
73+
"client_secret": client_secret,
74+
"scope": scope,
75+
}
76+
77+
with httpx.Client(**get_httpx_client_kwargs()) as client:
78+
response = client.post(url, data=data)
79+
response.raise_for_status()
80+
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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
21+
Args:
22+
tenant_url: The tenant base URL (e.g., "https://cloud.uipath.com/org/tenant").
23+
access_token: The Bearer access token for authorization.
24+
"""
25+
urls = [
26+
f"{tenant_url}/orchestrator_/api/StudioWeb/TryEnableFirstRun",
27+
f"{tenant_url}/orchestrator_/api/StudioWeb/AcquireLicense",
28+
]
29+
headers = {"Authorization": f"Bearer {access_token}"}
30+
31+
with httpx.Client(**get_httpx_client_kwargs()) as client:
32+
for url in urls:
33+
try:
34+
response = client.post(url, headers=headers)
35+
if not response.is_success:
36+
logger.warning(
37+
"StudioWeb enable_first_run: POST %s returned %s",
38+
url,
39+
response.status_code,
40+
)
41+
except httpx.HTTPError as exc:
42+
logger.warning(
43+
"StudioWeb enable_first_run: POST %s failed: %s",
44+
url,
45+
exc,
46+
)
47+
48+
@staticmethod
49+
def get_server_version(domain: str) -> str | None:
50+
"""Get the Orchestrator server version.
51+
52+
Args:
53+
domain: The base URL of the UiPath platform (e.g., "https://cloud.uipath.com").
54+
55+
Returns:
56+
The server version string, or None if the request fails for any reason.
57+
"""
58+
url = f"{domain}/orchestrator_/api/status/version"
59+
60+
try:
61+
client_kwargs = get_httpx_client_kwargs()
62+
client_kwargs["timeout"] = 5.0
63+
with httpx.Client(**client_kwargs) as client:
64+
response = client.get(url)
65+
response.raise_for_status()
66+
data = response.json()
67+
return data.get("version")
68+
except Exception:
69+
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)