Merge pull request #1 from PieterL75/develop

merge develop
This commit is contained in:
PieterL75 2022-03-18 10:53:25 +01:00 committed by GitHub
commit 7d7b3b5bcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 256 additions and 230 deletions

View File

@ -113,7 +113,3 @@ 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

View File

@ -5,6 +5,7 @@
### Enhancements
* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key usage by source IP
* [#8553](https://github.com/netbox-community/netbox/issues/8553) - Add missing object types to global search form
### Bug Fixes

View File

@ -10,11 +10,28 @@ class TokenAuthentication(authentication.TokenAuthentication):
A custom authentication scheme which enforces Token expiration times.
"""
model = Token
__request = False
def authenticate(self, request):
self.request = request
return super().authenticate(request)
authenticationresult = super().authenticate(request)
if authenticationresult:
token_user, token = authenticationresult
# Verify source IP is allowed
if token.allowed_ips:
# 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"A HTTP header containing the SourceIP (HTTP_X_REAL_IP, REMOTE_ADDR) is missing from the request.")
if not token.validate_client_ip(clientip):
raise exceptions.AuthenticationFailed(f"Source IP {clientip} is not allowed to use this token.")
return token_user, token
else:
return None
def authenticate_credentials(self, key):
model = self.get_model()
@ -23,20 +40,6 @@ class TokenAuthentication(authentication.TokenAuthentication):
except model.DoesNotExist:
raise exceptions.AuthenticationFailed("Invalid token")
# Verify source IP is allowed
request = self.request
if token.allowed_ips 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 HTTP headers (HTTP_X_REAL_IP, REMOTE_ADDR) are missing or do not contain a valid source IP.")
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.
if token.is_expired:
raise exceptions.AuthenticationFailed("Token expired")

View File

@ -1,4 +1,5 @@
from collections import OrderedDict
from typing import Dict
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.models import Circuit, ProviderNetwork, Provider
@ -26,8 +27,9 @@ from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineTable
SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict((
# Circuits
CIRCUIT_TYPES = OrderedDict(
(
('provider', {
'queryset': Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@ -50,7 +52,12 @@ SEARCH_TYPES = OrderedDict((
'table': ProviderNetworkTable,
'url': 'circuits:providernetwork_list',
}),
# DCIM
)
)
DCIM_TYPES = OrderedDict(
(
('site', {
'queryset': Site.objects.prefetch_related('region', 'tenant'),
'filterset': SiteFilterSet,
@ -123,25 +130,11 @@ SEARCH_TYPES = OrderedDict((
'table': PowerFeedTable,
'url': 'dcim:powerfeed_list',
}),
# Virtualization
('cluster', {
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
device_count=count_related(Device, 'cluster'),
vm_count=count_related(VirtualMachine, 'cluster')
),
'filterset': ClusterFilterSet,
'table': ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'queryset': VirtualMachine.objects.prefetch_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
),
'filterset': VirtualMachineFilterSet,
'table': VirtualMachineTable,
'url': 'virtualization:virtualmachine_list',
}),
# IPAM
)
)
IPAM_TYPES = OrderedDict(
(
('vrf', {
'queryset': VRF.objects.prefetch_related('tenant'),
'filterset': VRFFilterSet,
@ -178,7 +171,11 @@ SEARCH_TYPES = OrderedDict((
'table': ASNTable,
'url': 'ipam:asn_list',
}),
# Tenancy
)
)
TENANCY_TYPES = OrderedDict(
(
('tenant', {
'queryset': Tenant.objects.prefetch_related('group'),
'filterset': TenantFilterSet,
@ -186,9 +183,56 @@ SEARCH_TYPES = OrderedDict((
'url': 'tenancy:tenant_list',
}),
('contact', {
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')),
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
assignment_count=count_related(ContactAssignment, 'contact')),
'filterset': ContactFilterSet,
'table': ContactTable,
'url': 'tenancy:contact_list',
}),
))
)
)
VIRTUALIZATION_TYPES = OrderedDict(
(
('cluster', {
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate(
device_count=count_related(Device, 'cluster'),
vm_count=count_related(VirtualMachine, 'cluster')
),
'filterset': ClusterFilterSet,
'table': ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'queryset': VirtualMachine.objects.prefetch_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
),
'filterset': VirtualMachineFilterSet,
'table': VirtualMachineTable,
'url': 'virtualization:virtualmachine_list',
}),
)
)
SEARCH_TYPE_HIERARCHY = OrderedDict(
(
("Circuits", CIRCUIT_TYPES),
("DCIM", DCIM_TYPES),
("IPAM", IPAM_TYPES),
("Tenancy", TENANCY_TYPES),
("Virtualization", VIRTUALIZATION_TYPES),
)
)
def build_search_types() -> Dict[str, Dict]:
result = dict()
for app_types in SEARCH_TYPE_HIERARCHY.values():
for name, items in app_types.items():
result[name] = items
return result
SEARCH_TYPES = build_search_types()

View File

@ -1,39 +1,24 @@
from django import forms
from utilities.forms import BootstrapMixin
from netbox.constants import SEARCH_TYPE_HIERARCHY
OBJ_TYPE_CHOICES = (
('', 'All Objects'),
('Circuits', (
('provider', 'Providers'),
('circuit', 'Circuits'),
)),
('DCIM', (
('site', 'Sites'),
('rack', 'Racks'),
('rackreservation', 'Rack reservations'),
('location', 'Locations'),
('devicetype', 'Device Types'),
('device', 'Devices'),
('virtualchassis', 'Virtual chassis'),
('cable', 'Cables'),
('powerfeed', 'Power feeds'),
)),
('IPAM', (
('vrf', 'VRFs'),
('aggregate', 'Aggregates'),
('prefix', 'Prefixes'),
('ipaddress', 'IP Addresses'),
('vlan', 'VLANs'),
)),
('Tenancy', (
('tenant', 'Tenants'),
)),
('Virtualization', (
('cluster', 'Clusters'),
('virtualmachine', 'Virtual Machines'),
)),
)
def build_search_choices():
result = list()
result.append(('', 'All Objects'))
for category, items in SEARCH_TYPE_HIERARCHY.items():
subcategories = list()
for slug, obj in items.items():
name = obj['queryset'].model._meta.verbose_name_plural
name = name[0].upper() + name[1:]
subcategories.append((slug, name))
result.append((category, tuple(subcategories)))
return tuple(result)
OBJ_TYPE_CHOICES = build_search_choices()
def build_options():

View File

@ -321,7 +321,6 @@ INSTALLED_APPS = [
'wireless',
'django_rq', # Must come after extras to allow overriding management commands
'drf_yasg',
'django_better_admin_arrayfield',
]
# Middleware

View File

@ -1,7 +1,6 @@
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
@ -56,15 +55,16 @@ class UserAdmin(UserAdmin_):
#
@admin.register(Token)
class TokenAdmin(admin.ModelAdmin, DynamicArrayMixin):
class TokenAdmin(admin.ModelAdmin):
form = forms.TokenAdminForm
list_display = [
'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'list_allowed_ips'
]
def list_allowed_ips(self, obj):
if obj.allowed_ips:
return obj.allowed_ips
list_allowed_ips.empty_value_display = 'Any'
return 'Any'
list_allowed_ips.short_description = "Allowed IPs"

View File

@ -1,7 +1,7 @@
# Generated by Django 3.2.12 on 2022-03-15 13:08
# Generated by Django 3.2.12 on 2022-03-18 08:25
import django.contrib.postgres.fields
from django.db import migrations
import django_better_admin_arrayfield.models.fields
import ipam.fields
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
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),
field=django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None),
),
]

View File

@ -10,7 +10,6 @@ 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
@ -208,7 +207,7 @@ class Token(BigIDModel):
max_length=200,
blank=True
)
allowed_ips = betterArrayField(
allowed_ips = ArrayField(
base_field=IPNetworkField(),
blank=True,
null=True,

View File

@ -28,7 +28,6 @@ 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