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) # 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

@ -4,7 +4,8 @@
### Enhancements ### Enhancements
* [#8233](https://github.com/netbox-community/netbox/issues/8233) - Restrict API key usage by source IP * [#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 ### Bug Fixes

View File

@ -10,11 +10,28 @@ class TokenAuthentication(authentication.TokenAuthentication):
A custom authentication scheme which enforces Token expiration times. A custom authentication scheme which enforces Token expiration times.
""" """
model = Token model = Token
__request = False
def authenticate(self, request): def authenticate(self, request):
self.request = request authenticationresult = super().authenticate(request)
return 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): def authenticate_credentials(self, key):
model = self.get_model() model = self.get_model()
@ -23,20 +40,6 @@ class TokenAuthentication(authentication.TokenAuthentication):
except model.DoesNotExist: except model.DoesNotExist:
raise exceptions.AuthenticationFailed("Invalid token") 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. # Enforce the Token's expiration time, if one has been set.
if token.is_expired: if token.is_expired:
raise exceptions.AuthenticationFailed("Token expired") raise exceptions.AuthenticationFailed("Token expired")

View File

@ -1,4 +1,5 @@
from collections import OrderedDict from collections import OrderedDict
from typing import Dict
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.models import Circuit, ProviderNetwork, Provider from circuits.models import Circuit, ProviderNetwork, Provider
@ -26,169 +27,212 @@ from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineTable from virtualization.tables import ClusterTable, VirtualMachineTable
SEARCH_MAX_RESULTS = 15 SEARCH_MAX_RESULTS = 15
SEARCH_TYPES = OrderedDict((
# Circuits CIRCUIT_TYPES = OrderedDict(
('provider', { (
'queryset': Provider.objects.annotate( ('provider', {
count_circuits=count_related(Circuit, 'provider') 'queryset': Provider.objects.annotate(
), count_circuits=count_related(Circuit, 'provider')
'filterset': ProviderFilterSet,
'table': ProviderTable,
'url': 'circuits:provider_list',
}),
('circuit', {
'queryset': Circuit.objects.prefetch_related(
'type', 'provider', 'tenant', 'terminations__site'
),
'filterset': CircuitFilterSet,
'table': CircuitTable,
'url': 'circuits:circuit_list',
}),
('providernetwork', {
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
'filterset': ProviderNetworkFilterSet,
'table': ProviderNetworkTable,
'url': 'circuits:providernetwork_list',
}),
# DCIM
('site', {
'queryset': Site.objects.prefetch_related('region', 'tenant'),
'filterset': SiteFilterSet,
'table': SiteTable,
'url': 'dcim:site_list',
}),
('rack', {
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'),
'filterset': RackFilterSet,
'table': RackTable,
'url': 'dcim:rack_list',
}),
('rackreservation', {
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
'filterset': RackReservationFilterSet,
'table': RackReservationTable,
'url': 'dcim:rackreservation_list',
}),
('location', {
'queryset': Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
), ),
Rack, 'filterset': ProviderFilterSet,
'location', 'table': ProviderTable,
'rack_count', 'url': 'circuits:provider_list',
cumulative=True }),
).prefetch_related('site'), ('circuit', {
'filterset': LocationFilterSet, 'queryset': Circuit.objects.prefetch_related(
'table': LocationTable, 'type', 'provider', 'tenant', 'terminations__site'
'url': 'dcim:location_list', ),
}), 'filterset': CircuitFilterSet,
('devicetype', { 'table': CircuitTable,
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate( 'url': 'circuits:circuit_list',
instance_count=count_related(Device, 'device_type') }),
), ('providernetwork', {
'filterset': DeviceTypeFilterSet, 'queryset': ProviderNetwork.objects.prefetch_related('provider'),
'table': DeviceTypeTable, 'filterset': ProviderNetworkFilterSet,
'url': 'dcim:devicetype_list', 'table': ProviderNetworkTable,
}), 'url': 'circuits:providernetwork_list',
('device', { }),
'queryset': Device.objects.prefetch_related( )
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', )
),
'filterset': DeviceFilterSet,
'table': DeviceTable, DCIM_TYPES = OrderedDict(
'url': 'dcim:device_list', (
}), ('site', {
('virtualchassis', { 'queryset': Site.objects.prefetch_related('region', 'tenant'),
'queryset': VirtualChassis.objects.prefetch_related('master').annotate( 'filterset': SiteFilterSet,
member_count=count_related(Device, 'virtual_chassis') 'table': SiteTable,
), 'url': 'dcim:site_list',
'filterset': VirtualChassisFilterSet, }),
'table': VirtualChassisTable, ('rack', {
'url': 'dcim:virtualchassis_list', 'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'),
}), 'filterset': RackFilterSet,
('cable', { 'table': RackTable,
'queryset': Cable.objects.all(), 'url': 'dcim:rack_list',
'filterset': CableFilterSet, }),
'table': CableTable, ('rackreservation', {
'url': 'dcim:cable_list', 'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
}), 'filterset': RackReservationFilterSet,
('powerfeed', { 'table': RackReservationTable,
'queryset': PowerFeed.objects.all(), 'url': 'dcim:rackreservation_list',
'filterset': PowerFeedFilterSet, }),
'table': PowerFeedTable, ('location', {
'url': 'dcim:powerfeed_list', 'queryset': Location.objects.add_related_count(
}), Location.objects.add_related_count(
# Virtualization Location.objects.all(),
('cluster', { Device,
'queryset': Cluster.objects.prefetch_related('type', 'group').annotate( 'location',
device_count=count_related(Device, 'cluster'), 'device_count',
vm_count=count_related(VirtualMachine, 'cluster') cumulative=True
), ),
'filterset': ClusterFilterSet, Rack,
'table': ClusterTable, 'location',
'url': 'virtualization:cluster_list', 'rack_count',
}), cumulative=True
('virtualmachine', { ).prefetch_related('site'),
'queryset': VirtualMachine.objects.prefetch_related( 'filterset': LocationFilterSet,
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'table': LocationTable,
), 'url': 'dcim:location_list',
'filterset': VirtualMachineFilterSet, }),
'table': VirtualMachineTable, ('devicetype', {
'url': 'virtualization:virtualmachine_list', 'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
}), instance_count=count_related(Device, 'device_type')
# IPAM ),
('vrf', { 'filterset': DeviceTypeFilterSet,
'queryset': VRF.objects.prefetch_related('tenant'), 'table': DeviceTypeTable,
'filterset': VRFFilterSet, 'url': 'dcim:devicetype_list',
'table': VRFTable, }),
'url': 'ipam:vrf_list', ('device', {
}), 'queryset': Device.objects.prefetch_related(
('aggregate', { 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
'queryset': Aggregate.objects.prefetch_related('rir'), ),
'filterset': AggregateFilterSet, 'filterset': DeviceFilterSet,
'table': AggregateTable, 'table': DeviceTable,
'url': 'ipam:aggregate_list', 'url': 'dcim:device_list',
}), }),
('prefix', { ('virtualchassis', {
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), 'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
'filterset': PrefixFilterSet, member_count=count_related(Device, 'virtual_chassis')
'table': PrefixTable, ),
'url': 'ipam:prefix_list', 'filterset': VirtualChassisFilterSet,
}), 'table': VirtualChassisTable,
('ipaddress', { 'url': 'dcim:virtualchassis_list',
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), }),
'filterset': IPAddressFilterSet, ('cable', {
'table': IPAddressTable, 'queryset': Cable.objects.all(),
'url': 'ipam:ipaddress_list', 'filterset': CableFilterSet,
}), 'table': CableTable,
('vlan', { 'url': 'dcim:cable_list',
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'), }),
'filterset': VLANFilterSet, ('powerfeed', {
'table': VLANTable, 'queryset': PowerFeed.objects.all(),
'url': 'ipam:vlan_list', 'filterset': PowerFeedFilterSet,
}), 'table': PowerFeedTable,
('asn', { 'url': 'dcim:powerfeed_list',
'queryset': ASN.objects.prefetch_related('rir', 'tenant'), }),
'filterset': ASNFilterSet, )
'table': ASNTable, )
'url': 'ipam:asn_list',
}), IPAM_TYPES = OrderedDict(
# Tenancy (
('tenant', { ('vrf', {
'queryset': Tenant.objects.prefetch_related('group'), 'queryset': VRF.objects.prefetch_related('tenant'),
'filterset': TenantFilterSet, 'filterset': VRFFilterSet,
'table': TenantTable, 'table': VRFTable,
'url': 'tenancy:tenant_list', 'url': 'ipam:vrf_list',
}), }),
('contact', { ('aggregate', {
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')), 'queryset': Aggregate.objects.prefetch_related('rir'),
'filterset': ContactFilterSet, 'filterset': AggregateFilterSet,
'table': ContactTable, 'table': AggregateTable,
'url': 'tenancy:contact_list', 'url': 'ipam:aggregate_list',
}), }),
)) ('prefix', {
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filterset': PrefixFilterSet,
'table': PrefixTable,
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
'filterset': IPAddressFilterSet,
'table': IPAddressTable,
'url': 'ipam:ipaddress_list',
}),
('vlan', {
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
'filterset': VLANFilterSet,
'table': VLANTable,
'url': 'ipam:vlan_list',
}),
('asn', {
'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
'filterset': ASNFilterSet,
'table': ASNTable,
'url': 'ipam:asn_list',
}),
)
)
TENANCY_TYPES = OrderedDict(
(
('tenant', {
'queryset': Tenant.objects.prefetch_related('group'),
'filterset': TenantFilterSet,
'table': TenantTable,
'url': 'tenancy:tenant_list',
}),
('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 django import forms
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from netbox.constants import SEARCH_TYPE_HIERARCHY
OBJ_TYPE_CHOICES = (
('', 'All Objects'), def build_search_choices():
('Circuits', ( result = list()
('provider', 'Providers'), result.append(('', 'All Objects'))
('circuit', 'Circuits'), for category, items in SEARCH_TYPE_HIERARCHY.items():
)), subcategories = list()
('DCIM', ( for slug, obj in items.items():
('site', 'Sites'), name = obj['queryset'].model._meta.verbose_name_plural
('rack', 'Racks'), name = name[0].upper() + name[1:]
('rackreservation', 'Rack reservations'), subcategories.append((slug, name))
('location', 'Locations'), result.append((category, tuple(subcategories)))
('devicetype', 'Device Types'),
('device', 'Devices'), return tuple(result)
('virtualchassis', 'Virtual chassis'),
('cable', 'Cables'),
('powerfeed', 'Power feeds'), OBJ_TYPE_CHOICES = build_search_choices()
)),
('IPAM', (
('vrf', 'VRFs'),
('aggregate', 'Aggregates'),
('prefix', 'Prefixes'),
('ipaddress', 'IP Addresses'),
('vlan', 'VLANs'),
)),
('Tenancy', (
('tenant', 'Tenants'),
)),
('Virtualization', (
('cluster', 'Clusters'),
('virtualmachine', 'Virtual Machines'),
)),
)
def build_options(): def build_options():

View File

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

@ -1,7 +1,6 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as UserAdmin_ from django.contrib.auth.admin import UserAdmin as UserAdmin_
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from users.models import ObjectPermission, Token from users.models import ObjectPermission, Token
from . import filters, forms, inlines from . import filters, forms, inlines
@ -56,15 +55,16 @@ class UserAdmin(UserAdmin_):
# #
@admin.register(Token) @admin.register(Token)
class TokenAdmin(admin.ModelAdmin, DynamicArrayMixin): class TokenAdmin(admin.ModelAdmin):
form = forms.TokenAdminForm form = forms.TokenAdminForm
list_display = [ list_display = [
'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'list_allowed_ips' 'key', 'user', 'created', 'expires', 'write_enabled', 'description', 'list_allowed_ips'
] ]
def list_allowed_ips(self, obj): def list_allowed_ips(self, obj):
return obj.allowed_ips if obj.allowed_ips:
list_allowed_ips.empty_value_display = 'Any' return obj.allowed_ips
return 'Any'
list_allowed_ips.short_description = "Allowed IPs" 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 from django.db import migrations
import django_better_admin_arrayfield.models.fields
import ipam.fields import ipam.fields
@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='token', model_name='token',
name='allowed_ips', 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.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django_better_admin_arrayfield.models.fields import ArrayField as betterArrayField
from netbox.models import BigIDModel from netbox.models import BigIDModel
from ipam.fields import IPNetworkField from ipam.fields import IPNetworkField
@ -208,7 +207,7 @@ class Token(BigIDModel):
max_length=200, max_length=200,
blank=True blank=True
) )
allowed_ips = betterArrayField( allowed_ips = ArrayField(
base_field=IPNetworkField(), base_field=IPNetworkField(),
blank=True, blank=True,
null=True, null=True,

View File

@ -28,7 +28,6 @@ social-auth-core==4.2.0
svgwrite==1.4.1 svgwrite==1.4.1
tablib==3.2.0 tablib==3.2.0
tzdata==2021.5 tzdata==2021.5
django_better_admin_arrayfield==1.4.2
# Workaround for #7401 # Workaround for #7401
jsonschema==3.2.0 jsonschema==3.2.0