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 |
|
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 | ecdsa = super.buildPythonPackage { |
|
655 | ecdsa = super.buildPythonPackage { | |
656 |
name = "ecdsa-0.1 |
|
656 | name = "ecdsa-0.13"; | |
657 | buildInputs = with self; []; |
|
657 | buildInputs = with self; []; | |
658 | doCheck = false; |
|
658 | doCheck = false; | |
659 | propagatedBuildInputs = with self; []; |
|
659 | propagatedBuildInputs = with self; []; | |
660 | src = fetchurl { |
|
660 | src = fetchurl { | |
661 |
url = "https://pypi.python.org/packages/ |
|
661 | url = "https://pypi.python.org/packages/f9/e5/99ebb176e47f150ac115ffeda5fedb6a3dbb3c00c74a59fd84ddf12f5857/ecdsa-0.13.tar.gz"; | |
662 | md5 = "8ef586fe4dbb156697d756900cb41d7c"; |
|
662 | md5 = "1f60eda9cb5c46722856db41a3ae6670"; | |
663 | }; |
|
663 | }; | |
664 | meta = { |
|
664 | meta = { | |
665 | license = [ pkgs.lib.licenses.mit ]; |
|
665 | license = [ pkgs.lib.licenses.mit ]; | |
@@ -1172,19 +1172,6 b'' | |||||
1172 | license = [ pkgs.lib.licenses.bsdOriginal ]; |
|
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 | pathlib2 = super.buildPythonPackage { |
|
1175 | pathlib2 = super.buildPythonPackage { | |
1189 | name = "pathlib2-2.3.0"; |
|
1176 | name = "pathlib2-2.3.0"; | |
1190 | buildInputs = with self; []; |
|
1177 | buildInputs = with self; []; | |
@@ -1722,7 +1709,7 b'' | |||||
1722 | name = "rhodecode-enterprise-ce-4.9.0"; |
|
1709 | name = "rhodecode-enterprise-ce-4.9.0"; | |
1723 | buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj]; |
|
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 | doCheck = true; |
|
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 | src = ./.; |
|
1713 | src = ./.; | |
1727 | meta = { |
|
1714 | meta = { | |
1728 | license = [ { fullName = "Affero GNU General Public License v3 or later (AGPLv3+)"; } { fullName = "AGPLv3, and Commercial License"; } ]; |
|
1715 | license = [ { fullName = "Affero GNU General Public License v3 or later (AGPLv3+)"; } { fullName = "AGPLv3, and Commercial License"; } ]; | |
@@ -1832,6 +1819,19 b'' | |||||
1832 | license = [ pkgs.lib.licenses.mit ]; |
|
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 | subprocess32 = super.buildPythonPackage { |
|
1835 | subprocess32 = super.buildPythonPackage { | |
1836 | name = "subprocess32-3.2.7"; |
|
1836 | name = "subprocess32-3.2.7"; | |
1837 | buildInputs = with self; []; |
|
1837 | buildInputs = with self; []; |
@@ -19,7 +19,7 b' deform==2.0.4' | |||||
19 | docutils==0.13.1 |
|
19 | docutils==0.13.1 | |
20 | dogpile.cache==0.6.4 |
|
20 | dogpile.cache==0.6.4 | |
21 | dogpile.core==0.4.1 |
|
21 | dogpile.core==0.4.1 | |
22 |
ecdsa==0.1 |
|
22 | ecdsa==0.13 | |
23 | FormEncode==1.2.4 |
|
23 | FormEncode==1.2.4 | |
24 | future==0.14.3 |
|
24 | future==0.14.3 | |
25 | futures==3.0.2 |
|
25 | futures==3.0.2 | |
@@ -39,7 +39,6 b' MySQL-python==1.2.5' | |||||
39 | nose==1.3.6 |
|
39 | nose==1.3.6 | |
40 | objgraph==3.1.0 |
|
40 | objgraph==3.1.0 | |
41 | packaging==15.2 |
|
41 | packaging==15.2 | |
42 | paramiko==1.15.1 |
|
|||
43 | Paste==2.0.3 |
|
42 | Paste==2.0.3 | |
44 | PasteDeploy==1.5.2 |
|
43 | PasteDeploy==1.5.2 | |
45 | PasteScript==1.7.5 |
|
44 | PasteScript==1.7.5 | |
@@ -74,6 +73,7 b' simplejson==3.11.1' | |||||
74 | six==1.9.0 |
|
73 | six==1.9.0 | |
75 | Sphinx==1.2.2 |
|
74 | Sphinx==1.2.2 | |
76 | SQLAlchemy==1.1.11 |
|
75 | SQLAlchemy==1.1.11 | |
|
76 | sshpubkeys==2.2.0 | |||
77 | subprocess32==3.2.7 |
|
77 | subprocess32==3.2.7 | |
78 | supervisor==3.3.2 |
|
78 | supervisor==3.3.2 | |
79 | Tempita==0.5.2 |
|
79 | Tempita==0.5.2 |
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}' | |||||
51 | EXTENSIONS = {} |
|
51 | EXTENSIONS = {} | |
52 |
|
52 | |||
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
54 |
__dbversion__ = |
|
54 | __dbversion__ = 80 # defines current db version for migrations | |
55 | __platform__ = platform.system() |
|
55 | __platform__ = platform.system() | |
56 | __license__ = 'AGPLv3, and Commercial License' |
|
56 | __license__ = 'AGPLv3, and Commercial License' | |
57 | __author__ = 'RhodeCode GmbH' |
|
57 | __author__ = 'RhodeCode GmbH' |
@@ -126,6 +126,20 b' def admin_routes(config):' | |||||
126 | name='edit_user_auth_tokens_delete', |
|
126 | name='edit_user_auth_tokens_delete', | |
127 | pattern='/users/{user_id:\d+}/edit/auth_tokens/delete') |
|
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 | # user emails |
|
143 | # user emails | |
130 | config.add_route( |
|
144 | config.add_route( | |
131 | name='edit_user_emails', |
|
145 | name='edit_user_emails', |
@@ -25,6 +25,7 b' import formencode' | |||||
25 | from pyramid.httpexceptions import HTTPFound |
|
25 | from pyramid.httpexceptions import HTTPFound | |
26 | from pyramid.view import view_config |
|
26 | from pyramid.view import view_config | |
27 | from sqlalchemy.sql.functions import coalesce |
|
27 | from sqlalchemy.sql.functions import coalesce | |
|
28 | from sqlalchemy.exc import IntegrityError | |||
28 |
|
29 | |||
29 | from rhodecode.apps._base import BaseAppView, DataGridAppView |
|
30 | from rhodecode.apps._base import BaseAppView, DataGridAppView | |
30 |
|
31 | |||
@@ -35,9 +36,11 b' from rhodecode.lib.auth import (' | |||||
35 | from rhodecode.lib import helpers as h |
|
36 | from rhodecode.lib import helpers as h | |
36 | from rhodecode.lib.utils2 import safe_int, safe_unicode |
|
37 | from rhodecode.lib.utils2 import safe_int, safe_unicode | |
37 | from rhodecode.model.auth_token import AuthTokenModel |
|
38 | from rhodecode.model.auth_token import AuthTokenModel | |
|
39 | from rhodecode.model.ssh_key import SshKeyModel | |||
38 | from rhodecode.model.user import UserModel |
|
40 | from rhodecode.model.user import UserModel | |
39 | from rhodecode.model.user_group import UserGroupModel |
|
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 | from rhodecode.model.meta import Session |
|
44 | from rhodecode.model.meta import Session | |
42 |
|
45 | |||
43 | log = logging.getLogger(__name__) |
|
46 | log = logging.getLogger(__name__) | |
@@ -255,6 +258,123 b' class AdminUsersView(BaseAppView, DataGr' | |||||
255 | @LoginRequired() |
|
258 | @LoginRequired() | |
256 | @HasPermissionAllDecorator('hg.admin') |
|
259 | @HasPermissionAllDecorator('hg.admin') | |
257 | @view_config( |
|
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 | route_name='edit_user_emails', request_method='GET', |
|
378 | route_name='edit_user_emails', request_method='GET', | |
259 | renderer='rhodecode:templates/admin/users/user_edit.mako') |
|
379 | renderer='rhodecode:templates/admin/users/user_edit.mako') | |
260 | def emails(self): |
|
380 | def emails(self): |
@@ -46,6 +46,8 b' ACTIONS_V1 = {' | |||||
46 | 'user.edit.token.delete': {'token': {}, 'user': {}}, |
|
46 | 'user.edit.token.delete': {'token': {}, 'user': {}}, | |
47 | 'user.edit.email.add': {'email': ''}, |
|
47 | 'user.edit.email.add': {'email': ''}, | |
48 | 'user.edit.email.delete': {'email': ''}, |
|
48 | 'user.edit.email.delete': {'email': ''}, | |
|
49 | 'user.edit.ssh_key.add': {'token': {}, 'user': {}}, | |||
|
50 | 'user.edit.ssh_key.delete': {'token': {}, 'user': {}}, | |||
49 | 'user.edit.password_reset.enabled': {}, |
|
51 | 'user.edit.password_reset.enabled': {}, | |
50 | 'user.edit.password_reset.disabled': {}, |
|
52 | 'user.edit.password_reset.disabled': {}, | |
51 |
|
53 |
@@ -545,6 +545,8 b' class User(Base, BaseModel):' | |||||
545 | user_emails = relationship('UserEmailMap', cascade='all') |
|
545 | user_emails = relationship('UserEmailMap', cascade='all') | |
546 | user_ip_map = relationship('UserIpMap', cascade='all') |
|
546 | user_ip_map = relationship('UserIpMap', cascade='all') | |
547 | user_auth_tokens = relationship('UserApiKeys', cascade='all') |
|
547 | user_auth_tokens = relationship('UserApiKeys', cascade='all') | |
|
548 | user_ssh_keys = relationship('UserSshKeys', cascade='all') | |||
|
549 | ||||
548 | # gists |
|
550 | # gists | |
549 | user_gists = relationship('Gist', cascade='all') |
|
551 | user_gists = relationship('Gist', cascade='all') | |
550 | # user pull requests |
|
552 | # user pull requests | |
@@ -1143,6 +1145,43 b' class UserIpMap(Base, BaseModel):' | |||||
1143 | self.user_id, self.ip_addr) |
|
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 | class UserLog(Base, BaseModel): |
|
1185 | class UserLog(Base, BaseModel): | |
1147 | __tablename__ = 'user_logs' |
|
1186 | __tablename__ = 'user_logs' | |
1148 | __table_args__ = ( |
|
1187 | __table_args__ = ( |
@@ -62,6 +62,10 b' function registerRCRoutes() {' | |||||
62 | pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']); |
|
62 | pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']); | |
63 | pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']); |
|
63 | pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']); | |
64 | pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']); |
|
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 | pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']); |
|
69 | pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']); | |
66 | pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']); |
|
70 | pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']); | |
67 | pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']); |
|
71 | pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']); |
@@ -37,6 +37,7 b'' | |||||
37 | <ul class="nav nav-pills nav-stacked"> |
|
37 | <ul class="nav nav-pills nav-stacked"> | |
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> |
|
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 | <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> |
|
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 | <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 | <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 | <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 | <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 | <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> |
|
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> |
General Comments 0
You need to be logged in to leave comments.
Login now