##// END OF EJS Templates
users: added SSH key management for user admin pages
marcink -
r1993:dab53d0e default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,173 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.model.db import User, UserSshKeys
24
25 from rhodecode.tests import TestController, assert_session_flash
26 from rhodecode.tests.fixture import Fixture
27
28 fixture = Fixture()
29
30
31 def route_path(name, params=None, **kwargs):
32 import urllib
33 from rhodecode.apps._base import ADMIN_PREFIX
34
35 base_url = {
36 'edit_user_ssh_keys':
37 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 'edit_user_ssh_keys_generate_keypair':
39 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 'edit_user_ssh_keys_add':
41 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 'edit_user_ssh_keys_delete':
43 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44
45 }[name].format(**kwargs)
46
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 return base_url
50
51
52 class TestAdminUsersSshKeysView(TestController):
53 INVALID_KEY = """\
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 your_email@example.com
61 """
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 'your_email@example.com'
70
71 def test_ssh_keys_default_user(self):
72 self.log_user()
73 user = User.get_default_user()
74 self.app.get(
75 route_path('edit_user_ssh_keys', user_id=user.user_id),
76 status=302)
77
78 def test_add_ssh_key_error(self, user_util):
79 self.log_user()
80 user = user_util.create_user()
81 user_id = user.user_id
82
83 key_data = self.INVALID_KEY
84
85 desc = 'MY SSH KEY'
86 response = self.app.post(
87 route_path('edit_user_ssh_keys_add', user_id=user_id),
88 {'description': desc, 'key_data': key_data,
89 'csrf_token': self.csrf_token})
90 assert_session_flash(response, 'An error occurred during ssh '
91 'key saving: Unable to decode the key')
92
93 def test_ssh_key_duplicate(self, user_util):
94 self.log_user()
95 user = user_util.create_user()
96 user_id = user.user_id
97
98 key_data = self.VALID_KEY
99
100 desc = 'MY SSH KEY'
101 response = self.app.post(
102 route_path('edit_user_ssh_keys_add', user_id=user_id),
103 {'description': desc, 'key_data': key_data,
104 'csrf_token': self.csrf_token})
105 assert_session_flash(response, 'Ssh Key successfully created')
106 response.follow() # flush session flash
107
108 # add the same key AGAIN
109 desc = 'MY SSH KEY'
110 response = self.app.post(
111 route_path('edit_user_ssh_keys_add', user_id=user_id),
112 {'description': desc, 'key_data': key_data,
113 'csrf_token': self.csrf_token})
114 assert_session_flash(response, 'An error occurred during ssh key '
115 'saving: Such key already exists, '
116 'please use a different one')
117
118 def test_add_ssh_key(self, user_util):
119 self.log_user()
120 user = user_util.create_user()
121 user_id = user.user_id
122
123 key_data = self.VALID_KEY
124
125 desc = 'MY SSH KEY'
126 response = self.app.post(
127 route_path('edit_user_ssh_keys_add', user_id=user_id),
128 {'description': desc, 'key_data': key_data,
129 'csrf_token': self.csrf_token})
130 assert_session_flash(response, 'Ssh Key successfully created')
131
132 response = response.follow()
133 response.mustcontain(desc)
134
135 def test_delete_ssh_key(self, user_util):
136 self.log_user()
137 user = user_util.create_user()
138 user_id = user.user_id
139
140 key_data = self.VALID_KEY
141
142 desc = 'MY SSH KEY'
143 response = self.app.post(
144 route_path('edit_user_ssh_keys_add', user_id=user_id),
145 {'description': desc, 'key_data': key_data,
146 'csrf_token': self.csrf_token})
147 assert_session_flash(response, 'Ssh Key successfully created')
148 response = response.follow() # flush the Session flash
149
150 # now delete our key
151 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
152 assert 1 == len(keys)
153
154 response = self.app.post(
155 route_path('edit_user_ssh_keys_delete', user_id=user_id),
156 {'del_ssh_key': keys[0].ssh_key_id,
157 'csrf_token': self.csrf_token})
158
159 assert_session_flash(response, 'Ssh key successfully deleted')
160 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
161 assert 0 == len(keys)
162
163 def test_generate_keypair(self, user_util):
164 self.log_user()
165 user = user_util.create_user()
166 user_id = user.user_id
167
168 response = self.app.get(
169 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
170
171 response.mustcontain('Private key')
172 response.mustcontain('Public key')
173 response.mustcontain('-----BEGIN RSA PRIVATE KEY-----')
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,29 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_9_0_0 as db
17
18 db.UserSshKeys.__table__.create()
19
20 fixups(db, meta.Session)
21
22
23 def downgrade(migrate_engine):
24 meta = MetaData()
25 meta.bind = migrate_engine
26
27
28 def fixups(models, _SESSION):
29 pass
@@ -0,0 +1,123 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 import traceback
23
24 import sshpubkeys
25 import sshpubkeys.exceptions
26
27 from rhodecode.model import BaseModel
28 from rhodecode.model.db import UserSshKeys
29 from rhodecode.model.meta import Session
30
31 log = logging.getLogger(__name__)
32
33
34 class SshKeyModel(BaseModel):
35 cls = UserSshKeys
36
37 def parse_key(self, key_data):
38 """
39 print(ssh.bits) # 768
40 print(ssh.hash_md5()) # 56:84:1e:90:08:3b:60:c7:29:70:5f:5e:25:a6:3b:86
41 print(ssh.hash_sha256()) # SHA256:xk3IEJIdIoR9MmSRXTP98rjDdZocmXJje/28ohMQEwM
42 print(ssh.hash_sha512()) # SHA512:1C3lNBhjpDVQe39hnyy+xvlZYU3IPwzqK1rVneGavy6O3/ebjEQSFvmeWoyMTplIanmUK1hmr9nA8Skmj516HA
43 print(ssh.comment) # ojar@ojar-laptop
44 print(ssh.options_raw) # None (string of optional options at the beginning of public key)
45 print(ssh.options) # None (options as a dictionary, parsed and validated)
46
47 :param key_data:
48 :return:
49 """
50 ssh = sshpubkeys.SSHKey(strict_mode=True)
51 try:
52 ssh.parse(key_data)
53 return ssh
54 except sshpubkeys.exceptions.InvalidKeyException as err:
55 log.error("Invalid key: %s", err)
56 raise
57 except NotImplementedError as err:
58 log.error("Invalid key type: %s", err)
59 raise
60 except Exception as err:
61 log.error("Key Parse error: %s", err)
62 raise
63
64 def generate_keypair(self, comment=None):
65 from Crypto.PublicKey import RSA
66
67 key = RSA.generate(2048)
68 private = key.exportKey('PEM')
69
70 pubkey = key.publickey()
71 public = pubkey.exportKey('OpenSSH')
72 if comment:
73 public = public + " " + comment
74 return private, public
75
76 def create(self, user, fingerprint, key_data, description):
77 """
78 """
79 user = self._get_user(user)
80
81 new_ssh_key = UserSshKeys()
82 new_ssh_key.ssh_key_fingerprint = fingerprint
83 new_ssh_key.ssh_key_data = key_data
84 new_ssh_key.user_id = user.user_id
85 new_ssh_key.description = description
86
87 Session().add(new_ssh_key)
88
89 return new_ssh_key
90
91 def delete(self, ssh_key_id, user=None):
92 """
93 Deletes given api_key, if user is set it also filters the object for
94 deletion by given user.
95 """
96 ssh_key = UserSshKeys.query().filter(
97 UserSshKeys.ssh_key_id == ssh_key_id)
98
99 if user:
100 user = self._get_user(user)
101 ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id)
102 ssh_key = ssh_key.scalar()
103
104 if ssh_key:
105 try:
106 Session().delete(ssh_key)
107 except Exception:
108 log.error(traceback.format_exc())
109 raise
110
111 def get_ssh_keys(self, user):
112 user = self._get_user(user)
113 user_ssh_keys = UserSshKeys.query()\
114 .filter(UserSshKeys.user_id == user.user_id)
115 user_ssh_keys = user_ssh_keys.order_by(UserSshKeys.ssh_key_id)
116 return user_ssh_keys
117
118 def get_ssh_key_by_fingerprint(self, ssh_key_fingerprint):
119 user_ssh_key = UserSshKeys.query()\
120 .filter(UserSshKeys.ssh_key_fingerprint == ssh_key_fingerprint)\
121 .first()
122
123 return user_ssh_key
@@ -0,0 +1,78 b''
1 <div class="panel panel-default">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('SSH Keys')}</h3>
4 </div>
5 <div class="panel-body">
6 <div class="sshkeys_wrap">
7 <table class="rctable ssh_keys">
8 <tr>
9 <th>${_('Fingerprint')}</th>
10 <th>${_('Description')}</th>
11 <th>${_('Created')}</th>
12 <th>${_('Action')}</th>
13 </tr>
14 %if c.user_ssh_keys:
15 %for ssh_key in c.user_ssh_keys:
16 <tr class="">
17 <td class="">
18 <code>${ssh_key.ssh_key_fingerprint}</code>
19 </td>
20 <td class="td-wrap">${ssh_key.description}</td>
21 <td class="td-tags">${h.format_date(ssh_key.created_on)}</td>
22
23 <td class="td-action">
24 ${h.secure_form(h.route_path('edit_user_ssh_keys_delete', user_id=c.user.user_id), method='POST', request=request)}
25 ${h.hidden('del_ssh_key', ssh_key.ssh_key_id)}
26 <button class="btn btn-link btn-danger" type="submit"
27 onclick="return confirm('${_('Confirm to remove ssh key %s') % ssh_key.ssh_key_fingerprint}');">
28 ${_('Delete')}
29 </button>
30 ${h.end_form()}
31 </td>
32 </tr>
33 %endfor
34 %else:
35 <tr><td><div class="ip">${_('No additional ssh keys specified')}</div></td></tr>
36 %endif
37 </table>
38 </div>
39
40 <div class="user_ssh_keys">
41 ${h.secure_form(h.route_path('edit_user_ssh_keys_add', user_id=c.user.user_id), method='POST', request=request)}
42 <div class="form form-vertical">
43 <!-- fields -->
44 <div class="fields">
45 <div class="field">
46 <div class="label">
47 <label for="new_email">${_('New ssh key')}:</label>
48 </div>
49 <div class="input">
50 ${h.text('description', class_='medium', placeholder=_('Description'))}
51 <a href="${h.route_path('edit_user_ssh_keys_generate_keypair', user_id=c.user.user_id)}">${_('Generate random RSA key')}</a>
52 </div>
53 </div>
54
55 <div class="field">
56 <div class="textarea text-area editor">
57 ${h.textarea('key_data',c.default_key, size=30, placeholder=_("Public key, begins with 'ssh-rsa', 'ssh-dss', 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', or 'ecdsa-sha2-nistp521'"))}
58 </div>
59 </div>
60
61 <div class="buttons">
62 ${h.submit('save',_('Add'),class_="btn")}
63 ${h.reset('reset',_('Reset'),class_="btn")}
64 </div>
65 </div>
66 </div>
67 ${h.end_form()}
68 </div>
69 </div>
70 </div>
71
72 <script>
73
74 $(document).ready(function(){
75
76
77 });
78 </script>
@@ -0,0 +1,45 b''
1 <div class="panel panel-default">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('New SSH Key generated')}</h3>
4 </div>
5 <div class="panel-body">
6 <p>
7 ${_('Below is a 2048 bit generated SSH RSA key. You can use it to access RhodeCode via the SSH wrapper.')}
8 </p>
9 <h4>${_('Private key')}</h4>
10 <pre>
11 # Save the content as
12 ~/.ssh/id_rsa_rhodecode_access_priv.key
13 # Change permissions
14 chmod 0600 ~/.ssh/id_rsa_rhodecode_access_priv.key
15 </pre>
16
17 <div>
18 <textarea style="height: 300px">${c.private}</textarea>
19 </div>
20 <br/>
21
22
23 <h4>${_('Public key')}</h4>
24 <pre>
25 # Save the content as
26 ~/.ssh/id_rsa_rhodecode_access_pub.key
27 # Change permissions
28 chmod 0600 ~/.ssh/id_rsa_rhodecode_access_pub.key
29 </pre>
30
31 <input type="text" value="${c.public}" class="large text" size="100"/>
32 <p>
33 <a href="${h.route_path('edit_user_ssh_keys', user_id=c.user.user_id, _query=dict(default_key=c.public))}">${_('Add this generated key')}</a>
34 </p>
35
36 </div>
37 </div>
38
39 <script>
40
41 $(document).ready(function(){
42
43
44 });
45 </script>
@@ -653,13 +653,13 b''
653 653 };
654 654 };
655 655 ecdsa = super.buildPythonPackage {
656 name = "ecdsa-0.11";
656 name = "ecdsa-0.13";
657 657 buildInputs = with self; [];
658 658 doCheck = false;
659 659 propagatedBuildInputs = with self; [];
660 660 src = fetchurl {
661 url = "https://pypi.python.org/packages/6c/3f/92fe5dcdcaa7bd117be21e5520c9a54375112b66ec000d209e9e9519fad1/ecdsa-0.11.tar.gz";
662 md5 = "8ef586fe4dbb156697d756900cb41d7c";
661 url = "https://pypi.python.org/packages/f9/e5/99ebb176e47f150ac115ffeda5fedb6a3dbb3c00c74a59fd84ddf12f5857/ecdsa-0.13.tar.gz";
662 md5 = "1f60eda9cb5c46722856db41a3ae6670";
663 663 };
664 664 meta = {
665 665 license = [ pkgs.lib.licenses.mit ];
@@ -1172,19 +1172,6 b''
1172 1172 license = [ pkgs.lib.licenses.bsdOriginal ];
1173 1173 };
1174 1174 };
1175 paramiko = super.buildPythonPackage {
1176 name = "paramiko-1.15.1";
1177 buildInputs = with self; [];
1178 doCheck = false;
1179 propagatedBuildInputs = with self; [pycrypto ecdsa];
1180 src = fetchurl {
1181 url = "https://pypi.python.org/packages/04/2b/a22d2a560c1951abbbf95a0628e245945565f70dc082d9e784666887222c/paramiko-1.15.1.tar.gz";
1182 md5 = "48c274c3f9b1282932567b21f6acf3b5";
1183 };
1184 meta = {
1185 license = [ { fullName = "LGPL"; } { fullName = "GNU Library or Lesser General Public License (LGPL)"; } ];
1186 };
1187 };
1188 1175 pathlib2 = super.buildPythonPackage {
1189 1176 name = "pathlib2-2.3.0";
1190 1177 buildInputs = with self; [];
@@ -1722,7 +1709,7 b''
1722 1709 name = "rhodecode-enterprise-ce-4.9.0";
1723 1710 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
1724 1711 doCheck = true;
1725 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1712 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson sshpubkeys subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1726 1713 src = ./.;
1727 1714 meta = {
1728 1715 license = [ { fullName = "Affero GNU General Public License v3 or later (AGPLv3+)"; } { fullName = "AGPLv3, and Commercial License"; } ];
@@ -1832,6 +1819,19 b''
1832 1819 license = [ pkgs.lib.licenses.mit ];
1833 1820 };
1834 1821 };
1822 sshpubkeys = super.buildPythonPackage {
1823 name = "sshpubkeys-2.2.0";
1824 buildInputs = with self; [];
1825 doCheck = false;
1826 propagatedBuildInputs = with self; [pycrypto ecdsa];
1827 src = fetchurl {
1828 url = "https://pypi.python.org/packages/27/da/337fabeb3dca6b62039a93ceaa636f25065e0ae92b575b1235342076cf0a/sshpubkeys-2.2.0.tar.gz";
1829 md5 = "458e45f6b92b1afa84f0ffe1f1c90935";
1830 };
1831 meta = {
1832 license = [ pkgs.lib.licenses.bsdOriginal ];
1833 };
1834 };
1835 1835 subprocess32 = super.buildPythonPackage {
1836 1836 name = "subprocess32-3.2.7";
1837 1837 buildInputs = with self; [];
@@ -19,7 +19,7 b' deform==2.0.4'
19 19 docutils==0.13.1
20 20 dogpile.cache==0.6.4
21 21 dogpile.core==0.4.1
22 ecdsa==0.11
22 ecdsa==0.13
23 23 FormEncode==1.2.4
24 24 future==0.14.3
25 25 futures==3.0.2
@@ -39,7 +39,6 b' MySQL-python==1.2.5'
39 39 nose==1.3.6
40 40 objgraph==3.1.0
41 41 packaging==15.2
42 paramiko==1.15.1
43 42 Paste==2.0.3
44 43 PasteDeploy==1.5.2
45 44 PasteScript==1.7.5
@@ -74,6 +73,7 b' simplejson==3.11.1'
74 73 six==1.9.0
75 74 Sphinx==1.2.2
76 75 SQLAlchemy==1.1.11
76 sshpubkeys==2.2.0
77 77 subprocess32==3.2.7
78 78 supervisor==3.3.2
79 79 Tempita==0.5.2
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 79 # defines current db version for migrations
54 __dbversion__ = 80 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
@@ -126,6 +126,20 b' def admin_routes(config):'
126 126 name='edit_user_auth_tokens_delete',
127 127 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
128 128
129 # user ssh keys
130 config.add_route(
131 name='edit_user_ssh_keys',
132 pattern='/users/{user_id:\d+}/edit/ssh_keys')
133 config.add_route(
134 name='edit_user_ssh_keys_generate_keypair',
135 pattern='/users/{user_id:\d+}/edit/ssh_keys/generate')
136 config.add_route(
137 name='edit_user_ssh_keys_add',
138 pattern='/users/{user_id:\d+}/edit/ssh_keys/new')
139 config.add_route(
140 name='edit_user_ssh_keys_delete',
141 pattern='/users/{user_id:\d+}/edit/ssh_keys/delete')
142
129 143 # user emails
130 144 config.add_route(
131 145 name='edit_user_emails',
@@ -25,6 +25,7 b' import formencode'
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from sqlalchemy.sql.functions import coalesce
28 from sqlalchemy.exc import IntegrityError
28 29
29 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 31
@@ -35,9 +36,11 b' from rhodecode.lib.auth import ('
35 36 from rhodecode.lib import helpers as h
36 37 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 38 from rhodecode.model.auth_token import AuthTokenModel
39 from rhodecode.model.ssh_key import SshKeyModel
38 40 from rhodecode.model.user import UserModel
39 41 from rhodecode.model.user_group import UserGroupModel
40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
42 from rhodecode.model.db import (
43 or_, User, UserIpMap, UserEmailMap, UserApiKeys, UserSshKeys)
41 44 from rhodecode.model.meta import Session
42 45
43 46 log = logging.getLogger(__name__)
@@ -255,6 +258,123 b' class AdminUsersView(BaseAppView, DataGr'
255 258 @LoginRequired()
256 259 @HasPermissionAllDecorator('hg.admin')
257 260 @view_config(
261 route_name='edit_user_ssh_keys', request_method='GET',
262 renderer='rhodecode:templates/admin/users/user_edit.mako')
263 def ssh_keys(self):
264 _ = self.request.translate
265 c = self.load_default_context()
266
267 user_id = self.request.matchdict.get('user_id')
268 c.user = User.get_or_404(user_id)
269 self._redirect_for_default_user(c.user.username)
270
271 c.active = 'ssh_keys'
272 c.default_key = self.request.GET.get('default_key')
273 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
274 return self._get_template_context(c)
275
276 @LoginRequired()
277 @HasPermissionAllDecorator('hg.admin')
278 @view_config(
279 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
280 renderer='rhodecode:templates/admin/users/user_edit.mako')
281 def ssh_keys_generate_keypair(self):
282 _ = self.request.translate
283 c = self.load_default_context()
284
285 user_id = self.request.matchdict.get('user_id')
286 c.user = User.get_or_404(user_id)
287 self._redirect_for_default_user(c.user.username)
288
289 c.active = 'ssh_keys_generate'
290 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
291 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
292
293 return self._get_template_context(c)
294
295 @LoginRequired()
296 @HasPermissionAllDecorator('hg.admin')
297 @CSRFRequired()
298 @view_config(
299 route_name='edit_user_ssh_keys_add', request_method='POST')
300 def ssh_keys_add(self):
301 _ = self.request.translate
302 c = self.load_default_context()
303
304 user_id = self.request.matchdict.get('user_id')
305 c.user = User.get_or_404(user_id)
306
307 self._redirect_for_default_user(c.user.username)
308
309 user_data = c.user.get_api_data()
310 key_data = self.request.POST.get('key_data')
311 description = self.request.POST.get('description')
312
313 try:
314 if not key_data:
315 raise ValueError('Please add a valid public key')
316
317 key = SshKeyModel().parse_key(key_data.strip())
318 fingerprint = key.hash_md5()
319
320 ssh_key = SshKeyModel().create(
321 c.user.user_id, fingerprint, key_data, description)
322 ssh_key_data = ssh_key.get_api_data()
323
324 audit_logger.store_web(
325 'user.edit.ssh_key.add', action_data={
326 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
327 user=self._rhodecode_user, )
328 Session().commit()
329
330 h.flash(_("Ssh Key successfully created"), category='success')
331
332 except IntegrityError:
333 log.exception("Exception during ssh key saving")
334 h.flash(_('An error occurred during ssh key saving: {}').format(
335 'Such key already exists, please use a different one'),
336 category='error')
337 except Exception as e:
338 log.exception("Exception during ssh key saving")
339 h.flash(_('An error occurred during ssh key saving: {}').format(e),
340 category='error')
341
342 return HTTPFound(
343 h.route_path('edit_user_ssh_keys', user_id=user_id))
344
345 @LoginRequired()
346 @HasPermissionAllDecorator('hg.admin')
347 @CSRFRequired()
348 @view_config(
349 route_name='edit_user_ssh_keys_delete', request_method='POST')
350 def ssh_keys_delete(self):
351 _ = self.request.translate
352 c = self.load_default_context()
353
354 user_id = self.request.matchdict.get('user_id')
355 c.user = User.get_or_404(user_id)
356 self._redirect_for_default_user(c.user.username)
357 user_data = c.user.get_api_data()
358
359 del_ssh_key = self.request.POST.get('del_ssh_key')
360
361 if del_ssh_key:
362 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
363 ssh_key_data = ssh_key.get_api_data()
364
365 SshKeyModel().delete(del_ssh_key, c.user.user_id)
366 audit_logger.store_web(
367 'user.edit.ssh_key.delete', action_data={
368 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
369 user=self._rhodecode_user,)
370 Session().commit()
371 h.flash(_("Ssh key successfully deleted"), category='success')
372
373 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
374
375 @LoginRequired()
376 @HasPermissionAllDecorator('hg.admin')
377 @view_config(
258 378 route_name='edit_user_emails', request_method='GET',
259 379 renderer='rhodecode:templates/admin/users/user_edit.mako')
260 380 def emails(self):
@@ -46,6 +46,8 b' ACTIONS_V1 = {'
46 46 'user.edit.token.delete': {'token': {}, 'user': {}},
47 47 'user.edit.email.add': {'email': ''},
48 48 'user.edit.email.delete': {'email': ''},
49 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
50 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
49 51 'user.edit.password_reset.enabled': {},
50 52 'user.edit.password_reset.disabled': {},
51 53
@@ -545,6 +545,8 b' class User(Base, BaseModel):'
545 545 user_emails = relationship('UserEmailMap', cascade='all')
546 546 user_ip_map = relationship('UserIpMap', cascade='all')
547 547 user_auth_tokens = relationship('UserApiKeys', cascade='all')
548 user_ssh_keys = relationship('UserSshKeys', cascade='all')
549
548 550 # gists
549 551 user_gists = relationship('Gist', cascade='all')
550 552 # user pull requests
@@ -1143,6 +1145,43 b' class UserIpMap(Base, BaseModel):'
1143 1145 self.user_id, self.ip_addr)
1144 1146
1145 1147
1148 class UserSshKeys(Base, BaseModel):
1149 __tablename__ = 'user_ssh_keys'
1150 __table_args__ = (
1151 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1152
1153 UniqueConstraint('ssh_key_fingerprint'),
1154
1155 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1156 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1157 )
1158 __mapper_args__ = {}
1159
1160 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1161 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1162 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(1024), nullable=False, unique=None, default=None)
1163
1164 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1165
1166 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1167 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1168 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1169
1170 user = relationship('User', lazy='joined')
1171
1172 def __json__(self):
1173 data = {
1174 'ssh_fingerprint': self.ssh_key_fingerprint,
1175 'description': self.description,
1176 'created_on': self.created_on
1177 }
1178 return data
1179
1180 def get_api_data(self):
1181 data = self.__json__()
1182 return data
1183
1184
1146 1185 class UserLog(Base, BaseModel):
1147 1186 __tablename__ = 'user_logs'
1148 1187 __table_args__ = (
@@ -62,6 +62,10 b' function registerRCRoutes() {'
62 62 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
63 63 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
64 64 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
65 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
66 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
67 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
68 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
65 69 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
66 70 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
67 71 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
@@ -37,6 +37,7 b''
37 37 <ul class="nav nav-pills nav-stacked">
38 38 <li class="${'active' if c.active=='profile' else ''}"><a href="${h.url('edit_user', user_id=c.user.user_id)}">${_('User Profile')}</a></li>
39 39 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.route_path('edit_user_auth_tokens', user_id=c.user.user_id)}">${_('Auth tokens')}</a></li>
40 <li class="${'active' if c.active in ['ssh_keys','ssh_keys_generate'] else ''}"><a href="${h.route_path('edit_user_ssh_keys', user_id=c.user.user_id)}">${_('SSH Keys')}</a></li>
40 41 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.url('edit_user_advanced', user_id=c.user.user_id)}">${_('Advanced')}</a></li>
41 42 <li class="${'active' if c.active=='global_perms' else ''}"><a href="${h.url('edit_user_global_perms', user_id=c.user.user_id)}">${_('Global permissions')}</a></li>
42 43 <li class="${'active' if c.active=='perms_summary' else ''}"><a href="${h.url('edit_user_perms_summary', user_id=c.user.user_id)}">${_('Permissions summary')}</a></li>
@@ -134,6 +134,7 b' install_requirements = ['
134 134 'repoze.lru',
135 135 'requests',
136 136 'simplejson',
137 'sshpubkeys',
137 138 'subprocess32',
138 139 'waitress',
139 140 'zope.cachedescriptors',
General Comments 0
You need to be logged in to leave comments. Login now