Commit 9835afe5 authored by David Haynes's avatar David Haynes 🙆

Add /auth/token API endpoint

- Returns the current user's API token if there is a authenticated
session already established
parent d907af3c
Pipeline #3031 failed with stage
in 35 seconds
...@@ -7,6 +7,7 @@ name = "pypi" ...@@ -7,6 +7,7 @@ name = "pypi"
pylint = "*" pylint = "*"
pylint-django = "*" pylint-django = "*"
coverage = "*" coverage = "*"
black = "*"
[packages] [packages]
django = "<2.1,>=2.0" django = "<2.1,>=2.0"
......
...@@ -15,8 +15,6 @@ from django.db.models.signals import post_save ...@@ -15,8 +15,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
# Other Imports # Other Imports
from hashids import Hashids from hashids import Hashids
...@@ -27,62 +25,39 @@ from rest_framework.authtoken.models import Token ...@@ -27,62 +25,39 @@ from rest_framework.authtoken.models import Token
# Note: the Hashids library already implements several restrictions oncharacter # Note: the Hashids library already implements several restrictions oncharacter
# placement, including repeating or incrementing numbers, or placing curse word # placement, including repeating or incrementing numbers, or placing curse word
# characters adjacent to one another. # characters adjacent to one another.
SIMILAR_CHARS = set(['b', 'G', '6', 'g', 'q', 'l', SIMILAR_CHARS = set(["b", "G", "6", "g", "q", "l", "1", "I", "S", "5", "O", "0"])
'1', 'I', 'S', '5', 'O', '0'])
ALPHANUMERICS = set(string.ascii_letters + string.digits) ALPHANUMERICS = set(string.ascii_letters + string.digits)
LINK_CHARS = ''.join(ALPHANUMERICS - SIMILAR_CHARS) LINK_CHARS = "".join(ALPHANUMERICS - SIMILAR_CHARS)
HASHIDS = Hashids(salt="srct.gmu.edu", alphabet=(LINK_CHARS))
HASHIDS = Hashids(
salt="srct.gmu.edu", alphabet=(LINK_CHARS)
)
class RegisteredUser(models.Model): class RegisteredUser(models.Model):
""" """
Wrapper model for the built in User model which stores data pertaining to Wrapper model for the built in User model which stores data pertaining to
the registration / approval / blocked status of a django user. the registration / approval / blocked status of a django user.
""" """
user = models.OneToOneField( user = models.OneToOneField(
User, User, on_delete="cascade", verbose_name="Django User Object"
on_delete="cascade",
verbose_name="Django User Object"
) )
full_name = models.CharField( full_name = models.CharField("Full Name", max_length=100, default="")
"Full Name",
max_length=100,
default="",
)
organization = models.CharField( organization = models.CharField("Organization", max_length=100, default="")
"Organization",
max_length=100,
default="",
)
description = models.TextField( description = models.TextField("Signup Description", blank=True, default="")
"Signup Description",
blank=True,
default="",
)
registered = models.BooleanField( registered = models.BooleanField("Registration Status", default=False)
"Registration Status",
default=False,
)
approved = models.BooleanField( approved = models.BooleanField("Approval Status", default=False)
"Approval Status",
default=False,
)
blocked = models.BooleanField( blocked = models.BooleanField("Blocked Status", default=False)
"Blocked Status",
default=False,
)
def __str__(self): def __str__(self):
return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>" return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>"
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def handle_reguser_creation(sender, instance, created, **kwargs): def handle_reguser_creation(sender, instance, created, **kwargs):
""" """
...@@ -92,38 +67,29 @@ def handle_reguser_creation(sender, instance, created, **kwargs): ...@@ -92,38 +67,29 @@ def handle_reguser_creation(sender, instance, created, **kwargs):
if created: if created:
RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name()) RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name())
@receiver(post_save, sender=settings.AUTH_USER_MODEL) @receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs): def create_auth_token(sender, instance=None, created=False, **kwargs):
if created: if created:
token = Token.objects.create(user=instance) Token.objects.create(user=instance)
print(token.key)
class URL(models.Model): class URL(models.Model):
""" """
The representation of a stored URL redirection rule. Each URL has The representation of a stored URL redirection rule. Each URL has
attributes that are used for analytic purposes. attributes that are used for analytic purposes.
""" """
owner = models.ForeignKey( owner = models.ForeignKey(
RegisteredUser, RegisteredUser, on_delete="cascade", verbose_name="RegisteredUser Owner"
on_delete="cascade",
verbose_name="RegisteredUser Owner"
) )
date_created = models.DateTimeField( date_created = models.DateTimeField("Go Link Creation Date", default=timezone.now)
"Go Link Creation Date",
default=timezone.now,
)
date_expires = models.DateTimeField( date_expires = models.DateTimeField("Go Link Expiry Date", blank=True, null=True)
"Go Link Expiry Date",
blank=True,
null=True,
)
destination = models.URLField( destination = models.URLField(
"Go Link Destination URL", "Go Link Destination URL", max_length=1000, default="https://go.gmu.edu"
max_length=1000,
default="https://go.gmu.edu",
) )
# Note: min_length cannot exist on a model so it is enforced in forms.py # Note: min_length cannot exist on a model so it is enforced in forms.py
...@@ -143,7 +109,7 @@ class URL(models.Model): ...@@ -143,7 +109,7 @@ class URL(models.Model):
return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>" return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>"
class Meta: class Meta:
ordering = ['short'] ordering = ["short"]
@staticmethod @staticmethod
def generate_valid_short(): def generate_valid_short():
......
...@@ -8,9 +8,12 @@ from .models import URL, RegisteredUser ...@@ -8,9 +8,12 @@ from .models import URL, RegisteredUser
# Third Party Imports # Third Party Imports
from rest_framework import serializers from rest_framework import serializers
from rest_framework.authtoken.models import Token
class URLSerializer(serializers.HyperlinkedModelSerializer): class URLSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = URL model = URL
lookup_field = 'short' lookup_field = "short"
fields = ('destination', 'short', 'date_expires') fields = ("destination", "short", "date_expires")
...@@ -17,7 +17,7 @@ from cas import views as cas_views ...@@ -17,7 +17,7 @@ from cas import views as cas_views
from rest_framework import routers from rest_framework import routers
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'golinks', views.URLViewSet, base_name="golinks") router.register(r"golinks", views.URLViewSet, base_name="golinks")
# This function attempts to import an admin module in each installed # This function attempts to import an admin module in each installed
# application. Such modules are expected to register models with the admin. # application. Such modules are expected to register models with the admin.
...@@ -26,25 +26,21 @@ admin.autodiscover() ...@@ -26,25 +26,21 @@ admin.autodiscover()
urlpatterns = [ urlpatterns = [
# Root API URL # Root API URL
path("api/", include(router.urls)), path("api/", include(router.urls)),
# Authentication URLs # Authentication URLs
path('auth/login/', cas_views.login, name='cas_login'), path("auth/login/", cas_views.login, name="cas_login"),
path('auth/logout/', cas_views.logout, name='cas_logout'), path("auth/logout/", cas_views.logout, name="cas_logout"),
# /admin - Administrator interface. # /admin - Administrator interface.
path('admin/', admin.site.urls, name='go_admin'), path("admin/", admin.site.urls, name="go_admin"),
path('auth/', include('rest_framework.urls')) path("auth/", include("rest_framework.urls")),
path("auth/token/", views.CustomAuthToken.as_view())
# # /view/<short> - View URL data. Cached for 15 minutes
# # /view/<short> - View URL data. Cached for 15 minutes # re_path(r'^view/(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# re_path(r'^view/(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$', # cache_page(60 * 15)(go.views.view), name='view'),
# cache_page(60 * 15)(go.views.view), name='view'), # re_path(r'^view/(?P<short>[-\w]+)$',
# re_path(r'^view/(?P<short>[-\w]+)$', # cache_page(60 * 15)(go.views.view), name='view'),
# cache_page(60 * 15)(go.views.view), name='view'), # # Redirection regex.
# re_path(r'^(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# # Redirection regex. # go.views.redirection, name='redirection'),
# re_path(r'^(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$', # re_path(r'^(?P<short>[-\w]+)$',
# go.views.redirection, name='redirection'), # go.views.redirection, name='redirection'),
# re_path(r'^(?P<short>[-\w]+)$',
# go.views.redirection, name='redirection'),
] ]
...@@ -4,55 +4,58 @@ go/views.py ...@@ -4,55 +4,58 @@ go/views.py
The functions that handle a request to a given URL. Get some data, manipulate The functions that handle a request to a given URL. Get some data, manipulate
it, and return a rendered template. it, and return a rendered template.
""" """
# Python stdlib imports
from datetime import timedelta
# Django Imports
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied # ValidationError
from django.core.mail import EmailMessage, send_mail
from django.http import HttpResponseServerError # Http404
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
# Other imports
from ratelimit.decorators import ratelimit
# App Imports
from .forms import EditForm, SignupForm, URLForm
from .models import URL, RegisteredUser
from django.contrib.auth.models import User, Group
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework import permissions from rest_framework import permissions
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication, SessionAuthentication
from .serializers import URLSerializer from .serializers import URLSerializer
from .models import URL
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.permissions import IsAuthenticated
class URLPermission(permissions.BasePermission): class URLPermission(permissions.BasePermission):
"""Custom permission check on URL model operations."""
message = "You do not have the necessary approvals to perform that action." message = "You do not have the necessary approvals to perform that action."
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.registereduser.approved or request.user.is_staff return request.user.registereduser.approved or request.user.is_staff
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
return obj.owner == request.user.registereduser or request.user.is_staff return obj.owner == request.user.registereduser or request.user.is_staff
class URLViewSet(viewsets.ModelViewSet): class URLViewSet(viewsets.ModelViewSet):
""" """
API endpoint that handles creation/read/update/deletion of URL objects. API endpoint that handles creation/read/update/deletion of URL objects.
""" """
authentication_classes = (TokenAuthentication, )
authentication_classes = (TokenAuthentication,)
serializer_class = URLSerializer serializer_class = URLSerializer
permission_classes = (URLPermission,) permission_classes = (URLPermission, IsAuthenticated)
lookup_field = 'short' lookup_field = "short"
def get_queryset(self): def get_queryset(self):
if not self.request.user.is_staff: if not self.request.user.is_staff:
return URL.objects.filter(owner=self.request.user.registereduser) return URL.objects.filter(owner=self.request.user.registereduser)
else: return URL.objects.all()
return URL.objects.all()
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(owner=self.request.user.registereduser) serializer.save(owner=self.request.user.registereduser)
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken):
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
token, created = Token.objects.get_or_create(user=request.user)
return Response({"token": token.key})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment