Browse Source

Fix after-login redirects for users landing in the "Add Slack" page

pull/211/head
Pēteris Caune 6 years ago
parent
commit
bf1395801f
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
7 changed files with 90 additions and 34 deletions
  1. +5
    -2
      hc/accounts/models.py
  2. +8
    -0
      hc/accounts/tests/test_check_token.py
  3. +45
    -3
      hc/accounts/tests/test_login.py
  4. +23
    -4
      hc/accounts/views.py
  5. +5
    -3
      hc/front/tests/test_add_slack_btn.py
  6. +1
    -1
      hc/front/views.py
  7. +3
    -21
      templates/integrations/add_slack.html

+ 5
- 2
hc/accounts/models.py View File

@ -90,11 +90,14 @@ class Profile(models.Model):
def check_token(self, token, salt): def check_token(self, token, salt):
return salt in self.token and check_password(token, self.token) return salt in self.token and check_password(token, self.token)
def send_instant_login_link(self, inviting_profile=None):
def send_instant_login_link(self, inviting_profile=None, redirect_url=None):
token = self.prepare_token("login") token = self.prepare_token("login")
path = reverse("hc-check-token", args=[self.user.username, token]) path = reverse("hc-check-token", args=[self.user.username, token])
if redirect_url:
path += "?next=%s" % redirect_url
ctx = { ctx = {
"button_text": "Log In",
"button_text": "Sign In",
"button_url": settings.SITE_ROOT + path, "button_url": settings.SITE_ROOT + path,
"inviting_profile": inviting_profile "inviting_profile": inviting_profile
} }


+ 8
- 0
hc/accounts/tests/test_check_token.py View File

@ -35,3 +35,11 @@ class CheckTokenTestCase(BaseTestCase):
r = self.client.post(url, follow=True) r = self.client.post(url, follow=True)
self.assertRedirects(r, "/accounts/login/") self.assertRedirects(r, "/accounts/login/")
self.assertContains(r, "incorrect or expired") self.assertContains(r, "incorrect or expired")
def test_it_handles_next_parameter(self):
r = self.client.post("/accounts/check_token/alice/secret-token/?next=/integrations/add_slack/")
self.assertRedirects(r, "/integrations/add_slack/")
def test_it_ignores_bad_next_parameter(self):
r = self.client.post("/accounts/check_token/alice/secret-token/?next=/evil/")
self.assertRedirects(r, "/checks/")

+ 45
- 3
hc/accounts/tests/test_login.py View File

@ -14,7 +14,7 @@ class LoginTestCase(TestCase):
form = {"identity": "[email protected]"} form = {"identity": "[email protected]"}
r = self.client.post("/accounts/login/", form) r = self.client.post("/accounts/login/", form)
assert r.status_code == 302
self.assertRedirects(r, "/accounts/login_link_sent/")
# Alice should be the only existing user # Alice should be the only existing user
self.assertEqual(User.objects.count(), 1) self.assertEqual(User.objects.count(), 1)
@ -24,6 +24,20 @@ class LoginTestCase(TestCase):
subject = "Log in to %s" % settings.SITE_NAME subject = "Log in to %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, subject) self.assertEqual(mail.outbox[0].subject, subject)
def test_it_sends_link_with_next(self):
alice = User(username="alice", email="[email protected]")
alice.save()
form = {"identity": "[email protected]"}
r = self.client.post("/accounts/login/?next=/integrations/add_slack/", form)
self.assertRedirects(r, "/accounts/login_link_sent/")
# The check_token link should have a ?next= query parameter:
self.assertEqual(len(mail.outbox), 1)
body = mail.outbox[0].body
self.assertTrue("/?next=/integrations/add_slack/" in body)
def test_it_pops_bad_link_from_session(self): def test_it_pops_bad_link_from_session(self):
self.client.session["bad_link"] = True self.client.session["bad_link"] = True
self.client.get("/accounts/login/") self.client.get("/accounts/login/")
@ -36,7 +50,7 @@ class LoginTestCase(TestCase):
form = {"identity": "[email protected]"} form = {"identity": "[email protected]"}
r = self.client.post("/accounts/login/", form) r = self.client.post("/accounts/login/", form)
assert r.status_code == 302
self.assertRedirects(r, "/accounts/login_link_sent/")
# There should be exactly one user: # There should be exactly one user:
self.assertEqual(User.objects.count(), 1) self.assertEqual(User.objects.count(), 1)
@ -56,7 +70,35 @@ class LoginTestCase(TestCase):
} }
r = self.client.post("/accounts/login/", form) r = self.client.post("/accounts/login/", form)
self.assertEqual(r.status_code, 302)
self.assertRedirects(r, "/checks/")
def test_it_handles_password_login_with_redirect(self):
alice = User(username="alice", email="[email protected]")
alice.set_password("password")
alice.save()
form = {
"action": "login",
"email": "[email protected]",
"password": "password"
}
r = self.client.post("/accounts/login/?next=/integrations/add_slack/", form)
self.assertRedirects(r, "/integrations/add_slack/")
def test_it_handles_bad_next_parameter(self):
alice = User(username="alice", email="[email protected]")
alice.set_password("password")
alice.save()
form = {
"action": "login",
"email": "[email protected]",
"password": "password"
}
r = self.client.post("/accounts/login/?next=/evil/", form)
self.assertRedirects(r, "/checks/")
def test_it_handles_wrong_password(self): def test_it_handles_wrong_password(self):
alice = User(username="alice", email="[email protected]") alice = User(username="alice", email="[email protected]")


+ 23
- 4
hc/accounts/views.py View File

@ -25,6 +25,9 @@ from hc.api.models import Channel, Check
from hc.lib.badges import get_badge_url from hc.lib.badges import get_badge_url
from hc.payments.models import Subscription from hc.payments.models import Subscription
NEXT_WHITELIST = ("/checks/",
"/integrations/add_slack/")
def _make_user(email): def _make_user(email):
username = str(uuid.uuid4())[:30] username = str(uuid.uuid4())[:30]
@ -59,6 +62,16 @@ def _ensure_own_team(request):
request.profile.save() request.profile.save()
def _redirect_after_login(request):
""" Redirect to the URL indicated in ?next= query parameter. """
redirect_url = request.GET.get("next")
if redirect_url in NEXT_WHITELIST:
return redirect(redirect_url)
return redirect("hc-checks")
def login(request): def login(request):
form = EmailPasswordForm() form = EmailPasswordForm()
magic_form = ExistingEmailForm() magic_form = ExistingEmailForm()
@ -68,13 +81,19 @@ def login(request):
form = EmailPasswordForm(request.POST) form = EmailPasswordForm(request.POST)
if form.is_valid(): if form.is_valid():
auth_login(request, form.user) auth_login(request, form.user)
return redirect("hc-checks")
return _redirect_after_login(request)
else: else:
magic_form = ExistingEmailForm(request.POST) magic_form = ExistingEmailForm(request.POST)
if magic_form.is_valid(): if magic_form.is_valid():
profile = Profile.objects.for_user(magic_form.user) profile = Profile.objects.for_user(magic_form.user)
profile.send_instant_login_link()
redirect_url = request.GET.get("next")
if redirect_url in NEXT_WHITELIST:
profile.send_instant_login_link(redirect_url=redirect_url)
else:
profile.send_instant_login_link()
return redirect("hc-login-link-sent") return redirect("hc-login-link-sent")
bad_link = request.session.pop("bad_link", None) bad_link = request.session.pop("bad_link", None)
@ -122,7 +141,7 @@ def link_sent(request):
def check_token(request, username, token): def check_token(request, username, token):
if request.user.is_authenticated and request.user.username == username: if request.user.is_authenticated and request.user.username == username:
# User is already logged in # User is already logged in
return redirect("hc-checks")
return _redirect_after_login(request)
# Some email servers open links in emails to check for malicious content. # Some email servers open links in emails to check for malicious content.
# To work around this, we sign user in if the method is POST. # To work around this, we sign user in if the method is POST.
@ -137,7 +156,7 @@ def check_token(request, username, token):
user.profile.save() user.profile.save()
auth_login(request, user) auth_login(request, user)
return redirect("hc-checks")
return _redirect_after_login(request)
request.session["bad_link"] = True request.session["bad_link"] = True
return redirect("hc-login") return redirect("hc-login")


