Mercurial > kallithea
changeset 8739:f3e91cd075eb
vcs: move ssh handlers out of "backends"
The ssh handlers are much more high-level than the low-level backends ... and
not used as a part of the "backends" concept at all.
author | Mads Kiilerich <mads@kiilerich.com> |
---|---|
date | Tue, 20 Oct 2020 00:54:59 +0200 |
parents | 7b809e4a1ea5 |
children | 28b845dca1fd |
files | kallithea/bin/kallithea_cli_ssh.py kallithea/lib/vcs/backends/git/ssh.py kallithea/lib/vcs/backends/hg/ssh.py kallithea/lib/vcs/backends/ssh.py kallithea/lib/vcs/ssh/__init__.py kallithea/lib/vcs/ssh/base.py kallithea/lib/vcs/ssh/git.py kallithea/lib/vcs/ssh/hg.py |
diffstat | 7 files changed, 255 insertions(+), 255 deletions(-) [+] |
line wrap: on
line diff
--- a/kallithea/bin/kallithea_cli_ssh.py Wed Nov 04 14:30:48 2020 +0100 +++ b/kallithea/bin/kallithea_cli_ssh.py Tue Oct 20 00:54:59 2020 +0200 @@ -22,8 +22,8 @@ import kallithea import kallithea.bin.kallithea_cli_base as cli_base from kallithea.lib.utils2 import asbool -from kallithea.lib.vcs.backends.git.ssh import GitSshHandler -from kallithea.lib.vcs.backends.hg.ssh import MercurialSshHandler +from kallithea.lib.vcs.ssh.git import GitSshHandler +from kallithea.lib.vcs.ssh.hg import MercurialSshHandler from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException
--- a/kallithea/lib/vcs/backends/git/ssh.py Wed Nov 04 14:30:48 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import logging -import os - -from kallithea.lib.hooks import log_pull_action -from kallithea.lib.utils import make_ui -from kallithea.lib.vcs.backends.ssh import BaseSshHandler - - -log = logging.getLogger(__name__) - - -class GitSshHandler(BaseSshHandler): - vcs_type = 'git' - - @classmethod - def make(cls, ssh_command_parts): - r""" - >>> import shlex - - >>> GitSshHandler.make(shlex.split("git-upload-pack '/foo bar'")).repo_name - 'foo bar' - >>> GitSshHandler.make(shlex.split("git-upload-pack '/foo bar'")).verb - 'git-upload-pack' - >>> GitSshHandler.make(shlex.split(" git-upload-pack /blåbærgrød ")).repo_name # might not be necessary to support no quoting ... but we can - 'bl\xe5b\xe6rgr\xf8d' - >>> GitSshHandler.make(shlex.split('''git-upload-pack "/foo'bar"''')).repo_name - "foo'bar" - >>> GitSshHandler.make(shlex.split("git-receive-pack '/foo'")).repo_name - 'foo' - >>> GitSshHandler.make(shlex.split("git-receive-pack '/foo'")).verb - 'git-receive-pack' - - >>> GitSshHandler.make(shlex.split("/bin/git-upload-pack '/foo'")) # ssh-serve will report 'SSH command %r is not supported' - >>> GitSshHandler.make(shlex.split('''git-upload-pack /foo bar''')) # ssh-serve will report 'SSH command %r is not supported' - >>> shlex.split("git-upload-pack '/foo'bar' x") # ssh-serve will report: Error parsing SSH command "...": No closing quotation - Traceback (most recent call last): - ValueError: No closing quotation - >>> GitSshHandler.make(shlex.split('hg -R foo serve --stdio')) # not handled here - """ - if (len(ssh_command_parts) == 2 and - ssh_command_parts[0] in ['git-upload-pack', 'git-receive-pack'] and - ssh_command_parts[1].startswith('/') - ): - return cls(ssh_command_parts[1][1:], ssh_command_parts[0]) - - return None - - def __init__(self, repo_name, verb): - BaseSshHandler.__init__(self, repo_name) - self.verb = verb - - def _serve(self): - if self.verb == 'git-upload-pack': # action 'pull' - # base class called set_hook_environment - action is hardcoded to 'pull' - log_pull_action(ui=make_ui(), repo=self.db_repo.scm_instance._repo) - else: # probably verb 'git-receive-pack', action 'push' - if not self.allow_push: - self.exit('Push access to %r denied' % self.repo_name) - # Note: push logging is handled by Git post-receive hook - - # git shell is not a real shell but use shell inspired quoting *inside* the argument. - # Per https://github.com/git/git/blob/v2.22.0/quote.c#L12 : - # The path must be "'" quoted, but "'" and "!" must exit the quoting and be "\" escaped - quoted_abspath = "'%s'" % self.db_repo.repo_full_path.replace("'", r"'\''").replace("!", r"'\!'") - newcmd = ['git', 'shell', '-c', "%s %s" % (self.verb, quoted_abspath)] - log.debug('Serving: %s', newcmd) - os.execvp(newcmd[0], newcmd) - self.exit("Failed to exec 'git' as %s" % newcmd)
--- a/kallithea/lib/vcs/backends/hg/ssh.py Wed Nov 04 14:30:48 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import logging - -import mercurial.hg -import mercurial.wireprotoserver - -from kallithea.lib.utils import make_ui -from kallithea.lib.vcs.backends.ssh import BaseSshHandler -from kallithea.lib.vcs.utils import safe_bytes - - -log = logging.getLogger(__name__) - - -class MercurialSshHandler(BaseSshHandler): - vcs_type = 'hg' - - @classmethod - def make(cls, ssh_command_parts): - r""" - >>> import shlex - - >>> MercurialSshHandler.make(shlex.split('hg -R "foo bar" serve --stdio')).repo_name - 'foo bar' - >>> MercurialSshHandler.make(shlex.split(' hg -R blåbærgrød serve --stdio ')).repo_name - 'bl\xe5b\xe6rgr\xf8d' - >>> MercurialSshHandler.make(shlex.split('''hg -R 'foo"bar' serve --stdio''')).repo_name - 'foo"bar' - - >>> MercurialSshHandler.make(shlex.split('/bin/hg -R "foo" serve --stdio')) - >>> MercurialSshHandler.make(shlex.split('''hg -R "foo"bar" serve --stdio''')) # ssh-serve will report: Error parsing SSH command "...": invalid syntax - Traceback (most recent call last): - ValueError: No closing quotation - >>> MercurialSshHandler.make(shlex.split('git-upload-pack "/foo"')) # not handled here - """ - if ssh_command_parts[:2] == ['hg', '-R'] and ssh_command_parts[3:] == ['serve', '--stdio']: - return cls(ssh_command_parts[2]) - - return None - - def _serve(self): - # Note: we want a repo with config based on .hg/hgrc and can thus not use self.db_repo.scm_instance._repo.ui - baseui = make_ui(repo_path=self.db_repo.repo_full_path) - if not self.allow_push: - baseui.setconfig(b'hooks', b'pretxnopen._ssh_reject', b'python:kallithea.lib.hooks.rejectpush') - baseui.setconfig(b'hooks', b'prepushkey._ssh_reject', b'python:kallithea.lib.hooks.rejectpush') - - repo = mercurial.hg.repository(baseui, safe_bytes(self.db_repo.repo_full_path)) - log.debug("Starting Mercurial sshserver for %s", self.db_repo.repo_full_path) - mercurial.wireprotoserver.sshserver(baseui, repo).serve_forever()
--- a/kallithea/lib/vcs/backends/ssh.py Wed Nov 04 14:30:48 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -""" -vcs.backends.ssh -~~~~~~~~~~~~~~~~~ - -SSH backend for all available SCMs -""" - -import datetime -import logging -import sys - -from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware -from kallithea.lib.utils2 import set_hook_environment -from kallithea.model import db, meta - - -log = logging.getLogger(__name__) - - -class BaseSshHandler(object): - # Protocol for setting properties: - # Set by sub class: - # vcs_type: 'hg' or 'git' - # Set by make() / __init__(): - # repo_name: requested repo name - only validated by serve() - # Set by serve() - must not be accessed before: - # db_repo: repository db object - # authuser: user that has been authenticated - like request.authuser ... which isn't used here - # allow_push: false for read-only access to the repo - - # Set defaults, in case .exit should be called early - vcs_type = None - repo_name = None - - @staticmethod - def make(ssh_command): - """Factory function. Given a command as invoked over SSH (and preserved - in SSH_ORIGINAL_COMMAND when run as authorized_keys command), return a - handler if the command looks ok, else return None. - """ - raise NotImplementedError - - def __init__(self, repo_name): - self.repo_name = repo_name.rstrip('/') - - def serve(self, user_id, key_id, client_ip): - """Verify basic sanity of the repository, and that the user is - valid and has access - then serve the native VCS protocol for - repository access.""" - dbuser = db.User.get(user_id) - if dbuser is None: - self.exit('User %r not found' % user_id) - self.authuser = AuthUser.make(dbuser=dbuser, ip_addr=client_ip) - log.info('Authorized user %s from SSH %s trusting user id %s and key id %s for %r', dbuser, client_ip, user_id, key_id, self.repo_name) - if self.authuser is None: # not ok ... but already kind of authenticated by SSH ... but not really not authorized ... - self.exit('User %s from %s cannot be authorized' % (dbuser.username, client_ip)) - - ssh_key = db.UserSshKeys.get(key_id) - if ssh_key is None: - self.exit('SSH key %r not found' % key_id) - ssh_key.last_seen = datetime.datetime.now() - meta.Session().commit() - - if HasPermissionAnyMiddleware('repository.write', - 'repository.admin')(self.authuser, self.repo_name): - self.allow_push = True - elif HasPermissionAnyMiddleware('repository.read')(self.authuser, self.repo_name): - self.allow_push = False - else: - self.exit('Access to %r denied' % self.repo_name) - - self.db_repo = db.Repository.get_by_repo_name(self.repo_name) - if self.db_repo is None: - self.exit("Repository '%s' not found" % self.repo_name) - assert self.db_repo.repo_name == self.repo_name - - # Set global hook environment up for 'push' actions. - # If pull actions should be served, the actual hook invocation will be - # hardcoded to 'pull' when log_pull_action is invoked (directly on Git, - # or through the Mercurial 'outgoing' hook). - # For push actions, the action in global hook environment is used (in - # handle_git_post_receive when it is called as Git post-receive hook, - # or in log_push_action through the Mercurial 'changegroup' hook). - set_hook_environment(self.authuser.username, client_ip, self.repo_name, self.vcs_type, 'push') - return self._serve() - - def _serve(self): - """Serve the native protocol for repository access.""" - raise NotImplementedError - - def exit(self, error): - log.info('abort serving %s %s: %s', self.vcs_type, self.repo_name, error) - sys.stderr.write('abort: %s\n' % error) - sys.exit(1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/lib/vcs/ssh/base.py Tue Oct 20 00:54:59 2020 +0200 @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +vcs.backends.ssh +~~~~~~~~~~~~~~~~~ + +SSH backend for all available SCMs +""" + +import datetime +import logging +import sys + +from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware +from kallithea.lib.utils2 import set_hook_environment +from kallithea.model import db, meta + + +log = logging.getLogger(__name__) + + +class BaseSshHandler(object): + # Protocol for setting properties: + # Set by sub class: + # vcs_type: 'hg' or 'git' + # Set by make() / __init__(): + # repo_name: requested repo name - only validated by serve() + # Set by serve() - must not be accessed before: + # db_repo: repository db object + # authuser: user that has been authenticated - like request.authuser ... which isn't used here + # allow_push: false for read-only access to the repo + + # Set defaults, in case .exit should be called early + vcs_type = None + repo_name = None + + @staticmethod + def make(ssh_command): + """Factory function. Given a command as invoked over SSH (and preserved + in SSH_ORIGINAL_COMMAND when run as authorized_keys command), return a + handler if the command looks ok, else return None. + """ + raise NotImplementedError + + def __init__(self, repo_name): + self.repo_name = repo_name.rstrip('/') + + def serve(self, user_id, key_id, client_ip): + """Verify basic sanity of the repository, and that the user is + valid and has access - then serve the native VCS protocol for + repository access.""" + dbuser = db.User.get(user_id) + if dbuser is None: + self.exit('User %r not found' % user_id) + self.authuser = AuthUser.make(dbuser=dbuser, ip_addr=client_ip) + log.info('Authorized user %s from SSH %s trusting user id %s and key id %s for %r', dbuser, client_ip, user_id, key_id, self.repo_name) + if self.authuser is None: # not ok ... but already kind of authenticated by SSH ... but not really not authorized ... + self.exit('User %s from %s cannot be authorized' % (dbuser.username, client_ip)) + + ssh_key = db.UserSshKeys.get(key_id) + if ssh_key is None: + self.exit('SSH key %r not found' % key_id) + ssh_key.last_seen = datetime.datetime.now() + meta.Session().commit() + + if HasPermissionAnyMiddleware('repository.write', + 'repository.admin')(self.authuser, self.repo_name): + self.allow_push = True + elif HasPermissionAnyMiddleware('repository.read')(self.authuser, self.repo_name): + self.allow_push = False + else: + self.exit('Access to %r denied' % self.repo_name) + + self.db_repo = db.Repository.get_by_repo_name(self.repo_name) + if self.db_repo is None: + self.exit("Repository '%s' not found" % self.repo_name) + assert self.db_repo.repo_name == self.repo_name + + # Set global hook environment up for 'push' actions. + # If pull actions should be served, the actual hook invocation will be + # hardcoded to 'pull' when log_pull_action is invoked (directly on Git, + # or through the Mercurial 'outgoing' hook). + # For push actions, the action in global hook environment is used (in + # handle_git_post_receive when it is called as Git post-receive hook, + # or in log_push_action through the Mercurial 'changegroup' hook). + set_hook_environment(self.authuser.username, client_ip, self.repo_name, self.vcs_type, 'push') + return self._serve() + + def _serve(self): + """Serve the native protocol for repository access.""" + raise NotImplementedError + + def exit(self, error): + log.info('abort serving %s %s: %s', self.vcs_type, self.repo_name, error) + sys.stderr.write('abort: %s\n' % error) + sys.exit(1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/lib/vcs/ssh/git.py Tue Oct 20 00:54:59 2020 +0200 @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import os + +from kallithea.lib.hooks import log_pull_action +from kallithea.lib.utils import make_ui +from kallithea.lib.vcs.ssh import base + + +log = logging.getLogger(__name__) + + +class GitSshHandler(base.BaseSshHandler): + vcs_type = 'git' + + @classmethod + def make(cls, ssh_command_parts): + r""" + >>> import shlex + + >>> GitSshHandler.make(shlex.split("git-upload-pack '/foo bar'")).repo_name + 'foo bar' + >>> GitSshHandler.make(shlex.split("git-upload-pack '/foo bar'")).verb + 'git-upload-pack' + >>> GitSshHandler.make(shlex.split(" git-upload-pack /blåbærgrød ")).repo_name # might not be necessary to support no quoting ... but we can + 'bl\xe5b\xe6rgr\xf8d' + >>> GitSshHandler.make(shlex.split('''git-upload-pack "/foo'bar"''')).repo_name + "foo'bar" + >>> GitSshHandler.make(shlex.split("git-receive-pack '/foo'")).repo_name + 'foo' + >>> GitSshHandler.make(shlex.split("git-receive-pack '/foo'")).verb + 'git-receive-pack' + + >>> GitSshHandler.make(shlex.split("/bin/git-upload-pack '/foo'")) # ssh-serve will report 'SSH command %r is not supported' + >>> GitSshHandler.make(shlex.split('''git-upload-pack /foo bar''')) # ssh-serve will report 'SSH command %r is not supported' + >>> shlex.split("git-upload-pack '/foo'bar' x") # ssh-serve will report: Error parsing SSH command "...": No closing quotation + Traceback (most recent call last): + ValueError: No closing quotation + >>> GitSshHandler.make(shlex.split('hg -R foo serve --stdio')) # not handled here + """ + if (len(ssh_command_parts) == 2 and + ssh_command_parts[0] in ['git-upload-pack', 'git-receive-pack'] and + ssh_command_parts[1].startswith('/') + ): + return cls(ssh_command_parts[1][1:], ssh_command_parts[0]) + + return None + + def __init__(self, repo_name, verb): + base.BaseSshHandler.__init__(self, repo_name) + self.verb = verb + + def _serve(self): + if self.verb == 'git-upload-pack': # action 'pull' + # base class called set_hook_environment - action is hardcoded to 'pull' + log_pull_action(ui=make_ui(), repo=self.db_repo.scm_instance._repo) + else: # probably verb 'git-receive-pack', action 'push' + if not self.allow_push: + self.exit('Push access to %r denied' % self.repo_name) + # Note: push logging is handled by Git post-receive hook + + # git shell is not a real shell but use shell inspired quoting *inside* the argument. + # Per https://github.com/git/git/blob/v2.22.0/quote.c#L12 : + # The path must be "'" quoted, but "'" and "!" must exit the quoting and be "\" escaped + quoted_abspath = "'%s'" % self.db_repo.repo_full_path.replace("'", r"'\''").replace("!", r"'\!'") + newcmd = ['git', 'shell', '-c', "%s %s" % (self.verb, quoted_abspath)] + log.debug('Serving: %s', newcmd) + os.execvp(newcmd[0], newcmd) + self.exit("Failed to exec 'git' as %s" % newcmd)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/lib/vcs/ssh/hg.py Tue Oct 20 00:54:59 2020 +0200 @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +import mercurial.hg +import mercurial.wireprotoserver + +from kallithea.lib.utils import make_ui +from kallithea.lib.vcs.ssh import base +from kallithea.lib.vcs.utils import safe_bytes + + +log = logging.getLogger(__name__) + + +class MercurialSshHandler(base.BaseSshHandler): + vcs_type = 'hg' + + @classmethod + def make(cls, ssh_command_parts): + r""" + >>> import shlex + + >>> MercurialSshHandler.make(shlex.split('hg -R "foo bar" serve --stdio')).repo_name + 'foo bar' + >>> MercurialSshHandler.make(shlex.split(' hg -R blåbærgrød serve --stdio ')).repo_name + 'bl\xe5b\xe6rgr\xf8d' + >>> MercurialSshHandler.make(shlex.split('''hg -R 'foo"bar' serve --stdio''')).repo_name + 'foo"bar' + + >>> MercurialSshHandler.make(shlex.split('/bin/hg -R "foo" serve --stdio')) + >>> MercurialSshHandler.make(shlex.split('''hg -R "foo"bar" serve --stdio''')) # ssh-serve will report: Error parsing SSH command "...": invalid syntax + Traceback (most recent call last): + ValueError: No closing quotation + >>> MercurialSshHandler.make(shlex.split('git-upload-pack "/foo"')) # not handled here + """ + if ssh_command_parts[:2] == ['hg', '-R'] and ssh_command_parts[3:] == ['serve', '--stdio']: + return cls(ssh_command_parts[2]) + + return None + + def _serve(self): + # Note: we want a repo with config based on .hg/hgrc and can thus not use self.db_repo.scm_instance._repo.ui + baseui = make_ui(repo_path=self.db_repo.repo_full_path) + if not self.allow_push: + baseui.setconfig(b'hooks', b'pretxnopen._ssh_reject', b'python:kallithea.lib.hooks.rejectpush') + baseui.setconfig(b'hooks', b'prepushkey._ssh_reject', b'python:kallithea.lib.hooks.rejectpush') + + repo = mercurial.hg.repository(baseui, safe_bytes(self.db_repo.repo_full_path)) + log.debug("Starting Mercurial sshserver for %s", self.db_repo.repo_full_path) + mercurial.wireprotoserver.sshserver(baseui, repo).serve_forever()