##// END OF EJS Templates
constants: use correct unicode encoding - avoid extra conversions
Mads Kiilerich -
r5610:330c671d default
parent child Browse files
Show More
@@ -1,570 +1,570 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
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 General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.db_manage
16 16 ~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Database creation, and setup module for Kallithea. Used for creation
19 19 of database as well as for migration operations
20 20
21 21 This file was forked by the Kallithea project in July 2014.
22 22 Original author and date, and relevant copyright and licensing information is below:
23 23 :created_on: Apr 10, 2010
24 24 :author: marcink
25 25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 26 :license: GPLv3, see LICENSE.md for more details.
27 27 """
28 28
29 29 import os
30 30 import sys
31 31 import time
32 32 import uuid
33 33 import logging
34 34 from os.path import dirname as dn, join as jn
35 35
36 36 from kallithea import __dbversion__, __py_version__, EXTERN_TYPE_INTERNAL, DB_MIGRATIONS
37 37 from kallithea.model.user import UserModel
38 38 from kallithea.lib.utils import ask_ok
39 39 from kallithea.model import init_model
40 40 from kallithea.model.db import User, Permission, Ui, \
41 41 Setting, UserToPerm, DbMigrateVersion, RepoGroup, \
42 42 UserRepoGroupToPerm, CacheInvalidation, Repository
43 43
44 44 from sqlalchemy.engine import create_engine
45 45 from kallithea.model.repo_group import RepoGroupModel
46 46 #from kallithea.model import meta
47 47 from kallithea.model.meta import Session, Base
48 48 from kallithea.model.repo import RepoModel
49 49 from kallithea.model.permission import PermissionModel
50 50
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 def notify(msg):
56 56 """
57 57 Notification for migrations messages
58 58 """
59 59 ml = len(msg) + (4 * 2)
60 60 print('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper()
61 61
62 62
63 63 class DbManage(object):
64 64 def __init__(self, log_sql, dbconf, root, tests=False, SESSION=None, cli_args=None):
65 65 self.dbname = dbconf.split('/')[-1]
66 66 self.tests = tests
67 67 self.root = root
68 68 self.dburi = dbconf
69 69 self.log_sql = log_sql
70 70 self.db_exists = False
71 71 self.cli_args = cli_args or {}
72 72 self.init_db(SESSION=SESSION)
73 73
74 74 force_ask = self.cli_args.get('force_ask')
75 75 if force_ask is not None:
76 76 global ask_ok
77 77 ask_ok = lambda *args, **kwargs: force_ask
78 78
79 79 def init_db(self, SESSION=None):
80 80 if SESSION:
81 81 self.sa = SESSION
82 82 else:
83 83 #init new sessions
84 84 engine = create_engine(self.dburi, echo=self.log_sql)
85 85 init_model(engine)
86 86 self.sa = Session()
87 87
88 88 def create_tables(self, override=False):
89 89 """
90 90 Create a auth database
91 91 """
92 92
93 93 log.info("Any existing database is going to be destroyed")
94 94 if self.tests:
95 95 destroy = True
96 96 else:
97 97 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
98 98 if not destroy:
99 99 print 'Nothing done.'
100 100 sys.exit(0)
101 101 if destroy:
102 102 Base.metadata.drop_all()
103 103
104 104 checkfirst = not override
105 105 Base.metadata.create_all(checkfirst=checkfirst)
106 106 log.info('Created tables for %s', self.dbname)
107 107
108 108 def set_db_version(self):
109 109 ver = DbMigrateVersion()
110 110 ver.version = __dbversion__
111 111 ver.repository_id = DB_MIGRATIONS
112 112 ver.repository_path = 'versions'
113 113 self.sa.add(ver)
114 114 log.info('db version set to: %s', __dbversion__)
115 115
116 116 def upgrade(self):
117 117 """
118 118 Upgrades given database schema to given revision following
119 119 all needed steps, to perform the upgrade
120 120
121 121 """
122 122
123 123 from kallithea.lib.dbmigrate.migrate.versioning import api
124 124 from kallithea.lib.dbmigrate.migrate.exceptions import \
125 125 DatabaseNotControlledError
126 126
127 127 if 'sqlite' in self.dburi:
128 128 print (
129 129 '********************** WARNING **********************\n'
130 130 'Make sure your version of sqlite is at least 3.7.X. \n'
131 131 'Earlier versions are known to fail on some migrations\n'
132 132 '*****************************************************\n')
133 133
134 134 upgrade = ask_ok('You are about to perform database upgrade, make '
135 135 'sure You backed up your database before. '
136 136 'Continue ? [y/n]')
137 137 if not upgrade:
138 138 print 'No upgrade performed'
139 139 sys.exit(0)
140 140
141 141 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
142 142 'kallithea/lib/dbmigrate')
143 143 db_uri = self.dburi
144 144
145 145 try:
146 146 curr_version = api.db_version(db_uri, repository_path)
147 147 msg = ('Found current database under version '
148 148 'control with version %s' % curr_version)
149 149
150 150 except (RuntimeError, DatabaseNotControlledError):
151 151 curr_version = 1
152 152 msg = ('Current database is not under version control. Setting '
153 153 'as version %s' % curr_version)
154 154 api.version_control(db_uri, repository_path, curr_version)
155 155
156 156 notify(msg)
157 157 if curr_version == __dbversion__:
158 158 print 'This database is already at the newest version'
159 159 sys.exit(0)
160 160
161 161 # clear cache keys
162 162 log.info("Clearing cache keys now...")
163 163 CacheInvalidation.clear_cache()
164 164
165 165 upgrade_steps = range(curr_version + 1, __dbversion__ + 1)
166 166 notify('attempting to do database upgrade from '
167 167 'version %s to version %s' % (curr_version, __dbversion__))
168 168
169 169 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
170 170 _step = None
171 171 for step in upgrade_steps:
172 172 notify('performing upgrade step %s' % step)
173 173 time.sleep(0.5)
174 174
175 175 api.upgrade(db_uri, repository_path, step)
176 176 notify('schema upgrade for step %s completed' % (step,))
177 177
178 178 _step = step
179 179
180 180 notify('upgrade to version %s successful' % _step)
181 181
182 182 def fix_repo_paths(self):
183 183 """
184 184 Fixes a old kallithea version path into new one without a '*'
185 185 """
186 186
187 187 paths = self.sa.query(Ui) \
188 188 .filter(Ui.ui_key == '/') \
189 189 .scalar()
190 190
191 191 paths.ui_value = paths.ui_value.replace('*', '')
192 192
193 193 self.sa.add(paths)
194 194 self.sa.commit()
195 195
196 196 def fix_default_user(self):
197 197 """
198 198 Fixes a old default user with some 'nicer' default values,
199 199 used mostly for anonymous access
200 200 """
201 201 def_user = self.sa.query(User) \
202 202 .filter(User.username == User.DEFAULT_USER) \
203 203 .one()
204 204
205 205 def_user.name = 'Anonymous'
206 206 def_user.lastname = 'User'
207 207 def_user.email = 'anonymous@kallithea-scm.org'
208 208
209 209 self.sa.add(def_user)
210 210 self.sa.commit()
211 211
212 212 def fix_settings(self):
213 213 """
214 214 Fixes kallithea settings adds ga_code key for google analytics
215 215 """
216 216
217 217 hgsettings3 = Setting('ga_code', '')
218 218
219 219 self.sa.add(hgsettings3)
220 220 self.sa.commit()
221 221
222 222 def admin_prompt(self, second=False):
223 223 if not self.tests:
224 224 import getpass
225 225
226 226 # defaults
227 227 defaults = self.cli_args
228 228 username = defaults.get('username')
229 229 password = defaults.get('password')
230 230 email = defaults.get('email')
231 231
232 232 def get_password():
233 233 password = getpass.getpass('Specify admin password '
234 234 '(min 6 chars):')
235 235 confirm = getpass.getpass('Confirm password:')
236 236
237 237 if password != confirm:
238 238 log.error('passwords mismatch')
239 239 return False
240 240 if len(password) < 6:
241 241 log.error('password is to short use at least 6 characters')
242 242 return False
243 243
244 244 return password
245 245 if username is None:
246 246 username = raw_input('Specify admin username:')
247 247 if password is None:
248 248 password = get_password()
249 249 if not password:
250 250 #second try
251 251 password = get_password()
252 252 if not password:
253 253 sys.exit()
254 254 if email is None:
255 255 email = raw_input('Specify admin email:')
256 256 self.create_user(username, password, email, True)
257 257 else:
258 258 log.info('creating admin and regular test users')
259 259 from kallithea.tests import TEST_USER_ADMIN_LOGIN, \
260 260 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
261 261 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
262 262 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
263 263 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
264 264
265 265 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
266 266 TEST_USER_ADMIN_EMAIL, True)
267 267
268 268 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
269 269 TEST_USER_REGULAR_EMAIL, False)
270 270
271 271 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
272 272 TEST_USER_REGULAR2_EMAIL, False)
273 273
274 274 def create_ui_settings(self, repo_store_path):
275 275 """
276 276 Creates ui settings, fills out hooks
277 277 """
278 278
279 279 #HOOKS
280 280 hooks1_key = Ui.HOOK_UPDATE
281 281 hooks1_ = self.sa.query(Ui) \
282 282 .filter(Ui.ui_key == hooks1_key).scalar()
283 283
284 284 hooks1 = Ui() if hooks1_ is None else hooks1_
285 285 hooks1.ui_section = 'hooks'
286 286 hooks1.ui_key = hooks1_key
287 287 hooks1.ui_value = 'hg update >&2'
288 288 hooks1.ui_active = False
289 289 self.sa.add(hooks1)
290 290
291 291 hooks2_key = Ui.HOOK_REPO_SIZE
292 292 hooks2_ = self.sa.query(Ui) \
293 293 .filter(Ui.ui_key == hooks2_key).scalar()
294 294 hooks2 = Ui() if hooks2_ is None else hooks2_
295 295 hooks2.ui_section = 'hooks'
296 296 hooks2.ui_key = hooks2_key
297 297 hooks2.ui_value = 'python:kallithea.lib.hooks.repo_size'
298 298 self.sa.add(hooks2)
299 299
300 300 hooks3 = Ui()
301 301 hooks3.ui_section = 'hooks'
302 302 hooks3.ui_key = Ui.HOOK_PUSH
303 303 hooks3.ui_value = 'python:kallithea.lib.hooks.log_push_action'
304 304 self.sa.add(hooks3)
305 305
306 306 hooks4 = Ui()
307 307 hooks4.ui_section = 'hooks'
308 308 hooks4.ui_key = Ui.HOOK_PRE_PUSH
309 309 hooks4.ui_value = 'python:kallithea.lib.hooks.pre_push'
310 310 self.sa.add(hooks4)
311 311
312 312 hooks5 = Ui()
313 313 hooks5.ui_section = 'hooks'
314 314 hooks5.ui_key = Ui.HOOK_PULL
315 315 hooks5.ui_value = 'python:kallithea.lib.hooks.log_pull_action'
316 316 self.sa.add(hooks5)
317 317
318 318 hooks6 = Ui()
319 319 hooks6.ui_section = 'hooks'
320 320 hooks6.ui_key = Ui.HOOK_PRE_PULL
321 321 hooks6.ui_value = 'python:kallithea.lib.hooks.pre_pull'
322 322 self.sa.add(hooks6)
323 323
324 324 # enable largefiles
325 325 largefiles = Ui()
326 326 largefiles.ui_section = 'extensions'
327 327 largefiles.ui_key = 'largefiles'
328 328 largefiles.ui_value = ''
329 329 self.sa.add(largefiles)
330 330
331 331 # set default largefiles cache dir, defaults to
332 332 # /repo location/.cache/largefiles
333 333 largefiles = Ui()
334 334 largefiles.ui_section = 'largefiles'
335 335 largefiles.ui_key = 'usercache'
336 336 largefiles.ui_value = os.path.join(repo_store_path, '.cache',
337 337 'largefiles')
338 338 self.sa.add(largefiles)
339 339
340 340 # enable hgsubversion disabled by default
341 341 hgsubversion = Ui()
342 342 hgsubversion.ui_section = 'extensions'
343 343 hgsubversion.ui_key = 'hgsubversion'
344 344 hgsubversion.ui_value = ''
345 345 hgsubversion.ui_active = False
346 346 self.sa.add(hgsubversion)
347 347
348 348 # enable hggit disabled by default
349 349 hggit = Ui()
350 350 hggit.ui_section = 'extensions'
351 351 hggit.ui_key = 'hggit'
352 352 hggit.ui_value = ''
353 353 hggit.ui_active = False
354 354 self.sa.add(hggit)
355 355
356 356 def create_auth_plugin_options(self, skip_existing=False):
357 357 """
358 358 Create default auth plugin settings, and make it active
359 359
360 360 :param skip_existing:
361 361 """
362 362
363 363 for k, v, t in [('auth_plugins', 'kallithea.lib.auth_modules.auth_internal', 'list'),
364 364 ('auth_internal_enabled', 'True', 'bool')]:
365 365 if skip_existing and Setting.get_by_name(k) != None:
366 366 log.debug('Skipping option %s', k)
367 367 continue
368 368 setting = Setting(k, v, t)
369 369 self.sa.add(setting)
370 370
371 371 def create_default_options(self, skip_existing=False):
372 372 """Creates default settings"""
373 373
374 374 for k, v, t in [
375 375 ('default_repo_enable_locking', False, 'bool'),
376 376 ('default_repo_enable_downloads', False, 'bool'),
377 377 ('default_repo_enable_statistics', False, 'bool'),
378 378 ('default_repo_private', False, 'bool'),
379 379 ('default_repo_type', 'hg', 'unicode')]:
380 380
381 381 if skip_existing and Setting.get_by_name(k) is not None:
382 382 log.debug('Skipping option %s', k)
383 383 continue
384 384 setting = Setting(k, v, t)
385 385 self.sa.add(setting)
386 386
387 387 def fixup_groups(self):
388 388 def_usr = User.get_default_user()
389 389 for g in RepoGroup.query().all():
390 390 g.group_name = g.get_new_name(g.name)
391 391 self.sa.add(g)
392 392 # get default perm
393 393 default = UserRepoGroupToPerm.query() \
394 394 .filter(UserRepoGroupToPerm.group == g) \
395 395 .filter(UserRepoGroupToPerm.user == def_usr) \
396 396 .scalar()
397 397
398 398 if default is None:
399 399 log.debug('missing default permission for group %s adding', g)
400 400 perm_obj = RepoGroupModel()._create_default_perms(g)
401 401 self.sa.add(perm_obj)
402 402
403 403 def reset_permissions(self, username):
404 404 """
405 405 Resets permissions to default state, useful when old systems had
406 406 bad permissions, we must clean them up
407 407
408 408 :param username:
409 409 """
410 410 default_user = User.get_by_username(username)
411 411 if not default_user:
412 412 return
413 413
414 414 u2p = UserToPerm.query() \
415 415 .filter(UserToPerm.user == default_user).all()
416 416 fixed = False
417 417 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
418 418 for p in u2p:
419 419 Session().delete(p)
420 420 fixed = True
421 421 self.populate_default_permissions()
422 422 return fixed
423 423
424 424 def update_repo_info(self):
425 425 RepoModel.update_repoinfo()
426 426
427 427 def config_prompt(self, test_repo_path='', retries=3):
428 428 defaults = self.cli_args
429 429 _path = defaults.get('repos_location')
430 430 if retries == 3:
431 431 log.info('Setting up repositories config')
432 432
433 433 if _path is not None:
434 434 path = _path
435 435 elif not self.tests and not test_repo_path:
436 436 path = raw_input(
437 437 'Enter a valid absolute path to store repositories. '
438 438 'All repositories in that path will be added automatically:'
439 439 )
440 440 else:
441 441 path = test_repo_path
442 442 path_ok = True
443 443
444 444 # check proper dir
445 445 if not os.path.isdir(path):
446 446 path_ok = False
447 447 log.error('Given path %s is not a valid directory', path)
448 448
449 449 elif not os.path.isabs(path):
450 450 path_ok = False
451 451 log.error('Given path %s is not an absolute path', path)
452 452
453 453 # check if path is at least readable.
454 454 if not os.access(path, os.R_OK):
455 455 path_ok = False
456 456 log.error('Given path %s is not readable', path)
457 457
458 458 # check write access, warn user about non writeable paths
459 459 elif not os.access(path, os.W_OK) and path_ok:
460 460 log.warning('No write permission to given path %s', path)
461 461 if not ask_ok('Given path %s is not writeable, do you want to '
462 462 'continue with read only mode ? [y/n]' % (path,)):
463 463 log.error('Canceled by user')
464 464 sys.exit(-1)
465 465
466 466 if retries == 0:
467 467 sys.exit('max retries reached')
468 468 if not path_ok:
469 469 retries -= 1
470 470 return self.config_prompt(test_repo_path, retries)
471 471
472 472 real_path = os.path.normpath(os.path.realpath(path))
473 473
474 474 if real_path != os.path.normpath(path):
475 475 log.warning('Using normalized path %s instead of %s', real_path, path)
476 476
477 477 return real_path
478 478
479 479 def create_settings(self, path):
480 480
481 481 self.create_ui_settings(path)
482 482
483 483 ui_config = [
484 484 ('web', 'push_ssl', 'false'),
485 485 ('web', 'allow_archive', 'gz zip bz2'),
486 486 ('web', 'allow_push', '*'),
487 487 ('web', 'baseurl', '/'),
488 488 ('paths', '/', path),
489 489 #('phases', 'publish', 'false')
490 490 ]
491 491 for section, key, value in ui_config:
492 492 ui_conf = Ui()
493 493 setattr(ui_conf, 'ui_section', section)
494 494 setattr(ui_conf, 'ui_key', key)
495 495 setattr(ui_conf, 'ui_value', value)
496 496 self.sa.add(ui_conf)
497 497
498 498 settings = [
499 499 ('realm', 'Kallithea', 'unicode'),
500 500 ('title', '', 'unicode'),
501 501 ('ga_code', '', 'unicode'),
502 502 ('show_public_icon', True, 'bool'),
503 503 ('show_private_icon', True, 'bool'),
504 504 ('stylify_metatags', False, 'bool'),
505 505 ('dashboard_items', 100, 'int'),
506 506 ('admin_grid_items', 25, 'int'),
507 507 ('show_version', True, 'bool'),
508 508 ('use_gravatar', True, 'bool'),
509 509 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
510 510 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
511 511 ('update_url', Setting.DEFAULT_UPDATE_URL, 'unicode'),
512 512 ]
513 513 for key, val, type_ in settings:
514 514 sett = Setting(key, val, type_)
515 515 self.sa.add(sett)
516 516
517 517 self.create_auth_plugin_options()
518 518 self.create_default_options()
519 519
520 520 log.info('created ui config')
521 521
522 522 def create_user(self, username, password, email='', admin=False):
523 523 log.info('creating user %s', username)
524 524 UserModel().create_or_update(username, password, email,
525 firstname='Kallithea', lastname='Admin',
525 firstname=u'Kallithea', lastname=u'Admin',
526 526 active=True, admin=admin,
527 527 extern_type=EXTERN_TYPE_INTERNAL)
528 528
529 529 def create_default_user(self):
530 530 log.info('creating default user')
531 531 # create default user for handling default permissions.
532 532 user = UserModel().create_or_update(username=User.DEFAULT_USER,
533 533 password=str(uuid.uuid1())[:20],
534 534 email='anonymous@kallithea-scm.org',
535 firstname='Anonymous',
536 lastname='User')
535 firstname=u'Anonymous',
536 lastname=u'User')
537 537 # based on configuration options activate/deactivate this user which
538 538 # controls anonymous access
539 539 if self.cli_args.get('public_access') is False:
540 540 log.info('Public access disabled')
541 541 user.active = False
542 542 Session().add(user)
543 543 Session().commit()
544 544
545 545 def create_permissions(self):
546 546 """
547 547 Creates all permissions defined in the system
548 548 """
549 549 # module.(access|create|change|delete)_[name]
550 550 # module.(none|read|write|admin)
551 551 log.info('creating permissions')
552 552 PermissionModel(self.sa).create_permissions()
553 553
554 554 def populate_default_permissions(self):
555 555 """
556 556 Populate default permissions. It will create only the default
557 557 permissions that are missing, and not alter already defined ones
558 558 """
559 559 log.info('creating default user permissions')
560 560 PermissionModel(self.sa).create_default_permissions(user=User.DEFAULT_USER)
561 561
562 562 @staticmethod
563 563 def check_waitress():
564 564 """
565 565 Function executed at the end of setup
566 566 """
567 567 if not __py_version__ >= (2, 6):
568 568 notify('Python2.5 detected, please switch '
569 569 'egg:waitress#main -> egg:Paste#http '
570 570 'in your .ini file')
@@ -1,885 +1,885 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
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 General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.utils
16 16 ~~~~~~~~~~~~~~~~~~~
17 17
18 18 Utilities library for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 18, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import re
30 30 import logging
31 31 import datetime
32 32 import traceback
33 33 import paste
34 34 import beaker
35 35 import tarfile
36 36 import shutil
37 37 import decorator
38 38 import warnings
39 39 from os.path import abspath
40 40 from os.path import dirname as dn, join as jn
41 41
42 42 from paste.script.command import Command, BadCommand
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45 from beaker.cache import _cache_decorate
46 46
47 47 from kallithea import BRAND
48 48
49 49 from kallithea.lib.vcs.utils.hgcompat import ui, config
50 50 from kallithea.lib.vcs.utils.helpers import get_scm
51 51 from kallithea.lib.vcs.exceptions import VCSError
52 52
53 53 from kallithea.model import meta
54 54 from kallithea.model.db import Repository, User, Ui, \
55 55 UserLog, RepoGroup, Setting, UserGroup
56 56 from kallithea.model.meta import Session
57 57 from kallithea.model.repo_group import RepoGroupModel
58 58 from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
59 59 from kallithea.lib.vcs.utils.fakemod import create_module
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
64 64
65 65
66 66 def recursive_replace(str_, replace=' '):
67 67 """
68 68 Recursive replace of given sign to just one instance
69 69
70 70 :param str_: given string
71 71 :param replace: char to find and replace multiple instances
72 72
73 73 Examples::
74 74 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
75 75 'Mighty-Mighty-Bo-sstones'
76 76 """
77 77
78 78 if str_.find(replace * 2) == -1:
79 79 return str_
80 80 else:
81 81 str_ = str_.replace(replace * 2, replace)
82 82 return recursive_replace(str_, replace)
83 83
84 84
85 85 def repo_name_slug(value):
86 86 """
87 87 Return slug of name of repository
88 88 This function is called on each creation/modification
89 89 of repository to prevent bad names in repo
90 90 """
91 91
92 92 slug = remove_formatting(value)
93 93 slug = strip_tags(slug)
94 94
95 95 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
96 96 slug = slug.replace(c, '-')
97 97 slug = recursive_replace(slug, '-')
98 98 slug = collapse(slug, '-')
99 99 return slug
100 100
101 101
102 102 #==============================================================================
103 103 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
104 104 #==============================================================================
105 105 def get_repo_slug(request):
106 106 _repo = request.environ['pylons.routes_dict'].get('repo_name')
107 107 if _repo:
108 108 _repo = _repo.rstrip('/')
109 109 return _repo
110 110
111 111
112 112 def get_repo_group_slug(request):
113 113 _group = request.environ['pylons.routes_dict'].get('group_name')
114 114 if _group:
115 115 _group = _group.rstrip('/')
116 116 return _group
117 117
118 118
119 119 def get_user_group_slug(request):
120 120 _group = request.environ['pylons.routes_dict'].get('id')
121 121 _group = UserGroup.get(_group)
122 122 if _group:
123 123 return _group.users_group_name
124 124 return None
125 125
126 126
127 127 def _extract_id_from_repo_name(repo_name):
128 128 if repo_name.startswith('/'):
129 129 repo_name = repo_name.lstrip('/')
130 130 by_id_match = re.match(r'^_(\d{1,})', repo_name)
131 131 if by_id_match:
132 132 return by_id_match.groups()[0]
133 133
134 134
135 135 def get_repo_by_id(repo_name):
136 136 """
137 137 Extracts repo_name by id from special urls. Example url is _11/repo_name
138 138
139 139 :param repo_name:
140 140 :return: repo_name if matched else None
141 141 """
142 142 _repo_id = _extract_id_from_repo_name(repo_name)
143 143 if _repo_id:
144 144 from kallithea.model.db import Repository
145 145 repo = Repository.get(_repo_id)
146 146 if repo:
147 147 # TODO: return repo instead of reponame? or would that be a layering violation?
148 148 return repo.repo_name
149 149 return None
150 150
151 151
152 152 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
153 153 """
154 154 Action logger for various actions made by users
155 155
156 156 :param user: user that made this action, can be a unique username string or
157 157 object containing user_id attribute
158 158 :param action: action to log, should be on of predefined unique actions for
159 159 easy translations
160 160 :param repo: string name of repository or object containing repo_id,
161 161 that action was made on
162 162 :param ipaddr: optional IP address from what the action was made
163 163 :param sa: optional sqlalchemy session
164 164
165 165 """
166 166
167 167 if not sa:
168 168 sa = meta.Session()
169 169 # if we don't get explicit IP address try to get one from registered user
170 170 # in tmpl context var
171 171 if not ipaddr:
172 172 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
173 173
174 174 if getattr(user, 'user_id', None):
175 175 user_obj = User.get(user.user_id)
176 176 elif isinstance(user, basestring):
177 177 user_obj = User.get_by_username(user)
178 178 else:
179 179 raise Exception('You have to provide a user object or a username')
180 180
181 181 if getattr(repo, 'repo_id', None):
182 182 repo_obj = Repository.get(repo.repo_id)
183 183 repo_name = repo_obj.repo_name
184 184 elif isinstance(repo, basestring):
185 185 repo_name = repo.lstrip('/')
186 186 repo_obj = Repository.get_by_repo_name(repo_name)
187 187 else:
188 188 repo_obj = None
189 repo_name = ''
189 repo_name = u''
190 190
191 191 user_log = UserLog()
192 192 user_log.user_id = user_obj.user_id
193 193 user_log.username = user_obj.username
194 194 user_log.action = safe_unicode(action)
195 195
196 196 user_log.repository = repo_obj
197 197 user_log.repository_name = repo_name
198 198
199 199 user_log.action_date = datetime.datetime.now()
200 200 user_log.user_ip = ipaddr
201 201 sa.add(user_log)
202 202
203 203 log.info('Logging action:%s on %s by user:%s ip:%s',
204 204 action, safe_unicode(repo), user_obj, ipaddr)
205 205 if commit:
206 206 sa.commit()
207 207
208 208
209 209 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
210 210 """
211 211 Scans given path for repos and return (name,(type,path)) tuple
212 212
213 213 :param path: path to scan for repositories
214 214 :param recursive: recursive search and return names with subdirs in front
215 215 """
216 216
217 217 # remove ending slash for better results
218 218 path = path.rstrip(os.sep)
219 219 log.debug('now scanning in %s location recursive:%s...', path, recursive)
220 220
221 221 def _get_repos(p):
222 222 if not os.access(p, os.R_OK) or not os.access(p, os.X_OK):
223 223 log.warning('ignoring repo path without access: %s', p)
224 224 return
225 225 if not os.access(p, os.W_OK):
226 226 log.warning('repo path without write access: %s', p)
227 227 for dirpath in os.listdir(p):
228 228 if os.path.isfile(os.path.join(p, dirpath)):
229 229 continue
230 230 cur_path = os.path.join(p, dirpath)
231 231
232 232 # skip removed repos
233 233 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
234 234 continue
235 235
236 236 #skip .<somethin> dirs
237 237 if dirpath.startswith('.'):
238 238 continue
239 239
240 240 try:
241 241 scm_info = get_scm(cur_path)
242 242 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
243 243 except VCSError:
244 244 if not recursive:
245 245 continue
246 246 #check if this dir containts other repos for recursive scan
247 247 rec_path = os.path.join(p, dirpath)
248 248 if not os.path.islink(rec_path) and os.path.isdir(rec_path):
249 249 for inner_scm in _get_repos(rec_path):
250 250 yield inner_scm
251 251
252 252 return _get_repos(path)
253 253
254 254
255 255 def is_valid_repo(repo_name, base_path, scm=None):
256 256 """
257 257 Returns True if given path is a valid repository False otherwise.
258 258 If scm param is given also compare if given scm is the same as expected
259 259 from scm parameter
260 260
261 261 :param repo_name:
262 262 :param base_path:
263 263 :param scm:
264 264
265 265 :return True: if given path is a valid repository
266 266 """
267 267 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
268 268
269 269 try:
270 270 scm_ = get_scm(full_path)
271 271 if scm:
272 272 return scm_[0] == scm
273 273 return True
274 274 except VCSError:
275 275 return False
276 276
277 277
278 278 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
279 279 """
280 280 Returns True if given path is a repository group False otherwise
281 281
282 282 :param repo_name:
283 283 :param base_path:
284 284 """
285 285 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
286 286
287 287 # check if it's not a repo
288 288 if is_valid_repo(repo_group_name, base_path):
289 289 return False
290 290
291 291 try:
292 292 # we need to check bare git repos at higher level
293 293 # since we might match branches/hooks/info/objects or possible
294 294 # other things inside bare git repo
295 295 get_scm(os.path.dirname(full_path))
296 296 return False
297 297 except VCSError:
298 298 pass
299 299
300 300 # check if it's a valid path
301 301 if skip_path_check or os.path.isdir(full_path):
302 302 return True
303 303
304 304 return False
305 305
306 306
307 307 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
308 308 while True:
309 309 ok = raw_input(prompt)
310 310 if ok in ('y', 'ye', 'yes'):
311 311 return True
312 312 if ok in ('n', 'no', 'nop', 'nope'):
313 313 return False
314 314 retries = retries - 1
315 315 if retries < 0:
316 316 raise IOError
317 317 print complaint
318 318
319 319 #propagated from mercurial documentation
320 320 ui_sections = ['alias', 'auth',
321 321 'decode/encode', 'defaults',
322 322 'diff', 'email',
323 323 'extensions', 'format',
324 324 'merge-patterns', 'merge-tools',
325 325 'hooks', 'http_proxy',
326 326 'smtp', 'patch',
327 327 'paths', 'profiling',
328 328 'server', 'trusted',
329 329 'ui', 'web', ]
330 330
331 331
332 332 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
333 333 """
334 334 A function that will read python rc files or database
335 335 and make an mercurial ui object from read options
336 336
337 337 :param path: path to mercurial config file
338 338 :param checkpaths: check the path
339 339 :param read_from: read from 'file' or 'db'
340 340 """
341 341
342 342 baseui = ui.ui()
343 343
344 344 # clean the baseui object
345 345 baseui._ocfg = config.config()
346 346 baseui._ucfg = config.config()
347 347 baseui._tcfg = config.config()
348 348
349 349 if read_from == 'file':
350 350 if not os.path.isfile(path):
351 351 log.debug('hgrc file is not present at %s, skipping...', path)
352 352 return False
353 353 log.debug('reading hgrc from %s', path)
354 354 cfg = config.config()
355 355 cfg.read(path)
356 356 for section in ui_sections:
357 357 for k, v in cfg.items(section):
358 358 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
359 359 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
360 360
361 361 elif read_from == 'db':
362 362 sa = meta.Session()
363 363 ret = sa.query(Ui).all()
364 364
365 365 hg_ui = ret
366 366 for ui_ in hg_ui:
367 367 if ui_.ui_active:
368 368 ui_val = safe_str(ui_.ui_value)
369 369 if ui_.ui_section == 'hooks' and BRAND != 'kallithea' and ui_val.startswith('python:' + BRAND + '.lib.hooks.'):
370 370 ui_val = ui_val.replace('python:' + BRAND + '.lib.hooks.', 'python:kallithea.lib.hooks.')
371 371 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
372 372 ui_.ui_key, ui_val)
373 373 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
374 374 ui_val)
375 375 if ui_.ui_key == 'push_ssl':
376 376 # force set push_ssl requirement to False, kallithea
377 377 # handles that
378 378 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
379 379 False)
380 380 if clear_session:
381 381 meta.Session.remove()
382 382
383 383 # prevent interactive questions for ssh password / passphrase
384 384 ssh = baseui.config('ui', 'ssh', default='ssh')
385 385 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
386 386
387 387 return baseui
388 388
389 389
390 390 def set_app_settings(config):
391 391 """
392 392 Updates pylons config with new settings from database
393 393
394 394 :param config:
395 395 """
396 396 hgsettings = Setting.get_app_settings()
397 397
398 398 for k, v in hgsettings.items():
399 399 config[k] = v
400 400
401 401
402 402 def set_vcs_config(config):
403 403 """
404 404 Patch VCS config with some Kallithea specific stuff
405 405
406 406 :param config: kallithea.CONFIG
407 407 """
408 408 from kallithea.lib.vcs import conf
409 409 from kallithea.lib.utils2 import aslist
410 410 conf.settings.BACKENDS = {
411 411 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
412 412 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
413 413 }
414 414
415 415 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
416 416 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
417 417 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
418 418 'utf8'), sep=',')
419 419
420 420
421 421 def set_indexer_config(config):
422 422 """
423 423 Update Whoosh index mapping
424 424
425 425 :param config: kallithea.CONFIG
426 426 """
427 427 from kallithea.config import conf
428 428
429 429 log.debug('adding extra into INDEX_EXTENSIONS')
430 430 conf.INDEX_EXTENSIONS.extend(re.split('\s+', config.get('index.extensions', '')))
431 431
432 432 log.debug('adding extra into INDEX_FILENAMES')
433 433 conf.INDEX_FILENAMES.extend(re.split('\s+', config.get('index.filenames', '')))
434 434
435 435
436 436 def map_groups(path):
437 437 """
438 438 Given a full path to a repository, create all nested groups that this
439 439 repo is inside. This function creates parent-child relationships between
440 440 groups and creates default perms for all new groups.
441 441
442 442 :param paths: full path to repository
443 443 """
444 444 sa = meta.Session()
445 445 groups = path.split(Repository.url_sep())
446 446 parent = None
447 447 group = None
448 448
449 449 # last element is repo in nested groups structure
450 450 groups = groups[:-1]
451 451 rgm = RepoGroupModel(sa)
452 452 owner = User.get_first_admin()
453 453 for lvl, group_name in enumerate(groups):
454 group_name = '/'.join(groups[:lvl] + [group_name])
454 group_name = u'/'.join(groups[:lvl] + [group_name])
455 455 group = RepoGroup.get_by_group_name(group_name)
456 456 desc = '%s group' % group_name
457 457
458 458 # skip folders that are now removed repos
459 459 if REMOVED_REPO_PAT.match(group_name):
460 460 break
461 461
462 462 if group is None:
463 463 log.debug('creating group level: %s group_name: %s',
464 464 lvl, group_name)
465 465 group = RepoGroup(group_name, parent)
466 466 group.group_description = desc
467 467 group.user = owner
468 468 sa.add(group)
469 469 perm_obj = rgm._create_default_perms(group)
470 470 sa.add(perm_obj)
471 471 sa.flush()
472 472
473 473 parent = group
474 474 return group
475 475
476 476
477 477 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
478 478 install_git_hooks=False, user=None, overwrite_git_hooks=False):
479 479 """
480 480 maps all repos given in initial_repo_list, non existing repositories
481 481 are created, if remove_obsolete is True it also check for db entries
482 482 that are not in initial_repo_list and removes them.
483 483
484 484 :param initial_repo_list: list of repositories found by scanning methods
485 485 :param remove_obsolete: check for obsolete entries in database
486 486 :param install_git_hooks: if this is True, also check and install git hook
487 487 for a repo if missing
488 488 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
489 489 that may be encountered (even if user-deployed)
490 490 """
491 491 from kallithea.model.repo import RepoModel
492 492 from kallithea.model.scm import ScmModel
493 493 sa = meta.Session()
494 494 repo_model = RepoModel()
495 495 if user is None:
496 496 user = User.get_first_admin()
497 497 added = []
498 498
499 499 ##creation defaults
500 500 defs = Setting.get_default_repo_settings(strip_prefix=True)
501 501 enable_statistics = defs.get('repo_enable_statistics')
502 502 enable_locking = defs.get('repo_enable_locking')
503 503 enable_downloads = defs.get('repo_enable_downloads')
504 504 private = defs.get('repo_private')
505 505
506 506 for name, repo in initial_repo_list.items():
507 507 group = map_groups(name)
508 508 unicode_name = safe_unicode(name)
509 509 db_repo = repo_model.get_by_repo_name(unicode_name)
510 510 # found repo that is on filesystem not in Kallithea database
511 511 if not db_repo:
512 512 log.info('repository %s not found, creating now', name)
513 513 added.append(name)
514 514 desc = (repo.description
515 515 if repo.description != 'unknown'
516 516 else '%s repository' % name)
517 517
518 518 new_repo = repo_model._create_repo(
519 519 repo_name=name,
520 520 repo_type=repo.alias,
521 521 description=desc,
522 522 repo_group=getattr(group, 'group_id', None),
523 523 owner=user,
524 524 enable_locking=enable_locking,
525 525 enable_downloads=enable_downloads,
526 526 enable_statistics=enable_statistics,
527 527 private=private,
528 528 state=Repository.STATE_CREATED
529 529 )
530 530 sa.commit()
531 531 # we added that repo just now, and make sure it has githook
532 532 # installed, and updated server info
533 533 if new_repo.repo_type == 'git':
534 534 git_repo = new_repo.scm_instance
535 535 ScmModel().install_git_hooks(git_repo)
536 536 # update repository server-info
537 537 log.debug('Running update server info')
538 538 git_repo._update_server_info()
539 539 new_repo.update_changeset_cache()
540 540 elif install_git_hooks:
541 541 if db_repo.repo_type == 'git':
542 542 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
543 543
544 544 removed = []
545 545 # remove from database those repositories that are not in the filesystem
546 546 for repo in sa.query(Repository).all():
547 547 if repo.repo_name not in initial_repo_list.keys():
548 548 if remove_obsolete:
549 549 log.debug("Removing non-existing repository found in db `%s`",
550 550 repo.repo_name)
551 551 try:
552 552 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
553 553 sa.commit()
554 554 except Exception:
555 555 #don't hold further removals on error
556 556 log.error(traceback.format_exc())
557 557 sa.rollback()
558 558 removed.append(repo.repo_name)
559 559 return added, removed
560 560
561 561
562 562 # set cache regions for beaker so celery can utilise it
563 563 def add_cache(settings):
564 564 cache_settings = {'regions': None}
565 565 for key in settings.keys():
566 566 for prefix in ['beaker.cache.', 'cache.']:
567 567 if key.startswith(prefix):
568 568 name = key.split(prefix)[1].strip()
569 569 cache_settings[name] = settings[key].strip()
570 570 if cache_settings['regions']:
571 571 for region in cache_settings['regions'].split(','):
572 572 region = region.strip()
573 573 region_settings = {}
574 574 for key, value in cache_settings.items():
575 575 if key.startswith(region):
576 576 region_settings[key.split('.')[1]] = value
577 577 region_settings['expire'] = int(region_settings.get('expire',
578 578 60))
579 579 region_settings.setdefault('lock_dir',
580 580 cache_settings.get('lock_dir'))
581 581 region_settings.setdefault('data_dir',
582 582 cache_settings.get('data_dir'))
583 583
584 584 if 'type' not in region_settings:
585 585 region_settings['type'] = cache_settings.get('type',
586 586 'memory')
587 587 beaker.cache.cache_regions[region] = region_settings
588 588
589 589
590 590 def load_rcextensions(root_path):
591 591 import kallithea
592 592 from kallithea.config import conf
593 593
594 594 path = os.path.join(root_path, 'rcextensions', '__init__.py')
595 595 if os.path.isfile(path):
596 596 rcext = create_module('rc', path)
597 597 EXT = kallithea.EXTENSIONS = rcext
598 598 log.debug('Found rcextensions now loading %s...', rcext)
599 599
600 600 # Additional mappings that are not present in the pygments lexers
601 601 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
602 602
603 603 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
604 604
605 605 if getattr(EXT, 'INDEX_EXTENSIONS', []):
606 606 log.debug('settings custom INDEX_EXTENSIONS')
607 607 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
608 608
609 609 #ADDITIONAL MAPPINGS
610 610 log.debug('adding extra into INDEX_EXTENSIONS')
611 611 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
612 612
613 613 # auto check if the module is not missing any data, set to default if is
614 614 # this will help autoupdate new feature of rcext module
615 615 #from kallithea.config import rcextensions
616 616 #for k in dir(rcextensions):
617 617 # if not k.startswith('_') and not hasattr(EXT, k):
618 618 # setattr(EXT, k, getattr(rcextensions, k))
619 619
620 620
621 621 def get_custom_lexer(extension):
622 622 """
623 623 returns a custom lexer if it's defined in rcextensions module, or None
624 624 if there's no custom lexer defined
625 625 """
626 626 import kallithea
627 627 from pygments import lexers
628 628 #check if we didn't define this extension as other lexer
629 629 if kallithea.EXTENSIONS and extension in kallithea.EXTENSIONS.EXTRA_LEXERS:
630 630 _lexer_name = kallithea.EXTENSIONS.EXTRA_LEXERS[extension]
631 631 return lexers.get_lexer_by_name(_lexer_name)
632 632
633 633
634 634 #==============================================================================
635 635 # TEST FUNCTIONS AND CREATORS
636 636 #==============================================================================
637 637 def create_test_index(repo_location, config, full_index):
638 638 """
639 639 Makes default test index
640 640
641 641 :param config: test config
642 642 :param full_index:
643 643 """
644 644
645 645 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
646 646 from kallithea.lib.pidlock import DaemonLock, LockHeld
647 647
648 648 repo_location = repo_location
649 649
650 650 index_location = os.path.join(config['app_conf']['index_dir'])
651 651 if not os.path.exists(index_location):
652 652 os.makedirs(index_location)
653 653
654 654 try:
655 655 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
656 656 WhooshIndexingDaemon(index_location=index_location,
657 657 repo_location=repo_location) \
658 658 .run(full_index=full_index)
659 659 l.release()
660 660 except LockHeld:
661 661 pass
662 662
663 663
664 664 def create_test_env(repos_test_path, config):
665 665 """
666 666 Makes a fresh database and
667 667 install test repository into tmp dir
668 668 """
669 669 from kallithea.lib.db_manage import DbManage
670 670 from kallithea.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
671 671
672 672 # PART ONE create db
673 673 dbconf = config['sqlalchemy.db1.url']
674 674 log.debug('making test db %s', dbconf)
675 675
676 676 # create test dir if it doesn't exist
677 677 if not os.path.isdir(repos_test_path):
678 678 log.debug('Creating testdir %s', repos_test_path)
679 679 os.makedirs(repos_test_path)
680 680
681 681 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
682 682 tests=True)
683 683 dbmanage.create_tables(override=True)
684 684 # for tests dynamically set new root paths based on generated content
685 685 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
686 686 dbmanage.create_default_user()
687 687 dbmanage.admin_prompt()
688 688 dbmanage.create_permissions()
689 689 dbmanage.populate_default_permissions()
690 690 Session().commit()
691 691 # PART TWO make test repo
692 692 log.debug('making test vcs repositories')
693 693
694 694 idx_path = config['app_conf']['index_dir']
695 695 data_path = config['app_conf']['cache_dir']
696 696
697 697 #clean index and data
698 698 if idx_path and os.path.exists(idx_path):
699 699 log.debug('remove %s', idx_path)
700 700 shutil.rmtree(idx_path)
701 701
702 702 if data_path and os.path.exists(data_path):
703 703 log.debug('remove %s', data_path)
704 704 shutil.rmtree(data_path)
705 705
706 706 #CREATE DEFAULT TEST REPOS
707 707 cur_dir = dn(dn(abspath(__file__)))
708 708 tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_hg.tar.gz"))
709 709 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
710 710 tar.close()
711 711
712 712 cur_dir = dn(dn(abspath(__file__)))
713 713 tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_git.tar.gz"))
714 714 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
715 715 tar.close()
716 716
717 717 #LOAD VCS test stuff
718 718 from kallithea.tests.vcs import setup_package
719 719 setup_package()
720 720
721 721
722 722 #==============================================================================
723 723 # PASTER COMMANDS
724 724 #==============================================================================
725 725 class BasePasterCommand(Command):
726 726 """
727 727 Abstract Base Class for paster commands.
728 728
729 729 The celery commands are somewhat aggressive about loading
730 730 celery.conf, and since our module sets the `CELERY_LOADER`
731 731 environment variable to our loader, we have to bootstrap a bit and
732 732 make sure we've had a chance to load the pylons config off of the
733 733 command line, otherwise everything fails.
734 734 """
735 735 min_args = 1
736 736 min_args_error = "Please provide a paster config file as an argument."
737 737 takes_config_file = 1
738 738 requires_config_file = True
739 739
740 740 def notify_msg(self, msg, log=False):
741 741 """Make a notification to user, additionally if logger is passed
742 742 it logs this action using given logger
743 743
744 744 :param msg: message that will be printed to user
745 745 :param log: logging instance, to use to additionally log this message
746 746
747 747 """
748 748 if log and isinstance(log, logging):
749 749 log(msg)
750 750
751 751 def run(self, args):
752 752 """
753 753 Overrides Command.run
754 754
755 755 Checks for a config file argument and loads it.
756 756 """
757 757 if len(args) < self.min_args:
758 758 raise BadCommand(
759 759 self.min_args_error % {'min_args': self.min_args,
760 760 'actual_args': len(args)})
761 761
762 762 # Decrement because we're going to lob off the first argument.
763 763 # @@ This is hacky
764 764 self.min_args -= 1
765 765 self.bootstrap_config(args[0])
766 766 self.update_parser()
767 767 return super(BasePasterCommand, self).run(args[1:])
768 768
769 769 def update_parser(self):
770 770 """
771 771 Abstract method. Allows for the class's parser to be updated
772 772 before the superclass's `run` method is called. Necessary to
773 773 allow options/arguments to be passed through to the underlying
774 774 celery command.
775 775 """
776 776 raise NotImplementedError("Abstract Method.")
777 777
778 778 def bootstrap_config(self, conf):
779 779 """
780 780 Loads the pylons configuration.
781 781 """
782 782 from pylons import config as pylonsconfig
783 783
784 784 self.path_to_ini_file = os.path.realpath(conf)
785 785 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
786 786 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
787 787
788 788 def _init_session(self):
789 789 """
790 790 Inits SqlAlchemy Session
791 791 """
792 792 logging.config.fileConfig(self.path_to_ini_file)
793 793
794 794 from pylons import config
795 795 from kallithea.model import init_model
796 796 from kallithea.lib.utils2 import engine_from_config
797 797 add_cache(config)
798 798 engine = engine_from_config(config, 'sqlalchemy.db1.')
799 799 init_model(engine)
800 800
801 801
802 802 def check_git_version():
803 803 """
804 804 Checks what version of git is installed in system, and issues a warning
805 805 if it's too old for Kallithea to work properly.
806 806 """
807 807 from kallithea import BACKENDS
808 808 from kallithea.lib.vcs.backends.git.repository import GitRepository
809 809 from kallithea.lib.vcs.conf import settings
810 810 from distutils.version import StrictVersion
811 811
812 812 if 'git' not in BACKENDS:
813 813 return None
814 814
815 815 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
816 816 _safe=True)
817 817
818 818 m = re.search("\d+.\d+.\d+", stdout)
819 819 if m:
820 820 ver = StrictVersion(m.group(0))
821 821 else:
822 822 ver = StrictVersion('0.0.0')
823 823
824 824 req_ver = StrictVersion('1.7.4')
825 825
826 826 log.debug('Git executable: "%s" version %s detected: %s',
827 827 settings.GIT_EXECUTABLE_PATH, ver, stdout)
828 828 if stderr:
829 829 log.warning('Error detecting git version: %r', stderr)
830 830 elif ver < req_ver:
831 831 log.warning('Kallithea detected git version %s, which is too old '
832 832 'for the system to function properly. '
833 833 'Please upgrade to version %s or later.' % (ver, req_ver))
834 834 return ver
835 835
836 836
837 837 @decorator.decorator
838 838 def jsonify(func, *args, **kwargs):
839 839 """Action decorator that formats output for JSON
840 840
841 841 Given a function that will return content, this decorator will turn
842 842 the result into JSON, with a content-type of 'application/json' and
843 843 output it.
844 844
845 845 """
846 846 from pylons.decorators.util import get_pylons
847 847 from kallithea.lib.compat import json
848 848 pylons = get_pylons(args)
849 849 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
850 850 data = func(*args, **kwargs)
851 851 if isinstance(data, (list, tuple)):
852 852 msg = "JSON responses with Array envelopes are susceptible to " \
853 853 "cross-site data leak attacks, see " \
854 854 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
855 855 warnings.warn(msg, Warning, 2)
856 856 log.warning(msg)
857 857 log.debug("Returning JSON wrapped action output")
858 858 return json.dumps(data, encoding='utf-8')
859 859
860 860
861 861 def conditional_cache(region, prefix, condition, func):
862 862 """
863 863
864 864 Conditional caching function use like::
865 865 def _c(arg):
866 866 #heavy computation function
867 867 return data
868 868
869 869 # denpending from condition the compute is wrapped in cache or not
870 870 compute = conditional_cache('short_term', 'cache_desc', codnition=True, func=func)
871 871 return compute(arg)
872 872
873 873 :param region: name of cache region
874 874 :param prefix: cache region prefix
875 875 :param condition: condition for cache to be triggered, and return data cached
876 876 :param func: wrapped heavy function to compute
877 877
878 878 """
879 879 wrapped = func
880 880 if condition:
881 881 log.debug('conditional_cache: True, wrapping call of '
882 882 'func: %s into %s region cache' % (region, func))
883 883 wrapped = _cache_decorate((prefix,), None, None, region)(func)
884 884
885 885 return wrapped
@@ -1,526 +1,526 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
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 General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.user
16 16 ~~~~~~~~~~~~~~~~~~~~
17 17
18 18 users model for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 9, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import hashlib
30 30 import hmac
31 31 import logging
32 32 import time
33 33 import traceback
34 34
35 35 from pylons import config
36 36 from pylons.i18n.translation import _
37 37
38 38 from sqlalchemy.exc import DatabaseError
39 39
40 40 from kallithea import EXTERN_TYPE_INTERNAL
41 41 from kallithea.lib.utils2 import safe_str, generate_api_key, get_current_authuser
42 42 from kallithea.lib.caching_query import FromCache
43 43 from kallithea.model import BaseModel
44 44 from kallithea.model.db import User, UserToPerm, Notification, \
45 45 UserEmailMap, UserIpMap
46 46 from kallithea.lib.exceptions import DefaultUserException, \
47 47 UserOwnsReposException
48 48 from kallithea.model.meta import Session
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 password_reset_token_lifetime = 86400 # 24 hours
56 56
57 57 cls = User
58 58
59 59 def get(self, user_id, cache=False):
60 60 user = self.sa.query(User)
61 61 if cache:
62 62 user = user.options(FromCache("sql_cache_short",
63 63 "get_user_%s" % user_id))
64 64 return user.get(user_id)
65 65
66 66 def get_user(self, user):
67 67 return self._get_user(user)
68 68
69 69 def create(self, form_data, cur_user=None):
70 70 if not cur_user:
71 71 cur_user = getattr(get_current_authuser(), 'username', None)
72 72
73 73 from kallithea.lib.hooks import log_create_user, \
74 74 check_allowed_create_user
75 75 _fd = form_data
76 76 user_data = {
77 77 'username': _fd['username'],
78 78 'password': _fd['password'],
79 79 'email': _fd['email'],
80 80 'firstname': _fd['firstname'],
81 81 'lastname': _fd['lastname'],
82 82 'active': _fd['active'],
83 83 'admin': False
84 84 }
85 85 # raises UserCreationError if it's not allowed
86 86 check_allowed_create_user(user_data, cur_user)
87 87 from kallithea.lib.auth import get_crypt_password
88 88
89 89 new_user = User()
90 90 for k, v in form_data.items():
91 91 if k == 'password':
92 92 v = get_crypt_password(v)
93 93 if k == 'firstname':
94 94 k = 'name'
95 95 setattr(new_user, k, v)
96 96
97 97 new_user.api_key = generate_api_key()
98 98 self.sa.add(new_user)
99 99
100 100 log_create_user(new_user.get_dict(), cur_user)
101 101 return new_user
102 102
103 def create_or_update(self, username, password, email, firstname='',
104 lastname='', active=True, admin=False,
103 def create_or_update(self, username, password, email, firstname=u'',
104 lastname=u'', active=True, admin=False,
105 105 extern_type=None, extern_name=None, cur_user=None):
106 106 """
107 107 Creates a new instance if not found, or updates current one
108 108
109 109 :param username:
110 110 :param password:
111 111 :param email:
112 112 :param active:
113 113 :param firstname:
114 114 :param lastname:
115 115 :param active:
116 116 :param admin:
117 117 :param extern_name:
118 118 :param extern_type:
119 119 :param cur_user:
120 120 """
121 121 if not cur_user:
122 122 cur_user = getattr(get_current_authuser(), 'username', None)
123 123
124 124 from kallithea.lib.auth import get_crypt_password, check_password
125 125 from kallithea.lib.hooks import log_create_user, \
126 126 check_allowed_create_user
127 127 user_data = {
128 128 'username': username, 'password': password,
129 129 'email': email, 'firstname': firstname, 'lastname': lastname,
130 130 'active': active, 'admin': admin
131 131 }
132 132 # raises UserCreationError if it's not allowed
133 133 check_allowed_create_user(user_data, cur_user)
134 134
135 135 log.debug('Checking for %s account in Kallithea database', username)
136 136 user = User.get_by_username(username, case_insensitive=True)
137 137 if user is None:
138 138 log.debug('creating new user %s', username)
139 139 new_user = User()
140 140 edit = False
141 141 else:
142 142 log.debug('updating user %s', username)
143 143 new_user = user
144 144 edit = True
145 145
146 146 try:
147 147 new_user.username = username
148 148 new_user.admin = admin
149 149 new_user.email = email
150 150 new_user.active = active
151 151 new_user.extern_name = safe_str(extern_name) \
152 152 if extern_name else None
153 153 new_user.extern_type = safe_str(extern_type) \
154 154 if extern_type else None
155 155 new_user.name = firstname
156 156 new_user.lastname = lastname
157 157
158 158 if not edit:
159 159 new_user.api_key = generate_api_key()
160 160
161 161 # set password only if creating an user or password is changed
162 162 password_change = new_user.password and \
163 163 not check_password(password, new_user.password)
164 164 if not edit or password_change:
165 165 reason = 'new password' if edit else 'new user'
166 166 log.debug('Updating password reason=>%s', reason)
167 167 new_user.password = get_crypt_password(password) \
168 168 if password else None
169 169
170 170 self.sa.add(new_user)
171 171
172 172 if not edit:
173 173 log_create_user(new_user.get_dict(), cur_user)
174 174 return new_user
175 175 except (DatabaseError,):
176 176 log.error(traceback.format_exc())
177 177 raise
178 178
179 179 def create_registration(self, form_data):
180 180 from kallithea.model.notification import NotificationModel
181 181 import kallithea.lib.helpers as h
182 182
183 183 form_data['admin'] = False
184 184 form_data['extern_name'] = EXTERN_TYPE_INTERNAL
185 185 form_data['extern_type'] = EXTERN_TYPE_INTERNAL
186 186 new_user = self.create(form_data)
187 187
188 188 self.sa.add(new_user)
189 189 self.sa.flush()
190 190
191 191 # notification to admins
192 192 subject = _('New user registration')
193 193 body = (
194 'New user registration\n'
194 u'New user registration\n'
195 195 '---------------------\n'
196 196 '- Username: {user.username}\n'
197 197 '- Full Name: {user.full_name}\n'
198 198 '- Email: {user.email}\n'
199 199 ).format(user=new_user)
200 200 edit_url = h.canonical_url('edit_user', id=new_user.user_id)
201 201 email_kwargs = {
202 202 'registered_user_url': edit_url,
203 203 'new_username': new_user.username}
204 204 NotificationModel().create(created_by=new_user, subject=subject,
205 205 body=body, recipients=None,
206 206 type_=Notification.TYPE_REGISTRATION,
207 207 email_kwargs=email_kwargs)
208 208
209 209 def update(self, user_id, form_data, skip_attrs=None):
210 210 from kallithea.lib.auth import get_crypt_password
211 211 skip_attrs = skip_attrs or []
212 212 user = self.get(user_id, cache=False)
213 213 if user.username == User.DEFAULT_USER:
214 214 raise DefaultUserException(
215 215 _("You can't edit this user since it's "
216 216 "crucial for entire application"))
217 217
218 218 for k, v in form_data.items():
219 219 if k in skip_attrs:
220 220 continue
221 221 if k == 'new_password' and v:
222 222 user.password = get_crypt_password(v)
223 223 else:
224 224 # old legacy thing orm models store firstname as name,
225 225 # need proper refactor to username
226 226 if k == 'firstname':
227 227 k = 'name'
228 228 setattr(user, k, v)
229 229 self.sa.add(user)
230 230
231 231 def update_user(self, user, **kwargs):
232 232 from kallithea.lib.auth import get_crypt_password
233 233
234 234 user = self._get_user(user)
235 235 if user.username == User.DEFAULT_USER:
236 236 raise DefaultUserException(
237 237 _("You can't edit this user since it's"
238 238 " crucial for entire application")
239 239 )
240 240
241 241 for k, v in kwargs.items():
242 242 if k == 'password' and v:
243 243 v = get_crypt_password(v)
244 244
245 245 setattr(user, k, v)
246 246 self.sa.add(user)
247 247 return user
248 248
249 249 def delete(self, user, cur_user=None):
250 250 if cur_user is None:
251 251 cur_user = getattr(get_current_authuser(), 'username', None)
252 252 user = self._get_user(user)
253 253
254 254 if user.username == User.DEFAULT_USER:
255 255 raise DefaultUserException(
256 256 _("You can't remove this user since it is"
257 257 " crucial for the entire application"))
258 258 if user.repositories:
259 259 repos = [x.repo_name for x in user.repositories]
260 260 raise UserOwnsReposException(
261 261 _('User "%s" still owns %s repositories and cannot be '
262 262 'removed. Switch owners or remove those repositories: %s')
263 263 % (user.username, len(repos), ', '.join(repos)))
264 264 if user.repo_groups:
265 265 repogroups = [x.group_name for x in user.repo_groups]
266 266 raise UserOwnsReposException(_(
267 267 'User "%s" still owns %s repository groups and cannot be '
268 268 'removed. Switch owners or remove those repository groups: %s')
269 269 % (user.username, len(repogroups), ', '.join(repogroups)))
270 270 if user.user_groups:
271 271 usergroups = [x.users_group_name for x in user.user_groups]
272 272 raise UserOwnsReposException(
273 273 _('User "%s" still owns %s user groups and cannot be '
274 274 'removed. Switch owners or remove those user groups: %s')
275 275 % (user.username, len(usergroups), ', '.join(usergroups)))
276 276 self.sa.delete(user)
277 277
278 278 from kallithea.lib.hooks import log_delete_user
279 279 log_delete_user(user.get_dict(), cur_user)
280 280
281 281 def can_change_password(self, user):
282 282 from kallithea.lib import auth_modules
283 283 managed_fields = auth_modules.get_managed_fields(user)
284 284 return 'password' not in managed_fields
285 285
286 286 def get_reset_password_token(self, user, timestamp, session_id):
287 287 """
288 288 The token is a 40-digit hexstring, calculated as a HMAC-SHA1.
289 289
290 290 In a traditional HMAC scenario, an attacker is unable to know or
291 291 influence the secret key, but can know or influence the message
292 292 and token. This scenario is slightly different (in particular
293 293 since the message sender is also the message recipient), but
294 294 sufficiently similar to use an HMAC. Benefits compared to a plain
295 295 SHA1 hash includes resistance against a length extension attack.
296 296
297 297 The HMAC key consists of the following values (known only to the
298 298 server and authorized users):
299 299
300 300 * per-application secret (the `app_instance_uuid` setting), without
301 301 which an attacker cannot counterfeit tokens
302 302 * hashed user password, invalidating the token upon password change
303 303
304 304 The HMAC message consists of the following values (potentially known
305 305 to an attacker):
306 306
307 307 * session ID (the anti-CSRF token), requiring an attacker to have
308 308 access to the browser session in which the token was created
309 309 * numeric user ID, limiting the token to a specific user (yet allowing
310 310 users to be renamed)
311 311 * user email address
312 312 * time of token issue (a Unix timestamp, to enable token expiration)
313 313
314 314 The key and message values are separated by NUL characters, which are
315 315 guaranteed not to occur in any of the values.
316 316 """
317 317 app_secret = config.get('app_instance_uuid')
318 318 return hmac.HMAC(
319 319 key=u'\0'.join([app_secret, user.password]).encode('utf-8'),
320 320 msg=u'\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
321 321 digestmod=hashlib.sha1,
322 322 ).hexdigest()
323 323
324 324 def send_reset_password_email(self, data):
325 325 """
326 326 Sends email with a password reset token and link to the password
327 327 reset confirmation page with all information (including the token)
328 328 pre-filled. Also returns URL of that page, only without the token,
329 329 allowing users to copy-paste or manually enter the token from the
330 330 email.
331 331 """
332 332 from kallithea.lib.celerylib import tasks, run_task
333 333 from kallithea.model.notification import EmailNotificationModel
334 334 import kallithea.lib.helpers as h
335 335
336 336 user_email = data['email']
337 337 user = User.get_by_email(user_email)
338 338 timestamp = int(time.time())
339 339 if user is not None:
340 340 if self.can_change_password(user):
341 341 log.debug('password reset user %s found', user)
342 342 token = self.get_reset_password_token(user,
343 343 timestamp,
344 344 h.authentication_token())
345 345 # URL must be fully qualified; but since the token is locked to
346 346 # the current browser session, we must provide a URL with the
347 347 # current scheme and hostname, rather than the canonical_url.
348 348 link = h.url('reset_password_confirmation', qualified=True,
349 349 email=user_email,
350 350 timestamp=timestamp,
351 351 token=token)
352 352 else:
353 353 log.debug('password reset user %s found but was managed', user)
354 354 token = link = None
355 355 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
356 356 body = EmailNotificationModel().get_email_tmpl(
357 357 reg_type, 'txt',
358 358 user=user.short_contact,
359 359 reset_token=token,
360 360 reset_url=link)
361 361 html_body = EmailNotificationModel().get_email_tmpl(
362 362 reg_type, 'html',
363 363 user=user.short_contact,
364 364 reset_token=token,
365 365 reset_url=link)
366 366 log.debug('sending email')
367 367 run_task(tasks.send_email, [user_email],
368 368 _("Password reset link"), body, html_body)
369 369 log.info('send new password mail to %s', user_email)
370 370 else:
371 371 log.debug("password reset email %s not found", user_email)
372 372
373 373 return h.url('reset_password_confirmation',
374 374 email=user_email,
375 375 timestamp=timestamp)
376 376
377 377 def verify_reset_password_token(self, email, timestamp, token):
378 378 from kallithea.lib.celerylib import tasks, run_task
379 379 from kallithea.lib import auth
380 380 import kallithea.lib.helpers as h
381 381 user = User.get_by_email(email)
382 382 if user is None:
383 383 log.debug("user with email %s not found", email)
384 384 return False
385 385
386 386 token_age = int(time.time()) - int(timestamp)
387 387
388 388 if token_age < 0:
389 389 log.debug('timestamp is from the future')
390 390 return False
391 391
392 392 if token_age > UserModel.password_reset_token_lifetime:
393 393 log.debug('password reset token expired')
394 394 return False
395 395
396 396 expected_token = self.get_reset_password_token(user,
397 397 timestamp,
398 398 h.authentication_token())
399 399 log.debug('computed password reset token: %s', expected_token)
400 400 log.debug('received password reset token: %s', token)
401 401 return expected_token == token
402 402
403 403 def reset_password(self, user_email, new_passwd):
404 404 from kallithea.lib.celerylib import tasks, run_task
405 405 from kallithea.lib import auth
406 406 user = User.get_by_email(user_email)
407 407 if user is not None:
408 408 if not self.can_change_password(user):
409 409 raise Exception('trying to change password for external user')
410 410 user.password = auth.get_crypt_password(new_passwd)
411 411 Session().add(user)
412 412 Session().commit()
413 413 log.info('change password for %s', user_email)
414 414 if new_passwd is None:
415 415 raise Exception('unable to set new password')
416 416
417 417 run_task(tasks.send_email, [user_email],
418 418 _('Password reset notification'),
419 419 _('The password to your account %s has been changed using password reset form.') % (user.username,))
420 420 log.info('send password reset mail to %s', user_email)
421 421
422 422 return True
423 423
424 424 def has_perm(self, user, perm):
425 425 perm = self._get_perm(perm)
426 426 user = self._get_user(user)
427 427
428 428 return UserToPerm.query().filter(UserToPerm.user == user) \
429 429 .filter(UserToPerm.permission == perm).scalar() is not None
430 430
431 431 def grant_perm(self, user, perm):
432 432 """
433 433 Grant user global permissions
434 434
435 435 :param user:
436 436 :param perm:
437 437 """
438 438 user = self._get_user(user)
439 439 perm = self._get_perm(perm)
440 440 # if this permission is already granted skip it
441 441 _perm = UserToPerm.query() \
442 442 .filter(UserToPerm.user == user) \
443 443 .filter(UserToPerm.permission == perm) \
444 444 .scalar()
445 445 if _perm:
446 446 return
447 447 new = UserToPerm()
448 448 new.user = user
449 449 new.permission = perm
450 450 self.sa.add(new)
451 451 return new
452 452
453 453 def revoke_perm(self, user, perm):
454 454 """
455 455 Revoke users global permissions
456 456
457 457 :param user:
458 458 :param perm:
459 459 """
460 460 user = self._get_user(user)
461 461 perm = self._get_perm(perm)
462 462
463 463 UserToPerm.query().filter(
464 464 UserToPerm.user == user,
465 465 UserToPerm.permission == perm,
466 466 ).delete()
467 467
468 468 def add_extra_email(self, user, email):
469 469 """
470 470 Adds email address to UserEmailMap
471 471
472 472 :param user:
473 473 :param email:
474 474 """
475 475 from kallithea.model import forms
476 476 form = forms.UserExtraEmailForm()()
477 477 data = form.to_python(dict(email=email))
478 478 user = self._get_user(user)
479 479
480 480 obj = UserEmailMap()
481 481 obj.user = user
482 482 obj.email = data['email']
483 483 self.sa.add(obj)
484 484 return obj
485 485
486 486 def delete_extra_email(self, user, email_id):
487 487 """
488 488 Removes email address from UserEmailMap
489 489
490 490 :param user:
491 491 :param email_id:
492 492 """
493 493 user = self._get_user(user)
494 494 obj = UserEmailMap.query().get(email_id)
495 495 if obj is not None:
496 496 self.sa.delete(obj)
497 497
498 498 def add_extra_ip(self, user, ip):
499 499 """
500 500 Adds IP address to UserIpMap
501 501
502 502 :param user:
503 503 :param ip:
504 504 """
505 505 from kallithea.model import forms
506 506 form = forms.UserExtraIpForm()()
507 507 data = form.to_python(dict(ip=ip))
508 508 user = self._get_user(user)
509 509
510 510 obj = UserIpMap()
511 511 obj.user = user
512 512 obj.ip_addr = data['ip']
513 513 self.sa.add(obj)
514 514 return obj
515 515
516 516 def delete_extra_ip(self, user, ip_id):
517 517 """
518 518 Removes IP address from UserIpMap
519 519
520 520 :param user:
521 521 :param ip_id:
522 522 """
523 523 user = self._get_user(user)
524 524 obj = UserIpMap.query().get(ip_id)
525 525 if obj:
526 526 self.sa.delete(obj)
@@ -1,390 +1,390 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
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 General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.user_group
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 user group model for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Oct 1, 2011
23 23 :author: nvinot, marcink
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from kallithea.model import BaseModel
31 31 from kallithea.model.db import UserGroupMember, UserGroup, \
32 32 UserGroupRepoToPerm, Permission, UserGroupToPerm, User, UserUserGroupToPerm, \
33 33 UserGroupUserGroupToPerm
34 34 from kallithea.lib.exceptions import UserGroupsAssignedException, \
35 35 RepoGroupAssignmentError
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class UserGroupModel(BaseModel):
41 41
42 42 cls = UserGroup
43 43
44 44 def _get_user_group(self, user_group):
45 45 return self._get_instance(UserGroup, user_group,
46 46 callback=UserGroup.get_by_group_name)
47 47
48 48 def _create_default_perms(self, user_group):
49 49 # create default permission
50 50 default_perm = 'usergroup.read'
51 51 def_user = User.get_default_user()
52 52 for p in def_user.user_perms:
53 53 if p.permission.permission_name.startswith('usergroup.'):
54 54 default_perm = p.permission.permission_name
55 55 break
56 56
57 57 user_group_to_perm = UserUserGroupToPerm()
58 58 user_group_to_perm.permission = Permission.get_by_key(default_perm)
59 59
60 60 user_group_to_perm.user_group = user_group
61 61 user_group_to_perm.user_id = def_user.user_id
62 62 return user_group_to_perm
63 63
64 64 def _update_permissions(self, user_group, perms_new=None,
65 65 perms_updates=None):
66 66 from kallithea.lib.auth import HasUserGroupPermissionAny
67 67 if not perms_new:
68 68 perms_new = []
69 69 if not perms_updates:
70 70 perms_updates = []
71 71
72 72 # update permissions
73 73 for member, perm, member_type in perms_updates:
74 74 if member_type == 'user':
75 75 # this updates existing one
76 76 self.grant_user_permission(
77 77 user_group=user_group, user=member, perm=perm
78 78 )
79 79 else:
80 80 #check if we have permissions to alter this usergroup
81 81 if HasUserGroupPermissionAny('usergroup.read', 'usergroup.write',
82 82 'usergroup.admin')(member):
83 83 self.grant_user_group_permission(
84 84 target_user_group=user_group, user_group=member, perm=perm
85 85 )
86 86 # set new permissions
87 87 for member, perm, member_type in perms_new:
88 88 if member_type == 'user':
89 89 self.grant_user_permission(
90 90 user_group=user_group, user=member, perm=perm
91 91 )
92 92 else:
93 93 #check if we have permissions to alter this usergroup
94 94 if HasUserGroupPermissionAny('usergroup.read', 'usergroup.write',
95 95 'usergroup.admin')(member):
96 96 self.grant_user_group_permission(
97 97 target_user_group=user_group, user_group=member, perm=perm
98 98 )
99 99
100 100 def get(self, user_group_id, cache=False):
101 101 return UserGroup.get(user_group_id)
102 102
103 103 def get_group(self, user_group):
104 104 return self._get_user_group(user_group)
105 105
106 106 def get_by_name(self, name, cache=False, case_insensitive=False):
107 107 return UserGroup.get_by_group_name(name, cache, case_insensitive)
108 108
109 109 def create(self, name, description, owner, active=True, group_data=None):
110 110 try:
111 111 new_user_group = UserGroup()
112 112 new_user_group.user = self._get_user(owner)
113 113 new_user_group.users_group_name = name
114 114 new_user_group.user_group_description = description
115 115 new_user_group.users_group_active = active
116 116 if group_data:
117 117 new_user_group.group_data = group_data
118 118 self.sa.add(new_user_group)
119 119 perm_obj = self._create_default_perms(new_user_group)
120 120 self.sa.add(perm_obj)
121 121
122 122 self.grant_user_permission(user_group=new_user_group,
123 123 user=owner, perm='usergroup.admin')
124 124
125 125 return new_user_group
126 126 except Exception:
127 127 log.error(traceback.format_exc())
128 128 raise
129 129
130 130 def update(self, user_group, form_data):
131 131
132 132 try:
133 133 user_group = self._get_user_group(user_group)
134 134
135 135 for k, v in form_data.items():
136 136 if k == 'users_group_members':
137 137 user_group.members = []
138 138 self.sa.flush()
139 139 members_list = []
140 140 if v:
141 141 v = [v] if isinstance(v, basestring) else v
142 142 for u_id in set(v):
143 143 member = UserGroupMember(user_group.users_group_id, u_id)
144 144 members_list.append(member)
145 145 setattr(user_group, 'members', members_list)
146 146 setattr(user_group, k, v)
147 147
148 148 self.sa.add(user_group)
149 149 except Exception:
150 150 log.error(traceback.format_exc())
151 151 raise
152 152
153 153 def delete(self, user_group, force=False):
154 154 """
155 155 Deletes user group, unless force flag is used
156 156 raises exception if there are members in that group, else deletes
157 157 group and users
158 158
159 159 :param user_group:
160 160 :param force:
161 161 """
162 162 user_group = self._get_user_group(user_group)
163 163 try:
164 164 # check if this group is not assigned to repo
165 165 assigned_groups = UserGroupRepoToPerm.query() \
166 166 .filter(UserGroupRepoToPerm.users_group == user_group).all()
167 167 assigned_groups = [x.repository.repo_name for x in assigned_groups]
168 168
169 169 if assigned_groups and not force:
170 170 raise UserGroupsAssignedException(
171 171 'User Group assigned to %s' % ", ".join(assigned_groups))
172 172 self.sa.delete(user_group)
173 173 except Exception:
174 174 log.error(traceback.format_exc())
175 175 raise
176 176
177 177 def add_user_to_group(self, user_group, user):
178 178 user_group = self._get_user_group(user_group)
179 179 user = self._get_user(user)
180 180
181 181 for m in user_group.members:
182 182 u = m.user
183 183 if u.user_id == user.user_id:
184 184 # user already in the group, skip
185 185 return True
186 186
187 187 try:
188 188 user_group_member = UserGroupMember()
189 189 user_group_member.user = user
190 190 user_group_member.users_group = user_group
191 191
192 192 user_group.members.append(user_group_member)
193 193 user.group_member.append(user_group_member)
194 194
195 195 self.sa.add(user_group_member)
196 196 return user_group_member
197 197 except Exception:
198 198 log.error(traceback.format_exc())
199 199 raise
200 200
201 201 def remove_user_from_group(self, user_group, user):
202 202 user_group = self._get_user_group(user_group)
203 203 user = self._get_user(user)
204 204
205 205 user_group_member = None
206 206 for m in user_group.members:
207 207 if m.user.user_id == user.user_id:
208 208 # Found this user's membership row
209 209 user_group_member = m
210 210 break
211 211
212 212 if user_group_member:
213 213 try:
214 214 self.sa.delete(user_group_member)
215 215 return True
216 216 except Exception:
217 217 log.error(traceback.format_exc())
218 218 raise
219 219 else:
220 220 # User isn't in that group
221 221 return False
222 222
223 223 def has_perm(self, user_group, perm):
224 224 user_group = self._get_user_group(user_group)
225 225 perm = self._get_perm(perm)
226 226
227 227 return UserGroupToPerm.query() \
228 228 .filter(UserGroupToPerm.users_group == user_group) \
229 229 .filter(UserGroupToPerm.permission == perm).scalar() is not None
230 230
231 231 def grant_perm(self, user_group, perm):
232 232 user_group = self._get_user_group(user_group)
233 233 perm = self._get_perm(perm)
234 234
235 235 # if this permission is already granted skip it
236 236 _perm = UserGroupToPerm.query() \
237 237 .filter(UserGroupToPerm.users_group == user_group) \
238 238 .filter(UserGroupToPerm.permission == perm) \
239 239 .scalar()
240 240 if _perm:
241 241 return
242 242
243 243 new = UserGroupToPerm()
244 244 new.users_group = user_group
245 245 new.permission = perm
246 246 self.sa.add(new)
247 247 return new
248 248
249 249 def revoke_perm(self, user_group, perm):
250 250 user_group = self._get_user_group(user_group)
251 251 perm = self._get_perm(perm)
252 252
253 253 obj = UserGroupToPerm.query() \
254 254 .filter(UserGroupToPerm.users_group == user_group) \
255 255 .filter(UserGroupToPerm.permission == perm).scalar()
256 256 if obj is not None:
257 257 self.sa.delete(obj)
258 258
259 259 def grant_user_permission(self, user_group, user, perm):
260 260 """
261 261 Grant permission for user on given user group, or update
262 262 existing one if found
263 263
264 264 :param user_group: Instance of UserGroup, users_group_id,
265 265 or users_group_name
266 266 :param user: Instance of User, user_id or username
267 267 :param perm: Instance of Permission, or permission_name
268 268 """
269 269
270 270 user_group = self._get_user_group(user_group)
271 271 user = self._get_user(user)
272 272 permission = self._get_perm(perm)
273 273
274 274 # check if we have that permission already
275 275 obj = self.sa.query(UserUserGroupToPerm) \
276 276 .filter(UserUserGroupToPerm.user == user) \
277 277 .filter(UserUserGroupToPerm.user_group == user_group) \
278 278 .scalar()
279 279 if obj is None:
280 280 # create new !
281 281 obj = UserUserGroupToPerm()
282 282 obj.user_group = user_group
283 283 obj.user = user
284 284 obj.permission = permission
285 285 self.sa.add(obj)
286 286 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
287 287 return obj
288 288
289 289 def revoke_user_permission(self, user_group, user):
290 290 """
291 291 Revoke permission for user on given repository group
292 292
293 293 :param user_group: Instance of RepoGroup, repositories_group_id,
294 294 or repositories_group name
295 295 :param user: Instance of User, user_id or username
296 296 """
297 297
298 298 user_group = self._get_user_group(user_group)
299 299 user = self._get_user(user)
300 300
301 301 obj = self.sa.query(UserUserGroupToPerm) \
302 302 .filter(UserUserGroupToPerm.user == user) \
303 303 .filter(UserUserGroupToPerm.user_group == user_group) \
304 304 .scalar()
305 305 if obj is not None:
306 306 self.sa.delete(obj)
307 307 log.debug('Revoked perm on %s on %s', user_group, user)
308 308
309 309 def grant_user_group_permission(self, target_user_group, user_group, perm):
310 310 """
311 311 Grant user group permission for given target_user_group
312 312
313 313 :param target_user_group:
314 314 :param user_group:
315 315 :param perm:
316 316 """
317 317 target_user_group = self._get_user_group(target_user_group)
318 318 user_group = self._get_user_group(user_group)
319 319 permission = self._get_perm(perm)
320 320 # forbid assigning same user group to itself
321 321 if target_user_group == user_group:
322 322 raise RepoGroupAssignmentError('target repo:%s cannot be '
323 323 'assigned to itself' % target_user_group)
324 324
325 325 # check if we have that permission already
326 326 obj = self.sa.query(UserGroupUserGroupToPerm) \
327 327 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group) \
328 328 .filter(UserGroupUserGroupToPerm.user_group == user_group) \
329 329 .scalar()
330 330 if obj is None:
331 331 # create new !
332 332 obj = UserGroupUserGroupToPerm()
333 333 obj.user_group = user_group
334 334 obj.target_user_group = target_user_group
335 335 obj.permission = permission
336 336 self.sa.add(obj)
337 337 log.debug('Granted perm %s to %s on %s', perm, target_user_group, user_group)
338 338 return obj
339 339
340 340 def revoke_user_group_permission(self, target_user_group, user_group):
341 341 """
342 342 Revoke user group permission for given target_user_group
343 343
344 344 :param target_user_group:
345 345 :param user_group:
346 346 """
347 347 target_user_group = self._get_user_group(target_user_group)
348 348 user_group = self._get_user_group(user_group)
349 349
350 350 obj = self.sa.query(UserGroupUserGroupToPerm) \
351 351 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group) \
352 352 .filter(UserGroupUserGroupToPerm.user_group == user_group) \
353 353 .scalar()
354 354 if obj is not None:
355 355 self.sa.delete(obj)
356 356 log.debug('Revoked perm on %s on %s', target_user_group, user_group)
357 357
358 358 def enforce_groups(self, user, groups, extern_type=None):
359 359 user = self._get_user(user)
360 360 log.debug('Enforcing groups %s on user %s', user, groups)
361 361 current_groups = user.group_member
362 362 # find the external created groups
363 363 externals = [x.users_group for x in current_groups
364 364 if 'extern_type' in x.users_group.group_data]
365 365
366 366 # calculate from what groups user should be removed
367 367 # externals that are not in groups
368 368 for gr in externals:
369 369 if gr.users_group_name not in groups:
370 370 log.debug('Removing user %s from user group %s', user, gr)
371 371 self.remove_user_from_group(gr, user)
372 372
373 373 # now we calculate in which groups user should be == groups params
374 374 owner = User.get_first_admin().username
375 375 for gr in set(groups):
376 376 existing_group = UserGroup.get_by_group_name(gr)
377 377 if not existing_group:
378 desc = 'Automatically created from plugin:%s' % extern_type
378 desc = u'Automatically created from plugin:%s' % extern_type
379 379 # we use first admin account to set the owner of the group
380 380 existing_group = UserGroupModel().create(gr, desc, owner,
381 381 group_data={'extern_type': extern_type})
382 382
383 383 # we can only add users to special groups created via plugins
384 384 managed = 'extern_type' in existing_group.group_data
385 385 if managed:
386 386 log.debug('Adding user %s to user group %s', user, gr)
387 387 UserGroupModel().add_user_to_group(existing_group, user)
388 388 else:
389 389 log.debug('Skipping addition to group %s since it is '
390 390 'not managed by auth plugins' % gr)
General Comments 0
You need to be logged in to leave comments. Login now