From 572e9204662cd74d8b70d042b81b78e76ea1912a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 16 Aug 2022 07:08:51 +0530 Subject: [PATCH] Allow restricting the remote control actions in specific windows --- kitty/boss.py | 12 ++++++++---- kitty/launch.py | 30 ++++++++++++++++++++++++++++++ kitty/rc/launch.py | 1 + kitty/remote_control.py | 1 + kitty/tabs.py | 7 ++++--- kitty/window.py | 29 +++++++++++++++++++++++++---- 6 files changed, 69 insertions(+), 11 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index bde0b07c0..f494ab97e 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -462,12 +462,16 @@ class Boss: return response if not pcmd: return response - allowed_unconditionally = ( - self.allow_remote_control == 'y' or (peer_id > 0 and self.allow_remote_control in ('socket-only', 'socket')) or - (window and window.allow_remote_control)) + extra_data: Dict[str, Any] = {} + try: + allowed_unconditionally = ( + self.allow_remote_control == 'y' or (peer_id > 0 and self.allow_remote_control in ('socket-only', 'socket')) or + (window and window.remote_control_allowed(pcmd, extra_data))) + except PermissionError: + return {'ok': False, 'error': 'Remote control disallowed by window specific password'} if allowed_unconditionally: return self._execute_remote_command(pcmd, window, peer_id) - q = is_cmd_allowed(pcmd, window, peer_id > 0, {}) + q = is_cmd_allowed(pcmd, window, peer_id > 0, extra_data) if q is True: return self._execute_remote_command(pcmd, window, peer_id) if q is None: diff --git a/kitty/launch.py b/kitty/launch.py index 74f3fc537..8ce96a4d6 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -142,6 +142,27 @@ enabled in :file:`kitty.conf`). Note that any program with the right level of permissions can still write to the pipes of any other program on the same computer and therefore can control kitty. It can, however, be useful to block programs running on other computers (for example, over SSH) or as other users. +See :option:`--remote-control-password` for ways to restrict actions allowed by +remote control. + + +--remote-control-password +type=list +Restrict the actions remote control is allowed to take. This works like +:opt:`remote_control_password`. You can specify a password and list of actions +just as for :opt:`remote_control_password`. For example:: + + --remote-control-password '"my passphrase" get-* set-colors' + +This password will be in effect for this window only. +Note that any passwords you have defined for :opt:`remote_control_password` +in :file:`kitty.conf` are also in effect. You can override them by using the same password here. +You can also disable all :opt:`remote_control_password` global passwords for this window, by using:: + + --remote-control-password '!' + +This option only takes effect if :option:`--allow-remote-control` +is also specified. Can be specified multiple times to create multiple passwords. --stdin-source @@ -320,6 +341,7 @@ def load_watch_modules(watchers: Iterable[str]) -> Optional[Watchers]: class LaunchKwds(TypedDict): allow_remote_control: bool + remote_control_passwords: Optional[Dict[str, FrozenSet[str]]] cwd_from: Optional[CwdRequest] cwd: Optional[str] location: Optional[str] @@ -384,8 +406,16 @@ def launch( tm = boss.active_tab_manager opts.os_window_title = get_os_window_title(tm.os_window_id) if tm else None env = get_env(opts, active_child) + remote_control_restrictions: Optional[Dict[str, FrozenSet[str]]] = None + if opts.allow_remote_control and opts.remote_control_password: + from kitty.options.utils import remote_control_password + remote_control_restrictions = {} + for rcp in opts.remote_control_password: + for pw, rcp_items in remote_control_password(rcp, {}): + remote_control_restrictions[pw] = rcp_items kw: LaunchKwds = { 'allow_remote_control': opts.allow_remote_control, + 'remote_control_passwords': remote_control_restrictions, 'cwd_from': None, 'cwd': None, 'location': None, diff --git a/kitty/rc/launch.py b/kitty/rc/launch.py index de9a35eed..0804880c2 100644 --- a/kitty/rc/launch.py +++ b/kitty/rc/launch.py @@ -36,6 +36,7 @@ class Launch(RemoteCommand): hold/bool: Boolean indicating whether to keep window open after cmd exits location/choices.first.after.before.neighbor.last.vsplit.hsplit.split.default: Where in the tab to open the new window allow_remote_control/bool: Boolean indicating whether to allow remote control from the new window + remote_control_password/list/str: A list of remote control passwords stdin_source/choices.none.@selection.@screen.@screen_scrollback.@alternate.@alternate_scrollback.\ @first_cmd_output_on_screen.@last_cmd_output.@last_visited_cmd_output: Where to get stdin for the process from stdin_add_formatting/bool: Boolean indicating whether to add formatting codes to stdin diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 6b6d24c6b..a5c8c7ce9 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -97,6 +97,7 @@ class PasswordAuthorizer: def __init__(self, auth_items: FrozenSet[str]) -> None: self.command_patterns = [] self.function_checkers = [] + self.name = '' for item in auth_items: if item.endswith('.py'): path = os.path.abspath(resolve_custom_file(item)) diff --git a/kitty/tabs.py b/kitty/tabs.py index 00ddc0395..3fa4b8537 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -10,8 +10,8 @@ from contextlib import suppress from operator import attrgetter from time import monotonic from typing import ( - Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple, - Optional, Sequence, Set, Tuple, Union + Any, Deque, Dict, FrozenSet, Generator, Iterable, Iterator, List, + NamedTuple, Optional, Sequence, Set, Tuple, Union ) from .borders import Border, Borders @@ -441,6 +441,7 @@ class Tab: # {{{ watchers: Optional[Watchers] = None, overlay_behind: bool = False, is_clone_launch: str = '', + remote_control_passwords: Optional[Dict[str, FrozenSet[str]]] = None, ) -> Window: child = self.launch_child( use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env, @@ -449,8 +450,8 @@ class Tab: # {{{ window = Window( self, child, self.args, override_title=override_title, copy_colors_from=copy_colors_from, watchers=watchers, + allow_remote_control=allow_remote_control, remote_control_passwords=remote_control_passwords ) - window.allow_remote_control = allow_remote_control # Must add child before laying out so that resize_pty succeeds get_boss().add_child(window) self._add_window(window, location=location, overlay_for=overlay_for, overlay_behind=overlay_behind) diff --git a/kitty/window.py b/kitty/window.py index d9a6a5e02..5913a498e 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -14,8 +14,8 @@ from gettext import gettext as _ from itertools import chain from time import monotonic from typing import ( - TYPE_CHECKING, Any, Callable, Deque, Dict, Iterable, List, NamedTuple, - Optional, Pattern, Sequence, Tuple, Union + TYPE_CHECKING, Any, Callable, Deque, Dict, FrozenSet, Iterable, List, + NamedTuple, Optional, Pattern, Sequence, Tuple, Union ) from .child import ProcessDesc @@ -462,7 +462,6 @@ global_watchers = GlobalWatchers() class Window: window_custom_type: str = '' - allow_remote_control: bool = False def __init__( self, @@ -471,7 +470,9 @@ class Window: args: CLIOptions, override_title: Optional[str] = None, copy_colors_from: Optional['Window'] = None, - watchers: Optional[Watchers] = None + watchers: Optional[Watchers] = None, + allow_remote_control: bool = False, + remote_control_passwords: Optional[Dict[str, FrozenSet[str]]] = None, ): if watchers: self.watchers = watchers @@ -518,6 +519,25 @@ class Window: self.screen.copy_colors_from(copy_colors_from.screen) else: setup_colors(self.screen, opts) + self.remote_control_passwords = remote_control_passwords + self.allow_remote_control = allow_remote_control + + def remote_control_allowed(self, pcmd: Dict[str, Any], extra_data: Dict[str, Any]) -> bool: + if not self.allow_remote_control or not self.remote_control_passwords: + return False + pw = pcmd.get('password', '') + auth_items = self.remote_control_passwords.get(pw) + if pw == '!': + auth_items = None + if auth_items is None: + if '!' in self.remote_control_passwords: + raise PermissionError() + return False + from .remote_control import password_authorizer + pa = password_authorizer(auth_items) + if not pa.is_cmd_allowed(pcmd, self, False, extra_data): + raise PermissionError() + return True @property def file_transmission_control(self) -> 'FileTransmission': @@ -611,6 +631,7 @@ class Window: 'default_title': self.default_title, 'title_stack': list(self.title_stack), 'allow_remote_control': self.allow_remote_control, + 'remote_control_passwords': self.remote_control_passwords, 'cwd': self.child.current_cwd or self.child.cwd, 'env': self.child.environ, 'cmdline': self.child.cmdline,