Fix 8878: Restrict API key usage by Source IP

This commit is contained in:
Pieter Lambrecht
2022-04-19 14:44:35 +02:00
parent 098ef91583
commit 2587720298
9 changed files with 101 additions and 8 deletions
+5 -1
View File
@@ -58,9 +58,13 @@ class UserAdmin(UserAdmin_):
class TokenAdmin(admin.ModelAdmin):
form = forms.TokenAdminForm
list_display = [
'key', 'user', 'created', 'expires', 'write_enabled', 'description'
'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'list_allowed_ips'
]
def list_allowed_ips(self, obj):
return obj.allowed_ips or 'Any'
list_allowed_ips.short_description = "Allowed IPs"
#
# Permissions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+9 -1
View File
@@ -1,7 +1,9 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.html import mark_safe
from ipam.formfields import IPNetworkFormField
from netbox.preferences import PREFERENCES
from utilities.forms import BootstrapMixin, DateTimePicker, StaticSelect
from utilities.utils import flatten_dict
@@ -100,10 +102,16 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
help_text="If no key is provided, one will be generated automatically."
)
allowed_ips = SimpleArrayField(
base_field=IPNetworkFormField(),
required=False,
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:
model = Token
fields = [
'key', 'write_enabled', 'expires', 'description',
'key', 'write_enabled', 'expires', 'description', 'allowed_ips',
]
widgets = {
'expires': DateTimePicker(),
@@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2022-04-19 12:37
import django.contrib.postgres.fields
from django.db import migrations
import ipam.fields
class Migration(migrations.Migration):
dependencies = [
('users', '0002_standardize_id_fields'),
]
operations = [
migrations.AddField(
model_name='token',
name='allowed_ips',
field=django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None),
),
]
+27
View File
@@ -4,17 +4,20 @@ 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 ipam.fields import IPNetworkField
from netbox.config import get_config
from utilities.querysets import RestrictedQuerySet
from utilities.utils import flatten_dict
from .constants import *
import ipaddress
__all__ = (
'ObjectPermission',
@@ -216,6 +219,12 @@ class Token(models.Model):
max_length=200,
blank=True
)
allowed_ips = ArrayField(
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
@@ -240,6 +249,24 @@ class Token(models.Model):
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 as e:
raise ValidationError(str(e))
for ip_network in self.allowed_ips:
if ip_address in ipaddress.ip_network(ip_network):
return True
return False
#
# Permissions