feature: a shell opens with one of the user's passkeys in place of the password and the TOTP code.
Release / release (push) Successful in 17s
Release / release (push) Successful in 17s
This commit is contained in:
1 parent
3e78d520ae
commit
3ed79c7563
7 files changed
+129
-36
No files matched your search
@@ -1,5 +1,6 @@
|
||||
"""/shell: the ticket, the WebSocket of the terminal (shell), and the editor of the shell: the socket vpn-router-editor
|
||||
names files on (editor_listen) and /shell/editor/... (shell_editor)."""
|
||||
"""/shell: the ticket (shell_ticket), with the passkey proof it may take (shell_passkey), the WebSocket of the terminal
|
||||
(shell), and the editor of the shell: the socket vpn-router-editor names files on (editor_listen) and /shell/editor/...
|
||||
(shell_editor)."""
|
||||
|
||||
import base64
|
||||
import fcntl
|
||||
@@ -20,6 +21,7 @@ import urllib.parse
|
||||
import ldap
|
||||
|
||||
import vpnrouter
|
||||
import vpnrouter_passkeys
|
||||
import vpnrouter_users
|
||||
from vpnrouter_status import run
|
||||
|
||||
@@ -74,11 +76,14 @@ _shell_sessions = {}
|
||||
def shell_ticket(name, body):
|
||||
"""Answer POST /shell: the HTTP status and the JSON document.
|
||||
|
||||
The user named by the token signs in again with {"password", "code"?}; a wrong password or code is 403 and counts
|
||||
as a failed login. Without a password, the user is not asked to sign in while its last use of a shell (see
|
||||
_shell_used) is less than web.shell_auth_timeout minutes ago. The answer is {"ticket", "expires_in"}: a ticket that
|
||||
opens one shell (GET /shell) for the same user within SHELL_TICKET_SECONDS, or a null ticket when the user must
|
||||
sign in. 403 while web.shell is false, the user's console login is not on or the user has no TOTP token.
|
||||
The user named by the token signs in again with {"password", "code"}, its password and TOTP code, or with
|
||||
{"passkey": {"id", "credential"}}, the answer of the browser to a proof begun by POST /shell/passkey (see
|
||||
shell_passkey) with one of its own passkeys and the user verified. A wrong password or code counts as a failed
|
||||
login; it and a passkey that is not accepted are 403. Without either, the user is not asked to sign in while its
|
||||
last use of a shell (see _shell_used) is less than web.shell_auth_timeout minutes ago. The answer is {"ticket",
|
||||
"expires_in"}: a ticket that opens one shell (GET /shell) for the same user within SHELL_TICKET_SECONDS, or a null
|
||||
ticket when the user must sign in. 403 while web.shell is false, the user's console login is not on or the user has
|
||||
no TOTP token.
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
return 403, {'error': 'the shell needs a token of a signed-in user'}
|
||||
@@ -91,16 +96,20 @@ def shell_ticket(name, body):
|
||||
refusal = console_account(name)[1]
|
||||
if refusal:
|
||||
return 403, {'error': refusal}
|
||||
if 'password' not in body:
|
||||
if 'passkey' in body:
|
||||
passkey = body['passkey'] if isinstance(body['passkey'], dict) else {}
|
||||
with vpnrouter_users.session() as connection:
|
||||
vpnrouter_passkeys.shell_complete(connection, name, passkey.get('id'), passkey.get('credential'))
|
||||
elif 'password' in body:
|
||||
vpnrouter_users.login(name, body.get('password'), body.get('code'))
|
||||
else:
|
||||
with _shell_lock:
|
||||
used = _shell_used.get(name)
|
||||
if used is None or time.monotonic() - used >= options['shell_auth_timeout'] * 60:
|
||||
return 200, {'ticket': None, 'expires_in': 0}
|
||||
else:
|
||||
vpnrouter_users.login(name, body.get('password'), body.get('code'))
|
||||
except vpnrouter_users.Failure as failure:
|
||||
if failure.status == 5:
|
||||
return 403, {'error': 'the password or the code is wrong'}
|
||||
return 403, {'error': 'the passkey is not accepted' if 'passkey' in body else 'the password or the code is wrong'}
|
||||
return USER_STATUS.get(failure.status, 500), {'error': str(failure)}
|
||||
except (ldap.LDAPError, OSError) as exc:
|
||||
return 500, {'error': f'the user directory failed: {exc}'}
|
||||
@@ -116,9 +125,32 @@ def shell_ticket(name, body):
|
||||
return 200, {'ticket': ticket, 'expires_in': SHELL_TICKET_SECONDS}
|
||||
|
||||
|
||||
def shell_passkey(name):
|
||||
"""Answer POST /shell/passkey: begin the proof of the signed-in user with one of its own passkeys for POST /shell,
|
||||
{"id", "options"}, options those of navigator.credentials.get; the HTTP status and the JSON document. 403 while
|
||||
the user may not use a shell (console_account), 400 for a user without a passkey."""
|
||||
if not isinstance(name, str):
|
||||
return 403, {'error': 'the shell needs a token of a signed-in user'}
|
||||
try:
|
||||
if not vpnrouter.load_config()['web']['shell']:
|
||||
return 403, {'error': 'the shell is disabled; vpn-config-edit set web.shell true enables it'}
|
||||
refusal = console_account(name)[1]
|
||||
if refusal:
|
||||
return 403, {'error': refusal}
|
||||
with vpnrouter_users.session() as connection:
|
||||
return 200, vpnrouter_passkeys.shell_begin(connection, name)
|
||||
except vpnrouter_users.Failure as failure:
|
||||
return USER_STATUS.get(failure.status, 500), {'error': str(failure)}
|
||||
except (ldap.LDAPError, OSError) as exc:
|
||||
return 500, {'error': f'the user directory failed: {exc}'}
|
||||
except SystemExit as exc:
|
||||
return 500, {'error': str(exc.code)}
|
||||
|
||||
|
||||
def console_account(name):
|
||||
"""The system account the console of the user runs as and None, or None and the reason the user may not use a
|
||||
console: its console login is off, or it has no TOTP token."""
|
||||
console: its console login is off, or it has no TOTP token, which the PAM login session of the shell and sudo need
|
||||
also when a passkey opens the shell."""
|
||||
with vpnrouter_users.session() as connection:
|
||||
user = vpnrouter_users.summary(vpnrouter_users.entry(connection, name))
|
||||
if not user['console'] or not user['console']['enabled']:
|
||||
|
||||
Reference in new issue
Block a user