changeset 7777:b27e515df83c

ssh: introduce 'kallithea-cli ssh-update-authorized-keys' command for updating authorized_keys file Based on work by Ilya Beda <ir4y.ix@gmail.com> on https://bitbucket.org/ir4y/rhodecode/commits/branch/ssh_server_support , incorporating gearbox support by Anton Schur <tonich.sh@gmail.com> and also heavily modified by Mads Kiilerich. This commit also incorporates a fix for Windows by Dominik Ruf, and better handling of the case where the parent dir of 'authorized_keys' does not exist or is not writable, by Bradley M. Kuhn <bkuhn@ebb.org>.
author Christian Oyarzun <oyarzun@gmail.com>
date Mon, 17 Nov 2014 14:42:45 -0500
parents 8f3cf5d00d7f
children 267c0dbcddd3
files CONTRIBUTORS development.ini kallithea/bin/kallithea_cli_ssh.py kallithea/lib/paster_commands/template.ini.mako kallithea/lib/ssh.py kallithea/model/ssh_key.py kallithea/templates/about.html kallithea/tests/conftest.py scripts/contributor_data.py
diffstat 9 files changed, 105 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/CONTRIBUTORS	Wed Jul 31 03:56:57 2019 +0200
+++ b/CONTRIBUTORS	Mon Nov 17 14:42:45 2014 -0500
@@ -49,6 +49,7 @@
     YFdyh000 <yfdyh000@gmail.com> 2016
     Aras Pranckevičius <aras@unity3d.com> 2012-2013 2015
     Sean Farley <sean.michael.farley@gmail.com> 2013-2015
+    Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014-2015
     Christian Oyarzun <oyarzun@gmail.com> 2014-2015
     Joseph Rivera <rivera.d.joseph@gmail.com> 2014-2015
     Anatoly Bubenkov <bubenkoff@gmail.com> 2015
@@ -78,7 +79,6 @@
     Tuux <tuxa@galaxie.eu.org> 2015
     Viktar Palstsiuk <vipals@gmail.com> 2015
     Ante Ilic <ante@unity3d.com> 2014
-    Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014
     Calinou <calinou@opmbx.org> 2014
     Daniel Anderson <daniel@dattrix.com> 2014
     Henrik Stuart <hg@hstuart.dk> 2014
--- a/development.ini	Wed Jul 31 03:56:57 2019 +0200
+++ b/development.ini	Mon Nov 17 14:42:45 2014 -0500
@@ -232,6 +232,12 @@
 ## SSH is disabled by default, until an Administrator decides to enable it.
 ssh_enabled = false
 
+## File where users' SSH keys will be stored *if* ssh_enabled is true.
+#ssh_authorized_keys = /home/kallithea/.ssh/authorized_keys
+
+## Path to be used in ssh_authorized_keys file to invoke kallithea-cli with ssh-serve.
+#kallithea_cli_path = /srv/kallithea/venv/bin/kallithea-cli
+
 ####################################
 ###        CELERY CONFIG        ####
 ####################################
--- a/kallithea/bin/kallithea_cli_ssh.py	Wed Jul 31 03:56:57 2019 +0200
+++ b/kallithea/bin/kallithea_cli_ssh.py	Mon Nov 17 14:42:45 2014 -0500
@@ -25,6 +25,7 @@
 from kallithea.lib.utils2 import str2bool
 from kallithea.lib.vcs.backends.git.ssh import GitSshHandler
 from kallithea.lib.vcs.backends.hg.ssh import MercurialSshHandler
+from kallithea.model.ssh_key import SshKeyModel
 
 log = logging.getLogger(__name__)
 
@@ -69,3 +70,13 @@
 
     sys.stderr.write("This account can only be used for repository access. SSH command %r is not supported.\n" % ssh_original_command)
     sys.exit(1)
+
+
+@cli_base.register_command(config_file_initialize_app=True)
+def ssh_update_authorized_keys():
+    """Update .ssh/authorized_keys file.
+
+    The file is usually maintained automatically, but this command will also re-write it.
+    """
+
+    SshKeyModel().write_authorized_keys()
--- a/kallithea/lib/paster_commands/template.ini.mako	Wed Jul 31 03:56:57 2019 +0200
+++ b/kallithea/lib/paster_commands/template.ini.mako	Mon Nov 17 14:42:45 2014 -0500
@@ -329,6 +329,12 @@
 <%text>## SSH is disabled by default, until an Administrator decides to enable it.</%text>
 ssh_enabled = false
 
