remotivelabs-cli 0.0.42__py3-none-any.whl → 0.1.0a2__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.
- cli/.DS_Store +0 -0
- cli/api/cloud/tokens.py +62 -0
- cli/broker/brokers.py +0 -1
- cli/broker/export.py +4 -4
- cli/broker/lib/broker.py +9 -13
- cli/broker/license_flows.py +1 -1
- cli/broker/scripting.py +2 -1
- cli/broker/signals.py +9 -10
- cli/cloud/auth/cmd.py +37 -13
- cli/cloud/auth/login.py +279 -24
- cli/cloud/auth_tokens.py +319 -12
- cli/cloud/brokers.py +3 -4
- cli/cloud/cloud_cli.py +5 -5
- cli/cloud/configs.py +1 -2
- cli/cloud/organisations.py +101 -2
- cli/cloud/projects.py +5 -6
- cli/cloud/recordings.py +9 -16
- cli/cloud/recordings_playback.py +6 -8
- cli/cloud/sample_recordings.py +2 -3
- cli/cloud/service_account_tokens.py +21 -5
- cli/cloud/service_accounts.py +32 -4
- cli/cloud/storage/cmd.py +1 -1
- cli/cloud/storage/copy.py +3 -4
- cli/connect/connect.py +1 -1
- cli/connect/protopie/protopie.py +12 -14
- cli/errors.py +6 -1
- cli/remotive.py +30 -6
- cli/settings/__init__.py +1 -2
- cli/settings/config_file.py +92 -0
- cli/settings/core.py +188 -45
- cli/settings/migrate_all_token_files.py +74 -0
- cli/settings/migrate_token_file.py +52 -0
- cli/settings/token_file.py +69 -4
- cli/tools/can/can.py +2 -2
- cli/typer/typer_utils.py +18 -1
- cli/utils/__init__.py +0 -0
- cli/{cloud → utils}/rest_helper.py +114 -39
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/METADATA +6 -4
- remotivelabs_cli-0.1.0a2.dist-info/RECORD +59 -0
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/WHEEL +1 -1
- cli/settings/cmd.py +0 -72
- remotivelabs_cli-0.0.42.dist-info/RECORD +0 -54
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/LICENSE +0 -0
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/entry_points.txt +0 -0
cli/cloud/auth/login.py
CHANGED
@@ -1,18 +1,46 @@
|
|
1
|
-
import
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import base64
|
4
|
+
import hashlib
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
import secrets
|
8
|
+
import sys
|
2
9
|
import time
|
3
10
|
import webbrowser
|
4
11
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
5
12
|
from threading import Thread
|
6
|
-
from typing import Any
|
13
|
+
from typing import Any, Optional, Tuple
|
14
|
+
from urllib.parse import parse_qs, urlparse
|
7
15
|
|
16
|
+
import typer
|
17
|
+
from rich.console import Console
|
8
18
|
from typing_extensions import override
|
9
19
|
|
10
|
-
from cli.cloud.
|
11
|
-
from cli.
|
12
|
-
from cli.settings import
|
20
|
+
from cli.cloud.auth_tokens import do_activate, prompt_to_set_org
|
21
|
+
from cli.errors import ErrorPrinter
|
22
|
+
from cli.settings import TokenFile, TokenNotFoundError, settings
|
23
|
+
from cli.utils.rest_helper import RestHelper as Rest
|
13
24
|
|
14
25
|
httpd: HTTPServer
|
15
26
|
|
27
|
+
console = Console()
|
28
|
+
|
29
|
+
|
30
|
+
def generate_pkce_pair() -> Tuple[str, str]:
|
31
|
+
"""
|
32
|
+
PKCE is used for all cli login flows, both headless and browser.
|
33
|
+
"""
|
34
|
+
code_verifier_ = secrets.token_urlsafe(64) # High-entropy string
|
35
|
+
code_challenge_ = base64.urlsafe_b64encode(hashlib.sha256(code_verifier_.encode("ascii")).digest()).rstrip(b"=").decode("ascii")
|
36
|
+
return code_verifier_, code_challenge_
|
37
|
+
|
38
|
+
|
39
|
+
code_verifier, code_challenge = generate_pkce_pair()
|
40
|
+
state = secrets.token_urlsafe(16)
|
41
|
+
|
42
|
+
short_lived_token = None
|
43
|
+
|
16
44
|
|
17
45
|
class S(BaseHTTPRequestHandler):
|
18
46
|
def _set_response(self) -> None:
|
@@ -21,42 +49,269 @@ class S(BaseHTTPRequestHandler):
|
|
21
49
|
self.end_headers()
|
22
50
|
|
23
51
|
@override
|
24
|
-
def log_message(self, format: Any, *args: Any) -> None:
|
52
|
+
def log_message(self, format: Any, *args: Any) -> None:
|
25
53
|
return
|
26
54
|
|
27
55
|
# Please do not change this into lowercase!
|
28
56
|
@override
|
29
|
-
# type: ignore
|
30
|
-
def do_GET(self): # pylint: disable=invalid-name,
|
57
|
+
def do_GET(self) -> None: # type: ignore # noqa: PLR0912
|
31
58
|
self._set_response()
|
32
|
-
|
59
|
+
|
60
|
+
parsed_url = urlparse(self.path)
|
61
|
+
|
62
|
+
# Get query parameters as a dict
|
63
|
+
query_params = parse_qs(parsed_url.query)
|
64
|
+
|
65
|
+
# Example: Get the value of the "error" parameter if it exists
|
66
|
+
error_value = query_params.get("error", [None])[0]
|
33
67
|
path = self.path
|
68
|
+
auth_code = path[1:] # Remotive /
|
34
69
|
time.sleep(1)
|
35
70
|
httpd.server_close()
|
36
71
|
|
37
72
|
killerthread = Thread(target=httpd.shutdown)
|
38
73
|
killerthread.start()
|
74
|
+
if error_value is None:
|
75
|
+
res = Rest.handle_get(
|
76
|
+
f"/api/open/token?code={auth_code}&code_verifier={code_verifier}",
|
77
|
+
return_response=True,
|
78
|
+
skip_access_token=True,
|
79
|
+
allow_status_codes=[401, 400],
|
80
|
+
)
|
81
|
+
if res.status_code != 200:
|
82
|
+
ErrorPrinter.print_generic_error(
|
83
|
+
"Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com"
|
84
|
+
)
|
85
|
+
self.wfile.write(
|
86
|
+
"Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com".encode(
|
87
|
+
"utf-8"
|
88
|
+
)
|
89
|
+
)
|
90
|
+
sys.exit(1)
|
39
91
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
92
|
+
# TODO - This is written before we are done...
|
93
|
+
self.wfile.write(
|
94
|
+
"""Successfully setup CLI, you may close this window now. Return to your terminal to continue""".encode("utf-8")
|
95
|
+
)
|
96
|
+
access_token = res.json()["access_token"]
|
97
|
+
|
98
|
+
global short_lived_token # noqa: PLW0603
|
99
|
+
short_lived_token = access_token
|
48
100
|
|
101
|
+
else:
|
102
|
+
if error_value == "no_consent":
|
103
|
+
self.wfile.write(
|
104
|
+
"""
|
105
|
+
Authorization was cancelled.<br/>
|
106
|
+
To use RemotiveCLI, you need to grant access to your RemotiveCloud account.
|
107
|
+
<br/><br/>
|
108
|
+
Run `remotive cloud auth login` to try again.
|
109
|
+
""".encode("utf-8")
|
110
|
+
)
|
111
|
+
ErrorPrinter.print_generic_error("You did not grant access to RemotiveCloud, login aborted")
|
112
|
+
elif error_value == "user_not_exists":
|
113
|
+
self.wfile.write(
|
114
|
+
"""
|
115
|
+
It seems like you do not have an account at RemotiveCloud with that user<br/>
|
116
|
+
To use RemotiveCLI you must first sign up at <a href="https://cloud.remotivelabs.com">cloud.remotivelabs.com</a>
|
117
|
+
and approve our agreements.<br/>
|
118
|
+
<br/><br/>
|
119
|
+
Once you are signed up, Run `remotive cloud auth login` again.
|
120
|
+
""".encode("utf-8")
|
121
|
+
)
|
122
|
+
ErrorPrinter.print_generic_error(
|
123
|
+
"To use RemotiveCLI you must first sign up at https://cloud.remotivelabs.com and approve our agreements"
|
124
|
+
)
|
125
|
+
else:
|
126
|
+
self.wfile.write(f"Unknown error {error_value}, please contact support@remotivelabs.com".encode("utf-8"))
|
127
|
+
ErrorPrinter.print_generic_error(f"Unexpected error {error_value}, please contact support@remotivelabs.com")
|
128
|
+
sys.exit(1)
|
129
|
+
|
130
|
+
|
131
|
+
def prepare_local_webserver(server_class: type = HTTPServer, handler_class: type = S, port: Optional[int] = None) -> None:
|
132
|
+
if port is None:
|
133
|
+
env_val = os.getenv("REMOTIVE_LOGIN_CALLBACK_PORT" or "")
|
134
|
+
if env_val and env_val.isdigit():
|
135
|
+
port = int(env_val)
|
136
|
+
else:
|
137
|
+
port = 0
|
49
138
|
|
50
|
-
def start_local_webserver(server_class: type = HTTPServer, handler_class: type = S, port: int = 0) -> None:
|
51
139
|
server_address = ("", port)
|
52
|
-
global httpd #
|
140
|
+
global httpd # noqa: PLW0603
|
53
141
|
httpd = server_class(server_address, handler_class)
|
54
142
|
|
55
143
|
|
56
|
-
def
|
144
|
+
def create_personal_token() -> None:
|
145
|
+
response = Rest.handle_post(
|
146
|
+
url="/api/me/keys",
|
147
|
+
return_response=True,
|
148
|
+
body=json.dumps({"alias": "roine"}),
|
149
|
+
access_token=short_lived_token,
|
150
|
+
)
|
151
|
+
token = response.json()
|
152
|
+
email = token["account"]["email"]
|
153
|
+
existing_file = settings.get_token_file_by_email(email=email)
|
154
|
+
if existing_file is not None:
|
155
|
+
# ErrorPrinter.print_hint(f"Revoking and deleting existing credentials [remove_me]{existing_file.name}")
|
156
|
+
res = Rest.handle_patch(
|
157
|
+
f"/api/me/keys/{existing_file.name}/revoke",
|
158
|
+
quiet=True,
|
159
|
+
access_token=short_lived_token,
|
160
|
+
allow_status_codes=[400, 404],
|
161
|
+
)
|
162
|
+
if res is not None and res.status_code == 200:
|
163
|
+
Rest.handle_delete(
|
164
|
+
f"/api/me/keys/{existing_file.name}",
|
165
|
+
quiet=True,
|
166
|
+
access_token=short_lived_token,
|
167
|
+
)
|
168
|
+
settings.remove_token_file(existing_file.name)
|
169
|
+
|
170
|
+
settings.add_personal_token(response.text, activate=True)
|
171
|
+
|
172
|
+
print("Successfully logged on")
|
173
|
+
|
174
|
+
|
175
|
+
def _do_prompt_to_use_existing_credentials() -> Optional[TokenFile]:
|
176
|
+
files = settings.list_personal_token_files()
|
177
|
+
if len(files) > 0:
|
178
|
+
should_select_token = typer.confirm(
|
179
|
+
"You have credentials available already, would you like to choose one of these instead?", default=True
|
180
|
+
)
|
181
|
+
if should_select_token:
|
182
|
+
token = do_activate(token_name=None)
|
183
|
+
# token = list_and_select_personal_token(skip_prompt=False, include_service_accounts=True)
|
184
|
+
if token is not None:
|
185
|
+
return token
|
186
|
+
# TODO - fix so this is not needed
|
187
|
+
sys.exit(0)
|
188
|
+
return None
|
189
|
+
|
190
|
+
|
191
|
+
def login(headless: bool = False) -> bool: # noqa: C901, PLR0912, PLR0915
|
57
192
|
"""
|
58
|
-
Initiate login
|
193
|
+
Initiate login
|
59
194
|
"""
|
60
|
-
|
61
|
-
|
62
|
-
|
195
|
+
|
196
|
+
#
|
197
|
+
# Check login.md flowchart for better understanding
|
198
|
+
#
|
199
|
+
# 1. Check for active token valid and working credentials
|
200
|
+
#
|
201
|
+
try:
|
202
|
+
activate_token = settings.get_active_token_file()
|
203
|
+
|
204
|
+
if not activate_token.is_expired():
|
205
|
+
if Rest.has_access("/api/whoami"):
|
206
|
+
token = _do_prompt_to_use_existing_credentials()
|
207
|
+
if token is not None:
|
208
|
+
return True
|
209
|
+
else:
|
210
|
+
settings.clear_active_token()
|
211
|
+
raise TokenNotFoundError()
|
212
|
+
else:
|
213
|
+
# TODO - Cleanup token since expired
|
214
|
+
pass
|
215
|
+
|
216
|
+
except TokenNotFoundError:
|
217
|
+
#
|
218
|
+
# 2. If no token was found, let user choose an existing if exists
|
219
|
+
#
|
220
|
+
token = _do_prompt_to_use_existing_credentials()
|
221
|
+
if token is not None:
|
222
|
+
return True
|
223
|
+
|
224
|
+
prepare_local_webserver()
|
225
|
+
|
226
|
+
def force_use_webserver_callback() -> bool:
|
227
|
+
env_val = os.getenv("REMOTIVE_LOGIN_FORCE_CALLBACK" or "no")
|
228
|
+
if env_val and env_val == "yes":
|
229
|
+
return True
|
230
|
+
return False
|
231
|
+
|
232
|
+
def login_with_callback_but_copy_url() -> None:
|
233
|
+
"""
|
234
|
+
This will print a url the will trigger a callback later so the webserver must be up and running.
|
235
|
+
"""
|
236
|
+
print("Copy the following link in a browser to login to cloud, and complete the sign-in prompts:")
|
237
|
+
print("")
|
238
|
+
|
239
|
+
url = (
|
240
|
+
f"{Rest.get_base_frontend_url()}/login"
|
241
|
+
f"?state={state}"
|
242
|
+
f"&cli_version={Rest.get_cli_version()}"
|
243
|
+
f"&response_type=code"
|
244
|
+
f"&code_challenge={code_challenge}"
|
245
|
+
f"&redirect_uri=http://localhost:{httpd.server_address[1]}"
|
246
|
+
)
|
247
|
+
console.print(url, style="bold")
|
248
|
+
httpd.serve_forever()
|
249
|
+
|
250
|
+
def login_headless() -> None:
|
251
|
+
"""
|
252
|
+
Full headless, opens a browser and expects a auth code to be entered and exchanged for the token
|
253
|
+
"""
|
254
|
+
print("Copy the following link in a browser to login to cloud, and complete the sign-in prompts:")
|
255
|
+
print("")
|
256
|
+
|
257
|
+
url = (
|
258
|
+
f"{Rest.get_base_frontend_url()}/login"
|
259
|
+
f"?state={state}"
|
260
|
+
f"&cli_version={Rest.get_cli_version()}"
|
261
|
+
f"&response_type=code"
|
262
|
+
f"&code_challenge={code_challenge}"
|
263
|
+
)
|
264
|
+
console.print(url, style="bold")
|
265
|
+
|
266
|
+
code = typer.prompt(
|
267
|
+
"Once finished, enter the verification code provided in your browser",
|
268
|
+
hide_input=False,
|
269
|
+
)
|
270
|
+
res = Rest.handle_get(
|
271
|
+
f"/api/open/token?code={code}&code_verifier={code_verifier}",
|
272
|
+
return_response=True,
|
273
|
+
skip_access_token=True,
|
274
|
+
allow_status_codes=[401],
|
275
|
+
)
|
276
|
+
if res.status_code == 401:
|
277
|
+
ErrorPrinter.print_generic_error(
|
278
|
+
"Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com"
|
279
|
+
)
|
280
|
+
sys.exit(1)
|
281
|
+
access_token = res.json()["access_token"]
|
282
|
+
# res = Rest.handle_get("/api/whoami", return_response=True, access_token=access_token)
|
283
|
+
global short_lived_token # noqa: PLW0603
|
284
|
+
short_lived_token = access_token
|
285
|
+
create_personal_token()
|
286
|
+
prompt_to_set_org()
|
287
|
+
|
288
|
+
if headless and not force_use_webserver_callback():
|
289
|
+
login_headless()
|
290
|
+
elif headless and force_use_webserver_callback():
|
291
|
+
login_with_callback_but_copy_url()
|
292
|
+
else:
|
293
|
+
could_open = webbrowser.open_new_tab(
|
294
|
+
f"{Rest.get_base_frontend_url()}/login"
|
295
|
+
f"?state={state}"
|
296
|
+
f"&cli_version={Rest.get_cli_version()}"
|
297
|
+
f"&response_type=code"
|
298
|
+
f"&code_challenge={code_challenge}"
|
299
|
+
f"&redirect_uri=http://localhost:{httpd.server_address[1]}"
|
300
|
+
)
|
301
|
+
|
302
|
+
if not could_open:
|
303
|
+
print(
|
304
|
+
"Could not open a browser on this machine, this is likely because you are in an environment where no browser is avaialble"
|
305
|
+
)
|
306
|
+
print("")
|
307
|
+
if force_use_webserver_callback():
|
308
|
+
login_with_callback_but_copy_url()
|
309
|
+
else:
|
310
|
+
login_headless()
|
311
|
+
else:
|
312
|
+
httpd.serve_forever()
|
313
|
+
|
314
|
+
# Once we received our callback or code we are logged in and ready to go
|
315
|
+
create_personal_token()
|
316
|
+
|
317
|
+
return True
|
cli/cloud/auth_tokens.py
CHANGED
@@ -1,33 +1,340 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import List, Literal, Optional
|
4
|
+
|
1
5
|
import typer
|
6
|
+
from rich.console import Console
|
7
|
+
from rich.table import Table
|
2
8
|
|
3
|
-
from cli.
|
9
|
+
from cli.api.cloud import tokens
|
10
|
+
from cli.cloud.organisations import do_select_default_org
|
11
|
+
from cli.errors import ErrorPrinter
|
12
|
+
from cli.settings import TokenFile, TokenNotFoundError, settings
|
4
13
|
from cli.typer import typer_utils
|
14
|
+
from cli.utils.rest_helper import RestHelper as Rest
|
5
15
|
|
6
|
-
|
16
|
+
console = Console(stderr=False)
|
17
|
+
err_console = Console(stderr=True)
|
7
18
|
|
8
19
|
app = typer_utils.create_typer()
|
9
20
|
|
21
|
+
PromptType = Literal["activate", "login"]
|
22
|
+
|
23
|
+
|
24
|
+
def _prompt_choice( # noqa: C901, PLR0912
|
25
|
+
choices: List[TokenFile],
|
26
|
+
skip_prompt: bool = False,
|
27
|
+
info_message: Optional[str] = None,
|
28
|
+
) -> Optional[TokenFile]:
|
29
|
+
accounts = settings.get_cli_config().accounts
|
30
|
+
try:
|
31
|
+
active_account = settings.get_cli_config().get_active()
|
32
|
+
except TokenNotFoundError:
|
33
|
+
active_account = None
|
34
|
+
|
35
|
+
table = Table("#", "Active", "Type", "Token", "Account", "Created", "Expires")
|
36
|
+
|
37
|
+
included_tokens: list[TokenFile] = []
|
38
|
+
excluded_tokens: list[TokenFile] = []
|
39
|
+
|
40
|
+
for token in choices:
|
41
|
+
account = accounts.get(token.account.email)
|
42
|
+
if account and account.credentials_name and account.credentials_name in (token.name or ""):
|
43
|
+
included_tokens.append(token)
|
44
|
+
else:
|
45
|
+
excluded_tokens.append(token)
|
46
|
+
|
47
|
+
if len(included_tokens) == 0:
|
48
|
+
return None
|
49
|
+
|
50
|
+
included_tokens.sort(key=lambda token: token.created, reverse=True)
|
51
|
+
|
52
|
+
active_token_index = None
|
53
|
+
for idx, choice in enumerate(included_tokens, start=1):
|
54
|
+
is_active = active_account is not None and active_account.credentials_name == choice.name
|
55
|
+
active_token_index = idx if is_active else active_token_index
|
56
|
+
table.add_row(
|
57
|
+
f"[yellow]{idx}",
|
58
|
+
":white_check_mark:" if is_active else "",
|
59
|
+
"user" if choice.type == "authorized_user" else "sa",
|
60
|
+
choice.name,
|
61
|
+
f"[bold]{choice.account.email if choice.account else 'unknown'}[/bold]",
|
62
|
+
str(choice.created),
|
63
|
+
str(choice.expires),
|
64
|
+
)
|
65
|
+
# console.print("It seems like you have access tokens from previous login, you can select one of these instead of logging in")
|
66
|
+
console.print(table)
|
67
|
+
|
68
|
+
if skip_prompt:
|
69
|
+
return None
|
70
|
+
|
71
|
+
typer.echo("")
|
72
|
+
if info_message is not None:
|
73
|
+
console.print(info_message)
|
74
|
+
|
75
|
+
selection = typer.prompt(
|
76
|
+
f"Enter the number(# 1-{len(included_tokens)}) of the account to select (q to quit)",
|
77
|
+
default=f"{active_token_index}" if active_token_index is not None else None,
|
78
|
+
)
|
79
|
+
|
80
|
+
if selection == "q":
|
81
|
+
return None
|
82
|
+
try:
|
83
|
+
index = int(selection) - 1
|
84
|
+
if 0 <= index < len(included_tokens):
|
85
|
+
return included_tokens[index]
|
86
|
+
raise ValueError
|
87
|
+
except ValueError:
|
88
|
+
typer.echo("Invalid choice, please try again")
|
89
|
+
return _prompt_choice(included_tokens, skip_prompt, info_message)
|
10
90
|
|
11
|
-
|
12
|
-
@app.command(name="create"
|
13
|
-
def create(
|
14
|
-
|
15
|
-
|
91
|
+
|
92
|
+
# @app.command(name="create")
|
93
|
+
def create(
|
94
|
+
activate: bool = typer.Option(False, help="Activate the token for use after download"),
|
95
|
+
) -> None:
|
96
|
+
"""
|
97
|
+
Create a new personal access token in [bold]cloud[/bold] and download locally
|
98
|
+
"""
|
99
|
+
response = tokens.create()
|
100
|
+
pat = settings.add_personal_token(response.text())
|
16
101
|
print(f"Personal access token added: {pat.name}")
|
17
102
|
|
18
103
|
if not activate:
|
19
104
|
print(f"Use 'remotive cloud auth tokens activate {pat.name}' to use this access token from cli")
|
20
105
|
else:
|
21
|
-
settings.activate_token(pat
|
106
|
+
settings.activate_token(pat)
|
22
107
|
print("Token file activated and ready for use")
|
23
108
|
print("\033[93m This file contains secrets and must be kept safe")
|
24
109
|
|
25
110
|
|
26
|
-
@app.command(name="list", help="List personal
|
111
|
+
# @app.command(name="list", help="List personal credentials in [bold]cloud[/bold]")
|
27
112
|
def list_tokens() -> None:
|
28
113
|
Rest.handle_get("/api/me/keys")
|
29
114
|
|
30
115
|
|
31
|
-
@app.command(name="revoke"
|
32
|
-
def revoke(
|
33
|
-
|
116
|
+
# @app.command(name="revoke")
|
117
|
+
def revoke(
|
118
|
+
name: str = typer.Argument(help="Access token name"),
|
119
|
+
delete: bool = typer.Option(True, help="Also delete token"),
|
120
|
+
) -> None:
|
121
|
+
"""
|
122
|
+
Revoke personal credentials in cloud and removes the file from filesystem
|
123
|
+
|
124
|
+
If cloud token is not found but token is found on file system it will delete it and
|
125
|
+
vice versa.
|
126
|
+
"""
|
127
|
+
_revoke_and_delete_personal_token(name, delete)
|
128
|
+
|
129
|
+
|
130
|
+
# @app.command(name="activate")
|
131
|
+
def activate(
|
132
|
+
token_name: str = typer.Argument(..., help="Token path, filename or name to activate"),
|
133
|
+
) -> None:
|
134
|
+
"""
|
135
|
+
Activate a credential file to be used for authentication using filename, path or name.
|
136
|
+
|
137
|
+
This will be used as the current access token in all requests.
|
138
|
+
"""
|
139
|
+
try:
|
140
|
+
token_file = settings.get_token_file(token_name)
|
141
|
+
settings.activate_token(token_file)
|
142
|
+
except TokenNotFoundError:
|
143
|
+
err_console.print(f":boom: [bold red] Error: [/bold red] Token with filename or name {token_name} could not be found")
|
144
|
+
|
145
|
+
|
146
|
+
def prompt_to_set_org() -> None:
|
147
|
+
if settings.get_cli_config().get_active_default_organisation() is None:
|
148
|
+
set_default_organisation = typer.confirm(
|
149
|
+
"You have not set a default organization\nWould you like to choose one now?",
|
150
|
+
abort=False,
|
151
|
+
default=True,
|
152
|
+
)
|
153
|
+
if set_default_organisation:
|
154
|
+
do_select_default_org(get=False)
|
155
|
+
|
156
|
+
|
157
|
+
@app.command("activate")
|
158
|
+
def select_personal_token(
|
159
|
+
token_name: str = typer.Argument(None, help="Name, filename or path to a credentials file"),
|
160
|
+
) -> None:
|
161
|
+
"""
|
162
|
+
Activates is setting the current active credentials to use by the CLI, this can be done by specifying a name
|
163
|
+
of the token or getting prompted and choosing from existing.
|
164
|
+
"""
|
165
|
+
do_activate(token_name)
|
166
|
+
|
167
|
+
|
168
|
+
def do_activate(
|
169
|
+
token_name: Optional[str],
|
170
|
+
) -> Optional[TokenFile]:
|
171
|
+
if token_name is not None:
|
172
|
+
try:
|
173
|
+
token_file = settings.get_token_file(token_name)
|
174
|
+
settings.activate_token(token_file)
|
175
|
+
return token_file
|
176
|
+
except TokenNotFoundError:
|
177
|
+
err_console.print(f":boom: [bold red] Error: [/bold red] Token with filename or name {token_name} could not be found")
|
178
|
+
return None
|
179
|
+
else:
|
180
|
+
token_files = settings.list_personal_tokens()
|
181
|
+
token_files.extend(settings.list_service_account_tokens())
|
182
|
+
if len(token_files) > 0:
|
183
|
+
token_selected = list_and_select_personal_token(include_service_accounts=True)
|
184
|
+
if token_selected is not None:
|
185
|
+
is_logged_in = Rest.has_access("/api/whoami")
|
186
|
+
if not is_logged_in:
|
187
|
+
ErrorPrinter.print_generic_error("Could not access RemotiveCloud with selected token")
|
188
|
+
else:
|
189
|
+
console.print("[green]Success![/green] Access to RemotiveCloud granted")
|
190
|
+
# Only select default if activate was done with selection and successful
|
191
|
+
# and not SA since SA cannot list available organizations
|
192
|
+
if token_selected.type == "authorized_user":
|
193
|
+
prompt_to_set_org()
|
194
|
+
return token_selected
|
195
|
+
|
196
|
+
ErrorPrinter.print_hint("No credentials available, login to activate credentials")
|
197
|
+
return None
|
198
|
+
|
199
|
+
|
200
|
+
def list_and_select_personal_token(
|
201
|
+
skip_prompt: bool = False,
|
202
|
+
include_service_accounts: bool = False,
|
203
|
+
info_message: Optional[str] = None,
|
204
|
+
) -> Optional[TokenFile]:
|
205
|
+
personal_tokens = settings.list_personal_tokens()
|
206
|
+
|
207
|
+
if include_service_accounts:
|
208
|
+
sa_tokens = settings.list_service_account_tokens()
|
209
|
+
personal_tokens.extend(sa_tokens)
|
210
|
+
|
211
|
+
# merged = _merge_local_tokens_with_cloud(personal_tokens)
|
212
|
+
|
213
|
+
selected_token = _prompt_choice(personal_tokens, skip_prompt=skip_prompt, info_message=info_message)
|
214
|
+
if selected_token is not None:
|
215
|
+
settings.activate_token(selected_token)
|
216
|
+
|
217
|
+
return selected_token
|
218
|
+
|
219
|
+
|
220
|
+
# @app.command("select-revoke")
|
221
|
+
def select_revoke_personal_token() -> None:
|
222
|
+
"""
|
223
|
+
Prompts a user to select one of the credential files to revoke and delete
|
224
|
+
"""
|
225
|
+
personal_tokens = settings.list_personal_tokens()
|
226
|
+
sa_tokens = settings.list_service_account_tokens()
|
227
|
+
personal_tokens.extend(sa_tokens)
|
228
|
+
|
229
|
+
is_logged_in = Rest.has_access("/api/whoami")
|
230
|
+
if not is_logged_in:
|
231
|
+
ErrorPrinter.print_hint("You must be logged in")
|
232
|
+
raise typer.Exit(0)
|
233
|
+
|
234
|
+
# merged = _merge_local_tokens_with_cloud(personal_tokens)
|
235
|
+
|
236
|
+
selected_token = _prompt_choice(personal_tokens)
|
237
|
+
|
238
|
+
if selected_token is not None:
|
239
|
+
_revoke_and_delete_personal_token(selected_token.name, True)
|
240
|
+
# Rest.handle_patch(f"/api/me/keys/{selected_token.name}/revoke", quiet=True, access_token=selected_token.token)
|
241
|
+
# Rest.handle_delete(f"/api/me/keys/{selected_token.name}", quiet=True, access_token=selected_token.token)
|
242
|
+
# settings.remove_token_file(selected_token.name)
|
243
|
+
# active_token = settings.get_active_token_file()
|
244
|
+
# if active_token.name == selected_token.name:
|
245
|
+
# settings.clear_active_token()
|
246
|
+
# select_revoke_personal_token()
|
247
|
+
|
248
|
+
|
249
|
+
# @app.command("test-all")
|
250
|
+
def test_all_personal_tokens() -> None:
|
251
|
+
"""
|
252
|
+
Tests each credential file to see if it is valid
|
253
|
+
"""
|
254
|
+
personal_tokens = settings.list_personal_tokens()
|
255
|
+
personal_tokens.extend(settings.list_service_account_tokens())
|
256
|
+
if len(personal_tokens) == 0:
|
257
|
+
console.print("No personal tokens found on disk")
|
258
|
+
return
|
259
|
+
|
260
|
+
for token in personal_tokens:
|
261
|
+
r = Rest.handle_get(
|
262
|
+
"/api/whoami",
|
263
|
+
allow_status_codes=[401],
|
264
|
+
access_token=token.token,
|
265
|
+
use_progress_indicator=True,
|
266
|
+
return_response=True,
|
267
|
+
)
|
268
|
+
if r.status_code == 200:
|
269
|
+
if token.account is not None:
|
270
|
+
console.print(f"{token.account.email} ({token.name}) :white_check_mark:")
|
271
|
+
else:
|
272
|
+
console.print(f"{token.name} :white_check_mark:")
|
273
|
+
elif token.account is not None:
|
274
|
+
console.print(f"{token.account.email} ({token.name}) :x: Failed")
|
275
|
+
else:
|
276
|
+
console.print(f"{token.name} :x: Failed")
|
277
|
+
|
278
|
+
|
279
|
+
# @app.command(name="list-service-account-tokens-files")
|
280
|
+
def list_sats_files() -> None:
|
281
|
+
"""
|
282
|
+
List service account access token files in remotivelabs config directory
|
283
|
+
"""
|
284
|
+
service_account_files = settings.list_service_account_token_files()
|
285
|
+
for file in service_account_files:
|
286
|
+
print(file)
|
287
|
+
|
288
|
+
|
289
|
+
@app.command(name="list")
|
290
|
+
def list_pats_files(
|
291
|
+
accounts: bool = typer.Option(True, help="Lists all available accounts"),
|
292
|
+
files: bool = typer.Option(False, help="Shows all token files in config directory"),
|
293
|
+
) -> None:
|
294
|
+
"""
|
295
|
+
Lists available credential files on filesystem
|
296
|
+
"""
|
297
|
+
|
298
|
+
if accounts:
|
299
|
+
list_and_select_personal_token(skip_prompt=True, include_service_accounts=True, info_message="hello")
|
300
|
+
|
301
|
+
if files:
|
302
|
+
personal_files = settings.list_personal_token_files()
|
303
|
+
service_account_files = settings.list_service_account_token_files()
|
304
|
+
personal_files.extend(service_account_files)
|
305
|
+
for file in personal_files:
|
306
|
+
print(file)
|
307
|
+
|
308
|
+
|
309
|
+
def _revoke_and_delete_personal_token(name: str, delete: bool) -> None:
|
310
|
+
token_file = None
|
311
|
+
|
312
|
+
# First we try to find the file and make sure its not the currently active
|
313
|
+
try:
|
314
|
+
token_file = settings.get_token_file(name)
|
315
|
+
active_token = settings.get_active_token_file()
|
316
|
+
if token_file.name == active_token.name:
|
317
|
+
ErrorPrinter.print_hint("You cannot revoke the current active token")
|
318
|
+
return
|
319
|
+
except TokenNotFoundError:
|
320
|
+
pass
|
321
|
+
|
322
|
+
# The lets try to revoke from cloud
|
323
|
+
res_revoke = tokens.revoke(name)
|
324
|
+
if delete:
|
325
|
+
res_delete = tokens.delete(name)
|
326
|
+
if res_delete.is_success:
|
327
|
+
ErrorPrinter.print_generic_message("Token successfully revoked and deleted")
|
328
|
+
else:
|
329
|
+
ErrorPrinter.print_hint(f"Failed to revoke and delete token in cloud: {res_delete.status_code}")
|
330
|
+
elif res_revoke.is_success:
|
331
|
+
ErrorPrinter.print_generic_message("Token successfully revoked")
|
332
|
+
else:
|
333
|
+
ErrorPrinter.print_hint("Failed to revoke and delete token in cloud")
|
334
|
+
|
335
|
+
# Finally try to remove the file if exists
|
336
|
+
if token_file is not None:
|
337
|
+
settings.remove_token_file(token_file.name)
|
338
|
+
console.print("Successfully deleted token on filesystem")
|
339
|
+
else:
|
340
|
+
ErrorPrinter.print_hint("Token not found on filesystem")
|