diff --git a/EpisodesCommunity/settings.py b/EpisodesCommunity/settings.py index 8b9743e..63e691d 100644 --- a/EpisodesCommunity/settings.py +++ b/EpisodesCommunity/settings.py @@ -42,6 +42,13 @@ ALLOWED_HOSTS = [] # Application definition +AUTHENTICATION_BACKENDS = ( + 'LandingPage.backends.OAuthBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +AUTH_USER_MODEL = 'LandingPage.User' + INSTALLED_APPS = [ 'LandingPage.apps.LandingpageConfig', 'Show.apps.ShowConfig', @@ -65,6 +72,7 @@ MIDDLEWARE = [ ] ROOT_URLCONF = 'EpisodesCommunity.urls' +LOGIN_URL = '/login' TEMPLATES = [ { diff --git a/LandingPage/admin.py b/LandingPage/admin.py index 77fe1f1..44f4c55 100644 --- a/LandingPage/admin.py +++ b/LandingPage/admin.py @@ -1,10 +1,18 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin from .models import * +from .forms import SpecialUserChangeForm + +class SpecialUserAdmin(UserAdmin): + form = SpecialUserChangeForm + + fieldsets = UserAdmin.fieldsets + ( + (None, {'fields': ('display_name',)}), + ) # Register your models here. admin.site.register(Show) -admin.site.register(User) -admin.site.register(Admin) +admin.site.register(User, SpecialUserAdmin) admin.site.register(Ban) admin.site.register(ShowModerator) admin.site.register(Report) @@ -18,4 +26,3 @@ admin.site.register(Watch) admin.site.register(DiscussionBoard) admin.site.register(DiscussionReply) admin.site.register(DiscussionVote) - diff --git a/LandingPage/backends.py b/LandingPage/backends.py new file mode 100644 index 0000000..27673ee --- /dev/null +++ b/LandingPage/backends.py @@ -0,0 +1,60 @@ +import requests +import hashlib +import json +import logging +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + +class OAuthBackend(ModelBackend): + def authenticate(self, code=None): + resp = requests.post( + settings.AUTH_TOKEN_ENDPOINT+"token", + data={ + 'grant_type':'authorization_code', + 'code':code, + 'redirect_uri':settings.AUTH_REDIRECT_URL, + 'client_id':settings.AUTH_CLIENT_ID + }, + headers = { + 'Authorization':'Basic %s'%settings.AUTH_B64 + } + ) + resp_json = resp.json() + if 'error' in resp_json: + logging.warn('OAuth server returned an error: %s'%json.dumps(resp_json)) + else: + user_info = requests.get( + settings.AUTH_TOKEN_ENDPOINT+"user", + headers = { + 'Authorization': 'Bearer ' + resp_json['access_token'] + } + ).json() + + usermodel = get_user_model() + matches = usermodel.objects.filter(icy_id=user_info['uuid']) + match = None + + if not len(matches): + user = usermodel.objects.create_user( + username = user_info['username'], + email = user_info['email'], + icy_id = user_info['uuid'], + display_name = user_info['display_name'] + ) + + if 'privilege' in user_info: + priv = user_info['privilege'] + user.is_superuser = (priv == 5) + user.is_staff = (priv > 0) + + user.save() + + match = user + else: + match = matches[0] + + match.access_token = resp_json['access_token'] + + return match + return None diff --git a/LandingPage/forms.py b/LandingPage/forms.py new file mode 100644 index 0000000..773d760 --- /dev/null +++ b/LandingPage/forms.py @@ -0,0 +1,6 @@ +from django.contrib.auth.forms import UserChangeForm +from .models import User + +class SpecialUserChangeForm(UserChangeForm): + class Meta(UserChangeForm.Meta): + model = User diff --git a/LandingPage/models.py b/LandingPage/models.py index be27211..9295e45 100644 --- a/LandingPage/models.py +++ b/LandingPage/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.contrib.auth.models import AbstractUser from django.core.files.storage import FileSystemStorage from django.conf import settings import os @@ -71,14 +72,11 @@ class Show(TimestampedModel): def __str__(self): return '%s [%s]'%(self.name,self.abbr) -class User(TimestampedModel): - user_id = models.CharField( +class User(AbstractUser): + icy_id = models.CharField( max_length=36, help_text='The UUID assigned to this user by IcyNet\'s auth servers' ) - email = models.EmailField( - help_text='This user\'s email address' - ) display_name=models.CharField( max_length=20, help_text="The name shown to other users", @@ -94,11 +92,6 @@ class User(TimestampedModel): related_name='watched_by', through='Watch' ) - def __str__(self): - return self.email - -class Admin(User): - pass class Ban(TimestampedModel): user = models.OneToOneField( @@ -108,7 +101,7 @@ class Ban(TimestampedModel): verbose_name="Banned User" ) admin = models.ForeignKey( - Admin, + User, on_delete=models.SET_NULL, null=True, help_text='The admin which banned this user', @@ -286,7 +279,7 @@ class Submission(TimestampedModel): verbose_name='Submitted For' ) user = models.ForeignKey( - 'User', + User, on_delete=models.SET_NULL, null=True, related_name='submissions', @@ -310,7 +303,7 @@ class SubmissionVote(TimestampedModel): help_text='What this submission was cast on' ) user = models.ForeignKey( - 'User', + User, on_delete=models.CASCADE, related_name='votes', help_text='The user who cast this vote' diff --git a/LandingPage/templates/base.html b/LandingPage/templates/base.html index 9d3499a..44580b1 100644 --- a/LandingPage/templates/base.html +++ b/LandingPage/templates/base.html @@ -20,8 +20,8 @@
- {% if request.session.user_id %} - {{ request.session.disp_name }} + {% if user.is_authenticated %} + {{ user.display_name }} {% else %} Log in {% endif %} diff --git a/LandingPage/urls.py b/LandingPage/urls.py index 8c4c86d..7b2ab2c 100644 --- a/LandingPage/urls.py +++ b/LandingPage/urls.py @@ -3,6 +3,7 @@ from django.conf.urls import url from . import views urlpatterns = [ + url(r'^logout/$', views.LogoutView), url(r'^login/redirect$', views.LoginRedirect.as_view()), url(r'^login$', views.Login.as_view()), url(r'^$', views.LandingPage.as_view()), diff --git a/LandingPage/views.py b/LandingPage/views.py index 84eeb7f..b515f80 100644 --- a/LandingPage/views.py +++ b/LandingPage/views.py @@ -1,10 +1,12 @@ from django.shortcuts import render from django.views import View from django.views.generic.base import TemplateView +from django.contrib.auth import login as auth_login, authenticate from django.conf import settings from django.http import HttpResponse from django.http import HttpResponseRedirect from django.db.models import Max +from django.contrib.auth.views import logout import requests import hashlib import json @@ -29,57 +31,30 @@ class LoginRedirect(View): userstate = generateState(req) if userstate == req.GET['state']: code = req.GET['code'] - resp = requests.post( - settings.AUTH_TOKEN_ENDPOINT+"token", - data={ - 'grant_type':'authorization_code', - 'code':code, - 'redirect_uri':settings.AUTH_REDIRECT_URL, - 'client_id':settings.AUTH_CLIENT_ID - }, - headers = { - 'Authorization':'Basic %s'%settings.AUTH_B64 - } - ) - resp_json = resp.json() - if 'error' in resp_json: - r = HttpResponse('

OAuth Error

%s
'%json.dumps(resp_json)) - r.status = 500 - return r - else: - user_info = requests.get( - settings.AUTH_TOKEN_ENDPOINT+"user", - headers = { - 'Authorization': 'Bearer ' + resp_json['access_token'] - } - ).json() - req.session['user_id'] = user_info['uuid'] - matches = User.objects.filter(user_id=user_info['uuid']) - match = None - if not len(matches): - user = User( - user_id = user_info['uuid'], - email = user_info['email'], - display_name = user_info['display_name'] - ) - user.save() - match = user - else: - match = matches[0] - req.session['token'] = resp_json['access_token'] - req.session['disp_name'] = match.display_name + + user = authenticate(code=code) + + if user is not None and user.is_active: + auth_login(req, user) + return HttpResponseRedirect('/') - else: - return HttpResponse('

Unmatching state tokens


It looks like the request to login wasn\'t started by you. Try going back to the home page and logging in again.

', status=400) + + return HttpResponse('

Error


It looks like something went wrong while trying to authenticate you. Please try again later.

', status=500) + + return HttpResponse('

Unmatching state tokens


It looks like the request to login wasn\'t started by you. Try going back to the home page and logging in again.

', status=400) class Login(View): def get(self, req): - url = '%sauthorize?response_type=code&client_id=%s&redirect_uri=%s&scope=email&state=%s'%(settings.AUTH_TOKEN_ENDPOINT,settings.AUTH_CLIENT_ID,settings.AUTH_REDIRECT_URL, generateState(req)) + url = '%sauthorize?response_type=code&client_id=%s&redirect_uri=%s&scope=email privilege&state=%s'%(settings.AUTH_TOKEN_ENDPOINT,settings.AUTH_CLIENT_ID,settings.AUTH_REDIRECT_URL, generateState(req)) response = HttpResponse("Redirecting you to the IcyNet auth page...") response.status_code = 302 response['Location'] = url return response +def LogoutView(request): + logout(request) + return HttpResponseRedirect('/') + def generateState(request): request.session.save() diff --git a/Show/templates/episode.html b/Show/templates/episode.html index 9563ae7..0fa4fbe 100644 --- a/Show/templates/episode.html +++ b/Show/templates/episode.html @@ -30,7 +30,7 @@
 Show Index - {% if request.session.user_id %} + {% if user.is_authenticated %}  Submit New Link {% else %} Log in to submit a link diff --git a/Show/views.py b/Show/views.py index ba23497..f2e2ce4 100644 --- a/Show/views.py +++ b/Show/views.py @@ -2,11 +2,13 @@ from django.template import RequestContext from django.shortcuts import render from django.views import View from django.views.generic.base import TemplateView +from django.contrib.auth.decorators import login_required from django.conf import settings from django.http import Http404 from django.http import HttpResponse from django.http import HttpResponseRedirect from django.db.models import Case, When, Value, IntegerField, Count, F +from django.contrib.auth.mixins import LoginRequiredMixin from LandingPage.models import User from LandingPage.models import Show @@ -85,15 +87,11 @@ class EpisodeView(TemplateView): return ctx # Submission form GET and POST +@login_required def SubmissionForm(req, abbreviation, season, episode): show = Show.objects.get(abbr=abbreviation) episode = Episode.objects.filter(show=show,season__number=season,episode=episode).first() - - # Check for login status - if not 'user_id' in req.session: - return HttpResponse('

Error

You need to be logged in to submit. Please log in

', status=400) - - user = User.objects.get(user_id=req.session['user_id']) + user = req.user # 404's if not show: @@ -144,16 +142,12 @@ def SubmissionForm(req, abbreviation, season, episode): # Vote request # /show/{{abbr}}/vote/{{submission id}}/{{positive == 1}} -class SubmissionVoteSubmit(View): +class SubmissionVoteSubmit(LoginRequiredMixin, View): def post (self, req, abbreviation, subid, positive): # Convert positive parameter into a boolean pos_bool = int(positive) == 1 - # Check for login status - if not 'user_id' in req.session: - return HttpResponse('

Error

You need to be logged in to vote. Please log in

', status=400) - - user = User.objects.get(user_id=req.session['user_id']) + user = req.user # Get the submission from the database submission = Submission.objects.filter(id=subid).first() diff --git a/tests/LandingPage/test_views.py b/tests/LandingPage/test_views.py index 4615965..d2322cd 100644 --- a/tests/LandingPage/test_views.py +++ b/tests/LandingPage/test_views.py @@ -15,7 +15,7 @@ class TestLogin(TestCase): # Set up responses to control network flow with responses.RequestsMock() as rm: rm.add(responses.POST,'http://icynet.test/api/token',json={'access_token':'1accesstoken1'}) - rm.add(responses.GET,'http://icynet.test/api/user',json={'uuid':'935a41b5-b38d-42c3-96ef-653402fc44ca','email':'johnsmith@gmail.com','display_name':'Mr. Smith'}) + rm.add(responses.GET,'http://icynet.test/api/user',json={'uuid':'935a41b5-b38d-42c3-96ef-653402fc44ca','email':'johnsmith@gmail.com','display_name':'Mr. Smith','username':'jsmith'}) # Make initial request to redirect endpoint client = Client() @@ -26,7 +26,7 @@ class TestLogin(TestCase): self.assertEqual(query['client_id'][0],'clid') self.assertEqual(query['response_type'][0],'code') self.assertEqual(query['redirect_uri'][0],'http://redirect.test') - self.assertEqual(query['scope'][0],'email') + self.assertEqual(query['scope'][0],'email privilege') # Make connection to the real endpoint resp = client.get('/login/redirect?state=%s&code=%s'%(state, 'code')) @@ -36,14 +36,12 @@ class TestLogin(TestCase): users = User.objects.all() self.assertEqual(len(users), 1) user = users[0] - self.assertEqual(user.user_id,'935a41b5-b38d-42c3-96ef-653402fc44ca') + self.assertEqual(user.icy_id,'935a41b5-b38d-42c3-96ef-653402fc44ca') self.assertEqual(user.email,'johnsmith@gmail.com') self.assertEqual(user.display_name, 'Mr. Smith') - # Check appropriate values are in the session - self.assertEqual(client.session['user_id'], '935a41b5-b38d-42c3-96ef-653402fc44ca') - self.assertEqual(client.session['token'],'1accesstoken1') - self.assertEqual(client.session['disp_name'], 'Mr. Smith') + # Check that the user has been logged in + self.assertEqual(client.get('/').context['user'], user) def test_reject_bad_state(self): with responses.RequestsMock() as rm: @@ -55,10 +53,10 @@ class TestLogin(TestCase): # Set up responses to control network flow with responses.RequestsMock() as rm: rm.add(responses.POST,'http://icynet.test/api/token',json={'access_token':'1accesstoken1'}) - rm.add(responses.GET,'http://icynet.test/api/user',json={'uuid':'935a41b5-b38d-42c3-96ef-653402fc44ca','email':'johnsmith@gmail.com','display_name':'Mr. Smith'}) + rm.add(responses.GET,'http://icynet.test/api/user',json={'uuid':'935a41b5-b38d-42c3-96ef-653402fc44ca','email':'johnsmith@gmail.com','display_name':'Mr. Smith','username':'jsmith'}) # Set up the database - user = User(user_id='935a41b5-b38d-42c3-96ef-653402fc44ca',email='johnsmith@gmail.com',display_name='Mr. Smith') + user = User(icy_id='935a41b5-b38d-42c3-96ef-653402fc44ca',email='johnsmith@gmail.com',display_name='Mr. Smith') user.save() # Make initial request to redirect endpoint @@ -74,14 +72,12 @@ class TestLogin(TestCase): users = User.objects.all() self.assertEqual(len(users), 1) user = users[0] - self.assertEqual(user.user_id,'935a41b5-b38d-42c3-96ef-653402fc44ca') + self.assertEqual(user.icy_id,'935a41b5-b38d-42c3-96ef-653402fc44ca') self.assertEqual(user.email,'johnsmith@gmail.com') self.assertEqual(user.display_name, 'Mr. Smith') - # Check appropriate values are in the session - self.assertEqual(client.session['user_id'], '935a41b5-b38d-42c3-96ef-653402fc44ca') - self.assertEqual(client.session['token'],'1accesstoken1') - self.assertEqual(client.session['disp_name'], 'Mr. Smith') + # Check that the user has been logged in + self.assertEqual(client.get('/').context['user'], user) def test_states_unique(self): with responses.RequestsMock() as rm: