import re from logging import getLogger from django.conf import settings from django import forms from django.db.models import Q from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm as _PasswordResetForm from django.contrib.auth.tokens import default_token_generator from django.utils.translation import ugettext_lazy as _ from django.utils.http import urlsafe_base64_encode from django.utils.encoding import force_bytes from api.email import sendmail from api.sms.views import internal_send as send_sms from gui.models import User, UserProfile # noinspection PyProtectedMember from gui.widgets import EmailInput, TelPrefixInput, clean_international_phonenumber from gui.accounts.utils import generate_key, set_attempts_to_cache from gui.profile.forms import PasswordForm from vms.models import DefaultDc logger = getLogger(__name__) UNUSABLE_PASSWORD = '!' class LoginForm(AuthenticationForm): """ A form that is used for authenticating user into system. """ def __init__(self, request, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) self.request = request self.fields['username'].label = _('username or email') self.fields['username'].widget = EmailInput( attrs={'class': 'input-transparent', 'placeholder': _('Username or Email'), 'required': 'required'}, ) self.fields['password'].widget = forms.PasswordInput( render_value=False, attrs={'class': 'input-transparent', 'placeholder': _('Password'), 'required': 'required'}, ) def clean_username(self): return self.cleaned_data['username'].lower() def clean(self): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') if username and password: self.user_cache = authenticate(username=username, password=password) key, timeout = generate_key(self.request, username, 'login') attempts, timeout = set_attempts_to_cache(key, timeout) if self.user_cache is None or attempts > 3 or ( settings.SECURITY_OWASP_AT_002 and not self.user_cache.is_active): if attempts > 3: logger.warning('Ignoring login request from user "%s", %s attempts lock will expire in %s seconds.', username, attempts, timeout) if not settings.SECURITY_OWASP_AT_002: self.error_messages['invalid_login'] = _('You made %(attempts)s wrong login attempts, all ' 'further attempts will be ignored for %(timeout)s ' 'seconds.') % {'attempts': attempts, 'timeout': timeout} raise forms.ValidationError(self.error_messages['invalid_login'], code='invalid_login', params={'username': self.username_field.verbose_name}) elif not self.user_cache.is_active: raise forms.ValidationError(self.error_messages['inactive'], code='inactive') return self.cleaned_data class ForgotForm(_PasswordResetForm): """ A form that lets a user change,set his/her password without entering the old password. """ def __init__(self, request, *args, **kwargs): super(ForgotForm, self).__init__(*args, **kwargs) self.request = request self.fields['email'].widget.attrs = { 'class': 'input-transparent', 'placeholder': _('Your email address'), 'required': 'required' } self.fields['email'].help_text = _('You will receive an email with instructions on how to reset your password.') # noinspection PyArgumentList,PyUnusedLocal def clean_email(self, *args, **kwargs): """ We never raise an ValidationError, because user Enumeration and Guessable User Account OWASP-AT-002. Change this behaviour with settings.SECURITY_OWASP_AT_002. """ email = self.cleaned_data['email'] users = User.objects.filter(email__iexact=email, is_active=True).exclude(password=UNUSABLE_PASSWORD) self.users_cache = [] # Check for valid user: if users: key, timeout = generate_key(self.request, email, 'forgot') attempts, timeout = set_attempts_to_cache(key, timeout) # Don't allow a password reset if user is trying more than 2 time in calculated timeout if attempts > 2: logger.warning('Ignoring password reset request from user "%s", %s attempts lock will expire in %s ' 'seconds.', email, attempts, timeout) if settings.SECURITY_OWASP_AT_002: return email else: raise forms.ValidationError(_('You have requested password reset %(attempts)s times. ' 'All further attempts will be ignored for %(timeout)s ' 'seconds.') % {'attempts': attempts, 'timeout': timeout}) else: logger.warning('Ignoring password reset request from invalid user "%s"', email) if settings.SECURITY_OWASP_AT_002: return email else: raise forms.ValidationError(_("That email address doesn't have an associated user account. Are you " "sure you've registered?")) # A valid user is part of a self.users_cache list used in save() # noinspection PyAttributeOutsideInit self.users_cache = users return email def save(self, domain_override=None, subject_template_name='registration/password_reset_subject.txt', email_template_name='registration/password_reset_email.html', use_https=False, token_generator=default_token_generator, **kwargs): # Complete override, because we have to use our sendmail() for user in self.users_cache: # Update verification token profile = user.userprofile profile.email_token = token_generator.make_token(user) profile.save() sendmail(user, subject_template_name, email_template_name, extra_context={ 'user': user, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'token': profile.email_token, 'protocol': use_https and 'https' or 'http', }) class SMSSendPasswordResetForm(object): """ Dummy "password reset" form used by django.contrib.auth.forms.password_reset_confirm. """ # noinspection PyUnusedLocal def __init__(self, user, data=None): self.user = user def is_valid(self): return self.user.is_active def save(self): password = User.objects.make_random_password(length=7) self.user.set_password(password) self.user.save() msg = _('Your password at %(site_name)s has been reset to: %(password)s') % { 'site_name': self.user.current_dc.settings.SITE_NAME, 'password': password, } send_sms(self.user.userprofile.phone, msg) return None class PasswordResetForm(PasswordForm): def __init__(self, *args, **kwargs): super(PasswordResetForm, self).__init__(*args, **kwargs) self.fields['password1'].widget.attrs['placeholder'] = _('New password') self.fields['password2'].widget.attrs['placeholder'] = _('Confirm new password') # noinspection PyMethodOverriding def save(self): logger.info('Changing password for user "%s"', self.user.username) self.user.set_password(self.cleaned_data['password1']) self.user.save() return None class _RegisterForm(forms.ModelForm): """ User details registration form, for basic user data (Django users table) """ email_help_text = _('You will receive an email to activate your account.') class Meta: model = User fields = ['first_name', 'last_name', 'email'] widgets = { 'email': EmailInput(attrs={'class': 'input-transparent', 'placeholder': _('Email address'), 'required': 'required', 'maxlength': 254}), 'first_name': forms.TextInput(attrs={'class': 'input-transparent', 'placeholder': _('First name'), 'required': 'required', 'maxlength': 30}), 'last_name': forms.TextInput(attrs={'class': 'input-transparent', 'placeholder': _('Last name'), 'required': 'required', 'maxlength': 30}), } def __init__(self, *args, **kwargs): super(_RegisterForm, self).__init__(*args, **kwargs) self.fields['first_name'].required = True self.fields['last_name'].required = True self.fields['email'].required = True self.fields['email'].help_text = self.email_help_text # noinspection PyUnusedLocal def clean_email(self, *args, **kwargs): email = self.cleaned_data['email'] if User.objects.filter(Q(email__iexact=email) | Q(username__iexact=email)).exists(): raise forms.ValidationError(_('This email address is already in use. Please supply a different email ' 'address.')) email_clean_regex = re.compile("^[A-Za-z0-9@.+_-]+$") if email_clean_regex.match(email) is None: raise forms.ValidationError(_('Sorry. Your email address did not pass the validity test.')) return email class RegisterForm(_RegisterForm): password = forms.CharField( label=_('Password'), required=True, min_length=6, widget=forms.PasswordInput(render_value=False, attrs={ 'class': 'input-transparent', 'placeholder': _('New password'), 'required': 'required', 'pattern': '.{6,}', 'title': _('6 characters minimum'), }), ) def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) dc1_settings = DefaultDc().settings if dc1_settings.SMS_REGISTRATION_ENABLED: del self.fields['password'] def save(self, *args, **kwargs): password = self.cleaned_data.pop('password', None) user = super(RegisterForm, self).save(*args, **kwargs) if password: user.set_password(password) return user class UserProfileRegisterForm(forms.ModelForm): """ User profile registration form, for extended data collected about user (gui users table) """ include_company = False phone_help_text = _('You will receive a text message (SMS) with password.') class Meta: model = UserProfile fields = ['phone', 'tos_acceptation', 'company', 'companyid', 'country', 'timezone', 'language'] widgets = { 'phone': TelPrefixInput(attrs={ 'class': 'input-transparent', 'placeholder': _('Phone'), 'maxlength': 32, 'erase_on_empty_input': True, }), 'tos_acceptation': forms.CheckboxInput(attrs={ 'class': 'normal-check', 'placeholder': _('TOS Confirmation'), # 'required': 'required' # Do not use the HTML5 required attribute # Browser support on checkboxes lacks behind }), 'company': forms.TextInput(attrs={ 'class': 'input-transparent', 'placeholder': _('Company'), 'required': 'required', 'maxlength': 255 }), 'companyid': forms.TextInput(attrs={ 'class': 'input-transparent', 'placeholder': _('Company ID'), 'required': 'required', 'maxlength': 64 }), 'country': forms.HiddenInput(), 'timezone': forms.HiddenInput(), 'language': forms.HiddenInput(), } def __init__(self, *args, **kwargs): super(UserProfileRegisterForm, self).__init__(*args, **kwargs) dc1_settings = DefaultDc().settings if dc1_settings.PROFILE_PHONE_REQUIRED or dc1_settings.SMS_REGISTRATION_ENABLED: self.fields['phone'].widget.attrs['required'] = 'required' self.fields['phone'].widget.erase_on_empty_input = False self.fields['phone'].required = True if dc1_settings.SMS_REGISTRATION_ENABLED: self.fields['phone'].help_text = self.phone_help_text else: del self.fields['phone'] if not dc1_settings.TOS_LINK: del self.fields['tos_acceptation'] if self.include_company: self.fields['company'].required = True self.fields['companyid'].required = True else: del self.fields['company'] del self.fields['companyid'] def clean_phone(self): return clean_international_phonenumber(self.cleaned_data['phone']) def clean_tos_acceptation(self): data = self.cleaned_data['tos_acceptation'] if not data: raise forms.ValidationError(_('In order to use this service, you have to accept the Terms of Service.')) return data def save(self, *args, **kwargs): # a "hack" to get the actual instance alive instance = kwargs.pop('instance', None) if instance: self.instance = forms.models.construct_instance(self, instance, self._meta.fields, self._meta.exclude) return super(UserProfileRegisterForm, self).save(*args, **kwargs)