Skip to content

Commit 6d3e5aa

Browse files
committed
feat(auth): add support for custom urls
1 parent 9f88719 commit 6d3e5aa

File tree

5 files changed

+150
-5
lines changed

5 files changed

+150
-5
lines changed

src/uipath/_cli/_auth/_oidc_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,8 @@ def get_auth_url(domain: str) -> tuple[str, str, str]:
6565
}
6666

6767
query_string = urlencode(query_params)
68-
url = f"https://{domain}.uipath.com/identity_/connect/authorize?{query_string}"
68+
if domain.startswith("http"):
69+
url = f"{domain}/identity_/connect/authorize?{query_string}"
70+
else:
71+
url = f"https://{domain}.uipath.com/identity_/connect/authorize?{query_string}"
6972
return url, code_verifier, state

src/uipath/_cli/_auth/_portal_service.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ def get_tenants_and_organizations(self) -> TenantsAndOrganizationInfoResponse:
6565
if self._client is None:
6666
raise RuntimeError("HTTP client is not initialized")
6767

68-
url = f"https://{self.domain}.uipath.com/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
68+
if self.domain.startswith("http"):
69+
url = f"{self.domain}/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
70+
else:
71+
url = f"https://{self.domain}.uipath.com/{self.prt_id}/portal_/api/filtering/leftnav/tenantsAndOrganizationInfo"
6972
response = self._client.get(
7073
url, headers={"Authorization": f"Bearer {self.access_token}"}
7174
)
@@ -213,12 +216,19 @@ def select_tenant(
213216
account_name = tenants_and_organizations["organization"]["name"]
214217
console.info(f"Selected tenant: {click.style(tenant_name, fg='cyan')}")
215218

219+
if domain.startswith("http"):
220+
base_url = domain
221+
else:
222+
base_url = f"https://{domain if domain else 'cloud'}.uipath.com"
223+
224+
uipath_url = f"{base_url}/{account_name}/{tenant_name}"
225+
216226
update_env_file(
217227
{
218-
"UIPATH_URL": f"https://{domain if domain else 'alpha'}.uipath.com/{account_name}/{tenant_name}",
228+
"UIPATH_URL": uipath_url,
219229
"UIPATH_TENANT_ID": tenants_and_organizations["tenants"][tenant_idx]["id"],
220230
"UIPATH_ORGANIZATION_ID": tenants_and_organizations["organization"]["id"],
221231
}
222232
)
223233

224-
return f"https://{domain if domain else 'alpha'}.uipath.com/{account_name}/{tenant_name}"
234+
return uipath_url

src/uipath/_cli/_auth/index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,12 @@ <h1 class="auth-title" id="main-title">Authenticate CLI</h1>
477477
formData.append('client_id', '__PY_REPLACE_CLIENT_ID__');
478478
formData.append('code_verifier', codeVerifier);
479479

480-
const response = await fetch('https://__PY_REPLACE_DOMAIN__.uipath.com/identity_/connect/token', {
480+
const domain = '__PY_REPLACE_DOMAIN__';
481+
const tokenUrl = domain.startsWith('http')
482+
? `${domain}/identity_/connect/token`
483+
: `https://${domain}.uipath.com/identity_/connect/token`;
484+
485+
const response = await fetch(tokenUrl, {
481486
method: 'POST',
482487
headers: {
483488
'Content-Type': 'application/x-www-form-urlencoded'

src/uipath/_cli/cli_auth.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import socket
66
import webbrowser
7+
from urllib.parse import urlparse
78

89
import click
910
from dotenv import load_dotenv
@@ -91,6 +92,9 @@ def auth(
9192
):
9293
"""Authenticate with UiPath Cloud Platform.
9394
95+
The domain for authentication is determined by the UIPATH_URL environment variable if set.
96+
Otherwise, it can be specified with --cloud (default), --staging, or --alpha flags.
97+
9498
Interactive mode (default): Opens browser for OAuth authentication.
9599
Unattended mode: Use --client-id, --client-secret and --base-url for client credentials flow.
96100
@@ -99,6 +103,11 @@ def auth(
99103
- Set REQUESTS_CA_BUNDLE to specify a custom CA bundle for SSL verification
100104
- Set UIPATH_DISABLE_SSL_VERIFY to disable SSL verification (not recommended)
101105
"""
106+
uipath_url = os.getenv("UIPATH_URL")
107+
if uipath_url and domain == "cloud": # "cloud" is the default
108+
parsed_url = urlparse(uipath_url)
109+
domain = f"{parsed_url.scheme}://{parsed_url.netloc}"
110+
102111
# Check if client credentials are provided for unattended authentication
103112
if client_id and client_secret:
104113
if not base_url:

tests/cli/test_auth.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import os
2+
from unittest.mock import AsyncMock, patch
3+
4+
import pytest
5+
from click.testing import CliRunner
6+
7+
from uipath._cli.cli_auth import auth
8+
9+
"""
10+
Unit tests for the 'uipath auth' command.
11+
12+
This test suite covers the following scenarios for the authentication logic:
13+
14+
1. **UIPATH_URL Environment Variable**:
15+
Ensures the `auth` command correctly uses the domain from the `UIPATH_URL`
16+
environment variable when no specific environment flag is used.
17+
18+
2. **--alpha Flag**:
19+
Verifies that the `--alpha` flag correctly uses the 'alpha' environment and
20+
overrides the `UIPATH_URL` environment variable if it is set.
21+
22+
3. **--staging Flag**:
23+
Verifies that the `--staging` flag correctly uses the 'staging' environment and
24+
overrides the `UIPATH_URL` environment variable if it is set.
25+
26+
4. **--cloud Flag**:
27+
Checks that the `--cloud` flag works as expected, using the 'cloud' environment,
28+
when no `UIPATH_URL` environment variable is set.
29+
30+
5. **Default Behavior**:
31+
Confirms that the command defaults to the 'cloud' environment when no flags
32+
or environment variables are provided.
33+
"""
34+
35+
36+
@pytest.mark.parametrize(
37+
"scenario_name, cli_args, env_vars, expected_url_part, expected_select_tenant_return",
38+
[
39+
(
40+
"auth_with_uipath_url_env_variable",
41+
["--force"],
42+
{"UIPATH_URL": "https://custom.automationsuite.org/org/tenant"},
43+
"https://custom.automationsuite.org/identity_/connect/authorize",
44+
"https://custom.automationsuite.org/DefaultOrg/DefaultTenant",
45+
),
46+
(
47+
"auth_with_alpha_flag",
48+
["--alpha", "--force"],
49+
{"UIPATH_URL": "https://custom.uipath.com/org/tenant"},
50+
"https://alpha.uipath.com/identity_/connect/authorize",
51+
"https://alpha.uipath.com/DefaultOrg/DefaultTenant",
52+
),
53+
(
54+
"auth_with_staging_flag",
55+
["--staging", "--force"],
56+
{"UIPATH_URL": "https://custom.uipath.com/org/tenant"},
57+
"https://staging.uipath.com/identity_/connect/authorize",
58+
"https://staging.uipath.com/DefaultOrg/DefaultTenant",
59+
),
60+
(
61+
"auth_with_cloud_flag",
62+
["--cloud", "--force"],
63+
{},
64+
"https://cloud.uipath.com/identity_/connect/authorize",
65+
"https://cloud.uipath.com/DefaultOrg/DefaultTenant",
66+
),
67+
(
68+
"auth_default_to_cloud",
69+
["--force"],
70+
{},
71+
"https://cloud.uipath.com/identity_/connect/authorize",
72+
"https://cloud.uipath.com/DefaultOrg/DefaultTenant",
73+
),
74+
],
75+
ids=[
76+
"uipath_url_env",
77+
"alpha_flag_overrides_env",
78+
"staging_flag_overrides_env",
79+
"cloud_flag",
80+
"default_to_cloud",
81+
],
82+
)
83+
def test_auth_scenarios(
84+
scenario_name, cli_args, env_vars, expected_url_part, expected_select_tenant_return
85+
):
86+
"""
87+
Test 'uipath auth' with different configurations.
88+
"""
89+
runner = CliRunner()
90+
with (
91+
patch("uipath._cli.cli_auth.webbrowser.open") as mock_open,
92+
patch("uipath._cli.cli_auth.HTTPServer") as mock_server,
93+
patch("uipath._cli.cli_auth.PortalService") as mock_portal_service,
94+
):
95+
mock_server.return_value.start = AsyncMock(
96+
return_value={"access_token": "test_token"}
97+
)
98+
mock_portal_service.return_value.__enter__.return_value.get_tenants_and_organizations.return_value = {
99+
"tenants": [{"name": "DefaultTenant", "id": "tenant-id"}],
100+
"organization": {"name": "DefaultOrg", "id": "org-id"},
101+
}
102+
mock_portal_service.return_value.__enter__.return_value.select_tenant.return_value = expected_select_tenant_return
103+
104+
with runner.isolated_filesystem():
105+
for key, value in env_vars.items():
106+
os.environ[key] = value
107+
108+
result = runner.invoke(auth, cli_args)
109+
110+
for key in env_vars:
111+
del os.environ[key]
112+
113+
assert result.exit_code == 0, (
114+
f"Scenario '{scenario_name}' failed with exit code {result.exit_code}: {result.output}"
115+
)
116+
mock_open.assert_called_once()
117+
call_args = mock_open.call_args[0][0]
118+
assert expected_url_part in call_args

0 commit comments

Comments
 (0)