Browse Source

Team Access, test cleanup

pull/60/head
Pēteris Caune 9 years ago
parent
commit
fdf9c607e5
30 changed files with 255 additions and 162 deletions
  1. +10
    -4
      hc/accounts/admin.py
  2. +4
    -0
      hc/accounts/forms.py
  3. +1
    -2
      hc/accounts/management/commands/createprofiles.py
  4. +10
    -0
      hc/accounts/middleware.py
  5. +21
    -0
      hc/accounts/migrations/0006_profile_current_team.py
  6. +2
    -12
      hc/accounts/models.py
  7. +0
    -2
      hc/accounts/tests/test_check_token.py
  8. +23
    -13
      hc/accounts/tests/test_profile.py
  9. +37
    -0
      hc/accounts/tests/test_switch_team.py
  10. +4
    -0
      hc/accounts/urls.py
  11. +34
    -12
      hc/accounts/views.py
  12. +1
    -1
      hc/api/management/commands/prunepings.py
  13. +1
    -1
      hc/api/management/commands/prunepingsslow.py
  14. +0
    -6
      hc/api/management/commands/sendreports.py
  15. +0
    -2
      hc/api/tests/test_create_check.py
  16. +4
    -4
      hc/api/tests/test_list_checks.py
  17. +1
    -7
      hc/front/tests/test_channel_checks.py
  18. +0
    -6
      hc/front/tests/test_log.py
  19. +1
    -7
      hc/front/tests/test_remove_channel.py
  20. +1
    -7
      hc/front/tests/test_remove_check.py
  21. +8
    -18
      hc/front/tests/test_update_channel.py
  22. +0
    -7
      hc/front/tests/test_update_name.py
  23. +0
    -6
      hc/front/tests/test_update_timeout.py
  24. +0
    -1
      hc/front/urls.py
  25. +16
    -41
      hc/front/views.py
  26. +1
    -1
      hc/payments/views.py
  27. +1
    -0
      hc/settings.py
  28. +15
    -0
      hc/test.py
  29. +42
    -0
      templates/accounts/profile.html
  30. +17
    -2
      templates/base.html

+ 10
- 4
hc/accounts/admin.py View File

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from hc.accounts.models import Profile from hc.accounts.models import Profile
from hc.api.models import Channel, Check from hc.api.models import Channel, Check
@ -18,8 +19,8 @@ class ProfileAdmin(admin.ModelAdmin):
class HcUserAdmin(UserAdmin): class HcUserAdmin(UserAdmin):
actions = ["send_report"] actions = ["send_report"]
list_display = ('id', 'username', 'email', 'date_joined', 'involvement',
'is_staff')
list_display = ('id', 'email', 'date_joined', 'involvement',
'is_staff', 'checks')
ordering = ["-id"] ordering = ["-id"]
@ -46,10 +47,15 @@ class HcUserAdmin(UserAdmin):
involvement.allow_tags = True involvement.allow_tags = True
def checks(self, user):
url = reverse("hc-switch-team", args=[user.username])
return "<a href='%s'>Checks</a>" % url
checks.allow_tags = True
def send_report(self, request, qs): def send_report(self, request, qs):
for user in qs: for user in qs:
profile = Profile.objects.for_user(user)
profile.send_report()
user.profile.send_report()
self.message_user(request, "%d email(s) sent" % qs.count()) self.message_user(request, "%d email(s) sent" % qs.count())


+ 4
- 0
hc/accounts/forms.py View File

@ -27,3 +27,7 @@ class InviteTeamMemberForm(forms.Form):
class RemoveTeamMemberForm(forms.Form): class RemoveTeamMemberForm(forms.Form):
email = LowercaseEmailField() email = LowercaseEmailField()
class TeamNameForm(forms.Form):
team_name = forms.CharField(max_length=200, required=True)

+ 1
- 2
hc/accounts/management/commands/createprofiles.py View File

@ -8,7 +8,6 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
for user in User.objects.all(): for user in User.objects.all():
# this should create profile object if it does not exist
Profile.objects.for_user(user)
Profile.objects.get_or_create(user_id=user.id)
print("Done.") print("Done.")

+ 10
- 0
hc/accounts/middleware.py View File

@ -0,0 +1,10 @@
class TeamAccessMiddleware(object):
def process_request(self, request):
if not request.user.is_authenticated():
return
profile = request.user.profile
if profile.current_team:
request.team = profile.current_team
else:
request.team = profile

