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.

@@ -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(state, code_verifier, token_callback, domain):
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, code_verifier, self.token_received_callback, domain
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 set_force_flag
18
- from uipath._cli._auth._utils import update_auth_file, update_env_file
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) -> None:
66
- if not self._base_url:
67
- self._console.error(
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
- def _authenticate_authorization_code(self) -> None:
80
- with PortalService(self._domain) as portal_service:
81
- if not self._force:
82
- # use existing env vars
83
- if (
84
- os.getenv("UIPATH_URL")
85
- and os.getenv("UIPATH_TENANT_ID")
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
- if not token_data:
107
- self._console.error(
108
- "Authentication failed. Please try again.",
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
- tenants_and_organizations = portal_service.get_tenants_and_organizations()
89
+ tenant_info = portal_service.resolve_tenant_info(self._tenant)
90
+ uipath_url = portal_service.build_tenant_url()
117
91
 
118
- if self._tenant:
119
- base_url = get_tenant_id(
120
- self._domain, self._tenant, tenants_and_organizations
121
- )
122
- else:
123
- base_url = select_tenant(self._domain, tenants_and_organizations)
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.post_auth(base_url)
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 set_port(self):
133
- def is_port_in_use(target_port: int) -> bool:
134
- with socket(AF_INET, SOCK_STREAM) as s:
135
- try:
136
- s.bind(("localhost", target_port))
137
- s.close()
138
- return False
139
- except error:
140
- return True
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
- port = int(auth_config.get("port", 8104))
144
- port_option_one = int(auth_config.get("portOptionOne", 8104)) # type: ignore
145
- port_option_two = int(auth_config.get("portOptionTwo", 8055)) # type: ignore
146
- port_option_three = int(auth_config.get("portOptionThree", 42042)) # type: ignore
147
- if is_port_in_use(port):
148
- if is_port_in_use(port_option_one):
149
- if is_port_in_use(port_option_two):
150
- if is_port_in_use(port_option_three):
151
- self._console.error(
152
- "All configured ports are in use. Please close applications using ports or configure different ports."
153
- )
154
- else:
155
- port = port_option_three
156
- else:
157
- port = port_option_two
158
- else:
159
- port = port_option_one
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)
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
+ )
@@ -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
 
@@ -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
- port = auth_config.get("port", 8104)
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"],