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