+ 21
- 0
hc/accounts/migrations/0006_profile_current_team.py View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-05-09 10:34
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_auto_20160509_0801'),
]
operations = [
migrations.AddField(
model_name='profile',
name='current_team',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.Profile'),
),
]

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

@ -13,27 +13,17 @@ from django.utils import timezone
from hc.lib import emails from hc.lib import emails
class ProfileManager(models.Manager):
def for_user(self, user):
profile, created = Profile.objects.get_or_create(user_id=user.id)
return profile
class Profile(models.Model): class Profile(models.Model):
# Owner: # Owner:
user = models.OneToOneField(User, blank=True, null=True) user = models.OneToOneField(User, blank=True, null=True)
team_name = models.CharField(max_length=200, blank=True) team_name = models.CharField(max_length=200, blank=True)
team_access_allowed = models.BooleanField(default=False) team_access_allowed = models.BooleanField(default=False)
next_report_date = models.DateTimeField(null=True, blank=True) next_report_date = models.DateTimeField(null=True, blank=True)
reports_allowed = models.BooleanField(default=True) reports_allowed = models.BooleanField(default=True)
ping_log_limit = models.IntegerField(default=100) ping_log_limit = models.IntegerField(default=100)
token = models.CharField(max_length=128, blank=True) token = models.CharField(max_length=128, blank=True)
api_key = models.CharField(max_length=128, blank=True) api_key = models.CharField(max_length=128, blank=True)
objects = ProfileManager()
current_team = models.ForeignKey("self", null=True)
def __str__(self): def __str__(self):
return self.team_name or self.user.email return self.team_name or self.user.email
@ -85,7 +75,7 @@ class Profile(models.Model):
member = Member(team=self, user=user) member = Member(team=self, user=user)
member.save() member.save()
Profile.objects.for_user(user).send_instant_login_link(self)
user.profile.send_instant_login_link(self)
class Member(models.Model): class Member(models.Model):


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

@ -1,5 +1,4 @@
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from hc.accounts.models import Profile
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -7,7 +6,6 @@ class CheckTokenTestCase(BaseTestCase):
def setUp(self): def setUp(self):
super(CheckTokenTestCase, self).setUp() super(CheckTokenTestCase, self).setUp()
self.profile = Profile(user=self.alice)
self.profile.token = make_password("secret-token") self.profile.token = make_password("secret-token")
self.profile.save() self.profile.save()


+ 23
- 13
hc/accounts/tests/test_profile.py View File

