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