Merge pull request #54 from IcyNet/authstuff

Rewrite the user models to use Django's auth system.
This commit is contained in:
Evert Prants 2017-12-11 15:55:57 +02:00 committed by GitHub
commit 5f8d142508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 127 additions and 87 deletions

View File

@ -42,6 +42,13 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
AUTHENTICATION_BACKENDS = (
'LandingPage.backends.OAuthBackend',
'django.contrib.auth.backends.ModelBackend',
)
AUTH_USER_MODEL = 'LandingPage.User'
INSTALLED_APPS = [ INSTALLED_APPS = [
'LandingPage.apps.LandingpageConfig', 'LandingPage.apps.LandingpageConfig',
'Show.apps.ShowConfig', 'Show.apps.ShowConfig',
@ -65,6 +72,7 @@ MIDDLEWARE = [
] ]
ROOT_URLCONF = 'EpisodesCommunity.urls' ROOT_URLCONF = 'EpisodesCommunity.urls'
LOGIN_URL = '/login'
TEMPLATES = [ TEMPLATES = [
{ {

View File

@ -1,10 +1,18 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import * from .models import *
from .forms import SpecialUserChangeForm
class SpecialUserAdmin(UserAdmin):
form = SpecialUserChangeForm
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('display_name',)}),
)
# Register your models here. # Register your models here.
admin.site.register(Show) admin.site.register(Show)
admin.site.register(User) admin.site.register(User, SpecialUserAdmin)
admin.site.register(Admin)
admin.site.register(Ban) admin.site.register(Ban)
admin.site.register(ShowModerator) admin.site.register(ShowModerator)
admin.site.register(Report) admin.site.register(Report)
@ -18,4 +26,3 @@ admin.site.register(Watch)
admin.site.register(DiscussionBoard) admin.site.register(DiscussionBoard)
admin.site.register(DiscussionReply) admin.site.register(DiscussionReply)
admin.site.register(DiscussionVote) admin.site.register(DiscussionVote)

60
LandingPage/backends.py Normal file
View File

@ -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

6
LandingPage/forms.py Normal file
View File

@ -0,0 +1,6 @@
from django.contrib.auth.forms import UserChangeForm
from .models import User
class SpecialUserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = User

View File

@ -1,4 +1,5 @@
from django.db import models from django.db import models
from django.contrib.auth.models import AbstractUser
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.conf import settings from django.conf import settings
import os import os
@ -71,14 +72,11 @@ class Show(TimestampedModel):
def __str__(self): def __str__(self):
return '%s [%s]'%(self.name,self.abbr) return '%s [%s]'%(self.name,self.abbr)
class User(TimestampedModel): class User(AbstractUser):
user_id = models.CharField( icy_id = models.CharField(
max_length=36, max_length=36,
help_text='The UUID assigned to this user by IcyNet\'s auth servers' 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( display_name=models.CharField(
max_length=20, max_length=20,
help_text="The name shown to other users", help_text="The name shown to other users",
@ -94,11 +92,6 @@ class User(TimestampedModel):
related_name='watched_by', related_name='watched_by',
through='Watch' through='Watch'
) )
def __str__(self):
return self.email
class Admin(User):
pass
class Ban(TimestampedModel): class Ban(TimestampedModel):
user = models.OneToOneField( user = models.OneToOneField(
@ -108,7 +101,7 @@ class Ban(TimestampedModel):
verbose_name="Banned User" verbose_name="Banned User"
) )
admin = models.ForeignKey( admin = models.ForeignKey(
Admin, User,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
help_text='The admin which banned this user', help_text='The admin which banned this user',
@ -286,7 +279,7 @@ class Submission(TimestampedModel):
verbose_name='Submitted For' verbose_name='Submitted For'
) )
user = models.ForeignKey( user = models.ForeignKey(
'User', User,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
related_name='submissions', related_name='submissions',
@ -310,7 +303,7 @@ class SubmissionVote(TimestampedModel):
help_text='What this submission was cast on' help_text='What this submission was cast on'
) )
user = models.ForeignKey( user = models.ForeignKey(
'User', User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='votes', related_name='votes',
help_text='The user who cast this vote' help_text='The user who cast this vote'

View File

@ -20,8 +20,8 @@
<div class="unibar"> <div class="unibar">
<span class="logo">Episodes<span class="period">.</span>Community</span> <span class="logo">Episodes<span class="period">.</span>Community</span>
<div class="userdata"> <div class="userdata">
{% if request.session.user_id %} {% if user.is_authenticated %}
{{ request.session.disp_name }} {{ user.display_name }}
{% else %} {% else %}
<a href="/login">Log in</a> <a href="/login">Log in</a>
{% endif %} {% endif %}

View File

@ -3,6 +3,7 @@ from django.conf.urls import url
from . import views from . import views
urlpatterns = [ urlpatterns = [
url(r'^logout/$', views.LogoutView),
url(r'^login/redirect$', views.LoginRedirect.as_view()), url(r'^login/redirect$', views.LoginRedirect.as_view()),
url(r'^login$', views.Login.as_view()), url(r'^login$', views.Login.as_view()),
url(r'^$', views.LandingPage.as_view()), url(r'^$', views.LandingPage.as_view()),

View File

@ -1,10 +1,12 @@
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.contrib.auth import login as auth_login, authenticate
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.db.models import Max from django.db.models import Max
from django.contrib.auth.views import logout
import requests import requests
import hashlib import hashlib
import json import json
@ -29,57 +31,30 @@ class LoginRedirect(View):
userstate = generateState(req) userstate = generateState(req)
if userstate == req.GET['state']: if userstate == req.GET['state']:
code = req.GET['code'] code = req.GET['code']
resp = requests.post(
settings.AUTH_TOKEN_ENDPOINT+"token", user = authenticate(code=code)
data={
'grant_type':'authorization_code', if user is not None and user.is_active:
'code':code, auth_login(req, user)
'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('<h1>OAuth Error</h1><pre>%s</pre>'%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
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
else:
return HttpResponse('<h1>Unmatching state tokens</h1><br><p>It looks like the request to login wasn\'t started by you. Try going back to the home page and logging in again.</p>', status=400) return HttpResponse('<h1>Error</h1><br><p>It looks like something went wrong while trying to authenticate you. Please try again later.</p>', status=500)
return HttpResponse('<h1>Unmatching state tokens</h1><br><p>It looks like the request to login wasn\'t started by you. Try going back to the home page and logging in again.</p>', status=400)
class Login(View): class Login(View):
def get(self, req): 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 = HttpResponse("Redirecting you to the IcyNet auth page...")
response.status_code = 302 response.status_code = 302
response['Location'] = url response['Location'] = url
return response return response
def LogoutView(request):
logout(request)
return HttpResponseRedirect('/')
def generateState(request): def generateState(request):
request.session.save() request.session.save()

View File

@ -30,7 +30,7 @@
</section> </section>
<section class="submissions"> <section class="submissions">
<a href="/show/{{show.abbr}}" class="button"><i class="fa fa-fw fa-home"></i>&nbsp;Show Index</a> <a href="/show/{{show.abbr}}" class="button"><i class="fa fa-fw fa-home"></i>&nbsp;Show Index</a>
{% if request.session.user_id %} {% if user.is_authenticated %}
<a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="button"><i class="fa fa-fw fa-plus"></i>&nbsp;Submit New Link</a> <a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="button"><i class="fa fa-fw fa-plus"></i>&nbsp;Submit New Link</a>
{% else %} {% else %}
<span class="fillertext"><a href="/login">Log in</a> to submit a link</span> <span class="fillertext"><a href="/login">Log in</a> to submit a link</span>

View File

@ -2,11 +2,13 @@ from django.template import RequestContext
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.contrib.auth.decorators import login_required
from django.conf import settings from django.conf import settings
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.db.models import Case, When, Value, IntegerField, Count, F 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 User
from LandingPage.models import Show from LandingPage.models import Show
@ -85,15 +87,11 @@ class EpisodeView(TemplateView):
return ctx return ctx
# Submission form GET and POST # Submission form GET and POST
@login_required
def SubmissionForm(req, abbreviation, season, episode): def SubmissionForm(req, abbreviation, season, episode):
show = Show.objects.get(abbr=abbreviation) show = Show.objects.get(abbr=abbreviation)
episode = Episode.objects.filter(show=show,season__number=season,episode=episode).first() episode = Episode.objects.filter(show=show,season__number=season,episode=episode).first()
user = req.user
# Check for login status
if not 'user_id' in req.session:
return HttpResponse('<h1>Error</h1><p>You need to be logged in to submit. Please <a href=/login>log in</a></p>', status=400)
user = User.objects.get(user_id=req.session['user_id'])
# 404's # 404's
if not show: if not show:
@ -144,16 +142,12 @@ def SubmissionForm(req, abbreviation, season, episode):
# Vote request # Vote request
# /show/{{abbr}}/vote/{{submission id}}/{{positive == 1}} # /show/{{abbr}}/vote/{{submission id}}/{{positive == 1}}
class SubmissionVoteSubmit(View): class SubmissionVoteSubmit(LoginRequiredMixin, View):
def post (self, req, abbreviation, subid, positive): def post (self, req, abbreviation, subid, positive):
# Convert positive parameter into a boolean # Convert positive parameter into a boolean
pos_bool = int(positive) == 1 pos_bool = int(positive) == 1
# Check for login status user = req.user
if not 'user_id' in req.session:
return HttpResponse('<h1>Error</h1><p>You need to be logged in to vote. Please <a href=/login>log in</a></p>', status=400)
user = User.objects.get(user_id=req.session['user_id'])
# Get the submission from the database # Get the submission from the database
submission = Submission.objects.filter(id=subid).first() submission = Submission.objects.filter(id=subid).first()

View File

@ -15,7 +15,7 @@ class TestLogin(TestCase):
# Set up responses to control network flow # Set up responses to control network flow
with responses.RequestsMock() as rm: with responses.RequestsMock() as rm:
rm.add(responses.POST,'http://icynet.test/api/token',json={'access_token':'1accesstoken1'}) 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 # Make initial request to redirect endpoint
client = Client() client = Client()
@ -26,7 +26,7 @@ class TestLogin(TestCase):
self.assertEqual(query['client_id'][0],'clid') self.assertEqual(query['client_id'][0],'clid')
self.assertEqual(query['response_type'][0],'code') self.assertEqual(query['response_type'][0],'code')
self.assertEqual(query['redirect_uri'][0],'http://redirect.test') 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 # Make connection to the real endpoint
resp = client.get('/login/redirect?state=%s&code=%s'%(state, 'code')) resp = client.get('/login/redirect?state=%s&code=%s'%(state, 'code'))
@ -36,14 +36,12 @@ class TestLogin(TestCase):
users = User.objects.all() users = User.objects.all()
self.assertEqual(len(users), 1) self.assertEqual(len(users), 1)
user = users[0] 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.email,'johnsmith@gmail.com')
self.assertEqual(user.display_name, 'Mr. Smith') self.assertEqual(user.display_name, 'Mr. Smith')
# Check appropriate values are in the session # Check that the user has been logged in
self.assertEqual(client.session['user_id'], '935a41b5-b38d-42c3-96ef-653402fc44ca') self.assertEqual(client.get('/').context['user'], user)
self.assertEqual(client.session['token'],'1accesstoken1')
self.assertEqual(client.session['disp_name'], 'Mr. Smith')
def test_reject_bad_state(self): def test_reject_bad_state(self):
with responses.RequestsMock() as rm: with responses.RequestsMock() as rm:
@ -55,10 +53,10 @@ class TestLogin(TestCase):
# Set up responses to control network flow # Set up responses to control network flow
with responses.RequestsMock() as rm: with responses.RequestsMock() as rm:
rm.add(responses.POST,'http://icynet.test/api/token',json={'access_token':'1accesstoken1'}) 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 # 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() user.save()
# Make initial request to redirect endpoint # Make initial request to redirect endpoint
@ -74,14 +72,12 @@ class TestLogin(TestCase):
users = User.objects.all() users = User.objects.all()
self.assertEqual(len(users), 1) self.assertEqual(len(users), 1)
user = users[0] 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.email,'johnsmith@gmail.com')
self.assertEqual(user.display_name, 'Mr. Smith') self.assertEqual(user.display_name, 'Mr. Smith')
# Check appropriate values are in the session # Check that the user has been logged in
self.assertEqual(client.session['user_id'], '935a41b5-b38d-42c3-96ef-653402fc44ca') self.assertEqual(client.get('/').context['user'], user)
self.assertEqual(client.session['token'],'1accesstoken1')
self.assertEqual(client.session['disp_name'], 'Mr. Smith')
def test_states_unique(self): def test_states_unique(self):
with responses.RequestsMock() as rm: with responses.RequestsMock() as rm: