##// END OF EJS Templates
login: switch to re-use the baseApp pyramid view.
marcink -
r1536:995f91ba default
parent child Browse files
Show More
@@ -1,78 +1,79 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import logging
22 22 from pylons import tmpl_context as c
23 23
24 24 from rhodecode.lib.utils2 import StrictAttributeDict
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 ADMIN_PREFIX = '/_admin'
30 30 STATIC_FILE_PREFIX = '/_static'
31 31
32 32
33 33 class TemplateArgs(StrictAttributeDict):
34 34 pass
35 35
36 36
37 37 class BaseAppView(object):
38 38
39 39 def __init__(self, context, request):
40 40 self.request = request
41 41 self.context = context
42 42 self.session = request.session
43 43 self._rhodecode_user = request.user # auth user
44 44
45 45 def _get_local_tmpl_context(self):
46 46 c = TemplateArgs()
47 47 c.auth_user = self.request.user
48 48 return c
49 49
50 50 def _register_global_c(self, tmpl_args):
51 51 """
52 52 Registers attributes to pylons global `c`
53 53 """
54 54 # TODO(marcink): remove once pyramid migration is finished
55 55 for k, v in tmpl_args.items():
56 56 setattr(c, k, v)
57 57
58 58 def _get_template_context(self, tmpl_args):
59
60 59 self._register_global_c(tmpl_args)
61 60
62 return {
61 local_tmpl_args = {
63 62 'defaults': {},
64 63 'errors': {},
65 64 }
65 local_tmpl_args.update(tmpl_args)
66 return local_tmpl_args
66 67
67 68 def load_default_context(self):
68 69 """
69 70 example:
70 71
71 72 def load_default_context(self):
72 73 c = self._get_local_tmpl_context()
73 74 c.custom_var = 'foobar'
74 75 self._register_global_c(c)
75 76 return c
76 77 """
77 78 raise NotImplementedError('Needs implementation in view class')
78 79
@@ -1,400 +1,396 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 25 import logging
26 26 import urlparse
27 27
28 28 from pylons import url
29 29 from pyramid.httpexceptions import HTTPFound
30 30 from pyramid.view import view_config
31 31 from recaptcha.client.captcha import submit
32 32
33 from rhodecode.apps._base import BaseAppView
33 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 35 from rhodecode.events import UserRegistered
35 36 from rhodecode.lib import helpers as h
36 37 from rhodecode.lib.auth import (
37 38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
38 39 from rhodecode.lib.base import get_ip_addr
39 40 from rhodecode.lib.exceptions import UserCreationError
40 41 from rhodecode.lib.utils2 import safe_str
41 42 from rhodecode.model.db import User, UserApiKeys
42 43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
43 44 from rhodecode.model.meta import Session
44 45 from rhodecode.model.auth_token import AuthTokenModel
45 46 from rhodecode.model.settings import SettingsModel
46 47 from rhodecode.model.user import UserModel
47 48 from rhodecode.translation import _
48 49
49 50
50 51 log = logging.getLogger(__name__)
51 52
52 53 CaptchaData = collections.namedtuple(
53 54 'CaptchaData', 'active, private_key, public_key')
54 55
55 56
56 57 def _store_user_in_session(session, username, remember=False):
57 58 user = User.get_by_username(username, case_insensitive=True)
58 59 auth_user = AuthUser(user.user_id)
59 60 auth_user.set_authenticated()
60 61 cs = auth_user.get_cookie_store()
61 62 session['rhodecode_user'] = cs
62 63 user.update_lastlogin()
63 64 Session().commit()
64 65
65 66 # If they want to be remembered, update the cookie
66 67 if remember:
67 68 _year = (datetime.datetime.now() +
68 69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
69 70 session._set_cookie_expires(_year)
70 71
71 72 session.save()
72 73
73 74 safe_cs = cs.copy()
74 75 safe_cs['password'] = '****'
75 76 log.info('user %s is now authenticated and stored in '
76 77 'session, session attrs %s', username, safe_cs)
77 78
78 79 # dumps session attrs back to cookie
79 80 session._update_cookie_out()
80 81 # we set new cookie
81 82 headers = None
82 83 if session.request['set_cookie']:
83 84 # send set-cookie headers back to response to update cookie
84 85 headers = [('Set-Cookie', session.request['cookie_out'])]
85 86 return headers
86 87
87 88
88 89 def get_came_from(request):
89 90 came_from = safe_str(request.GET.get('came_from', ''))
90 91 parsed = urlparse.urlparse(came_from)
91 92 allowed_schemes = ['http', 'https']
92 93 if parsed.scheme and parsed.scheme not in allowed_schemes:
93 94 log.error('Suspicious URL scheme detected %s for url %s' %
94 95 (parsed.scheme, parsed))
95 96 came_from = url('home')
96 97 elif parsed.netloc and request.host != parsed.netloc:
97 98 log.error('Suspicious NETLOC detected %s for url %s server url '
98 99 'is: %s' % (parsed.netloc, parsed, request.host))
99 100 came_from = url('home')
100 101 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
101 102 log.error('Header injection detected `%s` for url %s server url ' %
102 103 (parsed.path, parsed))
103 104 came_from = url('home')
104 105
105 106 return came_from or url('home')
106 107
107 108
108 class LoginView(object):
109 class LoginView(BaseAppView):
109 110
110 def __init__(self, context, request):
111 self.request = request
112 self.context = context
113 self.session = request.session
114 self._rhodecode_user = request.user
115
116 def _get_template_context(self):
117 return {
118 'came_from': get_came_from(self.request),
119 'defaults': {},
120 'errors': {},
121 }
111 def load_default_context(self):
112 c = self._get_local_tmpl_context()
113 c.came_from = get_came_from(self.request)
114 self._register_global_c(c)
115 return c
122 116
123 117 def _get_captcha_data(self):
124 118 settings = SettingsModel().get_all_settings()
125 119 private_key = settings.get('rhodecode_captcha_private_key')
126 120 public_key = settings.get('rhodecode_captcha_public_key')
127 121 active = bool(private_key)
128 122 return CaptchaData(
129 123 active=active, private_key=private_key, public_key=public_key)
130 124
131 125 @view_config(
132 126 route_name='login', request_method='GET',
133 127 renderer='rhodecode:templates/login.mako')
134 128 def login(self):
135 came_from = get_came_from(self.request)
136 user = self.request.user
129 c = self.load_default_context()
130 auth_user = self._rhodecode_user
137 131
138 132 # redirect if already logged in
139 if user.is_authenticated and not user.is_default and user.ip_allowed:
140 raise HTTPFound(came_from)
133 if (auth_user.is_authenticated and
134 not auth_user.is_default and auth_user.ip_allowed):
135 raise HTTPFound(c.came_from)
141 136
142 137 # check if we use headers plugin, and try to login using it.
143 138 try:
144 139 log.debug('Running PRE-AUTH for headers based authentication')
145 140 auth_info = authenticate(
146 141 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
147 142 if auth_info:
148 143 headers = _store_user_in_session(
149 144 self.session, auth_info.get('username'))
150 raise HTTPFound(came_from, headers=headers)
145 raise HTTPFound(c.came_from, headers=headers)
151 146 except UserCreationError as e:
152 147 log.error(e)
153 148 self.session.flash(e, queue='error')
154 149
155 return self._get_template_context()
150 return self._get_template_context(c)
156 151
157 152 @view_config(
158 153 route_name='login', request_method='POST',
159 154 renderer='rhodecode:templates/login.mako')
160 155 def login_post(self):
161 came_from = get_came_from(self.request)
156 c = self.load_default_context()
162 157
163 158 login_form = LoginForm()()
164 159
165 160 try:
166 161 self.session.invalidate()
167 162 form_result = login_form.to_python(self.request.params)
168 163 # form checks for username/password, now we're authenticated
169 164 headers = _store_user_in_session(
170 165 self.session,
171 166 username=form_result['username'],
172 167 remember=form_result['remember'])
173 log.debug('Redirecting to "%s" after login.', came_from)
174 raise HTTPFound(came_from, headers=headers)
168 log.debug('Redirecting to "%s" after login.', c.came_from)
169 raise HTTPFound(c.came_from, headers=headers)
175 170 except formencode.Invalid as errors:
176 171 defaults = errors.value
177 172 # remove password from filling in form again
178 173 defaults.pop('password', None)
179 render_ctx = self._get_template_context()
174 render_ctx = self._get_template_context(c)
180 175 render_ctx.update({
181 176 'errors': errors.error_dict,
182 177 'defaults': defaults,
183 178 })
184 179 return render_ctx
185 180
186 181 except UserCreationError as e:
187 182 # headers auth or other auth functions that create users on
188 183 # the fly can throw this exception signaling that there's issue
189 184 # with user creation, explanation should be provided in
190 185 # Exception itself
191 186 self.session.flash(e, queue='error')
192 return self._get_template_context()
187 return self._get_template_context(c)
193 188
194 189 @CSRFRequired()
195 190 @view_config(route_name='logout', request_method='POST')
196 191 def logout(self):
197 user = self.request.user
198 log.info('Deleting session for user: `%s`', user)
192 auth_user = self._rhodecode_user
193 log.info('Deleting session for user: `%s`', auth_user)
199 194 self.session.delete()
200 195 return HTTPFound(url('home'))
201 196
202 197 @HasPermissionAnyDecorator(
203 198 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
204 199 @view_config(
205 200 route_name='register', request_method='GET',
206 201 renderer='rhodecode:templates/register.mako',)
207 202 def register(self, defaults=None, errors=None):
203 c = self.load_default_context()
208 204 defaults = defaults or {}
209 205 errors = errors or {}
210 206
211 207 settings = SettingsModel().get_all_settings()
212 208 register_message = settings.get('rhodecode_register_message') or ''
213 209 captcha = self._get_captcha_data()
214 210 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
215 211 .AuthUser.permissions['global']
216 212
217 render_ctx = self._get_template_context()
213 render_ctx = self._get_template_context(c)
218 214 render_ctx.update({
219 215 'defaults': defaults,
220 216 'errors': errors,
221 217 'auto_active': auto_active,
222 218 'captcha_active': captcha.active,
223 219 'captcha_public_key': captcha.public_key,
224 220 'register_message': register_message,
225 221 })
226 222 return render_ctx
227 223
228 224 @HasPermissionAnyDecorator(
229 225 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
230 226 @view_config(
231 227 route_name='register', request_method='POST',
232 228 renderer='rhodecode:templates/register.mako')
233 229 def register_post(self):
234 230 captcha = self._get_captcha_data()
235 231 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
236 232 .AuthUser.permissions['global']
237 233
238 234 register_form = RegisterForm()()
239 235 try:
240 236 form_result = register_form.to_python(self.request.params)
241 237 form_result['active'] = auto_active
242 238
243 239 if captcha.active:
244 240 response = submit(
245 241 self.request.params.get('recaptcha_challenge_field'),
246 242 self.request.params.get('recaptcha_response_field'),
247 243 private_key=captcha.private_key,
248 244 remoteip=get_ip_addr(self.request.environ))
249 245 if not response.is_valid:
250 246 _value = form_result
251 247 _msg = _('Bad captcha')
252 248 error_dict = {'recaptcha_field': _msg}
253 249 raise formencode.Invalid(_msg, _value, None,
254 250 error_dict=error_dict)
255 251
256 252 new_user = UserModel().create_registration(form_result)
257 253 event = UserRegistered(user=new_user, session=self.session)
258 254 self.request.registry.notify(event)
259 255 self.session.flash(
260 256 _('You have successfully registered with RhodeCode'),
261 257 queue='success')
262 258 Session().commit()
263 259
264 260 redirect_ro = self.request.route_path('login')
265 261 raise HTTPFound(redirect_ro)
266 262
267 263 except formencode.Invalid as errors:
268 264 errors.value.pop('password', None)
269 265 errors.value.pop('password_confirmation', None)
270 266 return self.register(
271 267 defaults=errors.value, errors=errors.error_dict)
272 268
273 269 except UserCreationError as e:
274 270 # container auth or other auth functions that create users on
275 271 # the fly can throw this exception signaling that there's issue
276 272 # with user creation, explanation should be provided in
277 273 # Exception itself
278 274 self.session.flash(e, queue='error')
279 275 return self.register()
280 276
281 277 @view_config(
282 278 route_name='reset_password', request_method=('GET', 'POST'),
283 279 renderer='rhodecode:templates/password_reset.mako')
284 280 def password_reset(self):
285 281 captcha = self._get_captcha_data()
286 282
287 283 render_ctx = {
288 284 'captcha_active': captcha.active,
289 285 'captcha_public_key': captcha.public_key,
290 286 'defaults': {},
291 287 'errors': {},
292 288 }
293 289
294 290 # always send implicit message to prevent from discovery of
295 291 # matching emails
296 292 msg = _('If such email exists, a password reset link was sent to it.')
297 293
298 294 if self.request.POST:
299 295 if h.HasPermissionAny('hg.password_reset.disabled')():
300 296 _email = self.request.POST.get('email', '')
301 297 log.error('Failed attempt to reset password for `%s`.', _email)
302 298 self.session.flash(_('Password reset has been disabled.'),
303 299 queue='error')
304 300 return HTTPFound(self.request.route_path('reset_password'))
305 301
306 302 password_reset_form = PasswordResetForm()()
307 303 try:
308 304 form_result = password_reset_form.to_python(
309 305 self.request.params)
310 306 user_email = form_result['email']
311 307
312 308 if captcha.active:
313 309 response = submit(
314 310 self.request.params.get('recaptcha_challenge_field'),
315 311 self.request.params.get('recaptcha_response_field'),
316 312 private_key=captcha.private_key,
317 313 remoteip=get_ip_addr(self.request.environ))
318 314 if not response.is_valid:
319 315 _value = form_result
320 316 _msg = _('Bad captcha')
321 317 error_dict = {'recaptcha_field': _msg}
322 318 raise formencode.Invalid(
323 319 _msg, _value, None, error_dict=error_dict)
324 320
325 321 # Generate reset URL and send mail.
326 322 user = User.get_by_email(user_email)
327 323
328 324 # generate password reset token that expires in 10minutes
329 325 desc = 'Generated token for password reset from {}'.format(
330 326 datetime.datetime.now().isoformat())
331 327 reset_token = AuthTokenModel().create(
332 328 user, lifetime=10,
333 329 description=desc,
334 330 role=UserApiKeys.ROLE_PASSWORD_RESET)
335 331 Session().commit()
336 332
337 333 log.debug('Successfully created password recovery token')
338 334 password_reset_url = self.request.route_url(
339 335 'reset_password_confirmation',
340 336 _query={'key': reset_token.api_key})
341 337 UserModel().reset_password_link(
342 338 form_result, password_reset_url)
343 339 # Display success message and redirect.
344 340 self.session.flash(msg, queue='success')
345 341 return HTTPFound(self.request.route_path('reset_password'))
346 342
347 343 except formencode.Invalid as errors:
348 344 render_ctx.update({
349 345 'defaults': errors.value,
350 346 'errors': errors.error_dict,
351 347 })
352 348 if not self.request.params.get('email'):
353 349 # case of empty email, we want to report that
354 350 return render_ctx
355 351
356 352 if 'recaptcha_field' in errors.error_dict:
357 353 # case of failed captcha
358 354 return render_ctx
359 355
360 356 log.debug('faking response on invalid password reset')
361 357 # make this take 2s, to prevent brute forcing.
362 358 time.sleep(2)
363 359 self.session.flash(msg, queue='success')
364 360 return HTTPFound(self.request.route_path('reset_password'))
365 361
366 362 return render_ctx
367 363
368 364 @view_config(route_name='reset_password_confirmation',
369 365 request_method='GET')
370 366 def password_reset_confirmation(self):
371 367
372 368 if self.request.GET and self.request.GET.get('key'):
373 369 # make this take 2s, to prevent brute forcing.
374 370 time.sleep(2)
375 371
376 372 token = AuthTokenModel().get_auth_token(
377 373 self.request.GET.get('key'))
378 374
379 375 # verify token is the correct role
380 376 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
381 377 log.debug('Got token with role:%s expected is %s',
382 378 getattr(token, 'role', 'EMPTY_TOKEN'),
383 379 UserApiKeys.ROLE_PASSWORD_RESET)
384 380 self.session.flash(
385 381 _('Given reset token is invalid'), queue='error')
386 382 return HTTPFound(self.request.route_path('reset_password'))
387 383
388 384 try:
389 385 owner = token.user
390 386 data = {'email': owner.email, 'token': token.api_key}
391 387 UserModel().reset_password(data)
392 388 self.session.flash(
393 389 _('Your password reset was successful, '
394 390 'a new password has been sent to your email'),
395 391 queue='success')
396 392 except Exception as e:
397 393 log.error(e)
398 394 return HTTPFound(self.request.route_path('reset_password'))
399 395
400 396 return HTTPFound(self.request.route_path('login'))
General Comments 0
You need to be logged in to leave comments. Login now