-
-
Notifications
You must be signed in to change notification settings - Fork 508
Fix adding roles when identity is username #686
base: develop
Are you sure you want to change the base?
Changes from all commits
30a2e59
d9e199c
33c6d67
d57f6ec
50fae9f
02100c6
26751f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,8 +25,10 @@ | |
from werkzeug.datastructures import ImmutableList | ||
from werkzeug.local import LocalProxy | ||
|
||
from .forms import ChangePasswordForm, ConfirmRegisterForm, \ | ||
ForgotPasswordForm, LoginForm, PasswordlessLoginForm, RegisterForm, \ | ||
from .forms import ChangePasswordForm, \ | ||
EmailConfirmRegisterForm, UsernameConfirmRegisterForm, \ | ||
ForgotPasswordForm, EmailLoginForm, UsernameLoginForm, \ | ||
PasswordlessLoginForm, EmailRegisterForm, UsernameRegisterForm, \ | ||
ResetPasswordForm, SendConfirmationForm | ||
from .utils import config_value as cv | ||
from .utils import _, get_config, hash_data, localize_callback, string_types, \ | ||
|
@@ -142,6 +144,8 @@ | |
_('Invalid confirmation token.'), 'error'), | ||
'EMAIL_ALREADY_ASSOCIATED': ( | ||
_('%(email)s is already associated with an account.'), 'error'), | ||
'USERNAME_ALREADY_IN_USE': ( | ||
_('%(username)s is already in use.'), 'error'), | ||
'PASSWORD_MISMATCH': ( | ||
_('Password does not match'), 'error'), | ||
'RETYPE_PASSWORD_MISMATCH': ( | ||
|
@@ -177,6 +181,10 @@ | |
_('Email not provided'), 'error'), | ||
'INVALID_EMAIL_ADDRESS': ( | ||
_('Invalid email address'), 'error'), | ||
'USERNAME_NOT_PROVIDED': ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similarly here I would propose |
||
_('Username not provided'), 'error'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we decide to be more generic the |
||
'INVALID_USERNAME': ( | ||
_('Invalid username'), 'error'), | ||
'PASSWORD_NOT_PROVIDED': ( | ||
_('Password not provided'), 'error'), | ||
'PASSWORD_NOT_SET': ( | ||
|
@@ -205,10 +213,21 @@ | |
_('Please reauthenticate to access this page.'), 'info'), | ||
} | ||
|
||
_default_forms = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather see this part unchanged and add the logic to the forms itself. |
||
'login_form': LoginForm, | ||
'confirm_register_form': ConfirmRegisterForm, | ||
'register_form': RegisterForm, | ||
_default_email_forms = { | ||
'login_form': EmailLoginForm, | ||
'confirm_register_form': EmailConfirmRegisterForm, | ||
'register_form': EmailRegisterForm, | ||
'forgot_password_form': ForgotPasswordForm, | ||
'reset_password_form': ResetPasswordForm, | ||
'change_password_form': ChangePasswordForm, | ||
'send_confirmation_form': SendConfirmationForm, | ||
'passwordless_login_form': PasswordlessLoginForm, | ||
} | ||
|
||
_default_username_forms = { | ||
'login_form': UsernameLoginForm, | ||
'confirm_register_form': UsernameConfirmRegisterForm, | ||
'register_form': UsernameRegisterForm, | ||
'forgot_password_form': ForgotPasswordForm, | ||
'reset_password_form': ResetPasswordForm, | ||
'change_password_form': ChangePasswordForm, | ||
|
@@ -342,7 +361,16 @@ def _get_state(app, datastore, anonymous_user=None, **kwargs): | |
_unauthorized_callback=None | ||
)) | ||
|
||
for key, value in _default_forms.items(): | ||
ident_attrs = app.config.get( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't shorten the variable names - I would prefer |
||
"SECURITY_USER_IDENTITY_ATTRIBUTES", | ||
["email"], | ||
) | ||
if ident_attrs == ["email"]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about more generic condition |
||
default_forms = _default_email_forms | ||
else: | ||
default_forms = _default_username_forms | ||
|
||
for key, value in default_forms.items(): | ||
if key not in kwargs or not kwargs[key]: | ||
kwargs[key] = value | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -118,7 +118,8 @@ def __init__(self, user_model, role_model): | |
|
||
def _prepare_role_modify_args(self, user, role): | ||
if isinstance(user, string_types): | ||
user = self.find_user(email=user) | ||
user_kwargs = {attr: user for attr in get_identity_attributes()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 this is very cool! I really like it - great idea 🎉 |
||
user = self.find_user(**user_kwargs) | ||
if isinstance(role, string_types): | ||
role = self.find_role(role) | ||
return user, role | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
|
||
_default_field_labels = { | ||
'email': _('Email Address'), | ||
'username': _('Username'), | ||
'password': _('Password'), | ||
'remember_me': _('Remember Me'), | ||
'login': _('Login'), | ||
|
@@ -61,12 +62,18 @@ class Email(ValidatorMixin, validators.Email): | |
pass | ||
|
||
|
||
class Regexp(ValidatorMixin, validators.Regexp): | ||
pass | ||
|
||
|
||
class Length(ValidatorMixin, validators.Length): | ||
pass | ||
|
||
|
||
email_required = Required(message='EMAIL_NOT_PROVIDED') | ||
email_validator = Email(message='INVALID_EMAIL_ADDRESS') | ||
username_required = Required(message='USERNAME_NOT_PROVIDED') | ||
username_validator = Regexp(r"[A-Za-z0-9_]+", message='INVALID_USERNAME') | ||
password_required = Required(message='PASSWORD_NOT_PROVIDED') | ||
password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH') | ||
|
||
|
@@ -81,19 +88,34 @@ def unique_user_email(form, field): | |
raise ValidationError(msg) | ||
|
||
|
||
def unique_user_username(form, field): | ||
if _datastore.get_user(field.data) is not None: | ||
msg = get_message('USERNAME_ALREADY_IN_USE', username=field.data)[0] | ||
raise ValidationError(msg) | ||
|
||
|
||
def valid_user_email(form, field): | ||
form.user = _datastore.get_user(field.data) | ||
if form.user is None: | ||
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0]) | ||
|
||
|
||
valid_user_username = valid_user_email | ||
|
||
|
||
class Form(BaseForm): | ||
def __init__(self, *args, **kwargs): | ||
if current_app.testing: | ||
self.TIME_LIMIT = None | ||
super(Form, self).__init__(*args, **kwargs) | ||
|
||
|
||
class IdentifierForm(Form): | ||
def __init__(self, *args, **kwargs): | ||
super(IdentifierForm, self).__init__(*args, **kwargs) | ||
setattr(self, "identifier", getattr(self, self.identifier_field)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO the default could be read from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively we could try something like: class IdentifierField(Field):
def __init__(...):
# autodetect label and validators from current_app.config
class AnyForm(Form):
identifier = IdentifierField() |
||
|
||
|
||
class EmailFormMixin(): | ||
email = StringField( | ||
get_form_field_label('email'), | ||
|
@@ -105,12 +127,41 @@ class UserEmailFormMixin(): | |
email = StringField( | ||
get_form_field_label('email'), | ||
validators=[email_required, email_validator, valid_user_email]) | ||
identifier_field = "email" | ||
|
||
|
||
class UniqueEmailFormMixin(): | ||
email = StringField( | ||
get_form_field_label('email'), | ||
validators=[email_required, email_validator, unique_user_email]) | ||
identifier_field = "email" | ||
|
||
|
||
class UsernameFormMixin(): | ||
username = StringField( | ||
get_form_field_label('username'), | ||
validators=[username_required, username_validator]) | ||
|
||
|
||
class UserUsernameFormMixin(): | ||
user = None | ||
username = StringField( | ||
get_form_field_label('username'), | ||
validators=[ | ||
username_required, username_validator, valid_user_username | ||
] | ||
) | ||
identifier_field = "username" | ||
|
||
|
||
class UniqueUsernameFormMixin(): | ||
username = StringField( | ||
get_form_field_label('username'), | ||
validators=[ | ||
username_required, username_validator, unique_user_username | ||
] | ||
) | ||
identifier_field = "username" | ||
|
||
|
||
class PasswordFormMixin(): | ||
|
@@ -150,11 +201,12 @@ def is_field_and_user_attr(member): | |
hasattr(_datastore.user_model, member.name) | ||
|
||
fields = inspect.getmembers(form, is_field_and_user_attr) | ||
return dict((key, value.data) for key, value in fields) | ||
return dict((key, value.data) for key, value in fields | ||
if key != "identifier") | ||
|
||
|
||
class SendConfirmationForm(Form, UserEmailFormMixin): | ||
"""The default forgot password form""" | ||
"""The default send confirmation form""" | ||
|
||
submit = SubmitField(get_form_field_label('send_confirmation')) | ||
|
||
|
@@ -172,20 +224,34 @@ def validate(self): | |
return True | ||
|
||
|
||
class ForgotPasswordForm(Form, UserEmailFormMixin): | ||
class AbstractForgotPasswordForm(IdentifierForm): | ||
"""The default forgot password form""" | ||
|
||
submit = SubmitField(get_form_field_label('recover_password')) | ||
|
||
def validate(self): | ||
if not super(ForgotPasswordForm, self).validate(): | ||
if not super(AbstractForgotPasswordForm, self).validate(): | ||
return False | ||
if requires_confirmation(self.user): | ||
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) | ||
self.identifier.errors.append( | ||
get_message('CONFIRMATION_REQUIRED')[0] | ||
) | ||
return False | ||
return True | ||
|
||
|
||
class EmailForgotPasswordForm(AbstractForgotPasswordForm, UserEmailFormMixin): | ||
pass | ||
|
||
|
||
class UsernameForgotPasswordForm(AbstractForgotPasswordForm, | ||
UserUsernameFormMixin): | ||
pass | ||
|
||
|
||
ForgotPasswordForm = EmailForgotPasswordForm | ||
|
||
|
||
class PasswordlessLoginForm(Form, UserEmailFormMixin): | ||
"""The passwordless login form""" | ||
|
||
|
@@ -203,18 +269,16 @@ def validate(self): | |
return True | ||
|
||
|
||
class LoginForm(Form, NextFormMixin): | ||
class AbstractLoginForm(IdentifierForm, NextFormMixin): | ||
"""The default login form""" | ||
|
||
email = StringField(get_form_field_label('email'), | ||
validators=[Required(message='EMAIL_NOT_PROVIDED')]) | ||
password = PasswordField(get_form_field_label('password'), | ||
validators=[password_required]) | ||
remember = BooleanField(get_form_field_label('remember_me')) | ||
submit = SubmitField(get_form_field_label('login')) | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(LoginForm, self).__init__(*args, **kwargs) | ||
super(AbstractLoginForm, self).__init__(*args, **kwargs) | ||
if not self.next.data: | ||
self.next.data = request.args.get('next', '') | ||
self.remember.default = config_value('DEFAULT_REMEMBER_ME') | ||
|
@@ -227,42 +291,77 @@ def __init__(self, *args, **kwargs): | |
self.password.description = html | ||
|
||
def validate(self): | ||
if not super(LoginForm, self).validate(): | ||
if not super(AbstractLoginForm, self).validate(): | ||
return False | ||
|
||
self.user = _datastore.get_user(self.email.data) | ||
self.user = _datastore.get_user(self.identifier.data) | ||
|
||
if self.user is None: | ||
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) | ||
self.identifier.errors.append( | ||
get_message('USER_DOES_NOT_EXIST')[0] | ||
) | ||
return False | ||
if not self.user.password: | ||
self.password.errors.append(get_message('PASSWORD_NOT_SET')[0]) | ||
self.identifier.errors.append(get_message('PASSWORD_NOT_SET')[0]) | ||
return False | ||
if not verify_and_update_password(self.password.data, self.user): | ||
self.password.errors.append(get_message('INVALID_PASSWORD')[0]) | ||
return False | ||
if requires_confirmation(self.user): | ||
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) | ||
self.identifier.errors.append( | ||
get_message('CONFIRMATION_REQUIRED')[0] | ||
) | ||
return False | ||
if not self.user.is_active: | ||
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) | ||
self.identifier.errors.append(get_message('DISABLED_ACCOUNT')[0]) | ||
return False | ||
return True | ||
|
||
|
||
class ConfirmRegisterForm(Form, RegisterFormMixin, | ||
UniqueEmailFormMixin, NewPasswordFormMixin): | ||
class EmailLoginForm(AbstractLoginForm, UserEmailFormMixin): | ||
pass | ||
|
||
|
||
class UsernameLoginForm(AbstractLoginForm, UserUsernameFormMixin): | ||
pass | ||
|
||
|
||
LoginForm = EmailLoginForm | ||
|
||
|
||
class EmailConfirmRegisterForm(IdentifierForm, RegisterFormMixin, | ||
UniqueEmailFormMixin, NewPasswordFormMixin): | ||
pass | ||
|
||
|
||
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin, | ||
NextFormMixin): | ||
class UsernameConfirmRegisterForm(IdentifierForm, RegisterFormMixin, | ||
UniqueUsernameFormMixin, | ||
NewPasswordFormMixin): | ||
pass | ||
|
||
|
||
ConfirmRegisterForm = EmailConfirmRegisterForm | ||
|
||
|
||
class EmailRegisterForm(EmailConfirmRegisterForm, PasswordConfirmFormMixin, | ||
NextFormMixin): | ||
def __init__(self, *args, **kwargs): | ||
super(EmailRegisterForm, self).__init__(*args, **kwargs) | ||
if not self.next.data: | ||
self.next.data = request.args.get('next', '') | ||
|
||
|
||
class UsernameRegisterForm(UsernameConfirmRegisterForm, | ||
PasswordConfirmFormMixin, NextFormMixin): | ||
def __init__(self, *args, **kwargs): | ||
super(RegisterForm, self).__init__(*args, **kwargs) | ||
super(UsernameRegisterForm, self).__init__(*args, **kwargs) | ||
if not self.next.data: | ||
self.next.data = request.args.get('next', '') | ||
|
||
|
||
RegisterForm = EmailRegisterForm | ||
|
||
|
||
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): | ||
"""The default reset password form""" | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we go with something more generic like:
IDENTIFIER_ALREADY_IN_USE
?