##// END OF EJS Templates
notifications: store notification status in channelstream
ergo -
r734:1eb83256 default
parent child Browse files
Show More
@@ -1,373 +1,371 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27
28 28 import formencode
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, session
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from sqlalchemy.orm import joinedload
34 34
35 35 from rhodecode import forms
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import auth
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.utils import jsonify
42 42 from rhodecode.lib.utils2 import safe_int, md5
43 43 from rhodecode.lib.ext_json import json
44 44
45 45 from rhodecode.model.validation_schema.schemas import user_schema
46 46 from rhodecode.model.db import (
47 47 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
48 48 UserFollowing)
49 49 from rhodecode.model.forms import UserForm
50 50 from rhodecode.model.scm import RepoList
51 51 from rhodecode.model.user import UserModel
52 52 from rhodecode.model.repo import RepoModel
53 53 from rhodecode.model.auth_token import AuthTokenModel
54 54 from rhodecode.model.meta import Session
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class MyAccountController(BaseController):
60 60 """REST Controller styled on the Atom Publishing Protocol"""
61 61 # To properly map this controller, ensure your config/routing.py
62 62 # file has a resource setup:
63 63 # map.resource('setting', 'settings', controller='admin/settings',
64 64 # path_prefix='/admin', name_prefix='admin_')
65 65
66 66 @LoginRequired()
67 67 @NotAnonymous()
68 68 def __before__(self):
69 69 super(MyAccountController, self).__before__()
70 70
71 71 def __load_data(self):
72 72 c.user = User.get(c.rhodecode_user.user_id)
73 73 if c.user.username == User.DEFAULT_USER:
74 74 h.flash(_("You can't edit this user since it's"
75 75 " crucial for entire application"), category='warning')
76 76 return redirect(url('users'))
77 77
78 78 def _load_my_repos_data(self, watched=False):
79 79 if watched:
80 80 admin = False
81 81 follows_repos = Session().query(UserFollowing)\
82 82 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
83 83 .options(joinedload(UserFollowing.follows_repository))\
84 84 .all()
85 85 repo_list = [x.follows_repository for x in follows_repos]
86 86 else:
87 87 admin = True
88 88 repo_list = Repository.get_all_repos(
89 89 user_id=c.rhodecode_user.user_id)
90 90 repo_list = RepoList(repo_list, perm_set=[
91 91 'repository.read', 'repository.write', 'repository.admin'])
92 92
93 93 repos_data = RepoModel().get_repos_as_dict(
94 94 repo_list=repo_list, admin=admin)
95 95 # json used to render the grid
96 96 return json.dumps(repos_data)
97 97
98 98 @auth.CSRFRequired()
99 99 def my_account_update(self):
100 100 """
101 101 POST /_admin/my_account Updates info of my account
102 102 """
103 103 # url('my_account')
104 104 c.active = 'profile_edit'
105 105 self.__load_data()
106 106 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
107 107 ip_addr=self.ip_addr)
108 108 c.extern_type = c.user.extern_type
109 109 c.extern_name = c.user.extern_name
110 110
111 111 defaults = c.user.get_dict()
112 112 update = False
113 113 _form = UserForm(edit=True,
114 114 old_data={'user_id': c.rhodecode_user.user_id,
115 115 'email': c.rhodecode_user.email})()
116 116 form_result = {}
117 117 try:
118 118 post_data = dict(request.POST)
119 119 post_data['new_password'] = ''
120 120 post_data['password_confirmation'] = ''
121 121 form_result = _form.to_python(post_data)
122 122 # skip updating those attrs for my account
123 123 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 124 'new_password', 'password_confirmation']
125 125 # TODO: plugin should define if username can be updated
126 126 if c.extern_type != "rhodecode":
127 127 # forbid updating username for external accounts
128 128 skip_attrs.append('username')
129 129
130 130 UserModel().update_user(
131 131 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
132 132 h.flash(_('Your account was updated successfully'),
133 133 category='success')
134 134 Session().commit()
135 135 update = True
136 136
137 137 except formencode.Invalid as errors:
138 138 return htmlfill.render(
139 139 render('admin/my_account/my_account.html'),
140 140 defaults=errors.value,
141 141 errors=errors.error_dict or {},
142 142 prefix_error=False,
143 143 encoding="UTF-8",
144 144 force_defaults=False)
145 145 except Exception:
146 146 log.exception("Exception updating user")
147 147 h.flash(_('Error occurred during update of user %s')
148 148 % form_result.get('username'), category='error')
149 149
150 150 if update:
151 151 return redirect('my_account')
152 152
153 153 return htmlfill.render(
154 154 render('admin/my_account/my_account.html'),
155 155 defaults=defaults,
156 156 encoding="UTF-8",
157 157 force_defaults=False
158 158 )
159 159
160 160 def my_account(self):
161 161 """
162 162 GET /_admin/my_account Displays info about my account
163 163 """
164 164 # url('my_account')
165 165 c.active = 'profile'
166 166 self.__load_data()
167 167
168 168 defaults = c.user.get_dict()
169 169 return htmlfill.render(
170 170 render('admin/my_account/my_account.html'),
171 171 defaults=defaults, encoding="UTF-8", force_defaults=False)
172 172
173 173 def my_account_edit(self):
174 174 """
175 175 GET /_admin/my_account/edit Displays edit form of my account
176 176 """
177 177 c.active = 'profile_edit'
178 178 self.__load_data()
179 179 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
180 180 ip_addr=self.ip_addr)
181 181 c.extern_type = c.user.extern_type
182 182 c.extern_name = c.user.extern_name
183 183
184 184 defaults = c.user.get_dict()
185 185 return htmlfill.render(
186 186 render('admin/my_account/my_account.html'),
187 187 defaults=defaults,
188 188 encoding="UTF-8",
189 189 force_defaults=False
190 190 )
191 191
192 192 @auth.CSRFRequired(except_methods=['GET'])
193 193 def my_account_password(self):
194 194 c.active = 'password'
195 195 self.__load_data()
196 196
197 197 schema = user_schema.ChangePasswordSchema().bind(
198 198 username=c.rhodecode_user.username)
199 199
200 200 form = forms.Form(schema,
201 201 buttons=(forms.buttons.save, forms.buttons.reset))
202 202
203 203 if request.method == 'POST':
204 204 controls = request.POST.items()
205 205 try:
206 206 valid_data = form.validate(controls)
207 207 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
208 208 instance = c.rhodecode_user.get_instance()
209 209 instance.update_userdata(force_password_change=False)
210 210 Session().commit()
211 211 except forms.ValidationFailure as e:
212 212 request.session.flash(
213 213 _('Error occurred during update of user password'),
214 214 queue='error')
215 215 form = e
216 216 except Exception:
217 217 log.exception("Exception updating password")
218 218 request.session.flash(
219 219 _('Error occurred during update of user password'),
220 220 queue='error')
221 221 else:
222 222 session.setdefault('rhodecode_user', {}).update(
223 223 {'password': md5(instance.password)})
224 224 session.save()
225 225 request.session.flash(
226 226 _("Successfully updated password"), queue='success')
227 227 return redirect(url('my_account_password'))
228 228
229 229 c.form = form
230 230 return render('admin/my_account/my_account.html')
231 231
232 232 def my_account_repos(self):
233 233 c.active = 'repos'
234 234 self.__load_data()
235 235
236 236 # json used to render the grid
237 237 c.data = self._load_my_repos_data()
238 238 return render('admin/my_account/my_account.html')
239 239
240 240 def my_account_watched(self):
241 241 c.active = 'watched'
242 242 self.__load_data()
243 243
244 244 # json used to render the grid
245 245 c.data = self._load_my_repos_data(watched=True)
246 246 return render('admin/my_account/my_account.html')
247 247
248 248 def my_account_perms(self):
249 249 c.active = 'perms'
250 250 self.__load_data()
251 251 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
252 252 ip_addr=self.ip_addr)
253 253
254 254 return render('admin/my_account/my_account.html')
255 255
256 256 def my_account_emails(self):
257 257 c.active = 'emails'
258 258 self.__load_data()
259 259
260 260 c.user_email_map = UserEmailMap.query()\
261 261 .filter(UserEmailMap.user == c.user).all()
262 262 return render('admin/my_account/my_account.html')
263 263
264 264 @auth.CSRFRequired()
265 265 def my_account_emails_add(self):
266 266 email = request.POST.get('new_email')
267 267
268 268 try:
269 269 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
270 270 Session().commit()
271 271 h.flash(_("Added new email address `%s` for user account") % email,
272 272 category='success')
273 273 except formencode.Invalid as error:
274 274 msg = error.error_dict['email']
275 275 h.flash(msg, category='error')
276 276 except Exception:
277 277 log.exception("Exception in my_account_emails")
278 278 h.flash(_('An error occurred during email saving'),
279 279 category='error')
280 280 return redirect(url('my_account_emails'))
281 281
282 282 @auth.CSRFRequired()
283 283 def my_account_emails_delete(self):
284 284 email_id = request.POST.get('del_email_id')
285 285 user_model = UserModel()
286 286 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
287 287 Session().commit()
288 288 h.flash(_("Removed email address from user account"),
289 289 category='success')
290 290 return redirect(url('my_account_emails'))
291 291
292 292 def my_account_pullrequests(self):
293 293 c.active = 'pullrequests'
294 294 self.__load_data()
295 295 c.show_closed = request.GET.get('pr_show_closed')
296 296
297 297 def _filter(pr):
298 298 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
299 299 if not c.show_closed:
300 300 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
301 301 return s
302 302
303 303 c.my_pull_requests = _filter(
304 304 PullRequest.query().filter(
305 305 PullRequest.user_id == c.rhodecode_user.user_id).all())
306 306 my_prs = [
307 307 x.pull_request for x in PullRequestReviewers.query().filter(
308 308 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
309 309 c.participate_in_pull_requests = _filter(my_prs)
310 310 return render('admin/my_account/my_account.html')
311 311
312 312 def my_account_auth_tokens(self):
313 313 c.active = 'auth_tokens'
314 314 self.__load_data()
315 315 show_expired = True
316 316 c.lifetime_values = [
317 317 (str(-1), _('forever')),
318 318 (str(5), _('5 minutes')),
319 319 (str(60), _('1 hour')),
320 320 (str(60 * 24), _('1 day')),
321 321 (str(60 * 24 * 30), _('1 month')),
322 322 ]
323 323 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
324 324 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
325 325 for x in AuthTokenModel.cls.ROLES]
326 326 c.role_options = [(c.role_values, _("Role"))]
327 327 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
328 328 c.rhodecode_user.user_id, show_expired=show_expired)
329 329 return render('admin/my_account/my_account.html')
330 330
331 331 @auth.CSRFRequired()
332 332 def my_account_auth_tokens_add(self):
333 333 lifetime = safe_int(request.POST.get('lifetime'), -1)
334 334 description = request.POST.get('description')
335 335 role = request.POST.get('role')
336 336 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
337 337 role)
338 338 Session().commit()
339 339 h.flash(_("Auth token successfully created"), category='success')
340 340 return redirect(url('my_account_auth_tokens'))
341 341
342 342 @auth.CSRFRequired()
343 343 def my_account_auth_tokens_delete(self):
344 344 auth_token = request.POST.get('del_auth_token')
345 345 user_id = c.rhodecode_user.user_id
346 346 if request.POST.get('del_auth_token_builtin'):
347 347 user = User.get(user_id)
348 348 if user:
349 349 user.api_key = generate_auth_token(user.username)
350 350 Session().add(user)
351 351 Session().commit()
352 352 h.flash(_("Auth token successfully reset"), category='success')
353 353 elif auth_token:
354 354 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
355 355 Session().commit()
356 356 h.flash(_("Auth token successfully deleted"), category='success')
357 357
358 358 return redirect(url('my_account_auth_tokens'))
359 359
360 360 def my_notifications(self):
361 361 c.active = 'notifications'
362 362 return render('admin/my_account/my_account.html')
363 363
364 364 @auth.CSRFRequired()
365 365 @jsonify
366 366 def my_notifications_toggle_visibility(self):
367 367 user = c.rhodecode_user.get_instance()
368 user_data = user.user_data
369 status = user_data.get('notification_status', False)
370 user_data['notification_status'] = not status
371 user.user_data = user_data
368 new_status = not user.user_data.get('notification_status', True)
369 user.update_userdata(notification_status=new_status)
372 370 Session().commit()
373 return user_data['notification_status']
371 return user.user_data['notification_status']
@@ -1,219 +1,220 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import os
23 23
24 24 import itsdangerous
25 25 import requests
26 26
27 27 from dogpile.core import ReadWriteMutex
28 28
29 29 import rhodecode.lib.helpers as h
30 30
31 31 from rhodecode.lib.auth import HasRepoPermissionAny
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.model.db import User
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37 LOCK = ReadWriteMutex()
38 38
39 39 STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
40 40 'icon_link', 'display_name', 'display_link']
41 41
42 42
43 43 class ChannelstreamException(Exception):
44 44 pass
45 45
46 46
47 47 class ChannelstreamConnectionException(Exception):
48 48 pass
49 49
50 50
51 51 class ChannelstreamPermissionException(Exception):
52 52 pass
53 53
54 54
55 55 def channelstream_request(config, payload, endpoint, raise_exc=True):
56 56 signer = itsdangerous.TimestampSigner(config['secret'])
57 57 sig_for_server = signer.sign(endpoint)
58 58 secret_headers = {'x-channelstream-secret': sig_for_server,
59 59 'x-channelstream-endpoint': endpoint,
60 60 'Content-Type': 'application/json'}
61 61 req_url = 'http://{}{}'.format(config['server'], endpoint)
62 62 response = None
63 63 try:
64 64 response = requests.post(req_url, data=json.dumps(payload),
65 65 headers=secret_headers).json()
66 66 except requests.ConnectionError:
67 67 log.exception('ConnectionError happened')
68 68 if raise_exc:
69 69 raise ChannelstreamConnectionException()
70 70 except Exception:
71 71 log.exception('Exception related to channelstream happened')
72 72 if raise_exc:
73 73 raise ChannelstreamConnectionException()
74 74 return response
75 75
76 76
77 77 def get_user_data(user_id):
78 78 user = User.get(user_id)
79 79 return {
80 80 'id': user.user_id,
81 81 'username': user.username,
82 82 'first_name': user.name,
83 83 'last_name': user.lastname,
84 84 'icon_link': h.gravatar_url(user.email, 14),
85 85 'display_name': h.person(user, 'username_or_name_or_email'),
86 86 'display_link': h.link_to_user(user),
87 'notifications': user.user_data.get('notification_status', True)
87 88 }
88 89
89 90
90 91 def broadcast_validator(channel_name):
91 92 """ checks if user can access the broadcast channel """
92 93 if channel_name == 'broadcast':
93 94 return True
94 95
95 96
96 97 def repo_validator(channel_name):
97 98 """ checks if user can access the broadcast channel """
98 99 channel_prefix = '/repo$'
99 100 if channel_name.startswith(channel_prefix):
100 101 elements = channel_name[len(channel_prefix):].split('$')
101 102 repo_name = elements[0]
102 103 can_access = HasRepoPermissionAny(
103 104 'repository.read',
104 105 'repository.write',
105 106 'repository.admin')(repo_name)
106 107 log.debug('permission check for {} channel '
107 108 'resulted in {}'.format(repo_name, can_access))
108 109 if can_access:
109 110 return True
110 111 return False
111 112
112 113
113 114 def check_channel_permissions(channels, plugin_validators, should_raise=True):
114 115 valid_channels = []
115 116
116 117 validators = [broadcast_validator, repo_validator]
117 118 if plugin_validators:
118 119 validators.extend(plugin_validators)
119 120 for channel_name in channels:
120 121 is_valid = False
121 122 for validator in validators:
122 123 if validator(channel_name):
123 124 is_valid = True
124 125 break
125 126 if is_valid:
126 127 valid_channels.append(channel_name)
127 128 else:
128 129 if should_raise:
129 130 raise ChannelstreamPermissionException()
130 131 return valid_channels
131 132
132 133
133 134 def get_channels_info(self, channels):
134 135 payload = {'channels': channels}
135 136 # gather persistence info
136 137 return channelstream_request(self._config(), payload, '/info')
137 138
138 139
139 140 def parse_channels_info(info_result, include_channel_info=None):
140 141 """
141 142 Returns data that contains only secure information that can be
142 143 presented to clients
143 144 """
144 145 include_channel_info = include_channel_info or []
145 146
146 147 user_state_dict = {}
147 148 for userinfo in info_result['users']:
148 149 user_state_dict[userinfo['user']] = {
149 150 k: v for k, v in userinfo['state'].items()
150 151 if k in STATE_PUBLIC_KEYS
151 152 }
152 153
153 154 channels_info = {}
154 155
155 156 for c_name, c_info in info_result['channels'].items():
156 157 if c_name not in include_channel_info:
157 158 continue
158 159 connected_list = []
159 160 for userinfo in c_info['users']:
160 161 connected_list.append({
161 162 'user': userinfo['user'],
162 163 'state': user_state_dict[userinfo['user']]
163 164 })
164 165 channels_info[c_name] = {'users': connected_list,
165 166 'history': c_info['history']}
166 167
167 168 return channels_info
168 169
169 170
170 171 def log_filepath(history_location, channel_name):
171 172 filename = '{}.log'.format(channel_name.encode('hex'))
172 173 filepath = os.path.join(history_location, filename)
173 174 return filepath
174 175
175 176
176 177 def read_history(history_location, channel_name):
177 178 filepath = log_filepath(history_location, channel_name)
178 179 if not os.path.exists(filepath):
179 180 return []
180 181 history_lines_limit = -100
181 182 history = []
182 183 with open(filepath, 'rb') as f:
183 184 for line in f.readlines()[history_lines_limit:]:
184 185 try:
185 186 history.append(json.loads(line))
186 187 except Exception:
187 188 log.exception('Failed to load history')
188 189 return history
189 190
190 191
191 192 def update_history_from_logs(config, channels, payload):
192 193 history_location = config.get('history.location')
193 194 for channel in channels:
194 195 history = read_history(history_location, channel)
195 196 payload['channels_info'][channel]['history'] = history
196 197
197 198
198 199 def write_history(config, message):
199 200 """ writes a messge to a base64encoded filename """
200 201 history_location = config.get('history.location')
201 202 if not os.path.exists(history_location):
202 203 return
203 204 try:
204 205 LOCK.acquire_write_lock()
205 206 filepath = log_filepath(history_location, message['channel'])
206 207 with open(filepath, 'ab') as f:
207 208 json.dump(message, f)
208 209 f.write('\n')
209 210 finally:
210 211 LOCK.release_write_lock()
211 212
212 213
213 214 def get_connection_validators(registry):
214 215 validators = []
215 216 for k, config in registry.rhodecode_plugins.iteritems():
216 217 validator = config.get('channelstream', {}).get('connect_validator')
217 218 if validator:
218 219 validators.append(validator)
219 220 return validators
@@ -1,838 +1,839 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode import events
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict)
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.db import (
43 43 User, UserToPerm, UserEmailMap, UserIpMap)
44 44 from rhodecode.lib.exceptions import (
45 45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(FromCache("sql_cache_short",
61 61 "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 80 return User.get_by_email(email, case_insensitive, cache)
81 81
82 82 def get_by_auth_token(self, auth_token, cache=False):
83 83 return User.get_by_auth_token(auth_token, cache)
84 84
85 85 def get_active_user_count(self, cache=False):
86 86 return User.query().filter(
87 87 User.active == True).filter(
88 88 User.username != User.DEFAULT_USER).count()
89 89
90 90 def create(self, form_data, cur_user=None):
91 91 if not cur_user:
92 92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 93
94 94 user_data = {
95 95 'username': form_data['username'],
96 96 'password': form_data['password'],
97 97 'email': form_data['email'],
98 98 'firstname': form_data['firstname'],
99 99 'lastname': form_data['lastname'],
100 100 'active': form_data['active'],
101 101 'extern_type': form_data['extern_type'],
102 102 'extern_name': form_data['extern_name'],
103 103 'admin': False,
104 104 'cur_user': cur_user
105 105 }
106 106
107 107 try:
108 108 if form_data.get('create_repo_group'):
109 109 user_data['create_repo_group'] = True
110 110 if form_data.get('password_change'):
111 111 user_data['force_password_change'] = True
112 112
113 113 return UserModel().create_or_update(**user_data)
114 114 except Exception:
115 115 log.error(traceback.format_exc())
116 116 raise
117 117
118 118 def update_user(self, user, skip_attrs=None, **kwargs):
119 119 from rhodecode.lib.auth import get_crypt_password
120 120
121 121 user = self._get_user(user)
122 122 if user.username == User.DEFAULT_USER:
123 123 raise DefaultUserException(
124 124 _("You can't Edit this user since it's"
125 125 " crucial for entire application"))
126 126
127 127 # first store only defaults
128 128 user_attrs = {
129 129 'updating_user_id': user.user_id,
130 130 'username': user.username,
131 131 'password': user.password,
132 132 'email': user.email,
133 133 'firstname': user.name,
134 134 'lastname': user.lastname,
135 135 'active': user.active,
136 136 'admin': user.admin,
137 137 'extern_name': user.extern_name,
138 138 'extern_type': user.extern_type,
139 139 'language': user.user_data.get('language')
140 140 }
141 141
142 142 # in case there's new_password, that comes from form, use it to
143 143 # store password
144 144 if kwargs.get('new_password'):
145 145 kwargs['password'] = kwargs['new_password']
146 146
147 147 # cleanups, my_account password change form
148 148 kwargs.pop('current_password', None)
149 149 kwargs.pop('new_password', None)
150 150
151 151 # cleanups, user edit password change form
152 152 kwargs.pop('password_confirmation', None)
153 153 kwargs.pop('password_change', None)
154 154
155 155 # create repo group on user creation
156 156 kwargs.pop('create_repo_group', None)
157 157
158 158 # legacy forms send name, which is the firstname
159 159 firstname = kwargs.pop('name', None)
160 160 if firstname:
161 161 kwargs['firstname'] = firstname
162 162
163 163 for k, v in kwargs.items():
164 164 # skip if we don't want to update this
165 165 if skip_attrs and k in skip_attrs:
166 166 continue
167 167
168 168 user_attrs[k] = v
169 169
170 170 try:
171 171 return self.create_or_update(**user_attrs)
172 172 except Exception:
173 173 log.error(traceback.format_exc())
174 174 raise
175 175
176 176 def create_or_update(
177 177 self, username, password, email, firstname='', lastname='',
178 178 active=True, admin=False, extern_type=None, extern_name=None,
179 179 cur_user=None, plugin=None, force_password_change=False,
180 180 allow_to_create_user=True, create_repo_group=False,
181 181 updating_user_id=None, language=None, strict_creation_check=True):
182 182 """
183 183 Creates a new instance if not found, or updates current one
184 184
185 185 :param username:
186 186 :param password:
187 187 :param email:
188 188 :param firstname:
189 189 :param lastname:
190 190 :param active:
191 191 :param admin:
192 192 :param extern_type:
193 193 :param extern_name:
194 194 :param cur_user:
195 195 :param plugin: optional plugin this method was called from
196 196 :param force_password_change: toggles new or existing user flag
197 197 for password change
198 198 :param allow_to_create_user: Defines if the method can actually create
199 199 new users
200 200 :param create_repo_group: Defines if the method should also
201 201 create an repo group with user name, and owner
202 202 :param updating_user_id: if we set it up this is the user we want to
203 203 update this allows to editing username.
204 204 :param language: language of user from interface.
205 205
206 206 :returns: new User object with injected `is_new_user` attribute.
207 207 """
208 208 if not cur_user:
209 209 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
210 210
211 211 from rhodecode.lib.auth import (
212 212 get_crypt_password, check_password, generate_auth_token)
213 213 from rhodecode.lib.hooks_base import (
214 214 log_create_user, check_allowed_create_user)
215 215
216 216 def _password_change(new_user, password):
217 217 # empty password
218 218 if not new_user.password:
219 219 return False
220 220
221 221 # password check is only needed for RhodeCode internal auth calls
222 222 # in case it's a plugin we don't care
223 223 if not plugin:
224 224
225 225 # first check if we gave crypted password back, and if it matches
226 226 # it's not password change
227 227 if new_user.password == password:
228 228 return False
229 229
230 230 password_match = check_password(password, new_user.password)
231 231 if not password_match:
232 232 return True
233 233
234 234 return False
235 235
236 236 user_data = {
237 237 'username': username,
238 238 'password': password,
239 239 'email': email,
240 240 'firstname': firstname,
241 241 'lastname': lastname,
242 242 'active': active,
243 243 'admin': admin
244 244 }
245 245
246 246 if updating_user_id:
247 247 log.debug('Checking for existing account in RhodeCode '
248 248 'database with user_id `%s` ' % (updating_user_id,))
249 249 user = User.get(updating_user_id)
250 250 else:
251 251 log.debug('Checking for existing account in RhodeCode '
252 252 'database with username `%s` ' % (username,))
253 253 user = User.get_by_username(username, case_insensitive=True)
254 254
255 255 if user is None:
256 256 # we check internal flag if this method is actually allowed to
257 257 # create new user
258 258 if not allow_to_create_user:
259 259 msg = ('Method wants to create new user, but it is not '
260 260 'allowed to do so')
261 261 log.warning(msg)
262 262 raise NotAllowedToCreateUserError(msg)
263 263
264 264 log.debug('Creating new user %s', username)
265 265
266 266 # only if we create user that is active
267 267 new_active_user = active
268 268 if new_active_user and strict_creation_check:
269 269 # raises UserCreationError if it's not allowed for any reason to
270 270 # create new active user, this also executes pre-create hooks
271 271 check_allowed_create_user(user_data, cur_user, strict_check=True)
272 272 events.trigger(events.UserPreCreate(user_data))
273 273 new_user = User()
274 274 edit = False
275 275 else:
276 276 log.debug('updating user %s', username)
277 277 events.trigger(events.UserPreUpdate(user, user_data))
278 278 new_user = user
279 279 edit = True
280 280
281 281 # we're not allowed to edit default user
282 282 if user.username == User.DEFAULT_USER:
283 283 raise DefaultUserException(
284 284 _("You can't edit this user (`%(username)s`) since it's "
285 285 "crucial for entire application") % {'username': user.username})
286 286
287 287 # inject special attribute that will tell us if User is new or old
288 288 new_user.is_new_user = not edit
289 289 # for users that didn's specify auth type, we use RhodeCode built in
290 290 from rhodecode.authentication.plugins import auth_rhodecode
291 291 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
292 292 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
293 293
294 294 try:
295 295 new_user.username = username
296 296 new_user.admin = admin
297 297 new_user.email = email
298 298 new_user.active = active
299 299 new_user.extern_name = safe_unicode(extern_name)
300 300 new_user.extern_type = safe_unicode(extern_type)
301 301 new_user.name = firstname
302 302 new_user.lastname = lastname
303 303
304 304 if not edit:
305 305 new_user.api_key = generate_auth_token(username)
306 306
307 307 # set password only if creating an user or password is changed
308 308 if not edit or _password_change(new_user, password):
309 309 reason = 'new password' if edit else 'new user'
310 310 log.debug('Updating password reason=>%s', reason)
311 311 new_user.password = get_crypt_password(password) if password else None
312 312
313 313 if force_password_change:
314 314 new_user.update_userdata(force_password_change=True)
315 315 if language:
316 316 new_user.update_userdata(language=language)
317 new_user.update_userdata(notification_status=True)
317 318
318 319 self.sa.add(new_user)
319 320
320 321 if not edit and create_repo_group:
321 322 # create new group same as username, and make this user an owner
322 323 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {'username': username}
323 324 RepoGroupModel().create(group_name=username,
324 325 group_description=desc,
325 326 owner=username, commit_early=False)
326 327 if not edit:
327 328 # add the RSS token
328 329 AuthTokenModel().create(username,
329 330 description='Generated feed token',
330 331 role=AuthTokenModel.cls.ROLE_FEED)
331 332 log_create_user(created_by=cur_user, **new_user.get_dict())
332 333 return new_user
333 334 except (DatabaseError,):
334 335 log.error(traceback.format_exc())
335 336 raise
336 337
337 338 def create_registration(self, form_data):
338 339 from rhodecode.model.notification import NotificationModel
339 340 from rhodecode.model.notification import EmailNotificationModel
340 341
341 342 try:
342 343 form_data['admin'] = False
343 344 form_data['extern_name'] = 'rhodecode'
344 345 form_data['extern_type'] = 'rhodecode'
345 346 new_user = self.create(form_data)
346 347
347 348 self.sa.add(new_user)
348 349 self.sa.flush()
349 350
350 351 user_data = new_user.get_dict()
351 352 kwargs = {
352 353 # use SQLALCHEMY safe dump of user data
353 354 'user': AttributeDict(user_data),
354 355 'date': datetime.datetime.now()
355 356 }
356 357 notification_type = EmailNotificationModel.TYPE_REGISTRATION
357 358 # pre-generate the subject for notification itself
358 359 (subject,
359 360 _h, _e, # we don't care about those
360 361 body_plaintext) = EmailNotificationModel().render_email(
361 362 notification_type, **kwargs)
362 363
363 364 # create notification objects, and emails
364 365 NotificationModel().create(
365 366 created_by=new_user,
366 367 notification_subject=subject,
367 368 notification_body=body_plaintext,
368 369 notification_type=notification_type,
369 370 recipients=None, # all admins
370 371 email_kwargs=kwargs,
371 372 )
372 373
373 374 return new_user
374 375 except Exception:
375 376 log.error(traceback.format_exc())
376 377 raise
377 378
378 379 def _handle_user_repos(self, username, repositories, handle_mode=None):
379 380 _superadmin = self.cls.get_first_super_admin()
380 381 left_overs = True
381 382
382 383 from rhodecode.model.repo import RepoModel
383 384
384 385 if handle_mode == 'detach':
385 386 for obj in repositories:
386 387 obj.user = _superadmin
387 388 # set description we know why we super admin now owns
388 389 # additional repositories that were orphaned !
389 390 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
390 391 self.sa.add(obj)
391 392 left_overs = False
392 393 elif handle_mode == 'delete':
393 394 for obj in repositories:
394 395 RepoModel().delete(obj, forks='detach')
395 396 left_overs = False
396 397
397 398 # if nothing is done we have left overs left
398 399 return left_overs
399 400
400 401 def _handle_user_repo_groups(self, username, repository_groups,
401 402 handle_mode=None):
402 403 _superadmin = self.cls.get_first_super_admin()
403 404 left_overs = True
404 405
405 406 from rhodecode.model.repo_group import RepoGroupModel
406 407
407 408 if handle_mode == 'detach':
408 409 for r in repository_groups:
409 410 r.user = _superadmin
410 411 # set description we know why we super admin now owns
411 412 # additional repositories that were orphaned !
412 413 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
413 414 self.sa.add(r)
414 415 left_overs = False
415 416 elif handle_mode == 'delete':
416 417 for r in repository_groups:
417 418 RepoGroupModel().delete(r)
418 419 left_overs = False
419 420
420 421 # if nothing is done we have left overs left
421 422 return left_overs
422 423
423 424 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
424 425 _superadmin = self.cls.get_first_super_admin()
425 426 left_overs = True
426 427
427 428 from rhodecode.model.user_group import UserGroupModel
428 429
429 430 if handle_mode == 'detach':
430 431 for r in user_groups:
431 432 for user_user_group_to_perm in r.user_user_group_to_perm:
432 433 if user_user_group_to_perm.user.username == username:
433 434 user_user_group_to_perm.user = _superadmin
434 435 r.user = _superadmin
435 436 # set description we know why we super admin now owns
436 437 # additional repositories that were orphaned !
437 438 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
438 439 self.sa.add(r)
439 440 left_overs = False
440 441 elif handle_mode == 'delete':
441 442 for r in user_groups:
442 443 UserGroupModel().delete(r)
443 444 left_overs = False
444 445
445 446 # if nothing is done we have left overs left
446 447 return left_overs
447 448
448 449 def delete(self, user, cur_user=None, handle_repos=None,
449 450 handle_repo_groups=None, handle_user_groups=None):
450 451 if not cur_user:
451 452 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
452 453 user = self._get_user(user)
453 454
454 455 try:
455 456 if user.username == User.DEFAULT_USER:
456 457 raise DefaultUserException(
457 458 _(u"You can't remove this user since it's"
458 459 u" crucial for entire application"))
459 460
460 461 left_overs = self._handle_user_repos(
461 462 user.username, user.repositories, handle_repos)
462 463 if left_overs and user.repositories:
463 464 repos = [x.repo_name for x in user.repositories]
464 465 raise UserOwnsReposException(
465 466 _(u'user "%s" still owns %s repositories and cannot be '
466 467 u'removed. Switch owners or remove those repositories:%s')
467 468 % (user.username, len(repos), ', '.join(repos)))
468 469
469 470 left_overs = self._handle_user_repo_groups(
470 471 user.username, user.repository_groups, handle_repo_groups)
471 472 if left_overs and user.repository_groups:
472 473 repo_groups = [x.group_name for x in user.repository_groups]
473 474 raise UserOwnsRepoGroupsException(
474 475 _(u'user "%s" still owns %s repository groups and cannot be '
475 476 u'removed. Switch owners or remove those repository groups:%s')
476 477 % (user.username, len(repo_groups), ', '.join(repo_groups)))
477 478
478 479 left_overs = self._handle_user_user_groups(
479 480 user.username, user.user_groups, handle_user_groups)
480 481 if left_overs and user.user_groups:
481 482 user_groups = [x.users_group_name for x in user.user_groups]
482 483 raise UserOwnsUserGroupsException(
483 484 _(u'user "%s" still owns %s user groups and cannot be '
484 485 u'removed. Switch owners or remove those user groups:%s')
485 486 % (user.username, len(user_groups), ', '.join(user_groups)))
486 487
487 488 # we might change the user data with detach/delete, make sure
488 489 # the object is marked as expired before actually deleting !
489 490 self.sa.expire(user)
490 491 self.sa.delete(user)
491 492 from rhodecode.lib.hooks_base import log_delete_user
492 493 log_delete_user(deleted_by=cur_user, **user.get_dict())
493 494 except Exception:
494 495 log.error(traceback.format_exc())
495 496 raise
496 497
497 498 def reset_password_link(self, data, pwd_reset_url):
498 499 from rhodecode.lib.celerylib import tasks, run_task
499 500 from rhodecode.model.notification import EmailNotificationModel
500 501 user_email = data['email']
501 502 try:
502 503 user = User.get_by_email(user_email)
503 504 if user:
504 505 log.debug('password reset user found %s', user)
505 506
506 507 email_kwargs = {
507 508 'password_reset_url': pwd_reset_url,
508 509 'user': user,
509 510 'email': user_email,
510 511 'date': datetime.datetime.now()
511 512 }
512 513
513 514 (subject, headers, email_body,
514 515 email_body_plaintext) = EmailNotificationModel().render_email(
515 516 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
516 517
517 518 recipients = [user_email]
518 519
519 520 action_logger_generic(
520 521 'sending password reset email to user: {}'.format(
521 522 user), namespace='security.password_reset')
522 523
523 524 run_task(tasks.send_email, recipients, subject,
524 525 email_body_plaintext, email_body)
525 526
526 527 else:
527 528 log.debug("password reset email %s not found", user_email)
528 529 except Exception:
529 530 log.error(traceback.format_exc())
530 531 return False
531 532
532 533 return True
533 534
534 535 def reset_password(self, data, pwd_reset_url):
535 536 from rhodecode.lib.celerylib import tasks, run_task
536 537 from rhodecode.model.notification import EmailNotificationModel
537 538 from rhodecode.lib import auth
538 539 user_email = data['email']
539 540 pre_db = True
540 541 try:
541 542 user = User.get_by_email(user_email)
542 543 new_passwd = auth.PasswordGenerator().gen_password(
543 544 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
544 545 if user:
545 546 user.password = auth.get_crypt_password(new_passwd)
546 547 # also force this user to reset his password !
547 548 user.update_userdata(force_password_change=True)
548 549
549 550 Session().add(user)
550 551 Session().commit()
551 552 log.info('change password for %s', user_email)
552 553 if new_passwd is None:
553 554 raise Exception('unable to generate new password')
554 555
555 556 pre_db = False
556 557
557 558 email_kwargs = {
558 559 'new_password': new_passwd,
559 560 'password_reset_url': pwd_reset_url,
560 561 'user': user,
561 562 'email': user_email,
562 563 'date': datetime.datetime.now()
563 564 }
564 565
565 566 (subject, headers, email_body,
566 567 email_body_plaintext) = EmailNotificationModel().render_email(
567 568 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, **email_kwargs)
568 569
569 570 recipients = [user_email]
570 571
571 572 action_logger_generic(
572 573 'sent new password to user: {} with email: {}'.format(
573 574 user, user_email), namespace='security.password_reset')
574 575
575 576 run_task(tasks.send_email, recipients, subject,
576 577 email_body_plaintext, email_body)
577 578
578 579 except Exception:
579 580 log.error('Failed to update user password')
580 581 log.error(traceback.format_exc())
581 582 if pre_db:
582 583 # we rollback only if local db stuff fails. If it goes into
583 584 # run_task, we're pass rollback state this wouldn't work then
584 585 Session().rollback()
585 586
586 587 return True
587 588
588 589 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
589 590 """
590 591 Fetches auth_user by user_id,or api_key if present.
591 592 Fills auth_user attributes with those taken from database.
592 593 Additionally set's is_authenitated if lookup fails
593 594 present in database
594 595
595 596 :param auth_user: instance of user to set attributes
596 597 :param user_id: user id to fetch by
597 598 :param api_key: api key to fetch by
598 599 :param username: username to fetch by
599 600 """
600 601 if user_id is None and api_key is None and username is None:
601 602 raise Exception('You need to pass user_id, api_key or username')
602 603
603 604 log.debug(
604 605 'doing fill data based on: user_id:%s api_key:%s username:%s',
605 606 user_id, api_key, username)
606 607 try:
607 608 dbuser = None
608 609 if user_id:
609 610 dbuser = self.get(user_id)
610 611 elif api_key:
611 612 dbuser = self.get_by_auth_token(api_key)
612 613 elif username:
613 614 dbuser = self.get_by_username(username)
614 615
615 616 if not dbuser:
616 617 log.warning(
617 618 'Unable to lookup user by id:%s api_key:%s username:%s',
618 619 user_id, api_key, username)
619 620 return False
620 621 if not dbuser.active:
621 622 log.debug('User `%s` is inactive, skipping fill data', username)
622 623 return False
623 624
624 625 log.debug('filling user:%s data', dbuser)
625 626
626 627 # TODO: johbo: Think about this and find a clean solution
627 628 user_data = dbuser.get_dict()
628 629 user_data.update(dbuser.get_api_data(include_secrets=True))
629 630
630 631 for k, v in user_data.iteritems():
631 632 # properties of auth user we dont update
632 633 if k not in ['auth_tokens', 'permissions']:
633 634 setattr(auth_user, k, v)
634 635
635 636 # few extras
636 637 setattr(auth_user, 'feed_token', dbuser.feed_token)
637 638 except Exception:
638 639 log.error(traceback.format_exc())
639 640 auth_user.is_authenticated = False
640 641 return False
641 642
642 643 return True
643 644
644 645 def has_perm(self, user, perm):
645 646 perm = self._get_perm(perm)
646 647 user = self._get_user(user)
647 648
648 649 return UserToPerm.query().filter(UserToPerm.user == user)\
649 650 .filter(UserToPerm.permission == perm).scalar() is not None
650 651
651 652 def grant_perm(self, user, perm):
652 653 """
653 654 Grant user global permissions
654 655
655 656 :param user:
656 657 :param perm:
657 658 """
658 659 user = self._get_user(user)
659 660 perm = self._get_perm(perm)
660 661 # if this permission is already granted skip it
661 662 _perm = UserToPerm.query()\
662 663 .filter(UserToPerm.user == user)\
663 664 .filter(UserToPerm.permission == perm)\
664 665 .scalar()
665 666 if _perm:
666 667 return
667 668 new = UserToPerm()
668 669 new.user = user
669 670 new.permission = perm
670 671 self.sa.add(new)
671 672 return new
672 673
673 674 def revoke_perm(self, user, perm):
674 675 """
675 676 Revoke users global permissions
676 677
677 678 :param user:
678 679 :param perm:
679 680 """
680 681 user = self._get_user(user)
681 682 perm = self._get_perm(perm)
682 683
683 684 obj = UserToPerm.query()\
684 685 .filter(UserToPerm.user == user)\
685 686 .filter(UserToPerm.permission == perm)\
686 687 .scalar()
687 688 if obj:
688 689 self.sa.delete(obj)
689 690
690 691 def add_extra_email(self, user, email):
691 692 """
692 693 Adds email address to UserEmailMap
693 694
694 695 :param user:
695 696 :param email:
696 697 """
697 698 from rhodecode.model import forms
698 699 form = forms.UserExtraEmailForm()()
699 700 data = form.to_python({'email': email})
700 701 user = self._get_user(user)
701 702
702 703 obj = UserEmailMap()
703 704 obj.user = user
704 705 obj.email = data['email']
705 706 self.sa.add(obj)
706 707 return obj
707 708
708 709 def delete_extra_email(self, user, email_id):
709 710 """
710 711 Removes email address from UserEmailMap
711 712
712 713 :param user:
713 714 :param email_id:
714 715 """
715 716 user = self._get_user(user)
716 717 obj = UserEmailMap.query().get(email_id)
717 718 if obj:
718 719 self.sa.delete(obj)
719 720
720 721 def parse_ip_range(self, ip_range):
721 722 ip_list = []
722 723 def make_unique(value):
723 724 seen = []
724 725 return [c for c in value if not (c in seen or seen.append(c))]
725 726
726 727 # firsts split by commas
727 728 for ip_range in ip_range.split(','):
728 729 if not ip_range:
729 730 continue
730 731 ip_range = ip_range.strip()
731 732 if '-' in ip_range:
732 733 start_ip, end_ip = ip_range.split('-', 1)
733 734 start_ip = ipaddress.ip_address(start_ip.strip())
734 735 end_ip = ipaddress.ip_address(end_ip.strip())
735 736 parsed_ip_range = []
736 737
737 738 for index in xrange(int(start_ip), int(end_ip) + 1):
738 739 new_ip = ipaddress.ip_address(index)
739 740 parsed_ip_range.append(str(new_ip))
740 741 ip_list.extend(parsed_ip_range)
741 742 else:
742 743 ip_list.append(ip_range)
743 744
744 745 return make_unique(ip_list)
745 746
746 747 def add_extra_ip(self, user, ip, description=None):
747 748 """
748 749 Adds ip address to UserIpMap
749 750
750 751 :param user:
751 752 :param ip:
752 753 """
753 754 from rhodecode.model import forms
754 755 form = forms.UserExtraIpForm()()
755 756 data = form.to_python({'ip': ip})
756 757 user = self._get_user(user)
757 758
758 759 obj = UserIpMap()
759 760 obj.user = user
760 761 obj.ip_addr = data['ip']
761 762 obj.description = description
762 763 self.sa.add(obj)
763 764 return obj
764 765
765 766 def delete_extra_ip(self, user, ip_id):
766 767 """
767 768 Removes ip address from UserIpMap
768 769
769 770 :param user:
770 771 :param ip_id:
771 772 """
772 773 user = self._get_user(user)
773 774 obj = UserIpMap.query().get(ip_id)
774 775 if obj:
775 776 self.sa.delete(obj)
776 777
777 778 def get_accounts_in_creation_order(self, current_user=None):
778 779 """
779 780 Get accounts in order of creation for deactivation for license limits
780 781
781 782 pick currently logged in user, and append to the list in position 0
782 783 pick all super-admins in order of creation date and add it to the list
783 784 pick all other accounts in order of creation and add it to the list.
784 785
785 786 Based on that list, the last accounts can be disabled as they are
786 787 created at the end and don't include any of the super admins as well
787 788 as the current user.
788 789
789 790 :param current_user: optionally current user running this operation
790 791 """
791 792
792 793 if not current_user:
793 794 current_user = get_current_rhodecode_user()
794 795 active_super_admins = [
795 796 x.user_id for x in User.query()
796 797 .filter(User.user_id != current_user.user_id)
797 798 .filter(User.active == true())
798 799 .filter(User.admin == true())
799 800 .order_by(User.created_on.asc())]
800 801
801 802 active_regular_users = [
802 803 x.user_id for x in User.query()
803 804 .filter(User.user_id != current_user.user_id)
804 805 .filter(User.active == true())
805 806 .filter(User.admin == false())
806 807 .order_by(User.created_on.asc())]
807 808
808 809 list_of_accounts = [current_user.user_id]
809 810 list_of_accounts += active_super_admins
810 811 list_of_accounts += active_regular_users
811 812
812 813 return list_of_accounts
813 814
814 815 def deactivate_last_users(self, expected_users):
815 816 """
816 817 Deactivate accounts that are over the license limits.
817 818 Algorithm of which accounts to disabled is based on the formula:
818 819
819 820 Get current user, then super admins in creation order, then regular
820 821 active users in creation order.
821 822
822 823 Using that list we mark all accounts from the end of it as inactive.
823 824 This way we block only latest created accounts.
824 825
825 826 :param expected_users: list of users in special order, we deactivate
826 827 the end N ammoun of users from that list
827 828 """
828 829
829 830 list_of_accounts = self.get_accounts_in_creation_order()
830 831
831 832 for acc_id in list_of_accounts[expected_users + 1:]:
832 833 user = User.get(acc_id)
833 834 log.info('Deactivating account %s for license unlock', user)
834 835 user.active = False
835 836 Session().add(user)
836 837 Session().commit()
837 838
838 839 return
General Comments 0
You need to be logged in to leave comments. Login now