1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on token authentication

This commit is contained in:
Jeremy Stretch
2017-03-07 17:17:39 -05:00
parent be0a3fb1f2
commit 0b10d98e0b
6 changed files with 129 additions and 5 deletions

View File

@ -185,14 +185,21 @@ SECRETS_MIN_PUBKEY_SIZE = 2048
# Django REST framework (API) # Django REST framework (API)
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',), 'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'utilities.api.TokenAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.DjangoFilterBackend',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': (
'utilities.api.TokenPermissions',
),
'DEFAULT_VERSION': VERSION.rsplit('.', 1)[0], # Use major.minor as API version 'DEFAULT_VERSION': VERSION.rsplit('.', 1)[0], # Use major.minor as API version
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'PAGE_SIZE': PAGINATE_COUNT, 'PAGE_SIZE': PAGINATE_COUNT,
} }
if LOGIN_REQUIRED:
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',)
# Django debug toolbar # Django debug toolbar
INTERNAL_IPS = ( INTERNAL_IPS = (

8
netbox/users/admin.py Normal file
View File

@ -0,0 +1,8 @@
from django.contrib import admin
from .models import Token
@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
list_display = ['user', 'key', 'created', 'expires', 'write_enabled', 'description']

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-07 20:57
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('expires', models.DateTimeField(blank=True, null=True)),
('key', models.CharField(max_length=64, unique=True)),
('write_enabled', models.BooleanField(default=True, help_text=b'Permit POST/PUT/DELETE operations using this key')),
('description', models.CharField(blank=True, max_length=100)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

View File

@ -0,0 +1,32 @@
import binascii
import os
from django.contrib.auth.models import User
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class Token(models.Model):
"""
An API token used for user authentication. This extends the stock model to allow each user to have multiple tokens.
It also supports setting an expiration time and toggling write ability.
"""
user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
expires = models.DateTimeField(blank=True, null=True)
key = models.CharField(max_length=64, unique=True)
write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key")
description = models.CharField(max_length=100, blank=True)
def __str__(self):
return u"API key for {}".format(self.user)
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super(Token, self).save(*args, **kwargs)
def generate_key(self):
# Generate a random 256-bit key expressed in hexadecimal.
return binascii.hexlify(os.urandom(32)).decode()

View File

@ -1,6 +1,13 @@
from django.conf import settings
from django.utils import timezone
from rest_framework import authentication, exceptions
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
from rest_framework.serializers import Field from rest_framework.serializers import Field
from users.models import Token
WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete'] WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
@ -10,11 +17,51 @@ class ServiceUnavailable(APIException):
default_detail = "Service temporarily unavailable, please try again later." default_detail = "Service temporarily unavailable, please try again later."
class TokenAuthentication(authentication.TokenAuthentication):
"""
A custom authentication scheme which enforces Token expiration times.
"""
model = Token
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed("Invalid token")
# Enforce the Token's expiration time, if one has been set.
if token.expires and token.expires < timezone.now():
raise exceptions.AuthenticationFailed("Token expired")
if not token.user.is_active:
raise exceptions.AuthenticationFailed("User inactive")
return token.user, token
class TokenPermissions(DjangoModelPermissions):
"""
Custom permissions handler which extends the built-in DjangoModelPermissions to validate a Token's write ability
for unsafe requests (POST/PUT/PATCH/DELETE).
"""
def __init__(self):
# LOGIN_REQUIRED determines whether read-only access is provided to anonymous users.
self.authenticated_users_only = settings.LOGIN_REQUIRED
super(TokenPermissions, self).__init__()
def has_permission(self, request, view):
# If token authentication is in use, verify that the token allows write operations (for unsafe methods).
if request.method not in SAFE_METHODS and isinstance(request.auth, Token):
if not request.auth.write_enabled:
return False
return super(TokenPermissions, self).has_permission(request, view)
class ChoiceFieldSerializer(Field): class ChoiceFieldSerializer(Field):
""" """
Represent a ChoiceField as (value, label). Represent a ChoiceField as (value, label).
""" """
def __init__(self, choices, **kwargs): def __init__(self, choices, **kwargs):
self._choices = {k: v for k, v in choices} self._choices = {k: v for k, v in choices}
super(ChoiceFieldSerializer, self).__init__(**kwargs) super(ChoiceFieldSerializer, self).__init__(**kwargs)
@ -30,7 +77,6 @@ class WritableSerializerMixin(object):
""" """
Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT). Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).
""" """
def get_serializer_class(self): def get_serializer_class(self):
if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'): if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'):
return self.write_serializer_class return self.write_serializer_class