Skip to content

Commit 2d007f6

Browse files
committed
feat: add auth network options
1 parent 55de34c commit 2d007f6

5 files changed

Lines changed: 132 additions & 56 deletions

File tree

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"
3-
version = "2.0.73"
3+
version = "2.0.74"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"

src/uipath/_cli/_auth/_client_credentials.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@
55

66
from .._utils._console import ConsoleLogger
77
from ._models import TokenData
8-
from ._utils import parse_access_token, update_env_file
8+
from ._utils import get_httpx_client_kwargs, parse_access_token, update_env_file
99

1010
console = ConsoleLogger()
1111

1212

1313
class ClientCredentialsService:
1414
"""Service for client credentials authentication flow."""
1515

16-
def __init__(self, domain: str):
16+
def __init__(
17+
self, domain: str, verify_ssl: bool = True, cert: str = None, proxy: str = None
18+
):
1719
self.domain = domain
20+
self.verify_ssl = verify_ssl
21+
self.cert = cert
22+
self.proxy = proxy
1823

1924
def get_token_url(self) -> str:
2025
"""Get the token URL for the specified domain."""
@@ -91,9 +96,10 @@ def authenticate(
9196
}
9297

9398
try:
94-
with httpx.Client(timeout=30.0) as client:
99+
with httpx.Client(
100+
**get_httpx_client_kwargs(self.verify_ssl, self.cert, self.proxy)
101+
) as client:
95102
response = client.post(token_url, data=data)
96-
97103
match response.status_code:
98104
case 200:
99105
token_data = response.json()

src/uipath/_cli/_auth/_portal_service.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
from ._oidc_utils import get_auth_config
1111
from ._utils import (
1212
get_auth_data,
13+
get_httpx_client_kwargs,
1314
get_parsed_token_data,
1415
update_auth_file,
1516
update_env_file,
1617
)
1718

1819
console = ConsoleLogger()
19-
client = httpx.Client(follow_redirects=True, timeout=30.0)
2020

2121

2222
class PortalService:
@@ -27,25 +27,46 @@ class PortalService:
2727
domain: Optional[str] = None
2828
selected_tenant: Optional[str] = None
2929

30+
_client: httpx.Client = None
31+
3032
_tenants_and_organizations: Optional[TenantsAndOrganizationInfoResponse] = None
3133

3234
def __init__(
3335
self,
3436
domain: str,
3537
access_token: Optional[str] = None,
3638
prt_id: Optional[str] = None,
39+
verify_ssl: bool = True,
40+
cert: str = None,
41+
proxy: str = None,
3742
):
3843
self.domain = domain
3944
self.access_token = access_token
4045
self.prt_id = prt_id
4146

47+
self._client = httpx.Client(**get_httpx_client_kwargs(verify_ssl, cert, proxy))
48+
49+
def close(self):
50+
"""Explicitly close the HTTP client."""
51+
if self._client:
52+
self._client.close()
53+
self._client = None
54+
55+
def __enter__(self):
56+
"""Enter the runtime context related to this object."""
57+
return self
58+
59+
def __exit__(self, exc_type, exc_value, traceback):
60+
"""Exit the runtime context and close the HTTP client."""
61+
self.close()
62+
4263
def update_token_data(self, token_data: TokenData):
4364
self.access_token = token_data["access_token"]
4465
self.prt_id = get_parsed_token_data(token_data).get("prt_id")
4566

