uipath 2.1.79__py3-none-any.whl → 2.1.81__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uipath might be problematic. Click here for more details.
- uipath/_cli/_auth/_auth_service.py +89 -109
- uipath/_cli/_auth/_models.py +0 -11
- uipath/_cli/_auth/_oidc_utils.py +30 -1
- uipath/_cli/_auth/_portal_service.py +125 -157
- uipath/_cli/_auth/_url_utils.py +61 -27
- uipath/_cli/_auth/_utils.py +6 -29
- uipath/_cli/_evals/_models/_output.py +17 -14
- uipath/_cli/_evals/_runtime.py +203 -102
- uipath/_cli/_runtime/_runtime.py +3 -1
- uipath/_cli/_utils/_common.py +3 -3
- uipath/_cli/cli_auth.py +2 -2
- uipath/_cli/cli_eval.py +2 -2
- uipath/_services/__init__.py +2 -0
- uipath/{_cli/_auth/_client_credentials.py → _services/external_application_service.py} +49 -54
- uipath/_uipath.py +9 -15
- uipath/_utils/_auth.py +82 -0
- uipath/models/auth.py +14 -0
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/METADATA +1 -1
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/RECORD +22 -20
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/WHEEL +0 -0
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.79.dist-info → uipath-2.1.81.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import json
|
|
3
2
|
import os
|
|
4
3
|
import webbrowser
|
|
5
|
-
from socket import AF_INET, SOCK_STREAM, error, socket
|
|
6
4
|
from typing import Optional
|
|
7
|
-
from urllib.parse import urlparse
|
|
8
5
|
|
|
9
6
|
from uipath._cli._auth._auth_server import HTTPServer
|
|
10
|
-
from uipath._cli._auth._client_credentials import ClientCredentialsService
|
|
11
7
|
from uipath._cli._auth._oidc_utils import OidcUtils
|
|
12
8
|
from uipath._cli._auth._portal_service import (
|
|
13
9
|
PortalService,
|
|
14
|
-
get_tenant_id,
|
|
15
|
-
select_tenant,
|
|
16
10
|
)
|
|
17
|
-
from uipath._cli._auth._url_utils import
|
|
18
|
-
from uipath._cli._auth._utils import
|
|
11
|
+
from uipath._cli._auth._url_utils import extract_org_tenant, resolve_domain
|
|
12
|
+
from uipath._cli._auth._utils import get_parsed_token_data
|
|
19
13
|
from uipath._cli._utils._console import ConsoleLogger
|
|
14
|
+
from uipath._services import ExternalApplicationService
|
|
15
|
+
from uipath._utils._auth import update_env_file
|
|
16
|
+
from uipath.models.auth import TokenData
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
class AuthService:
|
|
@@ -25,35 +22,20 @@ class AuthService:
|
|
|
25
22
|
environment: str,
|
|
26
23
|
*,
|
|
27
24
|
force: bool,
|
|
28
|
-
client_id: Optional[str],
|
|
29
|
-
client_secret: Optional[str],
|
|
30
|
-
base_url: Optional[str],
|
|
31
|
-
tenant: Optional[str],
|
|
32
|
-
scope: Optional[str],
|
|
25
|
+
client_id: Optional[str] = None,
|
|
26
|
+
client_secret: Optional[str] = None,
|
|
27
|
+
base_url: Optional[str] = None,
|
|
28
|
+
tenant: Optional[str] = None,
|
|
29
|
+
scope: Optional[str] = None,
|
|
33
30
|
):
|
|
34
31
|
self._force = force
|
|
35
32
|
self._console = ConsoleLogger()
|
|
36
|
-
self._domain = self._get_domain(environment)
|
|
37
33
|
self._client_id = client_id
|
|
38
34
|
self._client_secret = client_secret
|
|
39
35
|
self._base_url = base_url
|
|
40
36
|
self._tenant = tenant
|
|
37
|
+
self._domain = resolve_domain(self._base_url, environment, self._force)
|
|
41
38
|
self._scope = scope
|
|
42
|
-
set_force_flag(self._force)
|
|
43
|
-
|
|
44
|
-
def _get_domain(self, environment: str) -> str:
|
|
45
|
-
# only search env var if not force authentication
|
|
46
|
-
if not self._force:
|
|
47
|
-
uipath_url = os.getenv("UIPATH_URL")
|
|
48
|
-
if uipath_url and environment == "cloud": # "cloud" is the default
|
|
49
|
-
parsed_url = urlparse(uipath_url)
|
|
50
|
-
if parsed_url.scheme and parsed_url.netloc:
|
|
51
|
-
environment = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
52
|
-
else:
|
|
53
|
-
self._console.error(
|
|
54
|
-
f"Malformed UIPATH_URL: '{uipath_url}'. Please ensure it includes both scheme and netloc (e.g., 'https://cloud.uipath.com')."
|
|
55
|
-
)
|
|
56
|
-
return environment
|
|
57
39
|
|
|
58
40
|
def authenticate(self) -> None:
|
|
59
41
|
if self._client_id and self._client_secret:
|
|
@@ -62,103 +44,101 @@ class AuthService:
|
|
|
62
44
|
|
|
63
45
|
self._authenticate_authorization_code()
|
|
64
46
|
|
|
65
|
-
def _authenticate_client_credentials(self)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"--base-url is required when using client credentials authentication."
|
|
69
|
-
)
|
|
70
|
-
return
|
|
71
|
-
self._console.hint("Using client credentials authentication.")
|
|
72
|
-
credentials_service = ClientCredentialsService(self._base_url)
|
|
73
|
-
credentials_service.authenticate(
|
|
47
|
+
def _authenticate_client_credentials(self):
|
|
48
|
+
external_app_service = ExternalApplicationService(self._base_url)
|
|
49
|
+
token_data = external_app_service.get_token_data(
|
|
74
50
|
self._client_id, # type: ignore
|
|
75
51
|
self._client_secret, # type: ignore
|
|
76
52
|
self._scope,
|
|
77
53
|
)
|
|
78
54
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
and os.getenv("UIPATH_ORGANIZATION_ID")
|
|
87
|
-
):
|
|
88
|
-
try:
|
|
89
|
-
portal_service.ensure_valid_token()
|
|
90
|
-
return
|
|
91
|
-
except Exception:
|
|
92
|
-
self._console.error(
|
|
93
|
-
"Authentication token is invalid. Please reauthenticate using the '--force' flag.",
|
|
94
|
-
)
|
|
95
|
-
auth_url, code_verifier, state = OidcUtils.get_auth_url(self._domain)
|
|
96
|
-
webbrowser.open(auth_url, 1)
|
|
97
|
-
auth_config = OidcUtils.get_auth_config()
|
|
98
|
-
|
|
99
|
-
self._console.link(
|
|
100
|
-
"If a browser window did not open, please open the following URL in your browser:",
|
|
101
|
-
auth_url,
|
|
55
|
+
organization_name, tenant_name = extract_org_tenant(
|
|
56
|
+
external_app_service._base_url
|
|
57
|
+
)
|
|
58
|
+
if not (organization_name and tenant_name):
|
|
59
|
+
self._console.warning(
|
|
60
|
+
"--base-url should include both organization and tenant, "
|
|
61
|
+
"e.g., 'https://cloud.uipath.com/{organization}/{tenant}'."
|
|
102
62
|
)
|
|
103
|
-
server = HTTPServer(port=auth_config["port"])
|
|
104
|
-
token_data = asyncio.run(server.start(state, code_verifier, self._domain))
|
|
105
63
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
64
|
+
env_vars = {
|
|
65
|
+
"UIPATH_ACCESS_TOKEN": token_data.access_token,
|
|
66
|
+
"UIPATH_URL": external_app_service._base_url,
|
|
67
|
+
"UIPATH_ORGANIZATION_ID": get_parsed_token_data(token_data).get("prt_id"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if tenant_name:
|
|
71
|
+
self._tenant = tenant_name
|
|
72
|
+
with PortalService(
|
|
73
|
+
self._domain, access_token=token_data.access_token
|
|
74
|
+
) as portal_service:
|
|
75
|
+
tenant_info = portal_service.resolve_tenant_info(self._tenant)
|
|
76
|
+
env_vars["UIPATH_TENANT_ID"] = tenant_info["tenant_id"]
|
|
77
|
+
else:
|
|
78
|
+
self._console.warning("Could not extract tenant from --base-url.")
|
|
79
|
+
update_env_file(env_vars)
|
|
80
|
+
|
|
81
|
+
def _authenticate_authorization_code(self) -> None:
|
|
82
|
+
with PortalService(self._domain) as portal_service:
|
|
83
|
+
if not self._force and self._can_reuse_existing_token(portal_service):
|
|
84
|
+
return
|
|
110
85
|
|
|
86
|
+
token_data = self._perform_oauth_flow()
|
|
111
87
|
portal_service.update_token_data(token_data)
|
|
112
|
-
update_auth_file(token_data)
|
|
113
|
-
access_token = token_data["access_token"]
|
|
114
|
-
update_env_file({"UIPATH_ACCESS_TOKEN": access_token})
|
|
115
88
|
|
|
116
|
-
|
|
89
|
+
tenant_info = portal_service.resolve_tenant_info(self._tenant)
|
|
90
|
+
uipath_url = portal_service.build_tenant_url()
|
|
117
91
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
92
|
+
update_env_file(
|
|
93
|
+
{
|
|
94
|
+
"UIPATH_ACCESS_TOKEN": token_data.access_token,
|
|
95
|
+
"UIPATH_URL": uipath_url,
|
|
96
|
+
"UIPATH_TENANT_ID": tenant_info["tenant_id"],
|
|
97
|
+
"UIPATH_ORGANIZATION_ID": tenant_info["organization_id"],
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
portal_service.enable_studio_web(uipath_url)
|
|
103
|
+
except Exception:
|
|
104
|
+
self._console.error(
|
|
105
|
+
"Could not prepare the environment. Please try again."
|
|
121
106
|
)
|
|
122
|
-
else:
|
|
123
|
-
base_url = select_tenant(self._domain, tenants_and_organizations)
|
|
124
107
|
|
|
108
|
+
def _can_reuse_existing_token(self, portal_service: PortalService) -> bool:
|
|
109
|
+
if (
|
|
110
|
+
os.getenv("UIPATH_URL")
|
|
111
|
+
and os.getenv("UIPATH_TENANT_ID")
|
|
112
|
+
and os.getenv("UIPATH_ORGANIZATION_ID")
|
|
113
|
+
):
|
|
125
114
|
try:
|
|
126
|
-
portal_service.
|
|
115
|
+
portal_service.ensure_valid_token()
|
|
116
|
+
return True
|
|
127
117
|
except Exception:
|
|
128
118
|
self._console.error(
|
|
129
|
-
"
|
|
119
|
+
"Authentication token is invalid. Please reauthenticate using the '--force' flag."
|
|
130
120
|
)
|
|
121
|
+
return False
|
|
131
122
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
s.bind(("localhost", target_port))
|
|
137
|
-
s.close()
|
|
138
|
-
return False
|
|
139
|
-
except error:
|
|
140
|
-
return True
|
|
123
|
+
def _perform_oauth_flow(self) -> TokenData:
|
|
124
|
+
auth_url, code_verifier, state = OidcUtils.get_auth_url(self._domain)
|
|
125
|
+
self._open_browser(auth_url)
|
|
141
126
|
|
|
142
127
|
auth_config = OidcUtils.get_auth_config()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
auth_config["port"] = port
|
|
161
|
-
with open(
|
|
162
|
-
os.path.join(os.path.dirname(__file__), "..", "auth_config.json"), "w"
|
|
163
|
-
) as f:
|
|
164
|
-
json.dump(auth_config, f)
|
|
128
|
+
server = HTTPServer(port=auth_config["port"])
|
|
129
|
+
token_data = asyncio.run(server.start(state, code_verifier, self._domain))
|
|
130
|
+
|
|
131
|
+
if not token_data:
|
|
132
|
+
self._console.error(
|
|
133
|
+
"Authentication failed. Please try again.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return TokenData.model_validate(token_data)
|
|
137
|
+
|
|
138
|
+
def _open_browser(self, url: str) -> None:
|
|
139
|
+
# Try to open browser. Always print the fallback link.
|
|
140
|
+
webbrowser.open(url, new=1)
|
|
141
|
+
self._console.link(
|
|
142
|
+
"If a browser window did not open, please open the following URL in your browser:",
|
|
143
|
+
url,
|
|
144
|
+
)
|
uipath/_cli/_auth/_models.py
CHANGED
|
@@ -10,17 +10,6 @@ class AuthConfig(TypedDict):
|
|
|
10
10
|
scope: str
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class TokenData(TypedDict):
|
|
14
|
-
"""TypedDict for token data structure."""
|
|
15
|
-
|
|
16
|
-
access_token: str
|
|
17
|
-
refresh_token: str
|
|
18
|
-
expires_in: int
|
|
19
|
-
token_type: str
|
|
20
|
-
scope: str
|
|
21
|
-
id_token: str
|
|
22
|
-
|
|
23
|
-
|
|
24
13
|
class AccessTokenData(TypedDict):
|
|
25
14
|
"""TypedDict for access token data structure."""
|
|
26
15
|
|
uipath/_cli/_auth/_oidc_utils.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
from urllib.parse import urlencode
|
|
6
6
|
|
|
7
|
+
from .._utils._console import ConsoleLogger
|
|
7
8
|
from ._models import AuthConfig
|
|
8
9
|
from ._url_utils import build_service_url
|
|
9
10
|
|
|
@@ -25,6 +26,22 @@ def get_state_param() -> str:
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class OidcUtils:
|
|
29
|
+
_console = ConsoleLogger()
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _find_free_port(cls, candidates: list[int]):
|
|
33
|
+
from socket import AF_INET, SOCK_STREAM, error, socket
|
|
34
|
+
|
|
35
|
+
def is_free(port: int) -> bool:
|
|
36
|
+
with socket(AF_INET, SOCK_STREAM) as s:
|
|
37
|
+
try:
|
|
38
|
+
s.bind(("localhost", port))
|
|
39
|
+
return True
|
|
40
|
+
except error:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
return next((p for p in candidates if is_free(p)), None)
|
|
44
|
+
|
|
28
45
|
@classmethod
|
|
29
46
|
def get_auth_config(cls) -> AuthConfig:
|
|
30
47
|
with open(
|
|
@@ -32,7 +49,19 @@ class OidcUtils:
|
|
|
32
49
|
) as f:
|
|
33
50
|
auth_config = json.load(f)
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
candidates = [
|
|
53
|
+
int(auth_config.get("port", 8104)),
|
|
54
|
+
int(auth_config.get("portOptionOne", 8104)),
|
|
55
|
+
int(auth_config.get("portOptionTwo", 8055)),
|
|
56
|
+
int(auth_config.get("portOptionThree", 42042)),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
port = cls._find_free_port(candidates)
|
|
60
|
+
if port is None:
|
|
61
|
+
ports_str = ", ".join(str(p) for p in candidates)
|
|
62
|
+
cls._console.error(
|
|
63
|
+
f"All configured ports ({ports_str}) are in use. Please close applications using these ports or configure different ports."
|
|
64
|
+
)
|
|
36
65
|
|
|
37
66
|
redirect_uri = auth_config["redirect_uri"].replace(
|
|
38
67
|
"__PY_REPLACE_PORT__", str(port)
|