##// END OF EJS Templates
repo-groups: implemented default personal repo groups logic....
marcink -
r1094:6b71b2c4 default
parent child Browse files
Show More
@@ -0,0 +1,27 b''
1 import logging
2
3 from sqlalchemy import Column, MetaData, Boolean
4
5 from rhodecode.lib.dbmigrate.versions import _reset_base
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_5_0_0 as db
17
18 # Add personal column to RepoGroup table.
19 rg_table = db.RepoGroup.__table__
20 rg_col = Column(
21 'personal', Boolean(), nullable=True, unique=None, default=None)
22 rg_col.create(table=rg_table)
23
24
25 def downgrade(migrate_engine):
26 meta = MetaData()
27 meta.bind = migrate_engine
@@ -1,63 +1,63 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
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 62 # defines current db version for migrations
54 __dbversion__ = 63 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'http://rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
63 63 disable_error_handler = False
@@ -1,466 +1,472 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 logging
22 22
23 23 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
24 24 from rhodecode.api.utils import (
25 25 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
26 26 from rhodecode.lib.auth import AuthUser, PasswordGenerator
27 27 from rhodecode.lib.exceptions import DefaultUserException
28 from rhodecode.lib.utils2 import safe_int
28 from rhodecode.lib.utils2 import safe_int, str2bool
29 29 from rhodecode.model.db import Session, User, Repository
30 30 from rhodecode.model.user import UserModel
31 31
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 @jsonrpc_method()
37 37 def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))):
38 38 """
39 39 Returns the information associated with a username or userid.
40 40
41 41 * If the ``userid`` is not set, this command returns the information
42 42 for the ``userid`` calling the method.
43 43
44 44 .. note::
45 45
46 46 Normal users may only run this command against their ``userid``. For
47 47 full privileges you must run this command using an |authtoken| with
48 48 admin rights.
49 49
50 50 :param apiuser: This is filled automatically from the |authtoken|.
51 51 :type apiuser: AuthUser
52 52 :param userid: Sets the userid for which data will be returned.
53 53 :type userid: Optional(str or int)
54 54
55 55 Example output:
56 56
57 57 .. code-block:: bash
58 58
59 59 {
60 60 "error": null,
61 61 "id": <id>,
62 62 "result": {
63 63 "active": true,
64 64 "admin": false,
65 65 "api_key": "api-key",
66 66 "api_keys": [ list of keys ],
67 67 "email": "user@example.com",
68 68 "emails": [
69 69 "user@example.com"
70 70 ],
71 71 "extern_name": "rhodecode",
72 72 "extern_type": "rhodecode",
73 73 "firstname": "username",
74 74 "ip_addresses": [],
75 75 "language": null,
76 76 "last_login": "Timestamp",
77 77 "lastname": "surnae",
78 78 "permissions": {
79 79 "global": [
80 80 "hg.inherit_default_perms.true",
81 81 "usergroup.read",
82 82 "hg.repogroup.create.false",
83 83 "hg.create.none",
84 84 "hg.password_reset.enabled",
85 85 "hg.extern_activate.manual",
86 86 "hg.create.write_on_repogroup.false",
87 87 "hg.usergroup.create.false",
88 88 "group.none",
89 89 "repository.none",
90 90 "hg.register.none",
91 91 "hg.fork.repository"
92 92 ],
93 93 "repositories": { "username/example": "repository.write"},
94 94 "repositories_groups": { "user-group/repo": "group.none" },
95 95 "user_groups": { "user_group_name": "usergroup.read" }
96 96 },
97 97 "user_id": 32,
98 98 "username": "username"
99 99 }
100 100 }
101 101 """
102 102
103 103 if not has_superadmin_permission(apiuser):
104 104 # make sure normal user does not pass someone else userid,
105 105 # he is not allowed to do that
106 106 if not isinstance(userid, Optional) and userid != apiuser.user_id:
107 107 raise JSONRPCError('userid is not the same as your user')
108 108
109 109 userid = Optional.extract(userid, evaluate_locals=locals())
110 110 userid = getattr(userid, 'user_id', userid)
111 111
112 112 user = get_user_or_error(userid)
113 113 data = user.get_api_data(include_secrets=True)
114 114 data['permissions'] = AuthUser(user_id=user.user_id).permissions
115 115 return data
116 116
117 117
118 118 @jsonrpc_method()
119 119 def get_users(request, apiuser):
120 120 """
121 121 Lists all users in the |RCE| user database.
122 122
123 123 This command can only be run using an |authtoken| with admin rights to
124 124 the specified repository.
125 125
126 126 This command takes the following options:
127 127
128 128 :param apiuser: This is filled automatically from the |authtoken|.
129 129 :type apiuser: AuthUser
130 130
131 131 Example output:
132 132
133 133 .. code-block:: bash
134 134
135 135 id : <id_given_in_input>
136 136 result: [<user_object>, ...]
137 137 error: null
138 138 """
139 139
140 140 if not has_superadmin_permission(apiuser):
141 141 raise JSONRPCForbidden()
142 142
143 143 result = []
144 144 users_list = User.query().order_by(User.username) \
145 145 .filter(User.username != User.DEFAULT_USER) \
146 146 .all()
147 147 for user in users_list:
148 148 result.append(user.get_api_data(include_secrets=True))
149 149 return result
150 150
151 151
152 152 @jsonrpc_method()
153 153 def create_user(request, apiuser, username, email, password=Optional(''),
154 154 firstname=Optional(''), lastname=Optional(''),
155 155 active=Optional(True), admin=Optional(False),
156 156 extern_name=Optional('rhodecode'),
157 157 extern_type=Optional('rhodecode'),
158 force_password_change=Optional(False)):
158 force_password_change=Optional(False),
159 create_personal_repo_group=Optional(None)):
159 160 """
160 161 Creates a new user and returns the new user object.
161 162
162 163 This command can only be run using an |authtoken| with admin rights to
163 164 the specified repository.
164 165
165 166 This command takes the following options:
166 167
167 168 :param apiuser: This is filled automatically from the |authtoken|.
168 169 :type apiuser: AuthUser
169 170 :param username: Set the new username.
170 171 :type username: str or int
171 172 :param email: Set the user email address.
172 173 :type email: str
173 174 :param password: Set the new user password.
174 175 :type password: Optional(str)
175 176 :param firstname: Set the new user firstname.
176 177 :type firstname: Optional(str)
177 178 :param lastname: Set the new user surname.
178 179 :type lastname: Optional(str)
179 180 :param active: Set the user as active.
180 181 :type active: Optional(``True`` | ``False``)
181 182 :param admin: Give the new user admin rights.
182 183 :type admin: Optional(``True`` | ``False``)
183 184 :param extern_name: Set the authentication plugin name.
184 185 Using LDAP this is filled with LDAP UID.
185 186 :type extern_name: Optional(str)
186 187 :param extern_type: Set the new user authentication plugin.
187 188 :type extern_type: Optional(str)
188 189 :param force_password_change: Force the new user to change password
189 190 on next login.
190 191 :type force_password_change: Optional(``True`` | ``False``)
191
192 :param create_personal_repo_group: Create personal repo group for this user
193 :type create_personal_repo_group: Optional(``True`` | ``False``)
192 194 Example output:
193 195
194 196 .. code-block:: bash
195 197
196 198 id : <id_given_in_input>
197 199 result: {
198 200 "msg" : "created new user `<username>`",
199 201 "user": <user_obj>
200 202 }
201 203 error: null
202 204
203 205 Example error output:
204 206
205 207 .. code-block:: bash
206 208
207 209 id : <id_given_in_input>
208 210 result : null
209 211 error : {
210 212 "user `<username>` already exist"
211 213 or
212 214 "email `<email>` already exist"
213 215 or
214 216 "failed to create user `<username>`"
215 217 }
216 218
217 219 """
218 220 if not has_superadmin_permission(apiuser):
219 221 raise JSONRPCForbidden()
220 222
221 223 if UserModel().get_by_username(username):
222 224 raise JSONRPCError("user `%s` already exist" % (username,))
223 225
224 226 if UserModel().get_by_email(email, case_insensitive=True):
225 227 raise JSONRPCError("email `%s` already exist" % (email,))
226 228
227 229 # generate random password if we actually given the
228 230 # extern_name and it's not rhodecode
229 231 if (not isinstance(extern_name, Optional) and
230 232 Optional.extract(extern_name) != 'rhodecode'):
231 233 # generate temporary password if user is external
232 234 password = PasswordGenerator().gen_password(length=16)
235 create_repo_group = Optional.extract(create_personal_repo_group)
236 if isinstance(create_repo_group, basestring):
237 create_repo_group = str2bool(create_repo_group)
233 238
234 239 try:
235 240 user = UserModel().create_or_update(
236 241 username=Optional.extract(username),
237 242 password=Optional.extract(password),
238 243 email=Optional.extract(email),
239 244 firstname=Optional.extract(firstname),
240 245 lastname=Optional.extract(lastname),
241 246 active=Optional.extract(active),
242 247 admin=Optional.extract(admin),
243 248 extern_type=Optional.extract(extern_type),
244 249 extern_name=Optional.extract(extern_name),
245 250 force_password_change=Optional.extract(force_password_change),
251 create_repo_group=create_repo_group
246 252 )
247 253 Session().commit()
248 254 return {
249 255 'msg': 'created new user `%s`' % username,
250 256 'user': user.get_api_data(include_secrets=True)
251 257 }
252 258 except Exception:
253 259 log.exception('Error occurred during creation of user')
254 260 raise JSONRPCError('failed to create user `%s`' % (username,))
255 261
256 262
257 263 @jsonrpc_method()
258 264 def update_user(request, apiuser, userid, username=Optional(None),
259 265 email=Optional(None), password=Optional(None),
260 266 firstname=Optional(None), lastname=Optional(None),
261 267 active=Optional(None), admin=Optional(None),
262 268 extern_type=Optional(None), extern_name=Optional(None), ):
263 269 """
264 270 Updates the details for the specified user, if that user exists.
265 271
266 272 This command can only be run using an |authtoken| with admin rights to
267 273 the specified repository.
268 274
269 275 This command takes the following options:
270 276
271 277 :param apiuser: This is filled automatically from |authtoken|.
272 278 :type apiuser: AuthUser
273 279 :param userid: Set the ``userid`` to update.
274 280 :type userid: str or int
275 281 :param username: Set the new username.
276 282 :type username: str or int
277 283 :param email: Set the new email.
278 284 :type email: str
279 285 :param password: Set the new password.
280 286 :type password: Optional(str)
281 287 :param firstname: Set the new first name.
282 288 :type firstname: Optional(str)
283 289 :param lastname: Set the new surname.
284 290 :type lastname: Optional(str)
285 291 :param active: Set the new user as active.
286 292 :type active: Optional(``True`` | ``False``)
287 293 :param admin: Give the user admin rights.
288 294 :type admin: Optional(``True`` | ``False``)
289 295 :param extern_name: Set the authentication plugin user name.
290 296 Using LDAP this is filled with LDAP UID.
291 297 :type extern_name: Optional(str)
292 298 :param extern_type: Set the authentication plugin type.
293 299 :type extern_type: Optional(str)
294 300
295 301
296 302 Example output:
297 303
298 304 .. code-block:: bash
299 305
300 306 id : <id_given_in_input>
301 307 result: {
302 308 "msg" : "updated user ID:<userid> <username>",
303 309 "user": <user_object>,
304 310 }
305 311 error: null
306 312
307 313 Example error output:
308 314
309 315 .. code-block:: bash
310 316
311 317 id : <id_given_in_input>
312 318 result : null
313 319 error : {
314 320 "failed to update user `<username>`"
315 321 }
316 322
317 323 """
318 324 if not has_superadmin_permission(apiuser):
319 325 raise JSONRPCForbidden()
320 326
321 327 user = get_user_or_error(userid)
322 328
323 329 # only non optional arguments will be stored in updates
324 330 updates = {}
325 331
326 332 try:
327 333
328 334 store_update(updates, username, 'username')
329 335 store_update(updates, password, 'password')
330 336 store_update(updates, email, 'email')
331 337 store_update(updates, firstname, 'name')
332 338 store_update(updates, lastname, 'lastname')
333 339 store_update(updates, active, 'active')
334 340 store_update(updates, admin, 'admin')
335 341 store_update(updates, extern_name, 'extern_name')
336 342 store_update(updates, extern_type, 'extern_type')
337 343
338 344 user = UserModel().update_user(user, **updates)
339 345 Session().commit()
340 346 return {
341 347 'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
342 348 'user': user.get_api_data(include_secrets=True)
343 349 }
344 350 except DefaultUserException:
345 351 log.exception("Default user edit exception")
346 352 raise JSONRPCError('editing default user is forbidden')
347 353 except Exception:
348 354 log.exception("Error occurred during update of user")
349 355 raise JSONRPCError('failed to update user `%s`' % (userid,))
350 356
351 357
352 358 @jsonrpc_method()
353 359 def delete_user(request, apiuser, userid):
354 360 """
355 361 Deletes the specified user from the |RCE| user database.
356 362
357 363 This command can only be run using an |authtoken| with admin rights to
358 364 the specified repository.
359 365
360 366 .. important::
361 367
362 368 Ensure all open pull requests and open code review
363 369 requests to this user are close.
364 370
365 371 Also ensure all repositories, or repository groups owned by this
366 372 user are reassigned before deletion.
367 373
368 374 This command takes the following options:
369 375
370 376 :param apiuser: This is filled automatically from the |authtoken|.
371 377 :type apiuser: AuthUser
372 378 :param userid: Set the user to delete.
373 379 :type userid: str or int
374 380
375 381 Example output:
376 382
377 383 .. code-block:: bash
378 384
379 385 id : <id_given_in_input>
380 386 result: {
381 387 "msg" : "deleted user ID:<userid> <username>",
382 388 "user": null
383 389 }
384 390 error: null
385 391
386 392 Example error output:
387 393
388 394 .. code-block:: bash
389 395
390 396 id : <id_given_in_input>
391 397 result : null
392 398 error : {
393 399 "failed to delete user ID:<userid> <username>"
394 400 }
395 401
396 402 """
397 403 if not has_superadmin_permission(apiuser):
398 404 raise JSONRPCForbidden()
399 405
400 406 user = get_user_or_error(userid)
401 407
402 408 try:
403 409 UserModel().delete(userid)
404 410 Session().commit()
405 411 return {
406 412 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),
407 413 'user': None
408 414 }
409 415 except Exception:
410 416 log.exception("Error occurred during deleting of user")
411 417 raise JSONRPCError(
412 418 'failed to delete user ID:%s %s' % (user.user_id, user.username))
413 419
414 420
415 421 @jsonrpc_method()
416 422 def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
417 423 """
418 424 Displays all repositories locked by the specified user.
419 425
420 426 * If this command is run by a non-admin user, it returns
421 427 a list of |repos| locked by that user.
422 428
423 429 This command takes the following options:
424 430
425 431 :param apiuser: This is filled automatically from the |authtoken|.
426 432 :type apiuser: AuthUser
427 433 :param userid: Sets the userid whose list of locked |repos| will be
428 434 displayed.
429 435 :type userid: Optional(str or int)
430 436
431 437 Example output:
432 438
433 439 .. code-block:: bash
434 440
435 441 id : <id_given_in_input>
436 442 result : {
437 443 [repo_object, repo_object,...]
438 444 }
439 445 error : null
440 446 """
441 447
442 448 include_secrets = False
443 449 if not has_superadmin_permission(apiuser):
444 450 # make sure normal user does not pass someone else userid,
445 451 # he is not allowed to do that
446 452 if not isinstance(userid, Optional) and userid != apiuser.user_id:
447 453 raise JSONRPCError('userid is not the same as your user')
448 454 else:
449 455 include_secrets = True
450 456
451 457 userid = Optional.extract(userid, evaluate_locals=locals())
452 458 userid = getattr(userid, 'user_id', userid)
453 459 user = get_user_or_error(userid)
454 460
455 461 ret = []
456 462
457 463 # show all locks
458 464 for r in Repository.getAll():
459 465 _user_id, _time, _reason = r.locked
460 466 if _user_id and _time:
461 467 _api_data = r.get_api_data(include_secrets=include_secrets)
462 468 # if we use user filter just show the locks for this user
463 469 if safe_int(_user_id) == user.user_id:
464 470 ret.append(_api_data)
465 471
466 472 return ret
@@ -1,881 +1,878 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 Repositories controller for RhodeCode
24 24 """
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 import formencode
30 30 from formencode import htmlfill
31 31 from pylons import request, tmpl_context as c, url
32 32 from pylons.controllers.util import redirect
33 33 from pylons.i18n.translation import _
34 34 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest
35 35
36 36 import rhodecode
37 37 from rhodecode.lib import auth, helpers as h
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator,
40 40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny,
41 41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.ext_json import json
44 44 from rhodecode.lib.exceptions import AttachedForksError
45 45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
46 46 from rhodecode.lib.utils2 import safe_int, str2bool
47 47 from rhodecode.lib.vcs import RepositoryError
48 48 from rhodecode.model.db import (
49 49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
50 50 from rhodecode.model.forms import (
51 51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
52 52 IssueTrackerPatternsForm)
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.repo import RepoModel
55 55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
56 56 from rhodecode.model.settings import (
57 57 SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel,
58 58 SettingNotFound)
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class ReposController(BaseRepoController):
64 64 """
65 65 REST Controller styled on the Atom Publishing Protocol"""
66 66 # To properly map this controller, ensure your config/routing.py
67 67 # file has a resource setup:
68 68 # map.resource('repo', 'repos')
69 69
70 70 @LoginRequired()
71 71 def __before__(self):
72 72 super(ReposController, self).__before__()
73 73
74 74 def _load_repo(self, repo_name):
75 75 repo_obj = Repository.get_by_repo_name(repo_name)
76 76
77 77 if repo_obj is None:
78 78 h.not_mapped_error(repo_name)
79 79 return redirect(url('repos'))
80 80
81 81 return repo_obj
82 82
83 83 def __load_defaults(self, repo=None):
84 84 acl_groups = RepoGroupList(RepoGroup.query().all(),
85 85 perm_set=['group.write', 'group.admin'])
86 86 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
87 87 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
88 88
89 89 # in case someone no longer have a group.write access to a repository
90 90 # pre fill the list with this entry, we don't care if this is the same
91 91 # but it will allow saving repo data properly.
92 92
93 93 repo_group = None
94 94 if repo:
95 95 repo_group = repo.group
96 96 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
97 97 c.repo_groups_choices.append(unicode(repo_group.group_id))
98 98 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
99 99
100 100 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
101 101 c.landing_revs_choices = choices
102 102
103 103 def __load_data(self, repo_name=None):
104 104 """
105 105 Load defaults settings for edit, and update
106 106
107 107 :param repo_name:
108 108 """
109 109 c.repo_info = self._load_repo(repo_name)
110 110 self.__load_defaults(c.repo_info)
111 111
112 112 # override defaults for exact repo info here git/hg etc
113 113 if not c.repository_requirements_missing:
114 114 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
115 115 c.repo_info)
116 116 c.landing_revs_choices = choices
117 117 defaults = RepoModel()._get_defaults(repo_name)
118 118
119 119 return defaults
120 120
121 121 def _log_creation_exception(self, e, repo_name):
122 122 reason = None
123 123 if len(e.args) == 2:
124 124 reason = e.args[1]
125 125
126 126 if reason == 'INVALID_CERTIFICATE':
127 127 log.exception(
128 128 'Exception creating a repository: invalid certificate')
129 129 msg = (_('Error creating repository %s: invalid certificate')
130 130 % repo_name)
131 131 else:
132 132 log.exception("Exception creating a repository")
133 133 msg = (_('Error creating repository %s')
134 134 % repo_name)
135 135
136 136 return msg
137 137
138 138 @NotAnonymous()
139 139 def index(self, format='html'):
140 140 """GET /repos: All items in the collection"""
141 141 # url('repos')
142 142
143 143 repo_list = Repository.get_all_repos()
144 144 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
145 145 repos_data = RepoModel().get_repos_as_dict(
146 146 repo_list=c.repo_list, admin=True, super_user_actions=True)
147 147 # json used to render the grid
148 148 c.data = json.dumps(repos_data)
149 149
150 150 return render('admin/repos/repos.html')
151 151
152 152 # perms check inside
153 153 @NotAnonymous()
154 154 @auth.CSRFRequired()
155 155 def create(self):
156 156 """
157 157 POST /repos: Create a new item"""
158 158 # url('repos')
159 159
160 160 self.__load_defaults()
161 161 form_result = {}
162 162 task_id = None
163 c.personal_repo_group = c.rhodecode_user.personal_repo_group
163 164 try:
164 165 # CanWriteToGroup validators checks permissions of this POST
165 166 form_result = RepoForm(repo_groups=c.repo_groups_choices,
166 167 landing_revs=c.landing_revs_choices)()\
167 168 .to_python(dict(request.POST))
168 169
169 170 # create is done sometimes async on celery, db transaction
170 171 # management is handled there.
171 172 task = RepoModel().create(form_result, c.rhodecode_user.user_id)
172 173 from celery.result import BaseAsyncResult
173 174 if isinstance(task, BaseAsyncResult):
174 175 task_id = task.task_id
175 176 except formencode.Invalid as errors:
176 c.personal_repo_group = RepoGroup.get_by_group_name(
177 c.rhodecode_user.username)
178 177 return htmlfill.render(
179 178 render('admin/repos/repo_add.html'),
180 179 defaults=errors.value,
181 180 errors=errors.error_dict or {},
182 181 prefix_error=False,
183 182 encoding="UTF-8",
184 183 force_defaults=False)
185 184
186 185 except Exception as e:
187 186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
188 187 h.flash(msg, category='error')
189 188 return redirect(url('home'))
190 189
191 190 return redirect(h.url('repo_creating_home',
192 191 repo_name=form_result['repo_name_full'],
193 192 task_id=task_id))
194 193
195 194 # perms check inside
196 195 @NotAnonymous()
197 196 def create_repository(self):
198 197 """GET /_admin/create_repository: Form to create a new item"""
199 198 new_repo = request.GET.get('repo', '')
200 199 parent_group = request.GET.get('parent_group')
201 200 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
202 201 # you're not super admin nor have global create permissions,
203 202 # but maybe you have at least write permission to a parent group ?
204 203 _gr = RepoGroup.get(parent_group)
205 204 gr_name = _gr.group_name if _gr else None
206 205 # create repositories with write permission on group is set to true
207 206 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
208 207 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
209 208 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
210 209 if not (group_admin or (group_write and create_on_write)):
211 210 raise HTTPForbidden
212 211
213 212 acl_groups = RepoGroupList(RepoGroup.query().all(),
214 213 perm_set=['group.write', 'group.admin'])
215 214 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
216 215 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
217 216 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
218 c.personal_repo_group = RepoGroup.get_by_group_name(c.rhodecode_user.username)
217 c.personal_repo_group = c.rhodecode_user.personal_repo_group
219 218 c.new_repo = repo_name_slug(new_repo)
220 219
221 220 ## apply the defaults from defaults page
222 221 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
223 222 # set checkbox to autochecked
224 223 defaults['repo_copy_permissions'] = True
225 224 if parent_group:
226 225 defaults.update({'repo_group': parent_group})
227 226
228 227 return htmlfill.render(
229 228 render('admin/repos/repo_add.html'),
230 229 defaults=defaults,
231 230 errors={},
232 231 prefix_error=False,
233 232 encoding="UTF-8",
234 233 force_defaults=False
235 234 )
236 235
237 236 @NotAnonymous()
238 237 def repo_creating(self, repo_name):
239 238 c.repo = repo_name
240 239 c.task_id = request.GET.get('task_id')
241 240 if not c.repo:
242 241 raise HTTPNotFound()
243 242 return render('admin/repos/repo_creating.html')
244 243
245 244 @NotAnonymous()
246 245 @jsonify
247 246 def repo_check(self, repo_name):
248 247 c.repo = repo_name
249 248 task_id = request.GET.get('task_id')
250 249
251 250 if task_id and task_id not in ['None']:
252 251 import rhodecode
253 252 from celery.result import AsyncResult
254 253 if rhodecode.CELERY_ENABLED:
255 254 task = AsyncResult(task_id)
256 255 if task.failed():
257 256 msg = self._log_creation_exception(task.result, c.repo)
258 257 h.flash(msg, category='error')
259 258 return redirect(url('home'), code=501)
260 259
261 260 repo = Repository.get_by_repo_name(repo_name)
262 261 if repo and repo.repo_state == Repository.STATE_CREATED:
263 262 if repo.clone_uri:
264 263 clone_uri = repo.clone_uri_hidden
265 264 h.flash(_('Created repository %s from %s')
266 265 % (repo.repo_name, clone_uri), category='success')
267 266 else:
268 267 repo_url = h.link_to(repo.repo_name,
269 268 h.url('summary_home',
270 269 repo_name=repo.repo_name))
271 270 fork = repo.fork
272 271 if fork:
273 272 fork_name = fork.repo_name
274 273 h.flash(h.literal(_('Forked repository %s as %s')
275 274 % (fork_name, repo_url)), category='success')
276 275 else:
277 276 h.flash(h.literal(_('Created repository %s') % repo_url),
278 277 category='success')
279 278 return {'result': True}
280 279 return {'result': False}
281 280
282 281 @HasRepoPermissionAllDecorator('repository.admin')
283 282 @auth.CSRFRequired()
284 283 def update(self, repo_name):
285 284 """
286 285 PUT /repos/repo_name: Update an existing item"""
287 286 # Forms posted to this method should contain a hidden field:
288 287 # <input type="hidden" name="_method" value="PUT" />
289 288 # Or using helpers:
290 289 # h.form(url('repo', repo_name=ID),
291 290 # method='put')
292 291 # url('repo', repo_name=ID)
293 292
294 293 self.__load_data(repo_name)
295 294 c.active = 'settings'
296 295 c.repo_fields = RepositoryField.query()\
297 296 .filter(RepositoryField.repository == c.repo_info).all()
298 297
299 298 repo_model = RepoModel()
300 299 changed_name = repo_name
301 300
301 c.personal_repo_group = c.rhodecode_user.personal_repo_group
302 302 # override the choices with extracted revisions !
303 c.personal_repo_group = RepoGroup.get_by_group_name(
304 c.rhodecode_user.username)
305 303 repo = Repository.get_by_repo_name(repo_name)
306 304 old_data = {
307 305 'repo_name': repo_name,
308 306 'repo_group': repo.group.get_dict() if repo.group else {},
309 307 'repo_type': repo.repo_type,
310 308 }
311 309 _form = RepoForm(
312 310 edit=True, old_data=old_data, repo_groups=c.repo_groups_choices,
313 311 landing_revs=c.landing_revs_choices, allow_disabled=True)()
314 312
315 313 try:
316 314 form_result = _form.to_python(dict(request.POST))
317 315 repo = repo_model.update(repo_name, **form_result)
318 316 ScmModel().mark_for_invalidation(repo_name)
319 317 h.flash(_('Repository %s updated successfully') % repo_name,
320 318 category='success')
321 319 changed_name = repo.repo_name
322 320 action_logger(c.rhodecode_user, 'admin_updated_repo',
323 321 changed_name, self.ip_addr, self.sa)
324 322 Session().commit()
325 323 except formencode.Invalid as errors:
326 324 defaults = self.__load_data(repo_name)
327 325 defaults.update(errors.value)
328 326 return htmlfill.render(
329 327 render('admin/repos/repo_edit.html'),
330 328 defaults=defaults,
331 329 errors=errors.error_dict or {},
332 330 prefix_error=False,
333 331 encoding="UTF-8",
334 332 force_defaults=False)
335 333
336 334 except Exception:
337 335 log.exception("Exception during update of repository")
338 336 h.flash(_('Error occurred during update of repository %s') \
339 337 % repo_name, category='error')
340 338 return redirect(url('edit_repo', repo_name=changed_name))
341 339
342 340 @HasRepoPermissionAllDecorator('repository.admin')
343 341 @auth.CSRFRequired()
344 342 def delete(self, repo_name):
345 343 """
346 344 DELETE /repos/repo_name: Delete an existing item"""
347 345 # Forms posted to this method should contain a hidden field:
348 346 # <input type="hidden" name="_method" value="DELETE" />
349 347 # Or using helpers:
350 348 # h.form(url('repo', repo_name=ID),
351 349 # method='delete')
352 350 # url('repo', repo_name=ID)
353 351
354 352 repo_model = RepoModel()
355 353 repo = repo_model.get_by_repo_name(repo_name)
356 354 if not repo:
357 355 h.not_mapped_error(repo_name)
358 356 return redirect(url('repos'))
359 357 try:
360 358 _forks = repo.forks.count()
361 359 handle_forks = None
362 360 if _forks and request.POST.get('forks'):
363 361 do = request.POST['forks']
364 362 if do == 'detach_forks':
365 363 handle_forks = 'detach'
366 364 h.flash(_('Detached %s forks') % _forks, category='success')
367 365 elif do == 'delete_forks':
368 366 handle_forks = 'delete'
369 367 h.flash(_('Deleted %s forks') % _forks, category='success')
370 368 repo_model.delete(repo, forks=handle_forks)
371 369 action_logger(c.rhodecode_user, 'admin_deleted_repo',
372 370 repo_name, self.ip_addr, self.sa)
373 371 ScmModel().mark_for_invalidation(repo_name)
374 372 h.flash(_('Deleted repository %s') % repo_name, category='success')
375 373 Session().commit()
376 374 except AttachedForksError:
377 375 h.flash(_('Cannot delete %s it still contains attached forks')
378 376 % repo_name, category='warning')
379 377
380 378 except Exception:
381 379 log.exception("Exception during deletion of repository")
382 380 h.flash(_('An error occurred during deletion of %s') % repo_name,
383 381 category='error')
384 382
385 383 return redirect(url('repos'))
386 384
387 385 @HasPermissionAllDecorator('hg.admin')
388 386 def show(self, repo_name, format='html'):
389 387 """GET /repos/repo_name: Show a specific item"""
390 388 # url('repo', repo_name=ID)
391 389
392 390 @HasRepoPermissionAllDecorator('repository.admin')
393 391 def edit(self, repo_name):
394 392 """GET /repo_name/settings: Form to edit an existing item"""
395 393 # url('edit_repo', repo_name=ID)
396 394 defaults = self.__load_data(repo_name)
397 395 if 'clone_uri' in defaults:
398 396 del defaults['clone_uri']
399 397
400 398 c.repo_fields = RepositoryField.query()\
401 399 .filter(RepositoryField.repository == c.repo_info).all()
402 c.personal_repo_group = RepoGroup.get_by_group_name(
403 c.rhodecode_user.username)
400 c.personal_repo_group = c.rhodecode_user.personal_repo_group
404 401 c.active = 'settings'
405 402 return htmlfill.render(
406 403 render('admin/repos/repo_edit.html'),
407 404 defaults=defaults,
408 405 encoding="UTF-8",
409 406 force_defaults=False)
410 407
411 408 @HasRepoPermissionAllDecorator('repository.admin')
412 409 def edit_permissions(self, repo_name):
413 410 """GET /repo_name/settings: Form to edit an existing item"""
414 411 # url('edit_repo', repo_name=ID)
415 412 c.repo_info = self._load_repo(repo_name)
416 413 c.active = 'permissions'
417 414 defaults = RepoModel()._get_defaults(repo_name)
418 415
419 416 return htmlfill.render(
420 417 render('admin/repos/repo_edit.html'),
421 418 defaults=defaults,
422 419 encoding="UTF-8",
423 420 force_defaults=False)
424 421
425 422 @HasRepoPermissionAllDecorator('repository.admin')
426 423 @auth.CSRFRequired()
427 424 def edit_permissions_update(self, repo_name):
428 425 form = RepoPermsForm()().to_python(request.POST)
429 426 RepoModel().update_permissions(repo_name,
430 427 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
431 428
432 429 #TODO: implement this
433 430 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
434 431 # repo_name, self.ip_addr, self.sa)
435 432 Session().commit()
436 433 h.flash(_('Repository permissions updated'), category='success')
437 434 return redirect(url('edit_repo_perms', repo_name=repo_name))
438 435
439 436 @HasRepoPermissionAllDecorator('repository.admin')
440 437 def edit_fields(self, repo_name):
441 438 """GET /repo_name/settings: Form to edit an existing item"""
442 439 # url('edit_repo', repo_name=ID)
443 440 c.repo_info = self._load_repo(repo_name)
444 441 c.repo_fields = RepositoryField.query()\
445 442 .filter(RepositoryField.repository == c.repo_info).all()
446 443 c.active = 'fields'
447 444 if request.POST:
448 445
449 446 return redirect(url('repo_edit_fields'))
450 447 return render('admin/repos/repo_edit.html')
451 448
452 449 @HasRepoPermissionAllDecorator('repository.admin')
453 450 @auth.CSRFRequired()
454 451 def create_repo_field(self, repo_name):
455 452 try:
456 453 form_result = RepoFieldForm()().to_python(dict(request.POST))
457 454 RepoModel().add_repo_field(
458 455 repo_name, form_result['new_field_key'],
459 456 field_type=form_result['new_field_type'],
460 457 field_value=form_result['new_field_value'],
461 458 field_label=form_result['new_field_label'],
462 459 field_desc=form_result['new_field_desc'])
463 460
464 461 Session().commit()
465 462 except Exception as e:
466 463 log.exception("Exception creating field")
467 464 msg = _('An error occurred during creation of field')
468 465 if isinstance(e, formencode.Invalid):
469 466 msg += ". " + e.msg
470 467 h.flash(msg, category='error')
471 468 return redirect(url('edit_repo_fields', repo_name=repo_name))
472 469
473 470 @HasRepoPermissionAllDecorator('repository.admin')
474 471 @auth.CSRFRequired()
475 472 def delete_repo_field(self, repo_name, field_id):
476 473 field = RepositoryField.get_or_404(field_id)
477 474 try:
478 475 RepoModel().delete_repo_field(repo_name, field.field_key)
479 476 Session().commit()
480 477 except Exception as e:
481 478 log.exception("Exception during removal of field")
482 479 msg = _('An error occurred during removal of field')
483 480 h.flash(msg, category='error')
484 481 return redirect(url('edit_repo_fields', repo_name=repo_name))
485 482
486 483 @HasRepoPermissionAllDecorator('repository.admin')
487 484 def edit_advanced(self, repo_name):
488 485 """GET /repo_name/settings: Form to edit an existing item"""
489 486 # url('edit_repo', repo_name=ID)
490 487 c.repo_info = self._load_repo(repo_name)
491 488 c.default_user_id = User.get_default_user().user_id
492 489 c.in_public_journal = UserFollowing.query()\
493 490 .filter(UserFollowing.user_id == c.default_user_id)\
494 491 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
495 492
496 493 c.active = 'advanced'
497 494 c.has_origin_repo_read_perm = False
498 495 if c.repo_info.fork:
499 496 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
500 497 'repository.write', 'repository.read', 'repository.admin')(
501 498 c.repo_info.fork.repo_name, 'repo set as fork page')
502 499
503 500 if request.POST:
504 501 return redirect(url('repo_edit_advanced'))
505 502 return render('admin/repos/repo_edit.html')
506 503
507 504 @HasRepoPermissionAllDecorator('repository.admin')
508 505 @auth.CSRFRequired()
509 506 def edit_advanced_journal(self, repo_name):
510 507 """
511 508 Set's this repository to be visible in public journal,
512 509 in other words assing default user to follow this repo
513 510
514 511 :param repo_name:
515 512 """
516 513
517 514 try:
518 515 repo_id = Repository.get_by_repo_name(repo_name).repo_id
519 516 user_id = User.get_default_user().user_id
520 517 self.scm_model.toggle_following_repo(repo_id, user_id)
521 518 h.flash(_('Updated repository visibility in public journal'),
522 519 category='success')
523 520 Session().commit()
524 521 except Exception:
525 522 h.flash(_('An error occurred during setting this'
526 523 ' repository in public journal'),
527 524 category='error')
528 525
529 526 return redirect(url('edit_repo_advanced', repo_name=repo_name))
530 527
531 528 @HasRepoPermissionAllDecorator('repository.admin')
532 529 @auth.CSRFRequired()
533 530 def edit_advanced_fork(self, repo_name):
534 531 """
535 532 Mark given repository as a fork of another
536 533
537 534 :param repo_name:
538 535 """
539 536
540 537 new_fork_id = request.POST.get('id_fork_of')
541 538 try:
542 539
543 540 if new_fork_id and not new_fork_id.isdigit():
544 541 log.error('Given fork id %s is not an INT', new_fork_id)
545 542
546 543 fork_id = safe_int(new_fork_id)
547 544 repo = ScmModel().mark_as_fork(repo_name, fork_id,
548 545 c.rhodecode_user.username)
549 546 fork = repo.fork.repo_name if repo.fork else _('Nothing')
550 547 Session().commit()
551 548 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
552 549 category='success')
553 550 except RepositoryError as e:
554 551 log.exception("Repository Error occurred")
555 552 h.flash(str(e), category='error')
556 553 except Exception as e:
557 554 log.exception("Exception while editing fork")
558 555 h.flash(_('An error occurred during this operation'),
559 556 category='error')
560 557
561 558 return redirect(url('edit_repo_advanced', repo_name=repo_name))
562 559
563 560 @HasRepoPermissionAllDecorator('repository.admin')
564 561 @auth.CSRFRequired()
565 562 def edit_advanced_locking(self, repo_name):
566 563 """
567 564 Unlock repository when it is locked !
568 565
569 566 :param repo_name:
570 567 """
571 568 try:
572 569 repo = Repository.get_by_repo_name(repo_name)
573 570 if request.POST.get('set_lock'):
574 571 Repository.lock(repo, c.rhodecode_user.user_id,
575 572 lock_reason=Repository.LOCK_WEB)
576 573 h.flash(_('Locked repository'), category='success')
577 574 elif request.POST.get('set_unlock'):
578 575 Repository.unlock(repo)
579 576 h.flash(_('Unlocked repository'), category='success')
580 577 except Exception as e:
581 578 log.exception("Exception during unlocking")
582 579 h.flash(_('An error occurred during unlocking'),
583 580 category='error')
584 581 return redirect(url('edit_repo_advanced', repo_name=repo_name))
585 582
586 583 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
587 584 @auth.CSRFRequired()
588 585 def toggle_locking(self, repo_name):
589 586 """
590 587 Toggle locking of repository by simple GET call to url
591 588
592 589 :param repo_name:
593 590 """
594 591
595 592 try:
596 593 repo = Repository.get_by_repo_name(repo_name)
597 594
598 595 if repo.enable_locking:
599 596 if repo.locked[0]:
600 597 Repository.unlock(repo)
601 598 action = _('Unlocked')
602 599 else:
603 600 Repository.lock(repo, c.rhodecode_user.user_id,
604 601 lock_reason=Repository.LOCK_WEB)
605 602 action = _('Locked')
606 603
607 604 h.flash(_('Repository has been %s') % action,
608 605 category='success')
609 606 except Exception:
610 607 log.exception("Exception during unlocking")
611 608 h.flash(_('An error occurred during unlocking'),
612 609 category='error')
613 610 return redirect(url('summary_home', repo_name=repo_name))
614 611
615 612 @HasRepoPermissionAllDecorator('repository.admin')
616 613 @auth.CSRFRequired()
617 614 def edit_caches(self, repo_name):
618 615 """PUT /{repo_name}/settings/caches: invalidate the repo caches."""
619 616 try:
620 617 ScmModel().mark_for_invalidation(repo_name, delete=True)
621 618 Session().commit()
622 619 h.flash(_('Cache invalidation successful'),
623 620 category='success')
624 621 except Exception:
625 622 log.exception("Exception during cache invalidation")
626 623 h.flash(_('An error occurred during cache invalidation'),
627 624 category='error')
628 625
629 626 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
630 627
631 628 @HasRepoPermissionAllDecorator('repository.admin')
632 629 def edit_caches_form(self, repo_name):
633 630 """GET /repo_name/settings: Form to edit an existing item"""
634 631 # url('edit_repo', repo_name=ID)
635 632 c.repo_info = self._load_repo(repo_name)
636 633 c.active = 'caches'
637 634
638 635 return render('admin/repos/repo_edit.html')
639 636
640 637 @HasRepoPermissionAllDecorator('repository.admin')
641 638 @auth.CSRFRequired()
642 639 def edit_remote(self, repo_name):
643 640 """PUT /{repo_name}/settings/remote: edit the repo remote."""
644 641 try:
645 642 ScmModel().pull_changes(repo_name, c.rhodecode_user.username)
646 643 h.flash(_('Pulled from remote location'), category='success')
647 644 except Exception:
648 645 log.exception("Exception during pull from remote")
649 646 h.flash(_('An error occurred during pull from remote location'),
650 647 category='error')
651 648 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
652 649
653 650 @HasRepoPermissionAllDecorator('repository.admin')
654 651 def edit_remote_form(self, repo_name):
655 652 """GET /repo_name/settings: Form to edit an existing item"""
656 653 # url('edit_repo', repo_name=ID)
657 654 c.repo_info = self._load_repo(repo_name)
658 655 c.active = 'remote'
659 656
660 657 return render('admin/repos/repo_edit.html')
661 658
662 659 @HasRepoPermissionAllDecorator('repository.admin')
663 660 @auth.CSRFRequired()
664 661 def edit_statistics(self, repo_name):
665 662 """PUT /{repo_name}/settings/statistics: reset the repo statistics."""
666 663 try:
667 664 RepoModel().delete_stats(repo_name)
668 665 Session().commit()
669 666 except Exception as e:
670 667 log.error(traceback.format_exc())
671 668 h.flash(_('An error occurred during deletion of repository stats'),
672 669 category='error')
673 670 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
674 671
675 672 @HasRepoPermissionAllDecorator('repository.admin')
676 673 def edit_statistics_form(self, repo_name):
677 674 """GET /repo_name/settings: Form to edit an existing item"""
678 675 # url('edit_repo', repo_name=ID)
679 676 c.repo_info = self._load_repo(repo_name)
680 677 repo = c.repo_info.scm_instance()
681 678
682 679 if c.repo_info.stats:
683 680 # this is on what revision we ended up so we add +1 for count
684 681 last_rev = c.repo_info.stats.stat_on_revision + 1
685 682 else:
686 683 last_rev = 0
687 684 c.stats_revision = last_rev
688 685
689 686 c.repo_last_rev = repo.count()
690 687
691 688 if last_rev == 0 or c.repo_last_rev == 0:
692 689 c.stats_percentage = 0
693 690 else:
694 691 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
695 692
696 693 c.active = 'statistics'
697 694
698 695 return render('admin/repos/repo_edit.html')
699 696
700 697 @HasRepoPermissionAllDecorator('repository.admin')
701 698 @auth.CSRFRequired()
702 699 def repo_issuetracker_test(self, repo_name):
703 700 if request.is_xhr:
704 701 return h.urlify_commit_message(
705 702 request.POST.get('test_text', ''),
706 703 repo_name)
707 704 else:
708 705 raise HTTPBadRequest()
709 706
710 707 @HasRepoPermissionAllDecorator('repository.admin')
711 708 @auth.CSRFRequired()
712 709 def repo_issuetracker_delete(self, repo_name):
713 710 uid = request.POST.get('uid')
714 711 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
715 712 try:
716 713 repo_settings.delete_entries(uid)
717 714 except Exception:
718 715 h.flash(_('Error occurred during deleting issue tracker entry'),
719 716 category='error')
720 717 else:
721 718 h.flash(_('Removed issue tracker entry'), category='success')
722 719 return redirect(url('repo_settings_issuetracker',
723 720 repo_name=repo_name))
724 721
725 722 def _update_patterns(self, form, repo_settings):
726 723 for uid in form['delete_patterns']:
727 724 repo_settings.delete_entries(uid)
728 725
729 726 for pattern in form['patterns']:
730 727 for setting, value, type_ in pattern:
731 728 sett = repo_settings.create_or_update_setting(
732 729 setting, value, type_)
733 730 Session().add(sett)
734 731
735 732 Session().commit()
736 733
737 734 @HasRepoPermissionAllDecorator('repository.admin')
738 735 @auth.CSRFRequired()
739 736 def repo_issuetracker_save(self, repo_name):
740 737 # Save inheritance
741 738 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
742 739 inherited = (request.POST.get('inherit_global_issuetracker')
743 740 == "inherited")
744 741 repo_settings.inherit_global_settings = inherited
745 742 Session().commit()
746 743
747 744 form = IssueTrackerPatternsForm()().to_python(request.POST)
748 745 if form:
749 746 self._update_patterns(form, repo_settings)
750 747
751 748 h.flash(_('Updated issue tracker entries'), category='success')
752 749 return redirect(url('repo_settings_issuetracker',
753 750 repo_name=repo_name))
754 751
755 752 @HasRepoPermissionAllDecorator('repository.admin')
756 753 def repo_issuetracker(self, repo_name):
757 754 """GET /admin/settings/issue-tracker: All items in the collection"""
758 755 c.active = 'issuetracker'
759 756 c.data = 'data'
760 757 c.repo_info = self._load_repo(repo_name)
761 758
762 759 repo = Repository.get_by_repo_name(repo_name)
763 760 c.settings_model = IssueTrackerSettingsModel(repo=repo)
764 761 c.global_patterns = c.settings_model.get_global_settings()
765 762 c.repo_patterns = c.settings_model.get_repo_settings()
766 763
767 764 return render('admin/repos/repo_edit.html')
768 765
769 766 @HasRepoPermissionAllDecorator('repository.admin')
770 767 def repo_settings_vcs(self, repo_name):
771 768 """GET /{repo_name}/settings/vcs/: All items in the collection"""
772 769
773 770 model = VcsSettingsModel(repo=repo_name)
774 771
775 772 c.active = 'vcs'
776 773 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
777 774 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
778 775 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
779 776 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
780 777 c.repo_info = self._load_repo(repo_name)
781 778 defaults = self._vcs_form_defaults(repo_name)
782 779 c.inherit_global_settings = defaults['inherit_global_settings']
783 780 c.labs_active = str2bool(
784 781 rhodecode.CONFIG.get('labs_settings_active', 'true'))
785 782
786 783 return htmlfill.render(
787 784 render('admin/repos/repo_edit.html'),
788 785 defaults=defaults,
789 786 encoding="UTF-8",
790 787 force_defaults=False)
791 788
792 789 @HasRepoPermissionAllDecorator('repository.admin')
793 790 @auth.CSRFRequired()
794 791 def repo_settings_vcs_update(self, repo_name):
795 792 """POST /{repo_name}/settings/vcs/: All items in the collection"""
796 793 c.active = 'vcs'
797 794
798 795 model = VcsSettingsModel(repo=repo_name)
799 796 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
800 797 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
801 798 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
802 799 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
803 800 c.repo_info = self._load_repo(repo_name)
804 801 defaults = self._vcs_form_defaults(repo_name)
805 802 c.inherit_global_settings = defaults['inherit_global_settings']
806 803
807 804 application_form = RepoVcsSettingsForm(repo_name)()
808 805 try:
809 806 form_result = application_form.to_python(dict(request.POST))
810 807 except formencode.Invalid as errors:
811 808 h.flash(
812 809 _("Some form inputs contain invalid data."),
813 810 category='error')
814 811 return htmlfill.render(
815 812 render('admin/repos/repo_edit.html'),
816 813 defaults=errors.value,
817 814 errors=errors.error_dict or {},
818 815 prefix_error=False,
819 816 encoding="UTF-8",
820 817 force_defaults=False
821 818 )
822 819
823 820 try:
824 821 inherit_global_settings = form_result['inherit_global_settings']
825 822 model.create_or_update_repo_settings(
826 823 form_result, inherit_global_settings=inherit_global_settings)
827 824 except Exception:
828 825 log.exception("Exception while updating settings")
829 826 h.flash(
830 827 _('Error occurred during updating repository VCS settings'),
831 828 category='error')
832 829 else:
833 830 Session().commit()
834 831 h.flash(_('Updated VCS settings'), category='success')
835 832 return redirect(url('repo_vcs_settings', repo_name=repo_name))
836 833
837 834 return htmlfill.render(
838 835 render('admin/repos/repo_edit.html'),
839 836 defaults=self._vcs_form_defaults(repo_name),
840 837 encoding="UTF-8",
841 838 force_defaults=False)
842 839
843 840 @HasRepoPermissionAllDecorator('repository.admin')
844 841 @auth.CSRFRequired()
845 842 @jsonify
846 843 def repo_delete_svn_pattern(self, repo_name):
847 844 if not request.is_xhr:
848 845 return False
849 846
850 847 delete_pattern_id = request.POST.get('delete_svn_pattern')
851 848 model = VcsSettingsModel(repo=repo_name)
852 849 try:
853 850 model.delete_repo_svn_pattern(delete_pattern_id)
854 851 except SettingNotFound:
855 852 raise HTTPBadRequest()
856 853
857 854 Session().commit()
858 855 return True
859 856
860 857 def _vcs_form_defaults(self, repo_name):
861 858 model = VcsSettingsModel(repo=repo_name)
862 859 global_defaults = model.get_global_settings()
863 860
864 861 repo_defaults = {}
865 862 repo_defaults.update(global_defaults)
866 863 repo_defaults.update(model.get_repo_settings())
867 864
868 865 global_defaults = {
869 866 '{}_inherited'.format(k): global_defaults[k]
870 867 for k in global_defaults}
871 868
872 869 defaults = {
873 870 'inherit_global_settings': model.inherit_global_settings
874 871 }
875 872 defaults.update(global_defaults)
876 873 defaults.update(repo_defaults)
877 874 defaults.update({
878 875 'new_svn_branch': '',
879 876 'new_svn_tag': '',
880 877 })
881 878 return defaults
@@ -1,807 +1,814 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 """
23 23 settings controller for rhodecode admin
24 24 """
25 25
26 26 import collections
27 27 import logging
28 28 import urllib2
29 29
30 30 import datetime
31 31 import formencode
32 32 from formencode import htmlfill
33 33 import packaging.version
34 34 from pylons import request, tmpl_context as c, url, config
35 35 from pylons.controllers.util import redirect
36 36 from pylons.i18n.translation import _, lazy_ugettext
37 37 from pyramid.threadlocal import get_current_registry
38 38 from webob.exc import HTTPBadRequest
39 39
40 40 import rhodecode
41 41 from rhodecode.admin.navigation import navigation_list
42 42 from rhodecode.lib import auth
43 43 from rhodecode.lib import helpers as h
44 44 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
45 45 from rhodecode.lib.base import BaseController, render
46 46 from rhodecode.lib.celerylib import tasks, run_task
47 47 from rhodecode.lib.utils import repo2db_mapper
48 48 from rhodecode.lib.utils2 import (
49 49 str2bool, safe_unicode, AttributeDict, safe_int)
50 50 from rhodecode.lib.compat import OrderedDict
51 51 from rhodecode.lib.ext_json import json
52 52 from rhodecode.lib.utils import jsonify
53 53
54 54 from rhodecode.model.db import RhodeCodeUi, Repository
55 55 from rhodecode.model.forms import ApplicationSettingsForm, \
56 56 ApplicationUiSettingsForm, ApplicationVisualisationForm, \
57 57 LabsSettingsForm, IssueTrackerPatternsForm
58 from rhodecode.model.repo_group import RepoGroupModel
58 59
59 60 from rhodecode.model.scm import ScmModel
60 61 from rhodecode.model.notification import EmailNotificationModel
61 62 from rhodecode.model.meta import Session
62 63 from rhodecode.model.settings import (
63 64 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
64 65 SettingsModel)
65 66
66 67 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
67 68 from rhodecode.svn_support.config_keys import generate_config
68 69
69 70
70 71 log = logging.getLogger(__name__)
71 72
72 73
73 74 class SettingsController(BaseController):
74 75 """REST Controller styled on the Atom Publishing Protocol"""
75 76 # To properly map this controller, ensure your config/routing.py
76 77 # file has a resource setup:
77 78 # map.resource('setting', 'settings', controller='admin/settings',
78 79 # path_prefix='/admin', name_prefix='admin_')
79 80
80 81 @LoginRequired()
81 82 def __before__(self):
82 83 super(SettingsController, self).__before__()
83 84 c.labs_active = str2bool(
84 85 rhodecode.CONFIG.get('labs_settings_active', 'true'))
85 86 c.navlist = navigation_list(request)
86 87
87 88 def _get_hg_ui_settings(self):
88 89 ret = RhodeCodeUi.query().all()
89 90
90 91 if not ret:
91 92 raise Exception('Could not get application ui settings !')
92 93 settings = {}
93 94 for each in ret:
94 95 k = each.ui_key
95 96 v = each.ui_value
96 97 if k == '/':
97 98 k = 'root_path'
98 99
99 100 if k in ['push_ssl', 'publish']:
100 101 v = str2bool(v)
101 102
102 103 if k.find('.') != -1:
103 104 k = k.replace('.', '_')
104 105
105 106 if each.ui_section in ['hooks', 'extensions']:
106 107 v = each.ui_active
107 108
108 109 settings[each.ui_section + '_' + k] = v
109 110 return settings
110 111
111 112 @HasPermissionAllDecorator('hg.admin')
112 113 @auth.CSRFRequired()
113 114 @jsonify
114 115 def delete_svn_pattern(self):
115 116 if not request.is_xhr:
116 117 raise HTTPBadRequest()
117 118
118 119 delete_pattern_id = request.POST.get('delete_svn_pattern')
119 120 model = VcsSettingsModel()
120 121 try:
121 122 model.delete_global_svn_pattern(delete_pattern_id)
122 123 except SettingNotFound:
123 124 raise HTTPBadRequest()
124 125
125 126 Session().commit()
126 127 return True
127 128
128 129 @HasPermissionAllDecorator('hg.admin')
129 130 @auth.CSRFRequired()
130 131 def settings_vcs_update(self):
131 132 """POST /admin/settings: All items in the collection"""
132 133 # url('admin_settings_vcs')
133 134 c.active = 'vcs'
134 135
135 136 model = VcsSettingsModel()
136 137 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
137 138 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
138 139
139 140 # TODO: Replace with request.registry after migrating to pyramid.
140 141 pyramid_settings = get_current_registry().settings
141 142 c.svn_proxy_generate_config = pyramid_settings[generate_config]
142 143
143 144 application_form = ApplicationUiSettingsForm()()
144 145
145 146 try:
146 147 form_result = application_form.to_python(dict(request.POST))
147 148 except formencode.Invalid as errors:
148 149 h.flash(
149 150 _("Some form inputs contain invalid data."),
150 151 category='error')
151 152 return htmlfill.render(
152 153 render('admin/settings/settings.html'),
153 154 defaults=errors.value,
154 155 errors=errors.error_dict or {},
155 156 prefix_error=False,
156 157 encoding="UTF-8",
157 158 force_defaults=False
158 159 )
159 160
160 161 try:
161 162 if c.visual.allow_repo_location_change:
162 163 model.update_global_path_setting(
163 164 form_result['paths_root_path'])
164 165
165 166 model.update_global_ssl_setting(form_result['web_push_ssl'])
166 167 model.update_global_hook_settings(form_result)
167 168
168 169 model.create_or_update_global_svn_settings(form_result)
169 170 model.create_or_update_global_hg_settings(form_result)
170 171 model.create_or_update_global_pr_settings(form_result)
171 172 except Exception:
172 173 log.exception("Exception while updating settings")
173 174 h.flash(_('Error occurred during updating '
174 175 'application settings'), category='error')
175 176 else:
176 177 Session().commit()
177 178 h.flash(_('Updated VCS settings'), category='success')
178 179 return redirect(url('admin_settings_vcs'))
179 180
180 181 return htmlfill.render(
181 182 render('admin/settings/settings.html'),
182 183 defaults=self._form_defaults(),
183 184 encoding="UTF-8",
184 185 force_defaults=False)
185 186
186 187 @HasPermissionAllDecorator('hg.admin')
187 188 def settings_vcs(self):
188 189 """GET /admin/settings: All items in the collection"""
189 190 # url('admin_settings_vcs')
190 191 c.active = 'vcs'
191 192 model = VcsSettingsModel()
192 193 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
193 194 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
194 195
195 196 # TODO: Replace with request.registry after migrating to pyramid.
196 197 pyramid_settings = get_current_registry().settings
197 198 c.svn_proxy_generate_config = pyramid_settings[generate_config]
198 199
199 200 return htmlfill.render(
200 201 render('admin/settings/settings.html'),
201 202 defaults=self._form_defaults(),
202 203 encoding="UTF-8",
203 204 force_defaults=False)
204 205
205 206 @HasPermissionAllDecorator('hg.admin')
206 207 @auth.CSRFRequired()
207 208 def settings_mapping_update(self):
208 209 """POST /admin/settings/mapping: All items in the collection"""
209 210 # url('admin_settings_mapping')
210 211 c.active = 'mapping'
211 212 rm_obsolete = request.POST.get('destroy', False)
212 213 invalidate_cache = request.POST.get('invalidate', False)
213 214 log.debug(
214 215 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
215 216
216 217 if invalidate_cache:
217 218 log.debug('invalidating all repositories cache')
218 219 for repo in Repository.get_all():
219 220 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
220 221
221 222 filesystem_repos = ScmModel().repo_scan()
222 223 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
223 224 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
224 225 h.flash(_('Repositories successfully '
225 226 'rescanned added: %s ; removed: %s') %
226 227 (_repr(added), _repr(removed)),
227 228 category='success')
228 229 return redirect(url('admin_settings_mapping'))
229 230
230 231 @HasPermissionAllDecorator('hg.admin')
231 232 def settings_mapping(self):
232 233 """GET /admin/settings/mapping: All items in the collection"""
233 234 # url('admin_settings_mapping')
234 235 c.active = 'mapping'
235 236
236 237 return htmlfill.render(
237 238 render('admin/settings/settings.html'),
238 239 defaults=self._form_defaults(),
239 240 encoding="UTF-8",
240 241 force_defaults=False)
241 242
242 243 @HasPermissionAllDecorator('hg.admin')
243 244 @auth.CSRFRequired()
244 245 def settings_global_update(self):
245 246 """POST /admin/settings/global: All items in the collection"""
246 247 # url('admin_settings_global')
247 248 c.active = 'global'
249 c.personal_repo_group_default_pattern = RepoGroupModel()\
250 .get_personal_group_name_pattern()
248 251 application_form = ApplicationSettingsForm()()
249 252 try:
250 253 form_result = application_form.to_python(dict(request.POST))
251 254 except formencode.Invalid as errors:
252 255 return htmlfill.render(
253 256 render('admin/settings/settings.html'),
254 257 defaults=errors.value,
255 258 errors=errors.error_dict or {},
256 259 prefix_error=False,
257 260 encoding="UTF-8",
258 261 force_defaults=False)
259 262
260 263 try:
261 264 settings = [
262 ('title', 'rhodecode_title'),
263 ('realm', 'rhodecode_realm'),
264 ('pre_code', 'rhodecode_pre_code'),
265 ('post_code', 'rhodecode_post_code'),
266 ('captcha_public_key', 'rhodecode_captcha_public_key'),
267 ('captcha_private_key', 'rhodecode_captcha_private_key'),
265 ('title', 'rhodecode_title', 'unicode'),
266 ('realm', 'rhodecode_realm', 'unicode'),
267 ('pre_code', 'rhodecode_pre_code', 'unicode'),
268 ('post_code', 'rhodecode_post_code', 'unicode'),
269 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
270 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
271 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
272 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
268 273 ]
269 for setting, form_key in settings:
274 for setting, form_key, type_ in settings:
270 275 sett = SettingsModel().create_or_update_setting(
271 setting, form_result[form_key])
276 setting, form_result[form_key], type_)
272 277 Session().add(sett)
273 278
274 279 Session().commit()
275 280 SettingsModel().invalidate_settings_cache()
276 281 h.flash(_('Updated application settings'), category='success')
277 282 except Exception:
278 283 log.exception("Exception while updating application settings")
279 284 h.flash(
280 285 _('Error occurred during updating application settings'),
281 286 category='error')
282 287
283 288 return redirect(url('admin_settings_global'))
284 289
285 290 @HasPermissionAllDecorator('hg.admin')
286 291 def settings_global(self):
287 292 """GET /admin/settings/global: All items in the collection"""
288 293 # url('admin_settings_global')
289 294 c.active = 'global'
295 c.personal_repo_group_default_pattern = RepoGroupModel()\
296 .get_personal_group_name_pattern()
290 297
291 298 return htmlfill.render(
292 299 render('admin/settings/settings.html'),
293 300 defaults=self._form_defaults(),
294 301 encoding="UTF-8",
295 302 force_defaults=False)
296 303
297 304 @HasPermissionAllDecorator('hg.admin')
298 305 @auth.CSRFRequired()
299 306 def settings_visual_update(self):
300 307 """POST /admin/settings/visual: All items in the collection"""
301 308 # url('admin_settings_visual')
302 309 c.active = 'visual'
303 310 application_form = ApplicationVisualisationForm()()
304 311 try:
305 312 form_result = application_form.to_python(dict(request.POST))
306 313 except formencode.Invalid as errors:
307 314 return htmlfill.render(
308 315 render('admin/settings/settings.html'),
309 316 defaults=errors.value,
310 317 errors=errors.error_dict or {},
311 318 prefix_error=False,
312 319 encoding="UTF-8",
313 320 force_defaults=False
314 321 )
315 322
316 323 try:
317 324 settings = [
318 325 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
319 326 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
320 327 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
321 328 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
322 329 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
323 330 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
324 331 ('show_version', 'rhodecode_show_version', 'bool'),
325 332 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
326 333 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
327 334 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
328 335 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
329 336 ('support_url', 'rhodecode_support_url', 'unicode'),
330 337 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
331 338 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
332 339 ]
333 340 for setting, form_key, type_ in settings:
334 341 sett = SettingsModel().create_or_update_setting(
335 342 setting, form_result[form_key], type_)
336 343 Session().add(sett)
337 344
338 345 Session().commit()
339 346 SettingsModel().invalidate_settings_cache()
340 347 h.flash(_('Updated visualisation settings'), category='success')
341 348 except Exception:
342 349 log.exception("Exception updating visualization settings")
343 350 h.flash(_('Error occurred during updating '
344 351 'visualisation settings'),
345 352 category='error')
346 353
347 354 return redirect(url('admin_settings_visual'))
348 355
349 356 @HasPermissionAllDecorator('hg.admin')
350 357 def settings_visual(self):
351 358 """GET /admin/settings/visual: All items in the collection"""
352 359 # url('admin_settings_visual')
353 360 c.active = 'visual'
354 361
355 362 return htmlfill.render(
356 363 render('admin/settings/settings.html'),
357 364 defaults=self._form_defaults(),
358 365 encoding="UTF-8",
359 366 force_defaults=False)
360 367
361 368 @HasPermissionAllDecorator('hg.admin')
362 369 @auth.CSRFRequired()
363 370 def settings_issuetracker_test(self):
364 371 if request.is_xhr:
365 372 return h.urlify_commit_message(
366 373 request.POST.get('test_text', ''),
367 374 'repo_group/test_repo1')
368 375 else:
369 376 raise HTTPBadRequest()
370 377
371 378 @HasPermissionAllDecorator('hg.admin')
372 379 @auth.CSRFRequired()
373 380 def settings_issuetracker_delete(self):
374 381 uid = request.POST.get('uid')
375 382 IssueTrackerSettingsModel().delete_entries(uid)
376 383 h.flash(_('Removed issue tracker entry'), category='success')
377 384 return redirect(url('admin_settings_issuetracker'))
378 385
379 386 @HasPermissionAllDecorator('hg.admin')
380 387 def settings_issuetracker(self):
381 388 """GET /admin/settings/issue-tracker: All items in the collection"""
382 389 # url('admin_settings_issuetracker')
383 390 c.active = 'issuetracker'
384 391 defaults = SettingsModel().get_all_settings()
385 392
386 393 entry_key = 'rhodecode_issuetracker_pat_'
387 394
388 395 c.issuetracker_entries = {}
389 396 for k, v in defaults.items():
390 397 if k.startswith(entry_key):
391 398 uid = k[len(entry_key):]
392 399 c.issuetracker_entries[uid] = None
393 400
394 401 for uid in c.issuetracker_entries:
395 402 c.issuetracker_entries[uid] = AttributeDict({
396 403 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
397 404 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
398 405 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
399 406 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
400 407 })
401 408
402 409 return render('admin/settings/settings.html')
403 410
404 411 @HasPermissionAllDecorator('hg.admin')
405 412 @auth.CSRFRequired()
406 413 def settings_issuetracker_save(self):
407 414 settings_model = IssueTrackerSettingsModel()
408 415
409 416 form = IssueTrackerPatternsForm()().to_python(request.POST)
410 417 if form:
411 418 for uid in form.get('delete_patterns', []):
412 419 settings_model.delete_entries(uid)
413 420
414 421 for pattern in form.get('patterns', []):
415 422 for setting, value, type_ in pattern:
416 423 sett = settings_model.create_or_update_setting(
417 424 setting, value, type_)
418 425 Session().add(sett)
419 426
420 427 Session().commit()
421 428
422 429 SettingsModel().invalidate_settings_cache()
423 430 h.flash(_('Updated issue tracker entries'), category='success')
424 431 return redirect(url('admin_settings_issuetracker'))
425 432
426 433 @HasPermissionAllDecorator('hg.admin')
427 434 @auth.CSRFRequired()
428 435 def settings_email_update(self):
429 436 """POST /admin/settings/email: All items in the collection"""
430 437 # url('admin_settings_email')
431 438 c.active = 'email'
432 439
433 440 test_email = request.POST.get('test_email')
434 441
435 442 if not test_email:
436 443 h.flash(_('Please enter email address'), category='error')
437 444 return redirect(url('admin_settings_email'))
438 445
439 446 email_kwargs = {
440 447 'date': datetime.datetime.now(),
441 448 'user': c.rhodecode_user,
442 449 'rhodecode_version': c.rhodecode_version
443 450 }
444 451
445 452 (subject, headers, email_body,
446 453 email_body_plaintext) = EmailNotificationModel().render_email(
447 454 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
448 455
449 456 recipients = [test_email] if test_email else None
450 457
451 458 run_task(tasks.send_email, recipients, subject,
452 459 email_body_plaintext, email_body)
453 460
454 461 h.flash(_('Send email task created'), category='success')
455 462 return redirect(url('admin_settings_email'))
456 463
457 464 @HasPermissionAllDecorator('hg.admin')
458 465 def settings_email(self):
459 466 """GET /admin/settings/email: All items in the collection"""
460 467 # url('admin_settings_email')
461 468 c.active = 'email'
462 469 c.rhodecode_ini = rhodecode.CONFIG
463 470
464 471 return htmlfill.render(
465 472 render('admin/settings/settings.html'),
466 473 defaults=self._form_defaults(),
467 474 encoding="UTF-8",
468 475 force_defaults=False)
469 476
470 477 @HasPermissionAllDecorator('hg.admin')
471 478 @auth.CSRFRequired()
472 479 def settings_hooks_update(self):
473 480 """POST or DELETE /admin/settings/hooks: All items in the collection"""
474 481 # url('admin_settings_hooks')
475 482 c.active = 'hooks'
476 483 if c.visual.allow_custom_hooks_settings:
477 484 ui_key = request.POST.get('new_hook_ui_key')
478 485 ui_value = request.POST.get('new_hook_ui_value')
479 486
480 487 hook_id = request.POST.get('hook_id')
481 488 new_hook = False
482 489
483 490 model = SettingsModel()
484 491 try:
485 492 if ui_value and ui_key:
486 493 model.create_or_update_hook(ui_key, ui_value)
487 494 h.flash(_('Added new hook'), category='success')
488 495 new_hook = True
489 496 elif hook_id:
490 497 RhodeCodeUi.delete(hook_id)
491 498 Session().commit()
492 499
493 500 # check for edits
494 501 update = False
495 502 _d = request.POST.dict_of_lists()
496 503 for k, v in zip(_d.get('hook_ui_key', []),
497 504 _d.get('hook_ui_value_new', [])):
498 505 model.create_or_update_hook(k, v)
499 506 update = True
500 507
501 508 if update and not new_hook:
502 509 h.flash(_('Updated hooks'), category='success')
503 510 Session().commit()
504 511 except Exception:
505 512 log.exception("Exception during hook creation")
506 513 h.flash(_('Error occurred during hook creation'),
507 514 category='error')
508 515
509 516 return redirect(url('admin_settings_hooks'))
510 517
511 518 @HasPermissionAllDecorator('hg.admin')
512 519 def settings_hooks(self):
513 520 """GET /admin/settings/hooks: All items in the collection"""
514 521 # url('admin_settings_hooks')
515 522 c.active = 'hooks'
516 523
517 524 model = SettingsModel()
518 525 c.hooks = model.get_builtin_hooks()
519 526 c.custom_hooks = model.get_custom_hooks()
520 527
521 528 return htmlfill.render(
522 529 render('admin/settings/settings.html'),
523 530 defaults=self._form_defaults(),
524 531 encoding="UTF-8",
525 532 force_defaults=False)
526 533
527 534 @HasPermissionAllDecorator('hg.admin')
528 535 def settings_search(self):
529 536 """GET /admin/settings/search: All items in the collection"""
530 537 # url('admin_settings_search')
531 538 c.active = 'search'
532 539
533 540 from rhodecode.lib.index import searcher_from_config
534 541 searcher = searcher_from_config(config)
535 542 c.statistics = searcher.statistics()
536 543
537 544 return render('admin/settings/settings.html')
538 545
539 546 @HasPermissionAllDecorator('hg.admin')
540 547 def settings_system(self):
541 548 """GET /admin/settings/system: All items in the collection"""
542 549 # url('admin_settings_system')
543 550 snapshot = str2bool(request.GET.get('snapshot'))
544 551 c.active = 'system'
545 552
546 553 defaults = self._form_defaults()
547 554 c.rhodecode_ini = rhodecode.CONFIG
548 555 c.rhodecode_update_url = defaults.get('rhodecode_update_url')
549 556 server_info = ScmModel().get_server_info(request.environ)
550 557 for key, val in server_info.iteritems():
551 558 setattr(c, key, val)
552 559
553 560 if c.disk['percent'] > 90:
554 561 h.flash(h.literal(_(
555 562 'Critical: your disk space is very low <b>%s%%</b> used' %
556 563 c.disk['percent'])), 'error')
557 564 elif c.disk['percent'] > 70:
558 565 h.flash(h.literal(_(
559 566 'Warning: your disk space is running low <b>%s%%</b> used' %
560 567 c.disk['percent'])), 'warning')
561 568
562 569 try:
563 570 c.uptime_age = h._age(
564 571 h.time_to_datetime(c.boot_time), False, show_suffix=False)
565 572 except TypeError:
566 573 c.uptime_age = c.boot_time
567 574
568 575 try:
569 576 c.system_memory = '%s/%s, %s%% (%s%%) used%s' % (
570 577 h.format_byte_size_binary(c.memory['used']),
571 578 h.format_byte_size_binary(c.memory['total']),
572 579 c.memory['percent2'],
573 580 c.memory['percent'],
574 581 ' %s' % c.memory['error'] if 'error' in c.memory else '')
575 582 except TypeError:
576 583 c.system_memory = 'NOT AVAILABLE'
577 584
578 585 rhodecode_ini_safe = rhodecode.CONFIG.copy()
579 586 blacklist = [
580 587 'rhodecode_license_key',
581 588 'routes.map',
582 589 'pylons.h',
583 590 'pylons.app_globals',
584 591 'pylons.environ_config',
585 592 'sqlalchemy.db1.url',
586 593 ('app_conf', 'sqlalchemy.db1.url')
587 594 ]
588 595 for k in blacklist:
589 596 if isinstance(k, tuple):
590 597 section, key = k
591 598 if section in rhodecode_ini_safe:
592 599 rhodecode_ini_safe[section].pop(key, None)
593 600 else:
594 601 rhodecode_ini_safe.pop(k, None)
595 602
596 603 c.rhodecode_ini_safe = rhodecode_ini_safe
597 604
598 605 # TODO: marcink, figure out how to allow only selected users to do this
599 606 c.allowed_to_snapshot = False
600 607
601 608 if snapshot:
602 609 if c.allowed_to_snapshot:
603 610 return render('admin/settings/settings_system_snapshot.html')
604 611 else:
605 612 h.flash('You are not allowed to do this', category='warning')
606 613
607 614 return htmlfill.render(
608 615 render('admin/settings/settings.html'),
609 616 defaults=defaults,
610 617 encoding="UTF-8",
611 618 force_defaults=False)
612 619
613 620 @staticmethod
614 621 def get_update_data(update_url):
615 622 """Return the JSON update data."""
616 623 ver = rhodecode.__version__
617 624 log.debug('Checking for upgrade on `%s` server', update_url)
618 625 opener = urllib2.build_opener()
619 626 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
620 627 response = opener.open(update_url)
621 628 response_data = response.read()
622 629 data = json.loads(response_data)
623 630
624 631 return data
625 632
626 633 @HasPermissionAllDecorator('hg.admin')
627 634 def settings_system_update(self):
628 635 """GET /admin/settings/system/updates: All items in the collection"""
629 636 # url('admin_settings_system_update')
630 637 defaults = self._form_defaults()
631 638 update_url = defaults.get('rhodecode_update_url', '')
632 639
633 640 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
634 641 try:
635 642 data = self.get_update_data(update_url)
636 643 except urllib2.URLError as e:
637 644 log.exception("Exception contacting upgrade server")
638 645 return _err('Failed to contact upgrade server: %r' % e)
639 646 except ValueError as e:
640 647 log.exception("Bad data sent from update server")
641 648 return _err('Bad data sent from update server')
642 649
643 650 latest = data['versions'][0]
644 651
645 652 c.update_url = update_url
646 653 c.latest_data = latest
647 654 c.latest_ver = latest['version']
648 655 c.cur_ver = rhodecode.__version__
649 656 c.should_upgrade = False
650 657
651 658 if (packaging.version.Version(c.latest_ver) >
652 659 packaging.version.Version(c.cur_ver)):
653 660 c.should_upgrade = True
654 661 c.important_notices = latest['general']
655 662
656 663 return render('admin/settings/settings_system_update.html')
657 664
658 665 @HasPermissionAllDecorator('hg.admin')
659 666 def settings_supervisor(self):
660 667 c.rhodecode_ini = rhodecode.CONFIG
661 668 c.active = 'supervisor'
662 669
663 670 c.supervisor_procs = OrderedDict([
664 671 (SUPERVISOR_MASTER, {}),
665 672 ])
666 673
667 674 c.log_size = 10240
668 675 supervisor = SupervisorModel()
669 676
670 677 _connection = supervisor.get_connection(
671 678 c.rhodecode_ini.get('supervisor.uri'))
672 679 c.connection_error = None
673 680 try:
674 681 _connection.supervisor.getAllProcessInfo()
675 682 except Exception as e:
676 683 c.connection_error = str(e)
677 684 log.exception("Exception reading supervisor data")
678 685 return render('admin/settings/settings.html')
679 686
680 687 groupid = c.rhodecode_ini.get('supervisor.group_id')
681 688
682 689 # feed our group processes to the main
683 690 for proc in supervisor.get_group_processes(_connection, groupid):
684 691 c.supervisor_procs[proc['name']] = {}
685 692
686 693 for k in c.supervisor_procs.keys():
687 694 try:
688 695 # master process info
689 696 if k == SUPERVISOR_MASTER:
690 697 _data = supervisor.get_master_state(_connection)
691 698 _data['name'] = 'supervisor master'
692 699 _data['description'] = 'pid %s, id: %s, ver: %s' % (
693 700 _data['pid'], _data['id'], _data['ver'])
694 701 c.supervisor_procs[k] = _data
695 702 else:
696 703 procid = groupid + ":" + k
697 704 c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid)
698 705 except Exception as e:
699 706 log.exception("Exception reading supervisor data")
700 707 c.supervisor_procs[k] = {'_rhodecode_error': str(e)}
701 708
702 709 return render('admin/settings/settings.html')
703 710
704 711 @HasPermissionAllDecorator('hg.admin')
705 712 def settings_supervisor_log(self, procid):
706 713 import rhodecode
707 714 c.rhodecode_ini = rhodecode.CONFIG
708 715 c.active = 'supervisor_tail'
709 716
710 717 supervisor = SupervisorModel()
711 718 _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri'))
712 719 groupid = c.rhodecode_ini.get('supervisor.group_id')
713 720 procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid
714 721
715 722 c.log_size = 10240
716 723 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1
717 724 c.log = supervisor.read_process_log(_connection, procid, offset, 0)
718 725
719 726 return render('admin/settings/settings.html')
720 727
721 728 @HasPermissionAllDecorator('hg.admin')
722 729 @auth.CSRFRequired()
723 730 def settings_labs_update(self):
724 731 """POST /admin/settings/labs: All items in the collection"""
725 732 # url('admin_settings/labs', method={'POST'})
726 733 c.active = 'labs'
727 734
728 735 application_form = LabsSettingsForm()()
729 736 try:
730 737 form_result = application_form.to_python(dict(request.POST))
731 738 except formencode.Invalid as errors:
732 739 h.flash(
733 740 _('Some form inputs contain invalid data.'),
734 741 category='error')
735 742 return htmlfill.render(
736 743 render('admin/settings/settings.html'),
737 744 defaults=errors.value,
738 745 errors=errors.error_dict or {},
739 746 prefix_error=False,
740 747 encoding='UTF-8',
741 748 force_defaults=False
742 749 )
743 750
744 751 try:
745 752 session = Session()
746 753 for setting in _LAB_SETTINGS:
747 754 setting_name = setting.key[len('rhodecode_'):]
748 755 sett = SettingsModel().create_or_update_setting(
749 756 setting_name, form_result[setting.key], setting.type)
750 757 session.add(sett)
751 758
752 759 except Exception:
753 760 log.exception('Exception while updating lab settings')
754 761 h.flash(_('Error occurred during updating labs settings'),
755 762 category='error')
756 763 else:
757 764 Session().commit()
758 765 SettingsModel().invalidate_settings_cache()
759 766 h.flash(_('Updated Labs settings'), category='success')
760 767 return redirect(url('admin_settings_labs'))
761 768
762 769 return htmlfill.render(
763 770 render('admin/settings/settings.html'),
764 771 defaults=self._form_defaults(),
765 772 encoding='UTF-8',
766 773 force_defaults=False)
767 774
768 775 @HasPermissionAllDecorator('hg.admin')
769 776 def settings_labs(self):
770 777 """GET /admin/settings/labs: All items in the collection"""
771 778 # url('admin_settings_labs')
772 779 if not c.labs_active:
773 780 redirect(url('admin_settings'))
774 781
775 782 c.active = 'labs'
776 783 c.lab_settings = _LAB_SETTINGS
777 784
778 785 return htmlfill.render(
779 786 render('admin/settings/settings.html'),
780 787 defaults=self._form_defaults(),
781 788 encoding='UTF-8',
782 789 force_defaults=False)
783 790
784 791 def _form_defaults(self):
785 792 defaults = SettingsModel().get_all_settings()
786 793 defaults.update(self._get_hg_ui_settings())
787 794 defaults.update({
788 795 'new_svn_branch': '',
789 796 'new_svn_tag': '',
790 797 })
791 798 return defaults
792 799
793 800
794 801 # :param key: name of the setting including the 'rhodecode_' prefix
795 802 # :param type: the RhodeCodeSetting type to use.
796 803 # :param group: the i18ned group in which we should dispaly this setting
797 804 # :param label: the i18ned label we should display for this setting
798 805 # :param help: the i18ned help we should dispaly for this setting
799 806 LabSetting = collections.namedtuple(
800 807 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
801 808
802 809
803 810 # This list has to be kept in sync with the form
804 811 # rhodecode.model.forms.LabsSettingsForm.
805 812 _LAB_SETTINGS = [
806 813
807 814 ]
@@ -1,714 +1,748 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 crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 from formencode import htmlfill
29 29 from pylons import request, tmpl_context as c, url, config
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34 from rhodecode.lib.exceptions import (
35 35 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
36 36 UserOwnsUserGroupsException, UserCreationError)
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import auth
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token)
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43
44 44 from rhodecode.model.db import (
45 45 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
46 46 from rhodecode.model.forms import (
47 47 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
48 from rhodecode.model.repo_group import RepoGroupModel
48 49 from rhodecode.model.user import UserModel
49 50 from rhodecode.model.meta import Session
50 51 from rhodecode.model.permission import PermissionModel
51 52 from rhodecode.lib.utils import action_logger
52 53 from rhodecode.lib.ext_json import json
53 from rhodecode.lib.utils2 import datetime_to_time, safe_int
54 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
54 55
55 56 log = logging.getLogger(__name__)
56 57
57 58
58 59 class UsersController(BaseController):
59 60 """REST Controller styled on the Atom Publishing Protocol"""
60 61
61 62 @LoginRequired()
62 63 def __before__(self):
63 64 super(UsersController, self).__before__()
64 65 c.available_permissions = config['available_permissions']
65 66 c.allowed_languages = [
66 67 ('en', 'English (en)'),
67 68 ('de', 'German (de)'),
68 69 ('fr', 'French (fr)'),
69 70 ('it', 'Italian (it)'),
70 71 ('ja', 'Japanese (ja)'),
71 72 ('pl', 'Polish (pl)'),
72 73 ('pt', 'Portuguese (pt)'),
73 74 ('ru', 'Russian (ru)'),
74 75 ('zh', 'Chinese (zh)'),
75 76 ]
76 77 PermissionModel().set_global_permission_choices(c, translator=_)
77 78
78 79 @HasPermissionAllDecorator('hg.admin')
79 80 def index(self):
80 81 """GET /users: All items in the collection"""
81 82 # url('users')
82 83
83 84 from rhodecode.lib.utils import PartialRenderer
84 85 _render = PartialRenderer('data_table/_dt_elements.html')
85 86
86 87 def username(user_id, username):
87 88 return _render("user_name", user_id, username)
88 89
89 90 def user_actions(user_id, username):
90 91 return _render("user_actions", user_id, username)
91 92
92 93 # json generate
93 94 c.users_list = User.query()\
94 95 .filter(User.username != User.DEFAULT_USER) \
95 96 .all()
96 97
97 98 users_data = []
98 99 for user in c.users_list:
99 100 users_data.append({
100 101 "username": h.gravatar_with_user(user.username),
101 102 "username_raw": user.username,
102 103 "email": user.email,
103 104 "first_name": h.escape(user.name),
104 105 "last_name": h.escape(user.lastname),
105 106 "last_login": h.format_date(user.last_login),
106 107 "last_login_raw": datetime_to_time(user.last_login),
107 108 "last_activity": h.format_date(
108 109 h.time_to_datetime(user.user_data.get('last_activity', 0))),
109 110 "last_activity_raw": user.user_data.get('last_activity', 0),
110 111 "active": h.bool2icon(user.active),
111 112 "active_raw": user.active,
112 113 "admin": h.bool2icon(user.admin),
113 114 "admin_raw": user.admin,
114 115 "extern_type": user.extern_type,
115 116 "extern_name": user.extern_name,
116 117 "action": user_actions(user.user_id, user.username),
117 118 })
118 119
119 120
120 121 c.data = json.dumps(users_data)
121 122 return render('admin/users/users.html')
122 123
124 def _get_personal_repo_group_template_vars(self):
125 DummyUser = AttributeDict({
126 'username': '${username}',
127 'user_id': '${user_id}',
128 })
129 c.default_create_repo_group = RepoGroupModel() \
130 .get_default_create_personal_repo_group()
131 c.personal_repo_group_name = RepoGroupModel() \
132 .get_personal_group_name(DummyUser)
133
123 134 @HasPermissionAllDecorator('hg.admin')
124 135 @auth.CSRFRequired()
125 136 def create(self):
126 137 """POST /users: Create a new item"""
127 138 # url('users')
128 139 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
129 140 user_model = UserModel()
130 141 user_form = UserForm()()
131 142 try:
132 143 form_result = user_form.to_python(dict(request.POST))
133 144 user = user_model.create(form_result)
134 145 Session().flush()
135 146 username = form_result['username']
136 147 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
137 148 None, self.ip_addr, self.sa)
138 149
139 150 user_link = h.link_to(h.escape(username),
140 151 url('edit_user',
141 152 user_id=user.user_id))
142 153 h.flash(h.literal(_('Created user %(user_link)s')
143 154 % {'user_link': user_link}), category='success')
144 155 Session().commit()
145 156 except formencode.Invalid as errors:
157 self._get_personal_repo_group_template_vars()
146 158 return htmlfill.render(
147 159 render('admin/users/user_add.html'),
148 160 defaults=errors.value,
149 161 errors=errors.error_dict or {},
150 162 prefix_error=False,
151 163 encoding="UTF-8",
152 164 force_defaults=False)
153 165 except UserCreationError as e:
154 166 h.flash(e, 'error')
155 167 except Exception:
156 168 log.exception("Exception creation of user")
157 169 h.flash(_('Error occurred during creation of user %s')
158 170 % request.POST.get('username'), category='error')
159 171 return redirect(url('users'))
160 172
161 173 @HasPermissionAllDecorator('hg.admin')
162 174 def new(self):
163 175 """GET /users/new: Form to create a new item"""
164 176 # url('new_user')
165 177 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
178 self._get_personal_repo_group_template_vars()
166 179 return render('admin/users/user_add.html')
167 180
168 181 @HasPermissionAllDecorator('hg.admin')
169 182 @auth.CSRFRequired()
170 183 def update(self, user_id):
171 184 """PUT /users/user_id: Update an existing item"""
172 185 # Forms posted to this method should contain a hidden field:
173 186 # <input type="hidden" name="_method" value="PUT" />
174 187 # Or using helpers:
175 188 # h.form(url('update_user', user_id=ID),
176 189 # method='put')
177 190 # url('user', user_id=ID)
178 191 user_id = safe_int(user_id)
179 192 c.user = User.get_or_404(user_id)
180 193 c.active = 'profile'
181 194 c.extern_type = c.user.extern_type
182 195 c.extern_name = c.user.extern_name
183 196 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
184 197 available_languages = [x[0] for x in c.allowed_languages]
185 198 _form = UserForm(edit=True, available_languages=available_languages,
186 199 old_data={'user_id': user_id,
187 200 'email': c.user.email})()
188 201 form_result = {}
189 202 try:
190 203 form_result = _form.to_python(dict(request.POST))
191 204 skip_attrs = ['extern_type', 'extern_name']
192 205 # TODO: plugin should define if username can be updated
193 206 if c.extern_type != "rhodecode":
194 207 # forbid updating username for external accounts
195 208 skip_attrs.append('username')
196 209
197 210 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
198 211 usr = form_result['username']
199 212 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
200 213 None, self.ip_addr, self.sa)
201 214 h.flash(_('User updated successfully'), category='success')
202 215 Session().commit()
203 216 except formencode.Invalid as errors:
204 217 defaults = errors.value
205 218 e = errors.error_dict or {}
206 219
207 220 return htmlfill.render(
208 221 render('admin/users/user_edit.html'),
209 222 defaults=defaults,
210 223 errors=e,
211 224 prefix_error=False,
212 225 encoding="UTF-8",
213 226 force_defaults=False)
214 227 except UserCreationError as e:
215 228 h.flash(e, 'error')
216 229 except Exception:
217 230 log.exception("Exception updating user")
218 231 h.flash(_('Error occurred during update of user %s')
219 232 % form_result.get('username'), category='error')
220 233 return redirect(url('edit_user', user_id=user_id))
221 234
222 235 @HasPermissionAllDecorator('hg.admin')
223 236 @auth.CSRFRequired()
224 237 def delete(self, user_id):
225 238 """DELETE /users/user_id: Delete an existing item"""
226 239 # Forms posted to this method should contain a hidden field:
227 240 # <input type="hidden" name="_method" value="DELETE" />
228 241 # Or using helpers:
229 242 # h.form(url('delete_user', user_id=ID),
230 243 # method='delete')
231 244 # url('user', user_id=ID)
232 245 user_id = safe_int(user_id)
233 246 c.user = User.get_or_404(user_id)
234 247
235 248 _repos = c.user.repositories
236 249 _repo_groups = c.user.repository_groups
237 250 _user_groups = c.user.user_groups
238 251
239 252 handle_repos = None
240 253 handle_repo_groups = None
241 254 handle_user_groups = None
242 255 # dummy call for flash of handle
243 256 set_handle_flash_repos = lambda: None
244 257 set_handle_flash_repo_groups = lambda: None
245 258 set_handle_flash_user_groups = lambda: None
246 259
247 260 if _repos and request.POST.get('user_repos'):
248 261 do = request.POST['user_repos']
249 262 if do == 'detach':
250 263 handle_repos = 'detach'
251 264 set_handle_flash_repos = lambda: h.flash(
252 265 _('Detached %s repositories') % len(_repos),
253 266 category='success')
254 267 elif do == 'delete':
255 268 handle_repos = 'delete'
256 269 set_handle_flash_repos = lambda: h.flash(
257 270 _('Deleted %s repositories') % len(_repos),
258 271 category='success')
259 272
260 273 if _repo_groups and request.POST.get('user_repo_groups'):
261 274 do = request.POST['user_repo_groups']
262 275 if do == 'detach':
263 276 handle_repo_groups = 'detach'
264 277 set_handle_flash_repo_groups = lambda: h.flash(
265 278 _('Detached %s repository groups') % len(_repo_groups),
266 279 category='success')
267 280 elif do == 'delete':
268 281 handle_repo_groups = 'delete'
269 282 set_handle_flash_repo_groups = lambda: h.flash(
270 283 _('Deleted %s repository groups') % len(_repo_groups),
271 284 category='success')
272 285
273 286 if _user_groups and request.POST.get('user_user_groups'):
274 287 do = request.POST['user_user_groups']
275 288 if do == 'detach':
276 289 handle_user_groups = 'detach'
277 290 set_handle_flash_user_groups = lambda: h.flash(
278 291 _('Detached %s user groups') % len(_user_groups),
279 292 category='success')
280 293 elif do == 'delete':
281 294 handle_user_groups = 'delete'
282 295 set_handle_flash_user_groups = lambda: h.flash(
283 296 _('Deleted %s user groups') % len(_user_groups),
284 297 category='success')
285 298
286 299 try:
287 300 UserModel().delete(c.user, handle_repos=handle_repos,
288 301 handle_repo_groups=handle_repo_groups,
289 302 handle_user_groups=handle_user_groups)
290 303 Session().commit()
291 304 set_handle_flash_repos()
292 305 set_handle_flash_repo_groups()
293 306 set_handle_flash_user_groups()
294 307 h.flash(_('Successfully deleted user'), category='success')
295 308 except (UserOwnsReposException, UserOwnsRepoGroupsException,
296 309 UserOwnsUserGroupsException, DefaultUserException) as e:
297 310 h.flash(e, category='warning')
298 311 except Exception:
299 312 log.exception("Exception during deletion of user")
300 313 h.flash(_('An error occurred during deletion of user'),
301 314 category='error')
302 315 return redirect(url('users'))
303 316
304 317 @HasPermissionAllDecorator('hg.admin')
305 318 @auth.CSRFRequired()
306 319 def reset_password(self, user_id):
307 320 """
308 321 toggle reset password flag for this user
309 322
310 323 :param user_id:
311 324 """
312 325 user_id = safe_int(user_id)
313 326 c.user = User.get_or_404(user_id)
314 327 try:
315 328 old_value = c.user.user_data.get('force_password_change')
316 329 c.user.update_userdata(force_password_change=not old_value)
317 330 Session().commit()
318 331 if old_value:
319 332 msg = _('Force password change disabled for user')
320 333 else:
321 334 msg = _('Force password change enabled for user')
322 335 h.flash(msg, category='success')
323 336 except Exception:
324 337 log.exception("Exception during password reset for user")
325 338 h.flash(_('An error occurred during password reset for user'),
326 339 category='error')
327 340
328 341 return redirect(url('edit_user_advanced', user_id=user_id))
329 342
330 343 @HasPermissionAllDecorator('hg.admin')
331 344 @auth.CSRFRequired()
332 345 def create_personal_repo_group(self, user_id):
333 346 """
334 347 Create personal repository group for this user
335 348
336 349 :param user_id:
337 350 """
338 351 from rhodecode.model.repo_group import RepoGroupModel
339 352
340 353 user_id = safe_int(user_id)
341 354 c.user = User.get_or_404(user_id)
355 personal_repo_group = RepoGroup.get_user_personal_repo_group(
356 c.user.user_id)
357 if personal_repo_group:
358 return redirect(url('edit_user_advanced', user_id=user_id))
342 359
360 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
361 c.user)
362 named_personal_group = RepoGroup.get_by_group_name(
363 personal_repo_group_name)
343 364 try:
344 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {
345 'username': c.user.username}
346 if not RepoGroup.get_by_group_name(c.user.username):
347 RepoGroupModel().create(group_name=c.user.username,
348 group_description=desc,
349 owner=c.user.username)
350 365
351 msg = _('Created repository group `%s`' % (c.user.username,))
366 if named_personal_group and named_personal_group.user_id == c.user.user_id:
367 # migrate the same named group, and mark it as personal
368 named_personal_group.personal = True
369 Session().add(named_personal_group)
370 Session().commit()
371 msg = _('Linked repository group `%s` as personal' % (
372 personal_repo_group_name,))
352 373 h.flash(msg, category='success')
374 elif not named_personal_group:
375 RepoGroupModel().create_personal_repo_group(c.user)
376
377 msg = _('Created repository group `%s`' % (
378 personal_repo_group_name,))
379 h.flash(msg, category='success')
380 else:
381 msg = _('Repository group `%s` is already taken' % (
382 personal_repo_group_name,))
383 h.flash(msg, category='warning')
353 384 except Exception:
354 385 log.exception("Exception during repository group creation")
355 386 msg = _(
356 387 'An error occurred during repository group creation for user')
357 388 h.flash(msg, category='error')
389 Session().rollback()
358 390
359 391 return redirect(url('edit_user_advanced', user_id=user_id))
360 392
361 393 @HasPermissionAllDecorator('hg.admin')
362 394 def show(self, user_id):
363 395 """GET /users/user_id: Show a specific item"""
364 396 # url('user', user_id=ID)
365 397 User.get_or_404(-1)
366 398
367 399 @HasPermissionAllDecorator('hg.admin')
368 400 def edit(self, user_id):
369 401 """GET /users/user_id/edit: Form to edit an existing item"""
370 402 # url('edit_user', user_id=ID)
371 403 user_id = safe_int(user_id)
372 404 c.user = User.get_or_404(user_id)
373 405 if c.user.username == User.DEFAULT_USER:
374 406 h.flash(_("You can't edit this user"), category='warning')
375 407 return redirect(url('users'))
376 408
377 409 c.active = 'profile'
378 410 c.extern_type = c.user.extern_type
379 411 c.extern_name = c.user.extern_name
380 412 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
381 413
382 414 defaults = c.user.get_dict()
383 415 defaults.update({'language': c.user.user_data.get('language')})
384 416 return htmlfill.render(
385 417 render('admin/users/user_edit.html'),
386 418 defaults=defaults,
387 419 encoding="UTF-8",
388 420 force_defaults=False)
389 421
390 422 @HasPermissionAllDecorator('hg.admin')
391 423 def edit_advanced(self, user_id):
392 424 user_id = safe_int(user_id)
393 425 user = c.user = User.get_or_404(user_id)
394 426 if user.username == User.DEFAULT_USER:
395 427 h.flash(_("You can't edit this user"), category='warning')
396 428 return redirect(url('users'))
397 429
398 430 c.active = 'advanced'
399 431 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
400 c.personal_repo_group = RepoGroup.get_by_group_name(user.username)
432 c.personal_repo_group = c.perm_user.personal_repo_group
433 c.personal_repo_group_name = RepoGroupModel()\
434 .get_personal_group_name(user)
401 435 c.first_admin = User.get_first_super_admin()
402 436 defaults = user.get_dict()
403 437
404 438 # Interim workaround if the user participated on any pull requests as a
405 439 # reviewer.
406 440 has_review = bool(PullRequestReviewers.query().filter(
407 441 PullRequestReviewers.user_id == user_id).first())
408 442 c.can_delete_user = not has_review
409 443 c.can_delete_user_message = _(
410 444 'The user participates as reviewer in pull requests and '
411 445 'cannot be deleted. You can set the user to '
412 446 '"inactive" instead of deleting it.') if has_review else ''
413 447
414 448 return htmlfill.render(
415 449 render('admin/users/user_edit.html'),
416 450 defaults=defaults,
417 451 encoding="UTF-8",
418 452 force_defaults=False)
419 453
420 454 @HasPermissionAllDecorator('hg.admin')
421 455 def edit_auth_tokens(self, user_id):
422 456 user_id = safe_int(user_id)
423 457 c.user = User.get_or_404(user_id)
424 458 if c.user.username == User.DEFAULT_USER:
425 459 h.flash(_("You can't edit this user"), category='warning')
426 460 return redirect(url('users'))
427 461
428 462 c.active = 'auth_tokens'
429 463 show_expired = True
430 464 c.lifetime_values = [
431 465 (str(-1), _('forever')),
432 466 (str(5), _('5 minutes')),
433 467 (str(60), _('1 hour')),
434 468 (str(60 * 24), _('1 day')),
435 469 (str(60 * 24 * 30), _('1 month')),
436 470 ]
437 471 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
438 472 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
439 473 for x in AuthTokenModel.cls.ROLES]
440 474 c.role_options = [(c.role_values, _("Role"))]
441 475 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
442 476 c.user.user_id, show_expired=show_expired)
443 477 defaults = c.user.get_dict()
444 478 return htmlfill.render(
445 479 render('admin/users/user_edit.html'),
446 480 defaults=defaults,
447 481 encoding="UTF-8",
448 482 force_defaults=False)
449 483
450 484 @HasPermissionAllDecorator('hg.admin')
451 485 @auth.CSRFRequired()
452 486 def add_auth_token(self, user_id):
453 487 user_id = safe_int(user_id)
454 488 c.user = User.get_or_404(user_id)
455 489 if c.user.username == User.DEFAULT_USER:
456 490 h.flash(_("You can't edit this user"), category='warning')
457 491 return redirect(url('users'))
458 492
459 493 lifetime = safe_int(request.POST.get('lifetime'), -1)
460 494 description = request.POST.get('description')
461 495 role = request.POST.get('role')
462 496 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
463 497 Session().commit()
464 498 h.flash(_("Auth token successfully created"), category='success')
465 499 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
466 500
467 501 @HasPermissionAllDecorator('hg.admin')
468 502 @auth.CSRFRequired()
469 503 def delete_auth_token(self, user_id):
470 504 user_id = safe_int(user_id)
471 505 c.user = User.get_or_404(user_id)
472 506 if c.user.username == User.DEFAULT_USER:
473 507 h.flash(_("You can't edit this user"), category='warning')
474 508 return redirect(url('users'))
475 509
476 510 auth_token = request.POST.get('del_auth_token')
477 511 if request.POST.get('del_auth_token_builtin'):
478 512 user = User.get(c.user.user_id)
479 513 if user:
480 514 user.api_key = generate_auth_token(user.username)
481 515 Session().add(user)
482 516 Session().commit()
483 517 h.flash(_("Auth token successfully reset"), category='success')
484 518 elif auth_token:
485 519 AuthTokenModel().delete(auth_token, c.user.user_id)
486 520 Session().commit()
487 521 h.flash(_("Auth token successfully deleted"), category='success')
488 522
489 523 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
490 524
491 525 @HasPermissionAllDecorator('hg.admin')
492 526 def edit_global_perms(self, user_id):
493 527 user_id = safe_int(user_id)
494 528 c.user = User.get_or_404(user_id)
495 529 if c.user.username == User.DEFAULT_USER:
496 530 h.flash(_("You can't edit this user"), category='warning')
497 531 return redirect(url('users'))
498 532
499 533 c.active = 'global_perms'
500 534
501 535 c.default_user = User.get_default_user()
502 536 defaults = c.user.get_dict()
503 537 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
504 538 defaults.update(c.default_user.get_default_perms())
505 539 defaults.update(c.user.get_default_perms())
506 540
507 541 return htmlfill.render(
508 542 render('admin/users/user_edit.html'),
509 543 defaults=defaults,
510 544 encoding="UTF-8",
511 545 force_defaults=False)
512 546
513 547 @HasPermissionAllDecorator('hg.admin')
514 548 @auth.CSRFRequired()
515 549 def update_global_perms(self, user_id):
516 550 """PUT /users_perm/user_id: Update an existing item"""
517 551 # url('user_perm', user_id=ID, method='put')
518 552 user_id = safe_int(user_id)
519 553 user = User.get_or_404(user_id)
520 554 c.active = 'global_perms'
521 555 try:
522 556 # first stage that verifies the checkbox
523 557 _form = UserIndividualPermissionsForm()
524 558 form_result = _form.to_python(dict(request.POST))
525 559 inherit_perms = form_result['inherit_default_permissions']
526 560 user.inherit_default_permissions = inherit_perms
527 561 Session().add(user)
528 562
529 563 if not inherit_perms:
530 564 # only update the individual ones if we un check the flag
531 565 _form = UserPermissionsForm(
532 566 [x[0] for x in c.repo_create_choices],
533 567 [x[0] for x in c.repo_create_on_write_choices],
534 568 [x[0] for x in c.repo_group_create_choices],
535 569 [x[0] for x in c.user_group_create_choices],
536 570 [x[0] for x in c.fork_choices],
537 571 [x[0] for x in c.inherit_default_permission_choices])()
538 572
539 573 form_result = _form.to_python(dict(request.POST))
540 574 form_result.update({'perm_user_id': user.user_id})
541 575
542 576 PermissionModel().update_user_permissions(form_result)
543 577
544 578 Session().commit()
545 579 h.flash(_('User global permissions updated successfully'),
546 580 category='success')
547 581
548 582 Session().commit()
549 583 except formencode.Invalid as errors:
550 584 defaults = errors.value
551 585 c.user = user
552 586 return htmlfill.render(
553 587 render('admin/users/user_edit.html'),
554 588 defaults=defaults,
555 589 errors=errors.error_dict or {},
556 590 prefix_error=False,
557 591 encoding="UTF-8",
558 592 force_defaults=False)
559 593 except Exception:
560 594 log.exception("Exception during permissions saving")
561 595 h.flash(_('An error occurred during permissions saving'),
562 596 category='error')
563 597 return redirect(url('edit_user_global_perms', user_id=user_id))
564 598
565 599 @HasPermissionAllDecorator('hg.admin')
566 600 def edit_perms_summary(self, user_id):
567 601 user_id = safe_int(user_id)
568 602 c.user = User.get_or_404(user_id)
569 603 if c.user.username == User.DEFAULT_USER:
570 604 h.flash(_("You can't edit this user"), category='warning')
571 605 return redirect(url('users'))
572 606
573 607 c.active = 'perms_summary'
574 608 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
575 609
576 610 return render('admin/users/user_edit.html')
577 611
578 612 @HasPermissionAllDecorator('hg.admin')
579 613 def edit_emails(self, user_id):
580 614 user_id = safe_int(user_id)
581 615 c.user = User.get_or_404(user_id)
582 616 if c.user.username == User.DEFAULT_USER:
583 617 h.flash(_("You can't edit this user"), category='warning')
584 618 return redirect(url('users'))
585 619
586 620 c.active = 'emails'
587 621 c.user_email_map = UserEmailMap.query() \
588 622 .filter(UserEmailMap.user == c.user).all()
589 623
590 624 defaults = c.user.get_dict()
591 625 return htmlfill.render(
592 626 render('admin/users/user_edit.html'),
593 627 defaults=defaults,
594 628 encoding="UTF-8",
595 629 force_defaults=False)
596 630
597 631 @HasPermissionAllDecorator('hg.admin')
598 632 @auth.CSRFRequired()
599 633 def add_email(self, user_id):
600 634 """POST /user_emails:Add an existing item"""
601 635 # url('user_emails', user_id=ID, method='put')
602 636 user_id = safe_int(user_id)
603 637 c.user = User.get_or_404(user_id)
604 638
605 639 email = request.POST.get('new_email')
606 640 user_model = UserModel()
607 641
608 642 try:
609 643 user_model.add_extra_email(user_id, email)
610 644 Session().commit()
611 645 h.flash(_("Added new email address `%s` for user account") % email,
612 646 category='success')
613 647 except formencode.Invalid as error:
614 648 msg = error.error_dict['email']
615 649 h.flash(msg, category='error')
616 650 except Exception:
617 651 log.exception("Exception during email saving")
618 652 h.flash(_('An error occurred during email saving'),
619 653 category='error')
620 654 return redirect(url('edit_user_emails', user_id=user_id))
621 655
622 656 @HasPermissionAllDecorator('hg.admin')
623 657 @auth.CSRFRequired()
624 658 def delete_email(self, user_id):
625 659 """DELETE /user_emails_delete/user_id: Delete an existing item"""
626 660 # url('user_emails_delete', user_id=ID, method='delete')
627 661 user_id = safe_int(user_id)
628 662 c.user = User.get_or_404(user_id)
629 663 email_id = request.POST.get('del_email_id')
630 664 user_model = UserModel()
631 665 user_model.delete_extra_email(user_id, email_id)
632 666 Session().commit()
633 667 h.flash(_("Removed email address from user account"), category='success')
634 668 return redirect(url('edit_user_emails', user_id=user_id))
635 669
636 670 @HasPermissionAllDecorator('hg.admin')
637 671 def edit_ips(self, user_id):
638 672 user_id = safe_int(user_id)
639 673 c.user = User.get_or_404(user_id)
640 674 if c.user.username == User.DEFAULT_USER:
641 675 h.flash(_("You can't edit this user"), category='warning')
642 676 return redirect(url('users'))
643 677
644 678 c.active = 'ips'
645 679 c.user_ip_map = UserIpMap.query() \
646 680 .filter(UserIpMap.user == c.user).all()
647 681
648 682 c.inherit_default_ips = c.user.inherit_default_permissions
649 683 c.default_user_ip_map = UserIpMap.query() \
650 684 .filter(UserIpMap.user == User.get_default_user()).all()
651 685
652 686 defaults = c.user.get_dict()
653 687 return htmlfill.render(
654 688 render('admin/users/user_edit.html'),
655 689 defaults=defaults,
656 690 encoding="UTF-8",
657 691 force_defaults=False)
658 692
659 693 @HasPermissionAllDecorator('hg.admin')
660 694 @auth.CSRFRequired()
661 695 def add_ip(self, user_id):
662 696 """POST /user_ips:Add an existing item"""
663 697 # url('user_ips', user_id=ID, method='put')
664 698
665 699 user_id = safe_int(user_id)
666 700 c.user = User.get_or_404(user_id)
667 701 user_model = UserModel()
668 702 try:
669 703 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
670 704 except Exception as e:
671 705 ip_list = []
672 706 log.exception("Exception during ip saving")
673 707 h.flash(_('An error occurred during ip saving:%s' % (e,)),
674 708 category='error')
675 709
676 710 desc = request.POST.get('description')
677 711 added = []
678 712 for ip in ip_list:
679 713 try:
680 714 user_model.add_extra_ip(user_id, ip, desc)
681 715 Session().commit()
682 716 added.append(ip)
683 717 except formencode.Invalid as error:
684 718 msg = error.error_dict['ip']
685 719 h.flash(msg, category='error')
686 720 except Exception:
687 721 log.exception("Exception during ip saving")
688 722 h.flash(_('An error occurred during ip saving'),
689 723 category='error')
690 724 if added:
691 725 h.flash(
692 726 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
693 727 category='success')
694 728 if 'default_user' in request.POST:
695 729 return redirect(url('admin_permissions_ips'))
696 730 return redirect(url('edit_user_ips', user_id=user_id))
697 731
698 732 @HasPermissionAllDecorator('hg.admin')
699 733 @auth.CSRFRequired()
700 734 def delete_ip(self, user_id):
701 735 """DELETE /user_ips_delete/user_id: Delete an existing item"""
702 736 # url('user_ips_delete', user_id=ID, method='delete')
703 737 user_id = safe_int(user_id)
704 738 c.user = User.get_or_404(user_id)
705 739
706 740 ip_id = request.POST.get('del_ip_id')
707 741 user_model = UserModel()
708 742 user_model.delete_extra_ip(user_id, ip_id)
709 743 Session().commit()
710 744 h.flash(_("Removed ip address from user whitelist"), category='success')
711 745
712 746 if 'default_user' in request.POST:
713 747 return redirect(url('admin_permissions_ips'))
714 748 return redirect(url('edit_user_ips', user_id=user_id))
@@ -1,197 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 forks controller for rhodecode
23 23 """
24 24
25 25 import formencode
26 26 import logging
27 27 from formencode import htmlfill
28 28
29 29 from pylons import tmpl_context as c, request, url
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 import rhodecode.lib.helpers as h
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib.helpers import Page
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
39 39 HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType)
40 40 from rhodecode.lib.base import BaseRepoController, render
41 41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.forms import RepoForkForm
44 44 from rhodecode.model.scm import ScmModel, RepoGroupList
45 45 from rhodecode.lib.utils2 import safe_int
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class ForksController(BaseRepoController):
51 51
52 52 def __before__(self):
53 53 super(ForksController, self).__before__()
54 54
55 55 def __load_defaults(self):
56 56 acl_groups = RepoGroupList(
57 57 RepoGroup.query().all(),
58 58 perm_set=['group.write', 'group.admin'])
59 59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
60 60 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
61 61 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
62 62 c.landing_revs_choices = choices
63 c.personal_repo_group = RepoGroup.get_by_group_name(
64 c.rhodecode_user.username)
63 c.personal_repo_group = c.rhodecode_user.personal_repo_group
65 64
66 65 def __load_data(self, repo_name=None):
67 66 """
68 67 Load defaults settings for edit, and update
69 68
70 69 :param repo_name:
71 70 """
72 71 self.__load_defaults()
73 72
74 73 c.repo_info = Repository.get_by_repo_name(repo_name)
75 74 repo = c.repo_info.scm_instance()
76 75
77 76 if c.repo_info is None:
78 77 h.not_mapped_error(repo_name)
79 78 return redirect(url('repos'))
80 79
81 80 c.default_user_id = User.get_default_user().user_id
82 81 c.in_public_journal = UserFollowing.query()\
83 82 .filter(UserFollowing.user_id == c.default_user_id)\
84 83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
85 84
86 85 if c.repo_info.stats:
87 86 last_rev = c.repo_info.stats.stat_on_revision+1
88 87 else:
89 88 last_rev = 0
90 89 c.stats_revision = last_rev
91 90
92 91 c.repo_last_rev = repo.count()
93 92
94 93 if last_rev == 0 or c.repo_last_rev == 0:
95 94 c.stats_percentage = 0
96 95 else:
97 96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
98 97 c.repo_last_rev) * 100)
99 98
100 99 defaults = RepoModel()._get_defaults(repo_name)
101 100 # alter the description to indicate a fork
102 101 defaults['description'] = ('fork of repository: %s \n%s'
103 102 % (defaults['repo_name'],
104 103 defaults['description']))
105 104 # add suffix to fork
106 105 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
107 106
108 107 return defaults
109 108
110 109 @LoginRequired()
111 110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
112 111 'repository.admin')
113 112 @HasAcceptedRepoType('git', 'hg')
114 113 def forks(self, repo_name):
115 114 p = safe_int(request.GET.get('page', 1), 1)
116 115 repo_id = c.rhodecode_db_repo.repo_id
117 116 d = []
118 117 for r in Repository.get_repo_forks(repo_id):
119 118 if not HasRepoPermissionAny(
120 119 'repository.read', 'repository.write', 'repository.admin'
121 120 )(r.repo_name, 'get forks check'):
122 121 continue
123 122 d.append(r)
124 123 c.forks_pager = Page(d, page=p, items_per_page=20)
125 124
126 125 c.forks_data = render('/forks/forks_data.html')
127 126
128 127 if request.environ.get('HTTP_X_PJAX'):
129 128 return c.forks_data
130 129
131 130 return render('/forks/forks.html')
132 131
133 132 @LoginRequired()
134 133 @NotAnonymous()
135 134 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
136 135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 136 'repository.admin')
138 137 @HasAcceptedRepoType('git', 'hg')
139 138 def fork(self, repo_name):
140 139 c.repo_info = Repository.get_by_repo_name(repo_name)
141 140 if not c.repo_info:
142 141 h.not_mapped_error(repo_name)
143 142 return redirect(url('home'))
144 143
145 144 defaults = self.__load_data(repo_name)
146 145
147 146 return htmlfill.render(
148 147 render('forks/fork.html'),
149 148 defaults=defaults,
150 149 encoding="UTF-8",
151 150 force_defaults=False
152 151 )
153 152
154 153 @LoginRequired()
155 154 @NotAnonymous()
156 155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
157 156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
158 157 'repository.admin')
159 158 @HasAcceptedRepoType('git', 'hg')
160 159 @auth.CSRFRequired()
161 160 def fork_create(self, repo_name):
162 161 self.__load_defaults()
163 162 c.repo_info = Repository.get_by_repo_name(repo_name)
164 163 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
165 164 repo_groups=c.repo_groups_choices,
166 165 landing_revs=c.landing_revs_choices)()
167 166 form_result = {}
168 167 task_id = None
169 168 try:
170 169 form_result = _form.to_python(dict(request.POST))
171 170 # create fork is done sometimes async on celery, db transaction
172 171 # management is handled there.
173 172 task = RepoModel().create_fork(
174 173 form_result, c.rhodecode_user.user_id)
175 174 from celery.result import BaseAsyncResult
176 175 if isinstance(task, BaseAsyncResult):
177 176 task_id = task.task_id
178 177 except formencode.Invalid as errors:
179 178 c.new_repo = errors.value['repo_name']
180 179 return htmlfill.render(
181 180 render('forks/fork.html'),
182 181 defaults=errors.value,
183 182 errors=errors.error_dict or {},
184 183 prefix_error=False,
185 184 encoding="UTF-8",
186 185 force_defaults=False)
187 186 except Exception:
188 187 log.exception(
189 188 u'Exception while trying to fork the repository %s', repo_name)
190 189 msg = (
191 190 _('An error occurred during repository forking %s') %
192 191 (repo_name, ))
193 192 h.flash(msg, category='error')
194 193
195 194 return redirect(h.url('repo_creating_home',
196 195 repo_name=form_result['repo_name_full'],
197 196 task_id=task_id))
@@ -1,78 +1,79 b''
1 1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 from pyramid.threadlocal import get_current_registry
21 21
22 22 log = logging.getLogger(__name__)
23 23
24 24
25 25 def trigger(event, registry=None):
26 26 """
27 27 Helper method to send an event. This wraps the pyramid logic to send an
28 28 event.
29 29 """
30 30 # For the first step we are using pyramids thread locals here. If the
31 31 # event mechanism works out as a good solution we should think about
32 32 # passing the registry as an argument to get rid of it.
33 33 registry = registry or get_current_registry()
34 34 registry.notify(event)
35 35 log.debug('event %s triggered', event)
36 36
37 37 # Until we can work around the problem that VCS operations do not have a
38 38 # pyramid context to work with, we send the events to integrations directly
39 39
40 40 # Later it will be possible to use regular pyramid subscribers ie:
41 41 # config.add_subscriber(integrations_event_handler, RhodecodeEvent)
42 42 from rhodecode.integrations import integrations_event_handler
43 43 if isinstance(event, RhodecodeEvent):
44 44 integrations_event_handler(event)
45 45
46 46
47 47 from rhodecode.events.base import RhodecodeEvent
48 48
49 49 from rhodecode.events.user import ( # noqa
50 50 UserPreCreate,
51 UserPostCreate,
51 52 UserPreUpdate,
52 53 UserRegistered
53 54 )
54 55
55 56 from rhodecode.events.repo import ( # noqa
56 57 RepoEvent,
57 58 RepoPreCreateEvent, RepoCreateEvent,
58 59 RepoPreDeleteEvent, RepoDeleteEvent,
59 60 RepoPrePushEvent, RepoPushEvent,
60 61 RepoPrePullEvent, RepoPullEvent,
61 62 )
62 63
63 64 from rhodecode.events.repo_group import ( # noqa
64 65 RepoGroupEvent,
65 66 RepoGroupCreateEvent,
66 67 RepoGroupUpdateEvent,
67 68 RepoGroupDeleteEvent,
68 69 )
69 70
70 71 from rhodecode.events.pullrequest import ( # noqa
71 72 PullRequestEvent,
72 73 PullRequestCreateEvent,
73 74 PullRequestUpdateEvent,
74 75 PullRequestCommentEvent,
75 76 PullRequestReviewEvent,
76 77 PullRequestMergeEvent,
77 78 PullRequestCloseEvent,
78 79 )
@@ -1,65 +1,78 b''
1 1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from zope.interface import implementer
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 22 from rhodecode.events.base import RhodecodeEvent
23 23 from rhodecode.events.interfaces import (
24 24 IUserRegistered, IUserPreCreate, IUserPreUpdate)
25 25
26 26
27 27 @implementer(IUserRegistered)
28 28 class UserRegistered(RhodecodeEvent):
29 29 """
30 30 An instance of this class is emitted as an :term:`event` whenever a user
31 31 account is registered.
32 32 """
33 33 name = 'user-register'
34 34 display_name = lazy_ugettext('user registered')
35 35
36 36 def __init__(self, user, session):
37 37 self.user = user
38 38 self.session = session
39 39
40 40
41 41 @implementer(IUserPreCreate)
42 42 class UserPreCreate(RhodecodeEvent):
43 43 """
44 44 An instance of this class is emitted as an :term:`event` before a new user
45 45 object is created.
46 46 """
47 47 name = 'user-pre-create'
48 48 display_name = lazy_ugettext('user pre create')
49 49
50 50 def __init__(self, user_data):
51 51 self.user_data = user_data
52 52
53 53
54 @implementer(IUserPreCreate)
55 class UserPostCreate(RhodecodeEvent):
56 """
57 An instance of this class is emitted as an :term:`event` after a new user
58 object is created.
59 """
60 name = 'user-post-create'
61 display_name = lazy_ugettext('user post create')
62
63 def __init__(self, user_data):
64 self.user_data = user_data
65
66
54 67 @implementer(IUserPreUpdate)
55 68 class UserPreUpdate(RhodecodeEvent):
56 69 """
57 70 An instance of this class is emitted as an :term:`event` before a user
58 71 object is updated.
59 72 """
60 73 name = 'user-pre-update'
61 74 display_name = lazy_ugettext('user pre update')
62 75
63 76 def __init__(self, user, user_data):
64 77 self.user = user
65 78 self.user_data = user_data
@@ -1,1903 +1,1906 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 UserIpMap, UserApiKeys)
52 UserIpMap, UserApiKeys, RepoGroup)
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 @property
987 def personal_repo_group(self):
988 return RepoGroup.get_user_personal_repo_group(self.user_id)
986 989
987 990 @classmethod
988 991 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
989 992 allowed_ips = AuthUser.get_allowed_ips(
990 993 user_id, cache=True, inherit_from_default=inherit_from_default)
991 994 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
992 995 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
993 996 return True
994 997 else:
995 998 log.info('Access for IP:%s forbidden, '
996 999 'not in %s' % (ip_addr, allowed_ips))
997 1000 return False
998 1001
999 1002 def __repr__(self):
1000 1003 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1001 1004 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1002 1005
1003 1006 def set_authenticated(self, authenticated=True):
1004 1007 if self.user_id != self.anonymous_user.user_id:
1005 1008 self.is_authenticated = authenticated
1006 1009
1007 1010 def get_cookie_store(self):
1008 1011 return {
1009 1012 'username': self.username,
1010 1013 'password': md5(self.password),
1011 1014 'user_id': self.user_id,
1012 1015 'is_authenticated': self.is_authenticated
1013 1016 }
1014 1017
1015 1018 @classmethod
1016 1019 def from_cookie_store(cls, cookie_store):
1017 1020 """
1018 1021 Creates AuthUser from a cookie store
1019 1022
1020 1023 :param cls:
1021 1024 :param cookie_store:
1022 1025 """
1023 1026 user_id = cookie_store.get('user_id')
1024 1027 username = cookie_store.get('username')
1025 1028 api_key = cookie_store.get('api_key')
1026 1029 return AuthUser(user_id, api_key, username)
1027 1030
1028 1031 @classmethod
1029 1032 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1030 1033 _set = set()
1031 1034
1032 1035 if inherit_from_default:
1033 1036 default_ips = UserIpMap.query().filter(
1034 1037 UserIpMap.user == User.get_default_user(cache=True))
1035 1038 if cache:
1036 1039 default_ips = default_ips.options(FromCache("sql_cache_short",
1037 1040 "get_user_ips_default"))
1038 1041
1039 1042 # populate from default user
1040 1043 for ip in default_ips:
1041 1044 try:
1042 1045 _set.add(ip.ip_addr)
1043 1046 except ObjectDeletedError:
1044 1047 # since we use heavy caching sometimes it happens that
1045 1048 # we get deleted objects here, we just skip them
1046 1049 pass
1047 1050
1048 1051 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1049 1052 if cache:
1050 1053 user_ips = user_ips.options(FromCache("sql_cache_short",
1051 1054 "get_user_ips_%s" % user_id))
1052 1055
1053 1056 for ip in user_ips:
1054 1057 try:
1055 1058 _set.add(ip.ip_addr)
1056 1059 except ObjectDeletedError:
1057 1060 # since we use heavy caching sometimes it happens that we get
1058 1061 # deleted objects here, we just skip them
1059 1062 pass
1060 1063 return _set or set(['0.0.0.0/0', '::/0'])
1061 1064
1062 1065
1063 1066 def set_available_permissions(config):
1064 1067 """
1065 1068 This function will propagate pylons globals with all available defined
1066 1069 permission given in db. We don't want to check each time from db for new
1067 1070 permissions since adding a new permission also requires application restart
1068 1071 ie. to decorate new views with the newly created permission
1069 1072
1070 1073 :param config: current pylons config instance
1071 1074
1072 1075 """
1073 1076 log.info('getting information about all available permissions')
1074 1077 try:
1075 1078 sa = meta.Session
1076 1079 all_perms = sa.query(Permission).all()
1077 1080 config['available_permissions'] = [x.permission_name for x in all_perms]
1078 1081 except Exception:
1079 1082 log.error(traceback.format_exc())
1080 1083 finally:
1081 1084 meta.Session.remove()
1082 1085
1083 1086
1084 1087 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1085 1088 """
1086 1089 Return the current authentication token, creating one if one doesn't
1087 1090 already exist and the save_if_missing flag is present.
1088 1091
1089 1092 :param session: pass in the pylons session, else we use the global ones
1090 1093 :param force_new: force to re-generate the token and store it in session
1091 1094 :param save_if_missing: save the newly generated token if it's missing in
1092 1095 session
1093 1096 """
1094 1097 if not session:
1095 1098 from pylons import session
1096 1099
1097 1100 if (csrf_token_key not in session and save_if_missing) or force_new:
1098 1101 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1099 1102 session[csrf_token_key] = token
1100 1103 if hasattr(session, 'save'):
1101 1104 session.save()
1102 1105 return session.get(csrf_token_key)
1103 1106
1104 1107
1105 1108 # CHECK DECORATORS
1106 1109 class CSRFRequired(object):
1107 1110 """
1108 1111 Decorator for authenticating a form
1109 1112
1110 1113 This decorator uses an authorization token stored in the client's
1111 1114 session for prevention of certain Cross-site request forgery (CSRF)
1112 1115 attacks (See
1113 1116 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1114 1117 information).
1115 1118
1116 1119 For use with the ``webhelpers.secure_form`` helper functions.
1117 1120
1118 1121 """
1119 1122 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1120 1123 except_methods=None):
1121 1124 self.token = token
1122 1125 self.header = header
1123 1126 self.except_methods = except_methods or []
1124 1127
1125 1128 def __call__(self, func):
1126 1129 return get_cython_compat_decorator(self.__wrapper, func)
1127 1130
1128 1131 def _get_csrf(self, _request):
1129 1132 return _request.POST.get(self.token, _request.headers.get(self.header))
1130 1133
1131 1134 def check_csrf(self, _request, cur_token):
1132 1135 supplied_token = self._get_csrf(_request)
1133 1136 return supplied_token and supplied_token == cur_token
1134 1137
1135 1138 def __wrapper(self, func, *fargs, **fkwargs):
1136 1139 if request.method in self.except_methods:
1137 1140 return func(*fargs, **fkwargs)
1138 1141
1139 1142 cur_token = get_csrf_token(save_if_missing=False)
1140 1143 if self.check_csrf(request, cur_token):
1141 1144 if request.POST.get(self.token):
1142 1145 del request.POST[self.token]
1143 1146 return func(*fargs, **fkwargs)
1144 1147 else:
1145 1148 reason = 'token-missing'
1146 1149 supplied_token = self._get_csrf(request)
1147 1150 if supplied_token and cur_token != supplied_token:
1148 1151 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1149 1152 supplied_token or ''[:6])
1150 1153
1151 1154 csrf_message = \
1152 1155 ("Cross-site request forgery detected, request denied. See "
1153 1156 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1154 1157 "more information.")
1155 1158 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1156 1159 'REMOTE_ADDR:%s, HEADERS:%s' % (
1157 1160 request, reason, request.remote_addr, request.headers))
1158 1161
1159 1162 abort(403, detail=csrf_message)
1160 1163
1161 1164
1162 1165 class LoginRequired(object):
1163 1166 """
1164 1167 Must be logged in to execute this function else
1165 1168 redirect to login page
1166 1169
1167 1170 :param api_access: if enabled this checks only for valid auth token
1168 1171 and grants access based on valid token
1169 1172 """
1170 1173 def __init__(self, auth_token_access=False):
1171 1174 self.auth_token_access = auth_token_access
1172 1175
1173 1176 def __call__(self, func):
1174 1177 return get_cython_compat_decorator(self.__wrapper, func)
1175 1178
1176 1179 def __wrapper(self, func, *fargs, **fkwargs):
1177 1180 from rhodecode.lib import helpers as h
1178 1181 cls = fargs[0]
1179 1182 user = cls._rhodecode_user
1180 1183 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1181 1184 log.debug('Starting login restriction checks for user: %s' % (user,))
1182 1185 # check if our IP is allowed
1183 1186 ip_access_valid = True
1184 1187 if not user.ip_allowed:
1185 1188 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1186 1189 category='warning')
1187 1190 ip_access_valid = False
1188 1191
1189 1192 # check if we used an APIKEY and it's a valid one
1190 1193 # defined whitelist of controllers which API access will be enabled
1191 1194 _auth_token = request.GET.get(
1192 1195 'auth_token', '') or request.GET.get('api_key', '')
1193 1196 auth_token_access_valid = allowed_auth_token_access(
1194 1197 loc, auth_token=_auth_token)
1195 1198
1196 1199 # explicit controller is enabled or API is in our whitelist
1197 1200 if self.auth_token_access or auth_token_access_valid:
1198 1201 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1199 1202
1200 1203 if _auth_token and _auth_token in user.auth_tokens:
1201 1204 auth_token_access_valid = True
1202 1205 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1203 1206 else:
1204 1207 auth_token_access_valid = False
1205 1208 if not _auth_token:
1206 1209 log.debug("AUTH TOKEN *NOT* present in request")
1207 1210 else:
1208 1211 log.warning(
1209 1212 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1210 1213
1211 1214 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1212 1215 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1213 1216 else 'AUTH_TOKEN_AUTH'
1214 1217
1215 1218 if ip_access_valid and (
1216 1219 user.is_authenticated or auth_token_access_valid):
1217 1220 log.info(
1218 1221 'user %s authenticating with:%s IS authenticated on func %s'
1219 1222 % (user, reason, loc))
1220 1223
1221 1224 # update user data to check last activity
1222 1225 user.update_lastactivity()
1223 1226 Session().commit()
1224 1227 return func(*fargs, **fkwargs)
1225 1228 else:
1226 1229 log.warning(
1227 1230 'user %s authenticating with:%s NOT authenticated on '
1228 1231 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1229 1232 % (user, reason, loc, ip_access_valid,
1230 1233 auth_token_access_valid))
1231 1234 # we preserve the get PARAM
1232 1235 came_from = request.path_qs
1233 1236
1234 1237 log.debug('redirecting to login page with %s' % (came_from,))
1235 1238 return redirect(
1236 1239 h.route_path('login', _query={'came_from': came_from}))
1237 1240
1238 1241
1239 1242 class NotAnonymous(object):
1240 1243 """
1241 1244 Must be logged in to execute this function else
1242 1245 redirect to login page"""
1243 1246
1244 1247 def __call__(self, func):
1245 1248 return get_cython_compat_decorator(self.__wrapper, func)
1246 1249
1247 1250 def __wrapper(self, func, *fargs, **fkwargs):
1248 1251 cls = fargs[0]
1249 1252 self.user = cls._rhodecode_user
1250 1253
1251 1254 log.debug('Checking if user is not anonymous @%s' % cls)
1252 1255
1253 1256 anonymous = self.user.username == User.DEFAULT_USER
1254 1257
1255 1258 if anonymous:
1256 1259 came_from = request.path_qs
1257 1260
1258 1261 import rhodecode.lib.helpers as h
1259 1262 h.flash(_('You need to be a registered user to '
1260 1263 'perform this action'),
1261 1264 category='warning')
1262 1265 return redirect(
1263 1266 h.route_path('login', _query={'came_from': came_from}))
1264 1267 else:
1265 1268 return func(*fargs, **fkwargs)
1266 1269
1267 1270
1268 1271 class XHRRequired(object):
1269 1272 def __call__(self, func):
1270 1273 return get_cython_compat_decorator(self.__wrapper, func)
1271 1274
1272 1275 def __wrapper(self, func, *fargs, **fkwargs):
1273 1276 log.debug('Checking if request is XMLHttpRequest (XHR)')
1274 1277 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1275 1278 if not request.is_xhr:
1276 1279 abort(400, detail=xhr_message)
1277 1280
1278 1281 return func(*fargs, **fkwargs)
1279 1282
1280 1283
1281 1284 class HasAcceptedRepoType(object):
1282 1285 """
1283 1286 Check if requested repo is within given repo type aliases
1284 1287
1285 1288 TODO: anderson: not sure where to put this decorator
1286 1289 """
1287 1290
1288 1291 def __init__(self, *repo_type_list):
1289 1292 self.repo_type_list = set(repo_type_list)
1290 1293
1291 1294 def __call__(self, func):
1292 1295 return get_cython_compat_decorator(self.__wrapper, func)
1293 1296
1294 1297 def __wrapper(self, func, *fargs, **fkwargs):
1295 1298 cls = fargs[0]
1296 1299 rhodecode_repo = cls.rhodecode_repo
1297 1300
1298 1301 log.debug('%s checking repo type for %s in %s',
1299 1302 self.__class__.__name__,
1300 1303 rhodecode_repo.alias, self.repo_type_list)
1301 1304
1302 1305 if rhodecode_repo.alias in self.repo_type_list:
1303 1306 return func(*fargs, **fkwargs)
1304 1307 else:
1305 1308 import rhodecode.lib.helpers as h
1306 1309 h.flash(h.literal(
1307 1310 _('Action not supported for %s.' % rhodecode_repo.alias)),
1308 1311 category='warning')
1309 1312 return redirect(
1310 1313 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1311 1314
1312 1315
1313 1316 class PermsDecorator(object):
1314 1317 """
1315 1318 Base class for controller decorators, we extract the current user from
1316 1319 the class itself, which has it stored in base controllers
1317 1320 """
1318 1321
1319 1322 def __init__(self, *required_perms):
1320 1323 self.required_perms = set(required_perms)
1321 1324
1322 1325 def __call__(self, func):
1323 1326 return get_cython_compat_decorator(self.__wrapper, func)
1324 1327
1325 1328 def __wrapper(self, func, *fargs, **fkwargs):
1326 1329 cls = fargs[0]
1327 1330 _user = cls._rhodecode_user
1328 1331
1329 1332 log.debug('checking %s permissions %s for %s %s',
1330 1333 self.__class__.__name__, self.required_perms, cls, _user)
1331 1334
1332 1335 if self.check_permissions(_user):
1333 1336 log.debug('Permission granted for %s %s', cls, _user)
1334 1337 return func(*fargs, **fkwargs)
1335 1338
1336 1339 else:
1337 1340 log.debug('Permission denied for %s %s', cls, _user)
1338 1341 anonymous = _user.username == User.DEFAULT_USER
1339 1342
1340 1343 if anonymous:
1341 1344 came_from = request.path_qs
1342 1345
1343 1346 import rhodecode.lib.helpers as h
1344 1347 h.flash(_('You need to be signed in to view this page'),
1345 1348 category='warning')
1346 1349 return redirect(
1347 1350 h.route_path('login', _query={'came_from': came_from}))
1348 1351
1349 1352 else:
1350 1353 # redirect with forbidden ret code
1351 1354 return abort(403)
1352 1355
1353 1356 def check_permissions(self, user):
1354 1357 """Dummy function for overriding"""
1355 1358 raise NotImplementedError(
1356 1359 'You have to write this function in child class')
1357 1360
1358 1361
1359 1362 class HasPermissionAllDecorator(PermsDecorator):
1360 1363 """
1361 1364 Checks for access permission for all given predicates. All of them
1362 1365 have to be meet in order to fulfill the request
1363 1366 """
1364 1367
1365 1368 def check_permissions(self, user):
1366 1369 perms = user.permissions_with_scope({})
1367 1370 if self.required_perms.issubset(perms['global']):
1368 1371 return True
1369 1372 return False
1370 1373
1371 1374
1372 1375 class HasPermissionAnyDecorator(PermsDecorator):
1373 1376 """
1374 1377 Checks for access permission for any of given predicates. In order to
1375 1378 fulfill the request any of predicates must be meet
1376 1379 """
1377 1380
1378 1381 def check_permissions(self, user):
1379 1382 perms = user.permissions_with_scope({})
1380 1383 if self.required_perms.intersection(perms['global']):
1381 1384 return True
1382 1385 return False
1383 1386
1384 1387
1385 1388 class HasRepoPermissionAllDecorator(PermsDecorator):
1386 1389 """
1387 1390 Checks for access permission for all given predicates for specific
1388 1391 repository. All of them have to be meet in order to fulfill the request
1389 1392 """
1390 1393
1391 1394 def check_permissions(self, user):
1392 1395 perms = user.permissions
1393 1396 repo_name = get_repo_slug(request)
1394 1397 try:
1395 1398 user_perms = set([perms['repositories'][repo_name]])
1396 1399 except KeyError:
1397 1400 return False
1398 1401 if self.required_perms.issubset(user_perms):
1399 1402 return True
1400 1403 return False
1401 1404
1402 1405
1403 1406 class HasRepoPermissionAnyDecorator(PermsDecorator):
1404 1407 """
1405 1408 Checks for access permission for any of given predicates for specific
1406 1409 repository. In order to fulfill the request any of predicates must be meet
1407 1410 """
1408 1411
1409 1412 def check_permissions(self, user):
1410 1413 perms = user.permissions
1411 1414 repo_name = get_repo_slug(request)
1412 1415 try:
1413 1416 user_perms = set([perms['repositories'][repo_name]])
1414 1417 except KeyError:
1415 1418 return False
1416 1419
1417 1420 if self.required_perms.intersection(user_perms):
1418 1421 return True
1419 1422 return False
1420 1423
1421 1424
1422 1425 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1423 1426 """
1424 1427 Checks for access permission for all given predicates for specific
1425 1428 repository group. All of them have to be meet in order to
1426 1429 fulfill the request
1427 1430 """
1428 1431
1429 1432 def check_permissions(self, user):
1430 1433 perms = user.permissions
1431 1434 group_name = get_repo_group_slug(request)
1432 1435 try:
1433 1436 user_perms = set([perms['repositories_groups'][group_name]])
1434 1437 except KeyError:
1435 1438 return False
1436 1439
1437 1440 if self.required_perms.issubset(user_perms):
1438 1441 return True
1439 1442 return False
1440 1443
1441 1444
1442 1445 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1443 1446 """
1444 1447 Checks for access permission for any of given predicates for specific
1445 1448 repository group. In order to fulfill the request any
1446 1449 of predicates must be met
1447 1450 """
1448 1451
1449 1452 def check_permissions(self, user):
1450 1453 perms = user.permissions
1451 1454 group_name = get_repo_group_slug(request)
1452 1455 try:
1453 1456 user_perms = set([perms['repositories_groups'][group_name]])
1454 1457 except KeyError:
1455 1458 return False
1456 1459
1457 1460 if self.required_perms.intersection(user_perms):
1458 1461 return True
1459 1462 return False
1460 1463
1461 1464
1462 1465 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1463 1466 """
1464 1467 Checks for access permission for all given predicates for specific
1465 1468 user group. All of them have to be meet in order to fulfill the request
1466 1469 """
1467 1470
1468 1471 def check_permissions(self, user):
1469 1472 perms = user.permissions
1470 1473 group_name = get_user_group_slug(request)
1471 1474 try:
1472 1475 user_perms = set([perms['user_groups'][group_name]])
1473 1476 except KeyError:
1474 1477 return False
1475 1478
1476 1479 if self.required_perms.issubset(user_perms):
1477 1480 return True
1478 1481 return False
1479 1482
1480 1483
1481 1484 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1482 1485 """
1483 1486 Checks for access permission for any of given predicates for specific
1484 1487 user group. In order to fulfill the request any of predicates must be meet
1485 1488 """
1486 1489
1487 1490 def check_permissions(self, user):
1488 1491 perms = user.permissions
1489 1492 group_name = get_user_group_slug(request)
1490 1493 try:
1491 1494 user_perms = set([perms['user_groups'][group_name]])
1492 1495 except KeyError:
1493 1496 return False
1494 1497
1495 1498 if self.required_perms.intersection(user_perms):
1496 1499 return True
1497 1500 return False
1498 1501
1499 1502
1500 1503 # CHECK FUNCTIONS
1501 1504 class PermsFunction(object):
1502 1505 """Base function for other check functions"""
1503 1506
1504 1507 def __init__(self, *perms):
1505 1508 self.required_perms = set(perms)
1506 1509 self.repo_name = None
1507 1510 self.repo_group_name = None
1508 1511 self.user_group_name = None
1509 1512
1510 1513 def __bool__(self):
1511 1514 frame = inspect.currentframe()
1512 1515 stack_trace = traceback.format_stack(frame)
1513 1516 log.error('Checking bool value on a class instance of perm '
1514 1517 'function is not allowed: %s' % ''.join(stack_trace))
1515 1518 # rather than throwing errors, here we always return False so if by
1516 1519 # accident someone checks truth for just an instance it will always end
1517 1520 # up in returning False
1518 1521 return False
1519 1522 __nonzero__ = __bool__
1520 1523
1521 1524 def __call__(self, check_location='', user=None):
1522 1525 if not user:
1523 1526 log.debug('Using user attribute from global request')
1524 1527 # TODO: remove this someday,put as user as attribute here
1525 1528 user = request.user
1526 1529
1527 1530 # init auth user if not already given
1528 1531 if not isinstance(user, AuthUser):
1529 1532 log.debug('Wrapping user %s into AuthUser', user)
1530 1533 user = AuthUser(user.user_id)
1531 1534
1532 1535 cls_name = self.__class__.__name__
1533 1536 check_scope = self._get_check_scope(cls_name)
1534 1537 check_location = check_location or 'unspecified location'
1535 1538
1536 1539 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1537 1540 self.required_perms, user, check_scope, check_location)
1538 1541 if not user:
1539 1542 log.warning('Empty user given for permission check')
1540 1543 return False
1541 1544
1542 1545 if self.check_permissions(user):
1543 1546 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1544 1547 check_scope, user, check_location)
1545 1548 return True
1546 1549
1547 1550 else:
1548 1551 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1549 1552 check_scope, user, check_location)
1550 1553 return False
1551 1554
1552 1555 def _get_check_scope(self, cls_name):
1553 1556 return {
1554 1557 'HasPermissionAll': 'GLOBAL',
1555 1558 'HasPermissionAny': 'GLOBAL',
1556 1559 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1557 1560 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1558 1561 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1559 1562 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1560 1563 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1561 1564 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1562 1565 }.get(cls_name, '?:%s' % cls_name)
1563 1566
1564 1567 def check_permissions(self, user):
1565 1568 """Dummy function for overriding"""
1566 1569 raise Exception('You have to write this function in child class')
1567 1570
1568 1571
1569 1572 class HasPermissionAll(PermsFunction):
1570 1573 def check_permissions(self, user):
1571 1574 perms = user.permissions_with_scope({})
1572 1575 if self.required_perms.issubset(perms.get('global')):
1573 1576 return True
1574 1577 return False
1575 1578
1576 1579
1577 1580 class HasPermissionAny(PermsFunction):
1578 1581 def check_permissions(self, user):
1579 1582 perms = user.permissions_with_scope({})
1580 1583 if self.required_perms.intersection(perms.get('global')):
1581 1584 return True
1582 1585 return False
1583 1586
1584 1587
1585 1588 class HasRepoPermissionAll(PermsFunction):
1586 1589 def __call__(self, repo_name=None, check_location='', user=None):
1587 1590 self.repo_name = repo_name
1588 1591 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1589 1592
1590 1593 def check_permissions(self, user):
1591 1594 if not self.repo_name:
1592 1595 self.repo_name = get_repo_slug(request)
1593 1596
1594 1597 perms = user.permissions
1595 1598 try:
1596 1599 user_perms = set([perms['repositories'][self.repo_name]])
1597 1600 except KeyError:
1598 1601 return False
1599 1602 if self.required_perms.issubset(user_perms):
1600 1603 return True
1601 1604 return False
1602 1605
1603 1606
1604 1607 class HasRepoPermissionAny(PermsFunction):
1605 1608 def __call__(self, repo_name=None, check_location='', user=None):
1606 1609 self.repo_name = repo_name
1607 1610 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1608 1611
1609 1612 def check_permissions(self, user):
1610 1613 if not self.repo_name:
1611 1614 self.repo_name = get_repo_slug(request)
1612 1615
1613 1616 perms = user.permissions
1614 1617 try:
1615 1618 user_perms = set([perms['repositories'][self.repo_name]])
1616 1619 except KeyError:
1617 1620 return False
1618 1621 if self.required_perms.intersection(user_perms):
1619 1622 return True
1620 1623 return False
1621 1624
1622 1625
1623 1626 class HasRepoGroupPermissionAny(PermsFunction):
1624 1627 def __call__(self, group_name=None, check_location='', user=None):
1625 1628 self.repo_group_name = group_name
1626 1629 return super(HasRepoGroupPermissionAny, self).__call__(
1627 1630 check_location, user)
1628 1631
1629 1632 def check_permissions(self, user):
1630 1633 perms = user.permissions
1631 1634 try:
1632 1635 user_perms = set(
1633 1636 [perms['repositories_groups'][self.repo_group_name]])
1634 1637 except KeyError:
1635 1638 return False
1636 1639 if self.required_perms.intersection(user_perms):
1637 1640 return True
1638 1641 return False
1639 1642
1640 1643
1641 1644 class HasRepoGroupPermissionAll(PermsFunction):
1642 1645 def __call__(self, group_name=None, check_location='', user=None):
1643 1646 self.repo_group_name = group_name
1644 1647 return super(HasRepoGroupPermissionAll, self).__call__(
1645 1648 check_location, user)
1646 1649
1647 1650 def check_permissions(self, user):
1648 1651 perms = user.permissions
1649 1652 try:
1650 1653 user_perms = set(
1651 1654 [perms['repositories_groups'][self.repo_group_name]])
1652 1655 except KeyError:
1653 1656 return False
1654 1657 if self.required_perms.issubset(user_perms):
1655 1658 return True
1656 1659 return False
1657 1660
1658 1661
1659 1662 class HasUserGroupPermissionAny(PermsFunction):
1660 1663 def __call__(self, user_group_name=None, check_location='', user=None):
1661 1664 self.user_group_name = user_group_name
1662 1665 return super(HasUserGroupPermissionAny, self).__call__(
1663 1666 check_location, user)
1664 1667
1665 1668 def check_permissions(self, user):
1666 1669 perms = user.permissions
1667 1670 try:
1668 1671 user_perms = set([perms['user_groups'][self.user_group_name]])
1669 1672 except KeyError:
1670 1673 return False
1671 1674 if self.required_perms.intersection(user_perms):
1672 1675 return True
1673 1676 return False
1674 1677
1675 1678
1676 1679 class HasUserGroupPermissionAll(PermsFunction):
1677 1680 def __call__(self, user_group_name=None, check_location='', user=None):
1678 1681 self.user_group_name = user_group_name
1679 1682 return super(HasUserGroupPermissionAll, self).__call__(
1680 1683 check_location, user)
1681 1684
1682 1685 def check_permissions(self, user):
1683 1686 perms = user.permissions
1684 1687 try:
1685 1688 user_perms = set([perms['user_groups'][self.user_group_name]])
1686 1689 except KeyError:
1687 1690 return False
1688 1691 if self.required_perms.issubset(user_perms):
1689 1692 return True
1690 1693 return False
1691 1694
1692 1695
1693 1696 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1694 1697 class HasPermissionAnyMiddleware(object):
1695 1698 def __init__(self, *perms):
1696 1699 self.required_perms = set(perms)
1697 1700
1698 1701 def __call__(self, user, repo_name):
1699 1702 # repo_name MUST be unicode, since we handle keys in permission
1700 1703 # dict by unicode
1701 1704 repo_name = safe_unicode(repo_name)
1702 1705 user = AuthUser(user.user_id)
1703 1706 log.debug(
1704 1707 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1705 1708 self.required_perms, user, repo_name)
1706 1709
1707 1710 if self.check_permissions(user, repo_name):
1708 1711 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1709 1712 repo_name, user, 'PermissionMiddleware')
1710 1713 return True
1711 1714
1712 1715 else:
1713 1716 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1714 1717 repo_name, user, 'PermissionMiddleware')
1715 1718 return False
1716 1719
1717 1720 def check_permissions(self, user, repo_name):
1718 1721 perms = user.permissions_with_scope({'repo_name': repo_name})
1719 1722
1720 1723 try:
1721 1724 user_perms = set([perms['repositories'][repo_name]])
1722 1725 except Exception:
1723 1726 log.exception('Error while accessing user permissions')
1724 1727 return False
1725 1728
1726 1729 if self.required_perms.intersection(user_perms):
1727 1730 return True
1728 1731 return False
1729 1732
1730 1733
1731 1734 # SPECIAL VERSION TO HANDLE API AUTH
1732 1735 class _BaseApiPerm(object):
1733 1736 def __init__(self, *perms):
1734 1737 self.required_perms = set(perms)
1735 1738
1736 1739 def __call__(self, check_location=None, user=None, repo_name=None,
1737 1740 group_name=None, user_group_name=None):
1738 1741 cls_name = self.__class__.__name__
1739 1742 check_scope = 'global:%s' % (self.required_perms,)
1740 1743 if repo_name:
1741 1744 check_scope += ', repo_name:%s' % (repo_name,)
1742 1745
1743 1746 if group_name:
1744 1747 check_scope += ', repo_group_name:%s' % (group_name,)
1745 1748
1746 1749 if user_group_name:
1747 1750 check_scope += ', user_group_name:%s' % (user_group_name,)
1748 1751
1749 1752 log.debug(
1750 1753 'checking cls:%s %s %s @ %s'
1751 1754 % (cls_name, self.required_perms, check_scope, check_location))
1752 1755 if not user:
1753 1756 log.debug('Empty User passed into arguments')
1754 1757 return False
1755 1758
1756 1759 # process user
1757 1760 if not isinstance(user, AuthUser):
1758 1761 user = AuthUser(user.user_id)
1759 1762 if not check_location:
1760 1763 check_location = 'unspecified'
1761 1764 if self.check_permissions(user.permissions, repo_name, group_name,
1762 1765 user_group_name):
1763 1766 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1764 1767 check_scope, user, check_location)
1765 1768 return True
1766 1769
1767 1770 else:
1768 1771 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1769 1772 check_scope, user, check_location)
1770 1773 return False
1771 1774
1772 1775 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1773 1776 user_group_name=None):
1774 1777 """
1775 1778 implement in child class should return True if permissions are ok,
1776 1779 False otherwise
1777 1780
1778 1781 :param perm_defs: dict with permission definitions
1779 1782 :param repo_name: repo name
1780 1783 """
1781 1784 raise NotImplementedError()
1782 1785
1783 1786
1784 1787 class HasPermissionAllApi(_BaseApiPerm):
1785 1788 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1786 1789 user_group_name=None):
1787 1790 if self.required_perms.issubset(perm_defs.get('global')):
1788 1791 return True
1789 1792 return False
1790 1793
1791 1794
1792 1795 class HasPermissionAnyApi(_BaseApiPerm):
1793 1796 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1794 1797 user_group_name=None):
1795 1798 if self.required_perms.intersection(perm_defs.get('global')):
1796 1799 return True
1797 1800 return False
1798 1801
1799 1802
1800 1803 class HasRepoPermissionAllApi(_BaseApiPerm):
1801 1804 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1802 1805 user_group_name=None):
1803 1806 try:
1804 1807 _user_perms = set([perm_defs['repositories'][repo_name]])
1805 1808 except KeyError:
1806 1809 log.warning(traceback.format_exc())
1807 1810 return False
1808 1811 if self.required_perms.issubset(_user_perms):
1809 1812 return True
1810 1813 return False
1811 1814
1812 1815
1813 1816 class HasRepoPermissionAnyApi(_BaseApiPerm):
1814 1817 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1815 1818 user_group_name=None):
1816 1819 try:
1817 1820 _user_perms = set([perm_defs['repositories'][repo_name]])
1818 1821 except KeyError:
1819 1822 log.warning(traceback.format_exc())
1820 1823 return False
1821 1824 if self.required_perms.intersection(_user_perms):
1822 1825 return True
1823 1826 return False
1824 1827
1825 1828
1826 1829 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1827 1830 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1828 1831 user_group_name=None):
1829 1832 try:
1830 1833 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1831 1834 except KeyError:
1832 1835 log.warning(traceback.format_exc())
1833 1836 return False
1834 1837 if self.required_perms.intersection(_user_perms):
1835 1838 return True
1836 1839 return False
1837 1840
1838 1841
1839 1842 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1840 1843 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1841 1844 user_group_name=None):
1842 1845 try:
1843 1846 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1844 1847 except KeyError:
1845 1848 log.warning(traceback.format_exc())
1846 1849 return False
1847 1850 if self.required_perms.issubset(_user_perms):
1848 1851 return True
1849 1852 return False
1850 1853
1851 1854
1852 1855 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1853 1856 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1854 1857 user_group_name=None):
1855 1858 try:
1856 1859 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1857 1860 except KeyError:
1858 1861 log.warning(traceback.format_exc())
1859 1862 return False
1860 1863 if self.required_perms.intersection(_user_perms):
1861 1864 return True
1862 1865 return False
1863 1866
1864 1867
1865 1868 def check_ip_access(source_ip, allowed_ips=None):
1866 1869 """
1867 1870 Checks if source_ip is a subnet of any of allowed_ips.
1868 1871
1869 1872 :param source_ip:
1870 1873 :param allowed_ips: list of allowed ips together with mask
1871 1874 """
1872 1875 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1873 1876 source_ip_address = ipaddress.ip_address(source_ip)
1874 1877 if isinstance(allowed_ips, (tuple, list, set)):
1875 1878 for ip in allowed_ips:
1876 1879 try:
1877 1880 network_address = ipaddress.ip_network(ip, strict=False)
1878 1881 if source_ip_address in network_address:
1879 1882 log.debug('IP %s is network %s' %
1880 1883 (source_ip_address, network_address))
1881 1884 return True
1882 1885 # for any case we cannot determine the IP, don't crash just
1883 1886 # skip it and log as error, we want to say forbidden still when
1884 1887 # sending bad IP
1885 1888 except Exception:
1886 1889 log.error(traceback.format_exc())
1887 1890 continue
1888 1891 return False
1889 1892
1890 1893
1891 1894 def get_cython_compat_decorator(wrapper, func):
1892 1895 """
1893 1896 Creates a cython compatible decorator. The previously used
1894 1897 decorator.decorator() function seems to be incompatible with cython.
1895 1898
1896 1899 :param wrapper: __wrapper method of the decorator class
1897 1900 :param func: decorated function
1898 1901 """
1899 1902 @wraps(func)
1900 1903 def local_wrapper(*args, **kwds):
1901 1904 return wrapper(func, *args, **kwds)
1902 1905 local_wrapper.__wrapped__ = func
1903 1906 return local_wrapper
@@ -1,936 +1,936 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 Some simple helper functions
24 24 """
25 25
26 26
27 27 import collections
28 28 import datetime
29 29 import dateutil.relativedelta
30 30 import hashlib
31 31 import logging
32 32 import re
33 33 import sys
34 34 import time
35 35 import threading
36 36 import urllib
37 37 import urlobject
38 38 import uuid
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import webob
44 44 import routes.util
45 45
46 46 import rhodecode
47 47
48 48
49 49 def md5(s):
50 50 return hashlib.md5(s).hexdigest()
51 51
52 52
53 53 def md5_safe(s):
54 54 return md5(safe_str(s))
55 55
56 56
57 57 def __get_lem(extra_mapping=None):
58 58 """
59 59 Get language extension map based on what's inside pygments lexers
60 60 """
61 61 d = collections.defaultdict(lambda: [])
62 62
63 63 def __clean(s):
64 64 s = s.lstrip('*')
65 65 s = s.lstrip('.')
66 66
67 67 if s.find('[') != -1:
68 68 exts = []
69 69 start, stop = s.find('['), s.find(']')
70 70
71 71 for suffix in s[start + 1:stop]:
72 72 exts.append(s[:s.find('[')] + suffix)
73 73 return [e.lower() for e in exts]
74 74 else:
75 75 return [s.lower()]
76 76
77 77 for lx, t in sorted(pygments.lexers.LEXERS.items()):
78 78 m = map(__clean, t[-2])
79 79 if m:
80 80 m = reduce(lambda x, y: x + y, m)
81 81 for ext in m:
82 82 desc = lx.replace('Lexer', '')
83 83 d[ext].append(desc)
84 84
85 85 data = dict(d)
86 86
87 87 extra_mapping = extra_mapping or {}
88 88 if extra_mapping:
89 89 for k, v in extra_mapping.items():
90 90 if k not in data:
91 91 # register new mapping2lexer
92 92 data[k] = [v]
93 93
94 94 return data
95 95
96 96
97 97 def str2bool(_str):
98 98 """
99 returs True/False value from given string, it tries to translate the
99 returns True/False value from given string, it tries to translate the
100 100 string into boolean
101 101
102 102 :param _str: string value to translate into boolean
103 103 :rtype: boolean
104 104 :returns: boolean from given string
105 105 """
106 106 if _str is None:
107 107 return False
108 108 if _str in (True, False):
109 109 return _str
110 110 _str = str(_str).strip().lower()
111 111 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
112 112
113 113
114 114 def aslist(obj, sep=None, strip=True):
115 115 """
116 116 Returns given string separated by sep as list
117 117
118 118 :param obj:
119 119 :param sep:
120 120 :param strip:
121 121 """
122 122 if isinstance(obj, (basestring,)):
123 123 lst = obj.split(sep)
124 124 if strip:
125 125 lst = [v.strip() for v in lst]
126 126 return lst
127 127 elif isinstance(obj, (list, tuple)):
128 128 return obj
129 129 elif obj is None:
130 130 return []
131 131 else:
132 132 return [obj]
133 133
134 134
135 135 def convert_line_endings(line, mode):
136 136 """
137 137 Converts a given line "line end" accordingly to given mode
138 138
139 139 Available modes are::
140 140 0 - Unix
141 141 1 - Mac
142 142 2 - DOS
143 143
144 144 :param line: given line to convert
145 145 :param mode: mode to convert to
146 146 :rtype: str
147 147 :return: converted line according to mode
148 148 """
149 149 if mode == 0:
150 150 line = line.replace('\r\n', '\n')
151 151 line = line.replace('\r', '\n')
152 152 elif mode == 1:
153 153 line = line.replace('\r\n', '\r')
154 154 line = line.replace('\n', '\r')
155 155 elif mode == 2:
156 156 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
157 157 return line
158 158
159 159
160 160 def detect_mode(line, default):
161 161 """
162 162 Detects line break for given line, if line break couldn't be found
163 163 given default value is returned
164 164
165 165 :param line: str line
166 166 :param default: default
167 167 :rtype: int
168 168 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
169 169 """
170 170 if line.endswith('\r\n'):
171 171 return 2
172 172 elif line.endswith('\n'):
173 173 return 0
174 174 elif line.endswith('\r'):
175 175 return 1
176 176 else:
177 177 return default
178 178
179 179
180 180 def safe_int(val, default=None):
181 181 """
182 182 Returns int() of val if val is not convertable to int use default
183 183 instead
184 184
185 185 :param val:
186 186 :param default:
187 187 """
188 188
189 189 try:
190 190 val = int(val)
191 191 except (ValueError, TypeError):
192 192 val = default
193 193
194 194 return val
195 195
196 196
197 197 def safe_unicode(str_, from_encoding=None):
198 198 """
199 199 safe unicode function. Does few trick to turn str_ into unicode
200 200
201 201 In case of UnicodeDecode error, we try to return it with encoding detected
202 202 by chardet library if it fails fallback to unicode with errors replaced
203 203
204 204 :param str_: string to decode
205 205 :rtype: unicode
206 206 :returns: unicode object
207 207 """
208 208 if isinstance(str_, unicode):
209 209 return str_
210 210
211 211 if not from_encoding:
212 212 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
213 213 'utf8'), sep=',')
214 214 from_encoding = DEFAULT_ENCODINGS
215 215
216 216 if not isinstance(from_encoding, (list, tuple)):
217 217 from_encoding = [from_encoding]
218 218
219 219 try:
220 220 return unicode(str_)
221 221 except UnicodeDecodeError:
222 222 pass
223 223
224 224 for enc in from_encoding:
225 225 try:
226 226 return unicode(str_, enc)
227 227 except UnicodeDecodeError:
228 228 pass
229 229
230 230 try:
231 231 import chardet
232 232 encoding = chardet.detect(str_)['encoding']
233 233 if encoding is None:
234 234 raise Exception()
235 235 return str_.decode(encoding)
236 236 except (ImportError, UnicodeDecodeError, Exception):
237 237 return unicode(str_, from_encoding[0], 'replace')
238 238
239 239
240 240 def safe_str(unicode_, to_encoding=None):
241 241 """
242 242 safe str function. Does few trick to turn unicode_ into string
243 243
244 244 In case of UnicodeEncodeError, we try to return it with encoding detected
245 245 by chardet library if it fails fallback to string with errors replaced
246 246
247 247 :param unicode_: unicode to encode
248 248 :rtype: str
249 249 :returns: str object
250 250 """
251 251
252 252 # if it's not basestr cast to str
253 253 if not isinstance(unicode_, basestring):
254 254 return str(unicode_)
255 255
256 256 if isinstance(unicode_, str):
257 257 return unicode_
258 258
259 259 if not to_encoding:
260 260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
261 261 'utf8'), sep=',')
262 262 to_encoding = DEFAULT_ENCODINGS
263 263
264 264 if not isinstance(to_encoding, (list, tuple)):
265 265 to_encoding = [to_encoding]
266 266
267 267 for enc in to_encoding:
268 268 try:
269 269 return unicode_.encode(enc)
270 270 except UnicodeEncodeError:
271 271 pass
272 272
273 273 try:
274 274 import chardet
275 275 encoding = chardet.detect(unicode_)['encoding']
276 276 if encoding is None:
277 277 raise UnicodeEncodeError()
278 278
279 279 return unicode_.encode(encoding)
280 280 except (ImportError, UnicodeEncodeError):
281 281 return unicode_.encode(to_encoding[0], 'replace')
282 282
283 283
284 284 def remove_suffix(s, suffix):
285 285 if s.endswith(suffix):
286 286 s = s[:-1 * len(suffix)]
287 287 return s
288 288
289 289
290 290 def remove_prefix(s, prefix):
291 291 if s.startswith(prefix):
292 292 s = s[len(prefix):]
293 293 return s
294 294
295 295
296 296 def find_calling_context(ignore_modules=None):
297 297 """
298 298 Look through the calling stack and return the frame which called
299 299 this function and is part of core module ( ie. rhodecode.* )
300 300
301 301 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
302 302 """
303 303
304 304 ignore_modules = ignore_modules or []
305 305
306 306 f = sys._getframe(2)
307 307 while f.f_back is not None:
308 308 name = f.f_globals.get('__name__')
309 309 if name and name.startswith(__name__.split('.')[0]):
310 310 if name not in ignore_modules:
311 311 return f
312 312 f = f.f_back
313 313 return None
314 314
315 315
316 316 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
317 317 """Custom engine_from_config functions."""
318 318 log = logging.getLogger('sqlalchemy.engine')
319 319 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
320 320
321 321 def color_sql(sql):
322 322 color_seq = '\033[1;33m' # This is yellow: code 33
323 323 normal = '\x1b[0m'
324 324 return ''.join([color_seq, sql, normal])
325 325
326 326 if configuration['debug']:
327 327 # attach events only for debug configuration
328 328
329 329 def before_cursor_execute(conn, cursor, statement,
330 330 parameters, context, executemany):
331 331 setattr(conn, 'query_start_time', time.time())
332 332 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
333 333 calling_context = find_calling_context(ignore_modules=[
334 334 'rhodecode.lib.caching_query',
335 335 'rhodecode.model.settings',
336 336 ])
337 337 if calling_context:
338 338 log.info(color_sql('call context %s:%s' % (
339 339 calling_context.f_code.co_filename,
340 340 calling_context.f_lineno,
341 341 )))
342 342
343 343 def after_cursor_execute(conn, cursor, statement,
344 344 parameters, context, executemany):
345 345 delattr(conn, 'query_start_time')
346 346
347 347 sqlalchemy.event.listen(engine, "before_cursor_execute",
348 348 before_cursor_execute)
349 349 sqlalchemy.event.listen(engine, "after_cursor_execute",
350 350 after_cursor_execute)
351 351
352 352 return engine
353 353
354 354
355 355 def get_encryption_key(config):
356 356 secret = config.get('rhodecode.encrypted_values.secret')
357 357 default = config['beaker.session.secret']
358 358 return secret or default
359 359
360 360
361 361 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
362 362 short_format=False):
363 363 """
364 364 Turns a datetime into an age string.
365 365 If show_short_version is True, this generates a shorter string with
366 366 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
367 367
368 368 * IMPORTANT*
369 369 Code of this function is written in special way so it's easier to
370 370 backport it to javascript. If you mean to update it, please also update
371 371 `jquery.timeago-extension.js` file
372 372
373 373 :param prevdate: datetime object
374 374 :param now: get current time, if not define we use
375 375 `datetime.datetime.now()`
376 376 :param show_short_version: if it should approximate the date and
377 377 return a shorter string
378 378 :param show_suffix:
379 379 :param short_format: show short format, eg 2D instead of 2 days
380 380 :rtype: unicode
381 381 :returns: unicode words describing age
382 382 """
383 383 from pylons.i18n.translation import _, ungettext
384 384
385 385 def _get_relative_delta(now, prevdate):
386 386 base = dateutil.relativedelta.relativedelta(now, prevdate)
387 387 return {
388 388 'year': base.years,
389 389 'month': base.months,
390 390 'day': base.days,
391 391 'hour': base.hours,
392 392 'minute': base.minutes,
393 393 'second': base.seconds,
394 394 }
395 395
396 396 def _is_leap_year(year):
397 397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
398 398
399 399 def get_month(prevdate):
400 400 return prevdate.month
401 401
402 402 def get_year(prevdate):
403 403 return prevdate.year
404 404
405 405 now = now or datetime.datetime.now()
406 406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
407 407 deltas = {}
408 408 future = False
409 409
410 410 if prevdate > now:
411 411 now_old = now
412 412 now = prevdate
413 413 prevdate = now_old
414 414 future = True
415 415 if future:
416 416 prevdate = prevdate.replace(microsecond=0)
417 417 # Get date parts deltas
418 418 for part in order:
419 419 rel_delta = _get_relative_delta(now, prevdate)
420 420 deltas[part] = rel_delta[part]
421 421
422 422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
423 423 # not 1 hour, -59 minutes and -59 seconds)
424 424 offsets = [[5, 60], [4, 60], [3, 24]]
425 425 for element in offsets: # seconds, minutes, hours
426 426 num = element[0]
427 427 length = element[1]
428 428
429 429 part = order[num]
430 430 carry_part = order[num - 1]
431 431
432 432 if deltas[part] < 0:
433 433 deltas[part] += length
434 434 deltas[carry_part] -= 1
435 435
436 436 # Same thing for days except that the increment depends on the (variable)
437 437 # number of days in the month
438 438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
439 439 if deltas['day'] < 0:
440 440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
441 441 deltas['day'] += 29
442 442 else:
443 443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
444 444
445 445 deltas['month'] -= 1
446 446
447 447 if deltas['month'] < 0:
448 448 deltas['month'] += 12
449 449 deltas['year'] -= 1
450 450
451 451 # Format the result
452 452 if short_format:
453 453 fmt_funcs = {
454 454 'year': lambda d: u'%dy' % d,
455 455 'month': lambda d: u'%dm' % d,
456 456 'day': lambda d: u'%dd' % d,
457 457 'hour': lambda d: u'%dh' % d,
458 458 'minute': lambda d: u'%dmin' % d,
459 459 'second': lambda d: u'%dsec' % d,
460 460 }
461 461 else:
462 462 fmt_funcs = {
463 463 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
464 464 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
465 465 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
466 466 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
467 467 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
468 468 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
469 469 }
470 470
471 471 i = 0
472 472 for part in order:
473 473 value = deltas[part]
474 474 if value != 0:
475 475
476 476 if i < 5:
477 477 sub_part = order[i + 1]
478 478 sub_value = deltas[sub_part]
479 479 else:
480 480 sub_value = 0
481 481
482 482 if sub_value == 0 or show_short_version:
483 483 _val = fmt_funcs[part](value)
484 484 if future:
485 485 if show_suffix:
486 486 return _(u'in %s') % _val
487 487 else:
488 488 return _val
489 489
490 490 else:
491 491 if show_suffix:
492 492 return _(u'%s ago') % _val
493 493 else:
494 494 return _val
495 495
496 496 val = fmt_funcs[part](value)
497 497 val_detail = fmt_funcs[sub_part](sub_value)
498 498
499 499 if short_format:
500 500 datetime_tmpl = u'%s, %s'
501 501 if show_suffix:
502 502 datetime_tmpl = _(u'%s, %s ago')
503 503 if future:
504 504 datetime_tmpl = _(u'in %s, %s')
505 505 else:
506 506 datetime_tmpl = _(u'%s and %s')
507 507 if show_suffix:
508 508 datetime_tmpl = _(u'%s and %s ago')
509 509 if future:
510 510 datetime_tmpl = _(u'in %s and %s')
511 511
512 512 return datetime_tmpl % (val, val_detail)
513 513 i += 1
514 514 return _(u'just now')
515 515
516 516
517 517 def uri_filter(uri):
518 518 """
519 519 Removes user:password from given url string
520 520
521 521 :param uri:
522 522 :rtype: unicode
523 523 :returns: filtered list of strings
524 524 """
525 525 if not uri:
526 526 return ''
527 527
528 528 proto = ''
529 529
530 530 for pat in ('https://', 'http://'):
531 531 if uri.startswith(pat):
532 532 uri = uri[len(pat):]
533 533 proto = pat
534 534 break
535 535
536 536 # remove passwords and username
537 537 uri = uri[uri.find('@') + 1:]
538 538
539 539 # get the port
540 540 cred_pos = uri.find(':')
541 541 if cred_pos == -1:
542 542 host, port = uri, None
543 543 else:
544 544 host, port = uri[:cred_pos], uri[cred_pos + 1:]
545 545
546 546 return filter(None, [proto, host, port])
547 547
548 548
549 549 def credentials_filter(uri):
550 550 """
551 551 Returns a url with removed credentials
552 552
553 553 :param uri:
554 554 """
555 555
556 556 uri = uri_filter(uri)
557 557 # check if we have port
558 558 if len(uri) > 2 and uri[2]:
559 559 uri[2] = ':' + uri[2]
560 560
561 561 return ''.join(uri)
562 562
563 563
564 564 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
565 565 parsed_url = urlobject.URLObject(qualifed_home_url)
566 566 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
567 567 args = {
568 568 'scheme': parsed_url.scheme,
569 569 'user': '',
570 570 # path if we use proxy-prefix
571 571 'netloc': parsed_url.netloc+decoded_path,
572 572 'prefix': decoded_path,
573 573 'repo': repo_name,
574 574 'repoid': str(repo_id)
575 575 }
576 576 args.update(override)
577 577 args['user'] = urllib.quote(safe_str(args['user']))
578 578
579 579 for k, v in args.items():
580 580 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
581 581
582 582 # remove leading @ sign if it's present. Case of empty user
583 583 url_obj = urlobject.URLObject(uri_tmpl)
584 584 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
585 585
586 586 return safe_unicode(url)
587 587
588 588
589 589 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
590 590 """
591 591 Safe version of get_commit if this commit doesn't exists for a
592 592 repository it returns a Dummy one instead
593 593
594 594 :param repo: repository instance
595 595 :param commit_id: commit id as str
596 596 :param pre_load: optional list of commit attributes to load
597 597 """
598 598 # TODO(skreft): remove these circular imports
599 599 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
600 600 from rhodecode.lib.vcs.exceptions import RepositoryError
601 601 if not isinstance(repo, BaseRepository):
602 602 raise Exception('You must pass an Repository '
603 603 'object as first argument got %s', type(repo))
604 604
605 605 try:
606 606 commit = repo.get_commit(
607 607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
608 608 except (RepositoryError, LookupError):
609 609 commit = EmptyCommit()
610 610 return commit
611 611
612 612
613 613 def datetime_to_time(dt):
614 614 if dt:
615 615 return time.mktime(dt.timetuple())
616 616
617 617
618 618 def time_to_datetime(tm):
619 619 if tm:
620 620 if isinstance(tm, basestring):
621 621 try:
622 622 tm = float(tm)
623 623 except ValueError:
624 624 return
625 625 return datetime.datetime.fromtimestamp(tm)
626 626
627 627
628 628 def time_to_utcdatetime(tm):
629 629 if tm:
630 630 if isinstance(tm, basestring):
631 631 try:
632 632 tm = float(tm)
633 633 except ValueError:
634 634 return
635 635 return datetime.datetime.utcfromtimestamp(tm)
636 636
637 637
638 638 MENTIONS_REGEX = re.compile(
639 639 # ^@ or @ without any special chars in front
640 640 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
641 641 # main body starts with letter, then can be . - _
642 642 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
643 643 re.VERBOSE | re.MULTILINE)
644 644
645 645
646 646 def extract_mentioned_users(s):
647 647 """
648 648 Returns unique usernames from given string s that have @mention
649 649
650 650 :param s: string to get mentions
651 651 """
652 652 usrs = set()
653 653 for username in MENTIONS_REGEX.findall(s):
654 654 usrs.add(username)
655 655
656 656 return sorted(list(usrs), key=lambda k: k.lower())
657 657
658 658
659 659 class AttributeDict(dict):
660 660 def __getattr__(self, attr):
661 661 return self.get(attr, None)
662 662 __setattr__ = dict.__setitem__
663 663 __delattr__ = dict.__delitem__
664 664
665 665
666 666 def fix_PATH(os_=None):
667 667 """
668 668 Get current active python path, and append it to PATH variable to fix
669 669 issues of subprocess calls and different python versions
670 670 """
671 671 if os_ is None:
672 672 import os
673 673 else:
674 674 os = os_
675 675
676 676 cur_path = os.path.split(sys.executable)[0]
677 677 if not os.environ['PATH'].startswith(cur_path):
678 678 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
679 679
680 680
681 681 def obfuscate_url_pw(engine):
682 682 _url = engine or ''
683 683 try:
684 684 _url = sqlalchemy.engine.url.make_url(engine)
685 685 if _url.password:
686 686 _url.password = 'XXXXX'
687 687 except Exception:
688 688 pass
689 689 return unicode(_url)
690 690
691 691
692 692 def get_server_url(environ):
693 693 req = webob.Request(environ)
694 694 return req.host_url + req.script_name
695 695
696 696
697 697 def unique_id(hexlen=32):
698 698 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
699 699 return suuid(truncate_to=hexlen, alphabet=alphabet)
700 700
701 701
702 702 def suuid(url=None, truncate_to=22, alphabet=None):
703 703 """
704 704 Generate and return a short URL safe UUID.
705 705
706 706 If the url parameter is provided, set the namespace to the provided
707 707 URL and generate a UUID.
708 708
709 709 :param url to get the uuid for
710 710 :truncate_to: truncate the basic 22 UUID to shorter version
711 711
712 712 The IDs won't be universally unique any longer, but the probability of
713 713 a collision will still be very low.
714 714 """
715 715 # Define our alphabet.
716 716 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
717 717
718 718 # If no URL is given, generate a random UUID.
719 719 if url is None:
720 720 unique_id = uuid.uuid4().int
721 721 else:
722 722 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
723 723
724 724 alphabet_length = len(_ALPHABET)
725 725 output = []
726 726 while unique_id > 0:
727 727 digit = unique_id % alphabet_length
728 728 output.append(_ALPHABET[digit])
729 729 unique_id = int(unique_id / alphabet_length)
730 730 return "".join(output)[:truncate_to]
731 731
732 732
733 733 def get_current_rhodecode_user():
734 734 """
735 735 Gets rhodecode user from threadlocal tmpl_context variable if it's
736 736 defined, else returns None.
737 737 """
738 738 from pylons import tmpl_context as c
739 739 if hasattr(c, 'rhodecode_user'):
740 740 return c.rhodecode_user
741 741
742 742 return None
743 743
744 744
745 745 def action_logger_generic(action, namespace=''):
746 746 """
747 747 A generic logger for actions useful to the system overview, tries to find
748 748 an acting user for the context of the call otherwise reports unknown user
749 749
750 750 :param action: logging message eg 'comment 5 deleted'
751 751 :param type: string
752 752
753 753 :param namespace: namespace of the logging message eg. 'repo.comments'
754 754 :param type: string
755 755
756 756 """
757 757
758 758 logger_name = 'rhodecode.actions'
759 759
760 760 if namespace:
761 761 logger_name += '.' + namespace
762 762
763 763 log = logging.getLogger(logger_name)
764 764
765 765 # get a user if we can
766 766 user = get_current_rhodecode_user()
767 767
768 768 logfunc = log.info
769 769
770 770 if not user:
771 771 user = '<unknown user>'
772 772 logfunc = log.warning
773 773
774 774 logfunc('Logging action by {}: {}'.format(user, action))
775 775
776 776
777 777 def escape_split(text, sep=',', maxsplit=-1):
778 778 r"""
779 779 Allows for escaping of the separator: e.g. arg='foo\, bar'
780 780
781 781 It should be noted that the way bash et. al. do command line parsing, those
782 782 single quotes are required.
783 783 """
784 784 escaped_sep = r'\%s' % sep
785 785
786 786 if escaped_sep not in text:
787 787 return text.split(sep, maxsplit)
788 788
789 789 before, _mid, after = text.partition(escaped_sep)
790 790 startlist = before.split(sep, maxsplit) # a regular split is fine here
791 791 unfinished = startlist[-1]
792 792 startlist = startlist[:-1]
793 793
794 794 # recurse because there may be more escaped separators
795 795 endlist = escape_split(after, sep, maxsplit)
796 796
797 797 # finish building the escaped value. we use endlist[0] becaue the first
798 798 # part of the string sent in recursion is the rest of the escaped value.
799 799 unfinished += sep + endlist[0]
800 800
801 801 return startlist + [unfinished] + endlist[1:] # put together all the parts
802 802
803 803
804 804 class OptionalAttr(object):
805 805 """
806 806 Special Optional Option that defines other attribute. Example::
807 807
808 808 def test(apiuser, userid=Optional(OAttr('apiuser')):
809 809 user = Optional.extract(userid)
810 810 # calls
811 811
812 812 """
813 813
814 814 def __init__(self, attr_name):
815 815 self.attr_name = attr_name
816 816
817 817 def __repr__(self):
818 818 return '<OptionalAttr:%s>' % self.attr_name
819 819
820 820 def __call__(self):
821 821 return self
822 822
823 823
824 824 # alias
825 825 OAttr = OptionalAttr
826 826
827 827
828 828 class Optional(object):
829 829 """
830 830 Defines an optional parameter::
831 831
832 832 param = param.getval() if isinstance(param, Optional) else param
833 833 param = param() if isinstance(param, Optional) else param
834 834
835 835 is equivalent of::
836 836
837 837 param = Optional.extract(param)
838 838
839 839 """
840 840
841 841 def __init__(self, type_):
842 842 self.type_ = type_
843 843
844 844 def __repr__(self):
845 845 return '<Optional:%s>' % self.type_.__repr__()
846 846
847 847 def __call__(self):
848 848 return self.getval()
849 849
850 850 def getval(self):
851 851 """
852 852 returns value from this Optional instance
853 853 """
854 854 if isinstance(self.type_, OAttr):
855 855 # use params name
856 856 return self.type_.attr_name
857 857 return self.type_
858 858
859 859 @classmethod
860 860 def extract(cls, val):
861 861 """
862 862 Extracts value from Optional() instance
863 863
864 864 :param val:
865 865 :return: original value if it's not Optional instance else
866 866 value of instance
867 867 """
868 868 if isinstance(val, cls):
869 869 return val.getval()
870 870 return val
871 871
872 872
873 873 def get_routes_generator_for_server_url(server_url):
874 874 parsed_url = urlobject.URLObject(server_url)
875 875 netloc = safe_str(parsed_url.netloc)
876 876 script_name = safe_str(parsed_url.path)
877 877
878 878 if ':' in netloc:
879 879 server_name, server_port = netloc.split(':')
880 880 else:
881 881 server_name = netloc
882 882 server_port = (parsed_url.scheme == 'https' and '443' or '80')
883 883
884 884 environ = {
885 885 'REQUEST_METHOD': 'GET',
886 886 'PATH_INFO': '/',
887 887 'SERVER_NAME': server_name,
888 888 'SERVER_PORT': server_port,
889 889 'SCRIPT_NAME': script_name,
890 890 }
891 891 if parsed_url.scheme == 'https':
892 892 environ['HTTPS'] = 'on'
893 893 environ['wsgi.url_scheme'] = 'https'
894 894
895 895 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
896 896
897 897
898 898 def glob2re(pat):
899 899 """
900 900 Translate a shell PATTERN to a regular expression.
901 901
902 902 There is no way to quote meta-characters.
903 903 """
904 904
905 905 i, n = 0, len(pat)
906 906 res = ''
907 907 while i < n:
908 908 c = pat[i]
909 909 i = i+1
910 910 if c == '*':
911 911 #res = res + '.*'
912 912 res = res + '[^/]*'
913 913 elif c == '?':
914 914 #res = res + '.'
915 915 res = res + '[^/]'
916 916 elif c == '[':
917 917 j = i
918 918 if j < n and pat[j] == '!':
919 919 j = j+1
920 920 if j < n and pat[j] == ']':
921 921 j = j+1
922 922 while j < n and pat[j] != ']':
923 923 j = j+1
924 924 if j >= n:
925 925 res = res + '\\['
926 926 else:
927 927 stuff = pat[i:j].replace('\\','\\\\')
928 928 i = j+1
929 929 if stuff[0] == '!':
930 930 stuff = '^' + stuff[1:]
931 931 elif stuff[0] == '^':
932 932 stuff = '\\' + stuff
933 933 res = '%s[%s]' % (res, stuff)
934 934 else:
935 935 res = res + re.escape(c)
936 936 return res + '\Z(?ms)'
@@ -1,3693 +1,3701 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 584 feed_tokens = UserApiKeys.query()\
585 585 .filter(UserApiKeys.user == self)\
586 586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 587 .all()
588 588 if feed_tokens:
589 589 return feed_tokens[0].api_key
590 590 else:
591 591 # use the main token so we don't end up with nothing...
592 592 return self.api_key
593 593
594 594 @classmethod
595 595 def extra_valid_auth_tokens(cls, user, role=None):
596 596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 597 .filter(or_(UserApiKeys.expires == -1,
598 598 UserApiKeys.expires >= time.time()))
599 599 if role:
600 600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 602 return tokens.all()
603 603
604 604 @property
605 605 def ip_addresses(self):
606 606 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
607 607 return [x.ip_addr for x in ret]
608 608
609 609 @property
610 610 def username_and_name(self):
611 611 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
612 612
613 613 @property
614 614 def username_or_name_or_email(self):
615 615 full_name = self.full_name if self.full_name is not ' ' else None
616 616 return self.username or full_name or self.email
617 617
618 618 @property
619 619 def full_name(self):
620 620 return '%s %s' % (self.firstname, self.lastname)
621 621
622 622 @property
623 623 def full_name_or_username(self):
624 624 return ('%s %s' % (self.firstname, self.lastname)
625 625 if (self.firstname and self.lastname) else self.username)
626 626
627 627 @property
628 628 def full_contact(self):
629 629 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
630 630
631 631 @property
632 632 def short_contact(self):
633 633 return '%s %s' % (self.firstname, self.lastname)
634 634
635 635 @property
636 636 def is_admin(self):
637 637 return self.admin
638 638
639 639 @property
640 640 def AuthUser(self):
641 641 """
642 642 Returns instance of AuthUser for this user
643 643 """
644 644 from rhodecode.lib.auth import AuthUser
645 645 return AuthUser(user_id=self.user_id, api_key=self.api_key,
646 646 username=self.username)
647 647
648 648 @hybrid_property
649 649 def user_data(self):
650 650 if not self._user_data:
651 651 return {}
652 652
653 653 try:
654 654 return json.loads(self._user_data)
655 655 except TypeError:
656 656 return {}
657 657
658 658 @user_data.setter
659 659 def user_data(self, val):
660 660 if not isinstance(val, dict):
661 661 raise Exception('user_data must be dict, got %s' % type(val))
662 662 try:
663 663 self._user_data = json.dumps(val)
664 664 except Exception:
665 665 log.error(traceback.format_exc())
666 666
667 667 @classmethod
668 668 def get_by_username(cls, username, case_insensitive=False,
669 669 cache=False, identity_cache=False):
670 670 session = Session()
671 671
672 672 if case_insensitive:
673 673 q = cls.query().filter(
674 674 func.lower(cls.username) == func.lower(username))
675 675 else:
676 676 q = cls.query().filter(cls.username == username)
677 677
678 678 if cache:
679 679 if identity_cache:
680 680 val = cls.identity_cache(session, 'username', username)
681 681 if val:
682 682 return val
683 683 else:
684 684 q = q.options(
685 685 FromCache("sql_cache_short",
686 686 "get_user_by_name_%s" % _hash_key(username)))
687 687
688 688 return q.scalar()
689 689
690 690 @classmethod
691 691 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
692 692 q = cls.query().filter(cls.api_key == auth_token)
693 693
694 694 if cache:
695 695 q = q.options(FromCache("sql_cache_short",
696 696 "get_auth_token_%s" % auth_token))
697 697 res = q.scalar()
698 698
699 699 if fallback and not res:
700 700 #fallback to additional keys
701 701 _res = UserApiKeys.query()\
702 702 .filter(UserApiKeys.api_key == auth_token)\
703 703 .filter(or_(UserApiKeys.expires == -1,
704 704 UserApiKeys.expires >= time.time()))\
705 705 .first()
706 706 if _res:
707 707 res = _res.user
708 708 return res
709 709
710 710 @classmethod
711 711 def get_by_email(cls, email, case_insensitive=False, cache=False):
712 712
713 713 if case_insensitive:
714 714 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
715 715
716 716 else:
717 717 q = cls.query().filter(cls.email == email)
718 718
719 719 if cache:
720 720 q = q.options(FromCache("sql_cache_short",
721 721 "get_email_key_%s" % _hash_key(email)))
722 722
723 723 ret = q.scalar()
724 724 if ret is None:
725 725 q = UserEmailMap.query()
726 726 # try fetching in alternate email map
727 727 if case_insensitive:
728 728 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
729 729 else:
730 730 q = q.filter(UserEmailMap.email == email)
731 731 q = q.options(joinedload(UserEmailMap.user))
732 732 if cache:
733 733 q = q.options(FromCache("sql_cache_short",
734 734 "get_email_map_key_%s" % email))
735 735 ret = getattr(q.scalar(), 'user', None)
736 736
737 737 return ret
738 738
739 739 @classmethod
740 740 def get_from_cs_author(cls, author):
741 741 """
742 742 Tries to get User objects out of commit author string
743 743
744 744 :param author:
745 745 """
746 746 from rhodecode.lib.helpers import email, author_name
747 747 # Valid email in the attribute passed, see if they're in the system
748 748 _email = email(author)
749 749 if _email:
750 750 user = cls.get_by_email(_email, case_insensitive=True)
751 751 if user:
752 752 return user
753 753 # Maybe we can match by username?
754 754 _author = author_name(author)
755 755 user = cls.get_by_username(_author, case_insensitive=True)
756 756 if user:
757 757 return user
758 758
759 759 def update_userdata(self, **kwargs):
760 760 usr = self
761 761 old = usr.user_data
762 762 old.update(**kwargs)
763 763 usr.user_data = old
764 764 Session().add(usr)
765 765 log.debug('updated userdata with ', kwargs)
766 766
767 767 def update_lastlogin(self):
768 768 """Update user lastlogin"""
769 769 self.last_login = datetime.datetime.now()
770 770 Session().add(self)
771 771 log.debug('updated user %s lastlogin', self.username)
772 772
773 773 def update_lastactivity(self):
774 774 """Update user lastactivity"""
775 775 usr = self
776 776 old = usr.user_data
777 777 old.update({'last_activity': time.time()})
778 778 usr.user_data = old
779 779 Session().add(usr)
780 780 log.debug('updated user %s lastactivity', usr.username)
781 781
782 782 def update_password(self, new_password, change_api_key=False):
783 783 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
784 784
785 785 self.password = get_crypt_password(new_password)
786 786 if change_api_key:
787 787 self.api_key = generate_auth_token(self.username)
788 788 Session().add(self)
789 789
790 790 @classmethod
791 791 def get_first_super_admin(cls):
792 792 user = User.query().filter(User.admin == true()).first()
793 793 if user is None:
794 794 raise Exception('FATAL: Missing administrative account!')
795 795 return user
796 796
797 797 @classmethod
798 798 def get_all_super_admins(cls):
799 799 """
800 800 Returns all admin accounts sorted by username
801 801 """
802 802 return User.query().filter(User.admin == true())\
803 803 .order_by(User.username.asc()).all()
804 804
805 805 @classmethod
806 806 def get_default_user(cls, cache=False):
807 807 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
808 808 if user is None:
809 809 raise Exception('FATAL: Missing default account!')
810 810 return user
811 811
812 812 def _get_default_perms(self, user, suffix=''):
813 813 from rhodecode.model.permission import PermissionModel
814 814 return PermissionModel().get_default_perms(user.user_perms, suffix)
815 815
816 816 def get_default_perms(self, suffix=''):
817 817 return self._get_default_perms(self, suffix)
818 818
819 819 def get_api_data(self, include_secrets=False, details='full'):
820 820 """
821 821 Common function for generating user related data for API
822 822
823 823 :param include_secrets: By default secrets in the API data will be replaced
824 824 by a placeholder value to prevent exposing this data by accident. In case
825 825 this data shall be exposed, set this flag to ``True``.
826 826
827 827 :param details: details can be 'basic|full' basic gives only a subset of
828 828 the available user information that includes user_id, name and emails.
829 829 """
830 830 user = self
831 831 user_data = self.user_data
832 832 data = {
833 833 'user_id': user.user_id,
834 834 'username': user.username,
835 835 'firstname': user.name,
836 836 'lastname': user.lastname,
837 837 'email': user.email,
838 838 'emails': user.emails,
839 839 }
840 840 if details == 'basic':
841 841 return data
842 842
843 843 api_key_length = 40
844 844 api_key_replacement = '*' * api_key_length
845 845
846 846 extras = {
847 847 'api_key': api_key_replacement,
848 848 'api_keys': [api_key_replacement],
849 849 'active': user.active,
850 850 'admin': user.admin,
851 851 'extern_type': user.extern_type,
852 852 'extern_name': user.extern_name,
853 853 'last_login': user.last_login,
854 854 'ip_addresses': user.ip_addresses,
855 855 'language': user_data.get('language')
856 856 }
857 857 data.update(extras)
858 858
859 859 if include_secrets:
860 860 data['api_key'] = user.api_key
861 861 data['api_keys'] = user.auth_tokens
862 862 return data
863 863
864 864 def __json__(self):
865 865 data = {
866 866 'full_name': self.full_name,
867 867 'full_name_or_username': self.full_name_or_username,
868 868 'short_contact': self.short_contact,
869 869 'full_contact': self.full_contact,
870 870 }
871 871 data.update(self.get_api_data())
872 872 return data
873 873
874 874
875 875 class UserApiKeys(Base, BaseModel):
876 876 __tablename__ = 'user_api_keys'
877 877 __table_args__ = (
878 878 Index('uak_api_key_idx', 'api_key'),
879 879 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
880 880 UniqueConstraint('api_key'),
881 881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
882 882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
883 883 )
884 884 __mapper_args__ = {}
885 885
886 886 # ApiKey role
887 887 ROLE_ALL = 'token_role_all'
888 888 ROLE_HTTP = 'token_role_http'
889 889 ROLE_VCS = 'token_role_vcs'
890 890 ROLE_API = 'token_role_api'
891 891 ROLE_FEED = 'token_role_feed'
892 892 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
893 893
894 894 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
895 895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
896 896 api_key = Column("api_key", String(255), nullable=False, unique=True)
897 897 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
898 898 expires = Column('expires', Float(53), nullable=False)
899 899 role = Column('role', String(255), nullable=True)
900 900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
901 901
902 902 user = relationship('User', lazy='joined')
903 903
904 904 @classmethod
905 905 def _get_role_name(cls, role):
906 906 return {
907 907 cls.ROLE_ALL: _('all'),
908 908 cls.ROLE_HTTP: _('http/web interface'),
909 909 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
910 910 cls.ROLE_API: _('api calls'),
911 911 cls.ROLE_FEED: _('feed access'),
912 912 }.get(role, role)
913 913
914 914 @property
915 915 def expired(self):
916 916 if self.expires == -1:
917 917 return False
918 918 return time.time() > self.expires
919 919
920 920 @property
921 921 def role_humanized(self):
922 922 return self._get_role_name(self.role)
923 923
924 924
925 925 class UserEmailMap(Base, BaseModel):
926 926 __tablename__ = 'user_email_map'
927 927 __table_args__ = (
928 928 Index('uem_email_idx', 'email'),
929 929 UniqueConstraint('email'),
930 930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
931 931 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
932 932 )
933 933 __mapper_args__ = {}
934 934
935 935 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
936 936 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
937 937 _email = Column("email", String(255), nullable=True, unique=False, default=None)
938 938 user = relationship('User', lazy='joined')
939 939
940 940 @validates('_email')
941 941 def validate_email(self, key, email):
942 942 # check if this email is not main one
943 943 main_email = Session().query(User).filter(User.email == email).scalar()
944 944 if main_email is not None:
945 945 raise AttributeError('email %s is present is user table' % email)
946 946 return email
947 947
948 948 @hybrid_property
949 949 def email(self):
950 950 return self._email
951 951
952 952 @email.setter
953 953 def email(self, val):
954 954 self._email = val.lower() if val else None
955 955
956 956
957 957 class UserIpMap(Base, BaseModel):
958 958 __tablename__ = 'user_ip_map'
959 959 __table_args__ = (
960 960 UniqueConstraint('user_id', 'ip_addr'),
961 961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
962 962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
963 963 )
964 964 __mapper_args__ = {}
965 965
966 966 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
967 967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
968 968 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
969 969 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
970 970 description = Column("description", String(10000), nullable=True, unique=None, default=None)
971 971 user = relationship('User', lazy='joined')
972 972
973 973 @classmethod
974 974 def _get_ip_range(cls, ip_addr):
975 975 net = ipaddress.ip_network(ip_addr, strict=False)
976 976 return [str(net.network_address), str(net.broadcast_address)]
977 977
978 978 def __json__(self):
979 979 return {
980 980 'ip_addr': self.ip_addr,
981 981 'ip_range': self._get_ip_range(self.ip_addr),
982 982 }
983 983
984 984 def __unicode__(self):
985 985 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
986 986 self.user_id, self.ip_addr)
987 987
988 988 class UserLog(Base, BaseModel):
989 989 __tablename__ = 'user_logs'
990 990 __table_args__ = (
991 991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
992 992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
993 993 )
994 994 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
995 995 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
996 996 username = Column("username", String(255), nullable=True, unique=None, default=None)
997 997 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
998 998 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
999 999 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1000 1000 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1001 1001 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1002 1002
1003 1003 def __unicode__(self):
1004 1004 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1005 1005 self.repository_name,
1006 1006 self.action)
1007 1007
1008 1008 @property
1009 1009 def action_as_day(self):
1010 1010 return datetime.date(*self.action_date.timetuple()[:3])
1011 1011
1012 1012 user = relationship('User')
1013 1013 repository = relationship('Repository', cascade='')
1014 1014
1015 1015
1016 1016 class UserGroup(Base, BaseModel):
1017 1017 __tablename__ = 'users_groups'
1018 1018 __table_args__ = (
1019 1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1020 1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1021 1021 )
1022 1022
1023 1023 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1024 1024 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1025 1025 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1026 1026 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1027 1027 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1028 1028 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1029 1029 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1030 1030 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1031 1031
1032 1032 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1033 1033 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1034 1034 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1035 1035 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1036 1036 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1037 1037 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1038 1038
1039 1039 user = relationship('User')
1040 1040
1041 1041 @hybrid_property
1042 1042 def group_data(self):
1043 1043 if not self._group_data:
1044 1044 return {}
1045 1045
1046 1046 try:
1047 1047 return json.loads(self._group_data)
1048 1048 except TypeError:
1049 1049 return {}
1050 1050
1051 1051 @group_data.setter
1052 1052 def group_data(self, val):
1053 1053 try:
1054 1054 self._group_data = json.dumps(val)
1055 1055 except Exception:
1056 1056 log.error(traceback.format_exc())
1057 1057
1058 1058 def __unicode__(self):
1059 1059 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1060 1060 self.users_group_id,
1061 1061 self.users_group_name)
1062 1062
1063 1063 @classmethod
1064 1064 def get_by_group_name(cls, group_name, cache=False,
1065 1065 case_insensitive=False):
1066 1066 if case_insensitive:
1067 1067 q = cls.query().filter(func.lower(cls.users_group_name) ==
1068 1068 func.lower(group_name))
1069 1069
1070 1070 else:
1071 1071 q = cls.query().filter(cls.users_group_name == group_name)
1072 1072 if cache:
1073 1073 q = q.options(FromCache(
1074 1074 "sql_cache_short",
1075 1075 "get_group_%s" % _hash_key(group_name)))
1076 1076 return q.scalar()
1077 1077
1078 1078 @classmethod
1079 1079 def get(cls, user_group_id, cache=False):
1080 1080 user_group = cls.query()
1081 1081 if cache:
1082 1082 user_group = user_group.options(FromCache("sql_cache_short",
1083 1083 "get_users_group_%s" % user_group_id))
1084 1084 return user_group.get(user_group_id)
1085 1085
1086 1086 def permissions(self, with_admins=True, with_owner=True):
1087 1087 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1088 1088 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1089 1089 joinedload(UserUserGroupToPerm.user),
1090 1090 joinedload(UserUserGroupToPerm.permission),)
1091 1091
1092 1092 # get owners and admins and permissions. We do a trick of re-writing
1093 1093 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1094 1094 # has a global reference and changing one object propagates to all
1095 1095 # others. This means if admin is also an owner admin_row that change
1096 1096 # would propagate to both objects
1097 1097 perm_rows = []
1098 1098 for _usr in q.all():
1099 1099 usr = AttributeDict(_usr.user.get_dict())
1100 1100 usr.permission = _usr.permission.permission_name
1101 1101 perm_rows.append(usr)
1102 1102
1103 1103 # filter the perm rows by 'default' first and then sort them by
1104 1104 # admin,write,read,none permissions sorted again alphabetically in
1105 1105 # each group
1106 1106 perm_rows = sorted(perm_rows, key=display_sort)
1107 1107
1108 1108 _admin_perm = 'usergroup.admin'
1109 1109 owner_row = []
1110 1110 if with_owner:
1111 1111 usr = AttributeDict(self.user.get_dict())
1112 1112 usr.owner_row = True
1113 1113 usr.permission = _admin_perm
1114 1114 owner_row.append(usr)
1115 1115
1116 1116 super_admin_rows = []
1117 1117 if with_admins:
1118 1118 for usr in User.get_all_super_admins():
1119 1119 # if this admin is also owner, don't double the record
1120 1120 if usr.user_id == owner_row[0].user_id:
1121 1121 owner_row[0].admin_row = True
1122 1122 else:
1123 1123 usr = AttributeDict(usr.get_dict())
1124 1124 usr.admin_row = True
1125 1125 usr.permission = _admin_perm
1126 1126 super_admin_rows.append(usr)
1127 1127
1128 1128 return super_admin_rows + owner_row + perm_rows
1129 1129
1130 1130 def permission_user_groups(self):
1131 1131 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1132 1132 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1133 1133 joinedload(UserGroupUserGroupToPerm.target_user_group),
1134 1134 joinedload(UserGroupUserGroupToPerm.permission),)
1135 1135
1136 1136 perm_rows = []
1137 1137 for _user_group in q.all():
1138 1138 usr = AttributeDict(_user_group.user_group.get_dict())
1139 1139 usr.permission = _user_group.permission.permission_name
1140 1140 perm_rows.append(usr)
1141 1141
1142 1142 return perm_rows
1143 1143
1144 1144 def _get_default_perms(self, user_group, suffix=''):
1145 1145 from rhodecode.model.permission import PermissionModel
1146 1146 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1147 1147
1148 1148 def get_default_perms(self, suffix=''):
1149 1149 return self._get_default_perms(self, suffix)
1150 1150
1151 1151 def get_api_data(self, with_group_members=True, include_secrets=False):
1152 1152 """
1153 1153 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1154 1154 basically forwarded.
1155 1155
1156 1156 """
1157 1157 user_group = self
1158 1158
1159 1159 data = {
1160 1160 'users_group_id': user_group.users_group_id,
1161 1161 'group_name': user_group.users_group_name,
1162 1162 'group_description': user_group.user_group_description,
1163 1163 'active': user_group.users_group_active,
1164 1164 'owner': user_group.user.username,
1165 1165 }
1166 1166 if with_group_members:
1167 1167 users = []
1168 1168 for user in user_group.members:
1169 1169 user = user.user
1170 1170 users.append(user.get_api_data(include_secrets=include_secrets))
1171 1171 data['users'] = users
1172 1172
1173 1173 return data
1174 1174
1175 1175
1176 1176 class UserGroupMember(Base, BaseModel):
1177 1177 __tablename__ = 'users_groups_members'
1178 1178 __table_args__ = (
1179 1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1180 1180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1181 1181 )
1182 1182
1183 1183 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1184 1184 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1185 1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1186 1186
1187 1187 user = relationship('User', lazy='joined')
1188 1188 users_group = relationship('UserGroup')
1189 1189
1190 1190 def __init__(self, gr_id='', u_id=''):
1191 1191 self.users_group_id = gr_id
1192 1192 self.user_id = u_id
1193 1193
1194 1194
1195 1195 class RepositoryField(Base, BaseModel):
1196 1196 __tablename__ = 'repositories_fields'
1197 1197 __table_args__ = (
1198 1198 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1199 1199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1200 1200 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1201 1201 )
1202 1202 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1203 1203
1204 1204 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1205 1205 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1206 1206 field_key = Column("field_key", String(250))
1207 1207 field_label = Column("field_label", String(1024), nullable=False)
1208 1208 field_value = Column("field_value", String(10000), nullable=False)
1209 1209 field_desc = Column("field_desc", String(1024), nullable=False)
1210 1210 field_type = Column("field_type", String(255), nullable=False, unique=None)
1211 1211 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1212 1212
1213 1213 repository = relationship('Repository')
1214 1214
1215 1215 @property
1216 1216 def field_key_prefixed(self):
1217 1217 return 'ex_%s' % self.field_key
1218 1218
1219 1219 @classmethod
1220 1220 def un_prefix_key(cls, key):
1221 1221 if key.startswith(cls.PREFIX):
1222 1222 return key[len(cls.PREFIX):]
1223 1223 return key
1224 1224
1225 1225 @classmethod
1226 1226 def get_by_key_name(cls, key, repo):
1227 1227 row = cls.query()\
1228 1228 .filter(cls.repository == repo)\
1229 1229 .filter(cls.field_key == key).scalar()
1230 1230 return row
1231 1231
1232 1232
1233 1233 class Repository(Base, BaseModel):
1234 1234 __tablename__ = 'repositories'
1235 1235 __table_args__ = (
1236 1236 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1237 1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1238 1238 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1239 1239 )
1240 1240 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1241 1241 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1242 1242
1243 1243 STATE_CREATED = 'repo_state_created'
1244 1244 STATE_PENDING = 'repo_state_pending'
1245 1245 STATE_ERROR = 'repo_state_error'
1246 1246
1247 1247 LOCK_AUTOMATIC = 'lock_auto'
1248 1248 LOCK_API = 'lock_api'
1249 1249 LOCK_WEB = 'lock_web'
1250 1250 LOCK_PULL = 'lock_pull'
1251 1251
1252 1252 NAME_SEP = URL_SEP
1253 1253
1254 1254 repo_id = Column(
1255 1255 "repo_id", Integer(), nullable=False, unique=True, default=None,
1256 1256 primary_key=True)
1257 1257 _repo_name = Column(
1258 1258 "repo_name", Text(), nullable=False, default=None)
1259 1259 _repo_name_hash = Column(
1260 1260 "repo_name_hash", String(255), nullable=False, unique=True)
1261 1261 repo_state = Column("repo_state", String(255), nullable=True)
1262 1262
1263 1263 clone_uri = Column(
1264 1264 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1265 1265 default=None)
1266 1266 repo_type = Column(
1267 1267 "repo_type", String(255), nullable=False, unique=False, default=None)
1268 1268 user_id = Column(
1269 1269 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1270 1270 unique=False, default=None)
1271 1271 private = Column(
1272 1272 "private", Boolean(), nullable=True, unique=None, default=None)
1273 1273 enable_statistics = Column(
1274 1274 "statistics", Boolean(), nullable=True, unique=None, default=True)
1275 1275 enable_downloads = Column(
1276 1276 "downloads", Boolean(), nullable=True, unique=None, default=True)
1277 1277 description = Column(
1278 1278 "description", String(10000), nullable=True, unique=None, default=None)
1279 1279 created_on = Column(
1280 1280 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1281 1281 default=datetime.datetime.now)
1282 1282 updated_on = Column(
1283 1283 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1284 1284 default=datetime.datetime.now)
1285 1285 _landing_revision = Column(
1286 1286 "landing_revision", String(255), nullable=False, unique=False,
1287 1287 default=None)
1288 1288 enable_locking = Column(
1289 1289 "enable_locking", Boolean(), nullable=False, unique=None,
1290 1290 default=False)
1291 1291 _locked = Column(
1292 1292 "locked", String(255), nullable=True, unique=False, default=None)
1293 1293 _changeset_cache = Column(
1294 1294 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1295 1295
1296 1296 fork_id = Column(
1297 1297 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1298 1298 nullable=True, unique=False, default=None)
1299 1299 group_id = Column(
1300 1300 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1301 1301 unique=False, default=None)
1302 1302
1303 1303 user = relationship('User', lazy='joined')
1304 1304 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1305 1305 group = relationship('RepoGroup', lazy='joined')
1306 1306 repo_to_perm = relationship(
1307 1307 'UserRepoToPerm', cascade='all',
1308 1308 order_by='UserRepoToPerm.repo_to_perm_id')
1309 1309 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1310 1310 stats = relationship('Statistics', cascade='all', uselist=False)
1311 1311
1312 1312 followers = relationship(
1313 1313 'UserFollowing',
1314 1314 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1315 1315 cascade='all')
1316 1316 extra_fields = relationship(
1317 1317 'RepositoryField', cascade="all, delete, delete-orphan")
1318 1318 logs = relationship('UserLog')
1319 1319 comments = relationship(
1320 1320 'ChangesetComment', cascade="all, delete, delete-orphan")
1321 1321 pull_requests_source = relationship(
1322 1322 'PullRequest',
1323 1323 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1324 1324 cascade="all, delete, delete-orphan")
1325 1325 pull_requests_target = relationship(
1326 1326 'PullRequest',
1327 1327 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1328 1328 cascade="all, delete, delete-orphan")
1329 1329 ui = relationship('RepoRhodeCodeUi', cascade="all")
1330 1330 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1331 1331 integrations = relationship('Integration',
1332 1332 cascade="all, delete, delete-orphan")
1333 1333
1334 1334 def __unicode__(self):
1335 1335 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1336 1336 safe_unicode(self.repo_name))
1337 1337
1338 1338 @hybrid_property
1339 1339 def landing_rev(self):
1340 1340 # always should return [rev_type, rev]
1341 1341 if self._landing_revision:
1342 1342 _rev_info = self._landing_revision.split(':')
1343 1343 if len(_rev_info) < 2:
1344 1344 _rev_info.insert(0, 'rev')
1345 1345 return [_rev_info[0], _rev_info[1]]
1346 1346 return [None, None]
1347 1347
1348 1348 @landing_rev.setter
1349 1349 def landing_rev(self, val):
1350 1350 if ':' not in val:
1351 1351 raise ValueError('value must be delimited with `:` and consist '
1352 1352 'of <rev_type>:<rev>, got %s instead' % val)
1353 1353 self._landing_revision = val
1354 1354
1355 1355 @hybrid_property
1356 1356 def locked(self):
1357 1357 if self._locked:
1358 1358 user_id, timelocked, reason = self._locked.split(':')
1359 1359 lock_values = int(user_id), timelocked, reason
1360 1360 else:
1361 1361 lock_values = [None, None, None]
1362 1362 return lock_values
1363 1363
1364 1364 @locked.setter
1365 1365 def locked(self, val):
1366 1366 if val and isinstance(val, (list, tuple)):
1367 1367 self._locked = ':'.join(map(str, val))
1368 1368 else:
1369 1369 self._locked = None
1370 1370
1371 1371 @hybrid_property
1372 1372 def changeset_cache(self):
1373 1373 from rhodecode.lib.vcs.backends.base import EmptyCommit
1374 1374 dummy = EmptyCommit().__json__()
1375 1375 if not self._changeset_cache:
1376 1376 return dummy
1377 1377 try:
1378 1378 return json.loads(self._changeset_cache)
1379 1379 except TypeError:
1380 1380 return dummy
1381 1381 except Exception:
1382 1382 log.error(traceback.format_exc())
1383 1383 return dummy
1384 1384
1385 1385 @changeset_cache.setter
1386 1386 def changeset_cache(self, val):
1387 1387 try:
1388 1388 self._changeset_cache = json.dumps(val)
1389 1389 except Exception:
1390 1390 log.error(traceback.format_exc())
1391 1391
1392 1392 @hybrid_property
1393 1393 def repo_name(self):
1394 1394 return self._repo_name
1395 1395
1396 1396 @repo_name.setter
1397 1397 def repo_name(self, value):
1398 1398 self._repo_name = value
1399 1399 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1400 1400
1401 1401 @classmethod
1402 1402 def normalize_repo_name(cls, repo_name):
1403 1403 """
1404 1404 Normalizes os specific repo_name to the format internally stored inside
1405 1405 database using URL_SEP
1406 1406
1407 1407 :param cls:
1408 1408 :param repo_name:
1409 1409 """
1410 1410 return cls.NAME_SEP.join(repo_name.split(os.sep))
1411 1411
1412 1412 @classmethod
1413 1413 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1414 1414 session = Session()
1415 1415 q = session.query(cls).filter(cls.repo_name == repo_name)
1416 1416
1417 1417 if cache:
1418 1418 if identity_cache:
1419 1419 val = cls.identity_cache(session, 'repo_name', repo_name)
1420 1420 if val:
1421 1421 return val
1422 1422 else:
1423 1423 q = q.options(
1424 1424 FromCache("sql_cache_short",
1425 1425 "get_repo_by_name_%s" % _hash_key(repo_name)))
1426 1426
1427 1427 return q.scalar()
1428 1428
1429 1429 @classmethod
1430 1430 def get_by_full_path(cls, repo_full_path):
1431 1431 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1432 1432 repo_name = cls.normalize_repo_name(repo_name)
1433 1433 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1434 1434
1435 1435 @classmethod
1436 1436 def get_repo_forks(cls, repo_id):
1437 1437 return cls.query().filter(Repository.fork_id == repo_id)
1438 1438
1439 1439 @classmethod
1440 1440 def base_path(cls):
1441 1441 """
1442 1442 Returns base path when all repos are stored
1443 1443
1444 1444 :param cls:
1445 1445 """
1446 1446 q = Session().query(RhodeCodeUi)\
1447 1447 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1448 1448 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1449 1449 return q.one().ui_value
1450 1450
1451 1451 @classmethod
1452 1452 def is_valid(cls, repo_name):
1453 1453 """
1454 1454 returns True if given repo name is a valid filesystem repository
1455 1455
1456 1456 :param cls:
1457 1457 :param repo_name:
1458 1458 """
1459 1459 from rhodecode.lib.utils import is_valid_repo
1460 1460
1461 1461 return is_valid_repo(repo_name, cls.base_path())
1462 1462
1463 1463 @classmethod
1464 1464 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1465 1465 case_insensitive=True):
1466 1466 q = Repository.query()
1467 1467
1468 1468 if not isinstance(user_id, Optional):
1469 1469 q = q.filter(Repository.user_id == user_id)
1470 1470
1471 1471 if not isinstance(group_id, Optional):
1472 1472 q = q.filter(Repository.group_id == group_id)
1473 1473
1474 1474 if case_insensitive:
1475 1475 q = q.order_by(func.lower(Repository.repo_name))
1476 1476 else:
1477 1477 q = q.order_by(Repository.repo_name)
1478 1478 return q.all()
1479 1479
1480 1480 @property
1481 1481 def forks(self):
1482 1482 """
1483 1483 Return forks of this repo
1484 1484 """
1485 1485 return Repository.get_repo_forks(self.repo_id)
1486 1486
1487 1487 @property
1488 1488 def parent(self):
1489 1489 """
1490 1490 Returns fork parent
1491 1491 """
1492 1492 return self.fork
1493 1493
1494 1494 @property
1495 1495 def just_name(self):
1496 1496 return self.repo_name.split(self.NAME_SEP)[-1]
1497 1497
1498 1498 @property
1499 1499 def groups_with_parents(self):
1500 1500 groups = []
1501 1501 if self.group is None:
1502 1502 return groups
1503 1503
1504 1504 cur_gr = self.group
1505 1505 groups.insert(0, cur_gr)
1506 1506 while 1:
1507 1507 gr = getattr(cur_gr, 'parent_group', None)
1508 1508 cur_gr = cur_gr.parent_group
1509 1509 if gr is None:
1510 1510 break
1511 1511 groups.insert(0, gr)
1512 1512
1513 1513 return groups
1514 1514
1515 1515 @property
1516 1516 def groups_and_repo(self):
1517 1517 return self.groups_with_parents, self
1518 1518
1519 1519 @LazyProperty
1520 1520 def repo_path(self):
1521 1521 """
1522 1522 Returns base full path for that repository means where it actually
1523 1523 exists on a filesystem
1524 1524 """
1525 1525 q = Session().query(RhodeCodeUi).filter(
1526 1526 RhodeCodeUi.ui_key == self.NAME_SEP)
1527 1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1528 1528 return q.one().ui_value
1529 1529
1530 1530 @property
1531 1531 def repo_full_path(self):
1532 1532 p = [self.repo_path]
1533 1533 # we need to split the name by / since this is how we store the
1534 1534 # names in the database, but that eventually needs to be converted
1535 1535 # into a valid system path
1536 1536 p += self.repo_name.split(self.NAME_SEP)
1537 1537 return os.path.join(*map(safe_unicode, p))
1538 1538
1539 1539 @property
1540 1540 def cache_keys(self):
1541 1541 """
1542 1542 Returns associated cache keys for that repo
1543 1543 """
1544 1544 return CacheKey.query()\
1545 1545 .filter(CacheKey.cache_args == self.repo_name)\
1546 1546 .order_by(CacheKey.cache_key)\
1547 1547 .all()
1548 1548
1549 1549 def get_new_name(self, repo_name):
1550 1550 """
1551 1551 returns new full repository name based on assigned group and new new
1552 1552
1553 1553 :param group_name:
1554 1554 """
1555 1555 path_prefix = self.group.full_path_splitted if self.group else []
1556 1556 return self.NAME_SEP.join(path_prefix + [repo_name])
1557 1557
1558 1558 @property
1559 1559 def _config(self):
1560 1560 """
1561 1561 Returns db based config object.
1562 1562 """
1563 1563 from rhodecode.lib.utils import make_db_config
1564 1564 return make_db_config(clear_session=False, repo=self)
1565 1565
1566 1566 def permissions(self, with_admins=True, with_owner=True):
1567 1567 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1568 1568 q = q.options(joinedload(UserRepoToPerm.repository),
1569 1569 joinedload(UserRepoToPerm.user),
1570 1570 joinedload(UserRepoToPerm.permission),)
1571 1571
1572 1572 # get owners and admins and permissions. We do a trick of re-writing
1573 1573 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1574 1574 # has a global reference and changing one object propagates to all
1575 1575 # others. This means if admin is also an owner admin_row that change
1576 1576 # would propagate to both objects
1577 1577 perm_rows = []
1578 1578 for _usr in q.all():
1579 1579 usr = AttributeDict(_usr.user.get_dict())
1580 1580 usr.permission = _usr.permission.permission_name
1581 1581 perm_rows.append(usr)
1582 1582
1583 1583 # filter the perm rows by 'default' first and then sort them by
1584 1584 # admin,write,read,none permissions sorted again alphabetically in
1585 1585 # each group
1586 1586 perm_rows = sorted(perm_rows, key=display_sort)
1587 1587
1588 1588 _admin_perm = 'repository.admin'
1589 1589 owner_row = []
1590 1590 if with_owner:
1591 1591 usr = AttributeDict(self.user.get_dict())
1592 1592 usr.owner_row = True
1593 1593 usr.permission = _admin_perm
1594 1594 owner_row.append(usr)
1595 1595
1596 1596 super_admin_rows = []
1597 1597 if with_admins:
1598 1598 for usr in User.get_all_super_admins():
1599 1599 # if this admin is also owner, don't double the record
1600 1600 if usr.user_id == owner_row[0].user_id:
1601 1601 owner_row[0].admin_row = True
1602 1602 else:
1603 1603 usr = AttributeDict(usr.get_dict())
1604 1604 usr.admin_row = True
1605 1605 usr.permission = _admin_perm
1606 1606 super_admin_rows.append(usr)
1607 1607
1608 1608 return super_admin_rows + owner_row + perm_rows
1609 1609
1610 1610 def permission_user_groups(self):
1611 1611 q = UserGroupRepoToPerm.query().filter(
1612 1612 UserGroupRepoToPerm.repository == self)
1613 1613 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1614 1614 joinedload(UserGroupRepoToPerm.users_group),
1615 1615 joinedload(UserGroupRepoToPerm.permission),)
1616 1616
1617 1617 perm_rows = []
1618 1618 for _user_group in q.all():
1619 1619 usr = AttributeDict(_user_group.users_group.get_dict())
1620 1620 usr.permission = _user_group.permission.permission_name
1621 1621 perm_rows.append(usr)
1622 1622
1623 1623 return perm_rows
1624 1624
1625 1625 def get_api_data(self, include_secrets=False):
1626 1626 """
1627 1627 Common function for generating repo api data
1628 1628
1629 1629 :param include_secrets: See :meth:`User.get_api_data`.
1630 1630
1631 1631 """
1632 1632 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1633 1633 # move this methods on models level.
1634 1634 from rhodecode.model.settings import SettingsModel
1635 1635
1636 1636 repo = self
1637 1637 _user_id, _time, _reason = self.locked
1638 1638
1639 1639 data = {
1640 1640 'repo_id': repo.repo_id,
1641 1641 'repo_name': repo.repo_name,
1642 1642 'repo_type': repo.repo_type,
1643 1643 'clone_uri': repo.clone_uri or '',
1644 1644 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1645 1645 'private': repo.private,
1646 1646 'created_on': repo.created_on,
1647 1647 'description': repo.description,
1648 1648 'landing_rev': repo.landing_rev,
1649 1649 'owner': repo.user.username,
1650 1650 'fork_of': repo.fork.repo_name if repo.fork else None,
1651 1651 'enable_statistics': repo.enable_statistics,
1652 1652 'enable_locking': repo.enable_locking,
1653 1653 'enable_downloads': repo.enable_downloads,
1654 1654 'last_changeset': repo.changeset_cache,
1655 1655 'locked_by': User.get(_user_id).get_api_data(
1656 1656 include_secrets=include_secrets) if _user_id else None,
1657 1657 'locked_date': time_to_datetime(_time) if _time else None,
1658 1658 'lock_reason': _reason if _reason else None,
1659 1659 }
1660 1660
1661 1661 # TODO: mikhail: should be per-repo settings here
1662 1662 rc_config = SettingsModel().get_all_settings()
1663 1663 repository_fields = str2bool(
1664 1664 rc_config.get('rhodecode_repository_fields'))
1665 1665 if repository_fields:
1666 1666 for f in self.extra_fields:
1667 1667 data[f.field_key_prefixed] = f.field_value
1668 1668
1669 1669 return data
1670 1670
1671 1671 @classmethod
1672 1672 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1673 1673 if not lock_time:
1674 1674 lock_time = time.time()
1675 1675 if not lock_reason:
1676 1676 lock_reason = cls.LOCK_AUTOMATIC
1677 1677 repo.locked = [user_id, lock_time, lock_reason]
1678 1678 Session().add(repo)
1679 1679 Session().commit()
1680 1680
1681 1681 @classmethod
1682 1682 def unlock(cls, repo):
1683 1683 repo.locked = None
1684 1684 Session().add(repo)
1685 1685 Session().commit()
1686 1686
1687 1687 @classmethod
1688 1688 def getlock(cls, repo):
1689 1689 return repo.locked
1690 1690
1691 1691 def is_user_lock(self, user_id):
1692 1692 if self.lock[0]:
1693 1693 lock_user_id = safe_int(self.lock[0])
1694 1694 user_id = safe_int(user_id)
1695 1695 # both are ints, and they are equal
1696 1696 return all([lock_user_id, user_id]) and lock_user_id == user_id
1697 1697
1698 1698 return False
1699 1699
1700 1700 def get_locking_state(self, action, user_id, only_when_enabled=True):
1701 1701 """
1702 1702 Checks locking on this repository, if locking is enabled and lock is
1703 1703 present returns a tuple of make_lock, locked, locked_by.
1704 1704 make_lock can have 3 states None (do nothing) True, make lock
1705 1705 False release lock, This value is later propagated to hooks, which
1706 1706 do the locking. Think about this as signals passed to hooks what to do.
1707 1707
1708 1708 """
1709 1709 # TODO: johbo: This is part of the business logic and should be moved
1710 1710 # into the RepositoryModel.
1711 1711
1712 1712 if action not in ('push', 'pull'):
1713 1713 raise ValueError("Invalid action value: %s" % repr(action))
1714 1714
1715 1715 # defines if locked error should be thrown to user
1716 1716 currently_locked = False
1717 1717 # defines if new lock should be made, tri-state
1718 1718 make_lock = None
1719 1719 repo = self
1720 1720 user = User.get(user_id)
1721 1721
1722 1722 lock_info = repo.locked
1723 1723
1724 1724 if repo and (repo.enable_locking or not only_when_enabled):
1725 1725 if action == 'push':
1726 1726 # check if it's already locked !, if it is compare users
1727 1727 locked_by_user_id = lock_info[0]
1728 1728 if user.user_id == locked_by_user_id:
1729 1729 log.debug(
1730 1730 'Got `push` action from user %s, now unlocking', user)
1731 1731 # unlock if we have push from user who locked
1732 1732 make_lock = False
1733 1733 else:
1734 1734 # we're not the same user who locked, ban with
1735 1735 # code defined in settings (default is 423 HTTP Locked) !
1736 1736 log.debug('Repo %s is currently locked by %s', repo, user)
1737 1737 currently_locked = True
1738 1738 elif action == 'pull':
1739 1739 # [0] user [1] date
1740 1740 if lock_info[0] and lock_info[1]:
1741 1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 1742 currently_locked = True
1743 1743 else:
1744 1744 log.debug('Setting lock on repo %s by %s', repo, user)
1745 1745 make_lock = True
1746 1746
1747 1747 else:
1748 1748 log.debug('Repository %s do not have locking enabled', repo)
1749 1749
1750 1750 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1751 1751 make_lock, currently_locked, lock_info)
1752 1752
1753 1753 from rhodecode.lib.auth import HasRepoPermissionAny
1754 1754 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1755 1755 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1756 1756 # if we don't have at least write permission we cannot make a lock
1757 1757 log.debug('lock state reset back to FALSE due to lack '
1758 1758 'of at least read permission')
1759 1759 make_lock = False
1760 1760
1761 1761 return make_lock, currently_locked, lock_info
1762 1762
1763 1763 @property
1764 1764 def last_db_change(self):
1765 1765 return self.updated_on
1766 1766
1767 1767 @property
1768 1768 def clone_uri_hidden(self):
1769 1769 clone_uri = self.clone_uri
1770 1770 if clone_uri:
1771 1771 import urlobject
1772 1772 url_obj = urlobject.URLObject(clone_uri)
1773 1773 if url_obj.password:
1774 1774 clone_uri = url_obj.with_password('*****')
1775 1775 return clone_uri
1776 1776
1777 1777 def clone_url(self, **override):
1778 1778 qualified_home_url = url('home', qualified=True)
1779 1779
1780 1780 uri_tmpl = None
1781 1781 if 'with_id' in override:
1782 1782 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1783 1783 del override['with_id']
1784 1784
1785 1785 if 'uri_tmpl' in override:
1786 1786 uri_tmpl = override['uri_tmpl']
1787 1787 del override['uri_tmpl']
1788 1788
1789 1789 # we didn't override our tmpl from **overrides
1790 1790 if not uri_tmpl:
1791 1791 uri_tmpl = self.DEFAULT_CLONE_URI
1792 1792 try:
1793 1793 from pylons import tmpl_context as c
1794 1794 uri_tmpl = c.clone_uri_tmpl
1795 1795 except Exception:
1796 1796 # in any case if we call this outside of request context,
1797 1797 # ie, not having tmpl_context set up
1798 1798 pass
1799 1799
1800 1800 return get_clone_url(uri_tmpl=uri_tmpl,
1801 1801 qualifed_home_url=qualified_home_url,
1802 1802 repo_name=self.repo_name,
1803 1803 repo_id=self.repo_id, **override)
1804 1804
1805 1805 def set_state(self, state):
1806 1806 self.repo_state = state
1807 1807 Session().add(self)
1808 1808 #==========================================================================
1809 1809 # SCM PROPERTIES
1810 1810 #==========================================================================
1811 1811
1812 1812 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1813 1813 return get_commit_safe(
1814 1814 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1815 1815
1816 1816 def get_changeset(self, rev=None, pre_load=None):
1817 1817 warnings.warn("Use get_commit", DeprecationWarning)
1818 1818 commit_id = None
1819 1819 commit_idx = None
1820 1820 if isinstance(rev, basestring):
1821 1821 commit_id = rev
1822 1822 else:
1823 1823 commit_idx = rev
1824 1824 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1825 1825 pre_load=pre_load)
1826 1826
1827 1827 def get_landing_commit(self):
1828 1828 """
1829 1829 Returns landing commit, or if that doesn't exist returns the tip
1830 1830 """
1831 1831 _rev_type, _rev = self.landing_rev
1832 1832 commit = self.get_commit(_rev)
1833 1833 if isinstance(commit, EmptyCommit):
1834 1834 return self.get_commit()
1835 1835 return commit
1836 1836
1837 1837 def update_commit_cache(self, cs_cache=None, config=None):
1838 1838 """
1839 1839 Update cache of last changeset for repository, keys should be::
1840 1840
1841 1841 short_id
1842 1842 raw_id
1843 1843 revision
1844 1844 parents
1845 1845 message
1846 1846 date
1847 1847 author
1848 1848
1849 1849 :param cs_cache:
1850 1850 """
1851 1851 from rhodecode.lib.vcs.backends.base import BaseChangeset
1852 1852 if cs_cache is None:
1853 1853 # use no-cache version here
1854 1854 scm_repo = self.scm_instance(cache=False, config=config)
1855 1855 if scm_repo:
1856 1856 cs_cache = scm_repo.get_commit(
1857 1857 pre_load=["author", "date", "message", "parents"])
1858 1858 else:
1859 1859 cs_cache = EmptyCommit()
1860 1860
1861 1861 if isinstance(cs_cache, BaseChangeset):
1862 1862 cs_cache = cs_cache.__json__()
1863 1863
1864 1864 def is_outdated(new_cs_cache):
1865 1865 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1866 1866 new_cs_cache['revision'] != self.changeset_cache['revision']):
1867 1867 return True
1868 1868 return False
1869 1869
1870 1870 # check if we have maybe already latest cached revision
1871 1871 if is_outdated(cs_cache) or not self.changeset_cache:
1872 1872 _default = datetime.datetime.fromtimestamp(0)
1873 1873 last_change = cs_cache.get('date') or _default
1874 1874 log.debug('updated repo %s with new cs cache %s',
1875 1875 self.repo_name, cs_cache)
1876 1876 self.updated_on = last_change
1877 1877 self.changeset_cache = cs_cache
1878 1878 Session().add(self)
1879 1879 Session().commit()
1880 1880 else:
1881 1881 log.debug('Skipping update_commit_cache for repo:`%s` '
1882 1882 'commit already with latest changes', self.repo_name)
1883 1883
1884 1884 @property
1885 1885 def tip(self):
1886 1886 return self.get_commit('tip')
1887 1887
1888 1888 @property
1889 1889 def author(self):
1890 1890 return self.tip.author
1891 1891
1892 1892 @property
1893 1893 def last_change(self):
1894 1894 return self.scm_instance().last_change
1895 1895
1896 1896 def get_comments(self, revisions=None):
1897 1897 """
1898 1898 Returns comments for this repository grouped by revisions
1899 1899
1900 1900 :param revisions: filter query by revisions only
1901 1901 """
1902 1902 cmts = ChangesetComment.query()\
1903 1903 .filter(ChangesetComment.repo == self)
1904 1904 if revisions:
1905 1905 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1906 1906 grouped = collections.defaultdict(list)
1907 1907 for cmt in cmts.all():
1908 1908 grouped[cmt.revision].append(cmt)
1909 1909 return grouped
1910 1910
1911 1911 def statuses(self, revisions=None):
1912 1912 """
1913 1913 Returns statuses for this repository
1914 1914
1915 1915 :param revisions: list of revisions to get statuses for
1916 1916 """
1917 1917 statuses = ChangesetStatus.query()\
1918 1918 .filter(ChangesetStatus.repo == self)\
1919 1919 .filter(ChangesetStatus.version == 0)
1920 1920
1921 1921 if revisions:
1922 1922 # Try doing the filtering in chunks to avoid hitting limits
1923 1923 size = 500
1924 1924 status_results = []
1925 1925 for chunk in xrange(0, len(revisions), size):
1926 1926 status_results += statuses.filter(
1927 1927 ChangesetStatus.revision.in_(
1928 1928 revisions[chunk: chunk+size])
1929 1929 ).all()
1930 1930 else:
1931 1931 status_results = statuses.all()
1932 1932
1933 1933 grouped = {}
1934 1934
1935 1935 # maybe we have open new pullrequest without a status?
1936 1936 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1937 1937 status_lbl = ChangesetStatus.get_status_lbl(stat)
1938 1938 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1939 1939 for rev in pr.revisions:
1940 1940 pr_id = pr.pull_request_id
1941 1941 pr_repo = pr.target_repo.repo_name
1942 1942 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1943 1943
1944 1944 for stat in status_results:
1945 1945 pr_id = pr_repo = None
1946 1946 if stat.pull_request:
1947 1947 pr_id = stat.pull_request.pull_request_id
1948 1948 pr_repo = stat.pull_request.target_repo.repo_name
1949 1949 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1950 1950 pr_id, pr_repo]
1951 1951 return grouped
1952 1952
1953 1953 # ==========================================================================
1954 1954 # SCM CACHE INSTANCE
1955 1955 # ==========================================================================
1956 1956
1957 1957 def scm_instance(self, **kwargs):
1958 1958 import rhodecode
1959 1959
1960 1960 # Passing a config will not hit the cache currently only used
1961 1961 # for repo2dbmapper
1962 1962 config = kwargs.pop('config', None)
1963 1963 cache = kwargs.pop('cache', None)
1964 1964 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1965 1965 # if cache is NOT defined use default global, else we have a full
1966 1966 # control over cache behaviour
1967 1967 if cache is None and full_cache and not config:
1968 1968 return self._get_instance_cached()
1969 1969 return self._get_instance(cache=bool(cache), config=config)
1970 1970
1971 1971 def _get_instance_cached(self):
1972 1972 @cache_region('long_term')
1973 1973 def _get_repo(cache_key):
1974 1974 return self._get_instance()
1975 1975
1976 1976 invalidator_context = CacheKey.repo_context_cache(
1977 1977 _get_repo, self.repo_name, None, thread_scoped=True)
1978 1978
1979 1979 with invalidator_context as context:
1980 1980 context.invalidate()
1981 1981 repo = context.compute()
1982 1982
1983 1983 return repo
1984 1984
1985 1985 def _get_instance(self, cache=True, config=None):
1986 1986 config = config or self._config
1987 1987 custom_wire = {
1988 1988 'cache': cache # controls the vcs.remote cache
1989 1989 }
1990 1990
1991 1991 repo = get_vcs_instance(
1992 1992 repo_path=safe_str(self.repo_full_path),
1993 1993 config=config,
1994 1994 with_wire=custom_wire,
1995 1995 create=False)
1996 1996
1997 1997 return repo
1998 1998
1999 1999 def __json__(self):
2000 2000 return {'landing_rev': self.landing_rev}
2001 2001
2002 2002 def get_dict(self):
2003 2003
2004 2004 # Since we transformed `repo_name` to a hybrid property, we need to
2005 2005 # keep compatibility with the code which uses `repo_name` field.
2006 2006
2007 2007 result = super(Repository, self).get_dict()
2008 2008 result['repo_name'] = result.pop('_repo_name', None)
2009 2009 return result
2010 2010
2011 2011
2012 2012 class RepoGroup(Base, BaseModel):
2013 2013 __tablename__ = 'groups'
2014 2014 __table_args__ = (
2015 2015 UniqueConstraint('group_name', 'group_parent_id'),
2016 2016 CheckConstraint('group_id != group_parent_id'),
2017 2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2018 2018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2019 2019 )
2020 2020 __mapper_args__ = {'order_by': 'group_name'}
2021 2021
2022 2022 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2023 2023
2024 2024 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2025 2025 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2026 2026 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2027 2027 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2028 2028 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2029 2029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2030 2030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2031 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2031 2032
2032 2033 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2033 2034 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2034 2035 parent_group = relationship('RepoGroup', remote_side=group_id)
2035 2036 user = relationship('User')
2036 2037 integrations = relationship('Integration',
2037 2038 cascade="all, delete, delete-orphan")
2038 2039
2039 2040 def __init__(self, group_name='', parent_group=None):
2040 2041 self.group_name = group_name
2041 2042 self.parent_group = parent_group
2042 2043
2043 2044 def __unicode__(self):
2044 2045 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2045 2046 self.group_name)
2046 2047
2047 2048 @classmethod
2048 2049 def _generate_choice(cls, repo_group):
2049 2050 from webhelpers.html import literal as _literal
2050 2051 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2051 2052 return repo_group.group_id, _name(repo_group.full_path_splitted)
2052 2053
2053 2054 @classmethod
2054 2055 def groups_choices(cls, groups=None, show_empty_group=True):
2055 2056 if not groups:
2056 2057 groups = cls.query().all()
2057 2058
2058 2059 repo_groups = []
2059 2060 if show_empty_group:
2060 2061 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2061 2062
2062 2063 repo_groups.extend([cls._generate_choice(x) for x in groups])
2063 2064
2064 2065 repo_groups = sorted(
2065 2066 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2066 2067 return repo_groups
2067 2068
2068 2069 @classmethod
2069 2070 def url_sep(cls):
2070 2071 return URL_SEP
2071 2072
2072 2073 @classmethod
2073 2074 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2074 2075 if case_insensitive:
2075 2076 gr = cls.query().filter(func.lower(cls.group_name)
2076 2077 == func.lower(group_name))
2077 2078 else:
2078 2079 gr = cls.query().filter(cls.group_name == group_name)
2079 2080 if cache:
2080 2081 gr = gr.options(FromCache(
2081 2082 "sql_cache_short",
2082 2083 "get_group_%s" % _hash_key(group_name)))
2083 2084 return gr.scalar()
2084 2085
2085 2086 @classmethod
2087 def get_user_personal_repo_group(cls, user_id):
2088 user = User.get(user_id)
2089 return cls.query()\
2090 .filter(cls.personal == true())\
2091 .filter(cls.user == user).scalar()
2092
2093 @classmethod
2086 2094 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2087 2095 case_insensitive=True):
2088 2096 q = RepoGroup.query()
2089 2097
2090 2098 if not isinstance(user_id, Optional):
2091 2099 q = q.filter(RepoGroup.user_id == user_id)
2092 2100
2093 2101 if not isinstance(group_id, Optional):
2094 2102 q = q.filter(RepoGroup.group_parent_id == group_id)
2095 2103
2096 2104 if case_insensitive:
2097 2105 q = q.order_by(func.lower(RepoGroup.group_name))
2098 2106 else:
2099 2107 q = q.order_by(RepoGroup.group_name)
2100 2108 return q.all()
2101 2109
2102 2110 @property
2103 2111 def parents(self):
2104 2112 parents_recursion_limit = 10
2105 2113 groups = []
2106 2114 if self.parent_group is None:
2107 2115 return groups
2108 2116 cur_gr = self.parent_group
2109 2117 groups.insert(0, cur_gr)
2110 2118 cnt = 0
2111 2119 while 1:
2112 2120 cnt += 1
2113 2121 gr = getattr(cur_gr, 'parent_group', None)
2114 2122 cur_gr = cur_gr.parent_group
2115 2123 if gr is None:
2116 2124 break
2117 2125 if cnt == parents_recursion_limit:
2118 2126 # this will prevent accidental infinit loops
2119 2127 log.error(('more than %s parents found for group %s, stopping '
2120 2128 'recursive parent fetching' % (parents_recursion_limit, self)))
2121 2129 break
2122 2130
2123 2131 groups.insert(0, gr)
2124 2132 return groups
2125 2133
2126 2134 @property
2127 2135 def children(self):
2128 2136 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2129 2137
2130 2138 @property
2131 2139 def name(self):
2132 2140 return self.group_name.split(RepoGroup.url_sep())[-1]
2133 2141
2134 2142 @property
2135 2143 def full_path(self):
2136 2144 return self.group_name
2137 2145
2138 2146 @property
2139 2147 def full_path_splitted(self):
2140 2148 return self.group_name.split(RepoGroup.url_sep())
2141 2149
2142 2150 @property
2143 2151 def repositories(self):
2144 2152 return Repository.query()\
2145 2153 .filter(Repository.group == self)\
2146 2154 .order_by(Repository.repo_name)
2147 2155
2148 2156 @property
2149 2157 def repositories_recursive_count(self):
2150 2158 cnt = self.repositories.count()
2151 2159
2152 2160 def children_count(group):
2153 2161 cnt = 0
2154 2162 for child in group.children:
2155 2163 cnt += child.repositories.count()
2156 2164 cnt += children_count(child)
2157 2165 return cnt
2158 2166
2159 2167 return cnt + children_count(self)
2160 2168
2161 2169 def _recursive_objects(self, include_repos=True):
2162 2170 all_ = []
2163 2171
2164 2172 def _get_members(root_gr):
2165 2173 if include_repos:
2166 2174 for r in root_gr.repositories:
2167 2175 all_.append(r)
2168 2176 childs = root_gr.children.all()
2169 2177 if childs:
2170 2178 for gr in childs:
2171 2179 all_.append(gr)
2172 2180 _get_members(gr)
2173 2181
2174 2182 _get_members(self)
2175 2183 return [self] + all_
2176 2184
2177 2185 def recursive_groups_and_repos(self):
2178 2186 """
2179 2187 Recursive return all groups, with repositories in those groups
2180 2188 """
2181 2189 return self._recursive_objects()
2182 2190
2183 2191 def recursive_groups(self):
2184 2192 """
2185 2193 Returns all children groups for this group including children of children
2186 2194 """
2187 2195 return self._recursive_objects(include_repos=False)
2188 2196
2189 2197 def get_new_name(self, group_name):
2190 2198 """
2191 2199 returns new full group name based on parent and new name
2192 2200
2193 2201 :param group_name:
2194 2202 """
2195 2203 path_prefix = (self.parent_group.full_path_splitted if
2196 2204 self.parent_group else [])
2197 2205 return RepoGroup.url_sep().join(path_prefix + [group_name])
2198 2206
2199 2207 def permissions(self, with_admins=True, with_owner=True):
2200 2208 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2201 2209 q = q.options(joinedload(UserRepoGroupToPerm.group),
2202 2210 joinedload(UserRepoGroupToPerm.user),
2203 2211 joinedload(UserRepoGroupToPerm.permission),)
2204 2212
2205 2213 # get owners and admins and permissions. We do a trick of re-writing
2206 2214 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2207 2215 # has a global reference and changing one object propagates to all
2208 2216 # others. This means if admin is also an owner admin_row that change
2209 2217 # would propagate to both objects
2210 2218 perm_rows = []
2211 2219 for _usr in q.all():
2212 2220 usr = AttributeDict(_usr.user.get_dict())
2213 2221 usr.permission = _usr.permission.permission_name
2214 2222 perm_rows.append(usr)
2215 2223
2216 2224 # filter the perm rows by 'default' first and then sort them by
2217 2225 # admin,write,read,none permissions sorted again alphabetically in
2218 2226 # each group
2219 2227 perm_rows = sorted(perm_rows, key=display_sort)
2220 2228
2221 2229 _admin_perm = 'group.admin'
2222 2230 owner_row = []
2223 2231 if with_owner:
2224 2232 usr = AttributeDict(self.user.get_dict())
2225 2233 usr.owner_row = True
2226 2234 usr.permission = _admin_perm
2227 2235 owner_row.append(usr)
2228 2236
2229 2237 super_admin_rows = []
2230 2238 if with_admins:
2231 2239 for usr in User.get_all_super_admins():
2232 2240 # if this admin is also owner, don't double the record
2233 2241 if usr.user_id == owner_row[0].user_id:
2234 2242 owner_row[0].admin_row = True
2235 2243 else:
2236 2244 usr = AttributeDict(usr.get_dict())
2237 2245 usr.admin_row = True
2238 2246 usr.permission = _admin_perm
2239 2247 super_admin_rows.append(usr)
2240 2248
2241 2249 return super_admin_rows + owner_row + perm_rows
2242 2250
2243 2251 def permission_user_groups(self):
2244 2252 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2245 2253 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2246 2254 joinedload(UserGroupRepoGroupToPerm.users_group),
2247 2255 joinedload(UserGroupRepoGroupToPerm.permission),)
2248 2256
2249 2257 perm_rows = []
2250 2258 for _user_group in q.all():
2251 2259 usr = AttributeDict(_user_group.users_group.get_dict())
2252 2260 usr.permission = _user_group.permission.permission_name
2253 2261 perm_rows.append(usr)
2254 2262
2255 2263 return perm_rows
2256 2264
2257 2265 def get_api_data(self):
2258 2266 """
2259 2267 Common function for generating api data
2260 2268
2261 2269 """
2262 2270 group = self
2263 2271 data = {
2264 2272 'group_id': group.group_id,
2265 2273 'group_name': group.group_name,
2266 2274 'group_description': group.group_description,
2267 2275 'parent_group': group.parent_group.group_name if group.parent_group else None,
2268 2276 'repositories': [x.repo_name for x in group.repositories],
2269 2277 'owner': group.user.username,
2270 2278 }
2271 2279 return data
2272 2280
2273 2281
2274 2282 class Permission(Base, BaseModel):
2275 2283 __tablename__ = 'permissions'
2276 2284 __table_args__ = (
2277 2285 Index('p_perm_name_idx', 'permission_name'),
2278 2286 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2279 2287 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2280 2288 )
2281 2289 PERMS = [
2282 2290 ('hg.admin', _('RhodeCode Super Administrator')),
2283 2291
2284 2292 ('repository.none', _('Repository no access')),
2285 2293 ('repository.read', _('Repository read access')),
2286 2294 ('repository.write', _('Repository write access')),
2287 2295 ('repository.admin', _('Repository admin access')),
2288 2296
2289 2297 ('group.none', _('Repository group no access')),
2290 2298 ('group.read', _('Repository group read access')),
2291 2299 ('group.write', _('Repository group write access')),
2292 2300 ('group.admin', _('Repository group admin access')),
2293 2301
2294 2302 ('usergroup.none', _('User group no access')),
2295 2303 ('usergroup.read', _('User group read access')),
2296 2304 ('usergroup.write', _('User group write access')),
2297 2305 ('usergroup.admin', _('User group admin access')),
2298 2306
2299 2307 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2300 2308 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2301 2309
2302 2310 ('hg.usergroup.create.false', _('User Group creation disabled')),
2303 2311 ('hg.usergroup.create.true', _('User Group creation enabled')),
2304 2312
2305 2313 ('hg.create.none', _('Repository creation disabled')),
2306 2314 ('hg.create.repository', _('Repository creation enabled')),
2307 2315 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2308 2316 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2309 2317
2310 2318 ('hg.fork.none', _('Repository forking disabled')),
2311 2319 ('hg.fork.repository', _('Repository forking enabled')),
2312 2320
2313 2321 ('hg.register.none', _('Registration disabled')),
2314 2322 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2315 2323 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2316 2324
2317 2325 ('hg.password_reset.enabled', _('Password reset enabled')),
2318 2326 ('hg.password_reset.hidden', _('Password reset hidden')),
2319 2327 ('hg.password_reset.disabled', _('Password reset disabled')),
2320 2328
2321 2329 ('hg.extern_activate.manual', _('Manual activation of external account')),
2322 2330 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2323 2331
2324 2332 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2325 2333 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2326 2334 ]
2327 2335
2328 2336 # definition of system default permissions for DEFAULT user
2329 2337 DEFAULT_USER_PERMISSIONS = [
2330 2338 'repository.read',
2331 2339 'group.read',
2332 2340 'usergroup.read',
2333 2341 'hg.create.repository',
2334 2342 'hg.repogroup.create.false',
2335 2343 'hg.usergroup.create.false',
2336 2344 'hg.create.write_on_repogroup.true',
2337 2345 'hg.fork.repository',
2338 2346 'hg.register.manual_activate',
2339 2347 'hg.password_reset.enabled',
2340 2348 'hg.extern_activate.auto',
2341 2349 'hg.inherit_default_perms.true',
2342 2350 ]
2343 2351
2344 2352 # defines which permissions are more important higher the more important
2345 2353 # Weight defines which permissions are more important.
2346 2354 # The higher number the more important.
2347 2355 PERM_WEIGHTS = {
2348 2356 'repository.none': 0,
2349 2357 'repository.read': 1,
2350 2358 'repository.write': 3,
2351 2359 'repository.admin': 4,
2352 2360
2353 2361 'group.none': 0,
2354 2362 'group.read': 1,
2355 2363 'group.write': 3,
2356 2364 'group.admin': 4,
2357 2365
2358 2366 'usergroup.none': 0,
2359 2367 'usergroup.read': 1,
2360 2368 'usergroup.write': 3,
2361 2369 'usergroup.admin': 4,
2362 2370
2363 2371 'hg.repogroup.create.false': 0,
2364 2372 'hg.repogroup.create.true': 1,
2365 2373
2366 2374 'hg.usergroup.create.false': 0,
2367 2375 'hg.usergroup.create.true': 1,
2368 2376
2369 2377 'hg.fork.none': 0,
2370 2378 'hg.fork.repository': 1,
2371 2379 'hg.create.none': 0,
2372 2380 'hg.create.repository': 1
2373 2381 }
2374 2382
2375 2383 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 2384 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 2385 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378 2386
2379 2387 def __unicode__(self):
2380 2388 return u"<%s('%s:%s')>" % (
2381 2389 self.__class__.__name__, self.permission_id, self.permission_name
2382 2390 )
2383 2391
2384 2392 @classmethod
2385 2393 def get_by_key(cls, key):
2386 2394 return cls.query().filter(cls.permission_name == key).scalar()
2387 2395
2388 2396 @classmethod
2389 2397 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 2398 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 2399 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 2400 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 2401 .filter(UserRepoToPerm.user_id == user_id)
2394 2402 if repo_id:
2395 2403 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 2404 return q.all()
2397 2405
2398 2406 @classmethod
2399 2407 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 2408 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 2409 .join(
2402 2410 Permission,
2403 2411 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 2412 .join(
2405 2413 Repository,
2406 2414 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 2415 .join(
2408 2416 UserGroup,
2409 2417 UserGroupRepoToPerm.users_group_id ==
2410 2418 UserGroup.users_group_id)\
2411 2419 .join(
2412 2420 UserGroupMember,
2413 2421 UserGroupRepoToPerm.users_group_id ==
2414 2422 UserGroupMember.users_group_id)\
2415 2423 .filter(
2416 2424 UserGroupMember.user_id == user_id,
2417 2425 UserGroup.users_group_active == true())
2418 2426 if repo_id:
2419 2427 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 2428 return q.all()
2421 2429
2422 2430 @classmethod
2423 2431 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 2432 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 2433 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 2434 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 2435 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 2436 if repo_group_id:
2429 2437 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 2438 return q.all()
2431 2439
2432 2440 @classmethod
2433 2441 def get_default_group_perms_from_user_group(
2434 2442 cls, user_id, repo_group_id=None):
2435 2443 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 2444 .join(
2437 2445 Permission,
2438 2446 UserGroupRepoGroupToPerm.permission_id ==
2439 2447 Permission.permission_id)\
2440 2448 .join(
2441 2449 RepoGroup,
2442 2450 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 2451 .join(
2444 2452 UserGroup,
2445 2453 UserGroupRepoGroupToPerm.users_group_id ==
2446 2454 UserGroup.users_group_id)\
2447 2455 .join(
2448 2456 UserGroupMember,
2449 2457 UserGroupRepoGroupToPerm.users_group_id ==
2450 2458 UserGroupMember.users_group_id)\
2451 2459 .filter(
2452 2460 UserGroupMember.user_id == user_id,
2453 2461 UserGroup.users_group_active == true())
2454 2462 if repo_group_id:
2455 2463 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 2464 return q.all()
2457 2465
2458 2466 @classmethod
2459 2467 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 2468 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 2469 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 2470 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 2471 .filter(UserUserGroupToPerm.user_id == user_id)
2464 2472 if user_group_id:
2465 2473 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 2474 return q.all()
2467 2475
2468 2476 @classmethod
2469 2477 def get_default_user_group_perms_from_user_group(
2470 2478 cls, user_id, user_group_id=None):
2471 2479 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 2480 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 2481 .join(
2474 2482 Permission,
2475 2483 UserGroupUserGroupToPerm.permission_id ==
2476 2484 Permission.permission_id)\
2477 2485 .join(
2478 2486 TargetUserGroup,
2479 2487 UserGroupUserGroupToPerm.target_user_group_id ==
2480 2488 TargetUserGroup.users_group_id)\
2481 2489 .join(
2482 2490 UserGroup,
2483 2491 UserGroupUserGroupToPerm.user_group_id ==
2484 2492 UserGroup.users_group_id)\
2485 2493 .join(
2486 2494 UserGroupMember,
2487 2495 UserGroupUserGroupToPerm.user_group_id ==
2488 2496 UserGroupMember.users_group_id)\
2489 2497 .filter(
2490 2498 UserGroupMember.user_id == user_id,
2491 2499 UserGroup.users_group_active == true())
2492 2500 if user_group_id:
2493 2501 q = q.filter(
2494 2502 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495 2503
2496 2504 return q.all()
2497 2505
2498 2506
2499 2507 class UserRepoToPerm(Base, BaseModel):
2500 2508 __tablename__ = 'repo_to_perm'
2501 2509 __table_args__ = (
2502 2510 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 2511 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 2512 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 2513 )
2506 2514 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 2515 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 2516 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 2517 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510 2518
2511 2519 user = relationship('User')
2512 2520 repository = relationship('Repository')
2513 2521 permission = relationship('Permission')
2514 2522
2515 2523 @classmethod
2516 2524 def create(cls, user, repository, permission):
2517 2525 n = cls()
2518 2526 n.user = user
2519 2527 n.repository = repository
2520 2528 n.permission = permission
2521 2529 Session().add(n)
2522 2530 return n
2523 2531
2524 2532 def __unicode__(self):
2525 2533 return u'<%s => %s >' % (self.user, self.repository)
2526 2534
2527 2535
2528 2536 class UserUserGroupToPerm(Base, BaseModel):
2529 2537 __tablename__ = 'user_user_group_to_perm'
2530 2538 __table_args__ = (
2531 2539 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 2540 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 2541 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 2542 )
2535 2543 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 2544 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 2545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 2546 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539 2547
2540 2548 user = relationship('User')
2541 2549 user_group = relationship('UserGroup')
2542 2550 permission = relationship('Permission')
2543 2551
2544 2552 @classmethod
2545 2553 def create(cls, user, user_group, permission):
2546 2554 n = cls()
2547 2555 n.user = user
2548 2556 n.user_group = user_group
2549 2557 n.permission = permission
2550 2558 Session().add(n)
2551 2559 return n
2552 2560
2553 2561 def __unicode__(self):
2554 2562 return u'<%s => %s >' % (self.user, self.user_group)
2555 2563
2556 2564
2557 2565 class UserToPerm(Base, BaseModel):
2558 2566 __tablename__ = 'user_to_perm'
2559 2567 __table_args__ = (
2560 2568 UniqueConstraint('user_id', 'permission_id'),
2561 2569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 2570 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 2571 )
2564 2572 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 2573 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 2574 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567 2575
2568 2576 user = relationship('User')
2569 2577 permission = relationship('Permission', lazy='joined')
2570 2578
2571 2579 def __unicode__(self):
2572 2580 return u'<%s => %s >' % (self.user, self.permission)
2573 2581
2574 2582
2575 2583 class UserGroupRepoToPerm(Base, BaseModel):
2576 2584 __tablename__ = 'users_group_repo_to_perm'
2577 2585 __table_args__ = (
2578 2586 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 2587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 2588 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 2589 )
2582 2590 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 2591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 2592 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 2593 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586 2594
2587 2595 users_group = relationship('UserGroup')
2588 2596 permission = relationship('Permission')
2589 2597 repository = relationship('Repository')
2590 2598
2591 2599 @classmethod
2592 2600 def create(cls, users_group, repository, permission):
2593 2601 n = cls()
2594 2602 n.users_group = users_group
2595 2603 n.repository = repository
2596 2604 n.permission = permission
2597 2605 Session().add(n)
2598 2606 return n
2599 2607
2600 2608 def __unicode__(self):
2601 2609 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602 2610
2603 2611
2604 2612 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 2613 __tablename__ = 'user_group_user_group_to_perm'
2606 2614 __table_args__ = (
2607 2615 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 2616 CheckConstraint('target_user_group_id != user_group_id'),
2609 2617 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 2618 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 2619 )
2612 2620 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2613 2621 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 2622 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 2623 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616 2624
2617 2625 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 2626 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 2627 permission = relationship('Permission')
2620 2628
2621 2629 @classmethod
2622 2630 def create(cls, target_user_group, user_group, permission):
2623 2631 n = cls()
2624 2632 n.target_user_group = target_user_group
2625 2633 n.user_group = user_group
2626 2634 n.permission = permission
2627 2635 Session().add(n)
2628 2636 return n
2629 2637
2630 2638 def __unicode__(self):
2631 2639 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632 2640
2633 2641
2634 2642 class UserGroupToPerm(Base, BaseModel):
2635 2643 __tablename__ = 'users_group_to_perm'
2636 2644 __table_args__ = (
2637 2645 UniqueConstraint('users_group_id', 'permission_id',),
2638 2646 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 2647 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 2648 )
2641 2649 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 2650 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 2651 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644 2652
2645 2653 users_group = relationship('UserGroup')
2646 2654 permission = relationship('Permission')
2647 2655
2648 2656
2649 2657 class UserRepoGroupToPerm(Base, BaseModel):
2650 2658 __tablename__ = 'user_repo_group_to_perm'
2651 2659 __table_args__ = (
2652 2660 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 2661 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 2662 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 2663 )
2656 2664
2657 2665 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 2666 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 2667 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 2668 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661 2669
2662 2670 user = relationship('User')
2663 2671 group = relationship('RepoGroup')
2664 2672 permission = relationship('Permission')
2665 2673
2666 2674 @classmethod
2667 2675 def create(cls, user, repository_group, permission):
2668 2676 n = cls()
2669 2677 n.user = user
2670 2678 n.group = repository_group
2671 2679 n.permission = permission
2672 2680 Session().add(n)
2673 2681 return n
2674 2682
2675 2683
2676 2684 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 2685 __tablename__ = 'users_group_repo_group_to_perm'
2678 2686 __table_args__ = (
2679 2687 UniqueConstraint('users_group_id', 'group_id'),
2680 2688 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 2689 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 2690 )
2683 2691
2684 2692 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2685 2693 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 2694 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 2695 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688 2696
2689 2697 users_group = relationship('UserGroup')
2690 2698 permission = relationship('Permission')
2691 2699 group = relationship('RepoGroup')
2692 2700
2693 2701 @classmethod
2694 2702 def create(cls, user_group, repository_group, permission):
2695 2703 n = cls()
2696 2704 n.users_group = user_group
2697 2705 n.group = repository_group
2698 2706 n.permission = permission
2699 2707 Session().add(n)
2700 2708 return n
2701 2709
2702 2710 def __unicode__(self):
2703 2711 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704 2712
2705 2713
2706 2714 class Statistics(Base, BaseModel):
2707 2715 __tablename__ = 'statistics'
2708 2716 __table_args__ = (
2709 2717 UniqueConstraint('repository_id'),
2710 2718 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 2719 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 2720 )
2713 2721 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 2722 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 2723 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 2724 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 2725 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 2726 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719 2727
2720 2728 repository = relationship('Repository', single_parent=True)
2721 2729
2722 2730
2723 2731 class UserFollowing(Base, BaseModel):
2724 2732 __tablename__ = 'user_followings'
2725 2733 __table_args__ = (
2726 2734 UniqueConstraint('user_id', 'follows_repository_id'),
2727 2735 UniqueConstraint('user_id', 'follows_user_id'),
2728 2736 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 2737 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 2738 )
2731 2739
2732 2740 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 2741 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 2742 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 2743 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 2744 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737 2745
2738 2746 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739 2747
2740 2748 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 2749 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742 2750
2743 2751 @classmethod
2744 2752 def get_repo_followers(cls, repo_id):
2745 2753 return cls.query().filter(cls.follows_repo_id == repo_id)
2746 2754
2747 2755
2748 2756 class CacheKey(Base, BaseModel):
2749 2757 __tablename__ = 'cache_invalidation'
2750 2758 __table_args__ = (
2751 2759 UniqueConstraint('cache_key'),
2752 2760 Index('key_idx', 'cache_key'),
2753 2761 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 2762 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 2763 )
2756 2764 CACHE_TYPE_ATOM = 'ATOM'
2757 2765 CACHE_TYPE_RSS = 'RSS'
2758 2766 CACHE_TYPE_README = 'README'
2759 2767
2760 2768 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 2769 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 2770 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 2771 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764 2772
2765 2773 def __init__(self, cache_key, cache_args=''):
2766 2774 self.cache_key = cache_key
2767 2775 self.cache_args = cache_args
2768 2776 self.cache_active = False
2769 2777
2770 2778 def __unicode__(self):
2771 2779 return u"<%s('%s:%s[%s]')>" % (
2772 2780 self.__class__.__name__,
2773 2781 self.cache_id, self.cache_key, self.cache_active)
2774 2782
2775 2783 def _cache_key_partition(self):
2776 2784 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 2785 return prefix, repo_name, suffix
2778 2786
2779 2787 def get_prefix(self):
2780 2788 """
2781 2789 Try to extract prefix from existing cache key. The key could consist
2782 2790 of prefix, repo_name, suffix
2783 2791 """
2784 2792 # this returns prefix, repo_name, suffix
2785 2793 return self._cache_key_partition()[0]
2786 2794
2787 2795 def get_suffix(self):
2788 2796 """
2789 2797 get suffix that might have been used in _get_cache_key to
2790 2798 generate self.cache_key. Only used for informational purposes
2791 2799 in repo_edit.html.
2792 2800 """
2793 2801 # prefix, repo_name, suffix
2794 2802 return self._cache_key_partition()[2]
2795 2803
2796 2804 @classmethod
2797 2805 def delete_all_cache(cls):
2798 2806 """
2799 2807 Delete all cache keys from database.
2800 2808 Should only be run when all instances are down and all entries
2801 2809 thus stale.
2802 2810 """
2803 2811 cls.query().delete()
2804 2812 Session().commit()
2805 2813
2806 2814 @classmethod
2807 2815 def get_cache_key(cls, repo_name, cache_type):
2808 2816 """
2809 2817
2810 2818 Generate a cache key for this process of RhodeCode instance.
2811 2819 Prefix most likely will be process id or maybe explicitly set
2812 2820 instance_id from .ini file.
2813 2821 """
2814 2822 import rhodecode
2815 2823 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816 2824
2817 2825 repo_as_unicode = safe_unicode(repo_name)
2818 2826 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 2827 if cache_type else repo_as_unicode
2820 2828
2821 2829 return u'{}{}'.format(prefix, key)
2822 2830
2823 2831 @classmethod
2824 2832 def set_invalidate(cls, repo_name, delete=False):
2825 2833 """
2826 2834 Mark all caches of a repo as invalid in the database.
2827 2835 """
2828 2836
2829 2837 try:
2830 2838 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 2839 if delete:
2832 2840 log.debug('cache objects deleted for repo %s',
2833 2841 safe_str(repo_name))
2834 2842 qry.delete()
2835 2843 else:
2836 2844 log.debug('cache objects marked as invalid for repo %s',
2837 2845 safe_str(repo_name))
2838 2846 qry.update({"cache_active": False})
2839 2847
2840 2848 Session().commit()
2841 2849 except Exception:
2842 2850 log.exception(
2843 2851 'Cache key invalidation failed for repository %s',
2844 2852 safe_str(repo_name))
2845 2853 Session().rollback()
2846 2854
2847 2855 @classmethod
2848 2856 def get_active_cache(cls, cache_key):
2849 2857 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 2858 if inv_obj:
2851 2859 return inv_obj
2852 2860 return None
2853 2861
2854 2862 @classmethod
2855 2863 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 2864 thread_scoped=False):
2857 2865 """
2858 2866 @cache_region('long_term')
2859 2867 def _heavy_calculation(cache_key):
2860 2868 return 'result'
2861 2869
2862 2870 cache_context = CacheKey.repo_context_cache(
2863 2871 _heavy_calculation, repo_name, cache_type)
2864 2872
2865 2873 with cache_context as context:
2866 2874 context.invalidate()
2867 2875 computed = context.compute()
2868 2876
2869 2877 assert computed == 'result'
2870 2878 """
2871 2879 from rhodecode.lib import caches
2872 2880 return caches.InvalidationContext(
2873 2881 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874 2882
2875 2883
2876 2884 class ChangesetComment(Base, BaseModel):
2877 2885 __tablename__ = 'changeset_comments'
2878 2886 __table_args__ = (
2879 2887 Index('cc_revision_idx', 'revision'),
2880 2888 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 2889 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 2890 )
2883 2891
2884 2892 COMMENT_OUTDATED = u'comment_outdated'
2885 2893
2886 2894 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 2895 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 2896 revision = Column('revision', String(40), nullable=True)
2889 2897 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 2898 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 2899 line_no = Column('line_no', Unicode(10), nullable=True)
2892 2900 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 2901 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 2902 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 2903 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 2904 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 2905 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 2906 renderer = Column('renderer', Unicode(64), nullable=True)
2899 2907 display_state = Column('display_state', Unicode(128), nullable=True)
2900 2908
2901 2909 author = relationship('User', lazy='joined')
2902 2910 repo = relationship('Repository')
2903 2911 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 2912 pull_request = relationship('PullRequest', lazy='joined')
2905 2913 pull_request_version = relationship('PullRequestVersion')
2906 2914
2907 2915 @classmethod
2908 2916 def get_users(cls, revision=None, pull_request_id=None):
2909 2917 """
2910 2918 Returns user associated with this ChangesetComment. ie those
2911 2919 who actually commented
2912 2920
2913 2921 :param cls:
2914 2922 :param revision:
2915 2923 """
2916 2924 q = Session().query(User)\
2917 2925 .join(ChangesetComment.author)
2918 2926 if revision:
2919 2927 q = q.filter(cls.revision == revision)
2920 2928 elif pull_request_id:
2921 2929 q = q.filter(cls.pull_request_id == pull_request_id)
2922 2930 return q.all()
2923 2931
2924 2932 def render(self, mentions=False):
2925 2933 from rhodecode.lib import helpers as h
2926 2934 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927 2935
2928 2936 def __repr__(self):
2929 2937 if self.comment_id:
2930 2938 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 2939 else:
2932 2940 return '<DB:ChangesetComment at %#x>' % id(self)
2933 2941
2934 2942
2935 2943 class ChangesetStatus(Base, BaseModel):
2936 2944 __tablename__ = 'changeset_statuses'
2937 2945 __table_args__ = (
2938 2946 Index('cs_revision_idx', 'revision'),
2939 2947 Index('cs_version_idx', 'version'),
2940 2948 UniqueConstraint('repo_id', 'revision', 'version'),
2941 2949 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 2950 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 2951 )
2944 2952 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 2953 STATUS_APPROVED = 'approved'
2946 2954 STATUS_REJECTED = 'rejected'
2947 2955 STATUS_UNDER_REVIEW = 'under_review'
2948 2956
2949 2957 STATUSES = [
2950 2958 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 2959 (STATUS_APPROVED, _("Approved")),
2952 2960 (STATUS_REJECTED, _("Rejected")),
2953 2961 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 2962 ]
2955 2963
2956 2964 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 2965 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 2966 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 2967 revision = Column('revision', String(40), nullable=False)
2960 2968 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 2969 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 2970 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 2971 version = Column('version', Integer(), nullable=False, default=0)
2964 2972 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965 2973
2966 2974 author = relationship('User', lazy='joined')
2967 2975 repo = relationship('Repository')
2968 2976 comment = relationship('ChangesetComment', lazy='joined')
2969 2977 pull_request = relationship('PullRequest', lazy='joined')
2970 2978
2971 2979 def __unicode__(self):
2972 2980 return u"<%s('%s[%s]:%s')>" % (
2973 2981 self.__class__.__name__,
2974 2982 self.status, self.version, self.author
2975 2983 )
2976 2984
2977 2985 @classmethod
2978 2986 def get_status_lbl(cls, value):
2979 2987 return dict(cls.STATUSES).get(value)
2980 2988
2981 2989 @property
2982 2990 def status_lbl(self):
2983 2991 return ChangesetStatus.get_status_lbl(self.status)
2984 2992
2985 2993
2986 2994 class _PullRequestBase(BaseModel):
2987 2995 """
2988 2996 Common attributes of pull request and version entries.
2989 2997 """
2990 2998
2991 2999 # .status values
2992 3000 STATUS_NEW = u'new'
2993 3001 STATUS_OPEN = u'open'
2994 3002 STATUS_CLOSED = u'closed'
2995 3003
2996 3004 title = Column('title', Unicode(255), nullable=True)
2997 3005 description = Column(
2998 3006 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 3007 nullable=True)
3000 3008 # new/open/closed status of pull request (not approve/reject/etc)
3001 3009 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 3010 created_on = Column(
3003 3011 'created_on', DateTime(timezone=False), nullable=False,
3004 3012 default=datetime.datetime.now)
3005 3013 updated_on = Column(
3006 3014 'updated_on', DateTime(timezone=False), nullable=False,
3007 3015 default=datetime.datetime.now)
3008 3016
3009 3017 @declared_attr
3010 3018 def user_id(cls):
3011 3019 return Column(
3012 3020 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 3021 unique=None)
3014 3022
3015 3023 # 500 revisions max
3016 3024 _revisions = Column(
3017 3025 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018 3026
3019 3027 @declared_attr
3020 3028 def source_repo_id(cls):
3021 3029 # TODO: dan: rename column to source_repo_id
3022 3030 return Column(
3023 3031 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 3032 nullable=False)
3025 3033
3026 3034 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027 3035
3028 3036 @declared_attr
3029 3037 def target_repo_id(cls):
3030 3038 # TODO: dan: rename column to target_repo_id
3031 3039 return Column(
3032 3040 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 3041 nullable=False)
3034 3042
3035 3043 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036 3044 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3037 3045
3038 3046 # TODO: dan: rename column to last_merge_source_rev
3039 3047 _last_merge_source_rev = Column(
3040 3048 'last_merge_org_rev', String(40), nullable=True)
3041 3049 # TODO: dan: rename column to last_merge_target_rev
3042 3050 _last_merge_target_rev = Column(
3043 3051 'last_merge_other_rev', String(40), nullable=True)
3044 3052 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3045 3053 merge_rev = Column('merge_rev', String(40), nullable=True)
3046 3054
3047 3055 @hybrid_property
3048 3056 def revisions(self):
3049 3057 return self._revisions.split(':') if self._revisions else []
3050 3058
3051 3059 @revisions.setter
3052 3060 def revisions(self, val):
3053 3061 self._revisions = ':'.join(val)
3054 3062
3055 3063 @declared_attr
3056 3064 def author(cls):
3057 3065 return relationship('User', lazy='joined')
3058 3066
3059 3067 @declared_attr
3060 3068 def source_repo(cls):
3061 3069 return relationship(
3062 3070 'Repository',
3063 3071 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3064 3072
3065 3073 @property
3066 3074 def source_ref_parts(self):
3067 3075 return self.unicode_to_reference(self.source_ref)
3068 3076
3069 3077 @declared_attr
3070 3078 def target_repo(cls):
3071 3079 return relationship(
3072 3080 'Repository',
3073 3081 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074 3082
3075 3083 @property
3076 3084 def target_ref_parts(self):
3077 3085 return self.unicode_to_reference(self.target_ref)
3078 3086
3079 3087 @property
3080 3088 def shadow_merge_ref(self):
3081 3089 return self.unicode_to_reference(self._shadow_merge_ref)
3082 3090
3083 3091 @shadow_merge_ref.setter
3084 3092 def shadow_merge_ref(self, ref):
3085 3093 self._shadow_merge_ref = self.reference_to_unicode(ref)
3086 3094
3087 3095 def unicode_to_reference(self, raw):
3088 3096 """
3089 3097 Convert a unicode (or string) to a reference object.
3090 3098 If unicode evaluates to False it returns None.
3091 3099 """
3092 3100 if raw:
3093 3101 refs = raw.split(':')
3094 3102 return Reference(*refs)
3095 3103 else:
3096 3104 return None
3097 3105
3098 3106 def reference_to_unicode(self, ref):
3099 3107 """
3100 3108 Convert a reference object to unicode.
3101 3109 If reference is None it returns None.
3102 3110 """
3103 3111 if ref:
3104 3112 return u':'.join(ref)
3105 3113 else:
3106 3114 return None
3107 3115
3108 3116
3109 3117 class PullRequest(Base, _PullRequestBase):
3110 3118 __tablename__ = 'pull_requests'
3111 3119 __table_args__ = (
3112 3120 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3113 3121 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3114 3122 )
3115 3123
3116 3124 pull_request_id = Column(
3117 3125 'pull_request_id', Integer(), nullable=False, primary_key=True)
3118 3126
3119 3127 def __repr__(self):
3120 3128 if self.pull_request_id:
3121 3129 return '<DB:PullRequest #%s>' % self.pull_request_id
3122 3130 else:
3123 3131 return '<DB:PullRequest at %#x>' % id(self)
3124 3132
3125 3133 reviewers = relationship('PullRequestReviewers',
3126 3134 cascade="all, delete, delete-orphan")
3127 3135 statuses = relationship('ChangesetStatus')
3128 3136 comments = relationship('ChangesetComment',
3129 3137 cascade="all, delete, delete-orphan")
3130 3138 versions = relationship('PullRequestVersion',
3131 3139 cascade="all, delete, delete-orphan")
3132 3140
3133 3141 def is_closed(self):
3134 3142 return self.status == self.STATUS_CLOSED
3135 3143
3136 3144 def get_api_data(self):
3137 3145 from rhodecode.model.pull_request import PullRequestModel
3138 3146 pull_request = self
3139 3147 merge_status = PullRequestModel().merge_status(pull_request)
3140 3148
3141 3149 pull_request_url = url(
3142 3150 'pullrequest_show', repo_name=self.target_repo.repo_name,
3143 3151 pull_request_id=self.pull_request_id, qualified=True)
3144 3152
3145 3153 merge_data = {
3146 3154 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3147 3155 'reference': (
3148 3156 pull_request.shadow_merge_ref._asdict()
3149 3157 if pull_request.shadow_merge_ref else None),
3150 3158 }
3151 3159
3152 3160 data = {
3153 3161 'pull_request_id': pull_request.pull_request_id,
3154 3162 'url': pull_request_url,
3155 3163 'title': pull_request.title,
3156 3164 'description': pull_request.description,
3157 3165 'status': pull_request.status,
3158 3166 'created_on': pull_request.created_on,
3159 3167 'updated_on': pull_request.updated_on,
3160 3168 'commit_ids': pull_request.revisions,
3161 3169 'review_status': pull_request.calculated_review_status(),
3162 3170 'mergeable': {
3163 3171 'status': merge_status[0],
3164 3172 'message': unicode(merge_status[1]),
3165 3173 },
3166 3174 'source': {
3167 3175 'clone_url': pull_request.source_repo.clone_url(),
3168 3176 'repository': pull_request.source_repo.repo_name,
3169 3177 'reference': {
3170 3178 'name': pull_request.source_ref_parts.name,
3171 3179 'type': pull_request.source_ref_parts.type,
3172 3180 'commit_id': pull_request.source_ref_parts.commit_id,
3173 3181 },
3174 3182 },
3175 3183 'target': {
3176 3184 'clone_url': pull_request.target_repo.clone_url(),
3177 3185 'repository': pull_request.target_repo.repo_name,
3178 3186 'reference': {
3179 3187 'name': pull_request.target_ref_parts.name,
3180 3188 'type': pull_request.target_ref_parts.type,
3181 3189 'commit_id': pull_request.target_ref_parts.commit_id,
3182 3190 },
3183 3191 },
3184 3192 'merge': merge_data,
3185 3193 'author': pull_request.author.get_api_data(include_secrets=False,
3186 3194 details='basic'),
3187 3195 'reviewers': [
3188 3196 {
3189 3197 'user': reviewer.get_api_data(include_secrets=False,
3190 3198 details='basic'),
3191 3199 'reasons': reasons,
3192 3200 'review_status': st[0][1].status if st else 'not_reviewed',
3193 3201 }
3194 3202 for reviewer, reasons, st in pull_request.reviewers_statuses()
3195 3203 ]
3196 3204 }
3197 3205
3198 3206 return data
3199 3207
3200 3208 def __json__(self):
3201 3209 return {
3202 3210 'revisions': self.revisions,
3203 3211 }
3204 3212
3205 3213 def calculated_review_status(self):
3206 3214 from rhodecode.model.changeset_status import ChangesetStatusModel
3207 3215 return ChangesetStatusModel().calculated_review_status(self)
3208 3216
3209 3217 def reviewers_statuses(self):
3210 3218 from rhodecode.model.changeset_status import ChangesetStatusModel
3211 3219 return ChangesetStatusModel().reviewers_statuses(self)
3212 3220
3213 3221
3214 3222 class PullRequestVersion(Base, _PullRequestBase):
3215 3223 __tablename__ = 'pull_request_versions'
3216 3224 __table_args__ = (
3217 3225 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3218 3226 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3219 3227 )
3220 3228
3221 3229 pull_request_version_id = Column(
3222 3230 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3223 3231 pull_request_id = Column(
3224 3232 'pull_request_id', Integer(),
3225 3233 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3226 3234 pull_request = relationship('PullRequest')
3227 3235
3228 3236 def __repr__(self):
3229 3237 if self.pull_request_version_id:
3230 3238 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3231 3239 else:
3232 3240 return '<DB:PullRequestVersion at %#x>' % id(self)
3233 3241
3234 3242
3235 3243 class PullRequestReviewers(Base, BaseModel):
3236 3244 __tablename__ = 'pull_request_reviewers'
3237 3245 __table_args__ = (
3238 3246 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3239 3247 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3240 3248 )
3241 3249
3242 3250 def __init__(self, user=None, pull_request=None, reasons=None):
3243 3251 self.user = user
3244 3252 self.pull_request = pull_request
3245 3253 self.reasons = reasons or []
3246 3254
3247 3255 @hybrid_property
3248 3256 def reasons(self):
3249 3257 if not self._reasons:
3250 3258 return []
3251 3259 return self._reasons
3252 3260
3253 3261 @reasons.setter
3254 3262 def reasons(self, val):
3255 3263 val = val or []
3256 3264 if any(not isinstance(x, basestring) for x in val):
3257 3265 raise Exception('invalid reasons type, must be list of strings')
3258 3266 self._reasons = val
3259 3267
3260 3268 pull_requests_reviewers_id = Column(
3261 3269 'pull_requests_reviewers_id', Integer(), nullable=False,
3262 3270 primary_key=True)
3263 3271 pull_request_id = Column(
3264 3272 "pull_request_id", Integer(),
3265 3273 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3266 3274 user_id = Column(
3267 3275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3268 3276 _reasons = Column(
3269 3277 'reason', MutationList.as_mutable(
3270 3278 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3271 3279
3272 3280 user = relationship('User')
3273 3281 pull_request = relationship('PullRequest')
3274 3282
3275 3283
3276 3284 class Notification(Base, BaseModel):
3277 3285 __tablename__ = 'notifications'
3278 3286 __table_args__ = (
3279 3287 Index('notification_type_idx', 'type'),
3280 3288 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3281 3289 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3282 3290 )
3283 3291
3284 3292 TYPE_CHANGESET_COMMENT = u'cs_comment'
3285 3293 TYPE_MESSAGE = u'message'
3286 3294 TYPE_MENTION = u'mention'
3287 3295 TYPE_REGISTRATION = u'registration'
3288 3296 TYPE_PULL_REQUEST = u'pull_request'
3289 3297 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3290 3298
3291 3299 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3292 3300 subject = Column('subject', Unicode(512), nullable=True)
3293 3301 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3294 3302 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3295 3303 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3296 3304 type_ = Column('type', Unicode(255))
3297 3305
3298 3306 created_by_user = relationship('User')
3299 3307 notifications_to_users = relationship('UserNotification', lazy='joined',
3300 3308 cascade="all, delete, delete-orphan")
3301 3309
3302 3310 @property
3303 3311 def recipients(self):
3304 3312 return [x.user for x in UserNotification.query()\
3305 3313 .filter(UserNotification.notification == self)\
3306 3314 .order_by(UserNotification.user_id.asc()).all()]
3307 3315
3308 3316 @classmethod
3309 3317 def create(cls, created_by, subject, body, recipients, type_=None):
3310 3318 if type_ is None:
3311 3319 type_ = Notification.TYPE_MESSAGE
3312 3320
3313 3321 notification = cls()
3314 3322 notification.created_by_user = created_by
3315 3323 notification.subject = subject
3316 3324 notification.body = body
3317 3325 notification.type_ = type_
3318 3326 notification.created_on = datetime.datetime.now()
3319 3327
3320 3328 for u in recipients:
3321 3329 assoc = UserNotification()
3322 3330 assoc.notification = notification
3323 3331
3324 3332 # if created_by is inside recipients mark his notification
3325 3333 # as read
3326 3334 if u.user_id == created_by.user_id:
3327 3335 assoc.read = True
3328 3336
3329 3337 u.notifications.append(assoc)
3330 3338 Session().add(notification)
3331 3339
3332 3340 return notification
3333 3341
3334 3342 @property
3335 3343 def description(self):
3336 3344 from rhodecode.model.notification import NotificationModel
3337 3345 return NotificationModel().make_description(self)
3338 3346
3339 3347
3340 3348 class UserNotification(Base, BaseModel):
3341 3349 __tablename__ = 'user_to_notification'
3342 3350 __table_args__ = (
3343 3351 UniqueConstraint('user_id', 'notification_id'),
3344 3352 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3345 3353 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3346 3354 )
3347 3355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3348 3356 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3349 3357 read = Column('read', Boolean, default=False)
3350 3358 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3351 3359
3352 3360 user = relationship('User', lazy="joined")
3353 3361 notification = relationship('Notification', lazy="joined",
3354 3362 order_by=lambda: Notification.created_on.desc(),)
3355 3363
3356 3364 def mark_as_read(self):
3357 3365 self.read = True
3358 3366 Session().add(self)
3359 3367
3360 3368
3361 3369 class Gist(Base, BaseModel):
3362 3370 __tablename__ = 'gists'
3363 3371 __table_args__ = (
3364 3372 Index('g_gist_access_id_idx', 'gist_access_id'),
3365 3373 Index('g_created_on_idx', 'created_on'),
3366 3374 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3367 3375 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3368 3376 )
3369 3377 GIST_PUBLIC = u'public'
3370 3378 GIST_PRIVATE = u'private'
3371 3379 DEFAULT_FILENAME = u'gistfile1.txt'
3372 3380
3373 3381 ACL_LEVEL_PUBLIC = u'acl_public'
3374 3382 ACL_LEVEL_PRIVATE = u'acl_private'
3375 3383
3376 3384 gist_id = Column('gist_id', Integer(), primary_key=True)
3377 3385 gist_access_id = Column('gist_access_id', Unicode(250))
3378 3386 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3379 3387 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3380 3388 gist_expires = Column('gist_expires', Float(53), nullable=False)
3381 3389 gist_type = Column('gist_type', Unicode(128), nullable=False)
3382 3390 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3383 3391 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3384 3392 acl_level = Column('acl_level', Unicode(128), nullable=True)
3385 3393
3386 3394 owner = relationship('User')
3387 3395
3388 3396 def __repr__(self):
3389 3397 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3390 3398
3391 3399 @classmethod
3392 3400 def get_or_404(cls, id_):
3393 3401 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3394 3402 if not res:
3395 3403 raise HTTPNotFound
3396 3404 return res
3397 3405
3398 3406 @classmethod
3399 3407 def get_by_access_id(cls, gist_access_id):
3400 3408 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3401 3409
3402 3410 def gist_url(self):
3403 3411 import rhodecode
3404 3412 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3405 3413 if alias_url:
3406 3414 return alias_url.replace('{gistid}', self.gist_access_id)
3407 3415
3408 3416 return url('gist', gist_id=self.gist_access_id, qualified=True)
3409 3417
3410 3418 @classmethod
3411 3419 def base_path(cls):
3412 3420 """
3413 3421 Returns base path when all gists are stored
3414 3422
3415 3423 :param cls:
3416 3424 """
3417 3425 from rhodecode.model.gist import GIST_STORE_LOC
3418 3426 q = Session().query(RhodeCodeUi)\
3419 3427 .filter(RhodeCodeUi.ui_key == URL_SEP)
3420 3428 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3421 3429 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3422 3430
3423 3431 def get_api_data(self):
3424 3432 """
3425 3433 Common function for generating gist related data for API
3426 3434 """
3427 3435 gist = self
3428 3436 data = {
3429 3437 'gist_id': gist.gist_id,
3430 3438 'type': gist.gist_type,
3431 3439 'access_id': gist.gist_access_id,
3432 3440 'description': gist.gist_description,
3433 3441 'url': gist.gist_url(),
3434 3442 'expires': gist.gist_expires,
3435 3443 'created_on': gist.created_on,
3436 3444 'modified_at': gist.modified_at,
3437 3445 'content': None,
3438 3446 'acl_level': gist.acl_level,
3439 3447 }
3440 3448 return data
3441 3449
3442 3450 def __json__(self):
3443 3451 data = dict(
3444 3452 )
3445 3453 data.update(self.get_api_data())
3446 3454 return data
3447 3455 # SCM functions
3448 3456
3449 3457 def scm_instance(self, **kwargs):
3450 3458 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3451 3459 return get_vcs_instance(
3452 3460 repo_path=safe_str(full_repo_path), create=False)
3453 3461
3454 3462
3455 3463 class DbMigrateVersion(Base, BaseModel):
3456 3464 __tablename__ = 'db_migrate_version'
3457 3465 __table_args__ = (
3458 3466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3459 3467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3460 3468 )
3461 3469 repository_id = Column('repository_id', String(250), primary_key=True)
3462 3470 repository_path = Column('repository_path', Text)
3463 3471 version = Column('version', Integer)
3464 3472
3465 3473
3466 3474 class ExternalIdentity(Base, BaseModel):
3467 3475 __tablename__ = 'external_identities'
3468 3476 __table_args__ = (
3469 3477 Index('local_user_id_idx', 'local_user_id'),
3470 3478 Index('external_id_idx', 'external_id'),
3471 3479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3472 3480 'mysql_charset': 'utf8'})
3473 3481
3474 3482 external_id = Column('external_id', Unicode(255), default=u'',
3475 3483 primary_key=True)
3476 3484 external_username = Column('external_username', Unicode(1024), default=u'')
3477 3485 local_user_id = Column('local_user_id', Integer(),
3478 3486 ForeignKey('users.user_id'), primary_key=True)
3479 3487 provider_name = Column('provider_name', Unicode(255), default=u'',
3480 3488 primary_key=True)
3481 3489 access_token = Column('access_token', String(1024), default=u'')
3482 3490 alt_token = Column('alt_token', String(1024), default=u'')
3483 3491 token_secret = Column('token_secret', String(1024), default=u'')
3484 3492
3485 3493 @classmethod
3486 3494 def by_external_id_and_provider(cls, external_id, provider_name,
3487 3495 local_user_id=None):
3488 3496 """
3489 3497 Returns ExternalIdentity instance based on search params
3490 3498
3491 3499 :param external_id:
3492 3500 :param provider_name:
3493 3501 :return: ExternalIdentity
3494 3502 """
3495 3503 query = cls.query()
3496 3504 query = query.filter(cls.external_id == external_id)
3497 3505 query = query.filter(cls.provider_name == provider_name)
3498 3506 if local_user_id:
3499 3507 query = query.filter(cls.local_user_id == local_user_id)
3500 3508 return query.first()
3501 3509
3502 3510 @classmethod
3503 3511 def user_by_external_id_and_provider(cls, external_id, provider_name):
3504 3512 """
3505 3513 Returns User instance based on search params
3506 3514
3507 3515 :param external_id:
3508 3516 :param provider_name:
3509 3517 :return: User
3510 3518 """
3511 3519 query = User.query()
3512 3520 query = query.filter(cls.external_id == external_id)
3513 3521 query = query.filter(cls.provider_name == provider_name)
3514 3522 query = query.filter(User.user_id == cls.local_user_id)
3515 3523 return query.first()
3516 3524
3517 3525 @classmethod
3518 3526 def by_local_user_id(cls, local_user_id):
3519 3527 """
3520 3528 Returns all tokens for user
3521 3529
3522 3530 :param local_user_id:
3523 3531 :return: ExternalIdentity
3524 3532 """
3525 3533 query = cls.query()
3526 3534 query = query.filter(cls.local_user_id == local_user_id)
3527 3535 return query
3528 3536
3529 3537
3530 3538 class Integration(Base, BaseModel):
3531 3539 __tablename__ = 'integrations'
3532 3540 __table_args__ = (
3533 3541 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3534 3542 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3535 3543 )
3536 3544
3537 3545 integration_id = Column('integration_id', Integer(), primary_key=True)
3538 3546 integration_type = Column('integration_type', String(255))
3539 3547 enabled = Column('enabled', Boolean(), nullable=False)
3540 3548 name = Column('name', String(255), nullable=False)
3541 3549 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3542 3550 default=False)
3543 3551
3544 3552 settings = Column(
3545 3553 'settings_json', MutationObj.as_mutable(
3546 3554 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3547 3555 repo_id = Column(
3548 3556 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3549 3557 nullable=True, unique=None, default=None)
3550 3558 repo = relationship('Repository', lazy='joined')
3551 3559
3552 3560 repo_group_id = Column(
3553 3561 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3554 3562 nullable=True, unique=None, default=None)
3555 3563 repo_group = relationship('RepoGroup', lazy='joined')
3556 3564
3557 3565 @property
3558 3566 def scope(self):
3559 3567 if self.repo:
3560 3568 return repr(self.repo)
3561 3569 if self.repo_group:
3562 3570 if self.child_repos_only:
3563 3571 return repr(self.repo_group) + ' (child repos only)'
3564 3572 else:
3565 3573 return repr(self.repo_group) + ' (recursive)'
3566 3574 if self.child_repos_only:
3567 3575 return 'root_repos'
3568 3576 return 'global'
3569 3577
3570 3578 def __repr__(self):
3571 3579 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3572 3580
3573 3581
3574 3582 class RepoReviewRuleUser(Base, BaseModel):
3575 3583 __tablename__ = 'repo_review_rules_users'
3576 3584 __table_args__ = (
3577 3585 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3578 3586 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3579 3587 )
3580 3588 repo_review_rule_user_id = Column(
3581 3589 'repo_review_rule_user_id', Integer(), primary_key=True)
3582 3590 repo_review_rule_id = Column("repo_review_rule_id",
3583 3591 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3584 3592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3585 3593 nullable=False)
3586 3594 user = relationship('User')
3587 3595
3588 3596
3589 3597 class RepoReviewRuleUserGroup(Base, BaseModel):
3590 3598 __tablename__ = 'repo_review_rules_users_groups'
3591 3599 __table_args__ = (
3592 3600 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3593 3601 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3594 3602 )
3595 3603 repo_review_rule_users_group_id = Column(
3596 3604 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3597 3605 repo_review_rule_id = Column("repo_review_rule_id",
3598 3606 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3599 3607 users_group_id = Column("users_group_id", Integer(),
3600 3608 ForeignKey('users_groups.users_group_id'), nullable=False)
3601 3609 users_group = relationship('UserGroup')
3602 3610
3603 3611
3604 3612 class RepoReviewRule(Base, BaseModel):
3605 3613 __tablename__ = 'repo_review_rules'
3606 3614 __table_args__ = (
3607 3615 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3608 3616 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3609 3617 )
3610 3618
3611 3619 repo_review_rule_id = Column(
3612 3620 'repo_review_rule_id', Integer(), primary_key=True)
3613 3621 repo_id = Column(
3614 3622 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3615 3623 repo = relationship('Repository', backref='review_rules')
3616 3624
3617 3625 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3618 3626 default=u'*') # glob
3619 3627 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3620 3628 default=u'*') # glob
3621 3629
3622 3630 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3623 3631 nullable=False, default=False)
3624 3632 rule_users = relationship('RepoReviewRuleUser')
3625 3633 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3626 3634
3627 3635 @hybrid_property
3628 3636 def branch_pattern(self):
3629 3637 return self._branch_pattern or '*'
3630 3638
3631 3639 def _validate_glob(self, value):
3632 3640 re.compile('^' + glob2re(value) + '$')
3633 3641
3634 3642 @branch_pattern.setter
3635 3643 def branch_pattern(self, value):
3636 3644 self._validate_glob(value)
3637 3645 self._branch_pattern = value or '*'
3638 3646
3639 3647 @hybrid_property
3640 3648 def file_pattern(self):
3641 3649 return self._file_pattern or '*'
3642 3650
3643 3651 @file_pattern.setter
3644 3652 def file_pattern(self, value):
3645 3653 self._validate_glob(value)
3646 3654 self._file_pattern = value or '*'
3647 3655
3648 3656 def matches(self, branch, files_changed):
3649 3657 """
3650 3658 Check if this review rule matches a branch/files in a pull request
3651 3659
3652 3660 :param branch: branch name for the commit
3653 3661 :param files_changed: list of file paths changed in the pull request
3654 3662 """
3655 3663
3656 3664 branch = branch or ''
3657 3665 files_changed = files_changed or []
3658 3666
3659 3667 branch_matches = True
3660 3668 if branch:
3661 3669 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3662 3670 branch_matches = bool(branch_regex.search(branch))
3663 3671
3664 3672 files_matches = True
3665 3673 if self.file_pattern != '*':
3666 3674 files_matches = False
3667 3675 file_regex = re.compile(glob2re(self.file_pattern))
3668 3676 for filename in files_changed:
3669 3677 if file_regex.search(filename):
3670 3678 files_matches = True
3671 3679 break
3672 3680
3673 3681 return branch_matches and files_matches
3674 3682
3675 3683 @property
3676 3684 def review_users(self):
3677 3685 """ Returns the users which this rule applies to """
3678 3686
3679 3687 users = set()
3680 3688 users |= set([
3681 3689 rule_user.user for rule_user in self.rule_users
3682 3690 if rule_user.user.active])
3683 3691 users |= set(
3684 3692 member.user
3685 3693 for rule_user_group in self.rule_user_groups
3686 3694 for member in rule_user_group.users_group.members
3687 3695 if member.user.active
3688 3696 )
3689 3697 return users
3690 3698
3691 3699 def __repr__(self):
3692 3700 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3693 3701 self.repo_review_rule_id, self.repo)
@@ -1,547 +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 105 def UserForm(edit=False, available_languages=[], old_data={}):
106 106 class _UserForm(formencode.Schema):
107 107 allow_extra_fields = True
108 108 filter_extra_fields = True
109 109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 110 v.ValidUsername(edit, old_data))
111 111 if edit:
112 112 new_password = All(
113 113 v.ValidPassword(),
114 114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 115 )
116 116 password_confirmation = All(
117 117 v.ValidPassword(),
118 118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 119 )
120 120 admin = v.StringBoolean(if_missing=False)
121 121 else:
122 122 password = All(
123 123 v.ValidPassword(),
124 124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 125 )
126 126 password_confirmation = All(
127 127 v.ValidPassword(),
128 128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 129 )
130 130
131 131 password_change = v.StringBoolean(if_missing=False)
132 132 create_repo_group = v.StringBoolean(if_missing=False)
133 133
134 134 active = v.StringBoolean(if_missing=False)
135 135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 138 extern_name = v.UnicodeString(strip=True)
139 139 extern_type = v.UnicodeString(strip=True)
140 140 language = v.OneOf(available_languages, hideList=False,
141 141 testValueList=True, if_missing=None)
142 142 chained_validators = [v.ValidPasswordsMatch()]
143 143 return _UserForm
144 144
145 145
146 146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
147 147 old_data = old_data or {}
148 148
149 149 class _UserGroupForm(formencode.Schema):
150 150 allow_extra_fields = True
151 151 filter_extra_fields = True
152 152
153 153 users_group_name = All(
154 154 v.UnicodeString(strip=True, min=1, not_empty=True),
155 155 v.ValidUserGroup(edit, old_data)
156 156 )
157 157 user_group_description = v.UnicodeString(strip=True, min=1,
158 158 not_empty=False)
159 159
160 160 users_group_active = v.StringBoolean(if_missing=False)
161 161
162 162 if edit:
163 163 # this is user group owner
164 164 user = All(
165 165 v.UnicodeString(not_empty=True),
166 166 v.ValidRepoUser(allow_disabled))
167 167 return _UserGroupForm
168 168
169 169
170 170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
171 171 can_create_in_root=False, allow_disabled=False):
172 172 old_data = old_data or {}
173 173 available_groups = available_groups or []
174 174
175 175 class _RepoGroupForm(formencode.Schema):
176 176 allow_extra_fields = True
177 177 filter_extra_fields = False
178 178
179 179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
180 180 v.SlugifyName(),)
181 181 group_description = v.UnicodeString(strip=True, min=1,
182 182 not_empty=False)
183 183 group_copy_permissions = v.StringBoolean(if_missing=False)
184 184
185 185 group_parent_id = v.OneOf(available_groups, hideList=False,
186 186 testValueList=True, not_empty=True)
187 187 enable_locking = v.StringBoolean(if_missing=False)
188 188 chained_validators = [
189 189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
190 190
191 191 if edit:
192 192 # this is repo group owner
193 193 user = All(
194 194 v.UnicodeString(not_empty=True),
195 195 v.ValidRepoUser(allow_disabled))
196 196
197 197 return _RepoGroupForm
198 198
199 199
200 200 def RegisterForm(edit=False, old_data={}):
201 201 class _RegisterForm(formencode.Schema):
202 202 allow_extra_fields = True
203 203 filter_extra_fields = True
204 204 username = All(
205 205 v.ValidUsername(edit, old_data),
206 206 v.UnicodeString(strip=True, min=1, not_empty=True)
207 207 )
208 208 password = All(
209 209 v.ValidPassword(),
210 210 v.UnicodeString(strip=False, min=6, not_empty=True)
211 211 )
212 212 password_confirmation = All(
213 213 v.ValidPassword(),
214 214 v.UnicodeString(strip=False, min=6, not_empty=True)
215 215 )
216 216 active = v.StringBoolean(if_missing=False)
217 217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
218 218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
219 219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
220 220
221 221 chained_validators = [v.ValidPasswordsMatch()]
222 222
223 223 return _RegisterForm
224 224
225 225
226 226 def PasswordResetForm():
227 227 class _PasswordResetForm(formencode.Schema):
228 228 allow_extra_fields = True
229 229 filter_extra_fields = True
230 230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
231 231 return _PasswordResetForm
232 232
233 233
234 234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
235 235 allow_disabled=False):
236 236 old_data = old_data or {}
237 237 repo_groups = repo_groups or []
238 238 landing_revs = landing_revs or []
239 239 supported_backends = BACKENDS.keys()
240 240
241 241 class _RepoForm(formencode.Schema):
242 242 allow_extra_fields = True
243 243 filter_extra_fields = False
244 244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
245 245 v.SlugifyName())
246 246 repo_group = All(v.CanWriteGroup(old_data),
247 247 v.OneOf(repo_groups, hideList=True))
248 248 repo_type = v.OneOf(supported_backends, required=False,
249 249 if_missing=old_data.get('repo_type'))
250 250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
251 251 repo_private = v.StringBoolean(if_missing=False)
252 252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
253 253 repo_copy_permissions = v.StringBoolean(if_missing=False)
254 254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
255 255
256 256 repo_enable_statistics = v.StringBoolean(if_missing=False)
257 257 repo_enable_downloads = v.StringBoolean(if_missing=False)
258 258 repo_enable_locking = v.StringBoolean(if_missing=False)
259 259
260 260 if edit:
261 261 # this is repo owner
262 262 user = All(
263 263 v.UnicodeString(not_empty=True),
264 264 v.ValidRepoUser(allow_disabled))
265 265 clone_uri_change = v.UnicodeString(
266 266 not_empty=False, if_missing=v.Missing)
267 267
268 268 chained_validators = [v.ValidCloneUri(),
269 269 v.ValidRepoName(edit, old_data)]
270 270 return _RepoForm
271 271
272 272
273 273 def RepoPermsForm():
274 274 class _RepoPermsForm(formencode.Schema):
275 275 allow_extra_fields = True
276 276 filter_extra_fields = False
277 277 chained_validators = [v.ValidPerms(type_='repo')]
278 278 return _RepoPermsForm
279 279
280 280
281 281 def RepoGroupPermsForm(valid_recursive_choices):
282 282 class _RepoGroupPermsForm(formencode.Schema):
283 283 allow_extra_fields = True
284 284 filter_extra_fields = False
285 285 recursive = v.OneOf(valid_recursive_choices)
286 286 chained_validators = [v.ValidPerms(type_='repo_group')]
287 287 return _RepoGroupPermsForm
288 288
289 289
290 290 def UserGroupPermsForm():
291 291 class _UserPermsForm(formencode.Schema):
292 292 allow_extra_fields = True
293 293 filter_extra_fields = False
294 294 chained_validators = [v.ValidPerms(type_='user_group')]
295 295 return _UserPermsForm
296 296
297 297
298 298 def RepoFieldForm():
299 299 class _RepoFieldForm(formencode.Schema):
300 300 filter_extra_fields = True
301 301 allow_extra_fields = True
302 302
303 303 new_field_key = All(v.FieldKey(),
304 304 v.UnicodeString(strip=True, min=3, not_empty=True))
305 305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
306 306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
307 307 if_missing='str')
308 308 new_field_label = v.UnicodeString(not_empty=False)
309 309 new_field_desc = v.UnicodeString(not_empty=False)
310 310
311 311 return _RepoFieldForm
312 312
313 313
314 314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
315 315 repo_groups=[], landing_revs=[]):
316 316 class _RepoForkForm(formencode.Schema):
317 317 allow_extra_fields = True
318 318 filter_extra_fields = False
319 319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
320 320 v.SlugifyName())
321 321 repo_group = All(v.CanWriteGroup(),
322 322 v.OneOf(repo_groups, hideList=True))
323 323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
324 324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
325 325 private = v.StringBoolean(if_missing=False)
326 326 copy_permissions = v.StringBoolean(if_missing=False)
327 327 fork_parent_id = v.UnicodeString()
328 328 chained_validators = [v.ValidForkName(edit, old_data)]
329 329 landing_rev = v.OneOf(landing_revs, hideList=True)
330 330
331 331 return _RepoForkForm
332 332
333 333
334 334 def ApplicationSettingsForm():
335 335 class _ApplicationSettingsForm(formencode.Schema):
336 336 allow_extra_fields = True
337 337 filter_extra_fields = False
338 338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
339 339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
340 340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
341 341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
342 342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
343 343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
344 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
345 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
344 346
345 347 return _ApplicationSettingsForm
346 348
347 349
348 350 def ApplicationVisualisationForm():
349 351 class _ApplicationVisualisationForm(formencode.Schema):
350 352 allow_extra_fields = True
351 353 filter_extra_fields = False
352 354 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
353 355 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
354 356 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
355 357
356 358 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
357 359 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
358 360 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
359 361 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
360 362 rhodecode_show_version = v.StringBoolean(if_missing=False)
361 363 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
362 364 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
363 365 rhodecode_gravatar_url = v.UnicodeString(min=3)
364 366 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
365 367 rhodecode_support_url = v.UnicodeString()
366 368 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
367 369 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
368 370
369 371 return _ApplicationVisualisationForm
370 372
371 373
372 374 class _BaseVcsSettingsForm(formencode.Schema):
373 375 allow_extra_fields = True
374 376 filter_extra_fields = False
375 377 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
376 378 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
377 379 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
378 380
379 381 extensions_largefiles = v.StringBoolean(if_missing=False)
380 382 phases_publish = v.StringBoolean(if_missing=False)
381 383
382 384 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
383 385 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
384 386 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
385 387
386 388 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
387 389 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
388 390
389 391
390 392 def ApplicationUiSettingsForm():
391 393 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
392 394 web_push_ssl = v.StringBoolean(if_missing=False)
393 395 paths_root_path = All(
394 396 v.ValidPath(),
395 397 v.UnicodeString(strip=True, min=1, not_empty=True)
396 398 )
397 399 extensions_hgsubversion = v.StringBoolean(if_missing=False)
398 400 extensions_hggit = v.StringBoolean(if_missing=False)
399 401 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
400 402 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
401 403
402 404 return _ApplicationUiSettingsForm
403 405
404 406
405 407 def RepoVcsSettingsForm(repo_name):
406 408 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
407 409 inherit_global_settings = v.StringBoolean(if_missing=False)
408 410 new_svn_branch = v.ValidSvnPattern(
409 411 section='vcs_svn_branch', repo_name=repo_name)
410 412 new_svn_tag = v.ValidSvnPattern(
411 413 section='vcs_svn_tag', repo_name=repo_name)
412 414
413 415 return _RepoVcsSettingsForm
414 416
415 417
416 418 def LabsSettingsForm():
417 419 class _LabSettingsForm(formencode.Schema):
418 420 allow_extra_fields = True
419 421 filter_extra_fields = False
420 422
421 423 return _LabSettingsForm
422 424
423 425
424 426 def ApplicationPermissionsForm(
425 427 register_choices, password_reset_choices, extern_activate_choices):
426 428 class _DefaultPermissionsForm(formencode.Schema):
427 429 allow_extra_fields = True
428 430 filter_extra_fields = True
429 431
430 432 anonymous = v.StringBoolean(if_missing=False)
431 433 default_register = v.OneOf(register_choices)
432 434 default_register_message = v.UnicodeString()
433 435 default_password_reset = v.OneOf(password_reset_choices)
434 436 default_extern_activate = v.OneOf(extern_activate_choices)
435 437
436 438 return _DefaultPermissionsForm
437 439
438 440
439 441 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
440 442 user_group_perms_choices):
441 443 class _ObjectPermissionsForm(formencode.Schema):
442 444 allow_extra_fields = True
443 445 filter_extra_fields = True
444 446 overwrite_default_repo = v.StringBoolean(if_missing=False)
445 447 overwrite_default_group = v.StringBoolean(if_missing=False)
446 448 overwrite_default_user_group = v.StringBoolean(if_missing=False)
447 449 default_repo_perm = v.OneOf(repo_perms_choices)
448 450 default_group_perm = v.OneOf(group_perms_choices)
449 451 default_user_group_perm = v.OneOf(user_group_perms_choices)
450 452
451 453 return _ObjectPermissionsForm
452 454
453 455
454 456 def UserPermissionsForm(create_choices, create_on_write_choices,
455 457 repo_group_create_choices, user_group_create_choices,
456 458 fork_choices, inherit_default_permissions_choices):
457 459 class _DefaultPermissionsForm(formencode.Schema):
458 460 allow_extra_fields = True
459 461 filter_extra_fields = True
460 462
461 463 anonymous = v.StringBoolean(if_missing=False)
462 464
463 465 default_repo_create = v.OneOf(create_choices)
464 466 default_repo_create_on_write = v.OneOf(create_on_write_choices)
465 467 default_user_group_create = v.OneOf(user_group_create_choices)
466 468 default_repo_group_create = v.OneOf(repo_group_create_choices)
467 469 default_fork_create = v.OneOf(fork_choices)
468 470 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
469 471
470 472 return _DefaultPermissionsForm
471 473
472 474
473 475 def UserIndividualPermissionsForm():
474 476 class _DefaultPermissionsForm(formencode.Schema):
475 477 allow_extra_fields = True
476 478 filter_extra_fields = True
477 479
478 480 inherit_default_permissions = v.StringBoolean(if_missing=False)
479 481
480 482 return _DefaultPermissionsForm
481 483
482 484
483 485 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
484 486 class _DefaultsForm(formencode.Schema):
485 487 allow_extra_fields = True
486 488 filter_extra_fields = True
487 489 default_repo_type = v.OneOf(supported_backends)
488 490 default_repo_private = v.StringBoolean(if_missing=False)
489 491 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
490 492 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
491 493 default_repo_enable_locking = v.StringBoolean(if_missing=False)
492 494
493 495 return _DefaultsForm
494 496
495 497
496 498 def AuthSettingsForm():
497 499 class _AuthSettingsForm(formencode.Schema):
498 500 allow_extra_fields = True
499 501 filter_extra_fields = True
500 502 auth_plugins = All(v.ValidAuthPlugins(),
501 503 v.UniqueListFromString()(not_empty=True))
502 504
503 505 return _AuthSettingsForm
504 506
505 507
506 508 def UserExtraEmailForm():
507 509 class _UserExtraEmailForm(formencode.Schema):
508 510 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
509 511 return _UserExtraEmailForm
510 512
511 513
512 514 def UserExtraIpForm():
513 515 class _UserExtraIpForm(formencode.Schema):
514 516 ip = v.ValidIp()(not_empty=True)
515 517 return _UserExtraIpForm
516 518
517 519
518 520
519 521 def PullRequestForm(repo_id):
520 522 class ReviewerForm(formencode.Schema):
521 523 user_id = v.Int(not_empty=True)
522 524 reasons = All()
523 525
524 526 class _PullRequestForm(formencode.Schema):
525 527 allow_extra_fields = True
526 528 filter_extra_fields = True
527 529
528 530 user = v.UnicodeString(strip=True, required=True)
529 531 source_repo = v.UnicodeString(strip=True, required=True)
530 532 source_ref = v.UnicodeString(strip=True, required=True)
531 533 target_repo = v.UnicodeString(strip=True, required=True)
532 534 target_ref = v.UnicodeString(strip=True, required=True)
533 535 revisions = All(#v.NotReviewedRevisions(repo_id)(),
534 536 v.UniqueList()(not_empty=True))
535 537 review_members = formencode.ForEach(ReviewerForm())
536 538 pullrequest_title = v.UnicodeString(strip=True, required=True)
537 539 pullrequest_desc = v.UnicodeString(strip=True, required=False)
538 540
539 541 return _PullRequestForm
540 542
541 543
542 544 def IssueTrackerPatternsForm():
543 545 class _IssueTrackerPatternsForm(formencode.Schema):
544 546 allow_extra_fields = True
545 547 filter_extra_fields = False
546 548 chained_validators = [v.ValidPattern()]
547 549 return _IssueTrackerPatternsForm
@@ -1,659 +1,701 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 repo group model for RhodeCode
24 24 """
25 25
26
26 import os
27 27 import datetime
28 28 import itertools
29 29 import logging
30 import os
31 30 import shutil
32 31 import traceback
32 import string
33 33
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode import events
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import (
39 39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 40 UserGroup, Repository)
41 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.lib.utils2 import action_logger_generic
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoGroupModel(BaseModel):
49 49
50 50 cls = RepoGroup
51 51 PERSONAL_GROUP_DESC = '[personal] repo group: owner `%(username)s`'
52 PERSONAL_GROUP_PATTERN = '${username}' # default
52 53
53 54 def _get_user_group(self, users_group):
54 55 return self._get_instance(UserGroup, users_group,
55 56 callback=UserGroup.get_by_group_name)
56 57
57 58 def _get_repo_group(self, repo_group):
58 59 return self._get_instance(RepoGroup, repo_group,
59 60 callback=RepoGroup.get_by_group_name)
60 61
61 62 @LazyProperty
62 63 def repos_path(self):
63 64 """
64 65 Gets the repositories root path from database
65 66 """
66 67
67 68 settings_model = VcsSettingsModel(sa=self.sa)
68 69 return settings_model.get_repos_location()
69 70
70 71 def get_by_group_name(self, repo_group_name, cache=None):
71 72 repo = self.sa.query(RepoGroup) \
72 73 .filter(RepoGroup.group_name == repo_group_name)
73 74
74 75 if cache:
75 76 repo = repo.options(FromCache(
76 77 "sql_cache_short", "get_repo_group_%s" % repo_group_name))
77 78 return repo.scalar()
78 79
80 def get_default_create_personal_repo_group(self):
81 value = SettingsModel().get_setting_by_name(
82 'create_personal_repo_group')
83 return value.app_settings_value if value else None or False
84
85 def get_personal_group_name_pattern(self):
86 value = SettingsModel().get_setting_by_name(
87 'personal_repo_group_pattern')
88 val = value.app_settings_value if value else None
89 group_template = val or self.PERSONAL_GROUP_PATTERN
90
91 group_template = group_template.lstrip('/')
92 return group_template
93
94 def get_personal_group_name(self, user):
95 template = self.get_personal_group_name_pattern()
96 return string.Template(template).safe_substitute(
97 username=user.username,
98 user_id=user.user_id,
99 )
100
101 def create_personal_repo_group(self, user, commit_early=True):
102 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
103 personal_repo_group_name = self.get_personal_group_name(user)
104
105 # create a new one
106 RepoGroupModel().create(
107 group_name=personal_repo_group_name,
108 group_description=desc,
109 owner=user.username,
110 personal=True,
111 commit_early=commit_early)
112
79 113 def _create_default_perms(self, new_group):
80 114 # create default permission
81 115 default_perm = 'group.read'
82 116 def_user = User.get_default_user()
83 117 for p in def_user.user_perms:
84 118 if p.permission.permission_name.startswith('group.'):
85 119 default_perm = p.permission.permission_name
86 120 break
87 121
88 122 repo_group_to_perm = UserRepoGroupToPerm()
89 123 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
90 124
91 125 repo_group_to_perm.group = new_group
92 126 repo_group_to_perm.user_id = def_user.user_id
93 127 return repo_group_to_perm
94 128
95 129 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False):
96 130 """
97 131 Get's the group name and a parent group name from given group name.
98 132 If repo_in_path is set to truth, we asume the full path also includes
99 133 repo name, in such case we clean the last element.
100 134
101 135 :param group_name_full:
102 136 """
103 137 split_paths = 1
104 138 if repo_in_path:
105 139 split_paths = 2
106 140 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
107 141
108 142 if repo_in_path and len(_parts) > 1:
109 143 # such case last element is the repo_name
110 144 _parts.pop(-1)
111 145 group_name_cleaned = _parts[-1] # just the group name
112 146 parent_repo_group_name = None
113 147
114 148 if len(_parts) > 1:
115 149 parent_repo_group_name = _parts[0]
116 150
117 151 if parent_repo_group_name:
118 152 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
119 153
120 154 return group_name_cleaned, parent_repo_group_name
121 155
122 156 def check_exist_filesystem(self, group_name, exc_on_failure=True):
123 157 create_path = os.path.join(self.repos_path, group_name)
124 158 log.debug('creating new group in %s', create_path)
125 159
126 160 if os.path.isdir(create_path):
127 161 if exc_on_failure:
128 162 raise Exception('That directory already exists !')
129 163 return False
130 164 return True
131 165
132 166 def _create_group(self, group_name):
133 167 """
134 168 makes repository group on filesystem
135 169
136 170 :param repo_name:
137 171 :param parent_id:
138 172 """
139 173
140 174 self.check_exist_filesystem(group_name)
141 175 create_path = os.path.join(self.repos_path, group_name)
142 176 log.debug('creating new group in %s', create_path)
143 177 os.makedirs(create_path, mode=0755)
144 178 log.debug('created group in %s', create_path)
145 179
146 180 def _rename_group(self, old, new):
147 181 """
148 182 Renames a group on filesystem
149 183
150 184 :param group_name:
151 185 """
152 186
153 187 if old == new:
154 188 log.debug('skipping group rename')
155 189 return
156 190
157 191 log.debug('renaming repository group from %s to %s', old, new)
158 192
159 193 old_path = os.path.join(self.repos_path, old)
160 194 new_path = os.path.join(self.repos_path, new)
161 195
162 196 log.debug('renaming repos paths from %s to %s', old_path, new_path)
163 197
164 198 if os.path.isdir(new_path):
165 199 raise Exception('Was trying to rename to already '
166 200 'existing dir %s' % new_path)
167 201 shutil.move(old_path, new_path)
168 202
169 203 def _delete_filesystem_group(self, group, force_delete=False):
170 204 """
171 205 Deletes a group from a filesystem
172 206
173 207 :param group: instance of group from database
174 208 :param force_delete: use shutil rmtree to remove all objects
175 209 """
176 210 paths = group.full_path.split(RepoGroup.url_sep())
177 211 paths = os.sep.join(paths)
178 212
179 213 rm_path = os.path.join(self.repos_path, paths)
180 214 log.info("Removing group %s", rm_path)
181 215 # delete only if that path really exists
182 216 if os.path.isdir(rm_path):
183 217 if force_delete:
184 218 shutil.rmtree(rm_path)
185 219 else:
186 220 # archive that group`
187 221 _now = datetime.datetime.now()
188 222 _ms = str(_now.microsecond).rjust(6, '0')
189 223 _d = 'rm__%s_GROUP_%s' % (
190 224 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
191 225 shutil.move(rm_path, os.path.join(self.repos_path, _d))
192 226
193 227 def create(self, group_name, group_description, owner, just_db=False,
194 copy_permissions=False, commit_early=True):
228 copy_permissions=False, personal=None, commit_early=True):
195 229
196 230 (group_name_cleaned,
197 231 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
198 232
199 233 parent_group = None
200 234 if parent_group_name:
201 235 parent_group = self._get_repo_group(parent_group_name)
236 if not parent_group:
237 # we tried to create a nested group, but the parent is not
238 # existing
239 raise ValueError(
240 'Parent group `%s` given in `%s` group name '
241 'is not yet existing.' % (parent_group_name, group_name))
202 242
203 # becase we are doing a cleanup, we need to check if such directory
204 # already exists. If we don't do that we can accidentally delete existing
205 # directory via cleanup that can cause data issues, since delete does a
206 # folder rename to special syntax later cleanup functions can delete this
243 # because we are doing a cleanup, we need to check if such directory
244 # already exists. If we don't do that we can accidentally delete
245 # existing directory via cleanup that can cause data issues, since
246 # delete does a folder rename to special syntax later cleanup
247 # functions can delete this
207 248 cleanup_group = self.check_exist_filesystem(group_name,
208 249 exc_on_failure=False)
209 250 try:
210 251 user = self._get_user(owner)
211 252 new_repo_group = RepoGroup()
212 253 new_repo_group.user = user
213 254 new_repo_group.group_description = group_description or group_name
214 255 new_repo_group.parent_group = parent_group
215 256 new_repo_group.group_name = group_name
257 new_repo_group.personal = personal
216 258
217 259 self.sa.add(new_repo_group)
218 260
219 261 # create an ADMIN permission for owner except if we're super admin,
220 262 # later owner should go into the owner field of groups
221 263 if not user.is_admin:
222 264 self.grant_user_permission(repo_group=new_repo_group,
223 265 user=owner, perm='group.admin')
224 266
225 267 if parent_group and copy_permissions:
226 268 # copy permissions from parent
227 269 user_perms = UserRepoGroupToPerm.query() \
228 270 .filter(UserRepoGroupToPerm.group == parent_group).all()
229 271
230 272 group_perms = UserGroupRepoGroupToPerm.query() \
231 273 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
232 274
233 275 for perm in user_perms:
234 276 # don't copy over the permission for user who is creating
235 277 # this group, if he is not super admin he get's admin
236 278 # permission set above
237 279 if perm.user != user or user.is_admin:
238 280 UserRepoGroupToPerm.create(
239 281 perm.user, new_repo_group, perm.permission)
240 282
241 283 for perm in group_perms:
242 284 UserGroupRepoGroupToPerm.create(
243 285 perm.users_group, new_repo_group, perm.permission)
244 286 else:
245 287 perm_obj = self._create_default_perms(new_repo_group)
246 288 self.sa.add(perm_obj)
247 289
248 290 # now commit the changes, earlier so we are sure everything is in
249 291 # the database.
250 292 if commit_early:
251 293 self.sa.commit()
252 294 if not just_db:
253 295 self._create_group(new_repo_group.group_name)
254 296
255 297 # trigger the post hook
256 298 from rhodecode.lib.hooks_base import log_create_repository_group
257 299 repo_group = RepoGroup.get_by_group_name(group_name)
258 300 log_create_repository_group(
259 301 created_by=user.username, **repo_group.get_dict())
260 302
261 303 # Trigger create event.
262 304 events.trigger(events.RepoGroupCreateEvent(repo_group))
263 305
264 306 return new_repo_group
265 307 except Exception:
266 308 self.sa.rollback()
267 309 log.exception('Exception occurred when creating repository group, '
268 310 'doing cleanup...')
269 311 # rollback things manually !
270 312 repo_group = RepoGroup.get_by_group_name(group_name)
271 313 if repo_group:
272 314 RepoGroup.delete(repo_group.group_id)
273 315 self.sa.commit()
274 316 if cleanup_group:
275 317 RepoGroupModel()._delete_filesystem_group(repo_group)
276 318 raise
277 319
278 320 def update_permissions(
279 321 self, repo_group, perm_additions=None, perm_updates=None,
280 322 perm_deletions=None, recursive=None, check_perms=True,
281 323 cur_user=None):
282 324 from rhodecode.model.repo import RepoModel
283 325 from rhodecode.lib.auth import HasUserGroupPermissionAny
284 326
285 327 if not perm_additions:
286 328 perm_additions = []
287 329 if not perm_updates:
288 330 perm_updates = []
289 331 if not perm_deletions:
290 332 perm_deletions = []
291 333
292 334 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
293 335
294 336 def _set_perm_user(obj, user, perm):
295 337 if isinstance(obj, RepoGroup):
296 338 self.grant_user_permission(
297 339 repo_group=obj, user=user, perm=perm)
298 340 elif isinstance(obj, Repository):
299 341 # private repos will not allow to change the default
300 342 # permissions using recursive mode
301 343 if obj.private and user == User.DEFAULT_USER:
302 344 return
303 345
304 346 # we set group permission but we have to switch to repo
305 347 # permission
306 348 perm = perm.replace('group.', 'repository.')
307 349 RepoModel().grant_user_permission(
308 350 repo=obj, user=user, perm=perm)
309 351
310 352 def _set_perm_group(obj, users_group, perm):
311 353 if isinstance(obj, RepoGroup):
312 354 self.grant_user_group_permission(
313 355 repo_group=obj, group_name=users_group, perm=perm)
314 356 elif isinstance(obj, Repository):
315 357 # we set group permission but we have to switch to repo
316 358 # permission
317 359 perm = perm.replace('group.', 'repository.')
318 360 RepoModel().grant_user_group_permission(
319 361 repo=obj, group_name=users_group, perm=perm)
320 362
321 363 def _revoke_perm_user(obj, user):
322 364 if isinstance(obj, RepoGroup):
323 365 self.revoke_user_permission(repo_group=obj, user=user)
324 366 elif isinstance(obj, Repository):
325 367 RepoModel().revoke_user_permission(repo=obj, user=user)
326 368
327 369 def _revoke_perm_group(obj, user_group):
328 370 if isinstance(obj, RepoGroup):
329 371 self.revoke_user_group_permission(
330 372 repo_group=obj, group_name=user_group)
331 373 elif isinstance(obj, Repository):
332 374 RepoModel().revoke_user_group_permission(
333 375 repo=obj, group_name=user_group)
334 376
335 377 # start updates
336 378 updates = []
337 379 log.debug('Now updating permissions for %s in recursive mode:%s',
338 380 repo_group, recursive)
339 381
340 382 # initialize check function, we'll call that multiple times
341 383 has_group_perm = HasUserGroupPermissionAny(*req_perms)
342 384
343 385 for obj in repo_group.recursive_groups_and_repos():
344 386 # iterated obj is an instance of a repos group or repository in
345 387 # that group, recursive option can be: none, repos, groups, all
346 388 if recursive == 'all':
347 389 obj = obj
348 390 elif recursive == 'repos':
349 391 # skip groups, other than this one
350 392 if isinstance(obj, RepoGroup) and not obj == repo_group:
351 393 continue
352 394 elif recursive == 'groups':
353 395 # skip repos
354 396 if isinstance(obj, Repository):
355 397 continue
356 398 else: # recursive == 'none':
357 399 # DEFAULT option - don't apply to iterated objects
358 400 # also we do a break at the end of this loop. if we are not
359 401 # in recursive mode
360 402 obj = repo_group
361 403
362 404 # update permissions
363 405 for member_id, perm, member_type in perm_updates:
364 406 member_id = int(member_id)
365 407 if member_type == 'user':
366 408 # this updates also current one if found
367 409 _set_perm_user(obj, user=member_id, perm=perm)
368 410 else: # set for user group
369 411 member_name = UserGroup.get(member_id).users_group_name
370 412 if not check_perms or has_group_perm(member_name,
371 413 user=cur_user):
372 414 _set_perm_group(obj, users_group=member_id, perm=perm)
373 415
374 416 # set new permissions
375 417 for member_id, perm, member_type in perm_additions:
376 418 member_id = int(member_id)
377 419 if member_type == 'user':
378 420 _set_perm_user(obj, user=member_id, perm=perm)
379 421 else: # set for user group
380 422 # check if we have permissions to alter this usergroup
381 423 member_name = UserGroup.get(member_id).users_group_name
382 424 if not check_perms or has_group_perm(member_name,
383 425 user=cur_user):
384 426 _set_perm_group(obj, users_group=member_id, perm=perm)
385 427
386 428 # delete permissions
387 429 for member_id, perm, member_type in perm_deletions:
388 430 member_id = int(member_id)
389 431 if member_type == 'user':
390 432 _revoke_perm_user(obj, user=member_id)
391 433 else: # set for user group
392 434 # check if we have permissions to alter this usergroup
393 435 member_name = UserGroup.get(member_id).users_group_name
394 436 if not check_perms or has_group_perm(member_name,
395 437 user=cur_user):
396 438 _revoke_perm_group(obj, user_group=member_id)
397 439
398 440 updates.append(obj)
399 441 # if it's not recursive call for all,repos,groups
400 442 # break the loop and don't proceed with other changes
401 443 if recursive not in ['all', 'repos', 'groups']:
402 444 break
403 445
404 446 return updates
405 447
406 448 def update(self, repo_group, form_data):
407 449 try:
408 450 repo_group = self._get_repo_group(repo_group)
409 451 old_path = repo_group.full_path
410 452
411 453 # change properties
412 454 if 'group_description' in form_data:
413 455 repo_group.group_description = form_data['group_description']
414 456
415 457 if 'enable_locking' in form_data:
416 458 repo_group.enable_locking = form_data['enable_locking']
417 459
418 460 if 'group_parent_id' in form_data:
419 461 parent_group = (
420 462 self._get_repo_group(form_data['group_parent_id']))
421 463 repo_group.group_parent_id = (
422 464 parent_group.group_id if parent_group else None)
423 465 repo_group.parent_group = parent_group
424 466
425 467 # mikhail: to update the full_path, we have to explicitly
426 468 # update group_name
427 469 group_name = form_data.get('group_name', repo_group.name)
428 470 repo_group.group_name = repo_group.get_new_name(group_name)
429 471
430 472 new_path = repo_group.full_path
431 473
432 474 if 'user' in form_data:
433 475 repo_group.user = User.get_by_username(form_data['user'])
434 476
435 477 self.sa.add(repo_group)
436 478
437 479 # iterate over all members of this groups and do fixes
438 480 # set locking if given
439 481 # if obj is a repoGroup also fix the name of the group according
440 482 # to the parent
441 483 # if obj is a Repo fix it's name
442 484 # this can be potentially heavy operation
443 485 for obj in repo_group.recursive_groups_and_repos():
444 486 # set the value from it's parent
445 487 obj.enable_locking = repo_group.enable_locking
446 488 if isinstance(obj, RepoGroup):
447 489 new_name = obj.get_new_name(obj.name)
448 490 log.debug('Fixing group %s to new name %s',
449 491 obj.group_name, new_name)
450 492 obj.group_name = new_name
451 493 elif isinstance(obj, Repository):
452 494 # we need to get all repositories from this new group and
453 495 # rename them accordingly to new group path
454 496 new_name = obj.get_new_name(obj.just_name)
455 497 log.debug('Fixing repo %s to new name %s',
456 498 obj.repo_name, new_name)
457 499 obj.repo_name = new_name
458 500 self.sa.add(obj)
459 501
460 502 self._rename_group(old_path, new_path)
461 503
462 504 # Trigger update event.
463 505 events.trigger(events.RepoGroupUpdateEvent(repo_group))
464 506
465 507 return repo_group
466 508 except Exception:
467 509 log.error(traceback.format_exc())
468 510 raise
469 511
470 512 def delete(self, repo_group, force_delete=False, fs_remove=True):
471 513 repo_group = self._get_repo_group(repo_group)
472 514 if not repo_group:
473 515 return False
474 516 try:
475 517 self.sa.delete(repo_group)
476 518 if fs_remove:
477 519 self._delete_filesystem_group(repo_group, force_delete)
478 520 else:
479 521 log.debug('skipping removal from filesystem')
480 522
481 523 # Trigger delete event.
482 524 events.trigger(events.RepoGroupDeleteEvent(repo_group))
483 525 return True
484 526
485 527 except Exception:
486 528 log.error('Error removing repo_group %s', repo_group)
487 529 raise
488 530
489 531 def grant_user_permission(self, repo_group, user, perm):
490 532 """
491 533 Grant permission for user on given repository group, or update
492 534 existing one if found
493 535
494 536 :param repo_group: Instance of RepoGroup, repositories_group_id,
495 537 or repositories_group name
496 538 :param user: Instance of User, user_id or username
497 539 :param perm: Instance of Permission, or permission_name
498 540 """
499 541
500 542 repo_group = self._get_repo_group(repo_group)
501 543 user = self._get_user(user)
502 544 permission = self._get_perm(perm)
503 545
504 546 # check if we have that permission already
505 547 obj = self.sa.query(UserRepoGroupToPerm)\
506 548 .filter(UserRepoGroupToPerm.user == user)\
507 549 .filter(UserRepoGroupToPerm.group == repo_group)\
508 550 .scalar()
509 551 if obj is None:
510 552 # create new !
511 553 obj = UserRepoGroupToPerm()
512 554 obj.group = repo_group
513 555 obj.user = user
514 556 obj.permission = permission
515 557 self.sa.add(obj)
516 558 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
517 559 action_logger_generic(
518 560 'granted permission: {} to user: {} on repogroup: {}'.format(
519 561 perm, user, repo_group), namespace='security.repogroup')
520 562 return obj
521 563
522 564 def revoke_user_permission(self, repo_group, user):
523 565 """
524 566 Revoke permission for user on given repository group
525 567
526 568 :param repo_group: Instance of RepoGroup, repositories_group_id,
527 569 or repositories_group name
528 570 :param user: Instance of User, user_id or username
529 571 """
530 572
531 573 repo_group = self._get_repo_group(repo_group)
532 574 user = self._get_user(user)
533 575
534 576 obj = self.sa.query(UserRepoGroupToPerm)\
535 577 .filter(UserRepoGroupToPerm.user == user)\
536 578 .filter(UserRepoGroupToPerm.group == repo_group)\
537 579 .scalar()
538 580 if obj:
539 581 self.sa.delete(obj)
540 582 log.debug('Revoked perm on %s on %s', repo_group, user)
541 583 action_logger_generic(
542 584 'revoked permission from user: {} on repogroup: {}'.format(
543 585 user, repo_group), namespace='security.repogroup')
544 586
545 587 def grant_user_group_permission(self, repo_group, group_name, perm):
546 588 """
547 589 Grant permission for user group on given repository group, or update
548 590 existing one if found
549 591
550 592 :param repo_group: Instance of RepoGroup, repositories_group_id,
551 593 or repositories_group name
552 594 :param group_name: Instance of UserGroup, users_group_id,
553 595 or user group name
554 596 :param perm: Instance of Permission, or permission_name
555 597 """
556 598 repo_group = self._get_repo_group(repo_group)
557 599 group_name = self._get_user_group(group_name)
558 600 permission = self._get_perm(perm)
559 601
560 602 # check if we have that permission already
561 603 obj = self.sa.query(UserGroupRepoGroupToPerm)\
562 604 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
563 605 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
564 606 .scalar()
565 607
566 608 if obj is None:
567 609 # create new
568 610 obj = UserGroupRepoGroupToPerm()
569 611
570 612 obj.group = repo_group
571 613 obj.users_group = group_name
572 614 obj.permission = permission
573 615 self.sa.add(obj)
574 616 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
575 617 action_logger_generic(
576 618 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
577 619 perm, group_name, repo_group), namespace='security.repogroup')
578 620 return obj
579 621
580 622 def revoke_user_group_permission(self, repo_group, group_name):
581 623 """
582 624 Revoke permission for user group on given repository group
583 625
584 626 :param repo_group: Instance of RepoGroup, repositories_group_id,
585 627 or repositories_group name
586 628 :param group_name: Instance of UserGroup, users_group_id,
587 629 or user group name
588 630 """
589 631 repo_group = self._get_repo_group(repo_group)
590 632 group_name = self._get_user_group(group_name)
591 633
592 634 obj = self.sa.query(UserGroupRepoGroupToPerm)\
593 635 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
594 636 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
595 637 .scalar()
596 638 if obj:
597 639 self.sa.delete(obj)
598 640 log.debug('Revoked perm to %s on %s', repo_group, group_name)
599 641 action_logger_generic(
600 642 'revoked permission from usergroup: {} on repogroup: {}'.format(
601 643 group_name, repo_group), namespace='security.repogroup')
602 644
603 645 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
604 646 super_user_actions=False):
605 647
606 648 from rhodecode.lib.utils import PartialRenderer
607 649 _render = PartialRenderer('data_table/_dt_elements.html')
608 650 c = _render.c
609 651 h = _render.h
610 652
611 653 def quick_menu(repo_group_name):
612 654 return _render('quick_repo_group_menu', repo_group_name)
613 655
614 656 def repo_group_lnk(repo_group_name):
615 657 return _render('repo_group_name', repo_group_name)
616 658
617 659 def desc(desc):
618 660 if c.visual.stylify_metatags:
619 661 return h.urlify_text(h.escaped_stylize(h.truncate(desc, 60)))
620 662 else:
621 663 return h.urlify_text(h.html_escape(h.truncate(desc, 60)))
622 664
623 665 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
624 666 return _render(
625 667 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
626 668
627 669 def repo_group_name(repo_group_name, children_groups):
628 670 return _render("repo_group_name", repo_group_name, children_groups)
629 671
630 672 def user_profile(username):
631 673 return _render('user_profile', username)
632 674
633 675 repo_group_data = []
634 676 for group in repo_group_list:
635 677
636 678 row = {
637 679 "menu": quick_menu(group.group_name),
638 680 "name": repo_group_lnk(group.group_name),
639 681 "name_raw": group.group_name,
640 682 "desc": desc(group.group_description),
641 683 "top_level_repos": 0,
642 684 "owner": user_profile(group.user.username)
643 685 }
644 686 if admin:
645 687 repo_count = group.repositories.count()
646 688 children_groups = map(
647 689 h.safe_unicode,
648 690 itertools.chain((g.name for g in group.parents),
649 691 (x.name for x in [group])))
650 692 row.update({
651 693 "action": repo_group_actions(
652 694 group.group_id, group.group_name, repo_count),
653 695 "top_level_repos": repo_count,
654 696 "name": repo_group_name(group.group_name, children_groups),
655 697
656 698 })
657 699 repo_group_data.append(row)
658 700
659 701 return repo_group_data
@@ -1,839 +1,845 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 AttributeDict)
38 AttributeDict, str2bool)
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 if 'create_repo_group' in form_data:
108 user_data['create_repo_group'] = str2bool(
109 form_data.get('create_repo_group'))
110
107 111 try:
108 if form_data.get('create_repo_group'):
109 user_data['create_repo_group'] = True
110 112 if form_data.get('password_change'):
111 113 user_data['force_password_change'] = True
112
113 114 return UserModel().create_or_update(**user_data)
114 115 except Exception:
115 116 log.error(traceback.format_exc())
116 117 raise
117 118
118 119 def update_user(self, user, skip_attrs=None, **kwargs):
119 120 from rhodecode.lib.auth import get_crypt_password
120 121
121 122 user = self._get_user(user)
122 123 if user.username == User.DEFAULT_USER:
123 124 raise DefaultUserException(
124 125 _("You can't Edit this user since it's"
125 126 " crucial for entire application"))
126 127
127 128 # first store only defaults
128 129 user_attrs = {
129 130 'updating_user_id': user.user_id,
130 131 'username': user.username,
131 132 'password': user.password,
132 133 'email': user.email,
133 134 'firstname': user.name,
134 135 'lastname': user.lastname,
135 136 'active': user.active,
136 137 'admin': user.admin,
137 138 'extern_name': user.extern_name,
138 139 'extern_type': user.extern_type,
139 140 'language': user.user_data.get('language')
140 141 }
141 142
142 143 # in case there's new_password, that comes from form, use it to
143 144 # store password
144 145 if kwargs.get('new_password'):
145 146 kwargs['password'] = kwargs['new_password']
146 147
147 148 # cleanups, my_account password change form
148 149 kwargs.pop('current_password', None)
149 150 kwargs.pop('new_password', None)
150 151
151 152 # cleanups, user edit password change form
152 153 kwargs.pop('password_confirmation', None)
153 154 kwargs.pop('password_change', None)
154 155
155 156 # create repo group on user creation
156 157 kwargs.pop('create_repo_group', None)
157 158
158 159 # legacy forms send name, which is the firstname
159 160 firstname = kwargs.pop('name', None)
160 161 if firstname:
161 162 kwargs['firstname'] = firstname
162 163
163 164 for k, v in kwargs.items():
164 165 # skip if we don't want to update this
165 166 if skip_attrs and k in skip_attrs:
166 167 continue
167 168
168 169 user_attrs[k] = v
169 170
170 171 try:
171 172 return self.create_or_update(**user_attrs)
172 173 except Exception:
173 174 log.error(traceback.format_exc())
174 175 raise
175 176
176 177 def create_or_update(
177 178 self, username, password, email, firstname='', lastname='',
178 179 active=True, admin=False, extern_type=None, extern_name=None,
179 180 cur_user=None, plugin=None, force_password_change=False,
180 allow_to_create_user=True, create_repo_group=False,
181 allow_to_create_user=True, create_repo_group=None,
181 182 updating_user_id=None, language=None, strict_creation_check=True):
182 183 """
183 184 Creates a new instance if not found, or updates current one
184 185
185 186 :param username:
186 187 :param password:
187 188 :param email:
188 189 :param firstname:
189 190 :param lastname:
190 191 :param active:
191 192 :param admin:
192 193 :param extern_type:
193 194 :param extern_name:
194 195 :param cur_user:
195 196 :param plugin: optional plugin this method was called from
196 197 :param force_password_change: toggles new or existing user flag
197 198 for password change
198 199 :param allow_to_create_user: Defines if the method can actually create
199 200 new users
200 201 :param create_repo_group: Defines if the method should also
201 202 create an repo group with user name, and owner
202 203 :param updating_user_id: if we set it up this is the user we want to
203 204 update this allows to editing username.
204 205 :param language: language of user from interface.
205 206
206 207 :returns: new User object with injected `is_new_user` attribute.
207 208 """
208 209 if not cur_user:
209 210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
210 211
211 212 from rhodecode.lib.auth import (
212 213 get_crypt_password, check_password, generate_auth_token)
213 214 from rhodecode.lib.hooks_base import (
214 215 log_create_user, check_allowed_create_user)
215 216
216 217 def _password_change(new_user, password):
217 218 # empty password
218 219 if not new_user.password:
219 220 return False
220 221
221 222 # password check is only needed for RhodeCode internal auth calls
222 223 # in case it's a plugin we don't care
223 224 if not plugin:
224 225
225 # first check if we gave crypted password back, and if it matches
226 # it's not password change
226 # first check if we gave crypted password back, and if it
227 # matches it's not password change
227 228 if new_user.password == password:
228 229 return False
229 230
230 231 password_match = check_password(password, new_user.password)
231 232 if not password_match:
232 233 return True
233 234
234 235 return False
235 236
237 # read settings on default personal repo group creation
238 if create_repo_group is None:
239 default_create_repo_group = RepoGroupModel()\
240 .get_default_create_personal_repo_group()
241 create_repo_group = default_create_repo_group
242
236 243 user_data = {
237 244 'username': username,
238 245 'password': password,
239 246 'email': email,
240 247 'firstname': firstname,
241 248 'lastname': lastname,
242 249 'active': active,
243 250 'admin': admin
244 251 }
245 252
246 253 if updating_user_id:
247 254 log.debug('Checking for existing account in RhodeCode '
248 255 'database with user_id `%s` ' % (updating_user_id,))
249 256 user = User.get(updating_user_id)
250 257 else:
251 258 log.debug('Checking for existing account in RhodeCode '
252 259 'database with username `%s` ' % (username,))
253 260 user = User.get_by_username(username, case_insensitive=True)
254 261
255 262 if user is None:
256 263 # we check internal flag if this method is actually allowed to
257 264 # create new user
258 265 if not allow_to_create_user:
259 266 msg = ('Method wants to create new user, but it is not '
260 267 'allowed to do so')
261 268 log.warning(msg)
262 269 raise NotAllowedToCreateUserError(msg)
263 270
264 271 log.debug('Creating new user %s', username)
265 272
266 273 # only if we create user that is active
267 274 new_active_user = active
268 275 if new_active_user and strict_creation_check:
269 276 # raises UserCreationError if it's not allowed for any reason to
270 277 # create new active user, this also executes pre-create hooks
271 278 check_allowed_create_user(user_data, cur_user, strict_check=True)
272 279 events.trigger(events.UserPreCreate(user_data))
273 280 new_user = User()
274 281 edit = False
275 282 else:
276 283 log.debug('updating user %s', username)
277 284 events.trigger(events.UserPreUpdate(user, user_data))
278 285 new_user = user
279 286 edit = True
280 287
281 288 # we're not allowed to edit default user
282 289 if user.username == User.DEFAULT_USER:
283 290 raise DefaultUserException(
284 291 _("You can't edit this user (`%(username)s`) since it's "
285 292 "crucial for entire application") % {'username': user.username})
286 293
287 294 # inject special attribute that will tell us if User is new or old
288 295 new_user.is_new_user = not edit
289 296 # for users that didn's specify auth type, we use RhodeCode built in
290 297 from rhodecode.authentication.plugins import auth_rhodecode
291 298 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
292 299 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
293 300
294 301 try:
295 302 new_user.username = username
296 303 new_user.admin = admin
297 304 new_user.email = email
298 305 new_user.active = active
299 306 new_user.extern_name = safe_unicode(extern_name)
300 307 new_user.extern_type = safe_unicode(extern_type)
301 308 new_user.name = firstname
302 309 new_user.lastname = lastname
303 310
304 311 if not edit:
305 312 new_user.api_key = generate_auth_token(username)
306 313
307 314 # set password only if creating an user or password is changed
308 315 if not edit or _password_change(new_user, password):
309 316 reason = 'new password' if edit else 'new user'
310 317 log.debug('Updating password reason=>%s', reason)
311 318 new_user.password = get_crypt_password(password) if password else None
312 319
313 320 if force_password_change:
314 321 new_user.update_userdata(force_password_change=True)
315 322 if language:
316 323 new_user.update_userdata(language=language)
317 324 new_user.update_userdata(notification_status=True)
318 325
319 326 self.sa.add(new_user)
320 327
321 328 if not edit and create_repo_group:
322 # create new group same as username, and make this user an owner
323 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {'username': username}
324 RepoGroupModel().create(group_name=username,
325 group_description=desc,
326 owner=username, commit_early=False)
329 RepoGroupModel().create_personal_repo_group(
330 new_user, commit_early=False)
331
327 332 if not edit:
328 333 # add the RSS token
329 334 AuthTokenModel().create(username,
330 335 description='Generated feed token',
331 336 role=AuthTokenModel.cls.ROLE_FEED)
332 337 log_create_user(created_by=cur_user, **new_user.get_dict())
338 events.trigger(events.UserPostCreate(user_data))
333 339 return new_user
334 340 except (DatabaseError,):
335 341 log.error(traceback.format_exc())
336 342 raise
337 343
338 344 def create_registration(self, form_data):
339 345 from rhodecode.model.notification import NotificationModel
340 346 from rhodecode.model.notification import EmailNotificationModel
341 347
342 348 try:
343 349 form_data['admin'] = False
344 350 form_data['extern_name'] = 'rhodecode'
345 351 form_data['extern_type'] = 'rhodecode'
346 352 new_user = self.create(form_data)
347 353
348 354 self.sa.add(new_user)
349 355 self.sa.flush()
350 356
351 357 user_data = new_user.get_dict()
352 358 kwargs = {
353 359 # use SQLALCHEMY safe dump of user data
354 360 'user': AttributeDict(user_data),
355 361 'date': datetime.datetime.now()
356 362 }
357 363 notification_type = EmailNotificationModel.TYPE_REGISTRATION
358 364 # pre-generate the subject for notification itself
359 365 (subject,
360 366 _h, _e, # we don't care about those
361 367 body_plaintext) = EmailNotificationModel().render_email(
362 368 notification_type, **kwargs)
363 369
364 370 # create notification objects, and emails
365 371 NotificationModel().create(
366 372 created_by=new_user,
367 373 notification_subject=subject,
368 374 notification_body=body_plaintext,
369 375 notification_type=notification_type,
370 376 recipients=None, # all admins
371 377 email_kwargs=kwargs,
372 378 )
373 379
374 380 return new_user
375 381 except Exception:
376 382 log.error(traceback.format_exc())
377 383 raise
378 384
379 385 def _handle_user_repos(self, username, repositories, handle_mode=None):
380 386 _superadmin = self.cls.get_first_super_admin()
381 387 left_overs = True
382 388
383 389 from rhodecode.model.repo import RepoModel
384 390
385 391 if handle_mode == 'detach':
386 392 for obj in repositories:
387 393 obj.user = _superadmin
388 394 # set description we know why we super admin now owns
389 395 # additional repositories that were orphaned !
390 396 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
391 397 self.sa.add(obj)
392 398 left_overs = False
393 399 elif handle_mode == 'delete':
394 400 for obj in repositories:
395 401 RepoModel().delete(obj, forks='detach')
396 402 left_overs = False
397 403
398 404 # if nothing is done we have left overs left
399 405 return left_overs
400 406
401 407 def _handle_user_repo_groups(self, username, repository_groups,
402 408 handle_mode=None):
403 409 _superadmin = self.cls.get_first_super_admin()
404 410 left_overs = True
405 411
406 412 from rhodecode.model.repo_group import RepoGroupModel
407 413
408 414 if handle_mode == 'detach':
409 415 for r in repository_groups:
410 416 r.user = _superadmin
411 417 # set description we know why we super admin now owns
412 418 # additional repositories that were orphaned !
413 419 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
414 420 self.sa.add(r)
415 421 left_overs = False
416 422 elif handle_mode == 'delete':
417 423 for r in repository_groups:
418 424 RepoGroupModel().delete(r)
419 425 left_overs = False
420 426
421 427 # if nothing is done we have left overs left
422 428 return left_overs
423 429
424 430 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
425 431 _superadmin = self.cls.get_first_super_admin()
426 432 left_overs = True
427 433
428 434 from rhodecode.model.user_group import UserGroupModel
429 435
430 436 if handle_mode == 'detach':
431 437 for r in user_groups:
432 438 for user_user_group_to_perm in r.user_user_group_to_perm:
433 439 if user_user_group_to_perm.user.username == username:
434 440 user_user_group_to_perm.user = _superadmin
435 441 r.user = _superadmin
436 442 # set description we know why we super admin now owns
437 443 # additional repositories that were orphaned !
438 444 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
439 445 self.sa.add(r)
440 446 left_overs = False
441 447 elif handle_mode == 'delete':
442 448 for r in user_groups:
443 449 UserGroupModel().delete(r)
444 450 left_overs = False
445 451
446 452 # if nothing is done we have left overs left
447 453 return left_overs
448 454
449 455 def delete(self, user, cur_user=None, handle_repos=None,
450 456 handle_repo_groups=None, handle_user_groups=None):
451 457 if not cur_user:
452 458 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
453 459 user = self._get_user(user)
454 460
455 461 try:
456 462 if user.username == User.DEFAULT_USER:
457 463 raise DefaultUserException(
458 464 _(u"You can't remove this user since it's"
459 465 u" crucial for entire application"))
460 466
461 467 left_overs = self._handle_user_repos(
462 468 user.username, user.repositories, handle_repos)
463 469 if left_overs and user.repositories:
464 470 repos = [x.repo_name for x in user.repositories]
465 471 raise UserOwnsReposException(
466 472 _(u'user "%s" still owns %s repositories and cannot be '
467 473 u'removed. Switch owners or remove those repositories:%s')
468 474 % (user.username, len(repos), ', '.join(repos)))
469 475
470 476 left_overs = self._handle_user_repo_groups(
471 477 user.username, user.repository_groups, handle_repo_groups)
472 478 if left_overs and user.repository_groups:
473 479 repo_groups = [x.group_name for x in user.repository_groups]
474 480 raise UserOwnsRepoGroupsException(
475 481 _(u'user "%s" still owns %s repository groups and cannot be '
476 482 u'removed. Switch owners or remove those repository groups:%s')
477 483 % (user.username, len(repo_groups), ', '.join(repo_groups)))
478 484
479 485 left_overs = self._handle_user_user_groups(
480 486 user.username, user.user_groups, handle_user_groups)
481 487 if left_overs and user.user_groups:
482 488 user_groups = [x.users_group_name for x in user.user_groups]
483 489 raise UserOwnsUserGroupsException(
484 490 _(u'user "%s" still owns %s user groups and cannot be '
485 491 u'removed. Switch owners or remove those user groups:%s')
486 492 % (user.username, len(user_groups), ', '.join(user_groups)))
487 493
488 494 # we might change the user data with detach/delete, make sure
489 495 # the object is marked as expired before actually deleting !
490 496 self.sa.expire(user)
491 497 self.sa.delete(user)
492 498 from rhodecode.lib.hooks_base import log_delete_user
493 499 log_delete_user(deleted_by=cur_user, **user.get_dict())
494 500 except Exception:
495 501 log.error(traceback.format_exc())
496 502 raise
497 503
498 504 def reset_password_link(self, data, pwd_reset_url):
499 505 from rhodecode.lib.celerylib import tasks, run_task
500 506 from rhodecode.model.notification import EmailNotificationModel
501 507 user_email = data['email']
502 508 try:
503 509 user = User.get_by_email(user_email)
504 510 if user:
505 511 log.debug('password reset user found %s', user)
506 512
507 513 email_kwargs = {
508 514 'password_reset_url': pwd_reset_url,
509 515 'user': user,
510 516 'email': user_email,
511 517 'date': datetime.datetime.now()
512 518 }
513 519
514 520 (subject, headers, email_body,
515 521 email_body_plaintext) = EmailNotificationModel().render_email(
516 522 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
517 523
518 524 recipients = [user_email]
519 525
520 526 action_logger_generic(
521 527 'sending password reset email to user: {}'.format(
522 528 user), namespace='security.password_reset')
523 529
524 530 run_task(tasks.send_email, recipients, subject,
525 531 email_body_plaintext, email_body)
526 532
527 533 else:
528 534 log.debug("password reset email %s not found", user_email)
529 535 except Exception:
530 536 log.error(traceback.format_exc())
531 537 return False
532 538
533 539 return True
534 540
535 541 def reset_password(self, data, pwd_reset_url):
536 542 from rhodecode.lib.celerylib import tasks, run_task
537 543 from rhodecode.model.notification import EmailNotificationModel
538 544 from rhodecode.lib import auth
539 545 user_email = data['email']
540 546 pre_db = True
541 547 try:
542 548 user = User.get_by_email(user_email)
543 549 new_passwd = auth.PasswordGenerator().gen_password(
544 550 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
545 551 if user:
546 552 user.password = auth.get_crypt_password(new_passwd)
547 553 # also force this user to reset his password !
548 554 user.update_userdata(force_password_change=True)
549 555
550 556 Session().add(user)
551 557 Session().commit()
552 558 log.info('change password for %s', user_email)
553 559 if new_passwd is None:
554 560 raise Exception('unable to generate new password')
555 561
556 562 pre_db = False
557 563
558 564 email_kwargs = {
559 565 'new_password': new_passwd,
560 566 'password_reset_url': pwd_reset_url,
561 567 'user': user,
562 568 'email': user_email,
563 569 'date': datetime.datetime.now()
564 570 }
565 571
566 572 (subject, headers, email_body,
567 573 email_body_plaintext) = EmailNotificationModel().render_email(
568 574 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, **email_kwargs)
569 575
570 576 recipients = [user_email]
571 577
572 578 action_logger_generic(
573 579 'sent new password to user: {} with email: {}'.format(
574 580 user, user_email), namespace='security.password_reset')
575 581
576 582 run_task(tasks.send_email, recipients, subject,
577 583 email_body_plaintext, email_body)
578 584
579 585 except Exception:
580 586 log.error('Failed to update user password')
581 587 log.error(traceback.format_exc())
582 588 if pre_db:
583 589 # we rollback only if local db stuff fails. If it goes into
584 590 # run_task, we're pass rollback state this wouldn't work then
585 591 Session().rollback()
586 592
587 593 return True
588 594
589 595 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
590 596 """
591 597 Fetches auth_user by user_id,or api_key if present.
592 598 Fills auth_user attributes with those taken from database.
593 599 Additionally set's is_authenitated if lookup fails
594 600 present in database
595 601
596 602 :param auth_user: instance of user to set attributes
597 603 :param user_id: user id to fetch by
598 604 :param api_key: api key to fetch by
599 605 :param username: username to fetch by
600 606 """
601 607 if user_id is None and api_key is None and username is None:
602 608 raise Exception('You need to pass user_id, api_key or username')
603 609
604 610 log.debug(
605 611 'doing fill data based on: user_id:%s api_key:%s username:%s',
606 612 user_id, api_key, username)
607 613 try:
608 614 dbuser = None
609 615 if user_id:
610 616 dbuser = self.get(user_id)
611 617 elif api_key:
612 618 dbuser = self.get_by_auth_token(api_key)
613 619 elif username:
614 620 dbuser = self.get_by_username(username)
615 621
616 622 if not dbuser:
617 623 log.warning(
618 624 'Unable to lookup user by id:%s api_key:%s username:%s',
619 625 user_id, api_key, username)
620 626 return False
621 627 if not dbuser.active:
622 628 log.debug('User `%s` is inactive, skipping fill data', username)
623 629 return False
624 630
625 631 log.debug('filling user:%s data', dbuser)
626 632
627 633 # TODO: johbo: Think about this and find a clean solution
628 634 user_data = dbuser.get_dict()
629 635 user_data.update(dbuser.get_api_data(include_secrets=True))
630 636
631 637 for k, v in user_data.iteritems():
632 638 # properties of auth user we dont update
633 639 if k not in ['auth_tokens', 'permissions']:
634 640 setattr(auth_user, k, v)
635 641
636 642 # few extras
637 643 setattr(auth_user, 'feed_token', dbuser.feed_token)
638 644 except Exception:
639 645 log.error(traceback.format_exc())
640 646 auth_user.is_authenticated = False
641 647 return False
642 648
643 649 return True
644 650
645 651 def has_perm(self, user, perm):
646 652 perm = self._get_perm(perm)
647 653 user = self._get_user(user)
648 654
649 655 return UserToPerm.query().filter(UserToPerm.user == user)\
650 656 .filter(UserToPerm.permission == perm).scalar() is not None
651 657
652 658 def grant_perm(self, user, perm):
653 659 """
654 660 Grant user global permissions
655 661
656 662 :param user:
657 663 :param perm:
658 664 """
659 665 user = self._get_user(user)
660 666 perm = self._get_perm(perm)
661 667 # if this permission is already granted skip it
662 668 _perm = UserToPerm.query()\
663 669 .filter(UserToPerm.user == user)\
664 670 .filter(UserToPerm.permission == perm)\
665 671 .scalar()
666 672 if _perm:
667 673 return
668 674 new = UserToPerm()
669 675 new.user = user
670 676 new.permission = perm
671 677 self.sa.add(new)
672 678 return new
673 679
674 680 def revoke_perm(self, user, perm):
675 681 """
676 682 Revoke users global permissions
677 683
678 684 :param user:
679 685 :param perm:
680 686 """
681 687 user = self._get_user(user)
682 688 perm = self._get_perm(perm)
683 689
684 690 obj = UserToPerm.query()\
685 691 .filter(UserToPerm.user == user)\
686 692 .filter(UserToPerm.permission == perm)\
687 693 .scalar()
688 694 if obj:
689 695 self.sa.delete(obj)
690 696
691 697 def add_extra_email(self, user, email):
692 698 """
693 699 Adds email address to UserEmailMap
694 700
695 701 :param user:
696 702 :param email:
697 703 """
698 704 from rhodecode.model import forms
699 705 form = forms.UserExtraEmailForm()()
700 706 data = form.to_python({'email': email})
701 707 user = self._get_user(user)
702 708
703 709 obj = UserEmailMap()
704 710 obj.user = user
705 711 obj.email = data['email']
706 712 self.sa.add(obj)
707 713 return obj
708 714
709 715 def delete_extra_email(self, user, email_id):
710 716 """
711 717 Removes email address from UserEmailMap
712 718
713 719 :param user:
714 720 :param email_id:
715 721 """
716 722 user = self._get_user(user)
717 723 obj = UserEmailMap.query().get(email_id)
718 724 if obj:
719 725 self.sa.delete(obj)
720 726
721 727 def parse_ip_range(self, ip_range):
722 728 ip_list = []
723 729 def make_unique(value):
724 730 seen = []
725 731 return [c for c in value if not (c in seen or seen.append(c))]
726 732
727 733 # firsts split by commas
728 734 for ip_range in ip_range.split(','):
729 735 if not ip_range:
730 736 continue
731 737 ip_range = ip_range.strip()
732 738 if '-' in ip_range:
733 739 start_ip, end_ip = ip_range.split('-', 1)
734 740 start_ip = ipaddress.ip_address(start_ip.strip())
735 741 end_ip = ipaddress.ip_address(end_ip.strip())
736 742 parsed_ip_range = []
737 743
738 744 for index in xrange(int(start_ip), int(end_ip) + 1):
739 745 new_ip = ipaddress.ip_address(index)
740 746 parsed_ip_range.append(str(new_ip))
741 747 ip_list.extend(parsed_ip_range)
742 748 else:
743 749 ip_list.append(ip_range)
744 750
745 751 return make_unique(ip_list)
746 752
747 753 def add_extra_ip(self, user, ip, description=None):
748 754 """
749 755 Adds ip address to UserIpMap
750 756
751 757 :param user:
752 758 :param ip:
753 759 """
754 760 from rhodecode.model import forms
755 761 form = forms.UserExtraIpForm()()
756 762 data = form.to_python({'ip': ip})
757 763 user = self._get_user(user)
758 764
759 765 obj = UserIpMap()
760 766 obj.user = user
761 767 obj.ip_addr = data['ip']
762 768 obj.description = description
763 769 self.sa.add(obj)
764 770 return obj
765 771
766 772 def delete_extra_ip(self, user, ip_id):
767 773 """
768 774 Removes ip address from UserIpMap
769 775
770 776 :param user:
771 777 :param ip_id:
772 778 """
773 779 user = self._get_user(user)
774 780 obj = UserIpMap.query().get(ip_id)
775 781 if obj:
776 782 self.sa.delete(obj)
777 783
778 784 def get_accounts_in_creation_order(self, current_user=None):
779 785 """
780 786 Get accounts in order of creation for deactivation for license limits
781 787
782 788 pick currently logged in user, and append to the list in position 0
783 789 pick all super-admins in order of creation date and add it to the list
784 790 pick all other accounts in order of creation and add it to the list.
785 791
786 792 Based on that list, the last accounts can be disabled as they are
787 793 created at the end and don't include any of the super admins as well
788 794 as the current user.
789 795
790 796 :param current_user: optionally current user running this operation
791 797 """
792 798
793 799 if not current_user:
794 800 current_user = get_current_rhodecode_user()
795 801 active_super_admins = [
796 802 x.user_id for x in User.query()
797 803 .filter(User.user_id != current_user.user_id)
798 804 .filter(User.active == true())
799 805 .filter(User.admin == true())
800 806 .order_by(User.created_on.asc())]
801 807
802 808 active_regular_users = [
803 809 x.user_id for x in User.query()
804 810 .filter(User.user_id != current_user.user_id)
805 811 .filter(User.active == true())
806 812 .filter(User.admin == false())
807 813 .order_by(User.created_on.asc())]
808 814
809 815 list_of_accounts = [current_user.user_id]
810 816 list_of_accounts += active_super_admins
811 817 list_of_accounts += active_regular_users
812 818
813 819 return list_of_accounts
814 820
815 821 def deactivate_last_users(self, expected_users):
816 822 """
817 823 Deactivate accounts that are over the license limits.
818 824 Algorithm of which accounts to disabled is based on the formula:
819 825
820 826 Get current user, then super admins in creation order, then regular
821 827 active users in creation order.
822 828
823 829 Using that list we mark all accounts from the end of it as inactive.
824 830 This way we block only latest created accounts.
825 831
826 832 :param expected_users: list of users in special order, we deactivate
827 833 the end N ammoun of users from that list
828 834 """
829 835
830 836 list_of_accounts = self.get_accounts_in_creation_order()
831 837
832 838 for acc_id in list_of_accounts[expected_users + 1:]:
833 839 user = User.get(acc_id)
834 840 log.info('Deactivating account %s for license unlock', user)
835 841 user.active = False
836 842 Session().add(user)
837 843 Session().commit()
838 844
839 845 return
@@ -1,267 +1,296 b''
1 1 ${h.secure_form(url('admin_settings_global'), method='post')}
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading" id="branding-options">
5 5 <h3 class="panel-title">${_('Branding')} <a class="permalink" href="#branding-options"> ΒΆ</a></h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 <div class="label">
9 9 <label for="rhodecode_title">${_('Title')}</label>
10 10 </div>
11 11 <div class="field input">
12 12 ${h.text('rhodecode_title',size=60)}
13 13 </div>
14 14 <div class="field">
15 15 <span class="help-block">
16 16 ${_('Set a custom title for your RhodeCode instance (limited to 40 characters).')}
17 17 </span>
18 18 </div>
19 19 <div class="label">
20 20 <label for="rhodecode_realm">${_('HTTP[S] authentication realm')}</label>
21 21 </div>
22 22 <div class="field input">
23 23 ${h.text('rhodecode_realm',size=60)}
24 24 </div>
25 25 <div class="field">
26 26 <span class="help-block">
27 27 ${_('Set a custom text that is shown as authentication message to clients trying to connect.')}
28 28 </span>
29 29 </div>
30 30 </div>
31 31 </div>
32 32
33
34 <div class="panel panel-default">
35 <div class="panel-heading" id="personal-group-options">
36 <h3 class="panel-title">${_('Personal Repository Group')} <a class="permalink" href="#personal-group-options"> ΒΆ</a></h3>
37 </div>
38 <div class="panel-body">
39 <div class="checkbox">
40 ${h.checkbox('rhodecode_create_personal_repo_group','True')}
41 <label for="rhodecode_create_personal_repo_group">${_('Create Personal Repository Group')}</label>
42 </div>
43 <span class="help-block">
44 ${_('Always create Personal Repository Groups for new users.')} <br/>
45 ${_('When creating new users from add user form or API you can still turn this off via a checkbox or flag')}
46 </span>
47
48 <div class="label">
49 <label for="rhodecode_personal_repo_group_pattern">${_('Personal Repo Group Pattern')}</label>
50 </div>
51 <div class="field input">
52 ${h.text('rhodecode_personal_repo_group_pattern',size=60, placeholder=c.personal_repo_group_default_pattern)}
53 </div>
54 <span class="help-block">
55 ${_('Pattern used to create Personal Repository Groups. Prefix can be other existing repository group path[s], eg. /u/${username}')} <br/>
56 ${_('Available variables are currently ${username} and ${user_id}')}
57 </span>
58 </div>
59 </div>
60
61
33 62 <div class="panel panel-default">
34 63 <div class="panel-heading" id="captcha-options">
35 64 <h3 class="panel-title">${_('Registration Captcha')} <a class="permalink" href="#captcha-options"> ΒΆ</a></h3>
36 65 </div>
37 66 <div class="panel-body">
38 67 <div class="label">
39 68 <label for="rhodecode_captcha_public_key">${_('Google ReCaptcha public key')}</label>
40 69 </div>
41 70 <div class="field input">
42 71 ${h.text('rhodecode_captcha_public_key',size=60)}
43 72 </div>
44 73 <div class="field">
45 74 <span class="help-block">
46 75 ${_('Public key for reCaptcha system.')}
47 76 </span>
48 77 </div>
49 78
50 79 <div class="label">
51 80 <label for="rhodecode_captcha_private_key">${_('Google ReCaptcha private key')}</label>
52 81 </div>
53 82 <div class="field input">
54 83 ${h.text('rhodecode_captcha_private_key',size=60)}
55 84 </div>
56 85 <div class="field">
57 86 <span class="help-block">
58 87 ${_('Private key for reCaptcha system. Setting this value will enable captcha on registration')}
59 88 </span>
60 89 </div>
61 90 </div>
62 91 </div>
63 92
64 93 <div class="panel panel-default">
65 94 <div class="panel-heading" id="header-code-options">
66 95 <h3 class="panel-title">${_('Custom Header Code')} <a class="permalink" href="#header-code-options"> ΒΆ</a></h3>
67 96 </div>
68 97 <div class="panel-body">
69 98 <div class="select">
70 99 <select id="pre_template" >
71 100 <option value="#">${_('Templates...')}</option>
72 101 <option value="ga">Google Analytics</option>
73 102 <option value="clicky">Clicky</option>
74 103 <option value="server_announce">${_('Server Announcement')}</option>
75 104 <option value="flash_filtering">${_('Flash message filtering')}</option>
76 105 </select>
77 106 </div>
78 107 <div style="padding: 10px 0px"></div>
79 108 <div class="textarea text-area">
80 109 ${h.textarea('rhodecode_pre_code',cols=23,rows=5,class_="medium")}
81 110 <span class="help-block">${_('Custom js/css code added at the end of the <header/> tag.')}
82 111 ${_('Use <script/> or <css/> tags to define custom styling or scripting')}</span>
83 112 </div>
84 113 </div>
85 114 </div>
86 115
87 116 <div class="panel panel-default">
88 117 <div class="panel-heading" id="footer-code-options">
89 118 <h3 class="panel-title">${_('Custom Footer Code')} <a class="permalink" href="#footer-code-options"> ΒΆ</a></h3>
90 119 </div>
91 120 <div class="panel-body">
92 121 <div class="select">
93 122 <select id="post_template" >
94 123 <option value="#">${_('Templates...')}</option>
95 124 <option value="ga">Google Analytics</option>
96 125 <option value="clicky">Clicky</option>
97 126 <option value="server_announce">${_('Server Announcement')}</option>
98 127 </select>
99 128 </div>
100 129 <div style="padding: 10px 0px"></div>
101 130 <div class="textarea text-area">
102 131 ${h.textarea('rhodecode_post_code',cols=23,rows=5, class_="medium")}
103 132 <span class="help-block">${_('Custom js/css code added at the end of the <body> tag.')}
104 133 ${_('Use <script> or <css> tags to define custom styling or scripting')}</span>
105 134 </div>
106 135 </div>
107 136 </div>
108 137
109 138 <div class="buttons">
110 139 ${h.submit('save',_('Save settings'),class_="btn")}
111 140 ${h.reset('reset',_('Reset'),class_="btn")}
112 141 </div>
113 142 ${h.end_form()}
114 143
115 144
116 145
117 146 ## TEMPLATES ##
118 147 ###############
119 148
120 149 <script id="ga_tmpl" type="text/x-template">
121 150 <%text filter="h">
122 151 <script>
123 152 // Google Analytics
124 153 // Put your Google Analytics code instead of _GACODE_
125 154 var _gaq_code = '_GACODE_';
126 155 var _gaq = _gaq || [];
127 156 _gaq.push(['_setAccount', _gaq_code]);
128 157 _gaq.push(['_trackPageview']);
129 158
130 159 (function() {
131 160 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
132 161 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
133 162 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
134 163 })();
135 164
136 165 rhodecode_statechange_callback = function(url, data){
137 166 // ANALYTICS callback on html5 history state changed
138 167 // triggered by file browser, url is the new url,
139 168 // data is extra info passed from the State object
140 169 if (typeof window._gaq !== 'undefined') {
141 170 _gaq.push(['_trackPageview', url]);
142 171 }
143 172 };
144 173 </script>
145 174 </%text>
146 175 </script>
147 176
148 177
149 178
150 179 <script id="clicky_tmpl" type="text/x-template">
151 180 <%text filter="h">
152 181 <script src="//static.getclicky.com/js" type="text/javascript"></script>
153 182 <script type="text/javascript">
154 183 // Clicky Analytics - should be used in the footer code section.
155 184 // Put your Clicky code instead of _CLICKYCODE_ here,
156 185 // and below in the <img> tag.
157 186 var _cl_code = _CLICKYCODE_;
158 187 try{clicky.init(_cl_code);}catch(e){}
159 188
160 189 rhodecode_statechange_callback = function(url, data){
161 190 // ANALYTICS callback on html5 history state changed
162 191 // triggered by file browser, url is the new url,
163 192 // data is extra info passed from the State object
164 193 if (typeof window.clicky !== 'undefined') {
165 194 clicky.log(url);
166 195 }
167 196 }
168 197 </script>
169 198 <noscript>
170 199 // Put your clicky code in the src file.
171 200 <p><img alt="Clicky" width="1" height="1"
172 201 src="//in.getclicky.com/_CLICKYCODE_ns.gif" /></p>
173 202 </noscript>
174 203 </%text>
175 204 </script>
176 205
177 206
178 207
179 208 <script id="server_announce_tmpl" type='text/x-template'>
180 209 <%text filter="h">
181 210 <script>
182 211 // Server announcement displayed on the top of the page.
183 212 // This can be used to send a global maintainance messages or other
184 213 // important messages to all users of the RhodeCode Enterprise system.
185 214
186 215 $(document).ready(function(e){
187 216
188 217 // EDIT - put your message below
189 218 var message = "TYPE YOUR MESSAGE HERE";
190 219
191 220 // EDIT - choose "info"/"warning"/"error"/"success"/"neutral" as appropriate
192 221 var alert_level = "info";
193 222
194 223 $("#body").prepend(
195 224 ("<div id='server-announcement' class='"+alert_level+"'>_MSG_"+"</div>").replace("_MSG_", message)
196 225 )
197 226 })
198 227 </script>
199 228 </%text>
200 229 </script>
201 230
202 231 <script id="flash_filtering_tmpl" type='text/x-template'>
203 232 <%text filter="h">
204 233 <script>
205 234 // This filters out some flash messages before they are presented to user
206 235 // based on their contents. Could be used to filter out warnings/errors
207 236 // of license messages
208 237
209 238 var filteredMessages = [];
210 239 for(var i =0; i< alertMessagePayloads.length; i++){
211 240 if (typeof alertMessagePayloads[i].message.subdata.subtype !== 'undefined' &&
212 241 alertMessagePayloads[i].message.subdata.subtype.indexOf('rc_license') !== -1){
213 242 continue
214 243 }
215 244 filteredMessages.push(alertMessagePayloads[i]);
216 245 }
217 246 alertMessagePayloads = filteredMessages;
218 247 </script>
219 248 </%text>
220 249 </script>
221 250
222 251 <script>
223 252 var pre_cm = initCodeMirror('rhodecode_pre_code', '', false);
224 253 var pre_old = pre_cm.getValue();
225 254
226 255 var post_cm = initCodeMirror('rhodecode_post_code', '', false);
227 256 var post_old = post_cm.getValue();
228 257
229 258 var get_data = function(type, old){
230 259 var get_tmpl = function(tmpl_name){
231 260 // unescape some stuff
232 261 var html = htmlEnDeCode.htmlDecode($('#'+tmpl_name+'_tmpl').html());
233 262 return html;
234 263 };
235 264 return {
236 265 '#': old,
237 266 'ga': get_tmpl('ga'),
238 267 'clicky': get_tmpl('clicky'),
239 268 'server_announce': get_tmpl('server_announce'),
240 269 'flash_filtering': get_tmpl('flash_filtering')
241 270 }[type]
242 271 };
243 272
244 273 $('#pre_template').select2({
245 274 containerCssClass: 'drop-menu',
246 275 dropdownCssClass: 'drop-menu-dropdown',
247 276 dropdownAutoWidth: true,
248 277 minimumResultsForSearch: -1
249 278 });
250 279
251 280 $('#post_template').select2({
252 281 containerCssClass: 'drop-menu',
253 282 dropdownCssClass: 'drop-menu-dropdown',
254 283 dropdownAutoWidth: true,
255 284 minimumResultsForSearch: -1
256 285 });
257 286
258 287 $('#post_template').on('change', function(e){
259 288 var sel = this.value;
260 289 post_cm.setValue(get_data(sel, post_old))
261 290 });
262 291
263 292 $('#pre_template').on('change', function(e){
264 293 var sel = this.value;
265 294 pre_cm.setValue(get_data(sel, pre_old))
266 295 })
267 296 </script>
@@ -1,144 +1,147 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Add user')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10 <%def name="breadcrumbs_links()">
11 11 ${h.link_to(_('Admin'),h.url('admin_home'))}
12 12 &raquo;
13 13 ${h.link_to(_('Users'),h.url('users'))}
14 14 &raquo;
15 15 ${_('Add User')}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='admin')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <!-- box / title -->
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28 <!-- end box / title -->
29 29 ${h.secure_form(url('users'))}
30 30 <div class="form">
31 31 <!-- fields -->
32 32 <div class="fields">
33 33 <div class="field">
34 34 <div class="label">
35 35 <label for="username">${_('Username')}:</label>
36 36 </div>
37 37 <div class="input">
38 38 ${h.text('username', class_='medium')}
39 39 </div>
40 40 </div>
41 41
42 42 <div class="field">
43 43 <div class="label">
44 44 <label for="password">${_('Password')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 ${h.password('password', class_='medium')}
48 48 </div>
49 49 </div>
50 50
51 51 <div class="field">
52 52 <div class="label">
53 53 <label for="password_confirmation">${_('Password confirmation')}:</label>
54 54 </div>
55 55 <div class="input">
56 56 ${h.password('password_confirmation',autocomplete="off", class_='medium')}
57 57 <div class="info-block">
58 58 <a id="generate_password" href="#">
59 59 <i class="icon-lock"></i> ${_('Generate password')}
60 60 </a>
61 61 <span id="generate_password_preview"></span>
62 62 </div>
63 63 </div>
64 64 </div>
65 65
66 66 <div class="field">
67 67 <div class="label">
68 68 <label for="firstname">${_('First Name')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 ${h.text('firstname', class_='medium')}
72 72 </div>
73 73 </div>
74 74
75 75 <div class="field">
76 76 <div class="label">
77 77 <label for="lastname">${_('Last Name')}:</label>
78 78 </div>
79 79 <div class="input">
80 80 ${h.text('lastname', class_='medium')}
81 81 </div>
82 82 </div>
83 83
84 84 <div class="field">
85 85 <div class="label">
86 86 <label for="email">${_('Email')}:</label>
87 87 </div>
88 88 <div class="input">
89 89 ${h.text('email', class_='medium')}
90 90 ${h.hidden('extern_name', c.default_extern_type)}
91 91 ${h.hidden('extern_type', c.default_extern_type)}
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-checkbox">
97 97 <label for="active">${_('Active')}:</label>
98 98 </div>
99 99 <div class="checkboxes">
100 100 ${h.checkbox('active',value=True,checked='checked')}
101 101 </div>
102 102 </div>
103 103
104 104 <div class="field">
105 105 <div class="label label-checkbox">
106 106 <label for="password_change">${_('Password change')}:</label>
107 107 </div>
108 108 <div class="checkboxes">
109 109 ${h.checkbox('password_change',value=True)}
110 110 <span class="help-block">${_('Force user to change his password on the next login')}</span>
111 111 </div>
112 112 </div>
113 113
114 114 <div class="field">
115 115 <div class="label label-checkbox">
116 <label for="create_repo_group">${_('Add repository group')}:</label>
116 <label for="create_repo_group">${_('Add personal repository group')}:</label>
117 117 </div>
118 118 <div class="checkboxes">
119 ${h.checkbox('create_repo_group',value=True)}
120 <span class="help-block">${_('Add repository group with the same name as username. \nUser will be automatically set as this group owner.')}</span>
119 ${h.checkbox('create_repo_group',value=True, checked=c.default_create_repo_group)}
120 <span class="help-block">
121 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}<br/>
122 ${_('User will be automatically set as this group owner.')}
123 </span>
121 124 </div>
122 125 </div>
123 126
124 127 <div class="buttons">
125 128 ${h.submit('save',_('Save'),class_="btn")}
126 129 </div>
127 130 </div>
128 131 </div>
129 132 ${h.end_form()}
130 133 </div>
131 134 <script>
132 135 $(document).ready(function(){
133 136 $('#username').focus();
134 137
135 138 $('#generate_password').on('click', function(e){
136 139 var tmpl = "(${_('generated password:')} {0})"
137 140 var new_passwd = generatePassword(12)
138 141 $('#generate_password_preview').html(tmpl.format(new_passwd))
139 142 $('#password').val(new_passwd);
140 143 $('#password_confirmation').val(new_passwd);
141 144 })
142 145 })
143 146 </script>
144 147 </%def>
@@ -1,154 +1,158 b''
1 1 <%namespace name="base" file="/base/base.html"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Created on'), h.format_date(c.user.created_on), '', ''),
6 6 (_('Source of Record'), c.user.extern_type, '', ''),
7 7
8 8 (_('Last login'), c.user.last_login or '-', '', ''),
9 9 (_('Last activity'), h.format_date(h.time_to_datetime(c.user.user_data.get('last_activity', 0))), '', ''),
10 10
11 11 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
12 12 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
13 13 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
14 14
15 15 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
16 16 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
17 17 ]
18 18 %>
19 19
20 20 <div class="panel panel-default">
21 21 <div class="panel-heading">
22 22 <h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
23 23 </div>
24 24 <div class="panel-body">
25 25 ${base.dt_info_panel(elems)}
26 26 </div>
27 27 </div>
28 28
29 29 <div class="panel panel-default">
30 30 <div class="panel-heading">
31 31 <h3 class="panel-title">${_('Force Password Reset')}</h3>
32 32 </div>
33 33 <div class="panel-body">
34 34 ${h.secure_form(h.url('force_password_reset_user', user_id=c.user.user_id), method='post')}
35 35 <div class="field">
36 36 <button class="btn btn-default" type="submit">
37 37 <i class="icon-lock"></i>
38 38 %if c.user.user_data.get('force_password_change'):
39 39 ${_('Disable forced password reset')}
40 40 %else:
41 41 ${_('Enable forced password reset')}
42 42 %endif
43 43 </button>
44 44 </div>
45 45 <div class="field">
46 46 <span class="help-block">
47 47 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
48 48 </span>
49 49 </div>
50 50 ${h.end_form()}
51 51 </div>
52 52 </div>
53 53
54 54 <div class="panel panel-default">
55 55 <div class="panel-heading">
56 56 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
57 57 </div>
58 58 <div class="panel-body">
59 59 ${h.secure_form(h.url('create_personal_repo_group', user_id=c.user.user_id), method='post')}
60 60
61 61 %if c.personal_repo_group:
62 62 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, url('repo_group_home', group_name=c.personal_repo_group.group_name))}</div>
63 63 %else:
64 <div class="panel-body-title-text">${_('This user currently does not have a personal repository group')}</div>
64 <div class="panel-body-title-text">
65 ${_('This user currently does not have a personal repository group')}
66 <br/>
67 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
68 </div>
65 69 %endif
66 70 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
67 71 <i class="icon-folder-close"></i>
68 72 ${_('Create personal repository group')}
69 73 </button>
70 74 ${h.end_form()}
71 75 </div>
72 76 </div>
73 77
74 78
75 79 <div class="panel panel-danger">
76 80 <div class="panel-heading">
77 81 <h3 class="panel-title">${_('Delete User')}</h3>
78 82 </div>
79 83 <div class="panel-body">
80 84 ${h.secure_form(h.url('delete_user', user_id=c.user.user_id), method='delete')}
81 85
82 86 <table class="display">
83 87 <tr>
84 88 <td>
85 89 ${ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
86 90 </td>
87 91 <td>
88 92 %if len(c.user.repositories) > 0:
89 93 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked"/> <label for="user_repos_1">${_('Detach repositories')}</label>
90 94 %endif
91 95 </td>
92 96 <td>
93 97 %if len(c.user.repositories) > 0:
94 98 <input type="radio" id="user_repos_2" name="user_repos" value="delete" /> <label for="user_repos_2">${_('Delete repositories')}</label>
95 99 %endif
96 100 </td>
97 101 </tr>
98 102
99 103 <tr>
100 104 <td>
101 105 ${ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
102 106 </td>
103 107 <td>
104 108 %if len(c.user.repository_groups) > 0:
105 109 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked"/> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
106 110 %endif
107 111 </td>
108 112 <td>
109 113 %if len(c.user.repository_groups) > 0:
110 114 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" /> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
111 115 %endif
112 116 </td>
113 117 </tr>
114 118
115 119 <tr>
116 120 <td>
117 121 ${ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
118 122 </td>
119 123 <td>
120 124 %if len(c.user.user_groups) > 0:
121 125 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked"/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
122 126 %endif
123 127 </td>
124 128 <td>
125 129 %if len(c.user.user_groups) > 0:
126 130 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" /> <label for="user_user_groups_2">${_('Delete repositories')}</label>
127 131 %endif
128 132 </td>
129 133 </tr>
130 134 </table>
131 135 <div style="margin: 0 0 20px 0" class="fake-space"></div>
132 136
133 137 <div class="field">
134 138 <button class="btn btn-small btn-danger" type="submit"
135 139 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
136 140 ${"disabled" if not c.can_delete_user else ""}>
137 141 ${_('Delete this user')}
138 142 </button>
139 143 </div>
140 144 % if c.can_delete_user_message:
141 145 <p class="help-block">${c.can_delete_user_message}</p>
142 146 % endif
143 147
144 148 <div class="field">
145 149 <span class="help-block">
146 150 %if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
147 151 <p class="help-block">${_("When selecting the detach option, the depending objects owned by this user will be assigned to the `%s` super admin in the system. The delete option will delete the user's repositories!") % (c.first_admin.full_name)}</p>
148 152 %endif
149 153 </span>
150 154 </div>
151 155
152 156 ${h.end_form()}
153 157 </div>
154 158 </div>
@@ -1,653 +1,656 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.url('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26 <div class="main">
27 27 ${next.main()}
28 28 </div>
29 29 </div>
30 30 <!-- END CONTENT -->
31 31
32 32 </div>
33 33 <!-- FOOTER -->
34 34 <div id="footer">
35 35 <div id="footer-inner" class="title wrapper">
36 36 <div>
37 37 <p class="footer-link-right">
38 38 % if c.visual.show_version:
39 39 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
40 40 % endif
41 41 &copy; 2010-${h.datetime.today().year}, <a href="${h.url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
42 42 % if c.visual.rhodecode_support_url:
43 43 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
44 44 % endif
45 45 </p>
46 46 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
47 47 <p class="server-instance" style="display:${sid}">
48 48 ## display hidden instance ID if specially defined
49 49 % if c.rhodecode_instanceid:
50 50 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
51 51 % endif
52 52 </p>
53 53 </div>
54 54 </div>
55 55 </div>
56 56
57 57 <!-- END FOOTER -->
58 58
59 59 ### MAKO DEFS ###
60 60
61 61 <%def name="menu_bar_subnav()">
62 62 </%def>
63 63
64 64 <%def name="breadcrumbs(class_='breadcrumbs')">
65 65 <div class="${class_}">
66 66 ${self.breadcrumbs_links()}
67 67 </div>
68 68 </%def>
69 69
70 70 <%def name="admin_menu()">
71 71 <ul class="admin_menu submenu">
72 72 <li><a href="${h.url('admin_home')}">${_('Admin journal')}</a></li>
73 73 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
74 74 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
75 75 <li><a href="${h.url('users')}">${_('Users')}</a></li>
76 76 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
77 77 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
78 78 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
79 79 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
80 80 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
81 81 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
82 82 </ul>
83 83 </%def>
84 84
85 85
86 86 <%def name="dt_info_panel(elements)">
87 87 <dl class="dl-horizontal">
88 88 %for dt, dd, title, show_items in elements:
89 89 <dt>${dt}:</dt>
90 90 <dd title="${title}">
91 91 %if callable(dd):
92 92 ## allow lazy evaluation of elements
93 93 ${dd()}
94 94 %else:
95 95 ${dd}
96 96 %endif
97 97 %if show_items:
98 98 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
99 99 %endif
100 100 </dd>
101 101
102 102 %if show_items:
103 103 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
104 104 %for item in show_items:
105 105 <dt></dt>
106 106 <dd>${item}</dd>
107 107 %endfor
108 108 </div>
109 109 %endif
110 110
111 111 %endfor
112 112 </dl>
113 113 </%def>
114 114
115 115
116 116 <%def name="gravatar(email, size=16)">
117 117 <%
118 118 if (size > 16):
119 119 gravatar_class = 'gravatar gravatar-large'
120 120 else:
121 121 gravatar_class = 'gravatar'
122 122 %>
123 123 <%doc>
124 124 TODO: johbo: For now we serve double size images to make it smooth
125 125 for retina. This is how it worked until now. Should be replaced
126 126 with a better solution at some point.
127 127 </%doc>
128 128 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
129 129 </%def>
130 130
131 131
132 132 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
133 133 <% email = h.email_or_none(contact) %>
134 134 <div class="rc-user tooltip" title="${h.author_string(email)}">
135 135 ${self.gravatar(email, size)}
136 136 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
137 137 </div>
138 138 </%def>
139 139
140 140
141 141 ## admin menu used for people that have some admin resources
142 142 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
143 143 <ul class="submenu">
144 144 %if repositories:
145 145 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
146 146 %endif
147 147 %if repository_groups:
148 148 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
149 149 %endif
150 150 %if user_groups:
151 151 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
152 152 %endif
153 153 </ul>
154 154 </%def>
155 155
156 156 <%def name="repo_page_title(repo_instance)">
157 157 <div class="title-content">
158 158 <div class="title-main">
159 159 ## SVN/HG/GIT icons
160 160 %if h.is_hg(repo_instance):
161 161 <i class="icon-hg"></i>
162 162 %endif
163 163 %if h.is_git(repo_instance):
164 164 <i class="icon-git"></i>
165 165 %endif
166 166 %if h.is_svn(repo_instance):
167 167 <i class="icon-svn"></i>
168 168 %endif
169 169
170 170 ## public/private
171 171 %if repo_instance.private:
172 172 <i class="icon-repo-private"></i>
173 173 %else:
174 174 <i class="icon-repo-public"></i>
175 175 %endif
176 176
177 177 ## repo name with group name
178 178 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
179 179
180 180 </div>
181 181
182 182 ## FORKED
183 183 %if repo_instance.fork:
184 184 <p>
185 185 <i class="icon-code-fork"></i> ${_('Fork of')}
186 186 <a href="${h.url('summary_home',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
187 187 </p>
188 188 %endif
189 189
190 190 ## IMPORTED FROM REMOTE
191 191 %if repo_instance.clone_uri:
192 192 <p>
193 193 <i class="icon-code-fork"></i> ${_('Clone from')}
194 194 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
195 195 </p>
196 196 %endif
197 197
198 198 ## LOCKING STATUS
199 199 %if repo_instance.locked[0]:
200 200 <p class="locking_locked">
201 201 <i class="icon-repo-lock"></i>
202 202 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
203 203 </p>
204 204 %elif repo_instance.enable_locking:
205 205 <p class="locking_unlocked">
206 206 <i class="icon-repo-unlock"></i>
207 207 ${_('Repository not locked. Pull repository to lock it.')}
208 208 </p>
209 209 %endif
210 210
211 211 </div>
212 212 </%def>
213 213
214 214 <%def name="repo_menu(active=None)">
215 215 <%
216 216 def is_active(selected):
217 217 if selected == active:
218 218 return "active"
219 219 %>
220 220
221 221 <!--- CONTEXT BAR -->
222 222 <div id="context-bar">
223 223 <div class="wrapper">
224 224 <ul id="context-pages" class="horizontal-list navigation">
225 225 <li class="${is_active('summary')}"><a class="menulink" href="${h.url('summary_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
226 226 <li class="${is_active('changelog')}"><a class="menulink" href="${h.url('changelog_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
227 227 <li class="${is_active('files')}"><a class="menulink" href="${h.url('files_home', repo_name=c.repo_name, revision=c.rhodecode_db_repo.landing_rev[1])}"><div class="menulabel">${_('Files')}</div></a></li>
228 228 <li class="${is_active('compare')}">
229 229 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
230 230 </li>
231 231 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
232 232 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
233 233 <li class="${is_active('showpullrequest')}">
234 234 <a class="menulink" href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}">
235 235 %if c.repository_pull_requests:
236 236 <span class="pr_notifications">${c.repository_pull_requests}</span>
237 237 %endif
238 238 <div class="menulabel">${_('Pull Requests')}</div>
239 239 </a>
240 240 </li>
241 241 %endif
242 242 <li class="${is_active('options')}">
243 243 <a class="menulink" href="#" class="dropdown"><div class="menulabel">${_('Options')} <div class="show_more"></div></div></a>
244 244 <ul class="submenu">
245 245 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
246 246 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
247 247 %endif
248 248 %if c.rhodecode_db_repo.fork:
249 249 <li><a href="${h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1], target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1], merge=1)}">
250 250 ${_('Compare fork')}</a></li>
251 251 %endif
252 252
253 253 <li><a href="${h.url('search_repo_home',repo_name=c.repo_name)}">${_('Search')}</a></li>
254 254
255 255 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
256 256 %if c.rhodecode_db_repo.locked[0]:
257 257 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
258 258 %else:
259 259 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
260 260 %endif
261 261 %endif
262 262 %if c.rhodecode_user.username != h.DEFAULT_USER:
263 263 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
264 264 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
265 265 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
266 266 %endif
267 267 %endif
268 268 </ul>
269 269 </li>
270 270 </ul>
271 271 </div>
272 272 <div class="clear"></div>
273 273 </div>
274 274 <!--- END CONTEXT BAR -->
275 275
276 276 </%def>
277 277
278 278 <%def name="usermenu()">
279 279 ## USER MENU
280 280 <li id="quick_login_li">
281 281 <a id="quick_login_link" class="menulink childs">
282 282 ${gravatar(c.rhodecode_user.email, 20)}
283 283 <span class="user">
284 284 %if c.rhodecode_user.username != h.DEFAULT_USER:
285 285 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
286 286 %else:
287 287 <span>${_('Sign in')}</span>
288 288 %endif
289 289 </span>
290 290 </a>
291 291
292 292 <div class="user-menu submenu">
293 293 <div id="quick_login">
294 294 %if c.rhodecode_user.username == h.DEFAULT_USER:
295 295 <h4>${_('Sign in to your account')}</h4>
296 296 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
297 297 <div class="form form-vertical">
298 298 <div class="fields">
299 299 <div class="field">
300 300 <div class="label">
301 301 <label for="username">${_('Username')}:</label>
302 302 </div>
303 303 <div class="input">
304 304 ${h.text('username',class_='focus',tabindex=1)}
305 305 </div>
306 306
307 307 </div>
308 308 <div class="field">
309 309 <div class="label">
310 310 <label for="password">${_('Password')}:</label>
311 311 %if h.HasPermissionAny('hg.password_reset.enabled')():
312 312 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
313 313 %endif
314 314 </div>
315 315 <div class="input">
316 316 ${h.password('password',class_='focus',tabindex=2)}
317 317 </div>
318 318 </div>
319 319 <div class="buttons">
320 320 <div class="register">
321 321 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
322 322 ${h.link_to(_("Don't have an account ?"),h.route_path('register'))}
323 323 %endif
324 324 </div>
325 325 <div class="submit">
326 326 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
327 327 </div>
328 328 </div>
329 329 </div>
330 330 </div>
331 331 ${h.end_form()}
332 332 %else:
333 333 <div class="">
334 334 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
335 335 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
336 336 <div class="email">${c.rhodecode_user.email}</div>
337 337 </div>
338 338 <div class="">
339 339 <ol class="links">
340 340 <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li>
341 % if c.rhodecode_user.personal_repo_group:
342 <li>${h.link_to(_(u'My personal group'), h.url('repo_group_home', group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
343 % endif
341 344 <li class="logout">
342 345 ${h.secure_form(h.route_path('logout'))}
343 346 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
344 347 ${h.end_form()}
345 348 </li>
346 349 </ol>
347 350 </div>
348 351 %endif
349 352 </div>
350 353 </div>
351 354 %if c.rhodecode_user.username != h.DEFAULT_USER:
352 355 <div class="pill_container">
353 356 % if c.unread_notifications == 0:
354 357 <a class="menu_link_notifications empty" href="${h.url('notifications')}">${c.unread_notifications}</a>
355 358 % else:
356 359 <a class="menu_link_notifications" href="${h.url('notifications')}">${c.unread_notifications}</a>
357 360 % endif
358 361 </div>
359 362 % endif
360 363 </li>
361 364 </%def>
362 365
363 366 <%def name="menu_items(active=None)">
364 367 <%
365 368 def is_active(selected):
366 369 if selected == active:
367 370 return "active"
368 371 return ""
369 372 %>
370 373 <ul id="quick" class="main_nav navigation horizontal-list">
371 374 <!-- repo switcher -->
372 375 <li class="${is_active('repositories')} repo_switcher_li has_select2">
373 376 <input id="repo_switcher" name="repo_switcher" type="hidden">
374 377 </li>
375 378
376 379 ## ROOT MENU
377 380 %if c.rhodecode_user.username != h.DEFAULT_USER:
378 381 <li class="${is_active('journal')}">
379 382 <a class="menulink" title="${_('Show activity journal')}" href="${h.url('journal')}">
380 383 <div class="menulabel">${_('Journal')}</div>
381 384 </a>
382 385 </li>
383 386 %else:
384 387 <li class="${is_active('journal')}">
385 388 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.url('public_journal')}">
386 389 <div class="menulabel">${_('Public journal')}</div>
387 390 </a>
388 391 </li>
389 392 %endif
390 393 <li class="${is_active('gists')}">
391 394 <a class="menulink childs" title="${_('Show Gists')}" href="${h.url('gists')}">
392 395 <div class="menulabel">${_('Gists')}</div>
393 396 </a>
394 397 </li>
395 398 <li class="${is_active('search')}">
396 399 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.url('search')}">
397 400 <div class="menulabel">${_('Search')}</div>
398 401 </a>
399 402 </li>
400 403 % if h.HasPermissionAll('hg.admin')('access admin main page'):
401 404 <li class="${is_active('admin')}">
402 405 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
403 406 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
404 407 </a>
405 408 ${admin_menu()}
406 409 </li>
407 410 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
408 411 <li class="${is_active('admin')}">
409 412 <a class="menulink childs" title="${_('Delegated Admin settings')}">
410 413 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
411 414 </a>
412 415 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
413 416 c.rhodecode_user.repository_groups_admin,
414 417 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
415 418 </li>
416 419 % endif
417 420 % if c.debug_style:
418 421 <li class="${is_active('debug_style')}">
419 422 <a class="menulink" title="${_('Style')}" href="${h.url('debug_style_home')}">
420 423 <div class="menulabel">${_('Style')}</div>
421 424 </a>
422 425 </li>
423 426 % endif
424 427 ## render extra user menu
425 428 ${usermenu()}
426 429 </ul>
427 430
428 431 <script type="text/javascript">
429 432 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
430 433
431 434 /*format the look of items in the list*/
432 435 var format = function(state, escapeMarkup){
433 436 if (!state.id){
434 437 return state.text; // optgroup
435 438 }
436 439 var obj_dict = state.obj;
437 440 var tmpl = '';
438 441
439 442 if(obj_dict && state.type == 'repo'){
440 443 if(obj_dict['repo_type'] === 'hg'){
441 444 tmpl += '<i class="icon-hg"></i> ';
442 445 }
443 446 else if(obj_dict['repo_type'] === 'git'){
444 447 tmpl += '<i class="icon-git"></i> ';
445 448 }
446 449 else if(obj_dict['repo_type'] === 'svn'){
447 450 tmpl += '<i class="icon-svn"></i> ';
448 451 }
449 452 if(obj_dict['private']){
450 453 tmpl += '<i class="icon-lock" ></i> ';
451 454 }
452 455 else if(visual_show_public_icon){
453 456 tmpl += '<i class="icon-unlock-alt"></i> ';
454 457 }
455 458 }
456 459 if(obj_dict && state.type == 'commit') {
457 460 tmpl += '<i class="icon-tag"></i>';
458 461 }
459 462 if(obj_dict && state.type == 'group'){
460 463 tmpl += '<i class="icon-folder-close"></i> ';
461 464 }
462 465 tmpl += escapeMarkup(state.text);
463 466 return tmpl;
464 467 };
465 468
466 469 var formatResult = function(result, container, query, escapeMarkup) {
467 470 return format(result, escapeMarkup);
468 471 };
469 472
470 473 var formatSelection = function(data, container, escapeMarkup) {
471 474 return format(data, escapeMarkup);
472 475 };
473 476
474 477 $("#repo_switcher").select2({
475 478 cachedDataSource: {},
476 479 minimumInputLength: 2,
477 480 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
478 481 dropdownAutoWidth: true,
479 482 formatResult: formatResult,
480 483 formatSelection: formatSelection,
481 484 containerCssClass: "repo-switcher",
482 485 dropdownCssClass: "repo-switcher-dropdown",
483 486 escapeMarkup: function(m){
484 487 // don't escape our custom placeholder
485 488 if(m.substr(0,23) == '<div class="menulabel">'){
486 489 return m;
487 490 }
488 491
489 492 return Select2.util.escapeMarkup(m);
490 493 },
491 494 query: $.debounce(250, function(query){
492 495 self = this;
493 496 var cacheKey = query.term;
494 497 var cachedData = self.cachedDataSource[cacheKey];
495 498
496 499 if (cachedData) {
497 500 query.callback({results: cachedData.results});
498 501 } else {
499 502 $.ajax({
500 503 url: "${h.url('goto_switcher_data')}",
501 504 data: {'query': query.term},
502 505 dataType: 'json',
503 506 type: 'GET',
504 507 success: function(data) {
505 508 self.cachedDataSource[cacheKey] = data;
506 509 query.callback({results: data.results});
507 510 },
508 511 error: function(data, textStatus, errorThrown) {
509 512 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
510 513 }
511 514 })
512 515 }
513 516 })
514 517 });
515 518
516 519 $("#repo_switcher").on('select2-selecting', function(e){
517 520 e.preventDefault();
518 521 window.location = e.choice.url;
519 522 });
520 523
521 524 ## Global mouse bindings ##
522 525
523 526 // general help "?"
524 527 Mousetrap.bind(['?'], function(e) {
525 528 $('#help_kb').modal({})
526 529 });
527 530
528 531 // / open the quick filter
529 532 Mousetrap.bind(['/'], function(e) {
530 533 $("#repo_switcher").select2("open");
531 534
532 535 // return false to prevent default browser behavior
533 536 // and stop event from bubbling
534 537 return false;
535 538 });
536 539
537 540 // general nav g + action
538 541 Mousetrap.bind(['g h'], function(e) {
539 542 window.location = pyroutes.url('home');
540 543 });
541 544 Mousetrap.bind(['g g'], function(e) {
542 545 window.location = pyroutes.url('gists', {'private':1});
543 546 });
544 547 Mousetrap.bind(['g G'], function(e) {
545 548 window.location = pyroutes.url('gists', {'public':1});
546 549 });
547 550 Mousetrap.bind(['n g'], function(e) {
548 551 window.location = pyroutes.url('new_gist');
549 552 });
550 553 Mousetrap.bind(['n r'], function(e) {
551 554 window.location = pyroutes.url('new_repo');
552 555 });
553 556
554 557 % if hasattr(c, 'repo_name') and hasattr(c, 'rhodecode_db_repo'):
555 558 // nav in repo context
556 559 Mousetrap.bind(['g s'], function(e) {
557 560 window.location = pyroutes.url('summary_home', {'repo_name': REPO_NAME});
558 561 });
559 562 Mousetrap.bind(['g c'], function(e) {
560 563 window.location = pyroutes.url('changelog_home', {'repo_name': REPO_NAME});
561 564 });
562 565 Mousetrap.bind(['g F'], function(e) {
563 566 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.rhodecode_db_repo.landing_rev[1]}', 'f_path': '', 'search': '1'});
564 567 });
565 568 Mousetrap.bind(['g f'], function(e) {
566 569 window.location = pyroutes.url('files_home', {'repo_name': REPO_NAME, 'revision': '${c.rhodecode_db_repo.landing_rev[1]}', 'f_path': ''});
567 570 });
568 571 Mousetrap.bind(['g p'], function(e) {
569 572 window.location = pyroutes.url('pullrequest_show_all', {'repo_name': REPO_NAME});
570 573 });
571 574 Mousetrap.bind(['g o'], function(e) {
572 575 window.location = pyroutes.url('edit_repo', {'repo_name': REPO_NAME});
573 576 });
574 577 Mousetrap.bind(['g O'], function(e) {
575 578 window.location = pyroutes.url('edit_repo_perms', {'repo_name': REPO_NAME});
576 579 });
577 580 % endif
578 581
579 582 </script>
580 583 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
581 584 </%def>
582 585
583 586 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
584 587 <div class="modal-dialog">
585 588 <div class="modal-content">
586 589 <div class="modal-header">
587 590 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
588 591 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
589 592 </div>
590 593 <div class="modal-body">
591 594 <div class="block-left">
592 595 <table class="keyboard-mappings">
593 596 <tbody>
594 597 <tr>
595 598 <th></th>
596 599 <th>${_('Site-wide shortcuts')}</th>
597 600 </tr>
598 601 <%
599 602 elems = [
600 603 ('/', 'Open quick search box'),
601 604 ('g h', 'Goto home page'),
602 605 ('g g', 'Goto my private gists page'),
603 606 ('g G', 'Goto my public gists page'),
604 607 ('n r', 'New repository page'),
605 608 ('n g', 'New gist page'),
606 609 ]
607 610 %>
608 611 %for key, desc in elems:
609 612 <tr>
610 613 <td class="keys">
611 614 <span class="key tag">${key}</span>
612 615 </td>
613 616 <td>${desc}</td>
614 617 </tr>
615 618 %endfor
616 619 </tbody>
617 620 </table>
618 621 </div>
619 622 <div class="block-left">
620 623 <table class="keyboard-mappings">
621 624 <tbody>
622 625 <tr>
623 626 <th></th>
624 627 <th>${_('Repositories')}</th>
625 628 </tr>
626 629 <%
627 630 elems = [
628 631 ('g s', 'Goto summary page'),
629 632 ('g c', 'Goto changelog page'),
630 633 ('g f', 'Goto files page'),
631 634 ('g F', 'Goto files page with file search activated'),
632 635 ('g p', 'Goto pull requests page'),
633 636 ('g o', 'Goto repository settings'),
634 637 ('g O', 'Goto repository permissions settings'),
635 638 ]
636 639 %>
637 640 %for key, desc in elems:
638 641 <tr>
639 642 <td class="keys">
640 643 <span class="key tag">${key}</span>
641 644 </td>
642 645 <td>${desc}</td>
643 646 </tr>
644 647 %endfor
645 648 </tbody>
646 649 </table>
647 650 </div>
648 651 </div>
649 652 <div class="modal-footer">
650 653 </div>
651 654 </div><!-- /.modal-content -->
652 655 </div><!-- /.modal-dialog -->
653 656 </div><!-- /.modal -->
General Comments 0
You need to be logged in to leave comments. Login now