Implement suggestions

This commit is contained in:
Pieter Lambrecht 2022-03-14 17:59:41 +01:00
parent 82f26b34e3
commit a9652df871
12 changed files with 56 additions and 92 deletions

View File

@ -113,3 +113,7 @@ svgwrite
# Tabular dataset library (for table-based exports) # Tabular dataset library (for table-based exports)
# https://github.com/jazzband/tablib # https://github.com/jazzband/tablib
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

View File

@ -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 ## RELEASE_CHECK_URL
Default: None (disabled) Default: None (disabled)

View File

@ -25,16 +25,16 @@ class TokenAuthentication(authentication.TokenAuthentication):
# Verify source IP is allowed # Verify source IP is allowed
request = self.request request = self.request
if len(token.allowed_ipranges) > 0 and request: if len(token.allowed_ips) > 0 and request:
### Replace 'HTTP_X_REAL_IP' with the settings variable choosen in #8867
if settings.PROXY_HEADER_REALIP in request.META: if 'HTTP_X_REAL_IP' in request.META:
clientip = request.META[settings.PROXY_HEADER_REALIP].split(",")[0].strip() clientip = request.META['HTTP_X_REAL_IP'].split(",")[0].strip()
elif 'REMOTE_ADDR' in request.META: elif 'REMOTE_ADDR' in request.META:
clientip = request.META['REMOTE_ADDR'] clientip = request.META['REMOTE_ADDR']
else: 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.") 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. # Enforce the Token's expiration time, if one has been set.

View File

@ -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_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', [])
REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|') REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|')
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') 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) RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
@ -322,6 +321,7 @@ INSTALLED_APPS = [
'wireless', 'wireless',
'django_rq', # Must come after extras to allow overriding management commands 'django_rq', # Must come after extras to allow overriding management commands
'drf_yasg', 'drf_yasg',
'django_better_admin_arrayfield',
] ]
# Middleware # Middleware

View File

@ -35,11 +35,11 @@
{% endif %} {% endif %}
</div> </div>
<div class="col col-md-4"> <div class="col col-md-4">
<small class="text-muted">Allowed IP Sources</small><br /> <small class="text-muted">Allowed Source IPs</small><br />
{% if token.allowed_ipranges %} {% if token.allowed_ips %}
{{ token.allowed_ipranges }} {{ token.allowed_ips }}
{% else %} {% else %}
<span>Everywhere</span> <span>Any</span>
{% endif %} {% endif %}
</div> </div>
<div class="col col-md-4"> <div class="col col-md-4">

View File

@ -5,6 +5,7 @@ from django.contrib.auth.models import Group, User
from users.models import ObjectPermission, Token from users.models import ObjectPermission, Token
from . import filters, forms, inlines from . import filters, forms, inlines
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
# #
# Users & groups # Users & groups
@ -55,10 +56,10 @@ class UserAdmin(UserAdmin_):
# #
@admin.register(Token) @admin.register(Token)
class TokenAdmin(admin.ModelAdmin): class TokenAdmin(admin.ModelAdmin, DynamicArrayMixin):
form = forms.TokenAdminForm form = forms.TokenAdminForm
list_display = [ list_display = [
'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'allowed_ipranges' 'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'allowed_ips'
] ]

View File

@ -51,7 +51,7 @@ class TokenAdminForm(forms.ModelForm):
class Meta: class Meta:
fields = [ fields = [
'user', 'key', 'write_enabled', 'expires', 'description', 'allowed_ipranges' 'user', 'key', 'write_enabled', 'expires', 'description', 'allowed_ips'
] ]
model = Token model = Token

View File

@ -62,7 +62,7 @@ class TokenSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Token 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): def to_internal_value(self, data):
if 'key' not in data: if 'key' not in data:

View File

@ -22,7 +22,7 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Token model = Token
fields = [ fields = [
'key', 'write_enabled', 'expires', 'description', 'allowed_ipranges', 'key', 'write_enabled', 'expires', 'description', 'allowed_ips',
] ]
widgets = { widgets = {
'expires': DateTimePicker(), 'expires': DateTimePicker(),

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -15,6 +15,9 @@ from netbox.models import BigIDModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import flatten_dict from utilities.utils import flatten_dict
from .constants import * from .constants import *
from ipam.fields import IPNetworkField
from django_better_admin_arrayfield.models.fields import ArrayField as betterArrayField
import ipaddress import ipaddress
@ -205,10 +208,11 @@ class Token(BigIDModel):
max_length=200, max_length=200,
blank=True blank=True
) )
allowed_ipranges = models.CharField( allowed_ips = betterArrayField(
max_length=250, base_field = IPNetworkField(),
blank=True, 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"', 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: class Meta:
@ -221,8 +225,6 @@ class Token(BigIDModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.key: if not self.key:
self.key = self.generate_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) return super().save(*args, **kwargs)
@ -237,36 +239,11 @@ class Token(BigIDModel):
return False return False
return True return True
@staticmethod def validate_client_ip(self, raw_ip_address):
def validateipranges(ip_addresses):
""" """
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: if not self.allowed_ips:
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:
return True return True
try: try:
@ -274,21 +251,10 @@ class Token(BigIDModel):
except ValueError: except ValueError:
raise ValidationError(f"{raw_ip_address} is an invalid IP address") raise ValidationError(f"{raw_ip_address} is an invalid IP address")
for ip in self.allowed_ipranges.split(','): for ipnet in self.allowed_ips:
if '/' in ip: if ip_address in ipaddress.ip_network(ipnet):
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 return True
return False return False