##// END OF EJS Templates
fix(2fa): fixed typo from refactor
super-admin -
r5375:7ec0fbd3 default
parent child Browse files
Show More
@@ -1,553 +1,553 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import time
19 import time
20 import json
20 import json
21 import pyotp
21 import pyotp
22 import qrcode
22 import qrcode
23 import collections
23 import collections
24 import datetime
24 import datetime
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import logging
27 import logging
28 import urllib.parse
28 import urllib.parse
29 import requests
29 import requests
30 from io import BytesIO
30 from io import BytesIO
31 from base64 import b64encode
31 from base64 import b64encode
32
32
33 from pyramid.renderers import render
33 from pyramid.renderers import render
34 from pyramid.response import Response
34 from pyramid.response import Response
35 from pyramid.httpexceptions import HTTPFound
35 from pyramid.httpexceptions import HTTPFound
36
36
37 import rhodecode
37 import rhodecode
38 from rhodecode.apps._base import BaseAppView
38 from rhodecode.apps._base import BaseAppView
39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
39 from rhodecode.authentication.base import authenticate, HTTP_TYPE
40 from rhodecode.authentication.plugins import auth_rhodecode
40 from rhodecode.authentication.plugins import auth_rhodecode
41 from rhodecode.events import UserRegistered, trigger
41 from rhodecode.events import UserRegistered, trigger
42 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
43 from rhodecode.lib import audit_logger
43 from rhodecode.lib import audit_logger
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
45 AuthUser, HasPermissionAnyDecorator, CSRFRequired, LoginRequired, NotAnonymous)
46 from rhodecode.lib.base import get_ip_addr
46 from rhodecode.lib.base import get_ip_addr
47 from rhodecode.lib.exceptions import UserCreationError
47 from rhodecode.lib.exceptions import UserCreationError
48 from rhodecode.lib.utils2 import safe_str
48 from rhodecode.lib.utils2 import safe_str
49 from rhodecode.model.db import User, UserApiKeys
49 from rhodecode.model.db import User, UserApiKeys
50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
50 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm, TOTPForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.auth_token import AuthTokenModel
52 from rhodecode.model.auth_token import AuthTokenModel
53 from rhodecode.model.settings import SettingsModel
53 from rhodecode.model.settings import SettingsModel
54 from rhodecode.model.user import UserModel
54 from rhodecode.model.user import UserModel
55 from rhodecode.translation import _
55 from rhodecode.translation import _
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60 CaptchaData = collections.namedtuple(
60 CaptchaData = collections.namedtuple(
61 'CaptchaData', 'active, private_key, public_key')
61 'CaptchaData', 'active, private_key, public_key')
62
62
63
63
64 def store_user_in_session(session, user_identifier, remember=False):
64 def store_user_in_session(session, user_identifier, remember=False):
65 user = User.get_by_username_or_primary_email(user_identifier)
65 user = User.get_by_username_or_primary_email(user_identifier)
66 auth_user = AuthUser(user.user_id)
66 auth_user = AuthUser(user.user_id)
67 auth_user.set_authenticated()
67 auth_user.set_authenticated()
68 cs = auth_user.get_cookie_store()
68 cs = auth_user.get_cookie_store()
69 session['rhodecode_user'] = cs
69 session['rhodecode_user'] = cs
70 user.update_lastlogin()
70 user.update_lastlogin()
71 Session().commit()
71 Session().commit()
72
72
73 # If they want to be remembered, update the cookie
73 # If they want to be remembered, update the cookie
74 if remember:
74 if remember:
75 _year = (datetime.datetime.now() +
75 _year = (datetime.datetime.now() +
76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
76 datetime.timedelta(seconds=60 * 60 * 24 * 365))
77 session._set_cookie_expires(_year)
77 session._set_cookie_expires(_year)
78
78
79 session.save()
79 session.save()
80
80
81 safe_cs = cs.copy()
81 safe_cs = cs.copy()
82 safe_cs['password'] = '****'
82 safe_cs['password'] = '****'
83 log.info('user %s is now authenticated and stored in '
83 log.info('user %s is now authenticated and stored in '
84 'session, session attrs %s', user_identifier, safe_cs)
84 'session, session attrs %s', user_identifier, safe_cs)
85
85
86 # dumps session attrs back to cookie
86 # dumps session attrs back to cookie
87 session._update_cookie_out()
87 session._update_cookie_out()
88 # we set new cookie
88 # we set new cookie
89 headers = None
89 headers = None
90 if session.request['set_cookie']:
90 if session.request['set_cookie']:
91 # send set-cookie headers back to response to update cookie
91 # send set-cookie headers back to response to update cookie
92 headers = [('Set-Cookie', session.request['cookie_out'])]
92 headers = [('Set-Cookie', session.request['cookie_out'])]
93 return headers
93 return headers
94
94
95
95
96 def get_came_from(request):
96 def get_came_from(request):
97 came_from = safe_str(request.GET.get('came_from', ''))
97 came_from = safe_str(request.GET.get('came_from', ''))
98 parsed = urllib.parse.urlparse(came_from)
98 parsed = urllib.parse.urlparse(came_from)
99
99
100 allowed_schemes = ['http', 'https']
100 allowed_schemes = ['http', 'https']
101 default_came_from = h.route_path('home')
101 default_came_from = h.route_path('home')
102 if parsed.scheme and parsed.scheme not in allowed_schemes:
102 if parsed.scheme and parsed.scheme not in allowed_schemes:
103 log.error('Suspicious URL scheme detected %s for url %s',
103 log.error('Suspicious URL scheme detected %s for url %s',
104 parsed.scheme, parsed)
104 parsed.scheme, parsed)
105 came_from = default_came_from
105 came_from = default_came_from
106 elif parsed.netloc and request.host != parsed.netloc:
106 elif parsed.netloc and request.host != parsed.netloc:
107 log.error('Suspicious NETLOC detected %s for url %s server url '
107 log.error('Suspicious NETLOC detected %s for url %s server url '
108 'is: %s', parsed.netloc, parsed, request.host)
108 'is: %s', parsed.netloc, parsed, request.host)
109 came_from = default_came_from
109 came_from = default_came_from
110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
110 elif any(bad_char in came_from for bad_char in ('\r', '\n')):
111 log.error('Header injection detected `%s` for url %s server url ',
111 log.error('Header injection detected `%s` for url %s server url ',
112 parsed.path, parsed)
112 parsed.path, parsed)
113 came_from = default_came_from
113 came_from = default_came_from
114
114
115 return came_from or default_came_from
115 return came_from or default_came_from
116
116
117
117
118 class LoginView(BaseAppView):
118 class LoginView(BaseAppView):
119
119
120 def load_default_context(self):
120 def load_default_context(self):
121 c = self._get_local_tmpl_context()
121 c = self._get_local_tmpl_context()
122 c.came_from = get_came_from(self.request)
122 c.came_from = get_came_from(self.request)
123 return c
123 return c
124
124
125 def _get_captcha_data(self):
125 def _get_captcha_data(self):
126 settings = SettingsModel().get_all_settings()
126 settings = SettingsModel().get_all_settings()
127 private_key = settings.get('rhodecode_captcha_private_key')
127 private_key = settings.get('rhodecode_captcha_private_key')
128 public_key = settings.get('rhodecode_captcha_public_key')
128 public_key = settings.get('rhodecode_captcha_public_key')
129 active = bool(private_key)
129 active = bool(private_key)
130 return CaptchaData(
130 return CaptchaData(
131 active=active, private_key=private_key, public_key=public_key)
131 active=active, private_key=private_key, public_key=public_key)
132
132
133 def validate_captcha(self, private_key):
133 def validate_captcha(self, private_key):
134
134
135 captcha_rs = self.request.POST.get('g-recaptcha-response')
135 captcha_rs = self.request.POST.get('g-recaptcha-response')
136 url = "https://www.google.com/recaptcha/api/siteverify"
136 url = "https://www.google.com/recaptcha/api/siteverify"
137 params = {
137 params = {
138 'secret': private_key,
138 'secret': private_key,
139 'response': captcha_rs,
139 'response': captcha_rs,
140 'remoteip': get_ip_addr(self.request.environ)
140 'remoteip': get_ip_addr(self.request.environ)
141 }
141 }
142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
142 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
143 verify_rs = verify_rs.json()
143 verify_rs = verify_rs.json()
144 captcha_status = verify_rs.get('success', False)
144 captcha_status = verify_rs.get('success', False)
145 captcha_errors = verify_rs.get('error-codes', [])
145 captcha_errors = verify_rs.get('error-codes', [])
146 if not isinstance(captcha_errors, list):
146 if not isinstance(captcha_errors, list):
147 captcha_errors = [captcha_errors]
147 captcha_errors = [captcha_errors]
148 captcha_errors = ', '.join(captcha_errors)
148 captcha_errors = ', '.join(captcha_errors)
149 captcha_message = ''
149 captcha_message = ''
150 if captcha_status is False:
150 if captcha_status is False:
151 captcha_message = "Bad captcha. Errors: {}".format(
151 captcha_message = "Bad captcha. Errors: {}".format(
152 captcha_errors)
152 captcha_errors)
153
153
154 return captcha_status, captcha_message
154 return captcha_status, captcha_message
155
155
156 def login(self):
156 def login(self):
157 c = self.load_default_context()
157 c = self.load_default_context()
158 auth_user = self._rhodecode_user
158 auth_user = self._rhodecode_user
159
159
160 # redirect if already logged in
160 # redirect if already logged in
161 if (auth_user.is_authenticated and
161 if (auth_user.is_authenticated and
162 not auth_user.is_default and auth_user.ip_allowed):
162 not auth_user.is_default and auth_user.ip_allowed):
163 raise HTTPFound(c.came_from)
163 raise HTTPFound(c.came_from)
164
164
165 # check if we use headers plugin, and try to login using it.
165 # check if we use headers plugin, and try to login using it.
166 try:
166 try:
167 log.debug('Running PRE-AUTH for headers based authentication')
167 log.debug('Running PRE-AUTH for headers based authentication')
168 auth_info = authenticate(
168 auth_info = authenticate(
169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
169 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
170 if auth_info:
170 if auth_info:
171 headers = store_user_in_session(
171 headers = store_user_in_session(
172 self.session, auth_info.get('username'))
172 self.session, auth_info.get('username'))
173 raise HTTPFound(c.came_from, headers=headers)
173 raise HTTPFound(c.came_from, headers=headers)
174 except UserCreationError as e:
174 except UserCreationError as e:
175 log.error(e)
175 log.error(e)
176 h.flash(e, category='error')
176 h.flash(e, category='error')
177
177
178 return self._get_template_context(c)
178 return self._get_template_context(c)
179
179
180 def login_post(self):
180 def login_post(self):
181 c = self.load_default_context()
181 c = self.load_default_context()
182
182
183 login_form = LoginForm(self.request.translate)()
183 login_form = LoginForm(self.request.translate)()
184
184
185 try:
185 try:
186 self.session.invalidate()
186 self.session.invalidate()
187 form_result = login_form.to_python(self.request.POST)
187 form_result = login_form.to_python(self.request.POST)
188 # form checks for username/password, now we're authenticated
188 # form checks for username/password, now we're authenticated
189 username = form_result['username']
189 username = form_result['username']
190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
190 if (user := User.get_by_username_or_primary_email(username)).has_enabled_2fa:
191 user.check_2fa_required = True
191 user.check_2fa_required = True
192
192
193 headers = store_user_in_session(
193 headers = store_user_in_session(
194 self.session,
194 self.session,
195 user_identifier=username,
195 user_identifier=username,
196 remember=form_result['remember'])
196 remember=form_result['remember'])
197 log.debug('Redirecting to "%s" after login.', c.came_from)
197 log.debug('Redirecting to "%s" after login.', c.came_from)
198
198
199 audit_user = audit_logger.UserWrap(
199 audit_user = audit_logger.UserWrap(
200 username=self.request.POST.get('username'),
200 username=self.request.POST.get('username'),
201 ip_addr=self.request.remote_addr)
201 ip_addr=self.request.remote_addr)
202 action_data = {'user_agent': self.request.user_agent}
202 action_data = {'user_agent': self.request.user_agent}
203 audit_logger.store_web(
203 audit_logger.store_web(
204 'user.login.success', action_data=action_data,
204 'user.login.success', action_data=action_data,
205 user=audit_user, commit=True)
205 user=audit_user, commit=True)
206
206
207 raise HTTPFound(c.came_from, headers=headers)
207 raise HTTPFound(c.came_from, headers=headers)
208 except formencode.Invalid as errors:
208 except formencode.Invalid as errors:
209 defaults = errors.value
209 defaults = errors.value
210 # remove password from filling in form again
210 # remove password from filling in form again
211 defaults.pop('password', None)
211 defaults.pop('password', None)
212 render_ctx = {
212 render_ctx = {
213 'errors': errors.error_dict,
213 'errors': errors.error_dict,
214 'defaults': defaults,
214 'defaults': defaults,
215 }
215 }
216
216
217 audit_user = audit_logger.UserWrap(
217 audit_user = audit_logger.UserWrap(
218 username=self.request.POST.get('username'),
218 username=self.request.POST.get('username'),
219 ip_addr=self.request.remote_addr)
219 ip_addr=self.request.remote_addr)
220 action_data = {'user_agent': self.request.user_agent}
220 action_data = {'user_agent': self.request.user_agent}
221 audit_logger.store_web(
221 audit_logger.store_web(
222 'user.login.failure', action_data=action_data,
222 'user.login.failure', action_data=action_data,
223 user=audit_user, commit=True)
223 user=audit_user, commit=True)
224 return self._get_template_context(c, **render_ctx)
224 return self._get_template_context(c, **render_ctx)
225
225
226 except UserCreationError as e:
226 except UserCreationError as e:
227 # headers auth or other auth functions that create users on
227 # headers auth or other auth functions that create users on
228 # the fly can throw this exception signaling that there's issue
228 # the fly can throw this exception signaling that there's issue
229 # with user creation, explanation should be provided in
229 # with user creation, explanation should be provided in
230 # Exception itself
230 # Exception itself
231 h.flash(e, category='error')
231 h.flash(e, category='error')
232 return self._get_template_context(c)
232 return self._get_template_context(c)
233
233
234 @CSRFRequired()
234 @CSRFRequired()
235 def logout(self):
235 def logout(self):
236 auth_user = self._rhodecode_user
236 auth_user = self._rhodecode_user
237 log.info('Deleting session for user: `%s`', auth_user)
237 log.info('Deleting session for user: `%s`', auth_user)
238
238
239 action_data = {'user_agent': self.request.user_agent}
239 action_data = {'user_agent': self.request.user_agent}
240 audit_logger.store_web(
240 audit_logger.store_web(
241 'user.logout', action_data=action_data,
241 'user.logout', action_data=action_data,
242 user=auth_user, commit=True)
242 user=auth_user, commit=True)
243 self.session.delete()
243 self.session.delete()
244 return HTTPFound(h.route_path('home'))
244 return HTTPFound(h.route_path('home'))
245
245
246 @HasPermissionAnyDecorator(
246 @HasPermissionAnyDecorator(
247 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
247 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 def register(self, defaults=None, errors=None):
248 def register(self, defaults=None, errors=None):
249 c = self.load_default_context()
249 c = self.load_default_context()
250 defaults = defaults or {}
250 defaults = defaults or {}
251 errors = errors or {}
251 errors = errors or {}
252
252
253 settings = SettingsModel().get_all_settings()
253 settings = SettingsModel().get_all_settings()
254 register_message = settings.get('rhodecode_register_message') or ''
254 register_message = settings.get('rhodecode_register_message') or ''
255 captcha = self._get_captcha_data()
255 captcha = self._get_captcha_data()
256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
256 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
257 .AuthUser().permissions['global']
257 .AuthUser().permissions['global']
258
258
259 render_ctx = self._get_template_context(c)
259 render_ctx = self._get_template_context(c)
260 render_ctx.update({
260 render_ctx.update({
261 'defaults': defaults,
261 'defaults': defaults,
262 'errors': errors,
262 'errors': errors,
263 'auto_active': auto_active,
263 'auto_active': auto_active,
264 'captcha_active': captcha.active,
264 'captcha_active': captcha.active,
265 'captcha_public_key': captcha.public_key,
265 'captcha_public_key': captcha.public_key,
266 'register_message': register_message,
266 'register_message': register_message,
267 })
267 })
268 return render_ctx
268 return render_ctx
269
269
270 @HasPermissionAnyDecorator(
270 @HasPermissionAnyDecorator(
271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
271 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
272 def register_post(self):
272 def register_post(self):
273 from rhodecode.authentication.plugins import auth_rhodecode
273 from rhodecode.authentication.plugins import auth_rhodecode
274
274
275 self.load_default_context()
275 self.load_default_context()
276 captcha = self._get_captcha_data()
276 captcha = self._get_captcha_data()
277 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
277 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
278 .AuthUser().permissions['global']
278 .AuthUser().permissions['global']
279
279
280 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
280 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
281 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
281 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
282
282
283 register_form = RegisterForm(self.request.translate)()
283 register_form = RegisterForm(self.request.translate)()
284 try:
284 try:
285
285
286 form_result = register_form.to_python(self.request.POST)
286 form_result = register_form.to_python(self.request.POST)
287 form_result['active'] = auto_active
287 form_result['active'] = auto_active
288 external_identity = self.request.POST.get('external_identity')
288 external_identity = self.request.POST.get('external_identity')
289
289
290 if external_identity:
290 if external_identity:
291 extern_name = external_identity
291 extern_name = external_identity
292 extern_type = external_identity
292 extern_type = external_identity
293
293
294 if captcha.active:
294 if captcha.active:
295 captcha_status, captcha_message = self.validate_captcha(
295 captcha_status, captcha_message = self.validate_captcha(
296 captcha.private_key)
296 captcha.private_key)
297
297
298 if not captcha_status:
298 if not captcha_status:
299 _value = form_result
299 _value = form_result
300 _msg = _('Bad captcha')
300 _msg = _('Bad captcha')
301 error_dict = {'recaptcha_field': captcha_message}
301 error_dict = {'recaptcha_field': captcha_message}
302 raise formencode.Invalid(
302 raise formencode.Invalid(
303 _msg, _value, None, error_dict=error_dict)
303 _msg, _value, None, error_dict=error_dict)
304
304
305 new_user = UserModel().create_registration(
305 new_user = UserModel().create_registration(
306 form_result, extern_name=extern_name, extern_type=extern_type)
306 form_result, extern_name=extern_name, extern_type=extern_type)
307
307
308 action_data = {'data': new_user.get_api_data(),
308 action_data = {'data': new_user.get_api_data(),
309 'user_agent': self.request.user_agent}
309 'user_agent': self.request.user_agent}
310
310
311 if external_identity:
311 if external_identity:
312 action_data['external_identity'] = external_identity
312 action_data['external_identity'] = external_identity
313
313
314 audit_user = audit_logger.UserWrap(
314 audit_user = audit_logger.UserWrap(
315 username=new_user.username,
315 username=new_user.username,
316 user_id=new_user.user_id,
316 user_id=new_user.user_id,
317 ip_addr=self.request.remote_addr)
317 ip_addr=self.request.remote_addr)
318
318
319 audit_logger.store_web(
319 audit_logger.store_web(
320 'user.register', action_data=action_data,
320 'user.register', action_data=action_data,
321 user=audit_user)
321 user=audit_user)
322
322
323 event = UserRegistered(user=new_user, session=self.session)
323 event = UserRegistered(user=new_user, session=self.session)
324 trigger(event)
324 trigger(event)
325 h.flash(
325 h.flash(
326 _('You have successfully registered with RhodeCode. You can log-in now.'),
326 _('You have successfully registered with RhodeCode. You can log-in now.'),
327 category='success')
327 category='success')
328 if external_identity:
328 if external_identity:
329 h.flash(
329 h.flash(
330 _('Please use the {identity} button to log-in').format(
330 _('Please use the {identity} button to log-in').format(
331 identity=external_identity),
331 identity=external_identity),
332 category='success')
332 category='success')
333 Session().commit()
333 Session().commit()
334
334
335 redirect_ro = self.request.route_path('login')
335 redirect_ro = self.request.route_path('login')
336 raise HTTPFound(redirect_ro)
336 raise HTTPFound(redirect_ro)
337
337
338 except formencode.Invalid as errors:
338 except formencode.Invalid as errors:
339 errors.value.pop('password', None)
339 errors.value.pop('password', None)
340 errors.value.pop('password_confirmation', None)
340 errors.value.pop('password_confirmation', None)
341 return self.register(
341 return self.register(
342 defaults=errors.value, errors=errors.error_dict)
342 defaults=errors.value, errors=errors.error_dict)
343
343
344 except UserCreationError as e:
344 except UserCreationError as e:
345 # container auth or other auth functions that create users on
345 # container auth or other auth functions that create users on
346 # the fly can throw this exception signaling that there's issue
346 # the fly can throw this exception signaling that there's issue
347 # with user creation, explanation should be provided in
347 # with user creation, explanation should be provided in
348 # Exception itself
348 # Exception itself
349 h.flash(e, category='error')
349 h.flash(e, category='error')
350 return self.register()
350 return self.register()
351
351
352 def password_reset(self):
352 def password_reset(self):
353 c = self.load_default_context()
353 c = self.load_default_context()
354 captcha = self._get_captcha_data()
354 captcha = self._get_captcha_data()
355
355
356 template_context = {
356 template_context = {
357 'captcha_active': captcha.active,
357 'captcha_active': captcha.active,
358 'captcha_public_key': captcha.public_key,
358 'captcha_public_key': captcha.public_key,
359 'defaults': {},
359 'defaults': {},
360 'errors': {},
360 'errors': {},
361 }
361 }
362
362
363 # always send implicit message to prevent from discovery of
363 # always send implicit message to prevent from discovery of
364 # matching emails
364 # matching emails
365 msg = _('If such email exists, a password reset link was sent to it.')
365 msg = _('If such email exists, a password reset link was sent to it.')
366
366
367 def default_response():
367 def default_response():
368 log.debug('faking response on invalid password reset')
368 log.debug('faking response on invalid password reset')
369 # make this take 2s, to prevent brute forcing.
369 # make this take 2s, to prevent brute forcing.
370 time.sleep(2)
370 time.sleep(2)
371 h.flash(msg, category='success')
371 h.flash(msg, category='success')
372 return HTTPFound(self.request.route_path('reset_password'))
372 return HTTPFound(self.request.route_path('reset_password'))
373
373
374 if self.request.POST:
374 if self.request.POST:
375 if h.HasPermissionAny('hg.password_reset.disabled')():
375 if h.HasPermissionAny('hg.password_reset.disabled')():
376 _email = self.request.POST.get('email', '')
376 _email = self.request.POST.get('email', '')
377 log.error('Failed attempt to reset password for `%s`.', _email)
377 log.error('Failed attempt to reset password for `%s`.', _email)
378 h.flash(_('Password reset has been disabled.'), category='error')
378 h.flash(_('Password reset has been disabled.'), category='error')
379 return HTTPFound(self.request.route_path('reset_password'))
379 return HTTPFound(self.request.route_path('reset_password'))
380
380
381 password_reset_form = PasswordResetForm(self.request.translate)()
381 password_reset_form = PasswordResetForm(self.request.translate)()
382 description = 'Generated token for password reset from {}'.format(
382 description = 'Generated token for password reset from {}'.format(
383 datetime.datetime.now().isoformat())
383 datetime.datetime.now().isoformat())
384
384
385 try:
385 try:
386 form_result = password_reset_form.to_python(
386 form_result = password_reset_form.to_python(
387 self.request.POST)
387 self.request.POST)
388 user_email = form_result['email']
388 user_email = form_result['email']
389
389
390 if captcha.active:
390 if captcha.active:
391 captcha_status, captcha_message = self.validate_captcha(
391 captcha_status, captcha_message = self.validate_captcha(
392 captcha.private_key)
392 captcha.private_key)
393
393
394 if not captcha_status:
394 if not captcha_status:
395 _value = form_result
395 _value = form_result
396 _msg = _('Bad captcha')
396 _msg = _('Bad captcha')
397 error_dict = {'recaptcha_field': captcha_message}
397 error_dict = {'recaptcha_field': captcha_message}
398 raise formencode.Invalid(
398 raise formencode.Invalid(
399 _msg, _value, None, error_dict=error_dict)
399 _msg, _value, None, error_dict=error_dict)
400
400
401 # Generate reset URL and send mail.
401 # Generate reset URL and send mail.
402 user = User.get_by_email(user_email)
402 user = User.get_by_email(user_email)
403
403
404 # only allow rhodecode based users to reset their password
404 # only allow rhodecode based users to reset their password
405 # external auth shouldn't allow password reset
405 # external auth shouldn't allow password reset
406 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
406 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
407 log.warning('User %s with external type `%s` tried a password reset. '
407 log.warning('User %s with external type `%s` tried a password reset. '
408 'This try was rejected', user, user.extern_type)
408 'This try was rejected', user, user.extern_type)
409 return default_response()
409 return default_response()
410
410
411 # generate password reset token that expires in 10 minutes
411 # generate password reset token that expires in 10 minutes
412 reset_token = UserModel().add_auth_token(
412 reset_token = UserModel().add_auth_token(
413 user=user, lifetime_minutes=10,
413 user=user, lifetime_minutes=10,
414 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
414 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
415 description=description)
415 description=description)
416 Session().commit()
416 Session().commit()
417
417
418 log.debug('Successfully created password recovery token')
418 log.debug('Successfully created password recovery token')
419 password_reset_url = self.request.route_url(
419 password_reset_url = self.request.route_url(
420 'reset_password_confirmation',
420 'reset_password_confirmation',
421 _query={'key': reset_token.api_key})
421 _query={'key': reset_token.api_key})
422 UserModel().reset_password_link(
422 UserModel().reset_password_link(
423 form_result, password_reset_url)
423 form_result, password_reset_url)
424
424
425 action_data = {'email': user_email,
425 action_data = {'email': user_email,
426 'user_agent': self.request.user_agent}
426 'user_agent': self.request.user_agent}
427 audit_logger.store_web(
427 audit_logger.store_web(
428 'user.password.reset_request', action_data=action_data,
428 'user.password.reset_request', action_data=action_data,
429 user=self._rhodecode_user, commit=True)
429 user=self._rhodecode_user, commit=True)
430
430
431 return default_response()
431 return default_response()
432
432
433 except formencode.Invalid as errors:
433 except formencode.Invalid as errors:
434 template_context.update({
434 template_context.update({
435 'defaults': errors.value,
435 'defaults': errors.value,
436 'errors': errors.error_dict,
436 'errors': errors.error_dict,
437 })
437 })
438 if not self.request.POST.get('email'):
438 if not self.request.POST.get('email'):
439 # case of empty email, we want to report that
439 # case of empty email, we want to report that
440 return self._get_template_context(c, **template_context)
440 return self._get_template_context(c, **template_context)
441
441
442 if 'recaptcha_field' in errors.error_dict:
442 if 'recaptcha_field' in errors.error_dict:
443 # case of failed captcha
443 # case of failed captcha
444 return self._get_template_context(c, **template_context)
444 return self._get_template_context(c, **template_context)
445
445
446 return default_response()
446 return default_response()
447
447
448 return self._get_template_context(c, **template_context)
448 return self._get_template_context(c, **template_context)
449
449
450 def password_reset_confirmation(self):
450 def password_reset_confirmation(self):
451 self.load_default_context()
451 self.load_default_context()
452
452
453 if key := self.request.GET.get('key'):
453 if key := self.request.GET.get('key'):
454 # make this take 2s, to prevent brute forcing.
454 # make this take 2s, to prevent brute forcing.
455 time.sleep(2)
455 time.sleep(2)
456
456
457 token = AuthTokenModel().get_auth_token(key)
457 token = AuthTokenModel().get_auth_token(key)
458
458
459 # verify token is the correct role
459 # verify token is the correct role
460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
460 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
461 log.debug('Got token with role:%s expected is %s',
461 log.debug('Got token with role:%s expected is %s',
462 getattr(token, 'role', 'EMPTY_TOKEN'),
462 getattr(token, 'role', 'EMPTY_TOKEN'),
463 UserApiKeys.ROLE_PASSWORD_RESET)
463 UserApiKeys.ROLE_PASSWORD_RESET)
464 h.flash(
464 h.flash(
465 _('Given reset token is invalid'), category='error')
465 _('Given reset token is invalid'), category='error')
466 return HTTPFound(self.request.route_path('reset_password'))
466 return HTTPFound(self.request.route_path('reset_password'))
467
467
468 try:
468 try:
469 owner = token.user
469 owner = token.user
470 data = {'email': owner.email, 'token': token.api_key}
470 data = {'email': owner.email, 'token': token.api_key}
471 UserModel().reset_password(data)
471 UserModel().reset_password(data)
472 h.flash(
472 h.flash(
473 _('Your password reset was successful, '
473 _('Your password reset was successful, '
474 'a new password has been sent to your email'),
474 'a new password has been sent to your email'),
475 category='success')
475 category='success')
476 except Exception as e:
476 except Exception as e:
477 log.error(e)
477 log.error(e)
478 return HTTPFound(self.request.route_path('reset_password'))
478 return HTTPFound(self.request.route_path('reset_password'))
479
479
480 return HTTPFound(self.request.route_path('login'))
480 return HTTPFound(self.request.route_path('login'))
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 def setup_2fa(self):
484 def setup_2fa(self):
485 _ = self.request.translate
485 _ = self.request.translate
486 c = self.load_default_context()
486 c = self.load_default_context()
487 user_instance = self._rhodecode_db_user
487 user_instance = self._rhodecode_db_user
488 form = TOTPForm(_, user_instance)()
488 form = TOTPForm(_, user_instance)()
489 render_ctx = {}
489 render_ctx = {}
490 if self.request.method == 'POST':
490 if self.request.method == 'POST':
491 post_items = dict(self.request.POST)
491 post_items = dict(self.request.POST)
492
492
493 try:
493 try:
494 form_details = form.to_python(post_items)
494 form_details = form.to_python(post_items)
495 secret = form_details['secret_totp']
495 secret = form_details['secret_totp']
496
496
497 user_instance.init_2fa_recovery_codes(persist=True, force=True)
497 user_instance.init_2fa_recovery_codes(persist=True, force=True)
498 user_instance.2fa_secret = secret
498 user_instance.secret_2fa = secret
499
499
500 Session().commit()
500 Session().commit()
501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
501 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
502 except formencode.Invalid as errors:
502 except formencode.Invalid as errors:
503 defaults = errors.value
503 defaults = errors.value
504 render_ctx = {
504 render_ctx = {
505 'errors': errors.error_dict,
505 'errors': errors.error_dict,
506 'defaults': defaults,
506 'defaults': defaults,
507 }
507 }
508
508
509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
509 # NOTE: here we DO NOT persist the secret 2FA, since this is only for setup, once a setup is completed
510 # only then we should persist it
510 # only then we should persist it
511 secret = user_instance.init_secret_2fa(persist=False)
511 secret = user_instance.init_secret_2fa(persist=False)
512
512
513 instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode')
513 instance_name = rhodecode.ConfigGet().get_str('app.base_url', 'rhodecode')
514 totp_name = f'{instance_name}:{self.request.user.username}'
514 totp_name = f'{instance_name}:{self.request.user.username}'
515
515
516 qr = qrcode.QRCode(version=1, box_size=5, border=4)
516 qr = qrcode.QRCode(version=1, box_size=5, border=4)
517 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
517 qr.add_data(pyotp.totp.TOTP(secret).provisioning_uri(name=totp_name))
518 qr.make(fit=True)
518 qr.make(fit=True)
519 img = qr.make_image(fill_color='black', back_color='white')
519 img = qr.make_image(fill_color='black', back_color='white')
520 buffered = BytesIO()
520 buffered = BytesIO()
521 img.save(buffered)
521 img.save(buffered)
522 return self._get_template_context(
522 return self._get_template_context(
523 c,
523 c,
524 qr=b64encode(buffered.getvalue()).decode("utf-8"),
524 qr=b64encode(buffered.getvalue()).decode("utf-8"),
525 key=secret,
525 key=secret,
526 totp_name=totp_name,
526 totp_name=totp_name,
527 ** render_ctx
527 ** render_ctx
528 )
528 )
529
529
530 @LoginRequired()
530 @LoginRequired()
531 @NotAnonymous()
531 @NotAnonymous()
532 def verify_2fa(self):
532 def verify_2fa(self):
533 _ = self.request.translate
533 _ = self.request.translate
534 c = self.load_default_context()
534 c = self.load_default_context()
535 render_ctx = {}
535 render_ctx = {}
536 user_instance = self._rhodecode_db_user
536 user_instance = self._rhodecode_db_user
537 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
537 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
538 if self.request.method == 'POST':
538 if self.request.method == 'POST':
539 post_items = dict(self.request.POST)
539 post_items = dict(self.request.POST)
540 # NOTE: inject secret, as it's a post configured saved item.
540 # NOTE: inject secret, as it's a post configured saved item.
541 post_items['secret_totp'] = user_instance.secret_2fa
541 post_items['secret_totp'] = user_instance.secret_2fa
542 try:
542 try:
543 totp_form.to_python(post_items)
543 totp_form.to_python(post_items)
544 user_instance.check_2fa_required = False
544 user_instance.check_2fa_required = False
545 Session().commit()
545 Session().commit()
546 raise HTTPFound(c.came_from)
546 raise HTTPFound(c.came_from)
547 except formencode.Invalid as errors:
547 except formencode.Invalid as errors:
548 defaults = errors.value
548 defaults = errors.value
549 render_ctx = {
549 render_ctx = {
550 'errors': errors.error_dict,
550 'errors': errors.error_dict,
551 'defaults': defaults,
551 'defaults': defaults,
552 }
552 }
553 return self._get_template_context(c, **render_ctx)
553 return self._get_template_context(c, **render_ctx)
General Comments 0
You need to be logged in to leave comments. Login now