Skip to content

Commit

Permalink
Send a validation email on account creation
Browse files Browse the repository at this point in the history
  • Loading branch information
magopian committed Jan 18, 2019
1 parent b902e77 commit 0ff6728
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -18,3 +18,4 @@ kinto/plugins/admin/node_modules
kinto/plugins/admin/coverage
kinto/plugins/admin/npm-debug.log
.pytest_cache/
mail/
3 changes: 3 additions & 0 deletions kinto/plugins/accounts/__init__.py
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
31 changes: 28 additions & 3 deletions kinto/plugins/accounts/views.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion tests/plugins/test_accounts.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 0ff6728

Please sign in to comment.