Mercurial > kallithea
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 © 2016, timeless@gmail.com</li> <li>Copyright © 2016, YFdyh000</li> <li>Copyright © 2012–2013, 2015, Aras Pranckevičius</li> + <li>Copyright © 2014–2015, Bradley M. Kuhn</li> <li>Copyright © 2014–2015, Christian Oyarzun</li> <li>Copyright © 2014–2015, Joseph Rivera</li> <li>Copyright © 2014–2015, Sean Farley</li> @@ -101,7 +102,6 @@ <li>Copyright © 2015, Tuux</li> <li>Copyright © 2015, Viktar Palstsiuk</li> <li>Copyright © 2014, Ante Ilic</li> - <li>Copyright © 2014, Bradley M. Kuhn</li> <li>Copyright © 2014, Calinou</li> <li>Copyright © 2014, Daniel Anderson</li> <li>Copyright © 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