jumpserver/apps/users/views/profile/reset.py

239 lines
8.9 KiB
Python

# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
import time
from django.conf import settings
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import FormView, RedirectView
from authentication.errors import IntervalTooShort
from authentication.utils import check_user_property_is_correct
from common.const.choices import COUNTRY_CALLING_CODES
from common.utils import FlashMessageUtil, get_object_or_none, random_string
from common.utils.verify_code import SendAndVerifyCodeUtil
from users.notifications import ResetPasswordSuccessMsg
from ... import forms
from ...models import User
from ...utils import check_password_rules, get_password_check_rules
__all__ = [
'UserLoginView',
'UserResetPasswordView',
'UserForgotPasswordView',
'UserForgotPasswordPreviewingView',
]
class UserLoginView(RedirectView):
url = reverse_lazy('authentication:login')
query_string = True
class UserForgotPasswordPreviewingView(FormView):
template_name = 'users/forgot_password_previewing.html'
form_class = forms.UserForgotPasswordPreviewingForm
@staticmethod
def get_redirect_url(token):
return reverse('authentication:forgot-password') + '?token=%s' % token
@staticmethod
def generate_previewing_token(user):
sent_ttl = 60
token_sent_at_key = '{}_send_at'.format(user.username)
token_sent_at = cache.get(token_sent_at_key, 0)
if token_sent_at:
raise IntervalTooShort(sent_ttl)
token = random_string(36)
user_info = {'username': user.username, 'phone': user.phone, 'email': user.email}
cache.set(token, user_info, 5 * 60)
cache.set(token_sent_at_key, time.time(), sent_ttl)
return token
def form_valid(self, form):
username = form.cleaned_data['username']
user = get_object_or_none(User, username=username)
if not user:
form.add_error('username', _('User does not exist: {}').format(username))
return super().form_invalid(form)
if settings.ONLY_ALLOW_AUTH_FROM_SOURCE and not user.is_local:
error = _('Non-local users can log in only from third-party platforms '
'and cannot change their passwords: {}').format(username)
form.add_error('username', error)
return super().form_invalid(form)
try:
token = self.generate_previewing_token(user)
except IntervalTooShort as e:
form.add_error('username', e)
return super().form_invalid(form)
return redirect(self.get_redirect_url(token))
class UserForgotPasswordView(FormView):
template_name = 'users/forgot_password.html'
form_class = forms.UserForgotPasswordForm
def get(self, request, *args, **kwargs):
token = self.request.GET.get('token')
userinfo = cache.get(token)
if not userinfo:
return redirect(self.get_redirect_url(return_previewing=True))
else:
context = self.get_context_data(has_phone=bool(userinfo['phone']))
return self.render_to_response(context)
@staticmethod
def get_validate_backends_context(has_phone):
validate_backends = [{'name': _('Email'), 'is_active': True, 'value': 'email'}]
if settings.XPACK_LICENSE_IS_VALID:
if settings.SMS_ENABLED and has_phone:
is_active = True
else:
is_active = False
sms_backend = {'name': _('SMS'), 'is_active': is_active, 'value': 'sms'}
validate_backends.append(sms_backend)
return {'validate_backends': validate_backends}
def get_context_data(self, has_phone=False, **kwargs):
context = super().get_context_data(**kwargs)
form = context['form']
cleaned_data = getattr(form, 'cleaned_data', {})
for k, v in cleaned_data.items():
if v:
context[k] = v
context['countries'] = COUNTRY_CALLING_CODES
context['form_type'] = 'email'
context['XPACK_ENABLED'] = settings.XPACK_ENABLED
validate_backends = self.get_validate_backends_context(has_phone)
context.update(validate_backends)
return context
@staticmethod
def get_redirect_url(user=None, return_previewing=False):
if not user and return_previewing:
return reverse('authentication:forgot-previewing')
query_params = '?token=%s' % user.generate_reset_token()
reset_password_url = reverse('authentication:reset-password')
return reset_password_url + query_params
@staticmethod
def safe_verify_code(token, target, form_type, code):
token_verified_key = '{}_verified'.format(token)
token_verified_times = cache.get(token_verified_key, 0)
if token_verified_times >= 3:
cache.delete(token)
raise ValueError('Verification code has been used more than 3 times, please re-verify')
cache.set(token_verified_key, token_verified_times + 1, 5 * 60)
sender_util = SendAndVerifyCodeUtil(target, backend=form_type)
return sender_util.verify(code)
def form_valid(self, form):
token = self.request.GET.get('token')
user_info = cache.get(token)
if not user_info:
return redirect(self.get_redirect_url(return_previewing=True))
username = user_info.get('username')
form_type = form.cleaned_data['form_type']
target = form.cleaned_data[form_type]
code = form.cleaned_data['code']
country_code = form.cleaned_data.get('country_code', '')
query_key = form_type
if form_type == 'sms':
query_key = 'phone'
target = '{}{}'.format(country_code, target)
try:
self.safe_verify_code(token, target, form_type, code)
except ValueError as e:
return redirect(self.get_redirect_url(return_previewing=True))
except Exception as e:
form.add_error('code', str(e))
return super().form_invalid(form)
user = check_user_property_is_correct(username, **{query_key: target})
if not user:
form.add_error('code', _('No user matched'))
return super().form_invalid(form)
return redirect(self.get_redirect_url(user))
class UserResetPasswordView(FormView):
template_name = 'users/reset_password.html'
form_class = forms.UserTokenResetPasswordForm
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
errors = kwargs.get('errors')
if errors:
context['errors'] = errors
return self.render_to_response(context)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
token = self.request.GET.get('token', '')
user = User.validate_reset_password_token(token)
if not user:
context['errors'] = _('Token invalid or expired')
context['token_invalid'] = True
else:
check_rules = get_password_check_rules(user)
context['password_check_rules'] = check_rules
return context
def form_valid(self, form):
token = self.request.GET.get('token')
user = User.validate_reset_password_token(token)
if not user:
error = _('Token invalid or expired')
form.add_error('new_password', error)
return self.form_invalid(form)
if not user.can_update_password():
error = _('User auth from {}, go there change password')
form.add_error('new_password', error.format(user.get_source_display()))
return self.form_invalid(form)
password = form.cleaned_data['new_password']
is_ok = check_password_rules(password, is_org_admin=user.is_org_admin)
if not is_ok:
error = _('* Your password does not meet the requirements')
form.add_error('new_password', error)
return self.form_invalid(form)
if user.is_history_password(password):
limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
error = _('* The new password cannot be the last {} passwords').format(
limit_count
)
form.add_error('new_password', error)
return self.form_invalid(form)
user.reset_password(password)
User.expired_reset_password_token(token)
ResetPasswordSuccessMsg(user, self.request).publish_async()
url = self.get_redirect_url()
return redirect(url)
@staticmethod
def get_redirect_url():
message_data = {
'title': _('Reset password success'),
'message': _('Reset password success, return to login page'),
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
return FlashMessageUtil.gen_message_url(message_data)