From b1ac2598c0601547d226e64e8beac804539f0428 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Wed, 16 Oct 2013 10:55:56 -0400 Subject: [PATCH 01/18] Be sure to save user model when using `add_role_to_user` method for MongoEngineUserDatastore. Fixes #170 --- flask_security/datastore.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flask_security/datastore.py b/flask_security/datastore.py index b454dc8..4716931 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -234,6 +234,12 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): def find_role(self, role): return self.role_model.objects(name=role).first() + def add_role_to_user(self, user, role): + rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role) + if rv: + self.put(user) + return rv + class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): """A PeeweeD datastore implementation for Flask-Security that assumes From 9999325ffb2f6a7824f5f0d2a3dea58779f77129 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Wed, 16 Oct 2013 11:15:17 -0400 Subject: [PATCH 02/18] Show an error if a user tries to change their password and its the same as before. Fixes #160 --- flask_security/core.py | 1 + flask_security/forms.py | 5 ++++- tests/configured_tests.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/flask_security/core.py b/flask_security/core.py index dfb49ac..9ec4284 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -116,6 +116,7 @@ _default_messages = { 'INVALID_PASSWORD': ('Invalid password', 'error'), 'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'), 'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'), + 'PASSWORD_IS_THE_SAME': ('Your new password must be different than your previous password.', 'error'), 'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'), 'LOGIN': ('Please log in to access this page.', 'info'), 'REFRESH': ('Please reauthenticate to access this page.', 'info'), diff --git a/flask_security/forms.py b/flask_security/forms.py index e43ffd0..57ebb3e 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -22,7 +22,7 @@ from flask_login import current_user from werkzeug.local import LocalProxy from .confirmable import requires_confirmation -from .utils import verify_and_update_password, get_message +from .utils import verify_and_update_password, get_message, encrypt_password # Convenient reference _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) @@ -275,4 +275,7 @@ class ChangePasswordForm(Form, PasswordFormMixin): if not verify_and_update_password(self.password.data, current_user): self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False + if self.password.data.strip() == self.new_password.data.strip(): + self.password.errors.append(get_message('PASSWORD_IS_THE_SAME')[0]) + return False return True diff --git a/tests/configured_tests.py b/tests/configured_tests.py index a1213d8..0cc9031 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -474,6 +474,16 @@ class ChangePasswordTest(SecurityTest): self.assertNotIn('You successfully changed your password', r.data) self.assertIn('Password must be at least 6 characters', r.data) + def test_change_password_same_as_previous(self): + self.authenticate() + r = self._post('/change', data={ + 'password': 'password', + 'new_password': 'password', + 'new_password_confirm': 'password' + }, follow_redirects=True) + self.assertNotIn('You successfully changed your password', r.data) + self.assertIn('Your new password must be different than your previous password.', r.data) + def test_change_password_success(self): data = { 'password': 'password', From 1ae6bc3cf133887771ed5523ea295e859cd65c59 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Wed, 16 Oct 2013 14:00:36 -0400 Subject: [PATCH 03/18] Add the ability to specify additional fields on the user model that can be used for logging in. --- flask_security/core.py | 3 ++- flask_security/datastore.py | 46 +++++++++++++++++++++-------------- flask_security/forms.py | 1 + flask_security/utils.py | 10 ++++++++ tests/configured_tests.py | 11 +++++++++ tests/test_app/__init__.py | 16 ++++++------ tests/test_app/mongoengine.py | 1 + tests/test_app/peewee_app.py | 1 + tests/test_app/sqlalchemy.py | 1 + 9 files changed, 63 insertions(+), 27 deletions(-) diff --git a/flask_security/core.py b/flask_security/core.py index 9ec4284..00b7c41 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -84,7 +84,8 @@ _default_config = { 'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions', 'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset', 'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed', - 'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions' + 'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions', + 'USER_IDENTITY_ATTRIBUTES': ['email'] } #: Default Flask-Security messages diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 4716931..7342d41 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -9,6 +9,8 @@ :license: MIT, see LICENSE for more details. """ +from .utils import get_identity_attributes + class Datastore(object): def __init__(self, db): @@ -179,14 +181,14 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore): SQLAlchemyDatastore.__init__(self, db) UserDatastore.__init__(self, user_model, role_model) - def get_user(self, id_or_email): - returned = None - if self._is_numeric(id_or_email): - returned = self.user_model.query.get(id_or_email) - if not returned: - returned = self.user_model.query.filter( - self.user_model.email.ilike(id_or_email)).first() - return returned + def get_user(self, identifier): + if self._is_numeric(identifier): + return self.user_model.query.get(identifier) + for attr in get_identity_attributes(): + query = getattr(self.user_model, attr).ilike(identifier) + rv = self.user_model.query.filter(query).first() + if rv is not None: + return rv def _is_numeric(self, value): try: @@ -210,12 +212,18 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): MongoEngineDatastore.__init__(self, db) UserDatastore.__init__(self, user_model, role_model) - def get_user(self, id_or_email): + def get_user(self, identifier): from mongoengine import ValidationError try: - return self.user_model.objects(id=id_or_email).first() + return self.user_model.objects(id=identifier).first() except ValidationError: - return self.user_model.objects(email__iexact=id_or_email).first() + pass + for attr in get_identity_attributes(): + query_key = '%s__iexact' % attr + query = {query_key: identifier} + rv = self.user_model.objects(**query).first() + if rv is not None: + return rv def find_user(self, **kwargs): try: @@ -254,16 +262,18 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): UserDatastore.__init__(self, user_model, role_model) self.UserRole = role_link - def get_user(self, id_or_email): + def get_user(self, identifier): try: - return self.user_model.get(self.user_model.id == id_or_email) + return self.user_model.get(self.user_model.id == identifier) except ValueError: pass - try: - return self.user_model.get(self.user_model.email ** id_or_email) - except self.user_model.DoesNotExist: - pass - return None + + for attr in get_identity_attributes(): + column = getattr(self.user_model, attr) + try: + return self.user_model.get(column ** identifier) + except self.user_model.DoesNotExist: + pass def find_user(self, **kwargs): try: diff --git a/flask_security/forms.py b/flask_security/forms.py index 57ebb3e..debdf40 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -220,6 +220,7 @@ class LoginForm(Form, NextFormMixin): self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) return False + self.user = _datastore.get_user(self.email.data) if self.user is None: diff --git a/flask_security/utils.py b/flask_security/utils.py index 06effff..1f7fa14 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -279,6 +279,16 @@ def get_token_status(token, serializer, max_age=None): return expired, invalid, user +def get_identity_attributes(app=None): + app = app or current_app + attrs = app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] + try: + attrs = [f.strip() for f in attrs.split(',')] + except AttributeError: + pass + return attrs + + @contextmanager def capture_passwordless_login_requests(): login_requests = [] diff --git a/tests/configured_tests.py b/tests/configured_tests.py index 0cc9031..6be025e 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -826,3 +826,14 @@ class ConfirmableExtendFormsTest(SecurityTest): def test_send_confirmation(self): r = self._get('/confirm', follow_redirects=True) self.assertIn("My Send Confirmation Email Address Field", r.data) + + +class AdditionalUserIdentityAttributes(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username') + } + + def test_authenticate(self): + r = self.authenticate(email='matt') + self.assertIn('Hello matt@lp.com', r.data) diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py index 379e4ec..3d0c798 100644 --- a/tests/test_app/__init__.py +++ b/tests/test_app/__init__.py @@ -128,17 +128,17 @@ def create_roles(): def create_users(count=None): - users = [('matt@lp.com', 'password', ['admin'], True), - ('joe@lp.com', 'password', ['editor'], True), - ('dave@lp.com', 'password', ['admin', 'editor'], True), - ('jill@lp.com', 'password', ['author'], True), - ('tiya@lp.com', 'password', [], False)] + users = [('matt@lp.com', 'matt', 'password', ['admin'], True), + ('joe@lp.com', 'joe', 'password', ['editor'], True), + ('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True), + ('jill@lp.com', 'jill', 'password', ['author'], True), + ('tiya@lp.com', 'tiya', 'password', [], False)] count = count or len(users) for u in users[:count]: - pw = encrypt_password(u[1]) - ds.create_user(email=u[0], password=pw, - roles=u[2], active=u[3]) + pw = encrypt_password(u[2]) + ds.create_user(email=u[0], username=u[1], password=pw, + roles=u[3], active=u[4]) ds.commit() diff --git a/tests/test_app/mongoengine.py b/tests/test_app/mongoengine.py index d40f666..9be6f5f 100644 --- a/tests/test_app/mongoengine.py +++ b/tests/test_app/mongoengine.py @@ -30,6 +30,7 @@ def create_app(config, **kwargs): class User(db.Document, UserMixin): email = db.StringField(unique=True, max_length=255) + username = db.StringField(max_length=255) password = db.StringField(required=True, max_length=255) last_login_at = db.DateTimeField() current_login_at = db.DateTimeField() diff --git a/tests/test_app/peewee_app.py b/tests/test_app/peewee_app.py index 8b4fc05..b33ab29 100644 --- a/tests/test_app/peewee_app.py +++ b/tests/test_app/peewee_app.py @@ -29,6 +29,7 @@ def create_app(config, **kwargs): class User(db.Model, UserMixin): email = TextField() + username = TextField() password = TextField() last_login_at = DateTimeField(null=True) current_login_at = DateTimeField(null=True) diff --git a/tests/test_app/sqlalchemy.py b/tests/test_app/sqlalchemy.py index d1af62b..fcce996 100644 --- a/tests/test_app/sqlalchemy.py +++ b/tests/test_app/sqlalchemy.py @@ -32,6 +32,7 @@ def create_app(config, **kwargs): class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True) + username = db.Column(db.String(255)) password = db.Column(db.String(255)) last_login_at = db.Column(db.DateTime()) current_login_at = db.Column(db.DateTime()) From b6007cb515621605246f716d4ca6eb5346ec60f5 Mon Sep 17 00:00:00 2001 From: Robert Clark Date: Wed, 30 Oct 2013 18:04:42 -0400 Subject: [PATCH 04/18] this fixes #175 --- flask_security/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_security/views.py b/flask_security/views.py index b1b70df..781ed29 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -301,6 +301,7 @@ def change_password(): get_url(_security.post_login_view)) if request.json: + form.user = current_user return _render_json(form) return render_template('security/change_password.html', From e1c7ec303f52573b1177a2cf83df5ef771ee8c46 Mon Sep 17 00:00:00 2001 From: Srijan Choudhary Date: Thu, 31 Oct 2013 10:19:12 +0530 Subject: [PATCH 05/18] Use get_json instead of json The `request.json` method now calls `get_json`, which raises `BadRequest` if there is no json data or some error with it. So, it cannot be directly used as a check for presence of json data. This code currently returns a bad request if content type is `application/json` but json data is empty. https://github.com/mitsuhiko/flask/blob/master/flask/wrappers.py#L110 --- flask_security/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_security/decorators.py b/flask_security/decorators.py index 5eb06e5..40b5ba5 100644 --- a/flask_security/decorators.py +++ b/flask_security/decorators.py @@ -51,7 +51,7 @@ def _check_token(): args_key = _security.token_authentication_key header_token = request.headers.get(header_key, None) token = request.args.get(args_key, header_token) - if request.json: + if request.get_json(silent=True): token = request.json.get(args_key, token) user = _security.login_manager.token_callback(token) From e0881ed6a0205ff24d7be99a6767da3dd86fd3de Mon Sep 17 00:00:00 2001 From: sanek Date: Fri, 22 Nov 2013 18:02:35 +0400 Subject: [PATCH 06/18] Added a config parameter for change password template --- flask_security/core.py | 1 + flask_security/views.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_security/core.py b/flask_security/core.py index dfb49ac..aa8be7a 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -55,6 +55,7 @@ _default_config = { 'LOGIN_USER_TEMPLATE': 'security/login_user.html', 'REGISTER_USER_TEMPLATE': 'security/register_user.html', 'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html', + 'CHANGE_PASSWORD_TEMPLATE': 'security/change_password.html', 'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html', 'SEND_LOGIN_TEMPLATE': 'security/send_login.html', 'CONFIRMABLE': False, diff --git a/flask_security/views.py b/flask_security/views.py index b1b70df..d07bc0d 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -303,7 +303,7 @@ def change_password(): if request.json: return _render_json(form) - return render_template('security/change_password.html', + return render_template(config_value('CHANGE_PASSWORD_TEMPLATE'), change_password_form=form, **_ctx('change_password')) From 4549a022197a161631debcc2f9d4a1dfe09ba490 Mon Sep 17 00:00:00 2001 From: Alexander Sukharev Date: Fri, 22 Nov 2013 18:41:08 +0400 Subject: [PATCH 07/18] Added a doc entry for parameter from the previous commit --- docs/configuration.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index 816fa18..14f198b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -122,6 +122,9 @@ Template Paths ``SECURITY_RESET_PASSWORD_TEMPLATE`` Specifies the path to the template for the reset password page. Defaults to ``security/reset_password.html``. +``SECURITY_CHANGE_PASSWORD_TEMPLATE`` Specifies the path to the template for + the change password page. Defaults to + ``security/change_password.html``. ``SECURITY_SEND_CONFIRMATION_TEMPLATE`` Specifies the path to the template for the resend confirmation instructions page. Defaults to From 292f89c20408451b59011a12121534e3bdff9144 Mon Sep 17 00:00:00 2001 From: Nick Retallack Date: Wed, 11 Dec 2013 03:08:58 -0800 Subject: [PATCH 08/18] Prevent it from exploding if you try to log in with a user who has no password in the database. --- flask_security/core.py | 1 + flask_security/forms.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/flask_security/core.py b/flask_security/core.py index aa8be7a..9a4c671 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -112,6 +112,7 @@ _default_messages = { 'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'), 'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'), 'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'), + 'PASSWORD_NOT_SET': ('No password is set for this user', 'error'), 'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', 'error'), 'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'), 'INVALID_PASSWORD': ('Invalid password', 'error'), diff --git a/flask_security/forms.py b/flask_security/forms.py index e43ffd0..0b68717 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -225,6 +225,9 @@ class LoginForm(Form, NextFormMixin): if self.user is None: self.email.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]) + return False if not verify_and_update_password(self.password.data, self.user): self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False From 1596ef75d4f253903458ba96a4251b1ed81f14ff Mon Sep 17 00:00:00 2001 From: Nick Retallack Date: Wed, 11 Dec 2013 03:12:29 -0800 Subject: [PATCH 09/18] login_without_confirmation should allow you to log in without confirmation --- flask_security/confirmable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index ca13d71..bfdcd2d 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -58,7 +58,7 @@ def generate_confirmation_token(user): def requires_confirmation(user): """Returns `True` if the user requires confirmation.""" - return _security.confirmable and user.confirmed_at == None + return _security.confirmable and not _security.login_without_confirmation and user.confirmed_at == None def confirm_email_token_status(token): From 986a48c5e076e45f9e13ee34fb62cde8311bca5d Mon Sep 17 00:00:00 2001 From: kelvinhammond Date: Sat, 14 Dec 2013 13:01:41 -0600 Subject: [PATCH 10/18] Update forms.py Fixed: If login.next is not passed form throws error --- flask_security/forms.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flask_security/forms.py b/flask_security/forms.py index e43ffd0..c540ff9 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -136,11 +136,12 @@ class NextFormMixin(): next = HiddenField() def validate_next(self, field): - url_next = urlparse.urlsplit(field.data) - url_base = urlparse.urlsplit(request.host_url) - if url_next.netloc and url_next.netloc != url_base.netloc: - field.data = '' - raise ValidationError(get_message('INVALID_REDIRECT')[0]) + if field.data: + url_next = urlparse.urlsplit(field.data) + url_base = urlparse.urlsplit(request.host_url) + if url_next.netloc and url_next.netloc != url_base.netloc: + field.data = '' + raise ValidationError(get_message('INVALID_REDIRECT')[0]) class RegisterFormMixin(): From 615bc00c2681b9446cde035f80879fd14b29ffb4 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 12:57:11 -0500 Subject: [PATCH 11/18] Add flask_security.utils documentation for selected functions. Addressed #169 --- docs/api.rst | 22 ++++++++++++++++++++++ flask_security/utils.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index e186524..53dcd68 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -54,6 +54,28 @@ Datastores :inherited-members: +Utils +----- +.. autofunction:: flask_security.utils.login_user + +.. autofunction:: flask_security.utils.logout_user + +.. autofunction:: flask_security.utils.get_hmac + +.. autofunction:: flask_security.utils.verify_password + +.. autofunction:: flask_security.utils.verify_and_update_password + +.. autofunction:: flask_security.utils.encrypt_password + +.. autofunction:: flask_security.utils.url_for_security + +.. autofunction:: flask_security.utils.get_within_delta + +.. autofunction:: flask_security.utils.send_mail + +.. autofunction:: flask_security.utils.get_token_status + Signals ------- See the `Flask documentation on signals`_ for information on how to use these diff --git a/flask_security/utils.py b/flask_security/utils.py index 1f7fa14..dc85fee 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -39,7 +39,11 @@ _pwd_context = LocalProxy(lambda: _security.pwd_context) def login_user(user, remember=None): - """Performs the login and sends the appropriate signal.""" + """Performs the login routine. + + :param user: The user to login + :param remember: Flag specifying if the remember cookie should be set. Defaults to ``False`` + """ if remember is None: remember = config_value('DEFAULT_REMEMBER_ME') @@ -66,6 +70,8 @@ def login_user(user, remember=None): def logout_user(): + """Logs out the current. This will also clean up the remember me cookie if it exists.""" + for key in ('identity.name', 'identity.auth_type'): session.pop(key, None) identity_changed.send(current_app._get_current_object(), @@ -74,6 +80,11 @@ def logout_user(): def get_hmac(password): + """Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified + by ``SECURITY_PASSWORD_SALT``. + + :param password: The password to sign + """ if _security.password_hash == 'plaintext': return password @@ -88,10 +99,21 @@ def get_hmac(password): def verify_password(password, password_hash): + """Returns ``True`` if the password matches the supplied hash. + + :param password: A plaintext password to verify + :param password_hash: The expected hash value of the password (usually form your database) + """ return _pwd_context.verify(get_hmac(password), password_hash) def verify_and_update_password(password, user): + """Returns ``True`` if the password is valid for the specified user. Additionally, the hashed + password in the database is updated if the hashing algorithm happens to have changed. + + :param password: A plaintext password to verify + :param user: The user to verify against + """ verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password) if verified and new_password: user.password = new_password @@ -100,6 +122,10 @@ def verify_and_update_password(password, user): def encrypt_password(password): + """Encrypts the specified plaintext password using the configured encryption options. + + :param password: The plaintext passwrod to encrypt + """ return _pwd_context.encrypt(get_hmac(password)) @@ -259,6 +285,14 @@ def send_mail(subject, recipient, template, **context): def get_token_status(token, serializer, max_age=None): + """Get the status of a token. + + :param token: The token to check + :param serializer: The name of the seriailzer. Can be one of the + following: ``confirm``, ``login``, ``reset`` + :param max_age: The name of the max age config option. Can be on of + the following: ``CONFIRM_EMAIL``, ``LOGIN``, ``RESET_PASSWORD`` + """ serializer = getattr(_security, serializer + '_serializer') max_age = get_max_age(max_age) user, data = None, None From c1ff98cdf3bac6d0e91aa711327a8023745fdaa6 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 13:35:03 -0500 Subject: [PATCH 12/18] Document . Addresses #194 --- docs/customizing.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/customizing.rst b/docs/customizing.rst index 33257c6..ae5248c 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -140,3 +140,23 @@ templates you can specify an email context processor with the @security.mail_context_processor def security_mail_processor(): return dict(hello="world") + + +Emails with Celery +------------------ + +Sometimes it makes sense to send emails via a task queue, such as +`Celery`_. To delay the sending of emails you can +use the ``@security.send_mail_task`` decorator like so:: + + # Setup the task + @celery.task + def send_security_email(msg): + # Use the Flask-Mail extension instance to send the incoming ``msg`` parameter + # which is an instance of `flask_mail.Message` + mail.send(msg) + + @security.send_mail_task + def delay_security_email(msg): + send_security_email.delay(msg) + From fe170e6eb3296db0cf5cd1a859f0778a67a7e9c3 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 13:45:14 -0500 Subject: [PATCH 13/18] Fixes #196 --- flask_security/forms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_security/forms.py b/flask_security/forms.py index debdf40..6967a4f 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -22,7 +22,7 @@ from flask_login import current_user from werkzeug.local import LocalProxy from .confirmable import requires_confirmation -from .utils import verify_and_update_password, get_message, encrypt_password +from .utils import verify_and_update_password, get_message, encrypt_password, config_value # Convenient reference _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) @@ -207,6 +207,7 @@ class LoginForm(Form, NextFormMixin): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) + self.remember.default = config_value('DEFAULT_REMEMBER_ME') def validate(self): if not super(LoginForm, self).validate(): From 11b8222ec5db5428d941297a656f183cc962564a Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 13:56:34 -0500 Subject: [PATCH 14/18] Add `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` config option to optionally send password reset notice emails. Addresses #199 --- docs/configuration.rst | 94 ++++++++++++++++++++--------------- flask_security/core.py | 1 + flask_security/recoverable.py | 5 +- 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 14f198b..c9fa531 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -207,43 +207,57 @@ Miscellaneous .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| -======================================= ======================================== -``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration email is - sent. Defaults to ``True``. -``SECURITY_SEND_PASSWORD_CHANGE_EMAIL`` Specifies whether password change email is - sent. Defaults to ``True``. -``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a user has - before their confirmation link expires. - Always pluralized the time unit for this - value. Defaults to ``5 days``. -``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a user has - before their password reset link - expires. Always pluralized the time unit - for this value. Defaults to ``5 days``. -``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a user has - before a login link expires. This is - only used when the passwordless login - feature is enabled. Always pluralized - the time unit for this value. Defaults - to ``1 days``. -``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login before - confirming their email when the value - of ``SECURITY_CONFIRMABLE`` is set to - ``True``. Defaults to ``False``. -``SECURITY_CONFIRM_SALT`` Specifies the salt value when generating - confirmation links/tokens. Defaults to - ``confirm-salt``. -``SECURITY_RESET_SALT`` Specifies the salt value when generating - password reset links/tokens. Defaults to - ``reset-salt``. -``SECURITY_LOGIN_SALT`` Specifies the salt value when generating - login links/tokens. Defaults to - ``login-salt``. -``SECURITY_REMEMBER_SALT`` Specifies the salt value when generating - remember tokens. Remember tokens are - used instead of user ID's as it is more - secure. Defaults to ``remember-salt``. -``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember me" - value used when logging in a user. - Defaults to ``False``. -======================================= ======================================== +============================================= ================================== +``SECURITY_SEND_REGISTER_EMAIL`` Specifies whether registration + email is sent. Defaults to + ``True``. +``SECURITY_SEND_PASSWORD_CHANGE_EMAIL`` Specifies whether password change + email is sent. Defaults to + ``True``. +``SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL`` Specifies whether password reset + notice email is sent. Defaults to + ``True``. + +``SECURITY_CONFIRM_EMAIL_WITHIN`` Specifies the amount of time a + user has before their confirmation + link expires. Always pluralized + the time unit for this value. + Defaults to ``5 days``. +``SECURITY_RESET_PASSWORD_WITHIN`` Specifies the amount of time a + user has before their password + reset link expires. Always + pluralized the time unit for this + value. Defaults to ``5 days``. +``SECURITY_LOGIN_WITHIN`` Specifies the amount of time a + user has before a login link + expires. This is only used when + the passwordless login feature is + enabled. Always pluralized the + time unit for this value. + Defaults to ``1 days``. +``SECURITY_LOGIN_WITHOUT_CONFIRMATION`` Specifies if a user may login + before confirming their email when + the value of + ``SECURITY_CONFIRMABLE`` is set to + ``True``. Defaults to ``False``. +``SECURITY_CONFIRM_SALT`` Specifies the salt value when + generating confirmation + links/tokens. Defaults to + ``confirm-salt``. +``SECURITY_RESET_SALT`` Specifies the salt value when + generating password reset + links/tokens. Defaults to + ``reset-salt``. +``SECURITY_LOGIN_SALT`` Specifies the salt value when + generating login links/tokens. + Defaults to ``login-salt``. +``SECURITY_REMEMBER_SALT`` Specifies the salt value when + generating remember tokens. + Remember tokens are used instead + of user ID's as it is more + secure. Defaults to + ``remember-salt``. +``SECURITY_DEFAULT_REMEMBER_ME`` Specifies the default "remember + me" value used when logging in + a user. Defaults to ``False``. +============================================= ================================== diff --git a/flask_security/core.py b/flask_security/core.py index 36b5e02..76d303d 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -66,6 +66,7 @@ _default_config = { 'CHANGEABLE': False, 'SEND_REGISTER_EMAIL': True, 'SEND_PASSWORD_CHANGE_EMAIL': True, + 'SEND_PASSWORD_RESET_EMAIL': True, 'LOGIN_WITHIN': '1 days', 'CONFIRM_EMAIL_WITHIN': '5 days', 'RESET_PASSWORD_WITHIN': '5 days', diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index 784faab..eca5030 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -44,8 +44,9 @@ def send_password_reset_notice(user): :param user: The user to send the notice to """ - send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email, - 'reset_notice', user=user) + if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'): + send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email, + 'reset_notice', user=user) def generate_reset_password_token(user): From d7d090afc68f0afa4d2b67ddf275a98148e838b0 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 14:00:44 -0500 Subject: [PATCH 15/18] Polish --- flask_security/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_security/core.py b/flask_security/core.py index 76d303d..f36e6e9 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -66,7 +66,7 @@ _default_config = { 'CHANGEABLE': False, 'SEND_REGISTER_EMAIL': True, 'SEND_PASSWORD_CHANGE_EMAIL': True, - 'SEND_PASSWORD_RESET_EMAIL': True, + 'SEND_PASSWORD_RESET_NOTICE_EMAIL': True, 'LOGIN_WITHIN': '1 days', 'CONFIRM_EMAIL_WITHIN': '5 days', 'RESET_PASSWORD_WITHIN': '5 days', From 6e461a81bf7745a49c4a18c55ab0e8e8a5440640 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 14:09:37 -0500 Subject: [PATCH 16/18] Add AUTHORS file --- AUTHORS | 38 ++++++++++++++++++++++++++++++++++++++ docs/authors.rst | 1 + docs/contents.rst.inc | 3 ++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 AUTHORS create mode 100644 docs/authors.rst diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..e098338 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,38 @@ +Flask-Security is written and maintained by Matt Wright and +various contributors: + +Development Lead +```````````````` + +- Matt Wright + +Patches and Suggestions +``````````````````````` + +Alexander Sukharev +Alexey Poryadin +Andrew J. Camenga +Anthony Plunkett +Artem Andreev +Catherine Wise +Chris Haines +Christophe Simonis +David Ignacio +Eric Butler +Eskil Heyn Olsen +Iuri de Silvio +Jay Goel +Joe Esposito +Joe Hand +Josh Purvis +Kostyantyn Leschenko +Luca Invernizzi +Manuel Ebert +Martin Maillard +Paweł Krześniak +Robert Clark +Rodrigue Cloutier +Rotem Yaari +Srijan Choudhary +Tristan Escalada +Vadim Kotov diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..c29da5d --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index c905e06..c1cb811 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -10,4 +10,5 @@ Contents models customizing api - changelog \ No newline at end of file + changelog + authors From af8e9f7ca5a70746f1c8dd125f2702960278f223 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 14:26:47 -0500 Subject: [PATCH 17/18] Render auth token when registering a use with JSON --- flask_security/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_security/views.py b/flask_security/views.py index f7e94b2..0291f37 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -123,6 +123,7 @@ def register(): if not request.json: return redirect(get_post_register_redirect()) + return _render_json(form, True) if request.json: return _render_json(form) From d88299fc9b4a407c873e36bc21fcb02444050a02 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 19 Dec 2013 14:40:43 -0500 Subject: [PATCH 18/18] Add test to check SECURITY_LOGIN_WITHOUT_CONFIRMATION feature --- tests/configured_tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/configured_tests.py b/tests/configured_tests.py index 6be025e..0a5955c 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -352,6 +352,16 @@ class LoginWithoutImmediateConfirmTests(SecurityTest): self.assertIn(msg, r.data) self.assertIn('Hello %s' % e2, r.data) + def test_login_unconfirmed_user_when_login_without_confirmation_is_true(self): + e = 'dude@lp.com' + p = 'password' + data = dict(email=e, password=p, password_confirm=p) + r = self._post('/register', data=data, follow_redirects=True) + self.assertIn(e, r.data) + self.client.get('/logout') + r = self.authenticate(email=e) + self.assertIn(e, r.data) + class RecoverableTests(SecurityTest):