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