Changeset - b27e515df83c
[Not reviewed]
default
0 9 0
Christian Oyarzun - 11 years ago 2014-11-17 20:42:45
oyarzun@gmail.com
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>.
9 files changed with 105 insertions and 3 deletions:
0 comments (0 inline, 0 general)
CONTRIBUTORS
Show inline comments
 
@@ -40,24 +40,25 @@ List of contributors to Kallithea projec
 
    Robert Rauch <mail@robertrauch.de> 2015-2016
 
    Angel Ezquerra <angel.ezquerra@gmail.com> 2016
 
    Anton Shestakov <av6@dwimlabs.net> 2016
 
    Brandon Jones <bjones14@gmail.com> 2016
 
    Kateryna Musina <kateryna@unity3d.com> 2016
 
    Konstantin Veretennicov <kveretennicov@gmail.com> 2016
 
    Oscar Curero <oscar@naiandei.net> 2016
 
    Robert James Dennington <tinytimrob@googlemail.com> 2016
 
    timeless@gmail.com 2016
 
    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
 
    Andrew Bartlett <abartlet@catalyst.net.nz> 2015
 
    Balázs Úr <urbalazs@gmail.com> 2015
 
    Ben Finney <ben@benfinney.id.au> 2015
 
    Daniel Hobley <danielh@unity3d.com> 2015
 
    David Avigni <david.avigni@ankapi.com> 2015
 
    Denis Blanchette <dblanchette@coveo.com> 2015
 
    duanhongyi <duanhongyi@doopai.com> 2015
 
    EriCSN Chang <ericsning@gmail.com> 2015
 
    Grzegorz Krason <grzegorz.krason@gmail.com> 2015
 
@@ -69,25 +70,24 @@ List of contributors to Kallithea projec
 
    Marc Villetard <marc.villetard@gmail.com> 2015
 
    Matthias Zilk <matthias.zilk@gmail.com> 2015
 
    Michael Pohl <michael@mipapo.de> 2015
 
    Michael V. DePalatis <mike@depalatis.net> 2015
 
    Morten Skaaning <mortens@unity3d.com> 2015
 
    Nick High <nick@silverchip.org> 2015
 
    Niemand Jedermann <predatorix@web.de> 2015
 
    Peter Vitt <petervitt@web.de> 2015
 
    Ronny Pfannschmidt <opensource@ronnypfannschmidt.de> 2015
 
    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
 
    Ingo von Borstel <kallithea@planetmaker.de> 2014
 
    Jelmer Vernooij <jelmer@samba.org> 2014
 
    Jim Hague <jim.hague@acm.org> 2014
 
    Matt Fellows <kallithea@matt-fellows.me.uk> 2014
 
    Max Roman <max@choloclos.se> 2014
 
    Na'Tosha Bard <natosha@unity3d.com> 2014
 
    Rasmus Selsmark <rasmuss@unity3d.com> 2014
 
    Tim Freund <tim@freunds.net> 2014
 
    Travis Burtrum <android@moparisthebest.com> 2014
development.ini
Show inline comments
 
@@ -223,24 +223,30 @@ allow_custom_hooks_settings = True
 
#    .dockerignore
 
#    .editorconfig
 
#    INSTALL
 
#    CHANGELOG
 

	
 
####################################
 
###           SSH CONFIG        ####
 
####################################
 

	
 
## 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        ####
 
####################################
 

	
 
use_celery = false
 

	
 
## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:
 
broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
 

	
 
celery.imports = kallithea.lib.celerylib.tasks
 
celery.accept.content = pickle
 
celery.result.backend = amqp
kallithea/bin/kallithea_cli_ssh.py
Show inline comments
 
@@ -16,24 +16,25 @@ import click
 
import kallithea.bin.kallithea_cli_base as cli_base
 

	
 
import os
 
import sys
 
import re
 
import logging
 
import shlex
 

	
 
import kallithea
 
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__)
 

	
 

	
 
@cli_base.register_command(config_file_initialize_app=True, hidden=True)
 
@click.argument('user-id', type=click.INT, required=True)
 
