Browse Source

Update the "Set Password" function to use confirmation codes

pull/456/head
Pēteris Caune 4 years ago
parent
commit
ed6b15bfa9
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
15 changed files with 42 additions and 107 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +0
    -6
      hc/accounts/models.py
  3. +4
    -10
      hc/accounts/tests/test_add_credential.py
  4. +0
    -17
      hc/accounts/tests/test_profile.py
  5. +8
    -9
      hc/accounts/tests/test_remove_credential.py
  6. +14
    -22
      hc/accounts/tests/test_set_password.py
  7. +1
    -1
      hc/accounts/urls.py
  8. +2
    -7
      hc/accounts/views.py
  9. +0
    -4
      hc/lib/emails.py
  10. +6
    -0
      hc/test.py
  11. +3
    -4
      templates/accounts/profile.html
  12. +2
    -1
      templates/accounts/sudo.html
  13. +0
    -13
      templates/emails/set-password-body-html.html
  14. +0
    -11
      templates/emails/set-password-body-text.html
  15. +0
    -2
      templates/emails/set-password-subject.html

+ 2
- 0
CHANGELOG.md View File

@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file.
- Improve phone number sanitization: remove spaces and hyphens
- Change the "Test Integration" behavior for webhooks: don't retry failed requests
- Add retries to the the email sending logic
- Require confirmation codes (sent to email) before sensitive actions
- Implement Webauthn two-factor authentication
## v1.17.0 - 2020-10-14


+ 0
- 6
hc/accounts/models.py View File

@ -118,12 +118,6 @@ class Profile(models.Model):
}
emails.transfer_request(self.user.email, ctx)
def send_set_password_link(self):
token = self.prepare_token("set-password")
path = reverse("hc-set-password", args=[token])
ctx = {"button_text": "Set Password", "button_url": settings.SITE_ROOT + path}
emails.set_password(self.user.email, ctx)
def send_change_email_link(self):
token = self.prepare_token("change-email")
path = reverse("hc-change-email", args=[token])


+ 4
- 10
hc/accounts/tests/test_add_credential.py View File

