From 0ff67284aba10b6bf79840934790e50d37a6b99e Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Fri, 18 Jan 2019 15:46:01 +0100 Subject: [PATCH] Send a validation email on account creation --- .gitignore | 1 + kinto/plugins/accounts/__init__.py | 3 +++ kinto/plugins/accounts/views.py | 31 +++++++++++++++++++++++++++--- setup.py | 1 + tests/plugins/test_accounts.py | 11 ++++++++++- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b45fe67879..fb8fe405b7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ kinto/plugins/admin/node_modules kinto/plugins/admin/coverage kinto/plugins/admin/npm-debug.log .pytest_cache/ +mail/ diff --git a/kinto/plugins/accounts/__init__.py b/kinto/plugins/accounts/__init__.py index 8383dc4acb..58c7549f62 100644 --- a/kinto/plugins/accounts/__init__.py +++ b/kinto/plugins/accounts/__init__.py @@ -2,6 +2,7 @@ from kinto.authorization import PERMISSIONS_INHERITANCE_TREE from pyramid.exceptions import ConfigurationError +from pyramid.settings import asbool from .authentication import AccountsAuthenticationPolicy as AccountsPolicy from .utils import ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME @@ -35,6 +36,8 @@ def includeme(config): description="Validate accounts", url="https://kinto.readthedocs.io/en/latest/api/1.x/accounts.html", ) + debug = asbool(settings.get("mail.debug_mailer", "false")) + config.include("pyramid_mailer" + (".debug" if debug else "")) # Check that the account policy is mentioned in config if included. accountClass = "AccountsPolicy" diff --git a/kinto/plugins/accounts/views.py b/kinto/plugins/accounts/views.py index 0d46432667..66e4fede34 100644 --- a/kinto/plugins/accounts/views.py +++ b/kinto/plugins/accounts/views.py @@ -6,6 +6,8 @@ from pyramid.security import Authenticated, Everyone from pyramid.settings import aslist from pyramid.events import subscriber +from pyramid_mailer import get_mailer +from pyramid_mailer.message import Message from kinto.views import NameGenerator @@ -47,17 +49,18 @@ class Account(resource.Resource): schema = AccountSchema def __init__(self, request, context): + settings = request.registry.settings # Store if current user is administrator (before accessing get_parent_id()) - allowed_from_settings = request.registry.settings.get("account_write_principals", []) + allowed_from_settings = settings.get("account_write_principals", []) context.is_administrator = ( len(set(aslist(allowed_from_settings)) & set(request.prefixed_principals)) > 0 ) # Shortcut to check if current is anonymous (before get_parent_id()). context.is_anonymous = Authenticated not in request.effective_principals # Is the "accounts validation" setting set? - context.validation = request.registry.settings.get("account_validation", False) + context.validation = settings.get("account_validation", False) # Account validation requires the user id to be an email. - validation_email_regexp = request.registry.settings.get( + validation_email_regexp = settings.get( "account_validation.email_regexp", "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" ) context.validation_email_regexp = re.compile(validation_email_regexp) @@ -69,6 +72,19 @@ def __init__(self, request, context): # Creation is anonymous, but author with write perm is this: self.model.current_principal = f"{ACCOUNT_POLICY_NAME}:{self.model.parent_id}" + if context.validation: + # pyramid_mailer instance. + self.mailer = get_mailer(request) + self.email_sender = settings.get( + "account_validation.email_sender", "admin@example.com" + ) + self.email_subject_template = settings.get( + "account_validation.email_subject_template", "activate your account" + ) + self.email_body_template = settings.get( + "account_validation.email_body_template", "{activation-form-url}{activation-key}" + ) + @reify def id_generator(self): # This generator is used for ID validation. @@ -145,6 +161,15 @@ def process_object(self, new, old=None): activation_key = str(uuid.uuid4()) new["activation-key"] = activation_key + # Send an email to the user with the link to activate their account. + message = Message( + subject=self.email_subject_template.format(**new), + sender=self.email_sender, + recipients=[user_email], + body=self.email_body_template.format(**new), + ) + self.mailer.send(message) + # Administrators can reach other accounts and anonymous have no # selected_userid. So do not try to enforce. if self.context.is_administrator or self.context.is_anonymous: diff --git a/setup.py b/setup.py index b5f37d63c5..745eba91d6 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ def read_file(filename): "pyramid >= 1.9.1, < 2.0", "pyramid_multiauth >= 0.8", # User on policy selected event. "transaction", + "pyramid_mailer", # pyramid_tm changed the location of their tween in 2.x and one of # our tests fails on 2.0. "pyramid_tm >= 2.1", diff --git a/tests/plugins/test_accounts.py b/tests/plugins/test_accounts.py index 4d1d481970..4fc739ab92 100644 --- a/tests/plugins/test_accounts.py +++ b/tests/plugins/test_accounts.py @@ -28,6 +28,11 @@ def get_app_settings(cls, extras=None): class AccountsValidationWebTest(AccountsWebTest): + def setUp(self): + patch = mock.patch("kinto.plugins.accounts.views.get_mailer") + self.get_mailer = patch.start() + self.addCleanup(patch.stop) + @classmethod def get_app_settings(cls, extras=None): if extras is None: @@ -214,6 +219,11 @@ def test_create_account_appends_activation_code_to_activation_form_url(self): ) assert resp.json["data"]["activation-form-url"] == activation_form_url assert resp.json["data"]["activation-key"] == uuid_string + mailer_call = self.get_mailer().send.call_args_list[0] + assert mailer_call[0][0].sender == "admin@example.com" + assert mailer_call[0][0].subject == "activate your account" + assert mailer_call[0][0].recipients == ["alice@example.com"] + assert mailer_call[0][0].body == f"{activation_form_url}{uuid_string}" def test_cant_authenticate_with_unactivated_account(self): self.app.post_json( @@ -327,7 +337,6 @@ def test_dont_check_activation_form_url_on_an_active_user(self): status=200, headers=get_user_headers("alice@example.com", "12éé6"), ) - print(resp) assert "activation-form-url" not in resp.json["data"] assert "activation-key" not in resp.json["data"] assert resp.json["data"]["some-other-metadata"] == "foobar"