@click.argument('key-id', type=click.INT, required=True)
 
def ssh_serve(user_id, key_id):
 
    """Serve SSH repository protocol access.
 

	
 
    The trusted command that is invoked from .ssh/authorized_keys to serve SSH
 
    protocol access. The access will be granted as the specified user ID, and
 
@@ -60,12 +61,22 @@ def ssh_serve(user_id, key_id):
 
        ssh_command_parts = shlex.split(ssh_original_command)
 
    except ValueError as e:
 
        sys.stderr.write('Error parsing SSH command %r: %s\n' % (ssh_original_command, e))
 
        sys.exit(1)
 
    for VcsHandler in [MercurialSshHandler, GitSshHandler]:
 
        vcs_handler = VcsHandler.make(ssh_command_parts)
 
        if vcs_handler is not None:
 
            vcs_handler.serve(user_id, key_id, client_ip)
 
            assert False # serve is written so it never will terminate
 

	
 
    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()
kallithea/lib/paster_commands/template.ini.mako
Show inline comments
 
@@ -320,24 +320,30 @@ allow_custom_hooks_settings = True
 
#    .dockerignore
 
#    .editorconfig
 
#    INSTALL
 
#    CHANGELOG
 

	
 
<%text>####################################</%text>
 
<%text>###           SSH CONFIG        ####</%text>
 
<%text>####################################</%text>
 

	
 
<%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>
 

	
 
use_celery = false
 

	
 
<%text>## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:</%text>
 
broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
 

	
 
celery.imports = kallithea.lib.celerylib.tasks
 
celery.accept.content = pickle
 
celery.result.backend = amqp
kallithea/lib/ssh.py
Show inline comments
 
@@ -80,12 +80,37 @@ def parse_pub_key(ssh_key):
 
    if re.search(r'[^a-zA-Z0-9+/=]', keyvalue):
 
        raise SshKeyParseError(_("Incorrect SSH key - unexpected characters in base64 part %r") % keyvalue)
 

	
 
    try:
 
        decoded = keyvalue.decode('base64')
 
    except binascii.Error:
 
        raise SshKeyParseError(_("Incorrect SSH key - failed to decode base64 part %r") % keyvalue)
 

	
 
    if not decoded.startswith('\x00\x00\x00\x07' + str(keytype) + '\x00'):
 
        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)
kallithea/model/ssh_key.py
Show inline comments
 
@@ -11,28 +11,33 @@
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.model.ssh_key
 
~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
SSH key model for Kallithea
 

	
 
"""
 

	
 
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
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class SshKeyModelException(Exception):
 
    """Exception raised by SshKeyModel methods to report errors"""
 

	
 

	
 
class SshKeyModel(object):
 
@@ -79,12 +84,57 @@ class SshKeyModel(object):
 
            ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id)
 

	
 
        ssh_key = ssh_key.scalar()
 
        if ssh_key is None:
 
            raise SshKeyModelException(_('SSH key %r not found') % safe_str(public_key))
 
        Session().delete(ssh_key)
 

	
 
    def get_ssh_keys(self, user):
 
        user = User.guess_instance(user)
 
        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)
kallithea/templates/about.html
Show inline comments
 
@@ -62,24 +62,25 @@
 
  <li>Copyright &copy; 2015&ndash;2016, Robert Martinez</li>
 
  <li>Copyright &copy; 2015&ndash;2016, Robert Rauch</li>
 
  <li>Copyright &copy; 2016, Angel Ezquerra</li>
 
  <li>Copyright &copy; 2016, Anton Shestakov</li>
 
  <li>Copyright &copy; 2016, Brandon Jones</li>
 
  <li>Copyright &copy; 2016, Kateryna Musina</li>
 
  <li>Copyright &copy; 2016, Konstantin Veretennicov</li>
 
  <li>Copyright &copy; 2016, Oscar Curero</li>
 
  <li>Copyright &copy; 2016, Robert James Dennington</li>
 
  <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>
 
  <li>Copyright &copy; 2015, Anatoly Bubenkov</li>
 
  <li>Copyright &copy; 2015, Andrew Bartlett</li>
 
  <li>Copyright &copy; 2015, Balázs Úr</li>
 
  <li>Copyright &copy; 2015, Ben Finney</li>
 
  <li>Copyright &copy; 2015, Daniel Hobley</li>
 
  <li>Copyright &copy; 2015, David Avigni</li>
 
  <li>Copyright &copy; 2015, Denis Blanchette</li>
 
  <li>Copyright &copy; 2015, duanhongyi</li>
 
  <li>Copyright &copy; 2015, EriCSN Chang</li>
 
