uipath 2.1.80__py3-none-any.whl → 2.1.82__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_server.py +15 -12
- uipath/_cli/_auth/_auth_service.py +96 -110
- uipath/_cli/_auth/_models.py +0 -11
- uipath/_cli/_auth/_oidc_utils.py +28 -3
- 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/_utils/_common.py +3 -3
- uipath/_cli/cli_auth.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.80.dist-info → uipath-2.1.82.dist-info}/METADATA +1 -1
- {uipath-2.1.80.dist-info → uipath-2.1.82.dist-info}/RECORD +19 -17
- {uipath-2.1.80.dist-info → uipath-2.1.82.dist-info}/WHEEL +0 -0
- {uipath-2.1.80.dist-info → uipath-2.1.82.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.80.dist-info → uipath-2.1.82.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,8 +7,6 @@ import threading
|
|
|
7
7
|
import time
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from ._oidc_utils import OidcUtils
|
|
11
|
-
|
|
12
10
|
# Server port
|
|
13
11
|
PORT = 6234
|
|
14
12
|
|
|
@@ -22,7 +20,9 @@ class TokenReceivedSignal(Exception):
|
|
|
22
20
|
super().__init__("Token received successfully")
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
def make_request_handler_class(
|
|
23
|
+
def make_request_handler_class(
|
|
24
|
+
state, code_verifier, token_callback, domain, redirect_uri, client_id
|
|
25
|
+
):
|
|
26
26
|
class SimpleHTTPSRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
27
27
|
"""Simple HTTPS request handler that serves static files."""
|
|
28
28
|
|
|
@@ -73,16 +73,10 @@ def make_request_handler_class(state, code_verifier, token_callback, domain):
|
|
|
73
73
|
with open(index_path, "r") as f:
|
|
74
74
|
content = f.read()
|
|
75
75
|
|
|
76
|
-
# Get the redirect URI from auth config
|
|
77
|
-
auth_config = OidcUtils.get_auth_config()
|
|
78
|
-
redirect_uri = auth_config["redirect_uri"]
|
|
79
|
-
|
|
80
76
|
content = content.replace("__PY_REPLACE_EXPECTED_STATE__", state)
|
|
81
77
|
content = content.replace("__PY_REPLACE_CODE_VERIFIER__", code_verifier)
|
|
82
78
|
content = content.replace("__PY_REPLACE_REDIRECT_URI__", redirect_uri)
|
|
83
|
-
content = content.replace(
|
|
84
|
-
"__PY_REPLACE_CLIENT_ID__", auth_config["client_id"]
|
|
85
|
-
)
|
|
79
|
+
content = content.replace("__PY_REPLACE_CLIENT_ID__", client_id)
|
|
86
80
|
content = content.replace("__PY_REPLACE_DOMAIN__", domain)
|
|
87
81
|
|
|
88
82
|
self.send_response(200)
|
|
@@ -107,14 +101,18 @@ def make_request_handler_class(state, code_verifier, token_callback, domain):
|
|
|
107
101
|
|
|
108
102
|
|
|
109
103
|
class HTTPServer:
|
|
110
|
-
def __init__(self, port=6234):
|
|
104
|
+
def __init__(self, port=6234, redirect_uri=None, client_id=None):
|
|
111
105
|
"""Initialize HTTP server with configurable parameters.
|
|
112
106
|
|
|
113
107
|
Args:
|
|
114
108
|
port (int, optional): Port number to run the server on. Defaults to 6234.
|
|
109
|
+
redirect_uri (str, optional): OAuth redirect URI. Defaults to None.
|
|
110
|
+
client_id (str, optional): OAuth client ID. Defaults to None.
|
|
115
111
|
"""
|
|
116
112
|
self.current_path = os.path.dirname(os.path.abspath(__file__))
|
|
117
113
|
self.port = port
|
|
114
|
+
self.redirect_uri = redirect_uri
|
|
115
|
+
self.client_id = client_id
|
|
118
116
|
self.httpd: Optional[socketserver.TCPServer] = None
|
|
119
117
|
self.token_data = None
|
|
120
118
|
self.should_shutdown = False
|
|
@@ -145,7 +143,12 @@ class HTTPServer:
|
|
|
145
143
|
# Create server with address reuse
|
|
146
144
|
socketserver.TCPServer.allow_reuse_address = True
|
|
147
145
|
handler = make_request_handler_class(
|
|
148
|
-
state,
|
|
146
|
+
state,
|
|
147
|
+
code_verifier,
|
|
148
|
+
self.token_received_callback,
|
|
149
|
+
domain,
|
|
150
|
+
self.redirect_uri,
|
|
151
|
+
self.client_id,
|
|
149
152
|
)
|
|
150
153
|
self.httpd = socketserver.TCPServer(("", self.port), handler)
|
|
151
154
|
return self.httpd
|
|
@@ -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,107 @@ 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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
)
|
|
124
100
|
|
|
125
101
|
try:
|
|
126
|
-
portal_service.
|
|
102
|
+
portal_service.enable_studio_web(uipath_url)
|
|
127
103
|
except Exception:
|
|
128
104
|
self._console.error(
|
|
129
|
-
"Could not prepare the environment. Please try again."
|
|
105
|
+
"Could not prepare the environment. Please try again."
|
|
130
106
|
)
|
|
131
107
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
):
|
|
114
|
+
try:
|
|
115
|
+
portal_service.ensure_valid_token()
|
|
116
|
+
return True
|
|
117
|
+
except Exception:
|
|
118
|
+
self._console.error(
|
|
119
|
+
"Authentication token is invalid. Please reauthenticate using the '--force' flag."
|
|
120
|
+
)
|
|
121
|
+
return False
|
|
141
122
|
|
|
123
|
+
def _perform_oauth_flow(self) -> TokenData:
|
|
142
124
|
auth_config = OidcUtils.get_auth_config()
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
125
|
+
auth_url, code_verifier, state = OidcUtils.get_auth_url(
|
|
126
|
+
self._domain, auth_config
|
|
127
|
+
)
|
|
128
|
+
self._open_browser(auth_url)
|
|
129
|
+
|
|
130
|
+
server = HTTPServer(
|
|
131
|
+
port=auth_config["port"],
|
|
132
|
+
redirect_uri=auth_config["redirect_uri"],
|
|
133
|
+
client_id=auth_config["client_id"],
|
|
134
|
+
)
|
|
135
|
+
token_data = asyncio.run(server.start(state, code_verifier, self._domain))
|
|
136
|
+
|
|
137
|
+
if not token_data:
|
|
138
|
+
self._console.error(
|
|
139
|
+
"Authentication failed. Please try again.",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return TokenData.model_validate(token_data)
|
|
143
|
+
|
|
144
|
+
def _open_browser(self, url: str) -> None:
|
|
145
|
+
# Try to open browser. Always print the fallback link.
|
|
146
|
+
webbrowser.open(url, new=1)
|
|
147
|
+
self._console.link(
|
|
148
|
+
"If a browser window did not open, please open the following URL in your browser:",
|
|
149
|
+
url,
|
|
150
|
+
)
|
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,15 @@ class OidcUtils:
|
|
|
32
49
|
) as f:
|
|
33
50
|
auth_config = json.load(f)
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
custom_port = os.getenv("UIPATH_AUTH_PORT")
|
|
53
|
+
candidates = [int(custom_port)] if custom_port else [8104, 8055, 42042]
|
|
54
|
+
|
|
55
|
+
port = cls._find_free_port(candidates)
|
|
56
|
+
if port is None:
|
|
57
|
+
ports_str = ", ".join(str(p) for p in candidates)
|
|
58
|
+
cls._console.error(
|
|
59
|
+
f"All configured ports ({ports_str}) are in use. Please close applications using these ports or configure different ports."
|
|
60
|
+
)
|
|
36
61
|
|
|
37
62
|
redirect_uri = auth_config["redirect_uri"].replace(
|
|
38
63
|
"__PY_REPLACE_PORT__", str(port)
|
|
@@ -46,11 +71,12 @@ class OidcUtils:
|
|
|
46
71
|
)
|
|
47
72
|
|
|
48
73
|
@classmethod
|
|
49
|
-
def get_auth_url(cls, domain: str) -> tuple[str, str, str]:
|
|
74
|
+
def get_auth_url(cls, domain: str, auth_config: AuthConfig) -> tuple[str, str, str]:
|
|
50
75
|
"""Get the authorization URL for OAuth2 PKCE flow.
|
|
51
76
|
|
|
52
77
|
Args:
|
|
53
78
|
domain (str): The UiPath domain to authenticate against (e.g. 'alpha', 'cloud')
|
|
79
|
+
auth_config (AuthConfig): The authentication configuration to use
|
|
54
80
|
|
|
55
81
|
Returns:
|
|
56
82
|
tuple[str, str]: A tuple containing:
|
|
@@ -58,7 +84,6 @@ class OidcUtils:
|
|
|
58
84
|
- The code verifier for PKCE flow
|
|
59
85
|
"""
|
|
60
86
|
code_verifier, code_challenge = generate_code_verifier_and_challenge()
|
|
61
|
-
auth_config = cls.get_auth_config()
|
|
62
87
|
state = get_state_param()
|
|
63
88
|
query_params = {
|
|
64
89
|
"client_id": auth_config["client_id"],
|