##// END OF EJS Templates
auth: disable password change form for accounts that are not managed by RhodeCode....
marcink -
r1275:b650c2de default
parent child Browse files
Show More
@@ -1,467 +1,468 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 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 import datetime
28 28
29 29 import formencode
30 30 from formencode import htmlfill
31 31 from pyramid.threadlocal import get_current_registry
32 32 from pylons import request, tmpl_context as c, url, session
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from sqlalchemy.orm import joinedload
36 36 from webob.exc import HTTPBadGateway
37 37
38 38 from rhodecode import forms
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib import auth
41 41 from rhodecode.lib.auth import (
42 42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils import jsonify
45 45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
46 46 from rhodecode.lib.ext_json import json
47 47 from rhodecode.lib.channelstream import channelstream_request, \
48 48 ChannelstreamException
49 49
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51 from rhodecode.model.db import (
52 52 Repository, PullRequest, UserEmailMap, User, UserFollowing)
53 53 from rhodecode.model.forms import UserForm
54 54 from rhodecode.model.scm import RepoList
55 55 from rhodecode.model.user import UserModel
56 56 from rhodecode.model.repo import RepoModel
57 57 from rhodecode.model.auth_token import AuthTokenModel
58 58 from rhodecode.model.meta import Session
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.comment import ChangesetCommentsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 class MyAccountController(BaseController):
66 66 """REST Controller styled on the Atom Publishing Protocol"""
67 67 # To properly map this controller, ensure your config/routing.py
68 68 # file has a resource setup:
69 69 # map.resource('setting', 'settings', controller='admin/settings',
70 70 # path_prefix='/admin', name_prefix='admin_')
71 71
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 def __before__(self):
75 75 super(MyAccountController, self).__before__()
76 76
77 77 def __load_data(self):
78 78 c.user = User.get(c.rhodecode_user.user_id)
79 79 if c.user.username == User.DEFAULT_USER:
80 80 h.flash(_("You can't edit this user since it's"
81 81 " crucial for entire application"), category='warning')
82 82 return redirect(url('users'))
83 83
84 c.auth_user = AuthUser(
85 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
86
84 87 def _load_my_repos_data(self, watched=False):
85 88 if watched:
86 89 admin = False
87 90 follows_repos = Session().query(UserFollowing)\
88 91 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
89 92 .options(joinedload(UserFollowing.follows_repository))\
90 93 .all()
91 94 repo_list = [x.follows_repository for x in follows_repos]
92 95 else:
93 96 admin = True
94 97 repo_list = Repository.get_all_repos(
95 98 user_id=c.rhodecode_user.user_id)
96 99 repo_list = RepoList(repo_list, perm_set=[
97 100 'repository.read', 'repository.write', 'repository.admin'])
98 101
99 102 repos_data = RepoModel().get_repos_as_dict(
100 103 repo_list=repo_list, admin=admin)
101 104 # json used to render the grid
102 105 return json.dumps(repos_data)
103 106
104 107 @auth.CSRFRequired()
105 108 def my_account_update(self):
106 109 """
107 110 POST /_admin/my_account Updates info of my account
108 111 """
109 112 # url('my_account')
110 113 c.active = 'profile_edit'
111 114 self.__load_data()
112 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
113 ip_addr=self.ip_addr)
115 c.perm_user = c.auth_user
114 116 c.extern_type = c.user.extern_type
115 117 c.extern_name = c.user.extern_name
116 118
117 119 defaults = c.user.get_dict()
118 120 update = False
119 121 _form = UserForm(edit=True,
120 122 old_data={'user_id': c.rhodecode_user.user_id,
121 123 'email': c.rhodecode_user.email})()
122 124 form_result = {}
123 125 try:
124 126 post_data = dict(request.POST)
125 127 post_data['new_password'] = ''
126 128 post_data['password_confirmation'] = ''
127 129 form_result = _form.to_python(post_data)
128 130 # skip updating those attrs for my account
129 131 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
130 132 'new_password', 'password_confirmation']
131 133 # TODO: plugin should define if username can be updated
132 134 if c.extern_type != "rhodecode":
133 135 # forbid updating username for external accounts
134 136 skip_attrs.append('username')
135 137
136 138 UserModel().update_user(
137 139 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
138 140 h.flash(_('Your account was updated successfully'),
139 141 category='success')
140 142 Session().commit()
141 143 update = True
142 144
143 145 except formencode.Invalid as errors:
144 146 return htmlfill.render(
145 147 render('admin/my_account/my_account.html'),
146 148 defaults=errors.value,
147 149 errors=errors.error_dict or {},
148 150 prefix_error=False,
149 151 encoding="UTF-8",
150 152 force_defaults=False)
151 153 except Exception:
152 154 log.exception("Exception updating user")
153 155 h.flash(_('Error occurred during update of user %s')
154 156 % form_result.get('username'), category='error')
155 157
156 158 if update:
157 159 return redirect('my_account')
158 160
159 161 return htmlfill.render(
160 162 render('admin/my_account/my_account.html'),
161 163 defaults=defaults,
162 164 encoding="UTF-8",
163 165 force_defaults=False
164 166 )
165 167
166 168 def my_account(self):
167 169 """
168 170 GET /_admin/my_account Displays info about my account
169 171 """
170 172 # url('my_account')
171 173 c.active = 'profile'
172 174 self.__load_data()
173 175
174 176 defaults = c.user.get_dict()
175 177 return htmlfill.render(
176 178 render('admin/my_account/my_account.html'),
177 179 defaults=defaults, encoding="UTF-8", force_defaults=False)
178 180
179 181 def my_account_edit(self):
180 182 """
181 183 GET /_admin/my_account/edit Displays edit form of my account
182 184 """
183 185 c.active = 'profile_edit'
184 186 self.__load_data()
185 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
186 ip_addr=self.ip_addr)
187 c.perm_user = c.auth_user
187 188 c.extern_type = c.user.extern_type
188 189 c.extern_name = c.user.extern_name
189 190
190 191 defaults = c.user.get_dict()
191 192 return htmlfill.render(
192 193 render('admin/my_account/my_account.html'),
193 194 defaults=defaults,
194 195 encoding="UTF-8",
195 196 force_defaults=False
196 197 )
197 198
198 199 @auth.CSRFRequired(except_methods=['GET'])
199 200 def my_account_password(self):
200 201 c.active = 'password'
201 202 self.__load_data()
203 c.extern_type = c.user.extern_type
202 204
203 205 schema = user_schema.ChangePasswordSchema().bind(
204 206 username=c.rhodecode_user.username)
205 207
206 208 form = forms.Form(schema,
207 209 buttons=(forms.buttons.save, forms.buttons.reset))
208 210
209 if request.method == 'POST':
211 if request.method == 'POST' and c.extern_type == 'rhodecode':
210 212 controls = request.POST.items()
211 213 try:
212 214 valid_data = form.validate(controls)
213 215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
214 216 instance = c.rhodecode_user.get_instance()
215 217 instance.update_userdata(force_password_change=False)
216 218 Session().commit()
217 219 except forms.ValidationFailure as e:
218 220 request.session.flash(
219 221 _('Error occurred during update of user password'),
220 222 queue='error')
221 223 form = e
222 224 except Exception:
223 225 log.exception("Exception updating password")
224 226 request.session.flash(
225 227 _('Error occurred during update of user password'),
226 228 queue='error')
227 229 else:
228 230 session.setdefault('rhodecode_user', {}).update(
229 231 {'password': md5(instance.password)})
230 232 session.save()
231 233 request.session.flash(
232 234 _("Successfully updated password"), queue='success')
233 235 return redirect(url('my_account_password'))
234 236
235 237 c.form = form
236 238 return render('admin/my_account/my_account.html')
237 239
238 240 def my_account_repos(self):
239 241 c.active = 'repos'
240 242 self.__load_data()
241 243
242 244 # json used to render the grid
243 245 c.data = self._load_my_repos_data()
244 246 return render('admin/my_account/my_account.html')
245 247
246 248 def my_account_watched(self):
247 249 c.active = 'watched'
248 250 self.__load_data()
249 251
250 252 # json used to render the grid
251 253 c.data = self._load_my_repos_data(watched=True)
252 254 return render('admin/my_account/my_account.html')
253 255
254 256 def my_account_perms(self):
255 257 c.active = 'perms'
256 258 self.__load_data()
257 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
258 ip_addr=self.ip_addr)
259 c.perm_user = c.auth_user
259 260
260 261 return render('admin/my_account/my_account.html')
261 262
262 263 def my_account_emails(self):
263 264 c.active = 'emails'
264 265 self.__load_data()
265 266
266 267 c.user_email_map = UserEmailMap.query()\
267 268 .filter(UserEmailMap.user == c.user).all()
268 269 return render('admin/my_account/my_account.html')
269 270
270 271 @auth.CSRFRequired()
271 272 def my_account_emails_add(self):
272 273 email = request.POST.get('new_email')
273 274
274 275 try:
275 276 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
276 277 Session().commit()
277 278 h.flash(_("Added new email address `%s` for user account") % email,
278 279 category='success')
279 280 except formencode.Invalid as error:
280 281 msg = error.error_dict['email']
281 282 h.flash(msg, category='error')
282 283 except Exception:
283 284 log.exception("Exception in my_account_emails")
284 285 h.flash(_('An error occurred during email saving'),
285 286 category='error')
286 287 return redirect(url('my_account_emails'))
287 288
288 289 @auth.CSRFRequired()
289 290 def my_account_emails_delete(self):
290 291 email_id = request.POST.get('del_email_id')
291 292 user_model = UserModel()
292 293 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
293 294 Session().commit()
294 295 h.flash(_("Removed email address from user account"),
295 296 category='success')
296 297 return redirect(url('my_account_emails'))
297 298
298 299 def _extract_ordering(self, request):
299 300 column_index = safe_int(request.GET.get('order[0][column]'))
300 301 order_dir = request.GET.get('order[0][dir]', 'desc')
301 302 order_by = request.GET.get(
302 303 'columns[%s][data][sort]' % column_index, 'name_raw')
303 304 return order_by, order_dir
304 305
305 306 def _get_pull_requests_list(self, statuses):
306 307 start = safe_int(request.GET.get('start'), 0)
307 308 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
308 309 order_by, order_dir = self._extract_ordering(request)
309 310
310 311 pull_requests = PullRequestModel().get_im_participating_in(
311 312 user_id=c.rhodecode_user.user_id,
312 313 statuses=statuses,
313 314 offset=start, length=length, order_by=order_by,
314 315 order_dir=order_dir)
315 316
316 317 pull_requests_total_count = PullRequestModel().count_im_participating_in(
317 318 user_id=c.rhodecode_user.user_id, statuses=statuses)
318 319
319 320 from rhodecode.lib.utils import PartialRenderer
320 321 _render = PartialRenderer('data_table/_dt_elements.html')
321 322 data = []
322 323 for pr in pull_requests:
323 324 repo_id = pr.target_repo_id
324 325 comments = ChangesetCommentsModel().get_all_comments(
325 326 repo_id, pull_request=pr)
326 327 owned = pr.user_id == c.rhodecode_user.user_id
327 328 status = pr.calculated_review_status()
328 329
329 330 data.append({
330 331 'target_repo': _render('pullrequest_target_repo',
331 332 pr.target_repo.repo_name),
332 333 'name': _render('pullrequest_name',
333 334 pr.pull_request_id, pr.target_repo.repo_name,
334 335 short=True),
335 336 'name_raw': pr.pull_request_id,
336 337 'status': _render('pullrequest_status', status),
337 338 'title': _render(
338 339 'pullrequest_title', pr.title, pr.description),
339 340 'description': h.escape(pr.description),
340 341 'updated_on': _render('pullrequest_updated_on',
341 342 h.datetime_to_time(pr.updated_on)),
342 343 'updated_on_raw': h.datetime_to_time(pr.updated_on),
343 344 'created_on': _render('pullrequest_updated_on',
344 345 h.datetime_to_time(pr.created_on)),
345 346 'created_on_raw': h.datetime_to_time(pr.created_on),
346 347 'author': _render('pullrequest_author',
347 348 pr.author.full_contact, ),
348 349 'author_raw': pr.author.full_name,
349 350 'comments': _render('pullrequest_comments', len(comments)),
350 351 'comments_raw': len(comments),
351 352 'closed': pr.is_closed(),
352 353 'owned': owned
353 354 })
354 355 # json used to render the grid
355 356 data = ({
356 357 'data': data,
357 358 'recordsTotal': pull_requests_total_count,
358 359 'recordsFiltered': pull_requests_total_count,
359 360 })
360 361 return data
361 362
362 363 def my_account_pullrequests(self):
363 364 c.active = 'pullrequests'
364 365 self.__load_data()
365 366 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
366 367
367 368 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
368 369 if c.show_closed:
369 370 statuses += [PullRequest.STATUS_CLOSED]
370 371 data = self._get_pull_requests_list(statuses)
371 372 if not request.is_xhr:
372 373 c.data_participate = json.dumps(data['data'])
373 374 c.records_total_participate = data['recordsTotal']
374 375 return render('admin/my_account/my_account.html')
375 376 else:
376 377 return json.dumps(data)
377 378
378 379 def my_account_auth_tokens(self):
379 380 c.active = 'auth_tokens'
380 381 self.__load_data()
381 382 show_expired = True
382 383 c.lifetime_values = [
383 384 (str(-1), _('forever')),
384 385 (str(5), _('5 minutes')),
385 386 (str(60), _('1 hour')),
386 387 (str(60 * 24), _('1 day')),
387 388 (str(60 * 24 * 30), _('1 month')),
388 389 ]
389 390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
390 391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
391 392 for x in AuthTokenModel.cls.ROLES]
392 393 c.role_options = [(c.role_values, _("Role"))]
393 394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
394 395 c.rhodecode_user.user_id, show_expired=show_expired)
395 396 return render('admin/my_account/my_account.html')
396 397
397 398 @auth.CSRFRequired()
398 399 def my_account_auth_tokens_add(self):
399 400 lifetime = safe_int(request.POST.get('lifetime'), -1)
400 401 description = request.POST.get('description')
401 402 role = request.POST.get('role')
402 403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
403 404 role)
404 405 Session().commit()
405 406 h.flash(_("Auth token successfully created"), category='success')
406 407 return redirect(url('my_account_auth_tokens'))
407 408
408 409 @auth.CSRFRequired()
409 410 def my_account_auth_tokens_delete(self):
410 411 auth_token = request.POST.get('del_auth_token')
411 412 user_id = c.rhodecode_user.user_id
412 413 if request.POST.get('del_auth_token_builtin'):
413 414 user = User.get(user_id)
414 415 if user:
415 416 user.api_key = generate_auth_token(user.username)
416 417 Session().add(user)
417 418 Session().commit()
418 419 h.flash(_("Auth token successfully reset"), category='success')
419 420 elif auth_token:
420 421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
421 422 Session().commit()
422 423 h.flash(_("Auth token successfully deleted"), category='success')
423 424
424 425 return redirect(url('my_account_auth_tokens'))
425 426
426 427 def my_notifications(self):
427 428 c.active = 'notifications'
428 429 return render('admin/my_account/my_account.html')
429 430
430 431 @auth.CSRFRequired()
431 432 @jsonify
432 433 def my_notifications_toggle_visibility(self):
433 434 user = c.rhodecode_user.get_instance()
434 435 new_status = not user.user_data.get('notification_status', True)
435 436 user.update_userdata(notification_status=new_status)
436 437 Session().commit()
437 438 return user.user_data['notification_status']
438 439
439 440 @auth.CSRFRequired()
440 441 @jsonify
441 442 def my_account_notifications_test_channelstream(self):
442 443 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
443 444 c.rhodecode_user.username, datetime.datetime.now())
444 445 payload = {
445 446 'type': 'message',
446 447 'timestamp': datetime.datetime.utcnow(),
447 448 'user': 'system',
448 449 #'channel': 'broadcast',
449 450 'pm_users': [c.rhodecode_user.username],
450 451 'message': {
451 452 'message': message,
452 453 'level': 'info',
453 454 'topic': '/notifications'
454 455 }
455 456 }
456 457
457 458 registry = get_current_registry()
458 459 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
459 460 channelstream_config = rhodecode_plugins.get('channelstream', {})
460 461
461 462 try:
462 463 channelstream_request(channelstream_config, [payload], '/message')
463 464 except ChannelstreamException as e:
464 465 log.exception('Failed to send channelstream data')
465 466 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
466 467 return {"response": 'Channelstream data sent. '
467 468 'You should see a new live message now.'}
@@ -1,5 +1,13 b''
1 1 <%namespace name="widgets" file="/widgets.html"/>
2 2
3 3 <%widgets:panel title="${_('Change Your Account Password')}">
4 ${c.form.render() | n}
4
5 % if c.extern_type != 'rhodecode':
6 <p>${_('Your user account details are managed by an external source. Details cannot be managed here.')}
7 <br/>${_('Source type')}: <strong>${c.extern_type}</strong>
8 </p>
9 % else:
10 ${c.form.render() | n}
11 % endif
12
5 13 </%widgets:panel>
@@ -1,110 +1,113 b''
1 1 <%namespace name="base" file="/base/base.html"/>
2 2 <div class="panel panel-default user-profile">
3 3 <div class="panel-heading">
4 4 <h3 class="panel-title">${_('My Profile')}</h3>
5 5 <a href="${url('my_account')}" class="panel-edit">Close</a>
6 6 </div>
7 7
8 8 <div class="panel-body">
9 9 ${h.secure_form(url('my_account'), method='post', class_='form')}
10 10 <% readonly = None %>
11 11 <% disabled = "" %>
12 12
13 13 % if c.extern_type != 'rhodecode':
14 14 <% readonly = "readonly" %>
15 15 <% disabled = "disabled" %>
16 16 <div class="infoform">
17 17 <div class="fields">
18 <p>${_('Your user account details are managed by an external source, i.e. LDAP. Details cannot be managed here.')}.</p>
18 <p>${_('Your user account details are managed by an external source. Details cannot be managed here.')}
19 <br/>${_('Source type')}: <strong>${c.extern_type}</strong>
20 </p>
21
19 22 <div class="field">
20 23 <div class="label">
21 24 <label for="username">${_('Username')}:</label>
22 25 </div>
23 26 <div class="input">
24 27 ${h.text('username', class_='input-valuedisplay', readonly=readonly)}
25 28 </div>
26 29 </div>
27 30
28 31 <div class="field">
29 32 <div class="label">
30 33 <label for="name">${_('First Name')}:</label>
31 34 </div>
32 35 <div class="input">
33 36 ${h.text('firstname', class_='input-valuedisplay', readonly=readonly)}
34 37 </div>
35 38 </div>
36 39
37 40 <div class="field">
38 41 <div class="label">
39 42 <label for="lastname">${_('Last Name')}:</label>
40 43 </div>
41 44 <div class="input-valuedisplay">
42 45 ${h.text('lastname', class_='input-valuedisplay', readonly=readonly)}
43 46 </div>
44 47 </div>
45 48 </div>
46 49 </div>
47 50 % else:
48 51 <div class="form">
49 52 <div class="fields">
50 53 <div class="field">
51 54 <div class="label photo">
52 55 ${_('Photo')}:
53 56 </div>
54 57 <div class="input profile">
55 58 %if c.visual.use_gravatar:
56 59 ${base.gravatar(c.user.email, 100)}
57 60 <p class="help-block">${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a>.</p>
58 61 %else:
59 62 ${base.gravatar(c.user.email, 20)}
60 63 ${_('Avatars are disabled')}
61 64 %endif
62 65 </div>
63 66 </div>
64 67 <div class="field">
65 68 <div class="label">
66 69 <label for="username">${_('Username')}:</label>
67 70 </div>
68 71 <div class="input">
69 72 ${h.text('username', class_='medium%s' % disabled, readonly=readonly)}
70 73 ${h.hidden('extern_name', c.extern_name)}
71 74 ${h.hidden('extern_type', c.extern_type)}
72 75 </div>
73 76 </div>
74 77 <div class="field">
75 78 <div class="label">
76 79 <label for="name">${_('First Name')}:</label>
77 80 </div>
78 81 <div class="input">
79 82 ${h.text('firstname', class_="medium")}
80 83 </div>
81 84 </div>
82 85
83 86 <div class="field">
84 87 <div class="label">
85 88 <label for="lastname">${_('Last Name')}:</label>
86 89 </div>
87 90 <div class="input">
88 91 ${h.text('lastname', class_="medium")}
89 92 </div>
90 93 </div>
91 94
92 95 <div class="field">
93 96 <div class="label">
94 97 <label for="email">${_('Email')}:</label>
95 98 </div>
96 99 <div class="input">
97 100 ## we should be able to edit email !
98 101 ${h.text('email', class_="medium")}
99 102 </div>
100 103 </div>
101 104
102 105 <div class="buttons">
103 106 ${h.submit('save', _('Save'), class_="btn")}
104 107 ${h.reset('reset', _('Reset'), class_="btn")}
105 108 </div>
106 109 </div>
107 110 </div>
108 111 % endif
109 112 </div>
110 113 </div> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now