diff --git a/configs/development.ini b/configs/development.ini
--- a/configs/development.ini
+++ b/configs/development.ini
@@ -587,6 +587,27 @@ svn.proxy.location_root = /
## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
#svn.proxy.reload_timeout = 10
+############################################################
+### SSH Support Settings ###
+############################################################
+
+## Defines if the authorized_keys file should be written on any change of
+## user ssh keys
+ssh.generate_authorized_keyfile = false
+
+## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
+# ssh.authorized_keys_ssh_opts =
+
+## File to generate the authorized keys together with options
+ssh.authorized_keys_file_path = /home/USER/.ssh/authorized_keys
+
+## Command to execute as an SSH wrapper, available from
+## https://code.rhodecode.com/rhodecode-ssh
+ssh.wrapper_cmd = /home/USER/rhodecode-ssh/sshwrapper.py
+
+## Allow shell when executing the command
+ssh.wrapper_cmd_allow_shell = false
+
## Dummy marker to add new entries after.
## Add any custom entries below. Please don't remove.
custom.conf = 1
diff --git a/configs/production.ini b/configs/production.ini
--- a/configs/production.ini
+++ b/configs/production.ini
@@ -556,6 +556,27 @@ svn.proxy.location_root = /
## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
#svn.proxy.reload_timeout = 10
+############################################################
+### SSH Support Settings ###
+############################################################
+
+## Defines if the authorized_keys file should be written on any change of
+## user ssh keys
+ssh.generate_authorized_keyfile = false
+
+## Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
+# ssh.authorized_keys_ssh_opts =
+
+## File to generate the authorized keys together with options
+ssh.authorized_keys_file_path = /home/USER/.ssh/authorized_keys
+
+## Command to execute as an SSH wrapper, available from
+## https://code.rhodecode.com/rhodecode-ssh
+ssh.wrapper_cmd = /home/USER/rhodecode-ssh/sshwrapper.py
+
+## Allow shell when executing the command
+ssh.wrapper_cmd_allow_shell = false
+
## Dummy marker to add new entries after.
## Add any custom entries below. Please don't remove.
custom.conf = 1
diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py
--- a/rhodecode/apps/admin/views/users.py
+++ b/rhodecode/apps/admin/views/users.py
@@ -28,6 +28,8 @@ from sqlalchemy.sql.functions import coa
from sqlalchemy.exc import IntegrityError
from rhodecode.apps._base import BaseAppView, DataGridAppView
+from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
+from rhodecode.events import trigger
from rhodecode.lib import audit_logger
from rhodecode.lib.ext_json import json
@@ -327,6 +329,9 @@ class AdminUsersView(BaseAppView, DataGr
user=self._rhodecode_user, )
Session().commit()
+ # Trigger an event on change of keys.
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
+
h.flash(_("Ssh Key successfully created"), category='success')
except IntegrityError:
@@ -368,6 +373,8 @@ class AdminUsersView(BaseAppView, DataGr
'data': {'ssh_key': ssh_key_data, 'user': user_data}},
user=self._rhodecode_user,)
Session().commit()
+ # Trigger an event on change of keys.
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
h.flash(_("Ssh key successfully deleted"), category='success')
return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
diff --git a/rhodecode/apps/ssh_support/__init__.py b/rhodecode/apps/ssh_support/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/__init__.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import logging
+
+from . import config_keys
+from .events import SshKeyFileChangeEvent
+from .subscribers import generate_ssh_authorized_keys_file_subscriber
+
+from rhodecode.config.middleware import _bool_setting, _string_setting
+
+log = logging.getLogger(__name__)
+
+
+def _sanitize_settings_and_apply_defaults(settings):
+ """
+ Set defaults, convert to python types and validate settings.
+ """
+ _bool_setting(settings, config_keys.generate_authorized_keyfile, 'false')
+ _bool_setting(settings, config_keys.wrapper_allow_shell, 'false')
+
+ _string_setting(settings, config_keys.authorized_keys_file_path, '',
+ lower=False)
+ _string_setting(settings, config_keys.wrapper_cmd, '',
+ lower=False)
+ _string_setting(settings, config_keys.authorized_keys_line_ssh_opts, '',
+ lower=False)
+
+
+def includeme(config):
+ settings = config.registry.settings
+ _sanitize_settings_and_apply_defaults(settings)
+
+ # if we have enable generation of file, subscribe to event
+ if settings[config_keys.generate_authorized_keyfile]:
+ config.add_subscriber(
+ generate_ssh_authorized_keys_file_subscriber, SshKeyFileChangeEvent)
diff --git a/rhodecode/apps/ssh_support/config_keys.py b/rhodecode/apps/ssh_support/config_keys.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/config_keys.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+# Definition of setting keys used to configure this module. Defined here to
+# avoid repetition of keys throughout the module.
+generate_authorized_keyfile = 'ssh.generate_authorized_keyfile'
+authorized_keys_file_path = 'ssh.authorized_keys_file_path'
+authorized_keys_line_ssh_opts = 'ssh.authorized_keys_ssh_opts'
+wrapper_cmd = 'ssh.wrapper_cmd'
+wrapper_allow_shell = 'ssh.wrapper_cmd_allow_shell'
diff --git a/rhodecode/apps/ssh_support/events.py b/rhodecode/apps/ssh_support/events.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/events.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+from rhodecode.events.base import RhodecodeEvent
+from rhodecode.translation import _
+
+
+class SshKeyFileChangeEvent(RhodecodeEvent):
+ """
+ This event will be triggered on every modification of the stored SSH keys
+ """
+ name = 'rhodecode-ssh-key-file-change'
+ display_name = _('RhodeCode SSH Key files changed.')
diff --git a/rhodecode/apps/ssh_support/subscribers.py b/rhodecode/apps/ssh_support/subscribers.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/subscribers.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import logging
+
+
+from .utils import generate_ssh_authorized_keys_file
+
+
+log = logging.getLogger(__name__)
+
+
+def generate_ssh_authorized_keys_file_subscriber(event):
+ """
+ Subscriber to the `SshKeyFileChangeEvent`. This triggers the
+ automatic generation of authorized_keys file on any change in
+ ssh keys management
+ """
+ generate_ssh_authorized_keys_file(event.request.registry)
diff --git a/rhodecode/apps/ssh_support/tests/__init__.py b/rhodecode/apps/ssh_support/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/tests/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
diff --git a/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py b/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/tests/test_ssh_authorized_keys_gen.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+
+import os
+import pytest
+import mock
+
+from rhodecode.apps.ssh_support import utils
+from rhodecode.lib.utils2 import AttributeDict
+
+
+class TestSshKeyFileGeneration(object):
+ @pytest.mark.parametrize('ssh_wrapper_cmd', ['/tmp/sshwrapper.py'])
+ @pytest.mark.parametrize('allow_shell', [True, False])
+ @pytest.mark.parametrize('ssh_opts', [None, 'mycustom,option'])
+ def test_write_keyfile(self, tmpdir, ssh_wrapper_cmd, allow_shell, ssh_opts):
+
+ authorized_keys_file_path = os.path.join(str(tmpdir), 'authorized_keys')
+
+ def keys():
+ return [
+ AttributeDict({'user': AttributeDict(username='admin'),
+ 'ssh_key_data': 'ssh-rsa ADMIN_KEY'}),
+ AttributeDict({'user': AttributeDict(username='user'),
+ 'ssh_key_data': 'ssh-rsa USER_KEY'}),
+ ]
+
+ with mock.patch('rhodecode.apps.ssh_support.utils.get_all_active_keys',
+ return_value=keys()):
+ utils._generate_ssh_authorized_keys_file(
+ authorized_keys_file_path, ssh_wrapper_cmd,
+ allow_shell, ssh_opts
+ )
+
+ assert os.path.isfile(authorized_keys_file_path)
+ with open(authorized_keys_file_path) as f:
+ content = f.read()
+
+ assert 'command="/tmp/sshwrapper.py' in content
+ assert 'This file is managed by RhodeCode, ' \
+ 'please do not edit it manually.' in content
+
+ if allow_shell:
+ assert '--shell --user' in content
+ else:
+ assert '--user' in content
+
+ if ssh_opts:
+ assert ssh_opts in content
diff --git a/rhodecode/apps/ssh_support/utils.py b/rhodecode/apps/ssh_support/utils.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/ssh_support/utils.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# 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 Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import os
+import stat
+import logging
+import tempfile
+import datetime
+
+from . import config_keys
+from rhodecode.model.db import true, joinedload, User, UserSshKeys
+
+
+log = logging.getLogger(__name__)
+
+HEADER = \
+ "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
+ "# Current entries: {}, create date: UTC:{}.\n"
+
+# Default SSH options for authorized_keys file, can be override via .ini
+SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
+
+
+def get_all_active_keys():
+ result = UserSshKeys.query() \
+ .options(joinedload(UserSshKeys.user)) \
+ .filter(UserSshKeys.user != User.get_default_user()) \
+ .filter(User.active == true()) \
+ .all()
+ return result
+
+
+def _generate_ssh_authorized_keys_file(
+ authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts):
+ all_active_keys = get_all_active_keys()
+
+ if allow_shell:
+ ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
+
+ if not os.path.isfile(authorized_keys_file_path):
+ with open(authorized_keys_file_path, 'w'):
+ pass
+
+ if not os.access(authorized_keys_file_path, os.R_OK):
+ raise OSError('Access to file {} is without read access'.format(
+ authorized_keys_file_path))
+
+ line_tmpl = '{ssh_opts},command="{wrapper_command} --user {user}" {key}\n'
+
+ fd, tmp_authorized_keys = tempfile.mkstemp(
+ '.authorized_keys_write',
+ dir=os.path.dirname(authorized_keys_file_path))
+
+ now = datetime.datetime.utcnow().isoformat()
+ keys_file = os.fdopen(fd, 'wb')
+ keys_file.write(HEADER.format(len(all_active_keys), now))
+
+ for user_key in all_active_keys:
+ username = user_key.user.username
+ keys_file.write(
+ line_tmpl.format(
+ ssh_opts=ssh_opts or SSH_OPTS,
+ wrapper_command=ssh_wrapper_cmd,
+ user=username, key=user_key.ssh_key_data))
+ log.debug('addkey: Key added for user: `%s`', username)
+ keys_file.close()
+
+ # Explicitly setting read-only permissions to authorized_keys
+ os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
+ # Rename is atomic operation
+ os.rename(tmp_authorized_keys, authorized_keys_file_path)
+
+
+def generate_ssh_authorized_keys_file(registry):
+ log.info('Generating new authorized key file')
+
+ authorized_keys_file_path = registry.settings.get(
+ config_keys.authorized_keys_file_path)
+
+ ssh_wrapper_cmd = registry.settings.get(
+ config_keys.wrapper_cmd)
+ allow_shell = registry.settings.get(
+ config_keys.wrapper_allow_shell)
+ ssh_opts = registry.settings.get(
+ config_keys.authorized_keys_line_ssh_opts)
+
+ _generate_ssh_authorized_keys_file(
+ authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts)
+
+ return 0
diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py
--- a/rhodecode/config/middleware.py
+++ b/rhodecode/config/middleware.py
@@ -300,6 +300,7 @@ def includeme(config):
config.include('rhodecode.apps.user_profile')
config.include('rhodecode.apps.my_account')
config.include('rhodecode.apps.svn_support')
+ config.include('rhodecode.apps.ssh_support')
config.include('rhodecode.apps.gist')
config.include('rhodecode.apps.debug_style')