This repository has been archived by the owner on Feb 8, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 308
Add email auth #4539
Closed
Closed
Add email auth #4539
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
c662b9d
Extract out test_participant_emails.py
rohitpaulk 3d2430c
Allow queuing emails without participant
rohitpaulk f7bcc95
Add table for email_auth_nonces
rohitpaulk 55391a9
Expand authentication module to make space for email auth code
rohitpaulk f09199e
Add funcs to handle email nonces
rohitpaulk 966965f
Add Participant::from_email
rohitpaulk dc1fbdc
Add Participant::with_email_and_username
rohitpaulk 104b5f7
Add controller to generate links
rohitpaulk 08c62d2
Add controller to handle verification + signup form
rohitpaulk 5c25f7d
Add email auth to sign-in modal
rohitpaulk b3d5b65
Add signup endpoint
rohitpaulk dcf3c2f
Wireup signup form
rohitpaulk 13f0eff
Modify check_orphans to handle signups via email
rohitpaulk 114f36c
Allow deleting last elsewhere if email attached
rohitpaulk 2d73dd7
send_link -> send-link
chadwhitacre da7f739
Merge pull request #4550 from gratipay/dash-over-underscore
aandis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{{ _("Sign in to Gratipay") }} | ||
|
||
[---] text/html | ||
{{ _( "Click the button below to sign in to Gratipay. " | ||
"This link will expire in 1 hour and can only be used once.") }} | ||
<br> | ||
<br> | ||
<a href="{{ signin_link }}" style="{{ button_style }}">{{ _("Sign in to Gratipay") }}</a> | ||
|
||
[---] text/plain | ||
|
||
{{ _( "Click the link below to sign in to Gratipay. " | ||
"This link will expire in 1 hour and can only be used once.") }} | ||
|
||
{{ signin_link }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{{ _("Finish creating your account on Gratipay") }} | ||
|
||
[---] text/html | ||
{{ _( "Click the button below to create an account on Gratipay. " | ||
"This link will expire in 1 hour and can only be used once.") }} | ||
<br> | ||
<br> | ||
<a href="{{ signup_link }}" style="{{ button_style }}">{{ _("Create an account on Gratipay") }}</a> | ||
|
||
[---] text/plain | ||
|
||
{{ _( "Click the link below to create an account on Gratipay. " | ||
"This link will expire in 1 hour and can only be used once.") }} | ||
|
||
{{ signup_link }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,13 +55,17 @@ def _have_ses(self, env): | |
and env.aws_ses_default_region | ||
|
||
|
||
def put(self, to, template, _user_initiated=True, **context): | ||
def put(self, to, template, _user_initiated=True, email=None, **context): | ||
"""Put an email message on the queue. | ||
|
||
:param Participant to: the participant to send the email message to | ||
:param Participant to: the participant to send the email message to. | ||
In cases where an email is not linked to a participant, this can be | ||
``None``. | ||
:param unicode template: the name of the template to use when rendering | ||
the email, corresponding to a filename in ``emails/`` without the | ||
file extension | ||
:param unicode email: The email address to send this message to. If not | ||
provided, the ``to`` participant's primary email is used. | ||
:param bool _user_initiated: user-initiated emails are throttled; | ||
system-initiated messages don't count against throttling | ||
:param dict context: the values to use when rendering the template | ||
|
@@ -73,19 +77,57 @@ def put(self, to, template, _user_initiated=True, **context): | |
:returns: ``None`` | ||
|
||
""" | ||
|
||
assert to or email # Either participant or email address required. | ||
|
||
with self.db.get_cursor() as cursor: | ||
participant_id = to.id if to else None | ||
cursor.run(""" | ||
INSERT INTO email_queue | ||
(participant, spt_name, context, user_initiated) | ||
VALUES (%s, %s, %s, %s) | ||
""", (to.id, template, pickle.dumps(context), _user_initiated)) | ||
(participant, | ||
email_address, | ||
spt_name, | ||
context, | ||
user_initiated) | ||
VALUES (%s, %s, %s, %s, %s) | ||
""", (participant_id, email, template, pickle.dumps(context), _user_initiated)) | ||
|
||
if _user_initiated: | ||
n = cursor.one('SELECT count(*) FROM email_queue ' | ||
'WHERE participant=%s AND user_initiated', (to.id,)) | ||
if n > self.allow_up_to: | ||
nqueued = self._get_nqueued(cursor, to, email) | ||
if nqueued > self.allow_up_to: | ||
raise Throttled() | ||
|
||
|
||
def _get_nqueued(self, cursor, participant, email_address): | ||
"""Returns the number of messages already queued for the given | ||
participant or email address. Prefers participant if provided, falls | ||
back to email_address otherwise. | ||
|
||
:param Participant participant: The participant to check queued messages | ||
for. | ||
|
||
:param unicode email_address: The email address to check queued messages | ||
for. | ||
|
||
:returns number of queued messages | ||
""" | ||
|
||
if participant: | ||
return cursor.one(""" | ||
SELECT COUNT(*) | ||
FROM email_queue | ||
WHERE user_initiated | ||
AND participant=%s | ||
""", (participant.id, )) | ||
else: | ||
return cursor.one(""" | ||
SELECT COUNT(*) | ||
FROM email_queue | ||
WHERE user_initiated | ||
AND email_address=%s | ||
""", (email_address, )) | ||
|
||
|
||
def flush(self): | ||
"""Load messages queued for sending, and send them. | ||
""" | ||
|
@@ -142,22 +184,35 @@ def _prepare_email_message_for_ses(self, rec): | |
#. ``participant.email_address``. | ||
|
||
""" | ||
to = Participant.from_id(rec.participant) | ||
participant = Participant.from_id(rec.participant) | ||
spt = self._email_templates[rec.spt_name] | ||
context = pickle.loads(rec.context) | ||
|
||
context['participant'] = to | ||
context['username'] = to.username | ||
email = rec.email_address or participant.email_address | ||
|
||
# Previously, email_address was stored in the 'email' key on `context` | ||
# and not in the `email_address` field. Let's handle that case so that | ||
# old emails don't suffer | ||
# | ||
# TODO: Remove this once we're sure old emails have gone out. | ||
email = context.get('email', email) | ||
|
||
if not email: | ||
return None | ||
|
||
context['email'] = email | ||
if participant: | ||
context['participant'] = participant | ||
context['username'] = participant.username | ||
context['button_style'] = ( | ||
"color: #fff; text-decoration:none; display:inline-block; " | ||
"padding: 0 15px; background: #396; white-space: nowrap; " | ||
"font: normal 14px/40px Arial, sans-serif; border-radius: 3px" | ||
) | ||
context.setdefault('include_unsubscribe', True) | ||
email = context.setdefault('email', to.email_address) | ||
if not email: | ||
return None | ||
langs = i18n.parse_accept_lang(to.email_lang or 'en') | ||
|
||
accept_lang = (participant and participant.email_lang) or 'en' | ||
langs = i18n.parse_accept_lang(accept_lang) | ||
locale = i18n.match_lang(langs) | ||
i18n.add_helpers_to_context(self.tell_sentry, context, locale) | ||
context['escape'] = lambda s: s | ||
|
@@ -172,7 +227,12 @@ def render(t, context): | |
message = {} | ||
message['Source'] = 'Gratipay Support <[email protected]>' | ||
message['Destination'] = {} | ||
message['Destination']['ToAddresses'] = ["%s <%s>" % (to.username, email)] # "Name <[email protected]>" | ||
if participant: | ||
# "username <[email protected]>" | ||
destination = "%s <%s>" % (participant.username, email) | ||
else: | ||
destination = email | ||
message['Destination']['ToAddresses'] = [destination] | ||
message['Message'] = {} | ||
message['Message']['Subject'] = {} | ||
message['Message']['Subject']['Data'] = spt['subject'].render(context).strip() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This can lead to username enumeration. Although usernames are public on Gratipay, I still think it is best practice to prevent username enumeration on the account creation endpoint.
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.
@EdOverflow Is there a way around this, that doesn't sacrifice UX?
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.
The way I see it, username enumeration is not a risk for us because usernames are public (as you mentioned). Whether they are exposed via this endpoint or a normal
/~username
endpoint shouldn't make a differenceThere 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.
@rohitpaulk I have thought it through and agree with you. Let's leave it as it currently is.