##// END OF EJS Templates
account: convert change password form to colander schema and fix bug...
dan -
r665:a92da8f2 default
parent child Browse files
Show More
@@ -0,0 +1,33 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 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 """
22 Base module for form rendering / validation - currently just a wrapper for
23 deform - later can be replaced with something custom.
24 """
25
26 from rhodecode.translation import _
27 from deform import Button, Form, widget, ValidationFailure
28
29
30 class buttons:
31 save = Button(name='Save', type='submit')
32 reset = Button(name=_('Reset'), type='reset')
33 delete = Button(name=_('Delete'), type='submit')
@@ -0,0 +1,61 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 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 colander
22
23 from rhodecode import forms
24 from rhodecode.model.db import User
25 from rhodecode.translation import _
26 from rhodecode.lib.auth import check_password
27
28
29 @colander.deferred
30 def deferred_user_password_validator(node, kw):
31 username = kw.get('username')
32 user = User.get_by_username(username)
33
34 def _user_password_validator(node, value):
35 if not check_password(value, user.password):
36 msg = _('Password is incorrect')
37 raise colander.Invalid(node, msg)
38 return _user_password_validator
39
40
41 class ChangePasswordSchema(colander.Schema):
42
43 current_password = colander.SchemaNode(
44 colander.String(),
45 missing=colander.required,
46 widget=forms.widget.PasswordWidget(redisplay=True),
47 validator=deferred_user_password_validator)
48
49 new_password = colander.SchemaNode(
50 colander.String(),
51 missing=colander.required,
52 widget=forms.widget.CheckedPasswordWidget(redisplay=True),
53 validator=colander.Length(min=6))
54
55
56 def validator(self, form, values):
57 if values['current_password'] == values['new_password']:
58 exc = colander.Invalid(form)
59 exc['new_password'] = _('New password must be different '
60 'to old password')
61 raise exc
@@ -0,0 +1,10 b''
1 <%def name="panel(title, class_='default')">
2 <div class="panel panel-${class_}">
3 <div class="panel-heading">
4 <h3 class="panel-title">${title}</h3>
5 </div>
6 <div class="panel-body">
7 ${caller.body()}
8 </div>
9 </div>
10 </%def>
@@ -0,0 +1,72 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 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 colander
22 import pytest
23
24 from rhodecode.model import validation_schema
25 from rhodecode.model.validation_schema.schemas import user_schema
26
27
28 class TestChangePasswordSchema(object):
29 original_password = 'm092d903fnio0m'
30
31 def test_deserialize_bad_data(self, user_regular):
32 schema = user_schema.ChangePasswordSchema().bind(
33 username=user_regular.username)
34
35 with pytest.raises(validation_schema.Invalid) as exc_info:
36 schema.deserialize('err')
37 err = exc_info.value.asdict()
38 assert err[''] == '"err" is not a mapping type: ' \
39 'Does not implement dict-like functionality.'
40
41 def test_validate_valid_change_password_data(self, user_util):
42 user = user_util.create_user(password=self.original_password)
43 schema = user_schema.ChangePasswordSchema().bind(
44 username=user.username)
45
46 schema.deserialize({
47 'current_password': self.original_password,
48 'new_password': '23jf04rm04imr'
49 })
50
51 @pytest.mark.parametrize(
52 'current_password,new_password,key,message', [
53 ('', 'abcdef123', 'current_password', 'required'),
54 ('wrong_pw', 'abcdef123', 'current_password', 'incorrect'),
55 (original_password, original_password, 'new_password', 'different'),
56 (original_password, '', 'new_password', 'Required'),
57 (original_password, 'short', 'new_password', 'minimum'),
58 ])
59 def test_validate_invalid_change_password_data(self, current_password,
60 new_password, key, message,
61 user_util):
62 user = user_util.create_user(password=self.original_password)
63 schema = user_schema.ChangePasswordSchema().bind(
64 username=user.username)
65
66 with pytest.raises(validation_schema.Invalid) as exc_info:
67 schema.deserialize({
68 'current_password': current_password,
69 'new_password': new_password
70 })
71 err = exc_info.value.asdict()
72 assert message.lower() in err[key].lower()
@@ -1,1161 +1,1159 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 # repo names can have a slash in them, but they must not end with a slash
46 46 'repo_name': r'.*?[^/]',
47 47 # file path eats up everything at the end
48 48 'f_path': r'.*',
49 49 # reference types
50 50 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
51 51 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
52 52 }
53 53
54 54
55 55 def add_route_requirements(route_path, requirements):
56 56 """
57 57 Adds regex requirements to pyramid routes using a mapping dict
58 58
59 59 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
60 60 '/{action}/{id:\d+}'
61 61
62 62 """
63 63 for key, regex in requirements.items():
64 64 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
65 65 return route_path
66 66
67 67
68 68 class JSRoutesMapper(Mapper):
69 69 """
70 70 Wrapper for routes.Mapper to make pyroutes compatible url definitions
71 71 """
72 72 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
73 73 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
74 74 def __init__(self, *args, **kw):
75 75 super(JSRoutesMapper, self).__init__(*args, **kw)
76 76 self._jsroutes = []
77 77
78 78 def connect(self, *args, **kw):
79 79 """
80 80 Wrapper for connect to take an extra argument jsroute=True
81 81
82 82 :param jsroute: boolean, if True will add the route to the pyroutes list
83 83 """
84 84 if kw.pop('jsroute', False):
85 85 if not self._named_route_regex.match(args[0]):
86 86 raise Exception('only named routes can be added to pyroutes')
87 87 self._jsroutes.append(args[0])
88 88
89 89 super(JSRoutesMapper, self).connect(*args, **kw)
90 90
91 91 def _extract_route_information(self, route):
92 92 """
93 93 Convert a route into tuple(name, path, args), eg:
94 94 ('user_profile', '/profile/%(username)s', ['username'])
95 95 """
96 96 routepath = route.routepath
97 97 def replace(matchobj):
98 98 if matchobj.group(1):
99 99 return "%%(%s)s" % matchobj.group(1).split(':')[0]
100 100 else:
101 101 return "%%(%s)s" % matchobj.group(2)
102 102
103 103 routepath = self._argument_prog.sub(replace, routepath)
104 104 return (
105 105 route.name,
106 106 routepath,
107 107 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
108 108 for arg in self._argument_prog.findall(route.routepath)]
109 109 )
110 110
111 111 def jsroutes(self):
112 112 """
113 113 Return a list of pyroutes.js compatible routes
114 114 """
115 115 for route_name in self._jsroutes:
116 116 yield self._extract_route_information(self._routenames[route_name])
117 117
118 118
119 119 def make_map(config):
120 120 """Create, configure and return the routes Mapper"""
121 121 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
122 122 always_scan=config['debug'])
123 123 rmap.minimization = False
124 124 rmap.explicit = False
125 125
126 126 from rhodecode.lib.utils2 import str2bool
127 127 from rhodecode.model import repo, repo_group
128 128
129 129 def check_repo(environ, match_dict):
130 130 """
131 131 check for valid repository for proper 404 handling
132 132
133 133 :param environ:
134 134 :param match_dict:
135 135 """
136 136 repo_name = match_dict.get('repo_name')
137 137
138 138 if match_dict.get('f_path'):
139 139 # fix for multiple initial slashes that causes errors
140 140 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 141 repo_model = repo.RepoModel()
142 142 by_name_match = repo_model.get_by_repo_name(repo_name)
143 143 # if we match quickly from database, short circuit the operation,
144 144 # and validate repo based on the type.
145 145 if by_name_match:
146 146 return True
147 147
148 148 by_id_match = repo_model.get_repo_by_id(repo_name)
149 149 if by_id_match:
150 150 repo_name = by_id_match.repo_name
151 151 match_dict['repo_name'] = repo_name
152 152 return True
153 153
154 154 return False
155 155
156 156 def check_group(environ, match_dict):
157 157 """
158 158 check for valid repository group path for proper 404 handling
159 159
160 160 :param environ:
161 161 :param match_dict:
162 162 """
163 163 repo_group_name = match_dict.get('group_name')
164 164 repo_group_model = repo_group.RepoGroupModel()
165 165 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 166 if by_name_match:
167 167 return True
168 168
169 169 return False
170 170
171 171 def check_user_group(environ, match_dict):
172 172 """
173 173 check for valid user group for proper 404 handling
174 174
175 175 :param environ:
176 176 :param match_dict:
177 177 """
178 178 return True
179 179
180 180 def check_int(environ, match_dict):
181 181 return match_dict.get('id').isdigit()
182 182
183 183
184 184 #==========================================================================
185 185 # CUSTOM ROUTES HERE
186 186 #==========================================================================
187 187
188 188 # MAIN PAGE
189 189 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
190 190 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
191 191 action='goto_switcher_data')
192 192 rmap.connect('repo_list_data', '/_repos', controller='home',
193 193 action='repo_list_data')
194 194
195 195 rmap.connect('user_autocomplete_data', '/_users', controller='home',
196 196 action='user_autocomplete_data', jsroute=True)
197 197 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
198 198 action='user_group_autocomplete_data')
199 199
200 200 rmap.connect(
201 201 'user_profile', '/_profiles/{username}', controller='users',
202 202 action='user_profile')
203 203
204 204 # TODO: johbo: Static links, to be replaced by our redirection mechanism
205 205 rmap.connect('rst_help',
206 206 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
207 207 _static=True)
208 208 rmap.connect('markdown_help',
209 209 'http://daringfireball.net/projects/markdown/syntax',
210 210 _static=True)
211 211 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
212 212 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
213 213 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
214 214 # TODO: anderson - making this a static link since redirect won't play
215 215 # nice with POST requests
216 216 rmap.connect('enterprise_license_convert_from_old',
217 217 'https://rhodecode.com/u/license-upgrade',
218 218 _static=True)
219 219
220 220 routing_links.connect_redirection_links(rmap)
221 221
222 222 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
223 223 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
224 224
225 225 # ADMIN REPOSITORY ROUTES
226 226 with rmap.submapper(path_prefix=ADMIN_PREFIX,
227 227 controller='admin/repos') as m:
228 228 m.connect('repos', '/repos',
229 229 action='create', conditions={'method': ['POST']})
230 230 m.connect('repos', '/repos',
231 231 action='index', conditions={'method': ['GET']})
232 232 m.connect('new_repo', '/create_repository', jsroute=True,
233 233 action='create_repository', conditions={'method': ['GET']})
234 234 m.connect('/repos/{repo_name}',
235 235 action='update', conditions={'method': ['PUT'],
236 236 'function': check_repo},
237 237 requirements=URL_NAME_REQUIREMENTS)
238 238 m.connect('delete_repo', '/repos/{repo_name}',
239 239 action='delete', conditions={'method': ['DELETE']},
240 240 requirements=URL_NAME_REQUIREMENTS)
241 241 m.connect('repo', '/repos/{repo_name}',
242 242 action='show', conditions={'method': ['GET'],
243 243 'function': check_repo},
244 244 requirements=URL_NAME_REQUIREMENTS)
245 245
246 246 # ADMIN REPOSITORY GROUPS ROUTES
247 247 with rmap.submapper(path_prefix=ADMIN_PREFIX,
248 248 controller='admin/repo_groups') as m:
249 249 m.connect('repo_groups', '/repo_groups',
250 250 action='create', conditions={'method': ['POST']})
251 251 m.connect('repo_groups', '/repo_groups',
252 252 action='index', conditions={'method': ['GET']})
253 253 m.connect('new_repo_group', '/repo_groups/new',
254 254 action='new', conditions={'method': ['GET']})
255 255 m.connect('update_repo_group', '/repo_groups/{group_name}',
256 256 action='update', conditions={'method': ['PUT'],
257 257 'function': check_group},
258 258 requirements=URL_NAME_REQUIREMENTS)
259 259
260 260 # EXTRAS REPO GROUP ROUTES
261 261 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
262 262 action='edit',
263 263 conditions={'method': ['GET'], 'function': check_group},
264 264 requirements=URL_NAME_REQUIREMENTS)
265 265 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
266 266 action='edit',
267 267 conditions={'method': ['PUT'], 'function': check_group},
268 268 requirements=URL_NAME_REQUIREMENTS)
269 269
270 270 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
271 271 action='edit_repo_group_advanced',
272 272 conditions={'method': ['GET'], 'function': check_group},
273 273 requirements=URL_NAME_REQUIREMENTS)
274 274 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
275 275 action='edit_repo_group_advanced',
276 276 conditions={'method': ['PUT'], 'function': check_group},
277 277 requirements=URL_NAME_REQUIREMENTS)
278 278
279 279 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
280 280 action='edit_repo_group_perms',
281 281 conditions={'method': ['GET'], 'function': check_group},
282 282 requirements=URL_NAME_REQUIREMENTS)
283 283 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
284 284 action='update_perms',
285 285 conditions={'method': ['PUT'], 'function': check_group},
286 286 requirements=URL_NAME_REQUIREMENTS)
287 287
288 288 m.connect('delete_repo_group', '/repo_groups/{group_name}',
289 289 action='delete', conditions={'method': ['DELETE'],
290 290 'function': check_group},
291 291 requirements=URL_NAME_REQUIREMENTS)
292 292
293 293 # ADMIN USER ROUTES
294 294 with rmap.submapper(path_prefix=ADMIN_PREFIX,
295 295 controller='admin/users') as m:
296 296 m.connect('users', '/users',
297 297 action='create', conditions={'method': ['POST']})
298 298 m.connect('users', '/users',
299 299 action='index', conditions={'method': ['GET']})
300 300 m.connect('new_user', '/users/new',
301 301 action='new', conditions={'method': ['GET']})
302 302 m.connect('update_user', '/users/{user_id}',
303 303 action='update', conditions={'method': ['PUT']})
304 304 m.connect('delete_user', '/users/{user_id}',
305 305 action='delete', conditions={'method': ['DELETE']})
306 306 m.connect('edit_user', '/users/{user_id}/edit',
307 307 action='edit', conditions={'method': ['GET']})
308 308 m.connect('user', '/users/{user_id}',
309 309 action='show', conditions={'method': ['GET']})
310 310 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
311 311 action='reset_password', conditions={'method': ['POST']})
312 312 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
313 313 action='create_personal_repo_group', conditions={'method': ['POST']})
314 314
315 315 # EXTRAS USER ROUTES
316 316 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
317 317 action='edit_advanced', conditions={'method': ['GET']})
318 318 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
319 319 action='update_advanced', conditions={'method': ['PUT']})
320 320
321 321 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
322 322 action='edit_auth_tokens', conditions={'method': ['GET']})
323 323 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
324 324 action='add_auth_token', conditions={'method': ['PUT']})
325 325 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
326 326 action='delete_auth_token', conditions={'method': ['DELETE']})
327 327
328 328 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
329 329 action='edit_global_perms', conditions={'method': ['GET']})
330 330 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
331 331 action='update_global_perms', conditions={'method': ['PUT']})
332 332
333 333 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
334 334 action='edit_perms_summary', conditions={'method': ['GET']})
335 335
336 336 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
337 337 action='edit_emails', conditions={'method': ['GET']})
338 338 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
339 339 action='add_email', conditions={'method': ['PUT']})
340 340 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
341 341 action='delete_email', conditions={'method': ['DELETE']})
342 342
343 343 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
344 344 action='edit_ips', conditions={'method': ['GET']})
345 345 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
346 346 action='add_ip', conditions={'method': ['PUT']})
347 347 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
348 348 action='delete_ip', conditions={'method': ['DELETE']})
349 349
350 350 # ADMIN USER GROUPS REST ROUTES
351 351 with rmap.submapper(path_prefix=ADMIN_PREFIX,
352 352 controller='admin/user_groups') as m:
353 353 m.connect('users_groups', '/user_groups',
354 354 action='create', conditions={'method': ['POST']})
355 355 m.connect('users_groups', '/user_groups',
356 356 action='index', conditions={'method': ['GET']})
357 357 m.connect('new_users_group', '/user_groups/new',
358 358 action='new', conditions={'method': ['GET']})
359 359 m.connect('update_users_group', '/user_groups/{user_group_id}',
360 360 action='update', conditions={'method': ['PUT']})
361 361 m.connect('delete_users_group', '/user_groups/{user_group_id}',
362 362 action='delete', conditions={'method': ['DELETE']})
363 363 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
364 364 action='edit', conditions={'method': ['GET']},
365 365 function=check_user_group)
366 366
367 367 # EXTRAS USER GROUP ROUTES
368 368 m.connect('edit_user_group_global_perms',
369 369 '/user_groups/{user_group_id}/edit/global_permissions',
370 370 action='edit_global_perms', conditions={'method': ['GET']})
371 371 m.connect('edit_user_group_global_perms',
372 372 '/user_groups/{user_group_id}/edit/global_permissions',
373 373 action='update_global_perms', conditions={'method': ['PUT']})
374 374 m.connect('edit_user_group_perms_summary',
375 375 '/user_groups/{user_group_id}/edit/permissions_summary',
376 376 action='edit_perms_summary', conditions={'method': ['GET']})
377 377
378 378 m.connect('edit_user_group_perms',
379 379 '/user_groups/{user_group_id}/edit/permissions',
380 380 action='edit_perms', conditions={'method': ['GET']})
381 381 m.connect('edit_user_group_perms',
382 382 '/user_groups/{user_group_id}/edit/permissions',
383 383 action='update_perms', conditions={'method': ['PUT']})
384 384
385 385 m.connect('edit_user_group_advanced',
386 386 '/user_groups/{user_group_id}/edit/advanced',
387 387 action='edit_advanced', conditions={'method': ['GET']})
388 388
389 389 m.connect('edit_user_group_members',
390 390 '/user_groups/{user_group_id}/edit/members', jsroute=True,
391 391 action='edit_members', conditions={'method': ['GET']})
392 392
393 393 # ADMIN PERMISSIONS ROUTES
394 394 with rmap.submapper(path_prefix=ADMIN_PREFIX,
395 395 controller='admin/permissions') as m:
396 396 m.connect('admin_permissions_application', '/permissions/application',
397 397 action='permission_application_update', conditions={'method': ['POST']})
398 398 m.connect('admin_permissions_application', '/permissions/application',
399 399 action='permission_application', conditions={'method': ['GET']})
400 400
401 401 m.connect('admin_permissions_global', '/permissions/global',
402 402 action='permission_global_update', conditions={'method': ['POST']})
403 403 m.connect('admin_permissions_global', '/permissions/global',
404 404 action='permission_global', conditions={'method': ['GET']})
405 405
406 406 m.connect('admin_permissions_object', '/permissions/object',
407 407 action='permission_objects_update', conditions={'method': ['POST']})
408 408 m.connect('admin_permissions_object', '/permissions/object',
409 409 action='permission_objects', conditions={'method': ['GET']})
410 410
411 411 m.connect('admin_permissions_ips', '/permissions/ips',
412 412 action='permission_ips', conditions={'method': ['POST']})
413 413 m.connect('admin_permissions_ips', '/permissions/ips',
414 414 action='permission_ips', conditions={'method': ['GET']})
415 415
416 416 m.connect('admin_permissions_overview', '/permissions/overview',
417 417 action='permission_perms', conditions={'method': ['GET']})
418 418
419 419 # ADMIN DEFAULTS REST ROUTES
420 420 with rmap.submapper(path_prefix=ADMIN_PREFIX,
421 421 controller='admin/defaults') as m:
422 422 m.connect('admin_defaults_repositories', '/defaults/repositories',
423 423 action='update_repository_defaults', conditions={'method': ['POST']})
424 424 m.connect('admin_defaults_repositories', '/defaults/repositories',
425 425 action='index', conditions={'method': ['GET']})
426 426
427 427 # ADMIN DEBUG STYLE ROUTES
428 428 if str2bool(config.get('debug_style')):
429 429 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
430 430 controller='debug_style') as m:
431 431 m.connect('debug_style_home', '',
432 432 action='index', conditions={'method': ['GET']})
433 433 m.connect('debug_style_template', '/t/{t_path}',
434 434 action='template', conditions={'method': ['GET']})
435 435
436 436 # ADMIN SETTINGS ROUTES
437 437 with rmap.submapper(path_prefix=ADMIN_PREFIX,
438 438 controller='admin/settings') as m:
439 439
440 440 # default
441 441 m.connect('admin_settings', '/settings',
442 442 action='settings_global_update',
443 443 conditions={'method': ['POST']})
444 444 m.connect('admin_settings', '/settings',
445 445 action='settings_global', conditions={'method': ['GET']})
446 446
447 447 m.connect('admin_settings_vcs', '/settings/vcs',
448 448 action='settings_vcs_update',
449 449 conditions={'method': ['POST']})
450 450 m.connect('admin_settings_vcs', '/settings/vcs',
451 451 action='settings_vcs',
452 452 conditions={'method': ['GET']})
453 453 m.connect('admin_settings_vcs', '/settings/vcs',
454 454 action='delete_svn_pattern',
455 455 conditions={'method': ['DELETE']})
456 456
457 457 m.connect('admin_settings_mapping', '/settings/mapping',
458 458 action='settings_mapping_update',
459 459 conditions={'method': ['POST']})
460 460 m.connect('admin_settings_mapping', '/settings/mapping',
461 461 action='settings_mapping', conditions={'method': ['GET']})
462 462
463 463 m.connect('admin_settings_global', '/settings/global',
464 464 action='settings_global_update',
465 465 conditions={'method': ['POST']})
466 466 m.connect('admin_settings_global', '/settings/global',
467 467 action='settings_global', conditions={'method': ['GET']})
468 468
469 469 m.connect('admin_settings_visual', '/settings/visual',
470 470 action='settings_visual_update',
471 471 conditions={'method': ['POST']})
472 472 m.connect('admin_settings_visual', '/settings/visual',
473 473 action='settings_visual', conditions={'method': ['GET']})
474 474
475 475 m.connect('admin_settings_issuetracker',
476 476 '/settings/issue-tracker', action='settings_issuetracker',
477 477 conditions={'method': ['GET']})
478 478 m.connect('admin_settings_issuetracker_save',
479 479 '/settings/issue-tracker/save',
480 480 action='settings_issuetracker_save',
481 481 conditions={'method': ['POST']})
482 482 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
483 483 action='settings_issuetracker_test',
484 484 conditions={'method': ['POST']})
485 485 m.connect('admin_issuetracker_delete',
486 486 '/settings/issue-tracker/delete',
487 487 action='settings_issuetracker_delete',
488 488 conditions={'method': ['DELETE']})
489 489
490 490 m.connect('admin_settings_email', '/settings/email',
491 491 action='settings_email_update',
492 492 conditions={'method': ['POST']})
493 493 m.connect('admin_settings_email', '/settings/email',
494 494 action='settings_email', conditions={'method': ['GET']})
495 495
496 496 m.connect('admin_settings_hooks', '/settings/hooks',
497 497 action='settings_hooks_update',
498 498 conditions={'method': ['POST', 'DELETE']})
499 499 m.connect('admin_settings_hooks', '/settings/hooks',
500 500 action='settings_hooks', conditions={'method': ['GET']})
501 501
502 502 m.connect('admin_settings_search', '/settings/search',
503 503 action='settings_search', conditions={'method': ['GET']})
504 504
505 505 m.connect('admin_settings_system', '/settings/system',
506 506 action='settings_system', conditions={'method': ['GET']})
507 507
508 508 m.connect('admin_settings_system_update', '/settings/system/updates',
509 509 action='settings_system_update', conditions={'method': ['GET']})
510 510
511 511 m.connect('admin_settings_supervisor', '/settings/supervisor',
512 512 action='settings_supervisor', conditions={'method': ['GET']})
513 513 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
514 514 action='settings_supervisor_log', conditions={'method': ['GET']})
515 515
516 516 m.connect('admin_settings_labs', '/settings/labs',
517 517 action='settings_labs_update',
518 518 conditions={'method': ['POST']})
519 519 m.connect('admin_settings_labs', '/settings/labs',
520 520 action='settings_labs', conditions={'method': ['GET']})
521 521
522 522 # ADMIN MY ACCOUNT
523 523 with rmap.submapper(path_prefix=ADMIN_PREFIX,
524 524 controller='admin/my_account') as m:
525 525
526 526 m.connect('my_account', '/my_account',
527 527 action='my_account', conditions={'method': ['GET']})
528 528 m.connect('my_account_edit', '/my_account/edit',
529 529 action='my_account_edit', conditions={'method': ['GET']})
530 530 m.connect('my_account', '/my_account',
531 531 action='my_account_update', conditions={'method': ['POST']})
532 532
533 533 m.connect('my_account_password', '/my_account/password',
534 action='my_account_password', conditions={'method': ['GET']})
535 m.connect('my_account_password', '/my_account/password',
536 action='my_account_password_update', conditions={'method': ['POST']})
534 action='my_account_password', conditions={'method': ['GET', 'POST']})
537 535
538 536 m.connect('my_account_repos', '/my_account/repos',
539 537 action='my_account_repos', conditions={'method': ['GET']})
540 538
541 539 m.connect('my_account_watched', '/my_account/watched',
542 540 action='my_account_watched', conditions={'method': ['GET']})
543 541
544 542 m.connect('my_account_pullrequests', '/my_account/pull_requests',
545 543 action='my_account_pullrequests', conditions={'method': ['GET']})
546 544
547 545 m.connect('my_account_perms', '/my_account/perms',
548 546 action='my_account_perms', conditions={'method': ['GET']})
549 547
550 548 m.connect('my_account_emails', '/my_account/emails',
551 549 action='my_account_emails', conditions={'method': ['GET']})
552 550 m.connect('my_account_emails', '/my_account/emails',
553 551 action='my_account_emails_add', conditions={'method': ['POST']})
554 552 m.connect('my_account_emails', '/my_account/emails',
555 553 action='my_account_emails_delete', conditions={'method': ['DELETE']})
556 554
557 555 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
558 556 action='my_account_auth_tokens', conditions={'method': ['GET']})
559 557 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
560 558 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
561 559 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
562 560 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
563 561 m.connect('my_account_notifications', '/my_account/notifications',
564 562 action='my_notifications',
565 563 conditions={'method': ['GET']})
566 564 m.connect('my_account_notifications_toggle_visibility',
567 565 '/my_account/toggle_visibility',
568 566 action='my_notifications_toggle_visibility',
569 567 conditions={'method': ['POST']})
570 568
571 569 # NOTIFICATION REST ROUTES
572 570 with rmap.submapper(path_prefix=ADMIN_PREFIX,
573 571 controller='admin/notifications') as m:
574 572 m.connect('notifications', '/notifications',
575 573 action='index', conditions={'method': ['GET']})
576 574 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
577 575 action='mark_all_read', conditions={'method': ['POST']})
578 576 m.connect('/notifications/{notification_id}',
579 577 action='update', conditions={'method': ['PUT']})
580 578 m.connect('/notifications/{notification_id}',
581 579 action='delete', conditions={'method': ['DELETE']})
582 580 m.connect('notification', '/notifications/{notification_id}',
583 581 action='show', conditions={'method': ['GET']})
584 582
585 583 # ADMIN GIST
586 584 with rmap.submapper(path_prefix=ADMIN_PREFIX,
587 585 controller='admin/gists') as m:
588 586 m.connect('gists', '/gists',
589 587 action='create', conditions={'method': ['POST']})
590 588 m.connect('gists', '/gists', jsroute=True,
591 589 action='index', conditions={'method': ['GET']})
592 590 m.connect('new_gist', '/gists/new', jsroute=True,
593 591 action='new', conditions={'method': ['GET']})
594 592
595 593 m.connect('/gists/{gist_id}',
596 594 action='delete', conditions={'method': ['DELETE']})
597 595 m.connect('edit_gist', '/gists/{gist_id}/edit',
598 596 action='edit_form', conditions={'method': ['GET']})
599 597 m.connect('edit_gist', '/gists/{gist_id}/edit',
600 598 action='edit', conditions={'method': ['POST']})
601 599 m.connect(
602 600 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
603 601 action='check_revision', conditions={'method': ['GET']})
604 602
605 603 m.connect('gist', '/gists/{gist_id}',
606 604 action='show', conditions={'method': ['GET']})
607 605 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
608 606 revision='tip',
609 607 action='show', conditions={'method': ['GET']})
610 608 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
611 609 revision='tip',
612 610 action='show', conditions={'method': ['GET']})
613 611 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
614 612 revision='tip',
615 613 action='show', conditions={'method': ['GET']},
616 614 requirements=URL_NAME_REQUIREMENTS)
617 615
618 616 # ADMIN MAIN PAGES
619 617 with rmap.submapper(path_prefix=ADMIN_PREFIX,
620 618 controller='admin/admin') as m:
621 619 m.connect('admin_home', '', action='index')
622 620 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
623 621 action='add_repo')
624 622 m.connect(
625 623 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
626 624 action='pull_requests')
627 625 m.connect(
628 626 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
629 627 action='pull_requests')
630 628
631 629
632 630 # USER JOURNAL
633 631 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
634 632 controller='journal', action='index')
635 633 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
636 634 controller='journal', action='journal_rss')
637 635 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
638 636 controller='journal', action='journal_atom')
639 637
640 638 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
641 639 controller='journal', action='public_journal')
642 640
643 641 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
644 642 controller='journal', action='public_journal_rss')
645 643
646 644 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
647 645 controller='journal', action='public_journal_rss')
648 646
649 647 rmap.connect('public_journal_atom',
650 648 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
651 649 action='public_journal_atom')
652 650
653 651 rmap.connect('public_journal_atom_old',
654 652 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
655 653 action='public_journal_atom')
656 654
657 655 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
658 656 controller='journal', action='toggle_following', jsroute=True,
659 657 conditions={'method': ['POST']})
660 658
661 659 # FULL TEXT SEARCH
662 660 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
663 661 controller='search')
664 662 rmap.connect('search_repo_home', '/{repo_name}/search',
665 663 controller='search',
666 664 action='index',
667 665 conditions={'function': check_repo},
668 666 requirements=URL_NAME_REQUIREMENTS)
669 667
670 668 # FEEDS
671 669 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
672 670 controller='feed', action='rss',
673 671 conditions={'function': check_repo},
674 672 requirements=URL_NAME_REQUIREMENTS)
675 673
676 674 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
677 675 controller='feed', action='atom',
678 676 conditions={'function': check_repo},
679 677 requirements=URL_NAME_REQUIREMENTS)
680 678
681 679 #==========================================================================
682 680 # REPOSITORY ROUTES
683 681 #==========================================================================
684 682
685 683 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
686 684 controller='admin/repos', action='repo_creating',
687 685 requirements=URL_NAME_REQUIREMENTS)
688 686 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
689 687 controller='admin/repos', action='repo_check',
690 688 requirements=URL_NAME_REQUIREMENTS)
691 689
692 690 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
693 691 controller='summary', action='repo_stats',
694 692 conditions={'function': check_repo},
695 693 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
696 694
697 695 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
698 696 controller='summary', action='repo_refs_data', jsroute=True,
699 697 requirements=URL_NAME_REQUIREMENTS)
700 698 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
701 699 controller='summary', action='repo_refs_changelog_data',
702 700 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
703 701
704 702 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
705 703 controller='changeset', revision='tip', jsroute=True,
706 704 conditions={'function': check_repo},
707 705 requirements=URL_NAME_REQUIREMENTS)
708 706 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
709 707 controller='changeset', revision='tip', action='changeset_children',
710 708 conditions={'function': check_repo},
711 709 requirements=URL_NAME_REQUIREMENTS)
712 710 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
713 711 controller='changeset', revision='tip', action='changeset_parents',
714 712 conditions={'function': check_repo},
715 713 requirements=URL_NAME_REQUIREMENTS)
716 714
717 715 # repo edit options
718 716 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
719 717 controller='admin/repos', action='edit',
720 718 conditions={'method': ['GET'], 'function': check_repo},
721 719 requirements=URL_NAME_REQUIREMENTS)
722 720
723 721 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
724 722 jsroute=True,
725 723 controller='admin/repos', action='edit_permissions',
726 724 conditions={'method': ['GET'], 'function': check_repo},
727 725 requirements=URL_NAME_REQUIREMENTS)
728 726 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
729 727 controller='admin/repos', action='edit_permissions_update',
730 728 conditions={'method': ['PUT'], 'function': check_repo},
731 729 requirements=URL_NAME_REQUIREMENTS)
732 730
733 731 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
734 732 controller='admin/repos', action='edit_fields',
735 733 conditions={'method': ['GET'], 'function': check_repo},
736 734 requirements=URL_NAME_REQUIREMENTS)
737 735 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
738 736 controller='admin/repos', action='create_repo_field',
739 737 conditions={'method': ['PUT'], 'function': check_repo},
740 738 requirements=URL_NAME_REQUIREMENTS)
741 739 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
742 740 controller='admin/repos', action='delete_repo_field',
743 741 conditions={'method': ['DELETE'], 'function': check_repo},
744 742 requirements=URL_NAME_REQUIREMENTS)
745 743
746 744 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
747 745 controller='admin/repos', action='edit_advanced',
748 746 conditions={'method': ['GET'], 'function': check_repo},
749 747 requirements=URL_NAME_REQUIREMENTS)
750 748
751 749 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
752 750 controller='admin/repos', action='edit_advanced_locking',
753 751 conditions={'method': ['PUT'], 'function': check_repo},
754 752 requirements=URL_NAME_REQUIREMENTS)
755 753 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
756 754 controller='admin/repos', action='toggle_locking',
757 755 conditions={'method': ['GET'], 'function': check_repo},
758 756 requirements=URL_NAME_REQUIREMENTS)
759 757
760 758 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
761 759 controller='admin/repos', action='edit_advanced_journal',
762 760 conditions={'method': ['PUT'], 'function': check_repo},
763 761 requirements=URL_NAME_REQUIREMENTS)
764 762
765 763 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
766 764 controller='admin/repos', action='edit_advanced_fork',
767 765 conditions={'method': ['PUT'], 'function': check_repo},
768 766 requirements=URL_NAME_REQUIREMENTS)
769 767
770 768 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
771 769 controller='admin/repos', action='edit_caches_form',
772 770 conditions={'method': ['GET'], 'function': check_repo},
773 771 requirements=URL_NAME_REQUIREMENTS)
774 772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
775 773 controller='admin/repos', action='edit_caches',
776 774 conditions={'method': ['PUT'], 'function': check_repo},
777 775 requirements=URL_NAME_REQUIREMENTS)
778 776
779 777 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
780 778 controller='admin/repos', action='edit_remote_form',
781 779 conditions={'method': ['GET'], 'function': check_repo},
782 780 requirements=URL_NAME_REQUIREMENTS)
783 781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
784 782 controller='admin/repos', action='edit_remote',
785 783 conditions={'method': ['PUT'], 'function': check_repo},
786 784 requirements=URL_NAME_REQUIREMENTS)
787 785
788 786 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
789 787 controller='admin/repos', action='edit_statistics_form',
790 788 conditions={'method': ['GET'], 'function': check_repo},
791 789 requirements=URL_NAME_REQUIREMENTS)
792 790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
793 791 controller='admin/repos', action='edit_statistics',
794 792 conditions={'method': ['PUT'], 'function': check_repo},
795 793 requirements=URL_NAME_REQUIREMENTS)
796 794 rmap.connect('repo_settings_issuetracker',
797 795 '/{repo_name}/settings/issue-tracker',
798 796 controller='admin/repos', action='repo_issuetracker',
799 797 conditions={'method': ['GET'], 'function': check_repo},
800 798 requirements=URL_NAME_REQUIREMENTS)
801 799 rmap.connect('repo_issuetracker_test',
802 800 '/{repo_name}/settings/issue-tracker/test',
803 801 controller='admin/repos', action='repo_issuetracker_test',
804 802 conditions={'method': ['POST'], 'function': check_repo},
805 803 requirements=URL_NAME_REQUIREMENTS)
806 804 rmap.connect('repo_issuetracker_delete',
807 805 '/{repo_name}/settings/issue-tracker/delete',
808 806 controller='admin/repos', action='repo_issuetracker_delete',
809 807 conditions={'method': ['DELETE'], 'function': check_repo},
810 808 requirements=URL_NAME_REQUIREMENTS)
811 809 rmap.connect('repo_issuetracker_save',
812 810 '/{repo_name}/settings/issue-tracker/save',
813 811 controller='admin/repos', action='repo_issuetracker_save',
814 812 conditions={'method': ['POST'], 'function': check_repo},
815 813 requirements=URL_NAME_REQUIREMENTS)
816 814 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
817 815 controller='admin/repos', action='repo_settings_vcs_update',
818 816 conditions={'method': ['POST'], 'function': check_repo},
819 817 requirements=URL_NAME_REQUIREMENTS)
820 818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
821 819 controller='admin/repos', action='repo_settings_vcs',
822 820 conditions={'method': ['GET'], 'function': check_repo},
823 821 requirements=URL_NAME_REQUIREMENTS)
824 822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
825 823 controller='admin/repos', action='repo_delete_svn_pattern',
826 824 conditions={'method': ['DELETE'], 'function': check_repo},
827 825 requirements=URL_NAME_REQUIREMENTS)
828 826
829 827 # still working url for backward compat.
830 828 rmap.connect('raw_changeset_home_depraced',
831 829 '/{repo_name}/raw-changeset/{revision}',
832 830 controller='changeset', action='changeset_raw',
833 831 revision='tip', conditions={'function': check_repo},
834 832 requirements=URL_NAME_REQUIREMENTS)
835 833
836 834 # new URLs
837 835 rmap.connect('changeset_raw_home',
838 836 '/{repo_name}/changeset-diff/{revision}',
839 837 controller='changeset', action='changeset_raw',
840 838 revision='tip', conditions={'function': check_repo},
841 839 requirements=URL_NAME_REQUIREMENTS)
842 840
843 841 rmap.connect('changeset_patch_home',
844 842 '/{repo_name}/changeset-patch/{revision}',
845 843 controller='changeset', action='changeset_patch',
846 844 revision='tip', conditions={'function': check_repo},
847 845 requirements=URL_NAME_REQUIREMENTS)
848 846
849 847 rmap.connect('changeset_download_home',
850 848 '/{repo_name}/changeset-download/{revision}',
851 849 controller='changeset', action='changeset_download',
852 850 revision='tip', conditions={'function': check_repo},
853 851 requirements=URL_NAME_REQUIREMENTS)
854 852
855 853 rmap.connect('changeset_comment',
856 854 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
857 855 controller='changeset', revision='tip', action='comment',
858 856 conditions={'function': check_repo},
859 857 requirements=URL_NAME_REQUIREMENTS)
860 858
861 859 rmap.connect('changeset_comment_preview',
862 860 '/{repo_name}/changeset/comment/preview', jsroute=True,
863 861 controller='changeset', action='preview_comment',
864 862 conditions={'function': check_repo, 'method': ['POST']},
865 863 requirements=URL_NAME_REQUIREMENTS)
866 864
867 865 rmap.connect('changeset_comment_delete',
868 866 '/{repo_name}/changeset/comment/{comment_id}/delete',
869 867 controller='changeset', action='delete_comment',
870 868 conditions={'function': check_repo, 'method': ['DELETE']},
871 869 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
872 870
873 871 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
874 872 controller='changeset', action='changeset_info',
875 873 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
876 874
877 875 rmap.connect('compare_home',
878 876 '/{repo_name}/compare',
879 877 controller='compare', action='index',
880 878 conditions={'function': check_repo},
881 879 requirements=URL_NAME_REQUIREMENTS)
882 880
883 881 rmap.connect('compare_url',
884 882 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
885 883 controller='compare', action='compare',
886 884 conditions={'function': check_repo},
887 885 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
888 886
889 887 rmap.connect('pullrequest_home',
890 888 '/{repo_name}/pull-request/new', controller='pullrequests',
891 889 action='index', conditions={'function': check_repo,
892 890 'method': ['GET']},
893 891 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
894 892
895 893 rmap.connect('pullrequest',
896 894 '/{repo_name}/pull-request/new', controller='pullrequests',
897 895 action='create', conditions={'function': check_repo,
898 896 'method': ['POST']},
899 897 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900 898
901 899 rmap.connect('pullrequest_repo_refs',
902 900 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
903 901 controller='pullrequests',
904 902 action='get_repo_refs',
905 903 conditions={'function': check_repo, 'method': ['GET']},
906 904 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
907 905
908 906 rmap.connect('pullrequest_repo_destinations',
909 907 '/{repo_name}/pull-request/repo-destinations',
910 908 controller='pullrequests',
911 909 action='get_repo_destinations',
912 910 conditions={'function': check_repo, 'method': ['GET']},
913 911 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
914 912
915 913 rmap.connect('pullrequest_show',
916 914 '/{repo_name}/pull-request/{pull_request_id}',
917 915 controller='pullrequests',
918 916 action='show', conditions={'function': check_repo,
919 917 'method': ['GET']},
920 918 requirements=URL_NAME_REQUIREMENTS)
921 919
922 920 rmap.connect('pullrequest_update',
923 921 '/{repo_name}/pull-request/{pull_request_id}',
924 922 controller='pullrequests',
925 923 action='update', conditions={'function': check_repo,
926 924 'method': ['PUT']},
927 925 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
928 926
929 927 rmap.connect('pullrequest_merge',
930 928 '/{repo_name}/pull-request/{pull_request_id}',
931 929 controller='pullrequests',
932 930 action='merge', conditions={'function': check_repo,
933 931 'method': ['POST']},
934 932 requirements=URL_NAME_REQUIREMENTS)
935 933
936 934 rmap.connect('pullrequest_delete',
937 935 '/{repo_name}/pull-request/{pull_request_id}',
938 936 controller='pullrequests',
939 937 action='delete', conditions={'function': check_repo,
940 938 'method': ['DELETE']},
941 939 requirements=URL_NAME_REQUIREMENTS)
942 940
943 941 rmap.connect('pullrequest_show_all',
944 942 '/{repo_name}/pull-request',
945 943 controller='pullrequests',
946 944 action='show_all', conditions={'function': check_repo,
947 945 'method': ['GET']},
948 946 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
949 947
950 948 rmap.connect('pullrequest_comment',
951 949 '/{repo_name}/pull-request-comment/{pull_request_id}',
952 950 controller='pullrequests',
953 951 action='comment', conditions={'function': check_repo,
954 952 'method': ['POST']},
955 953 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
956 954
957 955 rmap.connect('pullrequest_comment_delete',
958 956 '/{repo_name}/pull-request-comment/{comment_id}/delete',
959 957 controller='pullrequests', action='delete_comment',
960 958 conditions={'function': check_repo, 'method': ['DELETE']},
961 959 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
962 960
963 961 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
964 962 controller='summary', conditions={'function': check_repo},
965 963 requirements=URL_NAME_REQUIREMENTS)
966 964
967 965 rmap.connect('branches_home', '/{repo_name}/branches',
968 966 controller='branches', conditions={'function': check_repo},
969 967 requirements=URL_NAME_REQUIREMENTS)
970 968
971 969 rmap.connect('tags_home', '/{repo_name}/tags',
972 970 controller='tags', conditions={'function': check_repo},
973 971 requirements=URL_NAME_REQUIREMENTS)
974 972
975 973 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
976 974 controller='bookmarks', conditions={'function': check_repo},
977 975 requirements=URL_NAME_REQUIREMENTS)
978 976
979 977 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
980 978 controller='changelog', conditions={'function': check_repo},
981 979 requirements=URL_NAME_REQUIREMENTS)
982 980
983 981 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
984 982 controller='changelog', action='changelog_summary',
985 983 conditions={'function': check_repo},
986 984 requirements=URL_NAME_REQUIREMENTS)
987 985
988 986 rmap.connect('changelog_file_home',
989 987 '/{repo_name}/changelog/{revision}/{f_path}',
990 988 controller='changelog', f_path=None,
991 989 conditions={'function': check_repo},
992 990 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
993 991
994 992 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
995 993 controller='changelog', action='changelog_details',
996 994 conditions={'function': check_repo},
997 995 requirements=URL_NAME_REQUIREMENTS)
998 996
999 997 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1000 998 controller='files', revision='tip', f_path='',
1001 999 conditions={'function': check_repo},
1002 1000 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1003 1001
1004 1002 rmap.connect('files_home_simple_catchrev',
1005 1003 '/{repo_name}/files/{revision}',
1006 1004 controller='files', revision='tip', f_path='',
1007 1005 conditions={'function': check_repo},
1008 1006 requirements=URL_NAME_REQUIREMENTS)
1009 1007
1010 1008 rmap.connect('files_home_simple_catchall',
1011 1009 '/{repo_name}/files',
1012 1010 controller='files', revision='tip', f_path='',
1013 1011 conditions={'function': check_repo},
1014 1012 requirements=URL_NAME_REQUIREMENTS)
1015 1013
1016 1014 rmap.connect('files_history_home',
1017 1015 '/{repo_name}/history/{revision}/{f_path}',
1018 1016 controller='files', action='history', revision='tip', f_path='',
1019 1017 conditions={'function': check_repo},
1020 1018 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1021 1019
1022 1020 rmap.connect('files_authors_home',
1023 1021 '/{repo_name}/authors/{revision}/{f_path}',
1024 1022 controller='files', action='authors', revision='tip', f_path='',
1025 1023 conditions={'function': check_repo},
1026 1024 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1027 1025
1028 1026 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1029 1027 controller='files', action='diff', f_path='',
1030 1028 conditions={'function': check_repo},
1031 1029 requirements=URL_NAME_REQUIREMENTS)
1032 1030
1033 1031 rmap.connect('files_diff_2way_home',
1034 1032 '/{repo_name}/diff-2way/{f_path}',
1035 1033 controller='files', action='diff_2way', f_path='',
1036 1034 conditions={'function': check_repo},
1037 1035 requirements=URL_NAME_REQUIREMENTS)
1038 1036
1039 1037 rmap.connect('files_rawfile_home',
1040 1038 '/{repo_name}/rawfile/{revision}/{f_path}',
1041 1039 controller='files', action='rawfile', revision='tip',
1042 1040 f_path='', conditions={'function': check_repo},
1043 1041 requirements=URL_NAME_REQUIREMENTS)
1044 1042
1045 1043 rmap.connect('files_raw_home',
1046 1044 '/{repo_name}/raw/{revision}/{f_path}',
1047 1045 controller='files', action='raw', revision='tip', f_path='',
1048 1046 conditions={'function': check_repo},
1049 1047 requirements=URL_NAME_REQUIREMENTS)
1050 1048
1051 1049 rmap.connect('files_render_home',
1052 1050 '/{repo_name}/render/{revision}/{f_path}',
1053 1051 controller='files', action='index', revision='tip', f_path='',
1054 1052 rendered=True, conditions={'function': check_repo},
1055 1053 requirements=URL_NAME_REQUIREMENTS)
1056 1054
1057 1055 rmap.connect('files_annotate_home',
1058 1056 '/{repo_name}/annotate/{revision}/{f_path}',
1059 1057 controller='files', action='index', revision='tip',
1060 1058 f_path='', annotate=True, conditions={'function': check_repo},
1061 1059 requirements=URL_NAME_REQUIREMENTS)
1062 1060
1063 1061 rmap.connect('files_edit',
1064 1062 '/{repo_name}/edit/{revision}/{f_path}',
1065 1063 controller='files', action='edit', revision='tip',
1066 1064 f_path='',
1067 1065 conditions={'function': check_repo, 'method': ['POST']},
1068 1066 requirements=URL_NAME_REQUIREMENTS)
1069 1067
1070 1068 rmap.connect('files_edit_home',
1071 1069 '/{repo_name}/edit/{revision}/{f_path}',
1072 1070 controller='files', action='edit_home', revision='tip',
1073 1071 f_path='', conditions={'function': check_repo},
1074 1072 requirements=URL_NAME_REQUIREMENTS)
1075 1073
1076 1074 rmap.connect('files_add',
1077 1075 '/{repo_name}/add/{revision}/{f_path}',
1078 1076 controller='files', action='add', revision='tip',
1079 1077 f_path='',
1080 1078 conditions={'function': check_repo, 'method': ['POST']},
1081 1079 requirements=URL_NAME_REQUIREMENTS)
1082 1080
1083 1081 rmap.connect('files_add_home',
1084 1082 '/{repo_name}/add/{revision}/{f_path}',
1085 1083 controller='files', action='add_home', revision='tip',
1086 1084 f_path='', conditions={'function': check_repo},
1087 1085 requirements=URL_NAME_REQUIREMENTS)
1088 1086
1089 1087 rmap.connect('files_delete',
1090 1088 '/{repo_name}/delete/{revision}/{f_path}',
1091 1089 controller='files', action='delete', revision='tip',
1092 1090 f_path='',
1093 1091 conditions={'function': check_repo, 'method': ['POST']},
1094 1092 requirements=URL_NAME_REQUIREMENTS)
1095 1093
1096 1094 rmap.connect('files_delete_home',
1097 1095 '/{repo_name}/delete/{revision}/{f_path}',
1098 1096 controller='files', action='delete_home', revision='tip',
1099 1097 f_path='', conditions={'function': check_repo},
1100 1098 requirements=URL_NAME_REQUIREMENTS)
1101 1099
1102 1100 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1103 1101 controller='files', action='archivefile',
1104 1102 conditions={'function': check_repo},
1105 1103 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1106 1104
1107 1105 rmap.connect('files_nodelist_home',
1108 1106 '/{repo_name}/nodelist/{revision}/{f_path}',
1109 1107 controller='files', action='nodelist',
1110 1108 conditions={'function': check_repo},
1111 1109 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1112 1110
1113 1111 rmap.connect('files_nodetree_full',
1114 1112 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1115 1113 controller='files', action='nodetree_full',
1116 1114 conditions={'function': check_repo},
1117 1115 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1118 1116
1119 1117 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1120 1118 controller='forks', action='fork_create',
1121 1119 conditions={'function': check_repo, 'method': ['POST']},
1122 1120 requirements=URL_NAME_REQUIREMENTS)
1123 1121
1124 1122 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1125 1123 controller='forks', action='fork',
1126 1124 conditions={'function': check_repo},
1127 1125 requirements=URL_NAME_REQUIREMENTS)
1128 1126
1129 1127 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1130 1128 controller='forks', action='forks',
1131 1129 conditions={'function': check_repo},
1132 1130 requirements=URL_NAME_REQUIREMENTS)
1133 1131
1134 1132 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1135 1133 controller='followers', action='followers',
1136 1134 conditions={'function': check_repo},
1137 1135 requirements=URL_NAME_REQUIREMENTS)
1138 1136
1139 1137 # must be here for proper group/repo catching pattern
1140 1138 _connect_with_slash(
1141 1139 rmap, 'repo_group_home', '/{group_name}',
1142 1140 controller='home', action='index_repo_group',
1143 1141 conditions={'function': check_group},
1144 1142 requirements=URL_NAME_REQUIREMENTS)
1145 1143
1146 1144 # catch all, at the end
1147 1145 _connect_with_slash(
1148 1146 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1149 1147 controller='summary', action='index',
1150 1148 conditions={'function': check_repo},
1151 1149 requirements=URL_NAME_REQUIREMENTS)
1152 1150
1153 1151 return rmap
1154 1152
1155 1153
1156 1154 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1157 1155 """
1158 1156 Connect a route with an optional trailing slash in `path`.
1159 1157 """
1160 1158 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1161 1159 mapper.connect(name, path, *args, **kwargs)
@@ -1,362 +1,371 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27
28 28 import formencode
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, session
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from sqlalchemy.orm import joinedload
34 34
35 from rhodecode import forms
35 36 from rhodecode.lib import helpers as h
36 37 from rhodecode.lib import auth
37 38 from rhodecode.lib.auth import (
38 39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
39 40 from rhodecode.lib.base import BaseController, render
40 41 from rhodecode.lib.utils2 import safe_int, md5
41 42 from rhodecode.lib.ext_json import json
43
44 from rhodecode.model.validation_schema.schemas import user_schema
42 45 from rhodecode.model.db import (
43 46 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
44 47 UserFollowing)
45 from rhodecode.model.forms import UserForm, PasswordChangeForm
48 from rhodecode.model.forms import UserForm
46 49 from rhodecode.model.scm import RepoList
47 50 from rhodecode.model.user import UserModel
48 51 from rhodecode.model.repo import RepoModel
49 52 from rhodecode.model.auth_token import AuthTokenModel
50 53 from rhodecode.model.meta import Session
51 54
52 55 log = logging.getLogger(__name__)
53 56
54 57
55 58 class MyAccountController(BaseController):
56 59 """REST Controller styled on the Atom Publishing Protocol"""
57 60 # To properly map this controller, ensure your config/routing.py
58 61 # file has a resource setup:
59 62 # map.resource('setting', 'settings', controller='admin/settings',
60 63 # path_prefix='/admin', name_prefix='admin_')
61 64
62 65 @LoginRequired()
63 66 @NotAnonymous()
64 67 def __before__(self):
65 68 super(MyAccountController, self).__before__()
66 69
67 70 def __load_data(self):
68 71 c.user = User.get(c.rhodecode_user.user_id)
69 72 if c.user.username == User.DEFAULT_USER:
70 73 h.flash(_("You can't edit this user since it's"
71 74 " crucial for entire application"), category='warning')
72 75 return redirect(url('users'))
73 76
74 77 def _load_my_repos_data(self, watched=False):
75 78 if watched:
76 79 admin = False
77 80 follows_repos = Session().query(UserFollowing)\
78 81 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
79 82 .options(joinedload(UserFollowing.follows_repository))\
80 83 .all()
81 84 repo_list = [x.follows_repository for x in follows_repos]
82 85 else:
83 86 admin = True
84 87 repo_list = Repository.get_all_repos(
85 88 user_id=c.rhodecode_user.user_id)
86 89 repo_list = RepoList(repo_list, perm_set=[
87 90 'repository.read', 'repository.write', 'repository.admin'])
88 91
89 92 repos_data = RepoModel().get_repos_as_dict(
90 93 repo_list=repo_list, admin=admin)
91 94 # json used to render the grid
92 95 return json.dumps(repos_data)
93 96
94 97 @auth.CSRFRequired()
95 98 def my_account_update(self):
96 99 """
97 100 POST /_admin/my_account Updates info of my account
98 101 """
99 102 # url('my_account')
100 103 c.active = 'profile_edit'
101 104 self.__load_data()
102 105 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
103 106 ip_addr=self.ip_addr)
104 107 c.extern_type = c.user.extern_type
105 108 c.extern_name = c.user.extern_name
106 109
107 110 defaults = c.user.get_dict()
108 111 update = False
109 112 _form = UserForm(edit=True,
110 113 old_data={'user_id': c.rhodecode_user.user_id,
111 114 'email': c.rhodecode_user.email})()
112 115 form_result = {}
113 116 try:
114 117 post_data = dict(request.POST)
115 118 post_data['new_password'] = ''
116 119 post_data['password_confirmation'] = ''
117 120 form_result = _form.to_python(post_data)
118 121 # skip updating those attrs for my account
119 122 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 123 'new_password', 'password_confirmation']
121 124 # TODO: plugin should define if username can be updated
122 125 if c.extern_type != "rhodecode":
123 126 # forbid updating username for external accounts
124 127 skip_attrs.append('username')
125 128
126 129 UserModel().update_user(
127 130 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
128 131 h.flash(_('Your account was updated successfully'),
129 132 category='success')
130 133 Session().commit()
131 134 update = True
132 135
133 136 except formencode.Invalid as errors:
134 137 return htmlfill.render(
135 138 render('admin/my_account/my_account.html'),
136 139 defaults=errors.value,
137 140 errors=errors.error_dict or {},
138 141 prefix_error=False,
139 142 encoding="UTF-8",
140 143 force_defaults=False)
141 144 except Exception:
142 145 log.exception("Exception updating user")
143 146 h.flash(_('Error occurred during update of user %s')
144 147 % form_result.get('username'), category='error')
145 148
146 149 if update:
147 150 return redirect('my_account')
148 151
149 152 return htmlfill.render(
150 153 render('admin/my_account/my_account.html'),
151 154 defaults=defaults,
152 155 encoding="UTF-8",
153 156 force_defaults=False
154 157 )
155 158
156 159 def my_account(self):
157 160 """
158 161 GET /_admin/my_account Displays info about my account
159 162 """
160 163 # url('my_account')
161 164 c.active = 'profile'
162 165 self.__load_data()
163 166
164 167 defaults = c.user.get_dict()
165 168 return htmlfill.render(
166 169 render('admin/my_account/my_account.html'),
167 170 defaults=defaults, encoding="UTF-8", force_defaults=False)
168 171
169 172 def my_account_edit(self):
170 173 """
171 174 GET /_admin/my_account/edit Displays edit form of my account
172 175 """
173 176 c.active = 'profile_edit'
174 177 self.__load_data()
175 178 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
176 179 ip_addr=self.ip_addr)
177 180 c.extern_type = c.user.extern_type
178 181 c.extern_name = c.user.extern_name
179 182
180 183 defaults = c.user.get_dict()
181 184 return htmlfill.render(
182 185 render('admin/my_account/my_account.html'),
183 186 defaults=defaults,
184 187 encoding="UTF-8",
185 188 force_defaults=False
186 189 )
187 190
188 @auth.CSRFRequired()
189 def my_account_password_update(self):
190 c.active = 'password'
191 self.__load_data()
192 _form = PasswordChangeForm(c.rhodecode_user.username)()
193 try:
194 form_result = _form.to_python(request.POST)
195 UserModel().update_user(c.rhodecode_user.user_id, **form_result)
196 instance = c.rhodecode_user.get_instance()
197 instance.update_userdata(force_password_change=False)
198 Session().commit()
199 session.setdefault('rhodecode_user', {}).update(
200 {'password': md5(instance.password)})
201 session.save()
202 h.flash(_("Successfully updated password"), category='success')
203 except formencode.Invalid as errors:
204 return htmlfill.render(
205 render('admin/my_account/my_account.html'),
206 defaults=errors.value,
207 errors=errors.error_dict or {},
208 prefix_error=False,
209 encoding="UTF-8",
210 force_defaults=False)
211 except Exception:
212 log.exception("Exception updating password")
213 h.flash(_('Error occurred during update of user password'),
214 category='error')
215 return render('admin/my_account/my_account.html')
216
191 @auth.CSRFRequired(except_methods=['GET'])
217 192 def my_account_password(self):
218 193 c.active = 'password'
219 194 self.__load_data()
195
196 schema = user_schema.ChangePasswordSchema().bind(
197 username=c.rhodecode_user.username)
198
199 form = forms.Form(schema,
200 buttons=(forms.buttons.save, forms.buttons.reset))
201
202 if request.method == 'POST':
203 controls = request.POST.items()
204 try:
205 valid_data = form.validate(controls)
206 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
207 instance = c.rhodecode_user.get_instance()
208 instance.update_userdata(force_password_change=False)
209 Session().commit()
210 except forms.ValidationFailure as e:
211 request.session.flash(
212 _('Error occurred during update of user password'),
213 queue='error')
214 form = e
215 except Exception:
216 log.exception("Exception updating password")
217 request.session.flash(
218 _('Error occurred during update of user password'),
219 queue='error')
220 else:
221 session.setdefault('rhodecode_user', {}).update(
222 {'password': md5(instance.password)})
223 session.save()
224 request.session.flash(
225 _("Successfully updated password"), queue='success')
226 return redirect(url('my_account_password'))
227
228 c.form = form
220 229 return render('admin/my_account/my_account.html')
221 230
222 231 def my_account_repos(self):
223 232 c.active = 'repos'
224 233 self.__load_data()
225 234
226 235 # json used to render the grid
227 236 c.data = self._load_my_repos_data()
228 237 return render('admin/my_account/my_account.html')
229 238
230 239 def my_account_watched(self):
231 240 c.active = 'watched'
232 241 self.__load_data()
233 242
234 243 # json used to render the grid
235 244 c.data = self._load_my_repos_data(watched=True)
236 245 return render('admin/my_account/my_account.html')
237 246
238 247 def my_account_perms(self):
239 248 c.active = 'perms'
240 249 self.__load_data()
241 250 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
242 251 ip_addr=self.ip_addr)
243 252
244 253 return render('admin/my_account/my_account.html')
245 254
246 255 def my_account_emails(self):
247 256 c.active = 'emails'
248 257 self.__load_data()
249 258
250 259 c.user_email_map = UserEmailMap.query()\
251 260 .filter(UserEmailMap.user == c.user).all()
252 261 return render('admin/my_account/my_account.html')
253 262
254 263 @auth.CSRFRequired()
255 264 def my_account_emails_add(self):
256 265 email = request.POST.get('new_email')
257 266
258 267 try:
259 268 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
260 269 Session().commit()
261 270 h.flash(_("Added new email address `%s` for user account") % email,
262 271 category='success')
263 272 except formencode.Invalid as error:
264 273 msg = error.error_dict['email']
265 274 h.flash(msg, category='error')
266 275 except Exception:
267 276 log.exception("Exception in my_account_emails")
268 277 h.flash(_('An error occurred during email saving'),
269 278 category='error')
270 279 return redirect(url('my_account_emails'))
271 280
272 281 @auth.CSRFRequired()
273 282 def my_account_emails_delete(self):
274 283 email_id = request.POST.get('del_email_id')
275 284 user_model = UserModel()
276 285 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
277 286 Session().commit()
278 287 h.flash(_("Removed email address from user account"),
279 288 category='success')
280 289 return redirect(url('my_account_emails'))
281 290
282 291 def my_account_pullrequests(self):
283 292 c.active = 'pullrequests'
284 293 self.__load_data()
285 294 c.show_closed = request.GET.get('pr_show_closed')
286 295
287 296 def _filter(pr):
288 297 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
289 298 if not c.show_closed:
290 299 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
291 300 return s
292 301
293 302 c.my_pull_requests = _filter(
294 303 PullRequest.query().filter(
295 304 PullRequest.user_id == c.rhodecode_user.user_id).all())
296 305 my_prs = [
297 306 x.pull_request for x in PullRequestReviewers.query().filter(
298 307 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
299 308 c.participate_in_pull_requests = _filter(my_prs)
300 309 return render('admin/my_account/my_account.html')
301 310
302 311 def my_account_auth_tokens(self):
303 312 c.active = 'auth_tokens'
304 313 self.__load_data()
305 314 show_expired = True
306 315 c.lifetime_values = [
307 316 (str(-1), _('forever')),
308 317 (str(5), _('5 minutes')),
309 318 (str(60), _('1 hour')),
310 319 (str(60 * 24), _('1 day')),
311 320 (str(60 * 24 * 30), _('1 month')),
312 321 ]
313 322 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
314 323 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
315 324 for x in AuthTokenModel.cls.ROLES]
316 325 c.role_options = [(c.role_values, _("Role"))]
317 326 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
318 327 c.rhodecode_user.user_id, show_expired=show_expired)
319 328 return render('admin/my_account/my_account.html')
320 329
321 330 @auth.CSRFRequired()
322 331 def my_account_auth_tokens_add(self):
323 332 lifetime = safe_int(request.POST.get('lifetime'), -1)
324 333 description = request.POST.get('description')
325 334 role = request.POST.get('role')
326 335 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
327 336 role)
328 337 Session().commit()
329 338 h.flash(_("Auth token successfully created"), category='success')
330 339 return redirect(url('my_account_auth_tokens'))
331 340
332 341 @auth.CSRFRequired()
333 342 def my_account_auth_tokens_delete(self):
334 343 auth_token = request.POST.get('del_auth_token')
335 344 user_id = c.rhodecode_user.user_id
336 345 if request.POST.get('del_auth_token_builtin'):
337 346 user = User.get(user_id)
338 347 if user:
339 348 user.api_key = generate_auth_token(user.username)
340 349 Session().add(user)
341 350 Session().commit()
342 351 h.flash(_("Auth token successfully reset"), category='success')
343 352 elif auth_token:
344 353 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
345 354 Session().commit()
346 355 h.flash(_("Auth token successfully deleted"), category='success')
347 356
348 357 return redirect(url('my_account_auth_tokens'))
349 358
350 359 def my_notifications(self):
351 360 c.active = 'notifications'
352 361 return render('admin/my_account/my_account.html')
353 362
354 363 @auth.CSRFRequired()
355 364 def my_notifications_toggle_visibility(self):
356 365 user = c.rhodecode_user.get_instance()
357 366 user_data = user.user_data
358 367 status = user_data.get('notification_status', False)
359 368 user_data['notification_status'] = not status
360 369 user.user_data = user_data
361 370 Session().commit()
362 371 return redirect(url('my_account_notifications'))
@@ -1,1898 +1,1903 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 authentication and permission libraries
23 23 """
24 24
25 25 import inspect
26 26 import collections
27 27 import fnmatch
28 28 import hashlib
29 29 import itertools
30 30 import logging
31 31 import os
32 32 import random
33 33 import time
34 34 import traceback
35 35 from functools import wraps
36 36
37 37 import ipaddress
38 38 from pylons import url, request
39 39 from pylons.controllers.util import abort, redirect
40 40 from pylons.i18n.translation import _
41 41 from sqlalchemy import or_
42 42 from sqlalchemy.orm.exc import ObjectDeletedError
43 43 from sqlalchemy.orm import joinedload
44 44 from zope.cachedescriptors.property import Lazy as LazyProperty
45 45
46 46 import rhodecode
47 47 from rhodecode.model import meta
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.db import (
51 51 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
52 52 UserIpMap, UserApiKeys)
53 53 from rhodecode.lib import caches
54 54 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
55 55 from rhodecode.lib.utils import (
56 56 get_repo_slug, get_repo_group_slug, get_user_group_slug)
57 57 from rhodecode.lib.caching_query import FromCache
58 58
59 59
60 60 if rhodecode.is_unix:
61 61 import bcrypt
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65 csrf_token_key = "csrf_token"
66 66
67 67
68 68 class PasswordGenerator(object):
69 69 """
70 70 This is a simple class for generating password from different sets of
71 71 characters
72 72 usage::
73 73
74 74 passwd_gen = PasswordGenerator()
75 75 #print 8-letter password containing only big and small letters
76 76 of alphabet
77 77 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
78 78 """
79 79 ALPHABETS_NUM = r'''1234567890'''
80 80 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
81 81 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
82 82 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
83 83 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
84 84 + ALPHABETS_NUM + ALPHABETS_SPECIAL
85 85 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
86 86 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
87 87 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
88 88 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
89 89
90 90 def __init__(self, passwd=''):
91 91 self.passwd = passwd
92 92
93 93 def gen_password(self, length, type_=None):
94 94 if type_ is None:
95 95 type_ = self.ALPHABETS_FULL
96 96 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
97 97 return self.passwd
98 98
99 99
100 100 class _RhodeCodeCryptoBase(object):
101 101
102 102 def hash_create(self, str_):
103 103 """
104 104 hash the string using
105 105
106 106 :param str_: password to hash
107 107 """
108 108 raise NotImplementedError
109 109
110 110 def hash_check_with_upgrade(self, password, hashed):
111 111 """
112 112 Returns tuple in which first element is boolean that states that
113 113 given password matches it's hashed version, and the second is new hash
114 114 of the password, in case this password should be migrated to new
115 115 cipher.
116 116 """
117 117 checked_hash = self.hash_check(password, hashed)
118 118 return checked_hash, None
119 119
120 120 def hash_check(self, password, hashed):
121 121 """
122 122 Checks matching password with it's hashed value.
123 123
124 124 :param password: password
125 125 :param hashed: password in hashed form
126 126 """
127 127 raise NotImplementedError
128 128
129 129 def _assert_bytes(self, value):
130 130 """
131 131 Passing in an `unicode` object can lead to hard to detect issues
132 132 if passwords contain non-ascii characters. Doing a type check
133 133 during runtime, so that such mistakes are detected early on.
134 134 """
135 135 if not isinstance(value, str):
136 136 raise TypeError(
137 137 "Bytestring required as input, got %r." % (value, ))
138 138
139 139
140 140 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
141 141
142 142 def hash_create(self, str_):
143 143 self._assert_bytes(str_)
144 144 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
145 145
146 146 def hash_check_with_upgrade(self, password, hashed):
147 147 """
148 148 Returns tuple in which first element is boolean that states that
149 149 given password matches it's hashed version, and the second is new hash
150 150 of the password, in case this password should be migrated to new
151 151 cipher.
152 152
153 153 This implements special upgrade logic which works like that:
154 154 - check if the given password == bcrypted hash, if yes then we
155 155 properly used password and it was already in bcrypt. Proceed
156 156 without any changes
157 157 - if bcrypt hash check is not working try with sha256. If hash compare
158 158 is ok, it means we using correct but old hashed password. indicate
159 159 hash change and proceed
160 160 """
161 161
162 162 new_hash = None
163 163
164 164 # regular pw check
165 165 password_match_bcrypt = self.hash_check(password, hashed)
166 166
167 167 # now we want to know if the password was maybe from sha256
168 168 # basically calling _RhodeCodeCryptoSha256().hash_check()
169 169 if not password_match_bcrypt:
170 170 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
171 171 new_hash = self.hash_create(password) # make new bcrypt hash
172 172 password_match_bcrypt = True
173 173
174 174 return password_match_bcrypt, new_hash
175 175
176 176 def hash_check(self, password, hashed):
177 177 """
178 178 Checks matching password with it's hashed value.
179 179
180 180 :param password: password
181 181 :param hashed: password in hashed form
182 182 """
183 183 self._assert_bytes(password)
184 184 try:
185 185 return bcrypt.hashpw(password, hashed) == hashed
186 186 except ValueError as e:
187 187 # we're having a invalid salt here probably, we should not crash
188 188 # just return with False as it would be a wrong password.
189 189 log.debug('Failed to check password hash using bcrypt %s',
190 190 safe_str(e))
191 191
192 192 return False
193 193
194 194
195 195 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
196 196
197 197 def hash_create(self, str_):
198 198 self._assert_bytes(str_)
199 199 return hashlib.sha256(str_).hexdigest()
200 200
201 201 def hash_check(self, password, hashed):
202 202 """
203 203 Checks matching password with it's hashed value.
204 204
205 205 :param password: password
206 206 :param hashed: password in hashed form
207 207 """
208 208 self._assert_bytes(password)
209 209 return hashlib.sha256(password).hexdigest() == hashed
210 210
211 211
212 212 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
213 213
214 214 def hash_create(self, str_):
215 215 self._assert_bytes(str_)
216 216 return hashlib.md5(str_).hexdigest()
217 217
218 218 def hash_check(self, password, hashed):
219 219 """
220 220 Checks matching password with it's hashed value.
221 221
222 222 :param password: password
223 223 :param hashed: password in hashed form
224 224 """
225 225 self._assert_bytes(password)
226 226 return hashlib.md5(password).hexdigest() == hashed
227 227
228 228
229 229 def crypto_backend():
230 230 """
231 231 Return the matching crypto backend.
232 232
233 233 Selection is based on if we run tests or not, we pick md5 backend to run
234 234 tests faster since BCRYPT is expensive to calculate
235 235 """
236 236 if rhodecode.is_test:
237 237 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
238 238 else:
239 239 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
240 240
241 241 return RhodeCodeCrypto
242 242
243 243
244 244 def get_crypt_password(password):
245 245 """
246 246 Create the hash of `password` with the active crypto backend.
247 247
248 248 :param password: The cleartext password.
249 249 :type password: unicode
250 250 """
251 251 password = safe_str(password)
252 252 return crypto_backend().hash_create(password)
253 253
254 254
255 255 def check_password(password, hashed):
256 256 """
257 257 Check if the value in `password` matches the hash in `hashed`.
258 258
259 259 :param password: The cleartext password.
260 260 :type password: unicode
261 261
262 262 :param hashed: The expected hashed version of the password.
263 263 :type hashed: The hash has to be passed in in text representation.
264 264 """
265 265 password = safe_str(password)
266 266 return crypto_backend().hash_check(password, hashed)
267 267
268 268
269 269 def generate_auth_token(data, salt=None):
270 270 """
271 271 Generates API KEY from given string
272 272 """
273 273
274 274 if salt is None:
275 275 salt = os.urandom(16)
276 276 return hashlib.sha1(safe_str(data) + salt).hexdigest()
277 277
278 278
279 279 class CookieStoreWrapper(object):
280 280
281 281 def __init__(self, cookie_store):
282 282 self.cookie_store = cookie_store
283 283
284 284 def __repr__(self):
285 285 return 'CookieStore<%s>' % (self.cookie_store)
286 286
287 287 def get(self, key, other=None):
288 288 if isinstance(self.cookie_store, dict):
289 289 return self.cookie_store.get(key, other)
290 290 elif isinstance(self.cookie_store, AuthUser):
291 291 return self.cookie_store.__dict__.get(key, other)
292 292
293 293
294 294 def _cached_perms_data(user_id, scope, user_is_admin,
295 295 user_inherit_default_permissions, explicit, algo):
296 296
297 297 permissions = PermissionCalculator(
298 298 user_id, scope, user_is_admin, user_inherit_default_permissions,
299 299 explicit, algo)
300 300 return permissions.calculate()
301 301
302 302 class PermOrigin:
303 303 ADMIN = 'superadmin'
304 304
305 305 REPO_USER = 'user:%s'
306 306 REPO_USERGROUP = 'usergroup:%s'
307 307 REPO_OWNER = 'repo.owner'
308 308 REPO_DEFAULT = 'repo.default'
309 309 REPO_PRIVATE = 'repo.private'
310 310
311 311 REPOGROUP_USER = 'user:%s'
312 312 REPOGROUP_USERGROUP = 'usergroup:%s'
313 313 REPOGROUP_OWNER = 'group.owner'
314 314 REPOGROUP_DEFAULT = 'group.default'
315 315
316 316 USERGROUP_USER = 'user:%s'
317 317 USERGROUP_USERGROUP = 'usergroup:%s'
318 318 USERGROUP_OWNER = 'usergroup.owner'
319 319 USERGROUP_DEFAULT = 'usergroup.default'
320 320
321 321
322 322 class PermOriginDict(dict):
323 323 """
324 324 A special dict used for tracking permissions along with their origins.
325 325
326 326 `__setitem__` has been overridden to expect a tuple(perm, origin)
327 327 `__getitem__` will return only the perm
328 328 `.perm_origin_stack` will return the stack of (perm, origin) set per key
329 329
330 330 >>> perms = PermOriginDict()
331 331 >>> perms['resource'] = 'read', 'default'
332 332 >>> perms['resource']
333 333 'read'
334 334 >>> perms['resource'] = 'write', 'admin'
335 335 >>> perms['resource']
336 336 'write'
337 337 >>> perms.perm_origin_stack
338 338 {'resource': [('read', 'default'), ('write', 'admin')]}
339 339 """
340 340
341 341
342 342 def __init__(self, *args, **kw):
343 343 dict.__init__(self, *args, **kw)
344 344 self.perm_origin_stack = {}
345 345
346 346 def __setitem__(self, key, (perm, origin)):
347 347 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
348 348 dict.__setitem__(self, key, perm)
349 349
350 350
351 351 class PermissionCalculator(object):
352 352
353 353 def __init__(
354 354 self, user_id, scope, user_is_admin,
355 355 user_inherit_default_permissions, explicit, algo):
356 356 self.user_id = user_id
357 357 self.user_is_admin = user_is_admin
358 358 self.inherit_default_permissions = user_inherit_default_permissions
359 359 self.explicit = explicit
360 360 self.algo = algo
361 361
362 362 scope = scope or {}
363 363 self.scope_repo_id = scope.get('repo_id')
364 364 self.scope_repo_group_id = scope.get('repo_group_id')
365 365 self.scope_user_group_id = scope.get('user_group_id')
366 366
367 367 self.default_user_id = User.get_default_user(cache=True).user_id
368 368
369 369 self.permissions_repositories = PermOriginDict()
370 370 self.permissions_repository_groups = PermOriginDict()
371 371 self.permissions_user_groups = PermOriginDict()
372 372 self.permissions_global = set()
373 373
374 374 self.default_repo_perms = Permission.get_default_repo_perms(
375 375 self.default_user_id, self.scope_repo_id)
376 376 self.default_repo_groups_perms = Permission.get_default_group_perms(
377 377 self.default_user_id, self.scope_repo_group_id)
378 378 self.default_user_group_perms = \
379 379 Permission.get_default_user_group_perms(
380 380 self.default_user_id, self.scope_user_group_id)
381 381
382 382 def calculate(self):
383 383 if self.user_is_admin:
384 384 return self._admin_permissions()
385 385
386 386 self._calculate_global_default_permissions()
387 387 self._calculate_global_permissions()
388 388 self._calculate_default_permissions()
389 389 self._calculate_repository_permissions()
390 390 self._calculate_repository_group_permissions()
391 391 self._calculate_user_group_permissions()
392 392 return self._permission_structure()
393 393
394 394 def _admin_permissions(self):
395 395 """
396 396 admin user have all default rights for repositories
397 397 and groups set to admin
398 398 """
399 399 self.permissions_global.add('hg.admin')
400 400 self.permissions_global.add('hg.create.write_on_repogroup.true')
401 401
402 402 # repositories
403 403 for perm in self.default_repo_perms:
404 404 r_k = perm.UserRepoToPerm.repository.repo_name
405 405 p = 'repository.admin'
406 406 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
407 407
408 408 # repository groups
409 409 for perm in self.default_repo_groups_perms:
410 410 rg_k = perm.UserRepoGroupToPerm.group.group_name
411 411 p = 'group.admin'
412 412 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
413 413
414 414 # user groups
415 415 for perm in self.default_user_group_perms:
416 416 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
417 417 p = 'usergroup.admin'
418 418 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
419 419
420 420 return self._permission_structure()
421 421
422 422 def _calculate_global_default_permissions(self):
423 423 """
424 424 global permissions taken from the default user
425 425 """
426 426 default_global_perms = UserToPerm.query()\
427 427 .filter(UserToPerm.user_id == self.default_user_id)\
428 428 .options(joinedload(UserToPerm.permission))
429 429
430 430 for perm in default_global_perms:
431 431 self.permissions_global.add(perm.permission.permission_name)
432 432
433 433 def _calculate_global_permissions(self):
434 434 """
435 435 Set global system permissions with user permissions or permissions
436 436 taken from the user groups of the current user.
437 437
438 438 The permissions include repo creating, repo group creating, forking
439 439 etc.
440 440 """
441 441
442 442 # now we read the defined permissions and overwrite what we have set
443 443 # before those can be configured from groups or users explicitly.
444 444
445 445 # TODO: johbo: This seems to be out of sync, find out the reason
446 446 # for the comment below and update it.
447 447
448 448 # In case we want to extend this list we should be always in sync with
449 449 # User.DEFAULT_USER_PERMISSIONS definitions
450 450 _configurable = frozenset([
451 451 'hg.fork.none', 'hg.fork.repository',
452 452 'hg.create.none', 'hg.create.repository',
453 453 'hg.usergroup.create.false', 'hg.usergroup.create.true',
454 454 'hg.repogroup.create.false', 'hg.repogroup.create.true',
455 455 'hg.create.write_on_repogroup.false',
456 456 'hg.create.write_on_repogroup.true',
457 457 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
458 458 ])
459 459
460 460 # USER GROUPS comes first user group global permissions
461 461 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
462 462 .options(joinedload(UserGroupToPerm.permission))\
463 463 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
464 464 UserGroupMember.users_group_id))\
465 465 .filter(UserGroupMember.user_id == self.user_id)\
466 466 .order_by(UserGroupToPerm.users_group_id)\
467 467 .all()
468 468
469 469 # need to group here by groups since user can be in more than
470 470 # one group, so we get all groups
471 471 _explicit_grouped_perms = [
472 472 [x, list(y)] for x, y in
473 473 itertools.groupby(user_perms_from_users_groups,
474 474 lambda _x: _x.users_group)]
475 475
476 476 for gr, perms in _explicit_grouped_perms:
477 477 # since user can be in multiple groups iterate over them and
478 478 # select the lowest permissions first (more explicit)
479 479 # TODO: marcink: do this^^
480 480
481 481 # group doesn't inherit default permissions so we actually set them
482 482 if not gr.inherit_default_permissions:
483 483 # NEED TO IGNORE all previously set configurable permissions
484 484 # and replace them with explicitly set from this user
485 485 # group permissions
486 486 self.permissions_global = self.permissions_global.difference(
487 487 _configurable)
488 488 for perm in perms:
489 489 self.permissions_global.add(perm.permission.permission_name)
490 490
491 491 # user explicit global permissions
492 492 user_perms = Session().query(UserToPerm)\
493 493 .options(joinedload(UserToPerm.permission))\
494 494 .filter(UserToPerm.user_id == self.user_id).all()
495 495
496 496 if not self.inherit_default_permissions:
497 497 # NEED TO IGNORE all configurable permissions and
498 498 # replace them with explicitly set from this user permissions
499 499 self.permissions_global = self.permissions_global.difference(
500 500 _configurable)
501 501 for perm in user_perms:
502 502 self.permissions_global.add(perm.permission.permission_name)
503 503
504 504 def _calculate_default_permissions(self):
505 505 """
506 506 Set default user permissions for repositories, repository groups
507 507 taken from the default user.
508 508
509 509 Calculate inheritance of object permissions based on what we have now
510 510 in GLOBAL permissions. We check if .false is in GLOBAL since this is
511 511 explicitly set. Inherit is the opposite of .false being there.
512 512
513 513 .. note::
514 514
515 515 the syntax is little bit odd but what we need to check here is
516 516 the opposite of .false permission being in the list so even for
517 517 inconsistent state when both .true/.false is there
518 518 .false is more important
519 519
520 520 """
521 521 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
522 522 in self.permissions_global)
523 523
524 524 # defaults for repositories, taken from `default` user permissions
525 525 # on given repo
526 526 for perm in self.default_repo_perms:
527 527 r_k = perm.UserRepoToPerm.repository.repo_name
528 528 o = PermOrigin.REPO_DEFAULT
529 529 if perm.Repository.private and not (
530 530 perm.Repository.user_id == self.user_id):
531 531 # disable defaults for private repos,
532 532 p = 'repository.none'
533 533 o = PermOrigin.REPO_PRIVATE
534 534 elif perm.Repository.user_id == self.user_id:
535 535 # set admin if owner
536 536 p = 'repository.admin'
537 537 o = PermOrigin.REPO_OWNER
538 538 else:
539 539 p = perm.Permission.permission_name
540 540 # if we decide this user isn't inheriting permissions from
541 541 # default user we set him to .none so only explicit
542 542 # permissions work
543 543 if not user_inherit_object_permissions:
544 544 p = 'repository.none'
545 545 self.permissions_repositories[r_k] = p, o
546 546
547 547 # defaults for repository groups taken from `default` user permission
548 548 # on given group
549 549 for perm in self.default_repo_groups_perms:
550 550 rg_k = perm.UserRepoGroupToPerm.group.group_name
551 551 o = PermOrigin.REPOGROUP_DEFAULT
552 552 if perm.RepoGroup.user_id == self.user_id:
553 553 # set admin if owner
554 554 p = 'group.admin'
555 555 o = PermOrigin.REPOGROUP_OWNER
556 556 else:
557 557 p = perm.Permission.permission_name
558 558
559 559 # if we decide this user isn't inheriting permissions from default
560 560 # user we set him to .none so only explicit permissions work
561 561 if not user_inherit_object_permissions:
562 562 p = 'group.none'
563 563 self.permissions_repository_groups[rg_k] = p, o
564 564
565 565 # defaults for user groups taken from `default` user permission
566 566 # on given user group
567 567 for perm in self.default_user_group_perms:
568 568 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
569 569 p = perm.Permission.permission_name
570 570 o = PermOrigin.USERGROUP_DEFAULT
571 571 # if we decide this user isn't inheriting permissions from default
572 572 # user we set him to .none so only explicit permissions work
573 573 if not user_inherit_object_permissions:
574 574 p = 'usergroup.none'
575 575 self.permissions_user_groups[u_k] = p, o
576 576
577 577 def _calculate_repository_permissions(self):
578 578 """
579 579 Repository permissions for the current user.
580 580
581 581 Check if the user is part of user groups for this repository and
582 582 fill in the permission from it. `_choose_permission` decides of which
583 583 permission should be selected based on selected method.
584 584 """
585 585
586 586 # user group for repositories permissions
587 587 user_repo_perms_from_user_group = Permission\
588 588 .get_default_repo_perms_from_user_group(
589 589 self.user_id, self.scope_repo_id)
590 590
591 591 multiple_counter = collections.defaultdict(int)
592 592 for perm in user_repo_perms_from_user_group:
593 593 r_k = perm.UserGroupRepoToPerm.repository.repo_name
594 594 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
595 595 multiple_counter[r_k] += 1
596 596 p = perm.Permission.permission_name
597 597 o = PermOrigin.REPO_USERGROUP % ug_k
598 598
599 599 if perm.Repository.user_id == self.user_id:
600 600 # set admin if owner
601 601 p = 'repository.admin'
602 602 o = PermOrigin.REPO_OWNER
603 603 else:
604 604 if multiple_counter[r_k] > 1:
605 605 cur_perm = self.permissions_repositories[r_k]
606 606 p = self._choose_permission(p, cur_perm)
607 607 self.permissions_repositories[r_k] = p, o
608 608
609 609 # user explicit permissions for repositories, overrides any specified
610 610 # by the group permission
611 611 user_repo_perms = Permission.get_default_repo_perms(
612 612 self.user_id, self.scope_repo_id)
613 613 for perm in user_repo_perms:
614 614 r_k = perm.UserRepoToPerm.repository.repo_name
615 615 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
616 616 # set admin if owner
617 617 if perm.Repository.user_id == self.user_id:
618 618 p = 'repository.admin'
619 619 o = PermOrigin.REPO_OWNER
620 620 else:
621 621 p = perm.Permission.permission_name
622 622 if not self.explicit:
623 623 cur_perm = self.permissions_repositories.get(
624 624 r_k, 'repository.none')
625 625 p = self._choose_permission(p, cur_perm)
626 626 self.permissions_repositories[r_k] = p, o
627 627
628 628 def _calculate_repository_group_permissions(self):
629 629 """
630 630 Repository group permissions for the current user.
631 631
632 632 Check if the user is part of user groups for repository groups and
633 633 fill in the permissions from it. `_choose_permmission` decides of which
634 634 permission should be selected based on selected method.
635 635 """
636 636 # user group for repo groups permissions
637 637 user_repo_group_perms_from_user_group = Permission\
638 638 .get_default_group_perms_from_user_group(
639 639 self.user_id, self.scope_repo_group_id)
640 640
641 641 multiple_counter = collections.defaultdict(int)
642 642 for perm in user_repo_group_perms_from_user_group:
643 643 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
644 644 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
645 645 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
646 646 multiple_counter[g_k] += 1
647 647 p = perm.Permission.permission_name
648 648 if perm.RepoGroup.user_id == self.user_id:
649 649 # set admin if owner
650 650 p = 'group.admin'
651 651 o = PermOrigin.REPOGROUP_OWNER
652 652 else:
653 653 if multiple_counter[g_k] > 1:
654 654 cur_perm = self.permissions_repository_groups[g_k]
655 655 p = self._choose_permission(p, cur_perm)
656 656 self.permissions_repository_groups[g_k] = p, o
657 657
658 658 # user explicit permissions for repository groups
659 659 user_repo_groups_perms = Permission.get_default_group_perms(
660 660 self.user_id, self.scope_repo_group_id)
661 661 for perm in user_repo_groups_perms:
662 662 rg_k = perm.UserRepoGroupToPerm.group.group_name
663 663 u_k = perm.UserRepoGroupToPerm.user.username
664 664 o = PermOrigin.REPOGROUP_USER % u_k
665 665
666 666 if perm.RepoGroup.user_id == self.user_id:
667 667 # set admin if owner
668 668 p = 'group.admin'
669 669 o = PermOrigin.REPOGROUP_OWNER
670 670 else:
671 671 p = perm.Permission.permission_name
672 672 if not self.explicit:
673 673 cur_perm = self.permissions_repository_groups.get(
674 674 rg_k, 'group.none')
675 675 p = self._choose_permission(p, cur_perm)
676 676 self.permissions_repository_groups[rg_k] = p, o
677 677
678 678 def _calculate_user_group_permissions(self):
679 679 """
680 680 User group permissions for the current user.
681 681 """
682 682 # user group for user group permissions
683 683 user_group_from_user_group = Permission\
684 684 .get_default_user_group_perms_from_user_group(
685 685 self.user_id, self.scope_repo_group_id)
686 686
687 687 multiple_counter = collections.defaultdict(int)
688 688 for perm in user_group_from_user_group:
689 689 g_k = perm.UserGroupUserGroupToPerm\
690 690 .target_user_group.users_group_name
691 691 u_k = perm.UserGroupUserGroupToPerm\
692 692 .user_group.users_group_name
693 693 o = PermOrigin.USERGROUP_USERGROUP % u_k
694 694 multiple_counter[g_k] += 1
695 695 p = perm.Permission.permission_name
696 696 if multiple_counter[g_k] > 1:
697 697 cur_perm = self.permissions_user_groups[g_k]
698 698 p = self._choose_permission(p, cur_perm)
699 699 self.permissions_user_groups[g_k] = p, o
700 700
701 701 # user explicit permission for user groups
702 702 user_user_groups_perms = Permission.get_default_user_group_perms(
703 703 self.user_id, self.scope_user_group_id)
704 704 for perm in user_user_groups_perms:
705 705 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
706 706 u_k = perm.UserUserGroupToPerm.user.username
707 707 p = perm.Permission.permission_name
708 708 o = PermOrigin.USERGROUP_USER % u_k
709 709 if not self.explicit:
710 710 cur_perm = self.permissions_user_groups.get(
711 711 ug_k, 'usergroup.none')
712 712 p = self._choose_permission(p, cur_perm)
713 713 self.permissions_user_groups[ug_k] = p, o
714 714
715 715 def _choose_permission(self, new_perm, cur_perm):
716 716 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
717 717 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
718 718 if self.algo == 'higherwin':
719 719 if new_perm_val > cur_perm_val:
720 720 return new_perm
721 721 return cur_perm
722 722 elif self.algo == 'lowerwin':
723 723 if new_perm_val < cur_perm_val:
724 724 return new_perm
725 725 return cur_perm
726 726
727 727 def _permission_structure(self):
728 728 return {
729 729 'global': self.permissions_global,
730 730 'repositories': self.permissions_repositories,
731 731 'repositories_groups': self.permissions_repository_groups,
732 732 'user_groups': self.permissions_user_groups,
733 733 }
734 734
735 735
736 736 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
737 737 """
738 738 Check if given controller_name is in whitelist of auth token access
739 739 """
740 740 if not whitelist:
741 741 from rhodecode import CONFIG
742 742 whitelist = aslist(
743 743 CONFIG.get('api_access_controllers_whitelist'), sep=',')
744 744 log.debug(
745 745 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
746 746
747 747 auth_token_access_valid = False
748 748 for entry in whitelist:
749 749 if fnmatch.fnmatch(controller_name, entry):
750 750 auth_token_access_valid = True
751 751 break
752 752
753 753 if auth_token_access_valid:
754 754 log.debug('controller:%s matches entry in whitelist'
755 755 % (controller_name,))
756 756 else:
757 757 msg = ('controller: %s does *NOT* match any entry in whitelist'
758 758 % (controller_name,))
759 759 if auth_token:
760 760 # if we use auth token key and don't have access it's a warning
761 761 log.warning(msg)
762 762 else:
763 763 log.debug(msg)
764 764
765 765 return auth_token_access_valid
766 766
767 767
768 768 class AuthUser(object):
769 769 """
770 770 A simple object that handles all attributes of user in RhodeCode
771 771
772 772 It does lookup based on API key,given user, or user present in session
773 773 Then it fills all required information for such user. It also checks if
774 774 anonymous access is enabled and if so, it returns default user as logged in
775 775 """
776 776 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
777 777
778 778 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
779 779
780 780 self.user_id = user_id
781 781 self._api_key = api_key
782 782
783 783 self.api_key = None
784 784 self.feed_token = ''
785 785 self.username = username
786 786 self.ip_addr = ip_addr
787 787 self.name = ''
788 788 self.lastname = ''
789 789 self.email = ''
790 790 self.is_authenticated = False
791 791 self.admin = False
792 792 self.inherit_default_permissions = False
793 793 self.password = ''
794 794
795 795 self.anonymous_user = None # propagated on propagate_data
796 796 self.propagate_data()
797 797 self._instance = None
798 798 self._permissions_scoped_cache = {} # used to bind scoped calculation
799 799
800 800 @LazyProperty
801 801 def permissions(self):
802 802 return self.get_perms(user=self, cache=False)
803 803
804 804 def permissions_with_scope(self, scope):
805 805 """
806 806 Call the get_perms function with scoped data. The scope in that function
807 807 narrows the SQL calls to the given ID of objects resulting in fetching
808 808 Just particular permission we want to obtain. If scope is an empty dict
809 809 then it basically narrows the scope to GLOBAL permissions only.
810 810
811 811 :param scope: dict
812 812 """
813 813 if 'repo_name' in scope:
814 814 obj = Repository.get_by_repo_name(scope['repo_name'])
815 815 if obj:
816 816 scope['repo_id'] = obj.repo_id
817 817 _scope = {
818 818 'repo_id': -1,
819 819 'user_group_id': -1,
820 820 'repo_group_id': -1,
821 821 }
822 822 _scope.update(scope)
823 823 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
824 824 _scope.items())))
825 825 if cache_key not in self._permissions_scoped_cache:
826 826 # store in cache to mimic how the @LazyProperty works,
827 827 # the difference here is that we use the unique key calculated
828 828 # from params and values
829 829 res = self.get_perms(user=self, cache=False, scope=_scope)
830 830 self._permissions_scoped_cache[cache_key] = res
831 831 return self._permissions_scoped_cache[cache_key]
832 832
833 833 @property
834 834 def auth_tokens(self):
835 835 return self.get_auth_tokens()
836 836
837 837 def get_instance(self):
838 838 return User.get(self.user_id)
839 839
840 840 def update_lastactivity(self):
841 841 if self.user_id:
842 842 User.get(self.user_id).update_lastactivity()
843 843
844 844 def propagate_data(self):
845 845 """
846 846 Fills in user data and propagates values to this instance. Maps fetched
847 847 user attributes to this class instance attributes
848 848 """
849 849
850 850 user_model = UserModel()
851 851 anon_user = self.anonymous_user = User.get_default_user(cache=True)
852 852 is_user_loaded = False
853 853
854 854 # lookup by userid
855 855 if self.user_id is not None and self.user_id != anon_user.user_id:
856 856 log.debug('Trying Auth User lookup by USER ID %s' % self.user_id)
857 857 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
858 858
859 859 # try go get user by api key
860 860 elif self._api_key and self._api_key != anon_user.api_key:
861 861 log.debug('Trying Auth User lookup by API KEY %s' % self._api_key)
862 862 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
863 863
864 864 # lookup by username
865 865 elif self.username:
866 866 log.debug('Trying Auth User lookup by USER NAME %s' % self.username)
867 867 is_user_loaded = user_model.fill_data(self, username=self.username)
868 868 else:
869 869 log.debug('No data in %s that could been used to log in' % self)
870 870
871 871 if not is_user_loaded:
872 872 log.debug('Failed to load user. Fallback to default user')
873 873 # if we cannot authenticate user try anonymous
874 874 if anon_user.active:
875 875 user_model.fill_data(self, user_id=anon_user.user_id)
876 876 # then we set this user is logged in
877 877 self.is_authenticated = True
878 878 else:
879 879 # in case of disabled anonymous user we reset some of the
880 880 # parameters so such user is "corrupted", skipping the fill_data
881 881 for attr in ['user_id', 'username', 'admin', 'active']:
882 882 setattr(self, attr, None)
883 883 self.is_authenticated = False
884 884
885 885 if not self.username:
886 886 self.username = 'None'
887 887
888 888 log.debug('Auth User is now %s' % self)
889 889
890 890 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
891 891 cache=False):
892 892 """
893 893 Fills user permission attribute with permissions taken from database
894 894 works for permissions given for repositories, and for permissions that
895 895 are granted to groups
896 896
897 897 :param user: instance of User object from database
898 898 :param explicit: In case there are permissions both for user and a group
899 899 that user is part of, explicit flag will defiine if user will
900 900 explicitly override permissions from group, if it's False it will
901 901 make decision based on the algo
902 902 :param algo: algorithm to decide what permission should be choose if
903 903 it's multiple defined, eg user in two different groups. It also
904 904 decides if explicit flag is turned off how to specify the permission
905 905 for case when user is in a group + have defined separate permission
906 906 """
907 907 user_id = user.user_id
908 908 user_is_admin = user.is_admin
909 909
910 910 # inheritance of global permissions like create repo/fork repo etc
911 911 user_inherit_default_permissions = user.inherit_default_permissions
912 912
913 913 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
914 914 compute = caches.conditional_cache(
915 915 'short_term', 'cache_desc',
916 916 condition=cache, func=_cached_perms_data)
917 917 result = compute(user_id, scope, user_is_admin,
918 918 user_inherit_default_permissions, explicit, algo)
919 919
920 920 result_repr = []
921 921 for k in result:
922 922 result_repr.append((k, len(result[k])))
923 923
924 924 log.debug('PERMISSION tree computed %s' % (result_repr,))
925 925 return result
926 926
927 927 def get_auth_tokens(self):
928 928 auth_tokens = [self.api_key]
929 929 for api_key in UserApiKeys.query()\
930 930 .filter(UserApiKeys.user_id == self.user_id)\
931 931 .filter(or_(UserApiKeys.expires == -1,
932 932 UserApiKeys.expires >= time.time())).all():
933 933 auth_tokens.append(api_key.api_key)
934 934
935 935 return auth_tokens
936 936
937 937 @property
938 938 def is_default(self):
939 939 return self.username == User.DEFAULT_USER
940 940
941 941 @property
942 942 def is_admin(self):
943 943 return self.admin
944 944
945 945 @property
946 946 def is_user_object(self):
947 947 return self.user_id is not None
948 948
949 949 @property
950 950 def repositories_admin(self):
951 951 """
952 952 Returns list of repositories you're an admin of
953 953 """
954 954 return [x[0] for x in self.permissions['repositories'].iteritems()
955 955 if x[1] == 'repository.admin']
956 956
957 957 @property
958 958 def repository_groups_admin(self):
959 959 """
960 960 Returns list of repository groups you're an admin of
961 961 """
962 962 return [x[0]
963 963 for x in self.permissions['repositories_groups'].iteritems()
964 964 if x[1] == 'group.admin']
965 965
966 966 @property
967 967 def user_groups_admin(self):
968 968 """
969 969 Returns list of user groups you're an admin of
970 970 """
971 971 return [x[0] for x in self.permissions['user_groups'].iteritems()
972 972 if x[1] == 'usergroup.admin']
973 973
974 974 @property
975 975 def ip_allowed(self):
976 976 """
977 977 Checks if ip_addr used in constructor is allowed from defined list of
978 978 allowed ip_addresses for user
979 979
980 980 :returns: boolean, True if ip is in allowed ip range
981 981 """
982 982 # check IP
983 983 inherit = self.inherit_default_permissions
984 984 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
985 985 inherit_from_default=inherit)
986 986
987 987 @classmethod
988 988 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
989 989 allowed_ips = AuthUser.get_allowed_ips(
990 990 user_id, cache=True, inherit_from_default=inherit_from_default)
991 991 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
992 992 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
993 993 return True
994 994 else:
995 995 log.info('Access for IP:%s forbidden, '
996 996 'not in %s' % (ip_addr, allowed_ips))
997 997 return False
998 998
999 999 def __repr__(self):
1000 1000 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1001 1001 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1002 1002
1003 1003 def set_authenticated(self, authenticated=True):
1004 1004 if self.user_id != self.anonymous_user.user_id:
1005 1005 self.is_authenticated = authenticated
1006 1006
1007 1007 def get_cookie_store(self):
1008 1008 return {
1009 1009 'username': self.username,
1010 1010 'password': md5(self.password),
1011 1011 'user_id': self.user_id,
1012 1012 'is_authenticated': self.is_authenticated
1013 1013 }
1014 1014
1015 1015 @classmethod
1016 1016 def from_cookie_store(cls, cookie_store):
1017 1017 """
1018 1018 Creates AuthUser from a cookie store
1019 1019
1020 1020 :param cls:
1021 1021 :param cookie_store:
1022 1022 """
1023 1023 user_id = cookie_store.get('user_id')
1024 1024 username = cookie_store.get('username')
1025 1025 api_key = cookie_store.get('api_key')
1026 1026 return AuthUser(user_id, api_key, username)
1027 1027
1028 1028 @classmethod
1029 1029 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1030 1030 _set = set()
1031 1031
1032 1032 if inherit_from_default:
1033 1033 default_ips = UserIpMap.query().filter(
1034 1034 UserIpMap.user == User.get_default_user(cache=True))
1035 1035 if cache:
1036 1036 default_ips = default_ips.options(FromCache("sql_cache_short",
1037 1037 "get_user_ips_default"))
1038 1038
1039 1039 # populate from default user
1040 1040 for ip in default_ips:
1041 1041 try:
1042 1042 _set.add(ip.ip_addr)
1043 1043 except ObjectDeletedError:
1044 1044 # since we use heavy caching sometimes it happens that
1045 1045 # we get deleted objects here, we just skip them
1046 1046 pass
1047 1047
1048 1048 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1049 1049 if cache:
1050 1050 user_ips = user_ips.options(FromCache("sql_cache_short",
1051 1051 "get_user_ips_%s" % user_id))
1052 1052
1053 1053 for ip in user_ips:
1054 1054 try:
1055 1055 _set.add(ip.ip_addr)
1056 1056 except ObjectDeletedError:
1057 1057 # since we use heavy caching sometimes it happens that we get
1058 1058 # deleted objects here, we just skip them
1059 1059 pass
1060 1060 return _set or set(['0.0.0.0/0', '::/0'])
1061 1061
1062 1062
1063 1063 def set_available_permissions(config):
1064 1064 """
1065 1065 This function will propagate pylons globals with all available defined
1066 1066 permission given in db. We don't want to check each time from db for new
1067 1067 permissions since adding a new permission also requires application restart
1068 1068 ie. to decorate new views with the newly created permission
1069 1069
1070 1070 :param config: current pylons config instance
1071 1071
1072 1072 """
1073 1073 log.info('getting information about all available permissions')
1074 1074 try:
1075 1075 sa = meta.Session
1076 1076 all_perms = sa.query(Permission).all()
1077 1077 config['available_permissions'] = [x.permission_name for x in all_perms]
1078 1078 except Exception:
1079 1079 log.error(traceback.format_exc())
1080 1080 finally:
1081 1081 meta.Session.remove()
1082 1082
1083 1083
1084 1084 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1085 1085 """
1086 1086 Return the current authentication token, creating one if one doesn't
1087 1087 already exist and the save_if_missing flag is present.
1088 1088
1089 1089 :param session: pass in the pylons session, else we use the global ones
1090 1090 :param force_new: force to re-generate the token and store it in session
1091 1091 :param save_if_missing: save the newly generated token if it's missing in
1092 1092 session
1093 1093 """
1094 1094 if not session:
1095 1095 from pylons import session
1096 1096
1097 1097 if (csrf_token_key not in session and save_if_missing) or force_new:
1098 1098 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1099 1099 session[csrf_token_key] = token
1100 1100 if hasattr(session, 'save'):
1101 1101 session.save()
1102 1102 return session.get(csrf_token_key)
1103 1103
1104 1104
1105 1105 # CHECK DECORATORS
1106 1106 class CSRFRequired(object):
1107 1107 """
1108 1108 Decorator for authenticating a form
1109 1109
1110 1110 This decorator uses an authorization token stored in the client's
1111 1111 session for prevention of certain Cross-site request forgery (CSRF)
1112 1112 attacks (See
1113 1113 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1114 1114 information).
1115 1115
1116 1116 For use with the ``webhelpers.secure_form`` helper functions.
1117 1117
1118 1118 """
1119 def __init__(self, token=csrf_token_key, header='X-CSRF-Token'):
1119 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1120 except_methods=None):
1120 1121 self.token = token
1121 1122 self.header = header
1123 self.except_methods = except_methods or []
1122 1124
1123 1125 def __call__(self, func):
1124 1126 return get_cython_compat_decorator(self.__wrapper, func)
1125 1127
1126 1128 def _get_csrf(self, _request):
1127 1129 return _request.POST.get(self.token, _request.headers.get(self.header))
1128 1130
1129 1131 def check_csrf(self, _request, cur_token):
1130 1132 supplied_token = self._get_csrf(_request)
1131 1133 return supplied_token and supplied_token == cur_token
1132 1134
1133 1135 def __wrapper(self, func, *fargs, **fkwargs):
1136 if request.method in self.except_methods:
1137 return func(*fargs, **fkwargs)
1138
1134 1139 cur_token = get_csrf_token(save_if_missing=False)
1135 1140 if self.check_csrf(request, cur_token):
1136 1141 if request.POST.get(self.token):
1137 1142 del request.POST[self.token]
1138 1143 return func(*fargs, **fkwargs)
1139 1144 else:
1140 1145 reason = 'token-missing'
1141 1146 supplied_token = self._get_csrf(request)
1142 1147 if supplied_token and cur_token != supplied_token:
1143 1148 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1144 1149 supplied_token or ''[:6])
1145 1150
1146 1151 csrf_message = \
1147 1152 ("Cross-site request forgery detected, request denied. See "
1148 1153 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1149 1154 "more information.")
1150 1155 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1151 1156 'REMOTE_ADDR:%s, HEADERS:%s' % (
1152 1157 request, reason, request.remote_addr, request.headers))
1153 1158
1154 1159 abort(403, detail=csrf_message)
1155 1160
1156 1161
1157 1162 class LoginRequired(object):
1158 1163 """
1159 1164 Must be logged in to execute this function else
1160 1165 redirect to login page
1161 1166
1162 1167 :param api_access: if enabled this checks only for valid auth token
1163 1168 and grants access based on valid token
1164 1169 """
1165 1170 def __init__(self, auth_token_access=False):
1166 1171 self.auth_token_access = auth_token_access
1167 1172
1168 1173 def __call__(self, func):
1169 1174 return get_cython_compat_decorator(self.__wrapper, func)
1170 1175
1171 1176 def __wrapper(self, func, *fargs, **fkwargs):
1172 1177 from rhodecode.lib import helpers as h
1173 1178 cls = fargs[0]
1174 1179 user = cls._rhodecode_user
1175 1180 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1176 1181 log.debug('Starting login restriction checks for user: %s' % (user,))
1177 1182 # check if our IP is allowed
1178 1183 ip_access_valid = True
1179 1184 if not user.ip_allowed:
1180 1185 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1181 1186 category='warning')
1182 1187 ip_access_valid = False
1183 1188
1184 1189 # check if we used an APIKEY and it's a valid one
1185 1190 # defined whitelist of controllers which API access will be enabled
1186 1191 _auth_token = request.GET.get(
1187 1192 'auth_token', '') or request.GET.get('api_key', '')
1188 1193 auth_token_access_valid = allowed_auth_token_access(
1189 1194 loc, auth_token=_auth_token)
1190 1195
1191 1196 # explicit controller is enabled or API is in our whitelist
1192 1197 if self.auth_token_access or auth_token_access_valid:
1193 1198 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1194 1199
1195 1200 if _auth_token and _auth_token in user.auth_tokens:
1196 1201 auth_token_access_valid = True
1197 1202 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1198 1203 else:
1199 1204 auth_token_access_valid = False
1200 1205 if not _auth_token:
1201 1206 log.debug("AUTH TOKEN *NOT* present in request")
1202 1207 else:
1203 1208 log.warning(
1204 1209 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1205 1210
1206 1211 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1207 1212 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1208 1213 else 'AUTH_TOKEN_AUTH'
1209 1214
1210 1215 if ip_access_valid and (
1211 1216 user.is_authenticated or auth_token_access_valid):
1212 1217 log.info(
1213 1218 'user %s authenticating with:%s IS authenticated on func %s'
1214 1219 % (user, reason, loc))
1215 1220
1216 1221 # update user data to check last activity
1217 1222 user.update_lastactivity()
1218 1223 Session().commit()
1219 1224 return func(*fargs, **fkwargs)
1220 1225 else:
1221 1226 log.warning(
1222 1227 'user %s authenticating with:%s NOT authenticated on '
1223 1228 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1224 1229 % (user, reason, loc, ip_access_valid,
1225 1230 auth_token_access_valid))
1226 1231 # we preserve the get PARAM
1227 1232 came_from = request.path_qs
1228 1233
1229 1234 log.debug('redirecting to login page with %s' % (came_from,))
1230 1235 return redirect(
1231 1236 h.route_path('login', _query={'came_from': came_from}))
1232 1237
1233 1238
1234 1239 class NotAnonymous(object):
1235 1240 """
1236 1241 Must be logged in to execute this function else
1237 1242 redirect to login page"""
1238 1243
1239 1244 def __call__(self, func):
1240 1245 return get_cython_compat_decorator(self.__wrapper, func)
1241 1246
1242 1247 def __wrapper(self, func, *fargs, **fkwargs):
1243 1248 cls = fargs[0]
1244 1249 self.user = cls._rhodecode_user
1245 1250
1246 1251 log.debug('Checking if user is not anonymous @%s' % cls)
1247 1252
1248 1253 anonymous = self.user.username == User.DEFAULT_USER
1249 1254
1250 1255 if anonymous:
1251 1256 came_from = request.path_qs
1252 1257
1253 1258 import rhodecode.lib.helpers as h
1254 1259 h.flash(_('You need to be a registered user to '
1255 1260 'perform this action'),
1256 1261 category='warning')
1257 1262 return redirect(
1258 1263 h.route_path('login', _query={'came_from': came_from}))
1259 1264 else:
1260 1265 return func(*fargs, **fkwargs)
1261 1266
1262 1267
1263 1268 class XHRRequired(object):
1264 1269 def __call__(self, func):
1265 1270 return get_cython_compat_decorator(self.__wrapper, func)
1266 1271
1267 1272 def __wrapper(self, func, *fargs, **fkwargs):
1268 1273 log.debug('Checking if request is XMLHttpRequest (XHR)')
1269 1274 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1270 1275 if not request.is_xhr:
1271 1276 abort(400, detail=xhr_message)
1272 1277
1273 1278 return func(*fargs, **fkwargs)
1274 1279
1275 1280
1276 1281 class HasAcceptedRepoType(object):
1277 1282 """
1278 1283 Check if requested repo is within given repo type aliases
1279 1284
1280 1285 TODO: anderson: not sure where to put this decorator
1281 1286 """
1282 1287
1283 1288 def __init__(self, *repo_type_list):
1284 1289 self.repo_type_list = set(repo_type_list)
1285 1290
1286 1291 def __call__(self, func):
1287 1292 return get_cython_compat_decorator(self.__wrapper, func)
1288 1293
1289 1294 def __wrapper(self, func, *fargs, **fkwargs):
1290 1295 cls = fargs[0]
1291 1296 rhodecode_repo = cls.rhodecode_repo
1292 1297
1293 1298 log.debug('%s checking repo type for %s in %s',
1294 1299 self.__class__.__name__,
1295 1300 rhodecode_repo.alias, self.repo_type_list)
1296 1301
1297 1302 if rhodecode_repo.alias in self.repo_type_list:
1298 1303 return func(*fargs, **fkwargs)
1299 1304 else:
1300 1305 import rhodecode.lib.helpers as h
1301 1306 h.flash(h.literal(
1302 1307 _('Action not supported for %s.' % rhodecode_repo.alias)),
1303 1308 category='warning')
1304 1309 return redirect(
1305 1310 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1306 1311
1307 1312
1308 1313 class PermsDecorator(object):
1309 1314 """
1310 1315 Base class for controller decorators, we extract the current user from
1311 1316 the class itself, which has it stored in base controllers
1312 1317 """
1313 1318
1314 1319 def __init__(self, *required_perms):
1315 1320 self.required_perms = set(required_perms)
1316 1321
1317 1322 def __call__(self, func):
1318 1323 return get_cython_compat_decorator(self.__wrapper, func)
1319 1324
1320 1325 def __wrapper(self, func, *fargs, **fkwargs):
1321 1326 cls = fargs[0]
1322 1327 _user = cls._rhodecode_user
1323 1328
1324 1329 log.debug('checking %s permissions %s for %s %s',
1325 1330 self.__class__.__name__, self.required_perms, cls, _user)
1326 1331
1327 1332 if self.check_permissions(_user):
1328 1333 log.debug('Permission granted for %s %s', cls, _user)
1329 1334 return func(*fargs, **fkwargs)
1330 1335
1331 1336 else:
1332 1337 log.debug('Permission denied for %s %s', cls, _user)
1333 1338 anonymous = _user.username == User.DEFAULT_USER
1334 1339
1335 1340 if anonymous:
1336 1341 came_from = request.path_qs
1337 1342
1338 1343 import rhodecode.lib.helpers as h
1339 1344 h.flash(_('You need to be signed in to view this page'),
1340 1345 category='warning')
1341 1346 return redirect(
1342 1347 h.route_path('login', _query={'came_from': came_from}))
1343 1348
1344 1349 else:
1345 1350 # redirect with forbidden ret code
1346 1351 return abort(403)
1347 1352
1348 1353 def check_permissions(self, user):
1349 1354 """Dummy function for overriding"""
1350 1355 raise NotImplementedError(
1351 1356 'You have to write this function in child class')
1352 1357
1353 1358
1354 1359 class HasPermissionAllDecorator(PermsDecorator):
1355 1360 """
1356 1361 Checks for access permission for all given predicates. All of them
1357 1362 have to be meet in order to fulfill the request
1358 1363 """
1359 1364
1360 1365 def check_permissions(self, user):
1361 1366 perms = user.permissions_with_scope({})
1362 1367 if self.required_perms.issubset(perms['global']):
1363 1368 return True
1364 1369 return False
1365 1370
1366 1371
1367 1372 class HasPermissionAnyDecorator(PermsDecorator):
1368 1373 """
1369 1374 Checks for access permission for any of given predicates. In order to
1370 1375 fulfill the request any of predicates must be meet
1371 1376 """
1372 1377
1373 1378 def check_permissions(self, user):
1374 1379 perms = user.permissions_with_scope({})
1375 1380 if self.required_perms.intersection(perms['global']):
1376 1381 return True
1377 1382 return False
1378 1383
1379 1384
1380 1385 class HasRepoPermissionAllDecorator(PermsDecorator):
1381 1386 """
1382 1387 Checks for access permission for all given predicates for specific
1383 1388 repository. All of them have to be meet in order to fulfill the request
1384 1389 """
1385 1390
1386 1391 def check_permissions(self, user):
1387 1392 perms = user.permissions
1388 1393 repo_name = get_repo_slug(request)
1389 1394 try:
1390 1395 user_perms = set([perms['repositories'][repo_name]])
1391 1396 except KeyError:
1392 1397 return False
1393 1398 if self.required_perms.issubset(user_perms):
1394 1399 return True
1395 1400 return False
1396 1401
1397 1402
1398 1403 class HasRepoPermissionAnyDecorator(PermsDecorator):
1399 1404 """
1400 1405 Checks for access permission for any of given predicates for specific
1401 1406 repository. In order to fulfill the request any of predicates must be meet
1402 1407 """
1403 1408
1404 1409 def check_permissions(self, user):
1405 1410 perms = user.permissions
1406 1411 repo_name = get_repo_slug(request)
1407 1412 try:
1408 1413 user_perms = set([perms['repositories'][repo_name]])
1409 1414 except KeyError:
1410 1415 return False
1411 1416
1412 1417 if self.required_perms.intersection(user_perms):
1413 1418 return True
1414 1419 return False
1415 1420
1416 1421
1417 1422 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1418 1423 """
1419 1424 Checks for access permission for all given predicates for specific
1420 1425 repository group. All of them have to be meet in order to
1421 1426 fulfill the request
1422 1427 """
1423 1428
1424 1429 def check_permissions(self, user):
1425 1430 perms = user.permissions
1426 1431 group_name = get_repo_group_slug(request)
1427 1432 try:
1428 1433 user_perms = set([perms['repositories_groups'][group_name]])
1429 1434 except KeyError:
1430 1435 return False
1431 1436
1432 1437 if self.required_perms.issubset(user_perms):
1433 1438 return True
1434 1439 return False
1435 1440
1436 1441
1437 1442 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1438 1443 """
1439 1444 Checks for access permission for any of given predicates for specific
1440 1445 repository group. In order to fulfill the request any
1441 1446 of predicates must be met
1442 1447 """
1443 1448
1444 1449 def check_permissions(self, user):
1445 1450 perms = user.permissions
1446 1451 group_name = get_repo_group_slug(request)
1447 1452 try:
1448 1453 user_perms = set([perms['repositories_groups'][group_name]])
1449 1454 except KeyError:
1450 1455 return False
1451 1456
1452 1457 if self.required_perms.intersection(user_perms):
1453 1458 return True
1454 1459 return False
1455 1460
1456 1461
1457 1462 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1458 1463 """
1459 1464 Checks for access permission for all given predicates for specific
1460 1465 user group. All of them have to be meet in order to fulfill the request
1461 1466 """
1462 1467
1463 1468 def check_permissions(self, user):
1464 1469 perms = user.permissions
1465 1470 group_name = get_user_group_slug(request)
1466 1471 try:
1467 1472 user_perms = set([perms['user_groups'][group_name]])
1468 1473 except KeyError:
1469 1474 return False
1470 1475
1471 1476 if self.required_perms.issubset(user_perms):
1472 1477 return True
1473 1478 return False
1474 1479
1475 1480
1476 1481 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1477 1482 """
1478 1483 Checks for access permission for any of given predicates for specific
1479 1484 user group. In order to fulfill the request any of predicates must be meet
1480 1485 """
1481 1486
1482 1487 def check_permissions(self, user):
1483 1488 perms = user.permissions
1484 1489 group_name = get_user_group_slug(request)
1485 1490 try:
1486 1491 user_perms = set([perms['user_groups'][group_name]])
1487 1492 except KeyError:
1488 1493 return False
1489 1494
1490 1495 if self.required_perms.intersection(user_perms):
1491 1496 return True
1492 1497 return False
1493 1498
1494 1499
1495 1500 # CHECK FUNCTIONS
1496 1501 class PermsFunction(object):
1497 1502 """Base function for other check functions"""
1498 1503
1499 1504 def __init__(self, *perms):
1500 1505 self.required_perms = set(perms)
1501 1506 self.repo_name = None
1502 1507 self.repo_group_name = None
1503 1508 self.user_group_name = None
1504 1509
1505 1510 def __bool__(self):
1506 1511 frame = inspect.currentframe()
1507 1512 stack_trace = traceback.format_stack(frame)
1508 1513 log.error('Checking bool value on a class instance of perm '
1509 1514 'function is not allowed: %s' % ''.join(stack_trace))
1510 1515 # rather than throwing errors, here we always return False so if by
1511 1516 # accident someone checks truth for just an instance it will always end
1512 1517 # up in returning False
1513 1518 return False
1514 1519 __nonzero__ = __bool__
1515 1520
1516 1521 def __call__(self, check_location='', user=None):
1517 1522 if not user:
1518 1523 log.debug('Using user attribute from global request')
1519 1524 # TODO: remove this someday,put as user as attribute here
1520 1525 user = request.user
1521 1526
1522 1527 # init auth user if not already given
1523 1528 if not isinstance(user, AuthUser):
1524 1529 log.debug('Wrapping user %s into AuthUser', user)
1525 1530 user = AuthUser(user.user_id)
1526 1531
1527 1532 cls_name = self.__class__.__name__
1528 1533 check_scope = self._get_check_scope(cls_name)
1529 1534 check_location = check_location or 'unspecified location'
1530 1535
1531 1536 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1532 1537 self.required_perms, user, check_scope, check_location)
1533 1538 if not user:
1534 1539 log.warning('Empty user given for permission check')
1535 1540 return False
1536 1541
1537 1542 if self.check_permissions(user):
1538 1543 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1539 1544 check_scope, user, check_location)
1540 1545 return True
1541 1546
1542 1547 else:
1543 1548 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1544 1549 check_scope, user, check_location)
1545 1550 return False
1546 1551
1547 1552 def _get_check_scope(self, cls_name):
1548 1553 return {
1549 1554 'HasPermissionAll': 'GLOBAL',
1550 1555 'HasPermissionAny': 'GLOBAL',
1551 1556 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1552 1557 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1553 1558 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1554 1559 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1555 1560 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1556 1561 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1557 1562 }.get(cls_name, '?:%s' % cls_name)
1558 1563
1559 1564 def check_permissions(self, user):
1560 1565 """Dummy function for overriding"""
1561 1566 raise Exception('You have to write this function in child class')
1562 1567
1563 1568
1564 1569 class HasPermissionAll(PermsFunction):
1565 1570 def check_permissions(self, user):
1566 1571 perms = user.permissions_with_scope({})
1567 1572 if self.required_perms.issubset(perms.get('global')):
1568 1573 return True
1569 1574 return False
1570 1575
1571 1576
1572 1577 class HasPermissionAny(PermsFunction):
1573 1578 def check_permissions(self, user):
1574 1579 perms = user.permissions_with_scope({})
1575 1580 if self.required_perms.intersection(perms.get('global')):
1576 1581 return True
1577 1582 return False
1578 1583
1579 1584
1580 1585 class HasRepoPermissionAll(PermsFunction):
1581 1586 def __call__(self, repo_name=None, check_location='', user=None):
1582 1587 self.repo_name = repo_name
1583 1588 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1584 1589
1585 1590 def check_permissions(self, user):
1586 1591 if not self.repo_name:
1587 1592 self.repo_name = get_repo_slug(request)
1588 1593
1589 1594 perms = user.permissions
1590 1595 try:
1591 1596 user_perms = set([perms['repositories'][self.repo_name]])
1592 1597 except KeyError:
1593 1598 return False
1594 1599 if self.required_perms.issubset(user_perms):
1595 1600 return True
1596 1601 return False
1597 1602
1598 1603
1599 1604 class HasRepoPermissionAny(PermsFunction):
1600 1605 def __call__(self, repo_name=None, check_location='', user=None):
1601 1606 self.repo_name = repo_name
1602 1607 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1603 1608
1604 1609 def check_permissions(self, user):
1605 1610 if not self.repo_name:
1606 1611 self.repo_name = get_repo_slug(request)
1607 1612
1608 1613 perms = user.permissions
1609 1614 try:
1610 1615 user_perms = set([perms['repositories'][self.repo_name]])
1611 1616 except KeyError:
1612 1617 return False
1613 1618 if self.required_perms.intersection(user_perms):
1614 1619 return True
1615 1620 return False
1616 1621
1617 1622
1618 1623 class HasRepoGroupPermissionAny(PermsFunction):
1619 1624 def __call__(self, group_name=None, check_location='', user=None):
1620 1625 self.repo_group_name = group_name
1621 1626 return super(HasRepoGroupPermissionAny, self).__call__(
1622 1627 check_location, user)
1623 1628
1624 1629 def check_permissions(self, user):
1625 1630 perms = user.permissions
1626 1631 try:
1627 1632 user_perms = set(
1628 1633 [perms['repositories_groups'][self.repo_group_name]])
1629 1634 except KeyError:
1630 1635 return False
1631 1636 if self.required_perms.intersection(user_perms):
1632 1637 return True
1633 1638 return False
1634 1639
1635 1640
1636 1641 class HasRepoGroupPermissionAll(PermsFunction):
1637 1642 def __call__(self, group_name=None, check_location='', user=None):
1638 1643 self.repo_group_name = group_name
1639 1644 return super(HasRepoGroupPermissionAll, self).__call__(
1640 1645 check_location, user)
1641 1646
1642 1647 def check_permissions(self, user):
1643 1648 perms = user.permissions
1644 1649 try:
1645 1650 user_perms = set(
1646 1651 [perms['repositories_groups'][self.repo_group_name]])
1647 1652 except KeyError:
1648 1653 return False
1649 1654 if self.required_perms.issubset(user_perms):
1650 1655 return True
1651 1656 return False
1652 1657
1653 1658
1654 1659 class HasUserGroupPermissionAny(PermsFunction):
1655 1660 def __call__(self, user_group_name=None, check_location='', user=None):
1656 1661 self.user_group_name = user_group_name
1657 1662 return super(HasUserGroupPermissionAny, self).__call__(
1658 1663 check_location, user)
1659 1664
1660 1665 def check_permissions(self, user):
1661 1666 perms = user.permissions
1662 1667 try:
1663 1668 user_perms = set([perms['user_groups'][self.user_group_name]])
1664 1669 except KeyError:
1665 1670 return False
1666 1671 if self.required_perms.intersection(user_perms):
1667 1672 return True
1668 1673 return False
1669 1674
1670 1675
1671 1676 class HasUserGroupPermissionAll(PermsFunction):
1672 1677 def __call__(self, user_group_name=None, check_location='', user=None):
1673 1678 self.user_group_name = user_group_name
1674 1679 return super(HasUserGroupPermissionAll, self).__call__(
1675 1680 check_location, user)
1676 1681
1677 1682 def check_permissions(self, user):
1678 1683 perms = user.permissions
1679 1684 try:
1680 1685 user_perms = set([perms['user_groups'][self.user_group_name]])
1681 1686 except KeyError:
1682 1687 return False
1683 1688 if self.required_perms.issubset(user_perms):
1684 1689 return True
1685 1690 return False
1686 1691
1687 1692
1688 1693 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1689 1694 class HasPermissionAnyMiddleware(object):
1690 1695 def __init__(self, *perms):
1691 1696 self.required_perms = set(perms)
1692 1697
1693 1698 def __call__(self, user, repo_name):
1694 1699 # repo_name MUST be unicode, since we handle keys in permission
1695 1700 # dict by unicode
1696 1701 repo_name = safe_unicode(repo_name)
1697 1702 user = AuthUser(user.user_id)
1698 1703 log.debug(
1699 1704 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1700 1705 self.required_perms, user, repo_name)
1701 1706
1702 1707 if self.check_permissions(user, repo_name):
1703 1708 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1704 1709 repo_name, user, 'PermissionMiddleware')
1705 1710 return True
1706 1711
1707 1712 else:
1708 1713 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1709 1714 repo_name, user, 'PermissionMiddleware')
1710 1715 return False
1711 1716
1712 1717 def check_permissions(self, user, repo_name):
1713 1718 perms = user.permissions_with_scope({'repo_name': repo_name})
1714 1719
1715 1720 try:
1716 1721 user_perms = set([perms['repositories'][repo_name]])
1717 1722 except Exception:
1718 1723 log.exception('Error while accessing user permissions')
1719 1724 return False
1720 1725
1721 1726 if self.required_perms.intersection(user_perms):
1722 1727 return True
1723 1728 return False
1724 1729
1725 1730
1726 1731 # SPECIAL VERSION TO HANDLE API AUTH
1727 1732 class _BaseApiPerm(object):
1728 1733 def __init__(self, *perms):
1729 1734 self.required_perms = set(perms)
1730 1735
1731 1736 def __call__(self, check_location=None, user=None, repo_name=None,
1732 1737 group_name=None, user_group_name=None):
1733 1738 cls_name = self.__class__.__name__
1734 1739 check_scope = 'global:%s' % (self.required_perms,)
1735 1740 if repo_name:
1736 1741 check_scope += ', repo_name:%s' % (repo_name,)
1737 1742
1738 1743 if group_name:
1739 1744 check_scope += ', repo_group_name:%s' % (group_name,)
1740 1745
1741 1746 if user_group_name:
1742 1747 check_scope += ', user_group_name:%s' % (user_group_name,)
1743 1748
1744 1749 log.debug(
1745 1750 'checking cls:%s %s %s @ %s'
1746 1751 % (cls_name, self.required_perms, check_scope, check_location))
1747 1752 if not user:
1748 1753 log.debug('Empty User passed into arguments')
1749 1754 return False
1750 1755
1751 1756 # process user
1752 1757 if not isinstance(user, AuthUser):
1753 1758 user = AuthUser(user.user_id)
1754 1759 if not check_location:
1755 1760 check_location = 'unspecified'
1756 1761 if self.check_permissions(user.permissions, repo_name, group_name,
1757 1762 user_group_name):
1758 1763 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1759 1764 check_scope, user, check_location)
1760 1765 return True
1761 1766
1762 1767 else:
1763 1768 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1764 1769 check_scope, user, check_location)
1765 1770 return False
1766 1771
1767 1772 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1768 1773 user_group_name=None):
1769 1774 """
1770 1775 implement in child class should return True if permissions are ok,
1771 1776 False otherwise
1772 1777
1773 1778 :param perm_defs: dict with permission definitions
1774 1779 :param repo_name: repo name
1775 1780 """
1776 1781 raise NotImplementedError()
1777 1782
1778 1783
1779 1784 class HasPermissionAllApi(_BaseApiPerm):
1780 1785 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1781 1786 user_group_name=None):
1782 1787 if self.required_perms.issubset(perm_defs.get('global')):
1783 1788 return True
1784 1789 return False
1785 1790
1786 1791
1787 1792 class HasPermissionAnyApi(_BaseApiPerm):
1788 1793 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1789 1794 user_group_name=None):
1790 1795 if self.required_perms.intersection(perm_defs.get('global')):
1791 1796 return True
1792 1797 return False
1793 1798
1794 1799
1795 1800 class HasRepoPermissionAllApi(_BaseApiPerm):
1796 1801 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1797 1802 user_group_name=None):
1798 1803 try:
1799 1804 _user_perms = set([perm_defs['repositories'][repo_name]])
1800 1805 except KeyError:
1801 1806 log.warning(traceback.format_exc())
1802 1807 return False
1803 1808 if self.required_perms.issubset(_user_perms):
1804 1809 return True
1805 1810 return False
1806 1811
1807 1812
1808 1813 class HasRepoPermissionAnyApi(_BaseApiPerm):
1809 1814 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1810 1815 user_group_name=None):
1811 1816 try:
1812 1817 _user_perms = set([perm_defs['repositories'][repo_name]])
1813 1818 except KeyError:
1814 1819 log.warning(traceback.format_exc())
1815 1820 return False
1816 1821 if self.required_perms.intersection(_user_perms):
1817 1822 return True
1818 1823 return False
1819 1824
1820 1825
1821 1826 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1822 1827 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1823 1828 user_group_name=None):
1824 1829 try:
1825 1830 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1826 1831 except KeyError:
1827 1832 log.warning(traceback.format_exc())
1828 1833 return False
1829 1834 if self.required_perms.intersection(_user_perms):
1830 1835 return True
1831 1836 return False
1832 1837
1833 1838
1834 1839 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1835 1840 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1836 1841 user_group_name=None):
1837 1842 try:
1838 1843 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1839 1844 except KeyError:
1840 1845 log.warning(traceback.format_exc())
1841 1846 return False
1842 1847 if self.required_perms.issubset(_user_perms):
1843 1848 return True
1844 1849 return False
1845 1850
1846 1851
1847 1852 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1848 1853 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1849 1854 user_group_name=None):
1850 1855 try:
1851 1856 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1852 1857 except KeyError:
1853 1858 log.warning(traceback.format_exc())
1854 1859 return False
1855 1860 if self.required_perms.intersection(_user_perms):
1856 1861 return True
1857 1862 return False
1858 1863
1859 1864
1860 1865 def check_ip_access(source_ip, allowed_ips=None):
1861 1866 """
1862 1867 Checks if source_ip is a subnet of any of allowed_ips.
1863 1868
1864 1869 :param source_ip:
1865 1870 :param allowed_ips: list of allowed ips together with mask
1866 1871 """
1867 1872 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1868 1873 source_ip_address = ipaddress.ip_address(source_ip)
1869 1874 if isinstance(allowed_ips, (tuple, list, set)):
1870 1875 for ip in allowed_ips:
1871 1876 try:
1872 1877 network_address = ipaddress.ip_network(ip, strict=False)
1873 1878 if source_ip_address in network_address:
1874 1879 log.debug('IP %s is network %s' %
1875 1880 (source_ip_address, network_address))
1876 1881 return True
1877 1882 # for any case we cannot determine the IP, don't crash just
1878 1883 # skip it and log as error, we want to say forbidden still when
1879 1884 # sending bad IP
1880 1885 except Exception:
1881 1886 log.error(traceback.format_exc())
1882 1887 continue
1883 1888 return False
1884 1889
1885 1890
1886 1891 def get_cython_compat_decorator(wrapper, func):
1887 1892 """
1888 1893 Creates a cython compatible decorator. The previously used
1889 1894 decorator.decorator() function seems to be incompatible with cython.
1890 1895
1891 1896 :param wrapper: __wrapper method of the decorator class
1892 1897 :param func: decorated function
1893 1898 """
1894 1899 @wraps(func)
1895 1900 def local_wrapper(*args, **kwds):
1896 1901 return wrapper(func, *args, **kwds)
1897 1902 local_wrapper.__wrapped__ = func
1898 1903 return local_wrapper
@@ -1,563 +1,549 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pylons.i18n.translation import _
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 return self.load(template_name)(**kw)
70 70
71 71
72 72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 73 deform.Form.set_default_renderer(form_renderer)
74 74
75 75
76 76 def LoginForm():
77 77 class _LoginForm(formencode.Schema):
78 78 allow_extra_fields = True
79 79 filter_extra_fields = True
80 80 username = v.UnicodeString(
81 81 strip=True,
82 82 min=1,
83 83 not_empty=True,
84 84 messages={
85 85 'empty': _(u'Please enter a login'),
86 86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 87 }
88 88 )
89 89
90 90 password = v.UnicodeString(
91 91 strip=False,
92 92 min=3,
93 93 not_empty=True,
94 94 messages={
95 95 'empty': _(u'Please enter a password'),
96 96 'tooShort': _(u'Enter %(min)i characters or more')}
97 97 )
98 98
99 99 remember = v.StringBoolean(if_missing=False)
100 100
101 101 chained_validators = [v.ValidAuth()]
102 102 return _LoginForm
103 103
104 104
105 def PasswordChangeForm(username):
106 class _PasswordChangeForm(formencode.Schema):
107 allow_extra_fields = True
108 filter_extra_fields = True
109
110 current_password = v.ValidOldPassword(username)(not_empty=True)
111 new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
112 new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
113
114 chained_validators = [v.ValidPasswordsMatch('new_password',
115 'new_password_confirmation')]
116 return _PasswordChangeForm
117
118
119 105 def UserForm(edit=False, available_languages=[], old_data={}):
120 106 class _UserForm(formencode.Schema):
121 107 allow_extra_fields = True
122 108 filter_extra_fields = True
123 109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
124 110 v.ValidUsername(edit, old_data))
125 111 if edit:
126 112 new_password = All(
127 113 v.ValidPassword(),
128 114 v.UnicodeString(strip=False, min=6, not_empty=False)
129 115 )
130 116 password_confirmation = All(
131 117 v.ValidPassword(),
132 118 v.UnicodeString(strip=False, min=6, not_empty=False),
133 119 )
134 120 admin = v.StringBoolean(if_missing=False)
135 121 else:
136 122 password = All(
137 123 v.ValidPassword(),
138 124 v.UnicodeString(strip=False, min=6, not_empty=True)
139 125 )
140 126 password_confirmation = All(
141 127 v.ValidPassword(),
142 128 v.UnicodeString(strip=False, min=6, not_empty=False)
143 129 )
144 130
145 131 password_change = v.StringBoolean(if_missing=False)
146 132 create_repo_group = v.StringBoolean(if_missing=False)
147 133
148 134 active = v.StringBoolean(if_missing=False)
149 135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
150 136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
151 137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
152 138 extern_name = v.UnicodeString(strip=True)
153 139 extern_type = v.UnicodeString(strip=True)
154 140 language = v.OneOf(available_languages, hideList=False,
155 141 testValueList=True, if_missing=None)
156 142 chained_validators = [v.ValidPasswordsMatch()]
157 143 return _UserForm
158 144
159 145
160 146 def UserGroupForm(edit=False, old_data=None, available_members=None,
161 147 allow_disabled=False):
162 148 old_data = old_data or {}
163 149 available_members = available_members or []
164 150
165 151 class _UserGroupForm(formencode.Schema):
166 152 allow_extra_fields = True
167 153 filter_extra_fields = True
168 154
169 155 users_group_name = All(
170 156 v.UnicodeString(strip=True, min=1, not_empty=True),
171 157 v.ValidUserGroup(edit, old_data)
172 158 )
173 159 user_group_description = v.UnicodeString(strip=True, min=1,
174 160 not_empty=False)
175 161
176 162 users_group_active = v.StringBoolean(if_missing=False)
177 163
178 164 if edit:
179 165 users_group_members = v.OneOf(
180 166 available_members, hideList=False, testValueList=True,
181 167 if_missing=None, not_empty=False
182 168 )
183 169 # this is user group owner
184 170 user = All(
185 171 v.UnicodeString(not_empty=True),
186 172 v.ValidRepoUser(allow_disabled))
187 173 return _UserGroupForm
188 174
189 175
190 176 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
191 177 can_create_in_root=False, allow_disabled=False):
192 178 old_data = old_data or {}
193 179 available_groups = available_groups or []
194 180
195 181 class _RepoGroupForm(formencode.Schema):
196 182 allow_extra_fields = True
197 183 filter_extra_fields = False
198 184
199 185 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
200 186 v.SlugifyName(),)
201 187 group_description = v.UnicodeString(strip=True, min=1,
202 188 not_empty=False)
203 189 group_copy_permissions = v.StringBoolean(if_missing=False)
204 190
205 191 group_parent_id = v.OneOf(available_groups, hideList=False,
206 192 testValueList=True, not_empty=True)
207 193 enable_locking = v.StringBoolean(if_missing=False)
208 194 chained_validators = [
209 195 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
210 196
211 197 if edit:
212 198 # this is repo group owner
213 199 user = All(
214 200 v.UnicodeString(not_empty=True),
215 201 v.ValidRepoUser(allow_disabled))
216 202
217 203 return _RepoGroupForm
218 204
219 205
220 206 def RegisterForm(edit=False, old_data={}):
221 207 class _RegisterForm(formencode.Schema):
222 208 allow_extra_fields = True
223 209 filter_extra_fields = True
224 210 username = All(
225 211 v.ValidUsername(edit, old_data),
226 212 v.UnicodeString(strip=True, min=1, not_empty=True)
227 213 )
228 214 password = All(
229 215 v.ValidPassword(),
230 216 v.UnicodeString(strip=False, min=6, not_empty=True)
231 217 )
232 218 password_confirmation = All(
233 219 v.ValidPassword(),
234 220 v.UnicodeString(strip=False, min=6, not_empty=True)
235 221 )
236 222 active = v.StringBoolean(if_missing=False)
237 223 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
238 224 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
239 225 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
240 226
241 227 chained_validators = [v.ValidPasswordsMatch()]
242 228
243 229 return _RegisterForm
244 230
245 231
246 232 def PasswordResetForm():
247 233 class _PasswordResetForm(formencode.Schema):
248 234 allow_extra_fields = True
249 235 filter_extra_fields = True
250 236 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
251 237 return _PasswordResetForm
252 238
253 239
254 240 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
255 241 allow_disabled=False):
256 242 old_data = old_data or {}
257 243 repo_groups = repo_groups or []
258 244 landing_revs = landing_revs or []
259 245 supported_backends = BACKENDS.keys()
260 246
261 247 class _RepoForm(formencode.Schema):
262 248 allow_extra_fields = True
263 249 filter_extra_fields = False
264 250 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
265 251 v.SlugifyName())
266 252 repo_group = All(v.CanWriteGroup(old_data),
267 253 v.OneOf(repo_groups, hideList=True))
268 254 repo_type = v.OneOf(supported_backends, required=False,
269 255 if_missing=old_data.get('repo_type'))
270 256 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
271 257 repo_private = v.StringBoolean(if_missing=False)
272 258 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
273 259 repo_copy_permissions = v.StringBoolean(if_missing=False)
274 260 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
275 261
276 262 repo_enable_statistics = v.StringBoolean(if_missing=False)
277 263 repo_enable_downloads = v.StringBoolean(if_missing=False)
278 264 repo_enable_locking = v.StringBoolean(if_missing=False)
279 265
280 266 if edit:
281 267 # this is repo owner
282 268 user = All(
283 269 v.UnicodeString(not_empty=True),
284 270 v.ValidRepoUser(allow_disabled))
285 271 clone_uri_change = v.UnicodeString(
286 272 not_empty=False, if_missing=v.Missing)
287 273
288 274 chained_validators = [v.ValidCloneUri(),
289 275 v.ValidRepoName(edit, old_data)]
290 276 return _RepoForm
291 277
292 278
293 279 def RepoPermsForm():
294 280 class _RepoPermsForm(formencode.Schema):
295 281 allow_extra_fields = True
296 282 filter_extra_fields = False
297 283 chained_validators = [v.ValidPerms(type_='repo')]
298 284 return _RepoPermsForm
299 285
300 286
301 287 def RepoGroupPermsForm(valid_recursive_choices):
302 288 class _RepoGroupPermsForm(formencode.Schema):
303 289 allow_extra_fields = True
304 290 filter_extra_fields = False
305 291 recursive = v.OneOf(valid_recursive_choices)
306 292 chained_validators = [v.ValidPerms(type_='repo_group')]
307 293 return _RepoGroupPermsForm
308 294
309 295
310 296 def UserGroupPermsForm():
311 297 class _UserPermsForm(formencode.Schema):
312 298 allow_extra_fields = True
313 299 filter_extra_fields = False
314 300 chained_validators = [v.ValidPerms(type_='user_group')]
315 301 return _UserPermsForm
316 302
317 303
318 304 def RepoFieldForm():
319 305 class _RepoFieldForm(formencode.Schema):
320 306 filter_extra_fields = True
321 307 allow_extra_fields = True
322 308
323 309 new_field_key = All(v.FieldKey(),
324 310 v.UnicodeString(strip=True, min=3, not_empty=True))
325 311 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
326 312 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
327 313 if_missing='str')
328 314 new_field_label = v.UnicodeString(not_empty=False)
329 315 new_field_desc = v.UnicodeString(not_empty=False)
330 316
331 317 return _RepoFieldForm
332 318
333 319
334 320 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
335 321 repo_groups=[], landing_revs=[]):
336 322 class _RepoForkForm(formencode.Schema):
337 323 allow_extra_fields = True
338 324 filter_extra_fields = False
339 325 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
340 326 v.SlugifyName())
341 327 repo_group = All(v.CanWriteGroup(),
342 328 v.OneOf(repo_groups, hideList=True))
343 329 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
344 330 description = v.UnicodeString(strip=True, min=1, not_empty=True)
345 331 private = v.StringBoolean(if_missing=False)
346 332 copy_permissions = v.StringBoolean(if_missing=False)
347 333 fork_parent_id = v.UnicodeString()
348 334 chained_validators = [v.ValidForkName(edit, old_data)]
349 335 landing_rev = v.OneOf(landing_revs, hideList=True)
350 336
351 337 return _RepoForkForm
352 338
353 339
354 340 def ApplicationSettingsForm():
355 341 class _ApplicationSettingsForm(formencode.Schema):
356 342 allow_extra_fields = True
357 343 filter_extra_fields = False
358 344 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
359 345 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
360 346 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
361 347 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
362 348 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
363 349 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
364 350
365 351 return _ApplicationSettingsForm
366 352
367 353
368 354 def ApplicationVisualisationForm():
369 355 class _ApplicationVisualisationForm(formencode.Schema):
370 356 allow_extra_fields = True
371 357 filter_extra_fields = False
372 358 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
373 359 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
374 360 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
375 361
376 362 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
377 363 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
378 364 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
379 365 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
380 366 rhodecode_show_version = v.StringBoolean(if_missing=False)
381 367 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
382 368 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
383 369 rhodecode_gravatar_url = v.UnicodeString(min=3)
384 370 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
385 371 rhodecode_support_url = v.UnicodeString()
386 372 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
387 373 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
388 374
389 375 return _ApplicationVisualisationForm
390 376
391 377
392 378 class _BaseVcsSettingsForm(formencode.Schema):
393 379 allow_extra_fields = True
394 380 filter_extra_fields = False
395 381 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
396 382 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
397 383 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
398 384
399 385 extensions_largefiles = v.StringBoolean(if_missing=False)
400 386 phases_publish = v.StringBoolean(if_missing=False)
401 387
402 388 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
403 389 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
404 390 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
405 391
406 392
407 393 def ApplicationUiSettingsForm():
408 394 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
409 395 web_push_ssl = v.StringBoolean(if_missing=False)
410 396 paths_root_path = All(
411 397 v.ValidPath(),
412 398 v.UnicodeString(strip=True, min=1, not_empty=True)
413 399 )
414 400 extensions_hgsubversion = v.StringBoolean(if_missing=False)
415 401 extensions_hggit = v.StringBoolean(if_missing=False)
416 402 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
417 403 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
418 404
419 405 return _ApplicationUiSettingsForm
420 406
421 407
422 408 def RepoVcsSettingsForm(repo_name):
423 409 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
424 410 inherit_global_settings = v.StringBoolean(if_missing=False)
425 411 new_svn_branch = v.ValidSvnPattern(
426 412 section='vcs_svn_branch', repo_name=repo_name)
427 413 new_svn_tag = v.ValidSvnPattern(
428 414 section='vcs_svn_tag', repo_name=repo_name)
429 415
430 416 return _RepoVcsSettingsForm
431 417
432 418
433 419 def LabsSettingsForm():
434 420 class _LabSettingsForm(formencode.Schema):
435 421 allow_extra_fields = True
436 422 filter_extra_fields = False
437 423
438 424 rhodecode_proxy_subversion_http_requests = v.StringBoolean(
439 425 if_missing=False)
440 426 rhodecode_subversion_http_server_url = v.UnicodeString(
441 427 strip=True, if_missing=None)
442 428
443 429 return _LabSettingsForm
444 430
445 431
446 432 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
447 433 class _DefaultPermissionsForm(formencode.Schema):
448 434 allow_extra_fields = True
449 435 filter_extra_fields = True
450 436
451 437 anonymous = v.StringBoolean(if_missing=False)
452 438 default_register = v.OneOf(register_choices)
453 439 default_register_message = v.UnicodeString()
454 440 default_extern_activate = v.OneOf(extern_activate_choices)
455 441
456 442 return _DefaultPermissionsForm
457 443
458 444
459 445 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
460 446 user_group_perms_choices):
461 447 class _ObjectPermissionsForm(formencode.Schema):
462 448 allow_extra_fields = True
463 449 filter_extra_fields = True
464 450 overwrite_default_repo = v.StringBoolean(if_missing=False)
465 451 overwrite_default_group = v.StringBoolean(if_missing=False)
466 452 overwrite_default_user_group = v.StringBoolean(if_missing=False)
467 453 default_repo_perm = v.OneOf(repo_perms_choices)
468 454 default_group_perm = v.OneOf(group_perms_choices)
469 455 default_user_group_perm = v.OneOf(user_group_perms_choices)
470 456
471 457 return _ObjectPermissionsForm
472 458
473 459
474 460 def UserPermissionsForm(create_choices, create_on_write_choices,
475 461 repo_group_create_choices, user_group_create_choices,
476 462 fork_choices, inherit_default_permissions_choices):
477 463 class _DefaultPermissionsForm(formencode.Schema):
478 464 allow_extra_fields = True
479 465 filter_extra_fields = True
480 466
481 467 anonymous = v.StringBoolean(if_missing=False)
482 468
483 469 default_repo_create = v.OneOf(create_choices)
484 470 default_repo_create_on_write = v.OneOf(create_on_write_choices)
485 471 default_user_group_create = v.OneOf(user_group_create_choices)
486 472 default_repo_group_create = v.OneOf(repo_group_create_choices)
487 473 default_fork_create = v.OneOf(fork_choices)
488 474 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
489 475
490 476 return _DefaultPermissionsForm
491 477
492 478
493 479 def UserIndividualPermissionsForm():
494 480 class _DefaultPermissionsForm(formencode.Schema):
495 481 allow_extra_fields = True
496 482 filter_extra_fields = True
497 483
498 484 inherit_default_permissions = v.StringBoolean(if_missing=False)
499 485
500 486 return _DefaultPermissionsForm
501 487
502 488
503 489 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
504 490 class _DefaultsForm(formencode.Schema):
505 491 allow_extra_fields = True
506 492 filter_extra_fields = True
507 493 default_repo_type = v.OneOf(supported_backends)
508 494 default_repo_private = v.StringBoolean(if_missing=False)
509 495 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
510 496 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
511 497 default_repo_enable_locking = v.StringBoolean(if_missing=False)
512 498
513 499 return _DefaultsForm
514 500
515 501
516 502 def AuthSettingsForm():
517 503 class _AuthSettingsForm(formencode.Schema):
518 504 allow_extra_fields = True
519 505 filter_extra_fields = True
520 506 auth_plugins = All(v.ValidAuthPlugins(),
521 507 v.UniqueListFromString()(not_empty=True))
522 508
523 509 return _AuthSettingsForm
524 510
525 511
526 512 def UserExtraEmailForm():
527 513 class _UserExtraEmailForm(formencode.Schema):
528 514 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
529 515 return _UserExtraEmailForm
530 516
531 517
532 518 def UserExtraIpForm():
533 519 class _UserExtraIpForm(formencode.Schema):
534 520 ip = v.ValidIp()(not_empty=True)
535 521 return _UserExtraIpForm
536 522
537 523
538 524 def PullRequestForm(repo_id):
539 525 class _PullRequestForm(formencode.Schema):
540 526 allow_extra_fields = True
541 527 filter_extra_fields = True
542 528
543 529 user = v.UnicodeString(strip=True, required=True)
544 530 source_repo = v.UnicodeString(strip=True, required=True)
545 531 source_ref = v.UnicodeString(strip=True, required=True)
546 532 target_repo = v.UnicodeString(strip=True, required=True)
547 533 target_ref = v.UnicodeString(strip=True, required=True)
548 534 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 535 v.UniqueList()(not_empty=True))
550 536 review_members = v.UniqueList(convert=int)(not_empty=True)
551 537
552 538 pullrequest_title = v.UnicodeString(strip=True, required=True)
553 539 pullrequest_desc = v.UnicodeString(strip=True, required=False)
554 540
555 541 return _PullRequestForm
556 542
557 543
558 544 def IssueTrackerPatternsForm():
559 545 class _IssueTrackerPatternsForm(formencode.Schema):
560 546 allow_extra_fields = True
561 547 filter_extra_fields = False
562 548 chained_validators = [v.ValidPattern()]
563 549 return _IssueTrackerPatternsForm
@@ -1,839 +1,838 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode import events
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict)
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.db import (
43 43 User, UserToPerm, UserEmailMap, UserIpMap)
44 44 from rhodecode.lib.exceptions import (
45 45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(FromCache("sql_cache_short",
61 61 "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 80 return User.get_by_email(email, case_insensitive, cache)
81 81
82 82 def get_by_auth_token(self, auth_token, cache=False):
83 83 return User.get_by_auth_token(auth_token, cache)
84 84
85 85 def get_active_user_count(self, cache=False):
86 86 return User.query().filter(
87 87 User.active == True).filter(
88 88 User.username != User.DEFAULT_USER).count()
89 89
90 90 def create(self, form_data, cur_user=None):
91 91 if not cur_user:
92 92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 93
94 94 user_data = {
95 95 'username': form_data['username'],
96 96 'password': form_data['password'],
97 97 'email': form_data['email'],
98 98 'firstname': form_data['firstname'],
99 99 'lastname': form_data['lastname'],
100 100 'active': form_data['active'],
101 101 'extern_type': form_data['extern_type'],
102 102 'extern_name': form_data['extern_name'],
103 103 'admin': False,
104 104 'cur_user': cur_user
105 105 }
106 106
107 107 try:
108 108 if form_data.get('create_repo_group'):
109 109 user_data['create_repo_group'] = True
110 110 if form_data.get('password_change'):
111 111 user_data['force_password_change'] = True
112 112
113 113 return UserModel().create_or_update(**user_data)
114 114 except Exception:
115 115 log.error(traceback.format_exc())
116 116 raise
117 117
118 118 def update_user(self, user, skip_attrs=None, **kwargs):
119 119 from rhodecode.lib.auth import get_crypt_password
120 120
121 121 user = self._get_user(user)
122 122 if user.username == User.DEFAULT_USER:
123 123 raise DefaultUserException(
124 124 _("You can't Edit this user since it's"
125 125 " crucial for entire application"))
126 126
127 127 # first store only defaults
128 128 user_attrs = {
129 129 'updating_user_id': user.user_id,
130 130 'username': user.username,
131 131 'password': user.password,
132 132 'email': user.email,
133 133 'firstname': user.name,
134 134 'lastname': user.lastname,
135 135 'active': user.active,
136 136 'admin': user.admin,
137 137 'extern_name': user.extern_name,
138 138 'extern_type': user.extern_type,
139 139 'language': user.user_data.get('language')
140 140 }
141 141
142 142 # in case there's new_password, that comes from form, use it to
143 143 # store password
144 144 if kwargs.get('new_password'):
145 145 kwargs['password'] = kwargs['new_password']
146 146
147 147 # cleanups, my_account password change form
148 148 kwargs.pop('current_password', None)
149 149 kwargs.pop('new_password', None)
150 kwargs.pop('new_password_confirmation', None)
151 150
152 151 # cleanups, user edit password change form
153 152 kwargs.pop('password_confirmation', None)
154 153 kwargs.pop('password_change', None)
155 154
156 155 # create repo group on user creation
157 156 kwargs.pop('create_repo_group', None)
158 157
159 158 # legacy forms send name, which is the firstname
160 159 firstname = kwargs.pop('name', None)
161 160 if firstname:
162 161 kwargs['firstname'] = firstname
163 162
164 163 for k, v in kwargs.items():
165 164 # skip if we don't want to update this
166 165 if skip_attrs and k in skip_attrs:
167 166 continue
168 167
169 168 user_attrs[k] = v
170 169
171 170 try:
172 171 return self.create_or_update(**user_attrs)
173 172 except Exception:
174 173 log.error(traceback.format_exc())
175 174 raise
176 175
177 176 def create_or_update(
178 177 self, username, password, email, firstname='', lastname='',
179 178 active=True, admin=False, extern_type=None, extern_name=None,
180 179 cur_user=None, plugin=None, force_password_change=False,
181 180 allow_to_create_user=True, create_repo_group=False,
182 181 updating_user_id=None, language=None, strict_creation_check=True):
183 182 """
184 183 Creates a new instance if not found, or updates current one
185 184
186 185 :param username:
187 186 :param password:
188 187 :param email:
189 188 :param firstname:
190 189 :param lastname:
191 190 :param active:
192 191 :param admin:
193 192 :param extern_type:
194 193 :param extern_name:
195 194 :param cur_user:
196 195 :param plugin: optional plugin this method was called from
197 196 :param force_password_change: toggles new or existing user flag
198 197 for password change
199 198 :param allow_to_create_user: Defines if the method can actually create
200 199 new users
201 200 :param create_repo_group: Defines if the method should also
202 201 create an repo group with user name, and owner
203 202 :param updating_user_id: if we set it up this is the user we want to
204 203 update this allows to editing username.
205 204 :param language: language of user from interface.
206 205
207 206 :returns: new User object with injected `is_new_user` attribute.
208 207 """
209 208 if not cur_user:
210 209 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211 210
212 211 from rhodecode.lib.auth import (
213 212 get_crypt_password, check_password, generate_auth_token)
214 213 from rhodecode.lib.hooks_base import (
215 214 log_create_user, check_allowed_create_user)
216 215
217 216 def _password_change(new_user, password):
218 217 # empty password
219 218 if not new_user.password:
220 219 return False
221 220
222 221 # password check is only needed for RhodeCode internal auth calls
223 222 # in case it's a plugin we don't care
224 223 if not plugin:
225 224
226 225 # first check if we gave crypted password back, and if it matches
227 226 # it's not password change
228 227 if new_user.password == password:
229 228 return False
230 229
231 230 password_match = check_password(password, new_user.password)
232 231 if not password_match:
233 232 return True
234 233
235 234 return False
236 235
237 236 user_data = {
238 237 'username': username,
239 238 'password': password,
240 239 'email': email,
241 240 'firstname': firstname,
242 241 'lastname': lastname,
243 242 'active': active,
244 243 'admin': admin
245 244 }
246 245
247 246 if updating_user_id:
248 247 log.debug('Checking for existing account in RhodeCode '
249 248 'database with user_id `%s` ' % (updating_user_id,))
250 249 user = User.get(updating_user_id)
251 250 else:
252 251 log.debug('Checking for existing account in RhodeCode '
253 252 'database with username `%s` ' % (username,))
254 253 user = User.get_by_username(username, case_insensitive=True)
255 254
256 255 if user is None:
257 256 # we check internal flag if this method is actually allowed to
258 257 # create new user
259 258 if not allow_to_create_user:
260 259 msg = ('Method wants to create new user, but it is not '
261 260 'allowed to do so')
262 261 log.warning(msg)
263 262 raise NotAllowedToCreateUserError(msg)
264 263
265 264 log.debug('Creating new user %s', username)
266 265
267 266 # only if we create user that is active
268 267 new_active_user = active
269 268 if new_active_user and strict_creation_check:
270 269 # raises UserCreationError if it's not allowed for any reason to
271 270 # create new active user, this also executes pre-create hooks
272 271 check_allowed_create_user(user_data, cur_user, strict_check=True)
273 272 events.trigger(events.UserPreCreate(user_data))
274 273 new_user = User()
275 274 edit = False
276 275 else:
277 276 log.debug('updating user %s', username)
278 277 events.trigger(events.UserPreUpdate(user, user_data))
279 278 new_user = user
280 279 edit = True
281 280
282 281 # we're not allowed to edit default user
283 282 if user.username == User.DEFAULT_USER:
284 283 raise DefaultUserException(
285 284 _("You can't edit this user (`%(username)s`) since it's "
286 285 "crucial for entire application") % {'username': user.username})
287 286
288 287 # inject special attribute that will tell us if User is new or old
289 288 new_user.is_new_user = not edit
290 289 # for users that didn's specify auth type, we use RhodeCode built in
291 290 from rhodecode.authentication.plugins import auth_rhodecode
292 291 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
293 292 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
294 293
295 294 try:
296 295 new_user.username = username
297 296 new_user.admin = admin
298 297 new_user.email = email
299 298 new_user.active = active
300 299 new_user.extern_name = safe_unicode(extern_name)
301 300 new_user.extern_type = safe_unicode(extern_type)
302 301 new_user.name = firstname
303 302 new_user.lastname = lastname
304 303
305 304 if not edit:
306 305 new_user.api_key = generate_auth_token(username)
307 306
308 307 # set password only if creating an user or password is changed
309 308 if not edit or _password_change(new_user, password):
310 309 reason = 'new password' if edit else 'new user'
311 310 log.debug('Updating password reason=>%s', reason)
312 311 new_user.password = get_crypt_password(password) if password else None
313 312
314 313 if force_password_change:
315 314 new_user.update_userdata(force_password_change=True)
316 315 if language:
317 316 new_user.update_userdata(language=language)
318 317
319 318 self.sa.add(new_user)
320 319
321 320 if not edit and create_repo_group:
322 321 # create new group same as username, and make this user an owner
323 322 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {'username': username}
324 323 RepoGroupModel().create(group_name=username,
325 324 group_description=desc,
326 325 owner=username, commit_early=False)
327 326 if not edit:
328 327 # add the RSS token
329 328 AuthTokenModel().create(username,
330 329 description='Generated feed token',
331 330 role=AuthTokenModel.cls.ROLE_FEED)
332 331 log_create_user(created_by=cur_user, **new_user.get_dict())
333 332 return new_user
334 333 except (DatabaseError,):
335 334 log.error(traceback.format_exc())
336 335 raise
337 336
338 337 def create_registration(self, form_data):
339 338 from rhodecode.model.notification import NotificationModel
340 339 from rhodecode.model.notification import EmailNotificationModel
341 340
342 341 try:
343 342 form_data['admin'] = False
344 343 form_data['extern_name'] = 'rhodecode'
345 344 form_data['extern_type'] = 'rhodecode'
346 345 new_user = self.create(form_data)
347 346
348 347 self.sa.add(new_user)
349 348 self.sa.flush()
350 349
351 350 user_data = new_user.get_dict()
352 351 kwargs = {
353 352 # use SQLALCHEMY safe dump of user data
354 353 'user': AttributeDict(user_data),
355 354 'date': datetime.datetime.now()
356 355 }
357 356 notification_type = EmailNotificationModel.TYPE_REGISTRATION
358 357 # pre-generate the subject for notification itself
359 358 (subject,
360 359 _h, _e, # we don't care about those
361 360 body_plaintext) = EmailNotificationModel().render_email(
362 361 notification_type, **kwargs)
363 362
364 363 # create notification objects, and emails
365 364 NotificationModel().create(
366 365 created_by=new_user,
367 366 notification_subject=subject,
368 367 notification_body=body_plaintext,
369 368 notification_type=notification_type,
370 369 recipients=None, # all admins
371 370 email_kwargs=kwargs,
372 371 )
373 372
374 373 return new_user
375 374 except Exception:
376 375 log.error(traceback.format_exc())
377 376 raise
378 377
379 378 def _handle_user_repos(self, username, repositories, handle_mode=None):
380 379 _superadmin = self.cls.get_first_super_admin()
381 380 left_overs = True
382 381
383 382 from rhodecode.model.repo import RepoModel
384 383
385 384 if handle_mode == 'detach':
386 385 for obj in repositories:
387 386 obj.user = _superadmin
388 387 # set description we know why we super admin now owns
389 388 # additional repositories that were orphaned !
390 389 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
391 390 self.sa.add(obj)
392 391 left_overs = False
393 392 elif handle_mode == 'delete':
394 393 for obj in repositories:
395 394 RepoModel().delete(obj, forks='detach')
396 395 left_overs = False
397 396
398 397 # if nothing is done we have left overs left
399 398 return left_overs
400 399
401 400 def _handle_user_repo_groups(self, username, repository_groups,
402 401 handle_mode=None):
403 402 _superadmin = self.cls.get_first_super_admin()
404 403 left_overs = True
405 404
406 405 from rhodecode.model.repo_group import RepoGroupModel
407 406
408 407 if handle_mode == 'detach':
409 408 for r in repository_groups:
410 409 r.user = _superadmin
411 410 # set description we know why we super admin now owns
412 411 # additional repositories that were orphaned !
413 412 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
414 413 self.sa.add(r)
415 414 left_overs = False
416 415 elif handle_mode == 'delete':
417 416 for r in repository_groups:
418 417 RepoGroupModel().delete(r)
419 418 left_overs = False
420 419
421 420 # if nothing is done we have left overs left
422 421 return left_overs
423 422
424 423 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
425 424 _superadmin = self.cls.get_first_super_admin()
426 425 left_overs = True
427 426
428 427 from rhodecode.model.user_group import UserGroupModel
429 428
430 429 if handle_mode == 'detach':
431 430 for r in user_groups:
432 431 for user_user_group_to_perm in r.user_user_group_to_perm:
433 432 if user_user_group_to_perm.user.username == username:
434 433 user_user_group_to_perm.user = _superadmin
435 434 r.user = _superadmin
436 435 # set description we know why we super admin now owns
437 436 # additional repositories that were orphaned !
438 437 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
439 438 self.sa.add(r)
440 439 left_overs = False
441 440 elif handle_mode == 'delete':
442 441 for r in user_groups:
443 442 UserGroupModel().delete(r)
444 443 left_overs = False
445 444
446 445 # if nothing is done we have left overs left
447 446 return left_overs
448 447
449 448 def delete(self, user, cur_user=None, handle_repos=None,
450 449 handle_repo_groups=None, handle_user_groups=None):
451 450 if not cur_user:
452 451 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
453 452 user = self._get_user(user)
454 453
455 454 try:
456 455 if user.username == User.DEFAULT_USER:
457 456 raise DefaultUserException(
458 457 _(u"You can't remove this user since it's"
459 458 u" crucial for entire application"))
460 459
461 460 left_overs = self._handle_user_repos(
462 461 user.username, user.repositories, handle_repos)
463 462 if left_overs and user.repositories:
464 463 repos = [x.repo_name for x in user.repositories]
465 464 raise UserOwnsReposException(
466 465 _(u'user "%s" still owns %s repositories and cannot be '
467 466 u'removed. Switch owners or remove those repositories:%s')
468 467 % (user.username, len(repos), ', '.join(repos)))
469 468
470 469 left_overs = self._handle_user_repo_groups(
471 470 user.username, user.repository_groups, handle_repo_groups)
472 471 if left_overs and user.repository_groups:
473 472 repo_groups = [x.group_name for x in user.repository_groups]
474 473 raise UserOwnsRepoGroupsException(
475 474 _(u'user "%s" still owns %s repository groups and cannot be '
476 475 u'removed. Switch owners or remove those repository groups:%s')
477 476 % (user.username, len(repo_groups), ', '.join(repo_groups)))
478 477
479 478 left_overs = self._handle_user_user_groups(
480 479 user.username, user.user_groups, handle_user_groups)
481 480 if left_overs and user.user_groups:
482 481 user_groups = [x.users_group_name for x in user.user_groups]
483 482 raise UserOwnsUserGroupsException(
484 483 _(u'user "%s" still owns %s user groups and cannot be '
485 484 u'removed. Switch owners or remove those user groups:%s')
486 485 % (user.username, len(user_groups), ', '.join(user_groups)))
487 486
488 487 # we might change the user data with detach/delete, make sure
489 488 # the object is marked as expired before actually deleting !
490 489 self.sa.expire(user)
491 490 self.sa.delete(user)
492 491 from rhodecode.lib.hooks_base import log_delete_user
493 492 log_delete_user(deleted_by=cur_user, **user.get_dict())
494 493 except Exception:
495 494 log.error(traceback.format_exc())
496 495 raise
497 496
498 497 def reset_password_link(self, data, pwd_reset_url):
499 498 from rhodecode.lib.celerylib import tasks, run_task
500 499 from rhodecode.model.notification import EmailNotificationModel
501 500 user_email = data['email']
502 501 try:
503 502 user = User.get_by_email(user_email)
504 503 if user:
505 504 log.debug('password reset user found %s', user)
506 505
507 506 email_kwargs = {
508 507 'password_reset_url': pwd_reset_url,
509 508 'user': user,
510 509 'email': user_email,
511 510 'date': datetime.datetime.now()
512 511 }
513 512
514 513 (subject, headers, email_body,
515 514 email_body_plaintext) = EmailNotificationModel().render_email(
516 515 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
517 516
518 517 recipients = [user_email]
519 518
520 519 action_logger_generic(
521 520 'sending password reset email to user: {}'.format(
522 521 user), namespace='security.password_reset')
523 522
524 523 run_task(tasks.send_email, recipients, subject,
525 524 email_body_plaintext, email_body)
526 525
527 526 else:
528 527 log.debug("password reset email %s not found", user_email)
529 528 except Exception:
530 529 log.error(traceback.format_exc())
531 530 return False
532 531
533 532 return True
534 533
535 534 def reset_password(self, data, pwd_reset_url):
536 535 from rhodecode.lib.celerylib import tasks, run_task
537 536 from rhodecode.model.notification import EmailNotificationModel
538 537 from rhodecode.lib import auth
539 538 user_email = data['email']
540 539 pre_db = True
541 540 try:
542 541 user = User.get_by_email(user_email)
543 542 new_passwd = auth.PasswordGenerator().gen_password(
544 543 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
545 544 if user:
546 545 user.password = auth.get_crypt_password(new_passwd)
547 546 # also force this user to reset his password !
548 547 user.update_userdata(force_password_change=True)
549 548
550 549 Session().add(user)
551 550 Session().commit()
552 551 log.info('change password for %s', user_email)
553 552 if new_passwd is None:
554 553 raise Exception('unable to generate new password')
555 554
556 555 pre_db = False
557 556
558 557 email_kwargs = {
559 558 'new_password': new_passwd,
560 559 'password_reset_url': pwd_reset_url,
561 560 'user': user,
562 561 'email': user_email,
563 562 'date': datetime.datetime.now()
564 563 }
565 564
566 565 (subject, headers, email_body,
567 566 email_body_plaintext) = EmailNotificationModel().render_email(
568 567 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, **email_kwargs)
569 568
570 569 recipients = [user_email]
571 570
572 571 action_logger_generic(
573 572 'sent new password to user: {} with email: {}'.format(
574 573 user, user_email), namespace='security.password_reset')
575 574
576 575 run_task(tasks.send_email, recipients, subject,
577 576 email_body_plaintext, email_body)
578 577
579 578 except Exception:
580 579 log.error('Failed to update user password')
581 580 log.error(traceback.format_exc())
582 581 if pre_db:
583 582 # we rollback only if local db stuff fails. If it goes into
584 583 # run_task, we're pass rollback state this wouldn't work then
585 584 Session().rollback()
586 585
587 586 return True
588 587
589 588 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
590 589 """
591 590 Fetches auth_user by user_id,or api_key if present.
592 591 Fills auth_user attributes with those taken from database.
593 592 Additionally set's is_authenitated if lookup fails
594 593 present in database
595 594
596 595 :param auth_user: instance of user to set attributes
597 596 :param user_id: user id to fetch by
598 597 :param api_key: api key to fetch by
599 598 :param username: username to fetch by
600 599 """
601 600 if user_id is None and api_key is None and username is None:
602 601 raise Exception('You need to pass user_id, api_key or username')
603 602
604 603 log.debug(
605 604 'doing fill data based on: user_id:%s api_key:%s username:%s',
606 605 user_id, api_key, username)
607 606 try:
608 607 dbuser = None
609 608 if user_id:
610 609 dbuser = self.get(user_id)
611 610 elif api_key:
612 611 dbuser = self.get_by_auth_token(api_key)
613 612 elif username:
614 613 dbuser = self.get_by_username(username)
615 614
616 615 if not dbuser:
617 616 log.warning(
618 617 'Unable to lookup user by id:%s api_key:%s username:%s',
619 618 user_id, api_key, username)
620 619 return False
621 620 if not dbuser.active:
622 621 log.debug('User `%s` is inactive, skipping fill data', username)
623 622 return False
624 623
625 624 log.debug('filling user:%s data', dbuser)
626 625
627 626 # TODO: johbo: Think about this and find a clean solution
628 627 user_data = dbuser.get_dict()
629 628 user_data.update(dbuser.get_api_data(include_secrets=True))
630 629
631 630 for k, v in user_data.iteritems():
632 631 # properties of auth user we dont update
633 632 if k not in ['auth_tokens', 'permissions']:
634 633 setattr(auth_user, k, v)
635 634
636 635 # few extras
637 636 setattr(auth_user, 'feed_token', dbuser.feed_token)
638 637 except Exception:
639 638 log.error(traceback.format_exc())
640 639 auth_user.is_authenticated = False
641 640 return False
642 641
643 642 return True
644 643
645 644 def has_perm(self, user, perm):
646 645 perm = self._get_perm(perm)
647 646 user = self._get_user(user)
648 647
649 648 return UserToPerm.query().filter(UserToPerm.user == user)\
650 649 .filter(UserToPerm.permission == perm).scalar() is not None
651 650
652 651 def grant_perm(self, user, perm):
653 652 """
654 653 Grant user global permissions
655 654
656 655 :param user:
657 656 :param perm:
658 657 """
659 658 user = self._get_user(user)
660 659 perm = self._get_perm(perm)
661 660 # if this permission is already granted skip it
662 661 _perm = UserToPerm.query()\
663 662 .filter(UserToPerm.user == user)\
664 663 .filter(UserToPerm.permission == perm)\
665 664 .scalar()
666 665 if _perm:
667 666 return
668 667 new = UserToPerm()
669 668 new.user = user
670 669 new.permission = perm
671 670 self.sa.add(new)
672 671 return new
673 672
674 673 def revoke_perm(self, user, perm):
675 674 """
676 675 Revoke users global permissions
677 676
678 677 :param user:
679 678 :param perm:
680 679 """
681 680 user = self._get_user(user)
682 681 perm = self._get_perm(perm)
683 682
684 683 obj = UserToPerm.query()\
685 684 .filter(UserToPerm.user == user)\
686 685 .filter(UserToPerm.permission == perm)\
687 686 .scalar()
688 687 if obj:
689 688 self.sa.delete(obj)
690 689
691 690 def add_extra_email(self, user, email):
692 691 """
693 692 Adds email address to UserEmailMap
694 693
695 694 :param user:
696 695 :param email:
697 696 """
698 697 from rhodecode.model import forms
699 698 form = forms.UserExtraEmailForm()()
700 699 data = form.to_python({'email': email})
701 700 user = self._get_user(user)
702 701
703 702 obj = UserEmailMap()
704 703 obj.user = user
705 704 obj.email = data['email']
706 705 self.sa.add(obj)
707 706 return obj
708 707
709 708 def delete_extra_email(self, user, email_id):
710 709 """
711 710 Removes email address from UserEmailMap
712 711
713 712 :param user:
714 713 :param email_id:
715 714 """
716 715 user = self._get_user(user)
717 716 obj = UserEmailMap.query().get(email_id)
718 717 if obj:
719 718 self.sa.delete(obj)
720 719
721 720 def parse_ip_range(self, ip_range):
722 721 ip_list = []
723 722 def make_unique(value):
724 723 seen = []
725 724 return [c for c in value if not (c in seen or seen.append(c))]
726 725
727 726 # firsts split by commas
728 727 for ip_range in ip_range.split(','):
729 728 if not ip_range:
730 729 continue
731 730 ip_range = ip_range.strip()
732 731 if '-' in ip_range:
733 732 start_ip, end_ip = ip_range.split('-', 1)
734 733 start_ip = ipaddress.ip_address(start_ip.strip())
735 734 end_ip = ipaddress.ip_address(end_ip.strip())
736 735 parsed_ip_range = []
737 736
738 737 for index in xrange(int(start_ip), int(end_ip) + 1):
739 738 new_ip = ipaddress.ip_address(index)
740 739 parsed_ip_range.append(str(new_ip))
741 740 ip_list.extend(parsed_ip_range)
742 741 else:
743 742 ip_list.append(ip_range)
744 743
745 744 return make_unique(ip_list)
746 745
747 746 def add_extra_ip(self, user, ip, description=None):
748 747 """
749 748 Adds ip address to UserIpMap
750 749
751 750 :param user:
752 751 :param ip:
753 752 """
754 753 from rhodecode.model import forms
755 754 form = forms.UserExtraIpForm()()
756 755 data = form.to_python({'ip': ip})
757 756 user = self._get_user(user)
758 757
759 758 obj = UserIpMap()
760 759 obj.user = user
761 760 obj.ip_addr = data['ip']
762 761 obj.description = description
763 762 self.sa.add(obj)
764 763 return obj
765 764
766 765 def delete_extra_ip(self, user, ip_id):
767 766 """
768 767 Removes ip address from UserIpMap
769 768
770 769 :param user:
771 770 :param ip_id:
772 771 """
773 772 user = self._get_user(user)
774 773 obj = UserIpMap.query().get(ip_id)
775 774 if obj:
776 775 self.sa.delete(obj)
777 776
778 777 def get_accounts_in_creation_order(self, current_user=None):
779 778 """
780 779 Get accounts in order of creation for deactivation for license limits
781 780
782 781 pick currently logged in user, and append to the list in position 0
783 782 pick all super-admins in order of creation date and add it to the list
784 783 pick all other accounts in order of creation and add it to the list.
785 784
786 785 Based on that list, the last accounts can be disabled as they are
787 786 created at the end and don't include any of the super admins as well
788 787 as the current user.
789 788
790 789 :param current_user: optionally current user running this operation
791 790 """
792 791
793 792 if not current_user:
794 793 current_user = get_current_rhodecode_user()
795 794 active_super_admins = [
796 795 x.user_id for x in User.query()
797 796 .filter(User.user_id != current_user.user_id)
798 797 .filter(User.active == true())
799 798 .filter(User.admin == true())
800 799 .order_by(User.created_on.asc())]
801 800
802 801 active_regular_users = [
803 802 x.user_id for x in User.query()
804 803 .filter(User.user_id != current_user.user_id)
805 804 .filter(User.active == true())
806 805 .filter(User.admin == false())
807 806 .order_by(User.created_on.asc())]
808 807
809 808 list_of_accounts = [current_user.user_id]
810 809 list_of_accounts += active_super_admins
811 810 list_of_accounts += active_regular_users
812 811
813 812 return list_of_accounts
814 813
815 814 def deactivate_last_users(self, expected_users):
816 815 """
817 816 Deactivate accounts that are over the license limits.
818 817 Algorithm of which accounts to disabled is based on the formula:
819 818
820 819 Get current user, then super admins in creation order, then regular
821 820 active users in creation order.
822 821
823 822 Using that list we mark all accounts from the end of it as inactive.
824 823 This way we block only latest created accounts.
825 824
826 825 :param expected_users: list of users in special order, we deactivate
827 826 the end N ammoun of users from that list
828 827 """
829 828
830 829 list_of_accounts = self.get_accounts_in_creation_order()
831 830
832 831 for acc_id in list_of_accounts[expected_users + 1:]:
833 832 user = User.get(acc_id)
834 833 log.info('Deactivating account %s for license unlock', user)
835 834 user.active = False
836 835 Session().add(user)
837 836 Session().commit()
838 837
839 838 return
@@ -1,19 +1,15 b''
1 1 import os
2 2
3 3 import ipaddress
4 4 import colander
5 5
6 6 from rhodecode.translation import _
7 7
8 8
9 9 def ip_addr_validator(node, value):
10 10 try:
11 11 # this raises an ValueError if address is not IpV4 or IpV6
12 12 ipaddress.ip_network(value, strict=False)
13 13 except ValueError:
14 14 msg = _(u'Please enter a valid IPv4 or IpV6 address')
15 15 raise colander.Invalid(node, msg)
16
17
18
19
@@ -1,91 +1,106 b''
1 1 .deform {
2 2
3 3 * {
4 4 box-sizing: border-box;
5 5 }
6 6
7 7 .required:after {
8 8 color: #e32;
9 9 content: '*';
10 10 display:inline;
11 11 }
12 12
13 13 .control-label {
14 14 width: 200px;
15 padding: 10px;
15 16 float: left;
16 17 }
17 18 .control-inputs {
18 19 width: 400px;
19 20 float: left;
20 21 }
21 22 .form-group .radio, .form-group .checkbox {
22 23 position: relative;
23 24 display: block;
24 25 /* margin-bottom: 10px; */
25 26 }
26 27
27 28 .form-group {
28 29 clear: left;
30 margin-bottom: 20px;
31
32 &:after { /* clear fix */
33 content: " ";
34 display: block;
35 clear: left;
36 }
29 37 }
30 38
31 39 .form-control {
32 40 width: 100%;
33 41 }
34 42
35 43 .error-block {
36 44 color: red;
45 margin: 0;
46 }
47
48 .help-block {
49 margin: 0;
37 50 }
38 51
39 52 .deform-seq-container .control-inputs {
40 53 width: 100%;
41 54 }
42 55
43 56 .deform-seq-container .deform-seq-item-handle {
44 57 width: 8.3%;
45 58 float: left;
46 59 }
47 60
48 61 .deform-seq-container .deform-seq-item-group {
49 62 width: 91.6%;
50 63 float: left;
51 64 }
52 65
53 66 .form-control {
54 67 input {
55 68 height: 40px;
56 69 }
57 70 input[type=checkbox], input[type=radio] {
58 71 height: auto;
59 72 }
60 73 select {
61 74 height: 40px;
62 75 }
63 76 }
64 77
65 .form-control.select2-container { height: 40px; }
78 .form-control.select2-container {
79 height: 40px;
80 }
66 81
67 82 .deform-two-field-sequence .deform-seq-container .deform-seq-item label {
68 83 display: none;
69 84 }
70 85 .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label {
71 86 display: block;
72 87 }
73 88 .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading {
74 89 display: none;
75 90 }
76 91 .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
77 background: red;
92 margin: 0;
78 93 }
79 94 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
80 95 width: 45%; padding: 0 2px; float: left; clear: none;
81 96 }
82 97 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel {
83 98 padding: 0;
84 99 margin: 5px 0;
85 100 border: none;
86 101 }
87 102 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body {
88 103 padding: 0;
89 104 }
90 105
91 106 }
@@ -1,42 +1,5 b''
1 <div class="panel panel-default">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('Change Your Account Password')}</h3>
4 </div>
5 ${h.secure_form(url('my_account_password'), method='post')}
6 <div class="panel-body">
7 <div class="fields">
8 <div class="field">
9 <div class="label">
10 <label for="current_password">${_('Current Password')}:</label>
11 </div>
12 <div class="input">
13 ${h.password('current_password',class_='medium',autocomplete="off")}
14 </div>
15 </div>
1 <%namespace name="widgets" file="/widgets.html"/>
16 2
17 <div class="field">
18 <div class="label">
19 <label for="new_password">${_('New Password')}:</label>
20 </div>
21 <div class="input">
22 ${h.password('new_password',class_='medium', autocomplete="off")}
23 </div>
24 </div>
25
26 <div class="field">
27 <div class="label">
28 <label for="password_confirmation">${_('Confirm New Password')}:</label>
29 </div>
30 <div class="input">
31 ${h.password('new_password_confirmation',class_='medium', autocomplete="off")}
32 </div>
33 </div>
34
35 <div class="buttons">
36 ${h.submit('save',_('Save'),class_="btn")}
37 ${h.reset('reset',_('Reset'),class_="btn")}
38 </div>
39 </div>
40 </div>
41 ${h.end_form()}
42 </div> No newline at end of file
3 <%widgets:panel title="${_('Change Your Account Password')}">
4 ${c.form.render() | n}
5 </%widgets:panel>
@@ -1,139 +1,138 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10
11 11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
14 14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
15 15
16 16 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
17 17 %>
18
19 18 <html xmlns="http://www.w3.org/1999/xhtml">
20 19 <head>
21 20 <title>${self.title()}</title>
22 21 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
23 22 <%def name="robots()">
24 23 <meta name="robots" content="index, nofollow"/>
25 24 </%def>
26 25 ${self.robots()}
27 26 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
28 27
29 28 ## CSS definitions
30 29 <%def name="css()">
31 30 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
32 31 <!--[if lt IE 9]>
33 32 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
34 33 <![endif]-->
35 34 ## EXTRA FOR CSS
36 35 ${self.css_extra()}
37 36 </%def>
38 37 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
39 38 <%def name="css_extra()">
40 39 </%def>
41 40
42 41 ${self.css()}
43 42
44 43 ## JAVASCRIPT
45 44 <%def name="js()">
46 45 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
47 46 <script type="text/javascript">
48 47 // register templateContext to pass template variables to JS
49 48 var templateContext = ${h.json.dumps(c.template_context)|n};
50 49
51 50 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
52 51 %if hasattr(c, 'rhodecode_db_repo'):
53 52 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
54 53 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
55 54 %else:
56 55 var REPO_LANDING_REV = '';
57 56 var REPO_TYPE = '';
58 57 %endif
59 58 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
60 59 var ASSET_URL = "${h.asset('')}";
61 60 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
62 61 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
63 62 % if getattr(c, 'rhodecode_user', None):
64 63 var USER = {name:'${c.rhodecode_user.username}'};
65 64 % else:
66 65 var USER = {name:null};
67 66 % endif
68 67
69 68 var APPENLIGHT = {
70 69 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
71 70 key: '${getattr(c, "appenlight_api_public_key", "")}',
72 71 % if getattr(c, 'appenlight_server_url', None):
73 72 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
74 73 % endif
75 74 requestInfo: {
76 75 % if getattr(c, 'rhodecode_user', None):
77 76 ip: '${c.rhodecode_user.ip_addr}',
78 77 username: '${c.rhodecode_user.username}'
79 78 % endif
80 79 }
81 80 };
82 81 </script>
83 82 <!--[if lt IE 9]>
84 83 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
85 84 <![endif]-->
86 85 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
87 86 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
88 87 ## avoide escaping the %N
89 88 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
90 89
91 90
92 91 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
93 92 ${self.js_extra()}
94 93
95 94 <script type="text/javascript">
96 95 $(document).ready(function(){
97 96 show_more_event();
98 97 timeagoActivate();
99 98 })
100 99 </script>
101 100
102 101 </%def>
103 102
104 103 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
105 104 <%def name="js_extra()"></%def>
106 105 ${self.js()}
107 106
108 107 <%def name="head_extra()"></%def>
109 108 ${self.head_extra()}
110 109 <%include file="/base/plugins_base.html"/>
111 110
112 111 ## extra stuff
113 112 %if c.pre_code:
114 113 ${c.pre_code|n}
115 114 %endif
116 115 </head>
117 116 <body id="body">
118 117 <noscript>
119 118 <div class="noscript-error">
120 119 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
121 120 </div>
122 121 </noscript>
123 122 ## IE hacks
124 123 <!--[if IE 7]>
125 124 <script>$(document.body).addClass('ie7')</script>
126 125 <![endif]-->
127 126 <!--[if IE 8]>
128 127 <script>$(document.body).addClass('ie8')</script>
129 128 <![endif]-->
130 129 <!--[if IE 9]>
131 130 <script>$(document.body).addClass('ie9')</script>
132 131 <![endif]-->
133 132
134 133 ${next.body()}
135 134 %if c.post_code:
136 135 ${c.post_code|n}
137 136 %endif
138 137 </body>
139 138 </html>
@@ -1,344 +1,397 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib import helpers as h
24 from rhodecode.lib.auth import check_password
24 25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
25 26 from rhodecode.model.meta import Session
26 27 from rhodecode.tests import (
27 28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
28 29 assert_session_flash)
29 30 from rhodecode.tests.fixture import Fixture
30 31 from rhodecode.tests.utils import AssertResponse
31 32
32 33 fixture = Fixture()
33 34
34 35
35 36 class TestMyAccountController(TestController):
36 37 test_user_1 = 'testme'
38 test_user_1_password = '0jd83nHNS/d23n'
37 39 destroy_users = set()
38 40
39 41 @classmethod
40 42 def teardown_class(cls):
41 43 fixture.destroy_users(cls.destroy_users)
42 44
43 45 def test_my_account(self):
44 46 self.log_user()
45 47 response = self.app.get(url('my_account'))
46 48
47 49 response.mustcontain('test_admin')
48 50 response.mustcontain('href="/_admin/my_account/edit"')
49 51
50 52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
51 53 response = self.app.get(url('my_account'))
52 54 assert_response = AssertResponse(response)
53 55 element = assert_response.get_element('.logout #csrf_token')
54 56 assert element.value == csrf_token
55 57
56 58 def test_my_account_edit(self):
57 59 self.log_user()
58 60 response = self.app.get(url('my_account_edit'))
59 61
60 62 response.mustcontain('value="test_admin')
61 63
62 64 def test_my_account_my_repos(self):
63 65 self.log_user()
64 66 response = self.app.get(url('my_account_repos'))
65 67 repos = Repository.query().filter(
66 68 Repository.user == User.get_by_username(
67 69 TEST_USER_ADMIN_LOGIN)).all()
68 70 for repo in repos:
69 71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
70 72
71 73 def test_my_account_my_watched(self):
72 74 self.log_user()
73 75 response = self.app.get(url('my_account_watched'))
74 76
75 77 repos = UserFollowing.query().filter(
76 78 UserFollowing.user == User.get_by_username(
77 79 TEST_USER_ADMIN_LOGIN)).all()
78 80 for repo in repos:
79 81 response.mustcontain(
80 82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
81 83
82 84 @pytest.mark.backends("git", "hg")
83 85 def test_my_account_my_pullrequests(self, pr_util):
84 86 self.log_user()
85 87 response = self.app.get(url('my_account_pullrequests'))
86 88 response.mustcontain('You currently have no open pull requests.')
87 89
88 90 pr = pr_util.create_pull_request(title='TestMyAccountPR')
89 91 response = self.app.get(url('my_account_pullrequests'))
90 92 response.mustcontain('There are currently no open pull requests '
91 93 'requiring your participation')
92 94
93 95 response.mustcontain('#%s: TestMyAccountPR' % pr.pull_request_id)
94 96
95 97 def test_my_account_my_emails(self):
96 98 self.log_user()
97 99 response = self.app.get(url('my_account_emails'))
98 100 response.mustcontain('No additional emails specified')
99 101
100 102 def test_my_account_my_emails_add_existing_email(self):
101 103 self.log_user()
102 104 response = self.app.get(url('my_account_emails'))
103 105 response.mustcontain('No additional emails specified')
104 106 response = self.app.post(url('my_account_emails'),
105 107 {'new_email': TEST_USER_REGULAR_EMAIL,
106 108 'csrf_token': self.csrf_token})
107 109 assert_session_flash(response, 'This e-mail address is already taken')
108 110
109 111 def test_my_account_my_emails_add_mising_email_in_form(self):
110 112 self.log_user()
111 113 response = self.app.get(url('my_account_emails'))
112 114 response.mustcontain('No additional emails specified')
113 115 response = self.app.post(url('my_account_emails'),
114 116 {'csrf_token': self.csrf_token})
115 117 assert_session_flash(response, 'Please enter an email address')
116 118
117 119 def test_my_account_my_emails_add_remove(self):
118 120 self.log_user()
119 121 response = self.app.get(url('my_account_emails'))
120 122 response.mustcontain('No additional emails specified')
121 123
122 124 response = self.app.post(url('my_account_emails'),
123 125 {'new_email': 'foo@barz.com',
124 126 'csrf_token': self.csrf_token})
125 127
126 128 response = self.app.get(url('my_account_emails'))
127 129
128 130 from rhodecode.model.db import UserEmailMap
129 131 email_id = UserEmailMap.query().filter(
130 132 UserEmailMap.user == User.get_by_username(
131 133 TEST_USER_ADMIN_LOGIN)).filter(
132 134 UserEmailMap.email == 'foo@barz.com').one().email_id
133 135
134 136 response.mustcontain('foo@barz.com')
135 137 response.mustcontain('<input id="del_email_id" name="del_email_id" '
136 138 'type="hidden" value="%s" />' % email_id)
137 139
138 140 response = self.app.post(
139 141 url('my_account_emails'), {
140 142 'del_email_id': email_id, '_method': 'delete',
141 143 'csrf_token': self.csrf_token})
142 144 assert_session_flash(response, 'Removed email address from user account')
143 145 response = self.app.get(url('my_account_emails'))
144 146 response.mustcontain('No additional emails specified')
145 147
146 148 @pytest.mark.parametrize(
147 149 "name, attrs", [
148 150 ('firstname', {'firstname': 'new_username'}),
149 151 ('lastname', {'lastname': 'new_username'}),
150 152 ('admin', {'admin': True}),
151 153 ('admin', {'admin': False}),
152 154 ('extern_type', {'extern_type': 'ldap'}),
153 155 ('extern_type', {'extern_type': None}),
154 156 # ('extern_name', {'extern_name': 'test'}),
155 157 # ('extern_name', {'extern_name': None}),
156 158 ('active', {'active': False}),
157 159 ('active', {'active': True}),
158 160 ('email', {'email': 'some@email.com'}),
159 161 ])
160 162 def test_my_account_update(self, name, attrs):
161 usr = fixture.create_user(self.test_user_1, password='qweqwe',
163 usr = fixture.create_user(self.test_user_1,
164 password=self.test_user_1_password,
162 165 email='testme@rhodecode.org',
163 166 extern_type='rhodecode',
164 167 extern_name=self.test_user_1,
165 168 skip_if_exists=True)
166 169 self.destroy_users.add(self.test_user_1)
167 170
168 171 params = usr.get_api_data() # current user data
169 172 user_id = usr.user_id
170 self.log_user(username=self.test_user_1, password='qweqwe')
173 self.log_user(
174 username=self.test_user_1, password=self.test_user_1_password)
171 175
172 176 params.update({'password_confirmation': ''})
173 177 params.update({'new_password': ''})
174 178 params.update({'extern_type': 'rhodecode'})
175 179 params.update({'extern_name': self.test_user_1})
176 180 params.update({'csrf_token': self.csrf_token})
177 181
178 182 params.update(attrs)
179 183 # my account page cannot set language param yet, only for admins
180 184 del params['language']
181 185 response = self.app.post(url('my_account'), params)
182 186
183 187 assert_session_flash(
184 188 response, 'Your account was updated successfully')
185 189
186 190 del params['csrf_token']
187 191
188 192 updated_user = User.get_by_username(self.test_user_1)
189 193 updated_params = updated_user.get_api_data()
190 194 updated_params.update({'password_confirmation': ''})
191 195 updated_params.update({'new_password': ''})
192 196
193 197 params['last_login'] = updated_params['last_login']
194 198 # my account page cannot set language param yet, only for admins
195 199 # but we get this info from API anyway
196 200 params['language'] = updated_params['language']
197 201
198 202 if name == 'email':
199 203 params['emails'] = [attrs['email']]
200 204 if name == 'extern_type':
201 205 # cannot update this via form, expected value is original one
202 206 params['extern_type'] = "rhodecode"
203 207 if name == 'extern_name':
204 208 # cannot update this via form, expected value is original one
205 209 params['extern_name'] = str(user_id)
206 210 if name == 'active':
207 211 # my account cannot deactivate account
208 212 params['active'] = True
209 213 if name == 'admin':
210 214 # my account cannot make you an admin !
211 215 params['admin'] = False
212 216
213 217 assert params == updated_params
214 218
215 219 def test_my_account_update_err_email_exists(self):
216 220 self.log_user()
217 221
218 222 new_email = 'test_regular@mail.com' # already exisitn email
219 223 response = self.app.post(url('my_account'),
220 224 params={
221 225 'username': 'test_admin',
222 226 'new_password': 'test12',
223 227 'password_confirmation': 'test122',
224 228 'firstname': 'NewName',
225 229 'lastname': 'NewLastname',
226 230 'email': new_email,
227 231 'csrf_token': self.csrf_token,
228 232 })
229 233
230 234 response.mustcontain('This e-mail address is already taken')
231 235
232 236 def test_my_account_update_err(self):
233 237 self.log_user('test_regular2', 'test12')
234 238
235 239 new_email = 'newmail.pl'
236 240 response = self.app.post(url('my_account'),
237 241 params={
238 242 'username': 'test_admin',
239 243 'new_password': 'test12',
240 244 'password_confirmation': 'test122',
241 245 'firstname': 'NewName',
242 246 'lastname': 'NewLastname',
243 247 'email': new_email,
244 248 'csrf_token': self.csrf_token,
245 249 })
246 250
247 251 response.mustcontain('An email address must contain a single @')
248 252 from rhodecode.model import validators
249 253 msg = validators.ValidUsername(
250 254 edit=False, old_data={})._messages['username_exists']
251 255 msg = h.html_escape(msg % {'username': 'test_admin'})
252 256 response.mustcontain(u"%s" % msg)
253 257
254 258 def test_my_account_auth_tokens(self):
255 259 usr = self.log_user('test_regular2', 'test12')
256 260 user = User.get(usr['user_id'])
257 261 response = self.app.get(url('my_account_auth_tokens'))
258 262 response.mustcontain(user.api_key)
259 263 response.mustcontain('expires: never')
260 264
261 265 @pytest.mark.parametrize("desc, lifetime", [
262 266 ('forever', -1),
263 267 ('5mins', 60*5),
264 268 ('30days', 60*60*24*30),
265 269 ])
266 270 def test_my_account_add_auth_tokens(self, desc, lifetime):
267 271 usr = self.log_user('test_regular2', 'test12')
268 272 user = User.get(usr['user_id'])
269 273 response = self.app.post(url('my_account_auth_tokens'),
270 274 {'description': desc, 'lifetime': lifetime,
271 275 'csrf_token': self.csrf_token})
272 276 assert_session_flash(response, 'Auth token successfully created')
273 277 try:
274 278 response = response.follow()
275 279 user = User.get(usr['user_id'])
276 280 for auth_token in user.auth_tokens:
277 281 response.mustcontain(auth_token)
278 282 finally:
279 283 for auth_token in UserApiKeys.query().all():
280 284 Session().delete(auth_token)
281 285 Session().commit()
282 286
283 287 def test_my_account_remove_auth_token(self):
284 288 # TODO: without this cleanup it fails when run with the whole
285 289 # test suite, so there must be some interference with other tests.
286 290 UserApiKeys.query().delete()
287 291
288 292 usr = self.log_user('test_regular2', 'test12')
289 293 User.get(usr['user_id'])
290 294 response = self.app.post(url('my_account_auth_tokens'),
291 295 {'description': 'desc', 'lifetime': -1,
292 296 'csrf_token': self.csrf_token})
293 297 assert_session_flash(response, 'Auth token successfully created')
294 298 response = response.follow()
295 299
296 300 # now delete our key
297 301 keys = UserApiKeys.query().all()
298 302 assert 1 == len(keys)
299 303
300 304 response = self.app.post(
301 305 url('my_account_auth_tokens'),
302 306 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
303 307 'csrf_token': self.csrf_token})
304 308 assert_session_flash(response, 'Auth token successfully deleted')
305 309 keys = UserApiKeys.query().all()
306 310 assert 0 == len(keys)
307 311
308 312 def test_my_account_reset_main_auth_token(self):
309 313 usr = self.log_user('test_regular2', 'test12')
310 314 user = User.get(usr['user_id'])
311 315 api_key = user.api_key
312 316 response = self.app.get(url('my_account_auth_tokens'))
313 317 response.mustcontain(api_key)
314 318 response.mustcontain('expires: never')
315 319
316 320 response = self.app.post(
317 321 url('my_account_auth_tokens'),
318 322 {'_method': 'delete', 'del_auth_token_builtin': api_key,
319 323 'csrf_token': self.csrf_token})
320 324 assert_session_flash(response, 'Auth token successfully reset')
321 325 response = response.follow()
322 326 response.mustcontain(no=[api_key])
323 327
324 def test_password_is_updated_in_session_on_password_change(
325 self, user_util):
328 def test_valid_change_password(self, user_util):
329 new_password = 'my_new_valid_password'
330 user = user_util.create_user(password=self.test_user_1_password)
331 session = self.log_user(user.username, self.test_user_1_password)
332 form_data = [
333 ('current_password', self.test_user_1_password),
334 ('__start__', 'new_password:mapping'),
335 ('new_password', new_password),
336 ('new_password-confirm', new_password),
337 ('__end__', 'new_password:mapping'),
338 ('csrf_token', self.csrf_token),
339 ]
340 response = self.app.post(url('my_account_password'), form_data).follow()
341 assert 'Successfully updated password' in response
342
343 # check_password depends on user being in session
344 Session().add(user)
345 try:
346 assert check_password(new_password, user.password)
347 finally:
348 Session().expunge(user)
349
350 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
351 ('', 'abcdef123', 'abcdef123'),
352 ('wrong_pw', 'abcdef123', 'abcdef123'),
353 (test_user_1_password, test_user_1_password, test_user_1_password),
354 (test_user_1_password, '', ''),
355 (test_user_1_password, 'abcdef123', ''),
356 (test_user_1_password, '', 'abcdef123'),
357 (test_user_1_password, 'not_the', 'same_pw'),
358 (test_user_1_password, 'short', 'short'),
359 ])
360 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
361 user_util):
362 user = user_util.create_user(password=self.test_user_1_password)
363 session = self.log_user(user.username, self.test_user_1_password)
364 old_password_hash = session['password']
365 form_data = [
366 ('current_password', current_pw),
367 ('__start__', 'new_password:mapping'),
368 ('new_password', new_pw),
369 ('new_password-confirm', confirm_pw),
370 ('__end__', 'new_password:mapping'),
371 ('csrf_token', self.csrf_token),
372 ]
373 response = self.app.post(url('my_account_password'), form_data)
374 assert 'Error occurred' in response
375
376 def test_password_is_updated_in_session_on_password_change(self, user_util):
326 377 old_password = 'abcdef123'
327 378 new_password = 'abcdef124'
328 379
329 380 user = user_util.create_user(password=old_password)
330 381 session = self.log_user(user.username, old_password)
331 382 old_password_hash = session['password']
332 383
333 form_data = {
334 'current_password': old_password,
335 'new_password': new_password,
336 'new_password_confirmation': new_password,
337 'csrf_token': self.csrf_token
338 }
384 form_data = [
385 ('current_password', old_password),
386 ('__start__', 'new_password:mapping'),
387 ('new_password', new_password),
388 ('new_password-confirm', new_password),
389 ('__end__', 'new_password:mapping'),
390 ('csrf_token', self.csrf_token),
391 ]
339 392 self.app.post(url('my_account_password'), form_data)
340 393
341 394 response = self.app.get(url('home'))
342 395 new_password_hash = response.session['rhodecode_user']['password']
343 396
344 397 assert old_password_hash != new_password_hash
General Comments 0
You need to be logged in to leave comments. Login now