termius-mcp 3.0.0__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.
- termius/__init__.py +3 -0
- termius/account/__init__.py +2 -0
- termius/account/managers.py +226 -0
- termius/cli.py +317 -0
- termius/cloud/__init__.py +2 -0
- termius/cloud/client/__init__.py +2 -0
- termius/cloud/client/browser_sso.py +184 -0
- termius/cloud/client/controllers.py +180 -0
- termius/cloud/client/cryptor.py +339 -0
- termius/cloud/client/grpc_login.py +422 -0
- termius/cloud/client/keyring.py +252 -0
- termius/cloud/client/sodium.py +216 -0
- termius/cloud/client/srp_session.py +348 -0
- termius/cloud/client/transformers/__init__.py +2 -0
- termius/cloud/client/transformers/base.py +34 -0
- termius/cloud/client/transformers/many.py +270 -0
- termius/cloud/client/transformers/mixins.py +15 -0
- termius/cloud/client/transformers/single.py +232 -0
- termius/cloud/client/transformers/utils.py +12 -0
- termius/core/__init__.py +2 -0
- termius/core/api.py +264 -0
- termius/core/constants.py +24 -0
- termius/core/device.py +34 -0
- termius/core/exceptions.py +59 -0
- termius/core/models/__init__.py +2 -0
- termius/core/models/base.py +174 -0
- termius/core/models/terminal.py +243 -0
- termius/core/models/utils.py +66 -0
- termius/core/paths.py +19 -0
- termius/core/settings.py +85 -0
- termius/core/signals.py +22 -0
- termius/core/ssh_command.py +60 -0
- termius/core/ssh_exec.py +138 -0
- termius/core/ssh_files.py +335 -0
- termius/core/ssh_merge.py +69 -0
- termius/core/storage/__init__.py +248 -0
- termius/core/storage/driver.py +152 -0
- termius/core/storage/idgenerators.py +25 -0
- termius/core/storage/operators.py +11 -0
- termius/core/storage/query.py +50 -0
- termius/core/storage/strategies.py +150 -0
- termius/core/subscribers.py +41 -0
- termius/core/utils.py +44 -0
- termius/main.py +43 -0
- termius/mcp/__init__.py +2 -0
- termius/mcp/protocol.py +93 -0
- termius/mcp/server.py +140 -0
- termius/mcp/tools.py +718 -0
- termius/runtime.py +50 -0
- termius/session.py +129 -0
- termius/sync.py +186 -0
- termius/vault.py +65 -0
- termius_mcp-3.0.0.dist-info/METADATA +278 -0
- termius_mcp-3.0.0.dist-info/RECORD +59 -0
- termius_mcp-3.0.0.dist-info/WHEEL +5 -0
- termius_mcp-3.0.0.dist-info/entry_points.txt +3 -0
- termius_mcp-3.0.0.dist-info/licenses/AUTHORS +7 -0
- termius_mcp-3.0.0.dist-info/licenses/LICENSE +30 -0
- termius_mcp-3.0.0.dist-info/top_level.txt +1 -0
termius/__init__.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Module with Account manager."""
|
|
3
|
+
import base64
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from six.moves import configparser
|
|
7
|
+
from ..core.api import API
|
|
8
|
+
from ..core.device import DeviceIdentity
|
|
9
|
+
from ..core.exceptions import (
|
|
10
|
+
ApiError, AuthyTokenIssue, NotMigratedError,
|
|
11
|
+
OptionNotSetException, OtpTokenRequired,
|
|
12
|
+
)
|
|
13
|
+
from ..cloud.client.grpc_login import GrpcLoginClient
|
|
14
|
+
from ..cloud.client.browser_sso import BrowserSso
|
|
15
|
+
|
|
16
|
+
_SCHEMA_NAMES = {
|
|
17
|
+
0: 'v3',
|
|
18
|
+
1: 'v3',
|
|
19
|
+
2: 'v5',
|
|
20
|
+
'0': 'v3',
|
|
21
|
+
'1': 'v3',
|
|
22
|
+
'2': 'v5',
|
|
23
|
+
'undefined': 'v3',
|
|
24
|
+
'v3': 'v3',
|
|
25
|
+
'v5': 'v5',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _camel(name):
|
|
30
|
+
parts = name.split('_')
|
|
31
|
+
return parts[0] + ''.join(part[:1].upper() + part[1:] for part in parts[1:])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _b64_field(credentials, payload, *keys):
|
|
35
|
+
"""Pick a credential string, accepting camelCase aliases."""
|
|
36
|
+
for source in (credentials, payload):
|
|
37
|
+
if not isinstance(source, dict):
|
|
38
|
+
continue
|
|
39
|
+
for key in keys:
|
|
40
|
+
value = source.get(key)
|
|
41
|
+
if value not in (None, ''):
|
|
42
|
+
if isinstance(value, (bytes, bytearray)):
|
|
43
|
+
return base64.b64encode(bytes(value)).decode('ascii')
|
|
44
|
+
return value
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _encryption_schema(value, default='v3'):
|
|
49
|
+
if value is None:
|
|
50
|
+
return default
|
|
51
|
+
mapped = _SCHEMA_NAMES.get(value)
|
|
52
|
+
if mapped:
|
|
53
|
+
return mapped
|
|
54
|
+
mapped = _SCHEMA_NAMES.get(str(value).lower())
|
|
55
|
+
if mapped:
|
|
56
|
+
return mapped
|
|
57
|
+
if isinstance(value, str) and value:
|
|
58
|
+
return value
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AccountManager(object):
|
|
63
|
+
"""Class to keep logic for login and logout."""
|
|
64
|
+
|
|
65
|
+
setting_names = ('synchronize_key', 'agent_forwarding')
|
|
66
|
+
|
|
67
|
+
def __init__(self, config):
|
|
68
|
+
"""Create new account manager."""
|
|
69
|
+
self.config = config
|
|
70
|
+
self.api = API()
|
|
71
|
+
self.device = DeviceIdentity(config)
|
|
72
|
+
|
|
73
|
+
def login(self, username, password, authy_token=None, firebase_token=None):
|
|
74
|
+
"""Retrieve apikey and crypto settings from server."""
|
|
75
|
+
device = self.device.to_json()
|
|
76
|
+
login_email = '' if firebase_token else username
|
|
77
|
+
try:
|
|
78
|
+
payload = GrpcLoginClient().login(
|
|
79
|
+
login_email, password, device,
|
|
80
|
+
authy_token=authy_token,
|
|
81
|
+
firebase_token=firebase_token,
|
|
82
|
+
account_email=username,
|
|
83
|
+
)
|
|
84
|
+
except (AuthyTokenIssue, OtpTokenRequired):
|
|
85
|
+
raise
|
|
86
|
+
except NotMigratedError:
|
|
87
|
+
payload = self.api.login(
|
|
88
|
+
login_email or username, password, authy_token=authy_token,
|
|
89
|
+
device=device, firebase_token=firebase_token,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
credentials = payload.get('credentials') or payload
|
|
93
|
+
token = credentials.get('token') or payload.get('token')
|
|
94
|
+
hmac_salt = _b64_field(
|
|
95
|
+
credentials, payload, 'hmac_salt', 'hmacSalt',
|
|
96
|
+
)
|
|
97
|
+
salt = _b64_field(credentials, payload, 'salt')
|
|
98
|
+
if not token:
|
|
99
|
+
raise OptionNotSetException('Login response did not include a token')
|
|
100
|
+
if not salt or not hmac_salt:
|
|
101
|
+
raise OptionNotSetException(
|
|
102
|
+
'Login response missing vault salts (salt/hmac_salt). '
|
|
103
|
+
'Sign in again after upgrading termius-cli.'
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.config.set('User', 'username', username)
|
|
107
|
+
self.config.set('User', 'apikey', token)
|
|
108
|
+
self.config.set('User', 'token_type', 'device')
|
|
109
|
+
self.config.set('User', 'hmac_salt', hmac_salt)
|
|
110
|
+
self.config.set('User', 'salt', salt)
|
|
111
|
+
|
|
112
|
+
bulk = payload.get('bulk_account') or {}
|
|
113
|
+
account = bulk.get('account') or payload.get('account') or {}
|
|
114
|
+
schema = (
|
|
115
|
+
(account.get('feature_toggles') or {}).get('encryption_schema')
|
|
116
|
+
or (account.get('featureToggles') or {}).get('encryptionSchema')
|
|
117
|
+
or payload.get('encryption_schema')
|
|
118
|
+
or 'v3'
|
|
119
|
+
)
|
|
120
|
+
self.config.set('User', 'encryption_schema', _encryption_schema(schema))
|
|
121
|
+
user_id = account.get('user_id')
|
|
122
|
+
if user_id:
|
|
123
|
+
self.config.set('User', 'user_id', str(user_id))
|
|
124
|
+
team = bulk.get('team') or payload.get('team') or {}
|
|
125
|
+
if team:
|
|
126
|
+
self.config.set('User', 'is_team', 'yes')
|
|
127
|
+
self.config.set(
|
|
128
|
+
'User', 'can_manage_team',
|
|
129
|
+
'yes' if team.get('is_owner') else 'no',
|
|
130
|
+
)
|
|
131
|
+
pkset = credentials.get('personal_keyset') or credentials.get(
|
|
132
|
+
'personalKeyset'
|
|
133
|
+
) or {}
|
|
134
|
+
if isinstance(pkset, dict):
|
|
135
|
+
for key in (
|
|
136
|
+
'public_key', 'encrypted_private_key', 'encrypted_personal_key',
|
|
137
|
+
):
|
|
138
|
+
value = pkset.get(key) or pkset.get(_camel(key))
|
|
139
|
+
if value:
|
|
140
|
+
self.config.set('User', key, value)
|
|
141
|
+
self.config.write()
|
|
142
|
+
return payload
|
|
143
|
+
|
|
144
|
+
def login_with_sso(self, password, provider='google', authy_token=None,
|
|
145
|
+
callback_url=None, log=None):
|
|
146
|
+
"""Browser SSO, then encryption-password login."""
|
|
147
|
+
identity = self.prepare_sso(
|
|
148
|
+
provider=provider, callback_url=callback_url, log=log,
|
|
149
|
+
open_browser=False,
|
|
150
|
+
)
|
|
151
|
+
return self.login(
|
|
152
|
+
identity['email'], password, authy_token=authy_token,
|
|
153
|
+
firebase_token=identity['firebase_token'],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def prepare_sso(self, provider='google', callback_url=None, log=None,
|
|
157
|
+
open_browser=False):
|
|
158
|
+
"""Run browser Google/Apple SSO and detect the Termius account."""
|
|
159
|
+
sso = BrowserSso(
|
|
160
|
+
provider=provider, open_browser=open_browser, log=log
|
|
161
|
+
).authenticate(
|
|
162
|
+
callback_url=callback_url
|
|
163
|
+
)
|
|
164
|
+
detected = self.api.detect_sso_action(sso['firebase_token'])
|
|
165
|
+
action = detected.get('action')
|
|
166
|
+
email = detected.get('email') or sso.get('email')
|
|
167
|
+
firebase_token = (
|
|
168
|
+
detected.get('firebase_token') or sso['firebase_token']
|
|
169
|
+
)
|
|
170
|
+
if action == 'sign-up':
|
|
171
|
+
raise ApiError(
|
|
172
|
+
'This Google account is not a Termius account yet. '
|
|
173
|
+
'Finish sign-up in the Termius app first, then retry.'
|
|
174
|
+
)
|
|
175
|
+
if not email:
|
|
176
|
+
raise ApiError('SSO did not return an email address')
|
|
177
|
+
return {'email': email, 'firebase_token': firebase_token, 'action': action}
|
|
178
|
+
|
|
179
|
+
def set_settings(self, dictionary):
|
|
180
|
+
"""Store settings."""
|
|
181
|
+
filtered_settings = {
|
|
182
|
+
k: (dictionary[k] and 'yes') or 'no' for k in self.setting_names
|
|
183
|
+
}
|
|
184
|
+
for k, i in filtered_settings.items():
|
|
185
|
+
self.config.set('Settings', k, i)
|
|
186
|
+
self.config.write()
|
|
187
|
+
|
|
188
|
+
def get_settings(self):
|
|
189
|
+
"""Get settings or return default."""
|
|
190
|
+
return {
|
|
191
|
+
i: self.config.get_safe('Settings', i, default='yes') == 'yes'
|
|
192
|
+
for i in self.setting_names
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def logout(self):
|
|
196
|
+
"""Remove apikey and other credentials."""
|
|
197
|
+
self.config.remove_section('User')
|
|
198
|
+
self.config.remove_section('Settings')
|
|
199
|
+
self.config.remove_section('CloudSynchronization')
|
|
200
|
+
self.config.write()
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def analytics_id(self):
|
|
204
|
+
"""Get or create analytics id for device."""
|
|
205
|
+
try:
|
|
206
|
+
client_id = self.config.get('User', 'analytics_id')
|
|
207
|
+
except (configparser.NoSectionError, configparser.NoOptionError):
|
|
208
|
+
client_id = uuid.uuid4()
|
|
209
|
+
|
|
210
|
+
self.config.set('User', 'analytics_id', client_id)
|
|
211
|
+
self.config.write()
|
|
212
|
+
|
|
213
|
+
return client_id
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def username(self):
|
|
217
|
+
"""Get username."""
|
|
218
|
+
try:
|
|
219
|
+
return self.config.get('User', 'username')
|
|
220
|
+
except (configparser.NoSectionError, configparser.NoOptionError):
|
|
221
|
+
raise OptionNotSetException
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def encryption_schema(self):
|
|
225
|
+
"""Return stored encryption schema (v3 or v5)."""
|
|
226
|
+
return self.config.get_safe('User', 'encryption_schema', default='v3')
|
termius/cli.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Terminal commands for Termius Cloud login."""
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import getpass
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .core.exceptions import (
|
|
11
|
+
ApiError, AuthyTokenIssue, OptionNotSetException, OtpTokenRequired,
|
|
12
|
+
)
|
|
13
|
+
from .runtime import Runtime
|
|
14
|
+
from .session import (
|
|
15
|
+
login_email, login_google_complete, login_google_start, logout,
|
|
16
|
+
)
|
|
17
|
+
from .sync import status_payload
|
|
18
|
+
from .vault import VAULT_ENV
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CliError(Exception):
|
|
22
|
+
"""User-facing CLI failure."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CliContext(object):
|
|
26
|
+
"""IO and runtime for one CLI invocation."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, runtime, prompt, secret, stdout, stderr, isatty):
|
|
29
|
+
self.runtime = runtime
|
|
30
|
+
self.prompt = prompt
|
|
31
|
+
self.secret = secret
|
|
32
|
+
self.stdout = stdout
|
|
33
|
+
self.stderr = stderr
|
|
34
|
+
self.isatty = isatty
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_LOGIN_ERRORS = (
|
|
38
|
+
ApiError, AuthyTokenIssue, CliError, OptionNotSetException,
|
|
39
|
+
OtpTokenRequired, ValueError,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_parser():
|
|
44
|
+
"""Return the argparse parser for terminal commands."""
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
prog='termius',
|
|
47
|
+
description=(
|
|
48
|
+
'Termius Cloud MCP server. With no arguments, speak MCP on '
|
|
49
|
+
'stdin/stdout. Use login to sign in from a terminal.'
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
sub = parser.add_subparsers(dest='command', metavar='command')
|
|
53
|
+
login = sub.add_parser(
|
|
54
|
+
'login',
|
|
55
|
+
help='Sign in with Google, email and password, or OTP',
|
|
56
|
+
description=(
|
|
57
|
+
'Sign in to Termius Cloud from a terminal. google prints an SSO '
|
|
58
|
+
'URL. email prompts for the vault password. If 2FA is on, the '
|
|
59
|
+
'command prompts for OTP.'
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
login.add_argument(
|
|
63
|
+
'method',
|
|
64
|
+
nargs='?',
|
|
65
|
+
choices=('google', 'email'),
|
|
66
|
+
help='google SSO or email/password. Prompt if omitted on a TTY.',
|
|
67
|
+
)
|
|
68
|
+
login.add_argument(
|
|
69
|
+
'-u', '--username',
|
|
70
|
+
help='Termius email (email method)',
|
|
71
|
+
)
|
|
72
|
+
login.add_argument(
|
|
73
|
+
'--otp',
|
|
74
|
+
help='Authenticator / Authy code if 2FA is on',
|
|
75
|
+
)
|
|
76
|
+
login.add_argument(
|
|
77
|
+
'--no-remember',
|
|
78
|
+
action='store_true',
|
|
79
|
+
help='Do not write the vault password to ~/.termius/vault',
|
|
80
|
+
)
|
|
81
|
+
sub.add_parser(
|
|
82
|
+
'logout',
|
|
83
|
+
help='Sign out and clear local inventory',
|
|
84
|
+
)
|
|
85
|
+
sub.add_parser(
|
|
86
|
+
'status',
|
|
87
|
+
help='Show login state without syncing',
|
|
88
|
+
)
|
|
89
|
+
return parser
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run(argv, runtime=None, prompt=None, secret=None, stdout=None,
|
|
93
|
+
stderr=None, isatty=None):
|
|
94
|
+
"""Run a terminal command. Return a process exit code."""
|
|
95
|
+
stdout = sys.stdout if stdout is None else stdout
|
|
96
|
+
stderr = sys.stderr if stderr is None else stderr
|
|
97
|
+
parser = build_parser()
|
|
98
|
+
args = parser.parse_args(argv)
|
|
99
|
+
if not args.command:
|
|
100
|
+
parser.print_help(stderr)
|
|
101
|
+
return 2
|
|
102
|
+
context = CliContext(
|
|
103
|
+
runtime=runtime or Runtime(),
|
|
104
|
+
prompt=prompt or _stdin_prompt,
|
|
105
|
+
secret=secret or getpass.getpass,
|
|
106
|
+
stdout=stdout,
|
|
107
|
+
stderr=stderr,
|
|
108
|
+
isatty=sys.stdin.isatty() if isatty is None else isatty,
|
|
109
|
+
)
|
|
110
|
+
return _dispatch(args, context)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _dispatch(args, context):
|
|
114
|
+
handlers = {
|
|
115
|
+
'login': cmd_login,
|
|
116
|
+
'logout': cmd_logout,
|
|
117
|
+
'status': cmd_status,
|
|
118
|
+
}
|
|
119
|
+
try:
|
|
120
|
+
return handlers[args.command](args, context)
|
|
121
|
+
except _LOGIN_ERRORS as exc:
|
|
122
|
+
return _fail(context.stderr, _error_message(exc))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_login(args, context):
|
|
126
|
+
"""Sign in with Google SSO or email and password."""
|
|
127
|
+
method = _login_method(args, context)
|
|
128
|
+
if method == 'google':
|
|
129
|
+
return cmd_login_google(args, context)
|
|
130
|
+
return cmd_login_email(args, context)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_login_google(args, context):
|
|
134
|
+
"""Print the desktop SSO URL, then finish with a pasted callback."""
|
|
135
|
+
started = login_google_start()
|
|
136
|
+
_print_google_instructions(context.stderr, started['url'])
|
|
137
|
+
callback = context.prompt('Paste callback URL: ').strip()
|
|
138
|
+
if not callback:
|
|
139
|
+
raise CliError('callback URL is required')
|
|
140
|
+
data = _google_complete(
|
|
141
|
+
context.runtime,
|
|
142
|
+
callback,
|
|
143
|
+
_vault_password(context),
|
|
144
|
+
args.otp,
|
|
145
|
+
not args.no_remember,
|
|
146
|
+
context,
|
|
147
|
+
)
|
|
148
|
+
_print_signed_in(context.stdout, data)
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_login_email(args, context):
|
|
153
|
+
"""Sign in with email, vault password, and OTP when required."""
|
|
154
|
+
data = _email_complete(
|
|
155
|
+
context.runtime,
|
|
156
|
+
_email_username(args, context),
|
|
157
|
+
_vault_password(context),
|
|
158
|
+
args.otp,
|
|
159
|
+
not args.no_remember,
|
|
160
|
+
context,
|
|
161
|
+
)
|
|
162
|
+
_print_signed_in(context.stdout, data)
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cmd_logout(args, context):
|
|
167
|
+
"""Clear the cloud session and local inventory."""
|
|
168
|
+
del args
|
|
169
|
+
logout(context.runtime)
|
|
170
|
+
_write_line(context.stdout, 'Signed out.')
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_status(args, context):
|
|
175
|
+
"""Print login state. Does not pull."""
|
|
176
|
+
del args
|
|
177
|
+
data = status_payload(context.runtime)
|
|
178
|
+
if not data['logged_in']:
|
|
179
|
+
_write_line(context.stdout, 'Not signed in.')
|
|
180
|
+
return 0
|
|
181
|
+
stale = 'stale' if data['stale'] else 'fresh'
|
|
182
|
+
_write_line(
|
|
183
|
+
context.stdout,
|
|
184
|
+
'Signed in as {}, {} hosts, cache {}.'.format(
|
|
185
|
+
data['username'] or 'unknown', data['hosts'], stale,
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _login_method(args, context):
|
|
192
|
+
if args.method:
|
|
193
|
+
return args.method
|
|
194
|
+
if args.username:
|
|
195
|
+
return 'email'
|
|
196
|
+
return _prompt_method(context)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _prompt_method(context):
|
|
200
|
+
if not context.isatty:
|
|
201
|
+
raise CliError('specify google or email')
|
|
202
|
+
raw = context.prompt(
|
|
203
|
+
'Sign-in method (google/email) [google]: '
|
|
204
|
+
).strip().lower()
|
|
205
|
+
if not raw:
|
|
206
|
+
return 'google'
|
|
207
|
+
if raw not in ('google', 'email'):
|
|
208
|
+
raise CliError('method must be google or email')
|
|
209
|
+
return raw
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _email_username(args, context):
|
|
213
|
+
if args.username:
|
|
214
|
+
username = args.username.strip()
|
|
215
|
+
if username:
|
|
216
|
+
return username
|
|
217
|
+
return _prompt_username(context)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _prompt_username(context):
|
|
221
|
+
if not context.isatty:
|
|
222
|
+
raise CliError('username is required for email login (use -u)')
|
|
223
|
+
username = context.prompt('Email: ').strip()
|
|
224
|
+
if not username:
|
|
225
|
+
raise CliError('username is required for email login')
|
|
226
|
+
return username
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _vault_password(context):
|
|
230
|
+
env = os.environ.get(VAULT_ENV)
|
|
231
|
+
if env:
|
|
232
|
+
return env
|
|
233
|
+
password = context.secret('Vault encryption password: ')
|
|
234
|
+
if not password:
|
|
235
|
+
raise CliError('vault encryption password is required')
|
|
236
|
+
return password
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _google_complete(runtime, callback, password, otp, remember, context):
|
|
240
|
+
try:
|
|
241
|
+
return login_google_complete(
|
|
242
|
+
runtime, callback, password,
|
|
243
|
+
otp=otp, remember_password=remember,
|
|
244
|
+
)
|
|
245
|
+
except ValueError as exc:
|
|
246
|
+
if not _is_otp_required(exc) or otp:
|
|
247
|
+
raise
|
|
248
|
+
return login_google_complete(
|
|
249
|
+
runtime, callback, password,
|
|
250
|
+
otp=_require_otp(context), remember_password=remember,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _email_complete(runtime, username, password, otp, remember, context):
|
|
255
|
+
try:
|
|
256
|
+
return login_email(
|
|
257
|
+
runtime, username, password,
|
|
258
|
+
otp=otp, remember_password=remember,
|
|
259
|
+
)
|
|
260
|
+
except ValueError as exc:
|
|
261
|
+
if not _is_otp_required(exc) or otp:
|
|
262
|
+
raise
|
|
263
|
+
return login_email(
|
|
264
|
+
runtime, username, password,
|
|
265
|
+
otp=_require_otp(context), remember_password=remember,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _is_otp_required(exc):
|
|
270
|
+
return 'requires otp' in str(exc).lower()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _require_otp(context):
|
|
274
|
+
code = context.prompt('OTP: ').strip()
|
|
275
|
+
if not code:
|
|
276
|
+
raise CliError('otp is required')
|
|
277
|
+
return code
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _print_google_instructions(stream, url):
|
|
281
|
+
_write_line(stream, 'Open this URL in a browser:')
|
|
282
|
+
_write_line(stream, '')
|
|
283
|
+
_write_line(stream, ' {}'.format(url))
|
|
284
|
+
_write_line(stream, '')
|
|
285
|
+
_write_line(stream, 'Sign in with Google.')
|
|
286
|
+
_write_line(
|
|
287
|
+
stream,
|
|
288
|
+
'When the page tries to open Termius, copy the '
|
|
289
|
+
'termius://app/continue-sso?... URL.',
|
|
290
|
+
)
|
|
291
|
+
_write_line(stream, 'Paste that URL below.')
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _print_signed_in(stream, data):
|
|
295
|
+
_write_line(stream, 'Signed in as {}.'.format(data['username']))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _error_message(exc):
|
|
299
|
+
if isinstance(exc, CliError):
|
|
300
|
+
return str(exc)
|
|
301
|
+
return 'Login failed: {}'.format(exc)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _fail(stream, message):
|
|
305
|
+
_write_line(stream, message)
|
|
306
|
+
return 1
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _write_line(stream, text):
|
|
310
|
+
stream.write(text)
|
|
311
|
+
if not text.endswith('\n'):
|
|
312
|
+
stream.write('\n')
|
|
313
|
+
stream.flush()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _stdin_prompt(message):
|
|
317
|
+
return input(message)
|