diff --git a/base_requirements.txt b/base_requirements.txt
index 0b8365e0e..6e93cbb69 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -113,3 +113,7 @@ svgwrite
# Tabular dataset library (for table-based exports)
# https://github.com/jazzband/tablib
tablib
+
+# It changes comma separated widget to list based in admin panel
+# https://github.com/gradam/django-better-admin-arrayfield
+django_better_admin_arrayfield
\ No newline at end of file
diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md
index 6d21bee09..d8d79b6ec 100644
--- a/docs/configuration/optional-settings.md
+++ b/docs/configuration/optional-settings.md
@@ -277,15 +277,6 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
---
-## PROXY_HEADER_REALIP
-
-Default: HTTP_X_REAL_IP
-
-This parameters sets the HTTP Header that contains the REAL IP of a client that connects through a PROXY. The Real IP is required to validate an API token's Allowed IPRanges.
-Other common values are HTTP_X_FORWARDED_FOR, HTTP_X_CLIENT_IP
-
----
-
## RELEASE_CHECK_URL
Default: None (disabled)
diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py
index a40514617..c73784029 100644
--- a/netbox/netbox/api/authentication.py
+++ b/netbox/netbox/api/authentication.py
@@ -25,16 +25,16 @@ class TokenAuthentication(authentication.TokenAuthentication):
# Verify source IP is allowed
request = self.request
- if len(token.allowed_ipranges) > 0 and request:
-
- if settings.PROXY_HEADER_REALIP in request.META:
- clientip = request.META[settings.PROXY_HEADER_REALIP].split(",")[0].strip()
+ if len(token.allowed_ips) > 0 and request:
+### Replace 'HTTP_X_REAL_IP' with the settings variable choosen in #8867
+ if 'HTTP_X_REAL_IP' in request.META:
+ clientip = request.META['HTTP_X_REAL_IP'].split(",")[0].strip()
elif 'REMOTE_ADDR' in request.META:
clientip = request.META['REMOTE_ADDR']
else:
- raise exceptions.AuthenticationFailed(f"The request headers ({settings.PROXY_HEADER_REALIP}, REMOTE_ADDR) are missing or do not contain a valid source IP.")
+ raise exceptions.AuthenticationFailed(f"The request headers (HTTP_X_REAL_IP, REMOTE_ADDR) are missing or do not contain a valid source IP.")
- if not token.validateclientip(clientip):
+ if not token.validate_client_ip(clientip):
raise exceptions.AuthenticationFailed(f"Source IP {clientip} is not allowed to use this token.")
# Enforce the Token's expiration time, if one has been set.
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index c2a5bbeac..1f25388da 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -115,7 +115,6 @@ REMOTE_AUTH_STAFF_GROUPS = getattr(configuration, 'REMOTE_AUTH_STAFF_GROUPS', []
REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
-PROXY_HEADER_REALIP = getattr(configuration, 'PROXY_HEADER_REALIP', 'HTTP_X_REAL_IP')
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
@@ -322,6 +321,7 @@ INSTALLED_APPS = [
'wireless',
'django_rq', # Must come after extras to allow overriding management commands
'drf_yasg',
+ 'django_better_admin_arrayfield',
]
# Middleware
diff --git a/netbox/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html
index 51cc3fb97..8e590baf4 100644
--- a/netbox/templates/users/api_tokens.html
+++ b/netbox/templates/users/api_tokens.html
@@ -35,11 +35,11 @@
{% endif %}
- Allowed IP Sources
- {% if token.allowed_ipranges %}
- {{ token.allowed_ipranges }}
+ Allowed Source IPs
+ {% if token.allowed_ips %}
+ {{ token.allowed_ips }}
{% else %}
- Everywhere
+ Any
{% endif %}
diff --git a/netbox/users/admin/__init__.py b/netbox/users/admin/__init__.py
index 9a115275f..7319b7d22 100644
--- a/netbox/users/admin/__init__.py
+++ b/netbox/users/admin/__init__.py
@@ -5,6 +5,7 @@ from django.contrib.auth.models import Group, User
from users.models import ObjectPermission, Token
from . import filters, forms, inlines
+from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
#
# Users & groups
@@ -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', 'allowed_ipranges'
+ 'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'allowed_ips'
]
diff --git a/netbox/users/admin/forms.py b/netbox/users/admin/forms.py
index a146dfc07..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', 'allowed_ipranges'
+ '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 8c30e4269..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', 'allowed_ipranges')
+ 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 ff2419fe5..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', 'allowed_ipranges',
+ 'key', 'write_enabled', 'expires', 'description', 'allowed_ips',
]
widgets = {
'expires': DateTimePicker(),
diff --git a/netbox/users/migrations/0002_token_allowed_ipranges.py b/netbox/users/migrations/0002_token_allowed_ipranges.py
deleted file mode 100644
index dadf8044c..000000000
--- a/netbox/users/migrations/0002_token_allowed_ipranges.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.2.12 on 2022-03-11 13:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0001_squashed_0011'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='token',
- name='allowed_ipranges',
- field=models.CharField(blank=True, max_length=250),
- ),
- ]
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..3e830b228
--- /dev/null
+++ b/netbox/users/migrations/0002_token_allowed_ips.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.12 on 2022-03-14 16:58
+
+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 363b77689..398b0d81d 100644
--- a/netbox/users/models.py
+++ b/netbox/users/models.py
@@ -15,6 +15,9 @@ from netbox.models import BigIDModel
from utilities.querysets import RestrictedQuerySet
from utilities.utils import flatten_dict
from .constants import *
+
+from ipam.fields import IPNetworkField
+from django_better_admin_arrayfield.models.fields import ArrayField as betterArrayField
import ipaddress
@@ -205,10 +208,11 @@ class Token(BigIDModel):
max_length=200,
blank=True
)
- allowed_ipranges = models.CharField(
- max_length=250,
- blank=True,
- help_text='Allowed ip addresses/ranges from where the token can be used (comma separated). Leave blank for no restrictions. Ex: "10.1.1.0/24, 192.168.10.16, 2001:DB8:1::/64"',
+ 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:
@@ -221,8 +225,6 @@ class Token(BigIDModel):
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
- if not self.validateipranges(self.allowed_ipranges):
- raise TypeError(f"{self.allowed_ipranges} contains an invalid ip address, range or prefix")
return super().save(*args, **kwargs)
@@ -237,36 +239,11 @@ class Token(BigIDModel):
return False
return True
- @staticmethod
- def validateipranges(ip_addresses):
+ def validate_client_ip(self, raw_ip_address):
"""
- Checks that the value is a comma separated list of IPv4 and/or IPv6 addresses, ranges or subnets.
+ Checks that an ip address falls within the allowed ips.
"""
- if len(ip_addresses) == 0:
- return True
-
- for ip in ip_addresses.split(','):
- try:
- if '/' in ip:
- iptest = ipaddress.ip_network(ip)
- elif '-' in ip:
- ips = ip.split('-')
- ip1 = ipaddress.ip_address(ips[0])
- ip2 = ipaddress.ip_address(ips[1])
- if ip1 > ip2:
- raise ValidationError()
- else:
- iptest = ipaddress.ip_address(ip)
- except ValueError:
- raise ValidationError(f"{ip} is an invalid value in the Allowed IP Ranges ({ip_addresses})")
-
- return True
-
- def validateclientip(self, raw_ip_address):
- """
- Checks that an ip address falls within the allowed ip ranges.
- """
- if len(self.allowed_ipranges) == 0:
+ if not self.allowed_ips:
return True
try:
@@ -274,21 +251,10 @@ class Token(BigIDModel):
except ValueError:
raise ValidationError(f"{raw_ip_address} is an invalid IP address")
- for ip in self.allowed_ipranges.split(','):
- if '/' in ip:
- ipnet = ipaddress.ip_network(ip)
- if ip_address in ipnet:
- return True
- elif '-' in ip:
- ips = ip.split('-')
- ip1 = ipaddress.ip_address(ips[0])
- ip2 = ipaddress.ip_address(ips[1])
- if ip_address >= ip1 and ip_address <= ip2:
- return True
- else:
- ipaddr = ipaddress.ip_address(ip)
- if ip_address == ipaddr:
- return True
+ for ipnet in self.allowed_ips:
+ if ip_address in ipaddress.ip_network(ipnet):
+ return True
+
return False