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.

@@ -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,101 @@ 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
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.post_auth(base_url)
115
+ portal_service.ensure_valid_token()
116
+ return True
127
117
  except Exception:
128
118
  self._console.error(
129
- "Could not prepare the environment. Please try again.",
119
+ "Authentication token is invalid. Please reauthenticate using the '--force' flag."
130
120
  )
121
+ return False
131
122
 
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
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
- 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)
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
+ )
@@ -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,19 @@ class OidcUtils:
32
49
  ) as f:
33
50
  auth_config = json.load(f)
34
51
 
35
- port = auth_config.get("port", 8104)
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)