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:
@ -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
8
netbox/users/admin.py
Normal 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']
|
31
netbox/users/migrations/0001_api_tokens.py
Normal file
31
netbox/users/migrations/0001_api_tokens.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
netbox/users/migrations/__init__.py
Normal file
0
netbox/users/migrations/__init__.py
Normal 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()
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user