From 864b3787c810ff892aa6a2c07ad238df859ad8d7 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Tue, 26 Sep 2017 11:30:34 +0300 Subject: [PATCH 1/4] Add sahkopiikki user create command --- members/management/__init__.py | 0 members/management/commands/__init__.py | 0 .../commands/createsahkopiikkiuser.py | 26 +++++++++++++++++++ members/views/utils.py | 5 ++-- sikweb/base.py | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 members/management/__init__.py create mode 100644 members/management/commands/__init__.py create mode 100644 members/management/commands/createsahkopiikkiuser.py diff --git a/members/management/__init__.py b/members/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/members/management/commands/__init__.py b/members/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/members/management/commands/createsahkopiikkiuser.py b/members/management/commands/createsahkopiikkiuser.py new file mode 100644 index 0000000..2274e86 --- /dev/null +++ b/members/management/commands/createsahkopiikkiuser.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User +from rest_framework.authtoken.models import Token + + +class Command(BaseCommand): + user_name = "sahkopiikki" + password = User.objects.make_random_password() + + def handle(self, *args, **options): + if User.objects.filter(username=self.user_name).exists(): + self.stdout.write("Sahkopiikki user already exists. Skipping.") + user = User.objects.get(username=self.user_name) + token = Token.objects.get(user=user) + self.stdout.write("Token: {}".format(token)) + return + + u = User(username=self.user_name) + u.set_password(self.password) + u.save() + + token = Token.objects.create(user=u) + + self.stdout.write("Created sahkopiikki user '{}' with password '{}' and token '{}'.".format( + self.user_name, self.password, token + )) diff --git a/members/views/utils.py b/members/views/utils.py index 4636505..80485c4 100644 --- a/members/views/utils.py +++ b/members/views/utils.py @@ -13,9 +13,10 @@ import csv # REST framework from members.serializers import MemberSerializer +from members.throttles import BurstRateThrottle, SustainedRateThrottle + from rest_framework import generics from rest_framework import permissions -from rest_framework.throttling import UserRateThrottle, AnonRateThrottle from members.models import Member, Request, Payment from members.forms import MemberForm, PaymentForm, ApplicationForm, CSVValidationError @@ -29,7 +30,7 @@ class MemberDetail(generics.RetrieveAPIView): queryset = Member.objects.all() serializer_class = MemberSerializer permission_classes = (permissions.IsAdminUser, ) - throttle_classes = (UserRateThrottle, AnonRateThrottle, ) + throttle_classes = (BurstRateThrottle, SustainedRateThrottle, ) def error_view(request, message): diff --git a/sikweb/base.py b/sikweb/base.py index e340969..e8b5bd7 100644 --- a/sikweb/base.py +++ b/sikweb/base.py @@ -72,6 +72,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework.authtoken', 'corsheaders', 'webapp', 'members', From e10af28c4da0edc37f8235177980cc718f623f18 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Tue, 26 Sep 2017 13:06:27 +0300 Subject: [PATCH 2/4] Remove unused log statement --- coffee_scale/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/coffee_scale/views.py b/coffee_scale/views.py index dc9bd00..9b8ae08 100644 --- a/coffee_scale/views.py +++ b/coffee_scale/views.py @@ -11,7 +11,6 @@ from django.conf import settings def coffee_view(request): - logging.info('User navigated to coffee page!') return render(request, 'coffee.html') From 61f5c293dbdca4171940d2b213ccb22df4873b9d Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Tue, 26 Sep 2017 14:39:21 +0300 Subject: [PATCH 3/4] =?UTF-8?q?Implement=20token=20auth=20for=20s=C3=A4hk?= =?UTF-8?q?=C3=B6piikki?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/createsahkopiikkiuser.py | 6 +++++- members/migrations/0017_auto_20170926_1316.py | 19 ++++++++++++++++++ members/models.py | 5 +++++ members/permissions.py | 8 ++++++++ members/urls.py | 4 +++- members/views/members.py | 20 ++++++++++++++++++- members/views/utils.py | 2 +- sikweb/base.py | 3 +++ 8 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 members/migrations/0017_auto_20170926_1316.py create mode 100644 members/permissions.py diff --git a/members/management/commands/createsahkopiikkiuser.py b/members/management/commands/createsahkopiikkiuser.py index 2274e86..ff9e3e5 100644 --- a/members/management/commands/createsahkopiikkiuser.py +++ b/members/management/commands/createsahkopiikkiuser.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Permission from rest_framework.authtoken.models import Token @@ -17,8 +17,12 @@ class Command(BaseCommand): u = User(username=self.user_name) u.set_password(self.password) + u.save() + permission = Permission.objects.get(codename='check_by_email') + u.user_permissions.add(permission) + token = Token.objects.create(user=u) self.stdout.write("Created sahkopiikki user '{}' with password '{}' and token '{}'.".format( diff --git a/members/migrations/0017_auto_20170926_1316.py b/members/migrations/0017_auto_20170926_1316.py new file mode 100644 index 0000000..8aea6fd --- /dev/null +++ b/members/migrations/0017_auto_20170926_1316.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-26 10:16 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0016_auto_20170925_1924'), + ] + + operations = [ + migrations.AlterModelOptions( + name='member', + options={'permissions': (('check_by_email', 'Can check if user exists by email'),)}, + ), + ] diff --git a/members/models.py b/members/models.py index 6cd4dc3..7e4d7ea 100644 --- a/members/models.py +++ b/members/models.py @@ -85,6 +85,11 @@ class Member(BaseMember): created = models.DateTimeField(_('Created'), default=timezone.now) + class Meta: + permissions = ( + ('check_by_email', 'Can check if user exists by email'), + ) + def last_paid(self): """Return member's last payment.""" try: diff --git a/members/permissions.py b/members/permissions.py new file mode 100644 index 0000000..f117b81 --- /dev/null +++ b/members/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission + +import logging + + +class CheckByEmailPermission(BasePermission): + def has_permission(self, request, view): + return request.user.has_perm('members.check_by_email') diff --git a/members/urls.py b/members/urls.py index 1578da4..39b973e 100644 --- a/members/urls.py +++ b/members/urls.py @@ -25,7 +25,8 @@ from members.views import add_many_confirm from members.views import MemberAutoComplete # rest api -from members.views import MemberDetail +from members.views import MemberDetail, CheckByEmail +from rest_framework.authtoken import views # application from members.views import application_form @@ -125,4 +126,5 @@ urlpatterns = [ name='member-autocomplete', ), + url(r'^check', CheckByEmail.as_view()) ] diff --git a/members/views/members.py b/members/views/members.py index 7c61fd4..16467a9 100644 --- a/members/views/members.py +++ b/members/views/members.py @@ -3,7 +3,7 @@ from django.contrib.auth.decorators import permission_required from django.utils.decorators import method_decorator from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import ensure_csrf_cookie -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse, HttpResponseForbidden from django.core.mail import send_mail from django.conf import settings from django.utils.translation import ugettext as _ @@ -13,6 +13,11 @@ from dal import autocomplete import logging import html +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import authentication, permissions +from members.permissions import CheckByEmailPermission + from members.models import Member, Request, Payment from members.forms import MemberForm, CSVValidationError from members.tables import MemberTable @@ -199,3 +204,16 @@ class MemberAutoComplete(autocomplete.Select2QuerySetView): qs = Member.find_members_by_name(self.q) return qs + + +class CheckByEmail(APIView): + """Check by email""" + + authentication_classes = (authentication.TokenAuthentication,) + permission_classes = (CheckByEmailPermission,) + + def get(self, request, format=None): + email = request.query_params.get('email') + exists = bool(email and Member.objects.filter(email=email).exists()) + resp = {'exists': exists} + return JsonResponse(resp) diff --git a/members/views/utils.py b/members/views/utils.py index 80485c4..3463914 100644 --- a/members/views/utils.py +++ b/members/views/utils.py @@ -29,7 +29,7 @@ class MemberDetail(generics.RetrieveAPIView): queryset = Member.objects.all() serializer_class = MemberSerializer - permission_classes = (permissions.IsAdminUser, ) + permission_classes = (permissions.DjangoModelPermissions, ) throttle_classes = (BurstRateThrottle, SustainedRateThrottle, ) diff --git a/sikweb/base.py b/sikweb/base.py index e8b5bd7..5cdb35a 100644 --- a/sikweb/base.py +++ b/sikweb/base.py @@ -193,6 +193,9 @@ REST_FRAMEWORK = { 'rest_framework.permissions.DjangoModelPermissions', 'rest_framework.permissions.IsAdminUser', ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + ), 'DEFAULT_THROTTLE_CLASSES': ( 'members.throttles.BurstRateThrottle', 'members.throttles.SustainedRateThrottle' From 5e33a8e30c66de34a91f26bad3c9996f64825a99 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Tue, 26 Sep 2017 15:30:23 +0300 Subject: [PATCH 4/4] =?UTF-8?q?Add=20tests=20for=20s=C3=A4hk=C3=B6piikki?= =?UTF-8?q?=20API=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- members/tests.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/members/tests.py b/members/tests.py index d2d6bb4..622fbf6 100644 --- a/members/tests.py +++ b/members/tests.py @@ -2,8 +2,11 @@ from django.test import TestCase, Client from django.contrib.auth.models import User - +from members.management.commands.createsahkopiikkiuser import Command as SahkopiikkiCommand from members.models import Member +from rest_framework.authtoken.models import Token + +import logging class MemberRegisterTestCase(TestCase): @@ -18,6 +21,9 @@ class MemberRegisterTestCase(TestCase): self.c = Client() self.c.login(username=username, password=password) + sc = SahkopiikkiCommand() + sc.handle() + def test_member_created(self): """Test member creation.""" exists = Member.objects.filter(first_name="Tidus").exists() @@ -50,3 +56,34 @@ class MemberRegisterTestCase(TestCase): response = self.c.get('/members/member-autocomplete?q={}'.format(search_terms), follow=True) results = response.json()['results'] self.assertEqual(len(results), 0) + + def test_sahkopiikki_check_by_email_not_found(self): + """Test if sähköpiikki auth and search work""" + email = 'teppo@tulppu.fi' + wrong_email = 'asd@asd.fi' + Member.objects.create(email=email, first_name='Teppo', last_name='Tulppu') + token = Token.objects.get(user__username='sahkopiikki').key + self.c.defaults['HTTP_AUTHORIZATION'] = 'Token ' + token + + response = self.c.get('/members/check?email={}'.format(wrong_email), follow=True) + self.assertEqual(response.json()['exists'], False) + + def test_sahkopiikki_check_by_email_found(self): + """Test if sähköpiikki auth and search work""" + email = 'teppo@tulppu.fi' + Member.objects.create(email=email, first_name='Teppo', last_name='Tulppu') + token = Token.objects.get(user__username='sahkopiikki').key + self.c.defaults['HTTP_AUTHORIZATION'] = 'Token ' + token + + response = self.c.get('/members/check?email={}'.format(email), follow=True) + self.assertEqual(response.json()['exists'], True) + + def test_sahkopiikki_check_by_email_forbidden(self): + """Test if sähköpiikki auth and search work""" + email = 'teppo@tulppu.fi' + Member.objects.create(email=email, first_name='Teppo', last_name='Tulppu') + token = Token.objects.get(user__username='sahkopiikki').key + self.c.defaults['HTTP_AUTHORIZATION'] = 'Token ' + token + 'DERP' + + response = self.c.get('/members/check?email={}'.format(email), follow=True) + self.assertEqual(response.status_code, 401)