From 3c15419bd0e10a153713707655798d1ac22f194f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 22 Jun 2022 21:51:43 -0400 Subject: [PATCH] Introduce IPNetworkSerializer to serialize allowed token IPs --- docs/release-notes/version-3.3.md | 3 ++- netbox/netbox/api/__init__.py | 3 ++- netbox/netbox/api/fields.py | 21 +++++++++++++++++++-- netbox/users/api/serializers.py | 3 ++- netbox/users/models.py | 6 ++---- netbox/utilities/request.py | 4 ++-- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index f9a229aef..81125451e 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -13,6 +13,8 @@ #### PoE Interface Attributes ([#1099](https://github.com/netbox-community/netbox/issues/1099)) +#### Restrict API Tokens by Client IP ([#8233](https://github.com/netbox-community/netbox/issues/8233)) + ### Enhancements * [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses @@ -21,7 +23,6 @@ * [#7120](https://github.com/netbox-community/netbox/issues/7120) - Add `termination_date` field to Circuit * [#7744](https://github.com/netbox-community/netbox/issues/7744) - Add `status` field to Location * [#8222](https://github.com/netbox-community/netbox/issues/8222) - Enable the assignment of a VM to a specific host device within a cluster -* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API token access by source IP * [#8471](https://github.com/netbox-community/netbox/issues/8471) - Add `status` field to Cluster * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results diff --git a/netbox/netbox/api/__init__.py b/netbox/netbox/api/__init__.py index 1eaa7d1c4..231ab55e6 100644 --- a/netbox/netbox/api/__init__.py +++ b/netbox/netbox/api/__init__.py @@ -1,4 +1,4 @@ -from .fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from .fields import * from .routers import NetBoxRouter from .serializers import BulkOperationSerializer, ValidatedModelSerializer, WritableNestedSerializer @@ -7,6 +7,7 @@ __all__ = ( 'BulkOperationSerializer', 'ChoiceField', 'ContentTypeField', + 'IPNetworkSerializer', 'NetBoxRouter', 'SerializedPKRelatedField', 'ValidatedModelSerializer', diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index d73cbcac2..1f3c40dc2 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -1,12 +1,18 @@ from collections import OrderedDict -import pytz -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist +from netaddr import IPNetwork from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.relations import PrimaryKeyRelatedField, RelatedField +__all__ = ( + 'ChoiceField', + 'ContentTypeField', + 'IPNetworkSerializer', + 'SerializedPKRelatedField', +) + class ChoiceField(serializers.Field): """ @@ -104,6 +110,17 @@ class ContentTypeField(RelatedField): return f"{obj.app_label}.{obj.model}" +class IPNetworkSerializer(serializers.Serializer): + """ + Representation of an IP network value (e.g. 192.0.2.0/24). + """ + def to_representation(self, instance): + return str(instance) + + def to_internal_value(self, value): + return IPNetwork(value) + + class SerializedPKRelatedField(PrimaryKeyRelatedField): """ Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 2a40e45ac..e5ed1bb34 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType from rest_framework import serializers -from netbox.api import ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer +from netbox.api import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField, ValidatedModelSerializer from users.models import ObjectPermission, Token from .nested_serializers import * @@ -64,6 +64,7 @@ class TokenSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail') key = serializers.CharField(min_length=40, max_length=40, allow_blank=True, required=False) user = NestedUserSerializer() + allowed_ips = serializers.ListField(child=IPNetworkSerializer()) class Meta: model = Token diff --git a/netbox/users/models.py b/netbox/users/models.py index 222b088d6..704516c71 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -4,12 +4,12 @@ import os from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField -from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone +from netaddr import IPNetwork from ipam.fields import IPNetworkField from netbox.config import get_config @@ -17,8 +17,6 @@ from utilities.querysets import RestrictedQuerySet from utilities.utils import flatten_dict from .constants import * -import ipaddress - __all__ = ( 'ObjectPermission', 'Token', @@ -259,7 +257,7 @@ class Token(models.Model): return True for ip_network in self.allowed_ips: - if client_ip in ipaddress.ip_network(ip_network): + if client_ip in IPNetwork(ip_network): return True return False diff --git a/netbox/utilities/request.py b/netbox/utilities/request.py index 0fac59d38..3b8e1edde 100644 --- a/netbox/utilities/request.py +++ b/netbox/utilities/request.py @@ -1,4 +1,4 @@ -import ipaddress +from netaddr import IPAddress __all__ = ( 'get_client_ip', @@ -19,7 +19,7 @@ def get_client_ip(request, additional_headers=()): if header in request.META: client_ip = request.META[header].split(',')[0] try: - return ipaddress.ip_address(client_ip) + return IPAddress(client_ip) except ValueError: raise ValueError(f"Invalid IP address set for {header}: {client_ip}")