+<%text>## File where users' SSH keys will be stored *if* ssh_enabled is true.</%text>
+#ssh_authorized_keys = /home/kallithea/.ssh/authorized_keys
+
+<%text>## Path to be used in ssh_authorized_keys file to invoke kallithea-cli with ssh-serve.</%text>
+#kallithea_cli_path = /srv/kallithea/venv/bin/kallithea-cli
+
 <%text>####################################</%text>
 <%text>###        CELERY CONFIG        ####</%text>
 <%text>####################################</%text>
--- a/kallithea/lib/ssh.py	Wed Jul 31 03:56:57 2019 +0200
+++ b/kallithea/lib/ssh.py	Mon Nov 17 14:42:45 2014 -0500
@@ -89,3 +89,28 @@
         raise SshKeyParseError(_("Incorrect SSH key - base64 part is not %r as claimed but %r") % (str(keytype), str(decoded[4:].split('\0', 1)[0])))
 
     return keytype, decoded, comment
+
+
+SSH_OPTIONS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
+
+
+def authorized_keys_line(kallithea_cli_path, config_file, key):
+    """
+    Return a line as it would appear in .authorized_keys
+
+    >>> from kallithea.model.db import UserSshKeys, User
+    >>> user = User(user_id=7, username='uu')
+    >>> key = UserSshKeys(user_ssh_key_id=17, user=user, description='test key')
+    >>> key.public_key='''ssh-rsa  AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ== and a comment'''
+    >>> authorized_keys_line('/srv/kallithea/venv/bin/kallithea-cli', '/srv/kallithea/my.ini', key)
+    'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,command="/srv/kallithea/venv/bin/kallithea-cli ssh-serve -c /srv/kallithea/my.ini 7 17" ssh-rsa AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==\\n'
+    """
+    try:
+        keytype, decoded, comment = parse_pub_key(key.public_key)
+    except SshKeyParseError:
+        return '# Invalid Kallithea SSH key: %s %s\n' % (key.user.user_id, key.user_ssh_key_id)
+    mimekey = decoded.encode('base64').replace('\n', '')
+    return '%s,command="%s ssh-serve -c %s %s %s" %s %s\n' % (
+        SSH_OPTIONS, kallithea_cli_path, config_file,
+        key.user.user_id, key.user_ssh_key_id,
+        keytype, mimekey)
--- a/kallithea/model/ssh_key.py	Wed Jul 31 03:56:57 2019 +0200
+++ b/kallithea/model/ssh_key.py	Mon Nov 17 14:42:45 2014 -0500
@@ -20,10 +20,15 @@
 """
 
 import logging
+import os
+import stat
+import tempfile
+import errno
 
+from tg import config
 from tg.i18n import ugettext as _
 
-from kallithea.lib.utils2 import safe_str
+from kallithea.lib.utils2 import safe_str, str2bool
 from kallithea.model.db import UserSshKeys, User
 from kallithea.model.meta import Session
 from kallithea.lib import ssh
@@ -88,3 +93,48 @@
         user_ssh_keys = UserSshKeys.query() \
             .filter(UserSshKeys.user_id == user.user_id).all()
         return user_ssh_keys
+
+    def write_authorized_keys(self):
+        if not str2bool(config.get('ssh_enabled', False)):
+            log.error("Will not write SSH authorized_keys file - ssh_enabled is not configured")
+            return
+        authorized_keys = config.get('ssh_authorized_keys')
+        kallithea_cli_path = config.get('kallithea_cli_path', 'kallithea-cli')
+        if not authorized_keys:
+            log.error('Cannot write SSH authorized_keys file - ssh_authorized_keys is not configured')
+            return
+        log.info('Writing %s', authorized_keys)
+
+        authorized_keys_dir = os.path.dirname(authorized_keys)
+        try:
+            os.makedirs(authorized_keys_dir)
+            os.chmod(authorized_keys_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # ~/.ssh/ must be 0700
+        except OSError as exception:
+            if exception.errno != errno.EEXIST:
+                raise
+        # Now, test that the directory is or was created in a readable way by previous.
+        if not (os.path.isdir(authorized_keys_dir) and
+                os.access(authorized_keys_dir, os.W_OK)):
+            raise Exception("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys))
+
+        # Make sure we don't overwrite a key file with important content
+        if os.path.exists(authorized_keys):
+            with open(authorized_keys) as f:
+                for l in f:
+                    if not l.strip() or l.startswith('#'):
+                        pass # accept empty lines and comments
+                    elif ssh.SSH_OPTIONS in l and ' ssh-serve ' in l:
+                        pass # Kallithea entries are ok to overwrite
+                    else:
+                        raise Exception("Safety check failed, found %r in %s - please review and remove it" % (l.strip(), authorized_keys))
+
+        fh, tmp_authorized_keys = tempfile.mkstemp('.authorized_keys', dir=os.path.dirname(authorized_keys))
+        with os.fdopen(fh, 'w') as f:
+            for key in UserSshKeys.query().join(UserSshKeys.user).filter(User.active == True):
+                f.write(ssh.authorized_keys_line(kallithea_cli_path, config['__file__'], key))
+        os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
+        # This preliminary remove is needed for Windows, not for Unix.
+        # TODO In Python 3, the remove+rename sequence below should become os.replace.
+        if os.path.exists(authorized_keys):
+            os.remove(authorized_keys)
+        os.rename(tmp_authorized_keys, authorized_keys)
--- a/kallithea/templates/about.html	Wed Jul 31 03:56:57 2019 +0200
+++ b/kallithea/templates/about.html	Mon Nov 17 14:42:45 2014 -0500
@@ -71,6 +71,7 @@
   <li>Copyright &copy; 2016, timeless@gmail.com</li>
   <li>Copyright &copy; 2016, YFdyh000</li>
   <li>Copyright &copy; 2012&ndash;2013, 2015, Aras Pranckevičius</li>
+  <li>Copyright &copy; 2014&ndash;2015, Bradley M. Kuhn</li>
   <li>Copyright &copy; 2014&ndash;2015, Christian Oyarzun</li>
   <li>Copyright &copy; 2014&ndash;2015, Joseph Rivera</li>
   <li>Copyright &copy; 2014&ndash;2015, Sean Farley</li>
@@ -101,7 +102,6 @@
   <li>Copyright &copy; 2015, Tuux</li>
   <li>Copyright &copy; 2015, Viktar Palstsiuk</li>
   <li>Copyright &copy; 2014, Ante Ilic</li>
-  <li>Copyright &copy; 2014, Bradley M. Kuhn</li>
   <li>Copyright &copy; 2014, Calinou</li>
   <li>Copyright &copy; 2014, Daniel Anderson</li>
   <li>Copyright &copy; 2014, Henrik Stuart</li>
--- a/kallithea/tests/conftest.py	Wed Jul 31 03:56:57 2019 +0200
+++ b/kallithea/tests/conftest.py	Mon Nov 17 14:42:45 2014 -0500
@@ -43,6 +43,9 @@
         },
         '[app:main]': {
             'ssh_enabled': 'true',
+            # Mainly to safeguard against accidentally overwriting the real one:
+            'ssh_authorized_keys': os.path.join(TESTS_TMP_PATH, 'authorized_keys'),
+            #'ssh_locale': 'C',
             'app_instance_uuid': 'test',
             'show_revision_number': 'true',
             'beaker.cache.sql_cache_short.expire': '1',
--- a/scripts/contributor_data.py	Wed Jul 31 03:56:57 2019 +0200
+++ b/scripts/contributor_data.py	Mon Nov 17 14:42:45 2014 -0500
@@ -67,6 +67,7 @@
 other = [
     # Work folded into commits attributed to others:
     ('2013', 'Ilya Beda <ir4y.ix@gmail.com>'),
+    ('2015', 'Bradley M. Kuhn <bkuhn@sfconservancy.org>'),
 ]
 
 # Preserve contributors listed in about.html but not appearing in repository