+
Expires
{% if token.expires %}
{{ token.expires|annotated_date }}
@@ -34,7 +34,7 @@
Never
{% endif %}
-
+
Create/Edit/Delete Operations
{% if token.write_enabled %}
Enabled
@@ -42,6 +42,14 @@
Disabled
{% endif %}
+
+ Allowed Source IPs
+ {% if token.allowed_ips %}
+ {{ token.allowed_ips }}
+ {% else %}
+ Any
+ {% endif %}
+
{% if token.description %}
{{ token.description }}
diff --git a/netbox/users/admin/__init__.py b/netbox/users/admin/__init__.py
index 1b163ed06..3fb5d93bb 100644
--- a/netbox/users/admin/__init__.py
+++ b/netbox/users/admin/__init__.py
@@ -1,6 +1,7 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as UserAdmin_
from django.contrib.auth.models import Group, User
+from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from users.models import ObjectPermission, Token
from . import filters, forms, inlines
@@ -55,10 +56,10 @@ class UserAdmin(UserAdmin_):
#
@admin.register(Token)
-class TokenAdmin(admin.ModelAdmin):
+class TokenAdmin(admin.ModelAdmin, DynamicArrayMixin):
form = forms.TokenAdminForm
list_display = [
- 'key', 'user', 'created', 'expires', 'write_enabled', 'description'
+ 'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'allowed_ips'
]
diff --git a/netbox/users/admin/forms.py b/netbox/users/admin/forms.py
index 7d0212441..bc3d44862 100644
--- a/netbox/users/admin/forms.py
+++ b/netbox/users/admin/forms.py
@@ -51,7 +51,7 @@ class TokenAdminForm(forms.ModelForm):
class Meta:
fields = [
- 'user', 'key', 'write_enabled', 'expires', 'description'
+ 'user', 'key', 'write_enabled', 'expires', 'description', 'allowed_ips'
]
model = Token
diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py
index d490e8fe9..4b1f5bff3 100644
--- a/netbox/users/api/serializers.py
+++ b/netbox/users/api/serializers.py
@@ -62,7 +62,7 @@ class TokenSerializer(ValidatedModelSerializer):
class Meta:
model = Token
- fields = ('id', 'url', 'display', 'user', 'created', 'expires', 'key', 'write_enabled', 'description')
+ fields = ('id', 'url', 'display', 'user', 'created', 'expires', 'key', 'write_enabled', 'description', 'allowed_ips')
def to_internal_value(self, data):
if 'key' not in data:
diff --git a/netbox/users/forms.py b/netbox/users/forms.py
index 8bd54cb66..4d2de2661 100644
--- a/netbox/users/forms.py
+++ b/netbox/users/forms.py
@@ -22,7 +22,7 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Token
fields = [
- 'key', 'write_enabled', 'expires', 'description',
+ 'key', 'write_enabled', 'expires', 'description', 'allowed_ips',
]
widgets = {
'expires': DateTimePicker(),
diff --git a/netbox/users/migrations/0002_token_allowed_ips.py b/netbox/users/migrations/0002_token_allowed_ips.py
new file mode 100644
index 000000000..aa3fe2be3
--- /dev/null
+++ b/netbox/users/migrations/0002_token_allowed_ips.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.12 on 2022-03-15 13:08
+
+from django.db import migrations
+import django_better_admin_arrayfield.models.fields
+import ipam.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0001_squashed_0011'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='token',
+ name='allowed_ips',
+ field=django_better_admin_arrayfield.models.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None),
+ ),
+ ]
diff --git a/netbox/users/models.py b/netbox/users/models.py
index 64b6432a7..62a081335 100644
--- a/netbox/users/models.py
+++ b/netbox/users/models.py
@@ -4,17 +4,22 @@ 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 django_better_admin_arrayfield.models.fields import ArrayField as betterArrayField
from netbox.models import BigIDModel
+from ipam.fields import IPNetworkField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import flatten_dict
from .constants import *
+import ipaddress
+
__all__ = (
'ObjectPermission',
@@ -203,6 +208,12 @@ class Token(BigIDModel):
max_length=200,
blank=True
)
+ allowed_ips = betterArrayField(
+ base_field=IPNetworkField(),
+ blank=True,
+ null=True,
+ help_text='Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"',
+ )
class Meta:
pass
@@ -227,6 +238,24 @@ class Token(BigIDModel):
return False
return True
+ def validate_client_ip(self, raw_ip_address):
+ """
+ Checks that an ip address falls within the allowed ips.
+ """
+ if not self.allowed_ips:
+ return True
+
+ try:
+ ip_address = ipaddress.ip_address(raw_ip_address)
+ except ValueError:
+ raise ValidationError(f"{raw_ip_address} is an invalid IP address")
+
+ for ipnet in self.allowed_ips:
+ if ip_address in ipaddress.ip_network(ipnet):
+ return True
+
+ return False
+
#
# Permissions
diff --git a/requirements.txt b/requirements.txt
index 04d053180..8532db73f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -28,6 +28,7 @@ social-auth-core==4.2.0
svgwrite==1.4.1
tablib==3.2.0
tzdata==2021.5
+django_better_admin_arrayfield==1.4.2
# Workaround for #7401
jsonschema==3.2.0