4667
def get_tenants_and_organizations(self) -> TenantsAndOrganizationInfoResponse:
4768
url = f"https://{self.domain}.uipath.com/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
48-
response = client.get(
69+
response = self._client.get(
4970
url, headers={"Authorization": f"Bearer {self.access_token}"}
5071
)
5172
if response.status_code < 400:
@@ -83,7 +104,7 @@ def post_refresh_token_request(self, refresh_token: str) -> TokenData:
83104

84105
headers = {"Content-Type": "application/x-www-form-urlencoded"}
85106

86-
response = client.post(url, data=data, headers=headers)
107+
response = self._client.post(url, data=data, headers=headers)
87108
if response.status_code < 400:
88109
return response.json()
89110
elif response.status_code == 401:
@@ -148,7 +169,7 @@ def post_auth(self, base_url: str) -> None:
148169

149170
try:
150171
[try_enable_first_run_response, acquire_license_response] = [
151-
client.post(
172+
self._client.post(
152173
url,
153174
headers={"Authorization": f"Bearer {self.access_token}"},
154175
)

src/uipath/_cli/_auth/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,21 @@ def update_env_file(env_contents):
4949
lines = [f"{key}={value}\n" for key, value in env_contents.items()]
5050
with open(env_path, "w") as f:
5151
f.writelines(lines)
52+
53+
54+
def get_httpx_client_kwargs(
55+
verify_ssl: bool = True, cert: str = None, proxy: str = None, **extra_kwargs
56+
) -> dict:
57+
"""Get standardized httpx client configuration."""
58+
client_kwargs = {"follow_redirects": True, "timeout": 30.0}
59+
client_kwargs.update(extra_kwargs)
60+
61+
if not verify_ssl:
62+
client_kwargs["verify"] = False
63+
elif cert:
64+
client_kwargs["verify"] = cert
65+
66+
if proxy:
67+
client_kwargs["proxies"] = proxy
68+
69+
return client_kwargs

src/uipath/_cli/cli_auth.py

Lines changed: 78 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,45 @@ def set_port():
8080
required=False,
8181
help="Base URL for the UiPath tenant instance (required for client credentials)",
8282
)
83+
@click.option(
84+
"--no-verify-ssl",
85+
"--insecure",
86+
is_flag=True,
87+
required=False,
88+
help="Disable SSL certificate verification (not recommended for production)",
89+
)
90+
@click.option(
91+
"--cert",
92+
required=False,
93+
type=click.Path(exists=True),
94+
help="Path to custom CA certificate bundle file (for corporate certificates)",
95+
)
96+
@click.option(
97+
"--proxy",
98+
required=False,
99+
help="Proxy URL in format: http://proxy:port or https://proxy:port",
100+
)
83101
@track
84102
def auth(
85103
domain,
86104
force: None | bool = False,
87105
client_id: str = None,
88106
client_secret: str = None,
89107
base_url: str = None,
108+
no_verify_ssl: bool = False,
109+
cert: str = None,
110+
proxy: str = None,
90111
):
91112
"""Authenticate with UiPath Cloud Platform.
92113
93114
Interactive mode (default): Opens browser for OAuth authentication.
94115
Unattended mode: Use --client-id, --client-secret and --base-url for client credentials flow.
116+
117+
Network options:
118+
- Use --cert to specify custom CA certificates for corporate environments
119+
- Use --no-verify-ssl to disable SSL verification (not recommended)
120+
- Use --proxy to configure proxy settings (e.g., --proxy http://proxy:8080)
121+
- Set HTTP_PROXY/HTTPS_PROXY environment variables for proxy configuration
95122
"""
96123
# Check if client credentials are provided for unattended authentication
97124
if client_id and client_secret:
@@ -102,8 +129,9 @@ def auth(
102129
return
103130

104131
with console.spinner("Authenticating with client credentials ..."):
105-
# Create service instance
106-
credentials_service = ClientCredentialsService(domain)
132+
credentials_service = ClientCredentialsService(
133+
domain, verify_ssl=not no_verify_ssl, cert=cert, proxy=proxy
134+
)
107135

108136
# If base_url is provided, extract domain from it to override the CLI domain parameter
109137
if base_url:
@@ -127,56 +155,59 @@ def auth(
127155

128156
# Interactive authentication flow (existing logic)
129157
with console.spinner("Authenticating with UiPath ..."):
130-
portal_service = PortalService(domain)
131-
132-
if not force:
133-
if (
134-
os.getenv("UIPATH_URL")
135-
and os.getenv("UIPATH_TENANT_ID")
136-
and os.getenv("UIPATH_ORGANIZATION_ID")
137-
):
158+
with PortalService(
159+
domain, verify_ssl=not no_verify_ssl, cert=cert, proxy=proxy
160+
) as portal_service:
161+
if not force:
162+
if (
163+
os.getenv("UIPATH_URL")
164+
and os.getenv("UIPATH_TENANT_ID")
165+
and os.getenv("UIPATH_ORGANIZATION_ID")
166+
):
167+
try:
168+
portal_service.ensure_valid_token()
169+
console.success(
170+
"Authentication successful.",
171+
)
172+
return
173+
except Exception:
174+
console.info(
175+
"Authentication token is invalid. Please reauthenticate.",
176+
)
177+
178+
auth_url, code_verifier, state = get_auth_url(domain)
179+
180+
webbrowser.open(auth_url, 1)
181+
auth_config = get_auth_config()
182+
183+
console.link(
184+
"If a browser window did not open, please open the following URL in your browser:",
185+
auth_url,
186+
)
187+
188+
server = HTTPServer(port=auth_config["port"])
189+
token_data = server.start(state, code_verifier, domain)
190+
191+
if token_data:
192+
portal_service.update_token_data(token_data)
193+
update_auth_file(token_data)
194+
access_token = token_data["access_token"]
195+
update_env_file({"UIPATH_ACCESS_TOKEN": access_token})
196+
197+
tenants_and_organizations = (
198+
portal_service.get_tenants_and_organizations()
199+
)
200+
base_url = select_tenant(domain, tenants_and_organizations)
138201
try:
139-
portal_service.ensure_valid_token()
202+
portal_service.post_auth(base_url)
140203
console.success(
141204
"Authentication successful.",
142205
)
143-
return
144206
except Exception:
145-
console.info(
146-
"Authentication token is invalid. Please reauthenticate.",
207+
console.error(
208+
"Could not prepare the environment. Please try again.",
147209
)
148-
149-
auth_url, code_verifier, state = get_auth_url(domain)
150-
151-
webbrowser.open(auth_url, 1)
152-
auth_config = get_auth_config()
153-
154-
console.link(
155-
"If a browser window did not open, please open the following URL in your browser:",
156-
auth_url,
157-
)
158-
159-
server = HTTPServer(port=auth_config["port"])
160-
token_data = server.start(state, code_verifier, domain)
161-
162-
if token_data:
163-
portal_service.update_token_data(token_data)
164-
update_auth_file(token_data)
165-
access_token = token_data["access_token"]
166-
update_env_file({"UIPATH_ACCESS_TOKEN": access_token})
167-
168-
tenants_and_organizations = portal_service.get_tenants_and_organizations()
169-
base_url = select_tenant(domain, tenants_and_organizations)
170-
try:
171-
portal_service.post_auth(base_url)
172-
console.success(
173-
"Authentication successful.",
174-
)
175-
except Exception:
210+
else:
176211
console.error(
177-
"Could not prepare the environment. Please try again.",
212+
"Authentication failed. Please try again.",
178213
)
179-
else:
180-
console.error(
181-
"Authentication failed. Please try again.",
182-
)

0 commit comments

Comments
 (0)