@ -2,11 +2,11 @@ from django.contrib.auth.models import User
from django.core import mail from django.core import mail
from hc.test import BaseTestCase from hc.test import BaseTestCase
from hc.accounts.models import Profile, Member
from hc.accounts.models import Member
from hc.api.models import Check from hc.api.models import Check
class LoginTestCase(BaseTestCase):
class ProfileTestCase(BaseTestCase):
def test_it_sends_set_password_link(self): def test_it_sends_set_password_link(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
@ -16,8 +16,9 @@ class LoginTestCase(BaseTestCase):
assert r.status_code == 302 assert r.status_code == 302
# profile.token should be set now # profile.token should be set now
profile = Profile.objects.for_user(self.alice)
self.assertTrue(len(profile.token) > 10)
self.alice.profile.refresh_from_db()
token = self.alice.profile.token
self.assertTrue(len(token) > 10)
# And an email should have been sent # And an email should have been sent
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
@ -31,8 +32,9 @@ class LoginTestCase(BaseTestCase):
r = self.client.post("/accounts/profile/", form) r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200 assert r.status_code == 200
profile = Profile.objects.for_user(self.alice)
self.assertTrue(len(profile.api_key) > 10)
self.alice.profile.refresh_from_db()
api_key = self.alice.profile.api_key
self.assertTrue(len(api_key) > 10)
def test_it_revokes_api_key(self): def test_it_revokes_api_key(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
@ -41,15 +43,14 @@ class LoginTestCase(BaseTestCase):
r = self.client.post("/accounts/profile/", form) r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200 assert r.status_code == 200
profile = Profile.objects.for_user(self.alice)
self.assertEqual(profile.api_key, "")
self.alice.profile.refresh_from_db()
self.assertEqual(self.alice.profile.api_key, "")
def test_it_sends_report(self): def test_it_sends_report(self):
check = Check(name="Test Check", user=self.alice) check = Check(name="Test Check", user=self.alice)
check.save() check.save()
profile = Profile.objects.for_user(self.alice)
profile.send_report()
self.alice.profile.send_report()
# And an email should have been sent # And an email should have been sent
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
@ -65,8 +66,7 @@ class LoginTestCase(BaseTestCase):
r = self.client.post("/accounts/profile/", form) r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200 assert r.status_code == 200
profile = Profile.objects.for_user(self.alice)
member = profile.member_set.get()
member = self.alice.profile.member_set.get()
self.assertEqual(member.user.email, "[email protected]") self.assertEqual(member.user.email, "[email protected]")
@ -81,7 +81,7 @@ class LoginTestCase(BaseTestCase):
bob = User(username="bob", email="[email protected]") bob = User(username="bob", email="[email protected]")
bob.save() bob.save()
m = Member(team=Profile.objects.for_user(self.alice), user=bob)
m = Member(team=self.alice.profile, user=bob)
m.save() m.save()
form = {"remove_team_member": "1", "email": "[email protected]"} form = {"remove_team_member": "1", "email": "[email protected]"}
@ -89,3 +89,13 @@ class LoginTestCase(BaseTestCase):
assert r.status_code == 200 assert r.status_code == 200
self.assertEqual(Member.objects.count(), 0) self.assertEqual(Member.objects.count(), 0)
def test_it_sets_team_name(self):
self.client.login(username="[email protected]", password="password")
form = {"set_team_name": "1", "team_name": "Alpha Team"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.alice.profile.refresh_from_db()
self.assertEqual(self.alice.profile.team_name, "Alpha Team")

+ 37
- 0
hc/accounts/tests/test_switch_team.py View File

@ -0,0 +1,37 @@
from django.contrib.auth.models import User
from hc.test import BaseTestCase
from hc.accounts.models import Member, Profile
class SwitchTeamTestCase(BaseTestCase):
def setUp(self):
super(SwitchTeamTestCase, self).setUp()
self.bob = User(username="bob", email="[email protected]")
self.bob.set_password("password")
self.bob.save()
bobs_profile = Profile(user=self.bob)
bobs_profile.save()
m = Member(team=bobs_profile, user=self.alice)
m.save()
def test_it_switches(self):
self.client.login(username="[email protected]", password="password")
url = "/accounts/switch_team/%s/" % self.bob.username
r = self.client.get(url, follow=True)
self.assertContains(r, "[email protected]")
def test_it_checks_team_membership(self):
self.client.login(username="[email protected]", password="password")
url = "/accounts/switch_team/%s/" % self.bob.username
r = self.client.get(url)
self.assertEqual(r.status_code, 403)

+ 4
- 0
hc/accounts/urls.py View File

@ -21,4 +21,8 @@ urlpatterns = [
url(r'^set_password/([\w-]+)/$', url(r'^set_password/([\w-]+)/$',
views.set_password, name="hc-set-password"), views.set_password, name="hc-set-password"),
url(r'^switch_team/([\w-]+)/$',
views.switch_team, name="hc-switch-team"),
] ]

+ 34
- 12
hc/accounts/views.py View File

@ -8,11 +8,11 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.hashers import check_password from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import signing from django.core import signing
from django.http import HttpResponseBadRequest
from django.http import HttpResponseForbidden, HttpResponseBadRequest
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm, from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm,
RemoveTeamMemberForm, ReportSettingsForm, RemoveTeamMemberForm, ReportSettingsForm,
SetPasswordForm)
SetPasswordForm, TeamNameForm)
from hc.accounts.models import Profile, Member from hc.accounts.models import Profile, Member
from hc.api.models import Channel, Check from hc.api.models import Channel, Check
@ -23,6 +23,9 @@ def _make_user(email):
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
profile = Profile(user=user)
profile.save()
channel = Channel() channel = Channel()
channel.user = user channel.user = user
channel.kind = "email" channel.kind = "email"
@ -67,8 +70,7 @@ def login(request):
user = _make_user(email) user = _make_user(email)
_associate_demo_check(request, user) _associate_demo_check(request, user)
profile = Profile.objects.for_user(user)
profile.send_instant_login_link()
user.profile.send_instant_login_link()
return redirect("hc-login-link-sent") return redirect("hc-login-link-sent")
else: else:
@ -106,9 +108,8 @@ def check_token(request, username, token):
# This should get rid of "welcome_code" in session # This should get rid of "welcome_code" in session
request.session.flush() request.session.flush()
profile = Profile.objects.for_user(user)
profile.token = ""
profile.save()
user.profile.token = ""
user.profile.save()
auth_login(request, user) auth_login(request, user)
return redirect("hc-checks") return redirect("hc-checks")
@ -119,7 +120,7 @@ def check_token(request, username, token):
@login_required @login_required
def profile(request): def profile(request):
profile = Profile.objects.for_user(request.user)
profile = request.user.profile
show_api_key = False show_api_key = False
if request.method == "POST": if request.method == "POST":
@ -161,6 +162,12 @@ def profile(request):
email = form.cleaned_data["email"] email = form.cleaned_data["email"]
Member.objects.filter(team=profile, user__email=email).delete() Member.objects.filter(team=profile, user__email=email).delete()
messages.info(request, "%s removed from team!" % email) messages.info(request, "%s removed from team!" % email)
elif "set_team_name" in request.POST:
form = TeamNameForm(request.POST)
if form.is_valid():
profile.team_name = form.cleaned_data["team_name"]
profile.save()
messages.info(request, "Team Name updated!")
ctx = { ctx = {
"profile": profile, "profile": profile,
@ -172,7 +179,7 @@ def profile(request):
@login_required @login_required
def set_password(request, token): def set_password(request, token):
profile = Profile.objects.for_user(request.user)
profile = request.user.profile
if not check_password(token, profile.token): if not check_password(token, profile.token):
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -204,8 +211,23 @@ def unsubscribe_reports(request, username):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user = User.objects.get(username=username) user = User.objects.get(username=username)
profile = Profile.objects.for_user(user)
profile.reports_allowed = False
profile.save()
user.profile.reports_allowed = False
user.profile.save()
return render(request, "accounts/unsubscribed.html") return render(request, "accounts/unsubscribed.html")
def switch_team(request, target_username):
other_user = User.objects.get(username=target_username)
# Superuser can switch to any team.
# Other users can only switch to a team they are members of.
if not request.user.is_superuser:
q = Member.objects.filter(team=other_user.profile, user=request.user)
if q.count() == 0:
return HttpResponseForbidden()
request.user.profile.current_team = other_user.profile
request.user.profile.save()
return redirect("hc-checks")

+ 1
- 1
hc/api/management/commands/prunepings.py View File

@ -11,7 +11,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
# Create any missing user profiles # Create any missing user profiles
for user in User.objects.filter(profile=None): for user in User.objects.filter(profile=None):
Profile.objects.for_user(user)
Profile.objects.get_or_create(user_id=user.id)
q = Ping.objects q = Ping.objects
q = q.annotate(limit=F("owner__user__profile__ping_log_limit")) q = q.annotate(limit=F("owner__user__profile__ping_log_limit"))


+ 1
- 1
hc/api/management/commands/prunepingsslow.py View File

@ -18,7 +18,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
# Create any missing user profiles # Create any missing user profiles
for user in User.objects.filter(profile=None): for user in User.objects.filter(profile=None):
Profile.objects.for_user(user)
Profile.objects.get_or_create(user_id=user.id)
checks = Check.objects.filter(user__isnull=False) checks = Check.objects.filter(user__isnull=False)
checks = checks.annotate(limit=F("user__profile__ping_log_limit")) checks = checks.annotate(limit=F("user__profile__ping_log_limit"))


+ 0
- 6
hc/api/management/commands/sendreports.py View File

@ -1,6 +1,5 @@
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
@ -19,11 +18,6 @@ class Command(BaseCommand):
tmpl = "Sending monthly report to %s" tmpl = "Sending monthly report to %s"
def handle(self, *args, **options): def handle(self, *args, **options):
# Create any missing profiles
for u in User.objects.filter(profile__isnull=True):
self.stdout.write("Creating profile for %s" % u.email)
Profile.objects.for_user(u)
now = timezone.now() now = timezone.now()
month_before = now - timedelta(days=30) month_before = now - timedelta(days=30)


+ 0
- 2
hc/api/tests/test_create_check.py View File

@ -9,8 +9,6 @@ class CreateCheckTestCase(BaseTestCase):
def setUp(self): def setUp(self):
super(CreateCheckTestCase, self).setUp() super(CreateCheckTestCase, self).setUp()
self.profile = Profile(user=self.alice, api_key="abc")
self.profile.save()
def post(self, url, data): def post(self, url, data):
return self.client.post(url, json.dumps(data), return self.client.post(url, json.dumps(data),


+ 4
- 4
hc/api/tests/test_list_checks.py View File

@ -3,14 +3,13 @@ from datetime import timedelta as td
from hc.api.models import Check, User from hc.api.models import Check, User
from hc.test import BaseTestCase from hc.test import BaseTestCase
from hc.accounts.models import Profile
class ListChecksTestCase(BaseTestCase): class ListChecksTestCase(BaseTestCase):
def setUp(self): def setUp(self):
super(ListChecksTestCase, self).setUp() super(ListChecksTestCase, self).setUp()
self.profile = Profile(user=self.alice, api_key="abc")
self.profile.save()
self.checks = [ self.checks = [
Check(user=self.alice, name="Alice 1", timeout=td(seconds=3600), grace=td(seconds=900)), Check(user=self.alice, name="Alice 1", timeout=td(seconds=3600), grace=td(seconds=900)),
Check(user=self.alice, name="Alice 2", timeout=td(seconds=86400), grace=td(seconds=3600)), Check(user=self.alice, name="Alice 2", timeout=td(seconds=86400), grace=td(seconds=3600)),
@ -40,8 +39,9 @@ class ListChecksTestCase(BaseTestCase):
bob = User(username="bob", email="[email protected]") bob = User(username="bob", email="[email protected]")
bob.save() bob.save()
bob_check = Check(user=bob, name="Bob 1") bob_check = Check(user=bob, name="Bob 1")
bob_check.save()
r = self.get("/api/v1/checks/", { "api_key": "abc" })
r = self.get("/api/v1/checks/", {"api_key": "abc"})
self.assertEqual(len(r.json()["checks"]), 2) self.assertEqual(len(r.json()["checks"]), 2)
checks = { check["name"]: check for check in r.json()["checks"] } checks = { check["name"]: check for check in r.json()["checks"] }


+ 1
- 7
hc/front/tests/test_channel_checks.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Channel from hc.api.models import Channel
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -20,14 +18,10 @@ class ChannelChecksTestCase(BaseTestCase):
self.assertContains(r, "Assign Checks to Channel", status_code=200) self.assertContains(r, "Assign Checks to Channel", status_code=200)
def test_it_checks_owner(self): def test_it_checks_owner(self):
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password")
mallory.save()
# channel does not belong to mallory so this should come back # channel does not belong to mallory so this should come back
# with 403 Forbidden: # with 403 Forbidden:
url = "/integrations/%s/checks/" % self.channel.code url = "/integrations/%s/checks/" % self.channel.code
self.client.login(username="mallory@example.org", password="password")
self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
assert r.status_code == 403 assert r.status_code == 403


+ 0
- 6
hc/front/tests/test_log.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Check, Ping from hc.api.models import Check, Ping
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -37,10 +35,6 @@ class LogTestCase(BaseTestCase):
assert r.status_code == 404 assert r.status_code == 404
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
charlie = User(username="charlie", email="[email protected]")
charlie.set_password("password")
charlie.save()
url = "/checks/%s/log/" % self.check.code url = "/checks/%s/log/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)


+ 1
- 7
hc/front/tests/test_remove_channel.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Channel from hc.api.models import Channel
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -31,11 +29,7 @@ class RemoveChannelTestCase(BaseTestCase):
def test_it_checks_owner(self): def test_it_checks_owner(self):
url = "/integrations/%s/remove/" % self.channel.code url = "/integrations/%s/remove/" % self.channel.code
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password")
mallory.save()
self.client.login(username="[email protected]", password="password")
self.client.login(username="[email protected]", password="password")
r = self.client.post(url) r = self.client.post(url)
assert r.status_code == 403 assert r.status_code == 403


+ 1
- 7
hc/front/tests/test_remove_check.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Check from hc.api.models import Check
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -30,11 +28,7 @@ class RemoveCheckTestCase(BaseTestCase):
def test_it_checks_owner(self): def test_it_checks_owner(self):
url = "/checks/%s/remove/" % self.check.code url = "/checks/%s/remove/" % self.check.code
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password")
mallory.save()
self.client.login(username="[email protected]", password="password")
self.client.login(username="[email protected]", password="password")
r = self.client.post(url) r = self.client.post(url)
assert r.status_code == 403 assert r.status_code == 403


+ 8
- 18
hc/front/tests/test_update_channel.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Channel, Check from hc.api.models import Channel, Check
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -31,35 +29,27 @@ class UpdateChannelTestCase(BaseTestCase):
assert checks[0].code == self.check.code assert checks[0].code == self.check.code
def test_it_checks_channel_user(self): def test_it_checks_channel_user(self):
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password")
mallory.save()
payload = {"channel": self.channel.code} payload = {"channel": self.channel.code}
self.client.login(username="mallory@example.org", password="password")
self.client.login(username="[email protected]", password="password")
r = self.client.post("/integrations/", data=payload) r = self.client.post("/integrations/", data=payload)
# self.channel does not belong to mallory, this should fail--
# self.channel does not belong to charlie, this should fail--
assert r.status_code == 403 assert r.status_code == 403
def test_it_checks_check_user(self): def test_it_checks_check_user(self):
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password")
mallory.save()
mc = Channel(user=mallory, kind="email")
mc.email = "[email protected]"
mc.save()
charlies_channel = Channel(user=self.charlie, kind="email")
charlies_channel.email = "[email protected]"
charlies_channel.save()
payload = { payload = {
"channel": mc.code,
"channel": charlies_channel.code,
"check-%s" % self.check.code: True "check-%s" % self.check.code: True
} }
self.client.login(username="mallory@example.org", password="password")
self.client.login(username="charlie@example.org", password="password")
r = self.client.post("/integrations/", data=payload) r = self.client.post("/integrations/", data=payload)
# mc belongs to mallorym but self.check does not--
# mc belongs to charlie but self.check does not--
assert r.status_code == 403 assert r.status_code == 403
def test_it_handles_missing_channel(self): def test_it_handles_missing_channel(self):


+ 0
- 7
hc/front/tests/test_update_name.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Check from hc.api.models import Check
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -23,11 +21,6 @@ class UpdateNameTestCase(BaseTestCase):
assert check.name == "Alice Was Here" assert check.name == "Alice Was Here"
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
charlie = User(username="charlie", email="[email protected]")
charlie.set_password("password")
charlie.save()
url = "/checks/%s/name/" % self.check.code url = "/checks/%s/name/" % self.check.code
payload = {"name": "Charlie Sent This"} payload = {"name": "Charlie Sent This"}


+ 0
- 6
hc/front/tests/test_update_timeout.py View File

@ -1,5 +1,3 @@
from django.contrib.auth.models import User
from hc.api.models import Check from hc.api.models import Check
from hc.test import BaseTestCase from hc.test import BaseTestCase
@ -41,10 +39,6 @@ class UpdateTimeoutTestCase(BaseTestCase):
assert r.status_code == 404 assert r.status_code == 404
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
charlie = User(username="charlie", email="[email protected]")
charlie.set_password("password")
charlie.save()
url = "/checks/%s/timeout/" % self.check.code url = "/checks/%s/timeout/" % self.check.code
payload = {"timeout": 3600, "grace": 60} payload = {"timeout": 3600, "grace": 60}


+ 0
- 1
hc/front/urls.py View File

@ -8,7 +8,6 @@ urlpatterns = [
url(r'^checks/add/$', views.add_check, name="hc-add-check"), url(r'^checks/add/$', views.add_check, name="hc-add-check"),
url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"), url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"),
url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"), url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"),
url(r'^checks/([\w-]+)/email/$', views.email_preview),
url(r'^checks/([\w-]+)/remove/$', views.remove_check, name="hc-remove-check"), url(r'^checks/([\w-]+)/remove/$', views.remove_check, name="hc-remove-check"),
url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"), url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"),
url(r'^docs/$', views.docs, name="hc-docs"), url(r'^docs/$', views.docs, name="hc-docs"),


+ 16
- 41
hc/front/views.py View File

@ -11,7 +11,6 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.six.moves.urllib.parse import urlencode from django.utils.six.moves.urllib.parse import urlencode
from hc.accounts.models import Profile
from hc.api.decorators import uuid_or_400 from hc.api.decorators import uuid_or_400
from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE
from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm, from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm,
@ -28,7 +27,7 @@ def pairwise(iterable):
@login_required @login_required
def my_checks(request): def my_checks(request):
checks = Check.objects.filter(user=request.user).order_by("created")
checks = Check.objects.filter(user=request.team.user).order_by("created")
counter = Counter() counter = Counter()
down_tags, grace_tags = set(), set() down_tags, grace_tags = set(), set()
@ -122,7 +121,7 @@ def about(request):
def add_check(request): def add_check(request):
assert request.method == "POST" assert request.method == "POST"
check = Check(user=request.user)
check = Check(user=request.team.user)
check.save() check.save()
check.assign_all_channels() check.assign_all_channels()
@ -136,7 +135,7 @@ def update_name(request, code):
assert request.method == "POST" assert request.method == "POST"
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user_id != request.user.id:
if check.user_id != request.team.user.id:
return HttpResponseForbidden() return HttpResponseForbidden()
form = NameTagsForm(request.POST) form = NameTagsForm(request.POST)
@ -154,7 +153,7 @@ def update_timeout(request, code):
assert request.method == "POST" assert request.method == "POST"
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user != request.user:
if check.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
form = TimeoutForm(request.POST) form = TimeoutForm(request.POST)
@ -166,36 +165,13 @@ def update_timeout(request, code):
return redirect("hc-checks") return redirect("hc-checks")
@login_required
@uuid_or_400
def email_preview(request, code):
""" A debug view to see how email will look.
Will keep it around until I'm happy with email stying.
"""
check = Check.objects.get(code=code)
if check.user != request.user:
return HttpResponseForbidden()
ctx = {
"check": check,
"checks": check.user.check_set.all(),
"now": timezone.now()
}
return render(request, "emails/alert/body.html", ctx)
@login_required @login_required
@uuid_or_400 @uuid_or_400
def remove_check(request, code): def remove_check(request, code):
assert request.method == "POST" assert request.method == "POST"
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user != request.user:
if check.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
check.delete() check.delete()
@ -207,11 +183,10 @@ def remove_check(request, code):
@uuid_or_400 @uuid_or_400
def log(request, code): def log(request, code):
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user != request.user:
if check.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
profile = Profile.objects.for_user(request.user)
limit = profile.ping_log_limit
limit = request.team.ping_log_limit
pings = Ping.objects.filter(owner=check).order_by("-id")[:limit] pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
pings = list(pings.iterator()) pings = list(pings.iterator())
@ -264,7 +239,7 @@ def channels(request):
channel = Channel.objects.get(code=code) channel = Channel.objects.get(code=code)
except Channel.DoesNotExist: except Channel.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if channel.user_id != request.user.id:
if channel.user_id != request.team.user.id:
return HttpResponseForbidden() return HttpResponseForbidden()
new_checks = [] new_checks = []
@ -275,17 +250,17 @@ def channels(request):
check = Check.objects.get(code=code) check = Check.objects.get(code=code)
except Check.DoesNotExist: except Check.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if check.user_id != request.user.id:
if check.user_id != request.team.user.id:
return HttpResponseForbidden() return HttpResponseForbidden()
new_checks.append(check) new_checks.append(check)
channel.checks = new_checks channel.checks = new_checks
return redirect("hc-channels") return redirect("hc-channels")
channels = Channel.objects.filter(user=request.user).order_by("created")
channels = Channel.objects.filter(user=request.team.user).order_by("created")
channels = channels.annotate(n_checks=Count("checks")) channels = channels.annotate(n_checks=Count("checks"))
num_checks = Check.objects.filter(user=request.user).count()
num_checks = Check.objects.filter(user=request.team.user).count()
ctx = { ctx = {
"page": "channels", "page": "channels",
@ -300,7 +275,7 @@ def do_add_channel(request, data):
form = AddChannelForm(data) form = AddChannelForm(data)
if form.is_valid(): if form.is_valid():
channel = form.save(commit=False) channel = form.save(commit=False)
channel.user = request.user
channel.user = request.team.user
channel.save() channel.save()
channel.assign_all_checks() channel.assign_all_checks()
@ -323,11 +298,11 @@ def add_channel(request):
@uuid_or_400 @uuid_or_400
def channel_checks(request, code): def channel_checks(request, code):
channel = get_object_or_404(Channel, code=code) channel = get_object_or_404(Channel, code=code)
if channel.user_id != request.user.id:
if channel.user_id != request.team.user.id:
return HttpResponseForbidden() return HttpResponseForbidden()
assigned = set(channel.checks.values_list('code', flat=True).distinct()) assigned = set(channel.checks.values_list('code', flat=True).distinct())
checks = Check.objects.filter(user=request.user).order_by("created")
checks = Check.objects.filter(user=request.team.user).order_by("created")
ctx = { ctx = {
"checks": checks, "checks": checks,
@ -357,7 +332,7 @@ def remove_channel(request, code):
# user may refresh the page during POST and cause two deletion attempts # user may refresh the page during POST and cause two deletion attempts
channel = Channel.objects.filter(code=code).first() channel = Channel.objects.filter(code=code).first()
if channel: if channel:
if channel.user != request.user:
if channel.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
channel.delete() channel.delete()
@ -375,7 +350,7 @@ def add_webhook(request):
if request.method == "POST": if request.method == "POST":
form = AddWebhookForm(request.POST) form = AddWebhookForm(request.POST)
if form.is_valid(): if form.is_valid():
channel = Channel(user=request.user, kind="webhook")
channel = Channel(user=request.team.user, kind="webhook")
channel.value = form.get_value() channel.value = form.get_value()
channel.save() channel.save()


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

@ -106,7 +106,7 @@ def create_plan(request):
sub.save() sub.save()
# Update user's profile # Update user's profile
profile = Profile.objects.for_user(request.user)
profile = request.user.profile
if plan_id == "P5": if plan_id == "P5":
profile.ping_log_limit = 1000 profile.ping_log_limit = 1000
profile.team_access_allowed = True profile.team_access_allowed = True


+ 1
- 0
hc/settings.py View File

@ -49,6 +49,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'hc.accounts.middleware.TeamAccessMiddleware',
) )
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (


+ 15
- 0
hc/test.py View File

@ -1,11 +1,26 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from hc.accounts.models import Profile
class BaseTestCase(TestCase): class BaseTestCase(TestCase):
def setUp(self): def setUp(self):
super(BaseTestCase, self).setUp() super(BaseTestCase, self).setUp()
# Normal user for tests
self.alice = User(username="alice", email="[email protected]") self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
self.profile = Profile(user=self.alice, api_key="abc")
self.profile.save()
# "malicious user for tests
self.charlie = User(username="charlie", email="[email protected]")
self.charlie.set_password("password")
self.charlie.save()
charlies_profile = Profile(user=self.charlie)
charlies_profile.save()

+ 42
- 0
templates/accounts/profile.html View File

@ -131,6 +131,14 @@
</p> </p>
{% endif %} {% endif %}
<br />
<a
href="#"
class="btn btn-default"
data-toggle="modal"
data-target="#set-team-name-modal">Set Team Name</a>
<a <a
href="#" href="#"
class="btn btn-primary pull-right" class="btn btn-primary pull-right"
@ -247,6 +255,40 @@
</form> </form>
</div> </div>
</div> </div>
<div id="set-team-name-modal" class="modal">
<div class="modal-dialog">
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button>
<h4 class="remove-check-title">Set Team Name</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="team-name" class="col-sm-4 control-label">Team Name</label>
<div class="col-sm-7">
<input
type="text"
class="form-control"
id="team-name"
name="team_name"
value="{{ profile }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="set_team_name"
class="btn btn-primary">Set Team Name</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}


+ 17
- 2
templates/base.html View File

@ -110,11 +110,26 @@
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="dropdown"> <li class="dropdown">
<a id="nav-email" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"> <a id="nav-email" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">
{{ request.user.email }} <span class="caret"></span>
{{ request.user.profile.current_team }} <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'hc-profile' %}">Settings</a></li>
<li class="dropdown-header">{{ request.user.profile }}</li>
<li>
<a href="{% url 'hc-switch-team' request.user.username %}" class="active">Checks</a>
</li>
<li><a href="{% url 'hc-profile' %}">Account Settings</a></li>
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
{% for m in request.user.member_set.all %}
<li class="dropdown-header">{{ m.team }}</li>
<li>
<a href="{% url 'hc-switch-team' m.team.user.username %}">Checks</a>
</li>
<li role="separator" class="divider"></li>
{% endfor %}
<li><a href="{% url 'hc-logout' %}">Log Out</a></li> <li><a href="{% url 'hc-logout' %}">Log Out</a></li>
</ul> </ul>
</li> </li>


Loading…
Cancel
Save