@ -1,6 +1,5 @@
from unittest.mock import patch
from django.core.signing import TimestampSigner
from hc.test import BaseTestCase
from hc.accounts.models import Credential
@ -11,11 +10,6 @@ class AddCredentialTestCase(BaseTestCase):
self.url = "/accounts/two_factor/add/"
def _set_sudo_flag(self):
session = self.client.session
session["sudo"] = TimestampSigner().sign("active")
session.save()
def test_it_requires_sudo_mode(self):
self.client.login(username="[email protected]", password="password")
@ -24,7 +18,7 @@ class AddCredentialTestCase(BaseTestCase):
def test_it_shows_form(self):
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertContains(r, "Add Security Key")
@ -37,7 +31,7 @@ class AddCredentialTestCase(BaseTestCase):
mock_get_credential_data.return_value = b"dummy-credential-data"
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
payload = {
"name": "My New Key",
@ -54,7 +48,7 @@ class AddCredentialTestCase(BaseTestCase):
def test_it_rejects_bad_base64(self):
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
payload = {
"name": "My New Key",
@ -67,7 +61,7 @@ class AddCredentialTestCase(BaseTestCase):
def test_it_requires_client_data_json(self):
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
payload = {
"name": "My New Key",


+ 0
- 17
hc/accounts/tests/test_profile.py View File

@ -9,23 +9,6 @@ from hc.api.models import Check
class ProfileTestCase(BaseTestCase):
def test_it_sends_set_password_link(self):
self.client.login(username="[email protected]", password="password")
form = {"set_password": "1"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 302
# profile.token should be set now
self.profile.refresh_from_db()
token = self.profile.token
self.assertTrue(len(token) > 10)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
expected_subject = "Set password on %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, expected_subject)
def test_it_sends_report(self):
check = Check(project=self.project, name="Test Check")
check.last_ping = now()


+ 8
- 9
hc/accounts/tests/test_remove_credential.py View File

@ -1,5 +1,3 @@
from django.core.signing import TimestampSigner
from hc.test import BaseTestCase
from hc.accounts.models import Credential
@ -11,14 +9,15 @@ class RemoveCredentialTestCase(BaseTestCase):
self.c = Credential.objects.create(user=self.alice, name="Alices Key")
self.url = f"/accounts/two_factor/{self.c.code}/remove/"
def _set_sudo_flag(self):
session = self.client.session
session["sudo"] = TimestampSigner().sign("active")
session.save()
def test_it_requires_sudo_mode(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "We have sent a confirmation code")
def test_it_shows_form(self):
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertContains(r, "Remove Security Key")
@ -26,7 +25,7 @@ class RemoveCredentialTestCase(BaseTestCase):
def test_it_removes_credential(self):
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
r = self.client.post(self.url, {"remove_credential": ""}, follow=True)
self.assertRedirects(r, "/accounts/profile/")
@ -36,7 +35,7 @@ class RemoveCredentialTestCase(BaseTestCase):
def test_it_checks_owner(self):
self.client.login(username="[email protected]", password="password")
self._set_sudo_flag()
self.set_sudo_flag()
r = self.client.post(self.url, {"remove_credential": ""})
self.assertEqual(r.status_code, 400)

+ 14
- 22
hc/accounts/tests/test_set_password.py View File

@ -2,45 +2,37 @@ from hc.test import BaseTestCase
class SetPasswordTestCase(BaseTestCase):
def test_it_shows_form(self):
token = self.profile.prepare_token("set-password")
def test_it_requires_sudo_mod(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get("/accounts/set_password/%s/" % token)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Please pick a password")
r = self.client.get("/accounts/set_password/")
self.assertContains(r, "We have sent a confirmation code")
def test_it_checks_token(self):
self.profile.prepare_token("set-password")
def test_it_shows_form(self):
self.client.login(username="[email protected]", password="password")
self.set_sudo_flag()
# GET
r = self.client.get("/accounts/set_password/invalid-token/")
self.assertEqual(r.status_code, 400)
# POST
r = self.client.post("/accounts/set_password/invalid-token/")
self.assertEqual(r.status_code, 400)
r = self.client.get("/accounts/set_password/")
self.assertContains(r, "Please pick a password")
def test_it_sets_password(self):
token = self.profile.prepare_token("set-password")
self.client.login(username="[email protected]", password="password")
self.set_sudo_flag()
payload = {"password": "correct horse battery staple"}
r = self.client.post("/accounts/set_password/%s/" % token, payload)
self.assertEqual(r.status_code, 302)
r = self.client.post("/accounts/set_password/", payload)
self.assertRedirects(r, "/accounts/profile/")
old_password = self.alice.password
self.alice.refresh_from_db()
self.assertNotEqual(self.alice.password, old_password)
def test_post_checks_length(self):
token = self.profile.prepare_token("set-password")
self.client.login(username="[email protected]", password="password")
self.set_sudo_flag()
payload = {"password": "abc"}
r = self.client.post("/accounts/set_password/%s/" % token, payload)
r = self.client.post("/accounts/set_password/", payload)
self.assertEqual(r.status_code, 200)
old_password = self.alice.password


+ 1
- 1
hc/accounts/urls.py View File

@ -21,7 +21,7 @@ urlpatterns = [
views.unsubscribe_reports,
name="hc-unsubscribe-reports",
),
path("set_password/<slug:token>/", views.set_password, name="hc-set-password"),
path("set_password/", views.set_password, name="hc-set-password"),
path("change_email/done/", views.change_email_done, name="hc-change-email-done"),
path("change_email/<slug:token>/", views.change_email, name="hc-change-email"),
path("two_factor/add/", views.add_credential, name="hc-add-credential"),


+ 2
- 7
hc/accounts/views.py View File

@ -239,9 +239,6 @@ def profile(request):
if "change_email" in request.POST:
profile.send_change_email_link()
return redirect("hc-link-sent")
elif "set_password" in request.POST:
profile.send_set_password_link()
return redirect("hc-link-sent")
elif "leave_project" in request.POST:
code = request.POST["code"]
try:
@ -466,10 +463,8 @@ def notifications(request):
@login_required
def set_password(request, token):
if not request.profile.check_token(token, "set-password"):
return HttpResponseBadRequest()
@require_sudo_mode
def set_password(request):
if request.method == "POST":
form = forms.SetPasswordForm(request.POST)
if form.is_valid():


+ 0
- 4
hc/lib/emails.py View File

@ -62,10 +62,6 @@ def transfer_request(to, ctx):
send("transfer-request", to, ctx)
def set_password(to, ctx):
send("set-password", to, ctx)
def change_email(to, ctx):
send("change-email", to, ctx)


+ 6
- 0
hc/test.py View File

@ -1,4 +1,5 @@
from django.contrib.auth.models import User
from django.core.signing import TimestampSigner
from django.test import TestCase
from hc.accounts.models import Member, Profile, Project
@ -51,3 +52,8 @@ class BaseTestCase(TestCase):
self.charlies_profile.save()
self.channels_url = "/projects/%s/integrations/" % self.project.code
def set_sudo_flag(self):
session = self.client.session
session["sudo"] = TimestampSigner().sign("active")
session.save()

+ 3
- 4
templates/accounts/profile.html View File

@ -50,10 +50,9 @@
<p class="clearfix"></p>
<p>
Attach a password to your {{ site_name }} account
<button
type="submit"
name="set_password"
class="btn btn-default pull-right">Set Password</button>
<a
href="{% url 'hc-set-password' %}"
class="btn btn-default pull-right">Set Password</a>
</p>
</form>
</div>


+ 2
- 1
templates/accounts/sudo.html View File

@ -20,7 +20,8 @@
type="text"
class="form-control input-lg"
maxlength="6"
name="sudo_code" />
name="sudo_code"
autofocus />
{% if wrong_code %}
<div class="help-block">


+ 0
- 13
templates/emails/set-password-body-html.html View File

@ -1,13 +0,0 @@
{% extends "emails/base.html" %}
{% load hc_extras %}
{% block content %}
Hello,<br />
To set up a password for your account on {% site_name %}, please press the
button below:</p>
{% endblock %}
{% block content_more %}
Regards,<br />
The {% site_name %} Team
{% endblock %}

+ 0
- 11
templates/emails/set-password-body-text.html View File

@ -1,11 +0,0 @@
{% load hc_extras %}
Hello,
Here's a link to set a password for your account on {% site_name %}:
{{ button_url }}
--
Regards,
{% site_name %}

+ 0
- 2
templates/emails/set-password-subject.html View File

@ -1,2 +0,0 @@
{% load hc_extras %}
Set password on {% site_name %}

Loading…
Cancel
Save