@@ -92,25 +93,24 @@
 
  <li>Copyright &copy; 2015, Marc Villetard</li>
 
  <li>Copyright &copy; 2015, Matthias Zilk</li>
 
  <li>Copyright &copy; 2015, Michael Pohl</li>
 
  <li>Copyright &copy; 2015, Michael V. DePalatis</li>
 
  <li>Copyright &copy; 2015, Morten Skaaning</li>
 
  <li>Copyright &copy; 2015, Nick High</li>
 
  <li>Copyright &copy; 2015, Niemand Jedermann</li>
 
  <li>Copyright &copy; 2015, Peter Vitt</li>
 
  <li>Copyright &copy; 2015, Ronny Pfannschmidt</li>
 
  <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>
 
  <li>Copyright &copy; 2014, Ingo von Borstel</li>
 
  <li>Copyright &copy; 2014, Jelmer Vernooij</li>
 
  <li>Copyright &copy; 2014, Jim Hague</li>
 
  <li>Copyright &copy; 2014, Matt Fellows</li>
 
  <li>Copyright &copy; 2014, Max Roman</li>
 
  <li>Copyright &copy; 2014, Na'Tosha Bard</li>
 
  <li>Copyright &copy; 2014, Rasmus Selsmark</li>
 
  <li>Copyright &copy; 2014, Tim Freund</li>
 
  <li>Copyright &copy; 2014, Travis Burtrum</li>
kallithea/tests/conftest.py
Show inline comments
 
@@ -34,24 +34,27 @@ def pytest_configure():
 
    sys.path.insert(0, path)
 
    pkg_resources.working_set.add_entry(path)
 

	
 
    # Disable INFO logging of test database creation, restore with NOTSET
 
    logging.disable(logging.INFO)
 

	
 
    ini_settings = {
 
        '[server:main]': {
 
            'port': '4999',
 
        },
 
        '[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',
 
            'session.secret': '{74e0cd75-b339-478b-b129-07dd221def1f}',
 
            #'i18n.lang': '',
 
        },
 
        '[handler_console]': {
 
            'formatter': 'color_formatter',
 
        },
 
        # The 'handler_console_sql' block is very similar to the one in
 
        # development.ini, but without the explicit 'level=DEBUG' setting:
 
        # it causes duplicate sqlalchemy debug logs, one through
scripts/contributor_data.py
Show inline comments
 
@@ -58,24 +58,25 @@ no_about.add(('Vincent Caron <vcaron@bea
 
# These contributors' contributions might be too small to be copyrightable:
 
no_about.add(('philip.j@hostdime.com', '2012'))
 
no_about.add(('Stefan Engel <mail@engel-stefan.de>', '2012'))
 
no_about.add(('Ton Plomp <tcplomp@gmail.com>', '2013'))
 
# Was reworked and contributed later and shadowed by other contributions:
 
no_about.add(('Sean Farley <sean.michael.farley@gmail.com>', '2013'))
 

	
 
# Contributors in about.html and CONTRIBUTORS not appearing in repository
 
# history:
 
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
 
# history:
 
other_about = [
 
    ("2011", "Aparkar <aparkar@icloud.com>"),
 
    ("2010", "RhodeCode GmbH"),
 
    ("2011", "RhodeCode GmbH"),
 
    ("2012", "RhodeCode GmbH"),
 
    ("2013", "RhodeCode GmbH"),
 
]
 

	
0 comments (0 inline, 0 general)