+ 5
- 3
hc/front/tests/test_add_slack_btn.py View File

@ -9,13 +9,12 @@ from mock import patch
class AddSlackBtnTestCase(BaseTestCase): class AddSlackBtnTestCase(BaseTestCase):
@override_settings(SLACK_CLIENT_ID="foo") @override_settings(SLACK_CLIENT_ID="foo")
def test_instructions_work(self):
def test_it_prepares_login_link(self):
r = self.client.get("/integrations/add_slack/") r = self.client.get("/integrations/add_slack/")
self.assertContains(r, "Before adding Slack integration", self.assertContains(r, "Before adding Slack integration",
status_code=200) status_code=200)
# There should now be a key in session
self.assertTrue("slack" in self.client.session)
self.assertContains(r, "?next=/integrations/add_slack/")
@override_settings(SLACK_CLIENT_ID="foo") @override_settings(SLACK_CLIENT_ID="foo")
def test_slack_button(self): def test_slack_button(self):
@ -23,6 +22,9 @@ class AddSlackBtnTestCase(BaseTestCase):
r = self.client.get("/integrations/add_slack/") r = self.client.get("/integrations/add_slack/")
self.assertContains(r, "slack.com/oauth/authorize", status_code=200) self.assertContains(r, "slack.com/oauth/authorize", status_code=200)
# There should now be a key in session
self.assertTrue("slack" in self.client.session)
@patch("hc.front.views.requests.post") @patch("hc.front.views.requests.post")
def test_it_handles_oauth_response(self, mock_post): def test_it_handles_oauth_response(self, mock_post):
session = self.client.session session = self.client.session


+ 1
- 1
hc/front/views.py View File

@ -692,7 +692,7 @@ def add_slack(request):
"slack_client_id": settings.SLACK_CLIENT_ID "slack_client_id": settings.SLACK_CLIENT_ID
} }
if settings.SLACK_CLIENT_ID:
if settings.SLACK_CLIENT_ID and request.user.is_authenticated:
ctx["state"] = _prepare_state(request, "slack") ctx["state"] = _prepare_state(request, "slack")
return render(request, "integrations/add_slack.html", ctx) return render(request, "integrations/add_slack.html", ctx)


+ 3
- 21
templates/integrations/add_slack.html View File

@ -35,26 +35,8 @@
{% site_name %}:</p> {% site_name %}:</p>
<div class="text-center"> <div class="text-center">
<form class="form-inline" action="{% url 'hc-login' %}" method="post">
{% csrf_token %}
<div class="form-group">
<div class="input-group input-group-lg">
<div class="input-group-addon">@</div>
<input
type="email"
class="form-control"
name="email"
autocomplete="email"
placeholder="Email">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary pull-right">
Log In
</button>
</div>
</form>
<a href="{% url 'hc-login' %}?next={% url 'hc-add-slack' %}"
class="btn btn-primary btn-lg">Sign In</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -65,7 +47,7 @@
<div class="col-sm-6"> <div class="col-sm-6">
<span class="step-no">1</span> <span class="step-no">1</span>
<p> <p>
After {% if request.user.is_authenticated %}{% else %}logging in and{% endif %}
After {% if request.user.is_authenticated %}{% else %}signing in and{% endif %}
clicking on "Add to Slack", you should clicking on "Add to Slack", you should
be on a page that says "{% site_name %} would like access to be on a page that says "{% site_name %} would like access to
your Slack team". Select the team you want to add the your Slack team". Select the team you want to add the


Loading…
Cancel
Save