From 33d01786e6644e9506f63077be58c5f78ac9698a Mon Sep 17 00:00:00 2001 From: Terje Kvernes Date: Fri, 30 Jun 2023 03:41:56 +0200 Subject: [PATCH] Migrating away from postgres CI fields. *Caveats:* - Assumes fields in LCI fields are all already in lowercase. No efforts are currently made to manipulate these fields. This is fixable if required. - Email fields are migrated to models.EmailField. Case insensitivity is not preserved. - Needs (much) more testing. Implementation details: - Creates two LowerCase-fields to replace the LCI-fields. - Uses a manager to hook into the calls to objects.get, objects.filter, and objects.exclude (the latter is currently unused). - Uses a mixin for views to overload get_object for relevant detail views. - Overloaded get_queryset() usage to check for exists are handled manually, should be cleaned up. --- hostpolicy/api/v1/tests.py | 7 +- hostpolicy/api/v1/views.py | 29 ++--- .../0004_migrate_away_from_ci_fields.py | 25 +++++ hostpolicy/models.py | 15 ++- mreg/api/v1/views.py | 4 + mreg/api/v1/views_hostgroups.py | 7 +- mreg/api/v1/views_labels.py | 4 +- mreg/api/v1/views_zones.py | 6 +- mreg/fields.py | 15 +++ mreg/managers.py | 42 +++++++ .../0013_migrate_away_from_ci_fields.py | 105 ++++++++++++++++++ mreg/mixins.py | 21 ++++ mreg/models/base.py | 20 +++- mreg/models/host.py | 15 ++- mreg/models/network.py | 5 +- mreg/models/resource_records.py | 21 ++-- mreg/models/zone.py | 30 +++-- 17 files changed, 315 insertions(+), 56 deletions(-) create mode 100644 hostpolicy/migrations/0004_migrate_away_from_ci_fields.py create mode 100644 mreg/managers.py create mode 100644 mreg/migrations/0013_migrate_away_from_ci_fields.py create mode 100644 mreg/mixins.py diff --git a/hostpolicy/api/v1/tests.py b/hostpolicy/api/v1/tests.py index 575febeb..834c047c 100644 --- a/hostpolicy/api/v1/tests.py +++ b/hostpolicy/api/v1/tests.py @@ -3,12 +3,11 @@ from django.contrib.auth.models import Group from hostpolicy.models import HostPolicyAtom, HostPolicyRole +from mreg.api.v1.tests.tests import MregAPITestCase from mreg.models.base import Label from mreg.models.host import Host, Ipaddress from mreg.models.network import NetGroupRegexPermission -from mreg.api.v1.tests.tests import MregAPITestCase - class HostPolicyUniqueNameSpace(MregAPITestCase): @@ -43,6 +42,10 @@ def test_list_200_ok(self): self.assertEqual(data['count'], 2) self.assertEqual(len(data['results']), 2) + def test_case_insensitive(self): + """Case insensitive lookups should work""" + self.assert_get(self.basejoin(self.object_one.name.upper())) + def test_get_404_not_found(self): """"Getting a non-existing entry should return 404""" self.assert_get_and_404(self.basejoin('nonexisting')) diff --git a/hostpolicy/api/v1/views.py b/hostpolicy/api/v1/views.py index 8139d72a..5025a3de 100644 --- a/hostpolicy/api/v1/views.py +++ b/hostpolicy/api/v1/views.py @@ -1,20 +1,20 @@ from django.db.models import Prefetch from rest_framework import status from rest_framework.response import Response - from url_filter.filtersets import ModelFilterSet -from hostpolicy.models import HostPolicyAtom, HostPolicyRole from hostpolicy.api.permissions import IsSuperOrHostPolicyAdminOrReadOnly -from mreg.api.v1.serializers import HostNameSerializer -from mreg.api.v1.views import (MregListCreateAPIView, - MregPermissionsListCreateAPIView, - MregPermissionsUpdateDestroy, - MregRetrieveUpdateDestroyAPIView, - ) - +from hostpolicy.models import HostPolicyAtom, HostPolicyRole from mreg.api.v1.history import HistoryLog +from mreg.api.v1.serializers import HostNameSerializer +from mreg.api.v1.views import ( + MregListCreateAPIView, + MregPermissionsListCreateAPIView, + MregPermissionsUpdateDestroy, + MregRetrieveUpdateDestroyAPIView, +) from mreg.api.v1.views_m2m import M2MDetail, M2MList, M2MPermissions +from mreg.mixins import LowerCaseLookupMixin from mreg.models.host import Host from . import serializers @@ -68,14 +68,15 @@ def get_queryset(self): def post(self, request, *args, **kwargs): if "name" in request.data: - if self.get_queryset().filter(name=request.data['name']).exists(): + # Due to the overriding of get_queryset, we need to manually use lower() + if self.get_queryset().filter(name=request.data['name'].lower()).exists(): content = {'ERROR': 'name already in use'} return Response(content, status=status.HTTP_409_CONFLICT) return super().post(request, *args, **kwargs) -class HostPolicyAtomDetail(HostPolicyAtomLogMixin, MregRetrieveUpdateDestroyAPIView): +class HostPolicyAtomDetail(HostPolicyAtomLogMixin, LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): queryset = HostPolicyAtom.objects.all() serializer_class = serializers.HostPolicyAtomSerializer @@ -103,14 +104,14 @@ def get_queryset(self): def post(self, request, *args, **kwargs): if "name" in request.data: - if self.get_queryset().filter(name=request.data['name']).exists(): + # Due to the overriding of get_queryset, we need to manually use lower() + if self.get_queryset().filter(name=request.data['name'].lower()).exists(): content = {'ERROR': 'name already in use'} return Response(content, status=status.HTTP_409_CONFLICT) - return super().post(request, *args, **kwargs) -class HostPolicyRoleDetail(HostPolicyRoleLogMixin, MregRetrieveUpdateDestroyAPIView): +class HostPolicyRoleDetail(HostPolicyRoleLogMixin, LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): queryset = HostPolicyRole.objects.all() serializer_class = serializers.HostPolicyRoleSerializer diff --git a/hostpolicy/migrations/0004_migrate_away_from_ci_fields.py b/hostpolicy/migrations/0004_migrate_away_from_ci_fields.py new file mode 100644 index 00000000..50eaea9b --- /dev/null +++ b/hostpolicy/migrations/0004_migrate_away_from_ci_fields.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.18 on 2023-06-29 09:26 + +from django.db import migrations +import hostpolicy.models +import mreg.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('hostpolicy', '0003_hostpolicyrole_labels'), + ] + + operations = [ + migrations.AlterField( + model_name='hostpolicyatom', + name='name', + field=mreg.fields.LowerCaseCharField(max_length=64, unique=True, validators=[hostpolicy.models._validate_atom_name]), + ), + migrations.AlterField( + model_name='hostpolicyrole', + name='name', + field=mreg.fields.LowerCaseCharField(max_length=64, unique=True, validators=[hostpolicy.models._validate_role_name]), + ), + ] diff --git a/hostpolicy/models.py b/hostpolicy/models.py index 44ed21b3..3468b9cd 100644 --- a/hostpolicy/models.py +++ b/hostpolicy/models.py @@ -1,11 +1,12 @@ -from django.core.exceptions import ValidationError import datetime +from django.core.exceptions import ValidationError from django.db import models -from mreg.fields import LCICharField -from mreg.models.host import Host +from mreg.fields import LowerCaseCharField +from mreg.managers import LowerCaseManager from mreg.models.base import Label +from mreg.models.host import Host class HostPolicyComponent(models.Model): @@ -29,7 +30,9 @@ def _validate_atom_name(name): class HostPolicyAtom(HostPolicyComponent): - name = LCICharField(max_length=64, unique=True, validators=[_validate_atom_name]) + name = LowerCaseCharField(max_length=64, unique=True, validators=[_validate_atom_name]) + + objects = LowerCaseManager() class Meta: db_table = 'hostpolicy_atom' @@ -44,11 +47,13 @@ def _validate_role_name(name): class HostPolicyRole(HostPolicyComponent): - name = LCICharField(max_length=64, unique=True, validators=[_validate_role_name]) + name = LowerCaseCharField(max_length=64, unique=True, validators=[_validate_role_name]) atoms = models.ManyToManyField(HostPolicyAtom, related_name='roles') hosts = models.ManyToManyField(Host, related_name='hostpolicyroles') labels = models.ManyToManyField(Label, blank=True, related_name='hostpolicyroles') + objects = LowerCaseManager() + class Meta: db_table = 'hostpolicy_role' ordering = ('name',) diff --git a/mreg/api/v1/views.py b/mreg/api/v1/views.py index ad6e9001..67093fea 100644 --- a/mreg/api/v1/views.py +++ b/mreg/api/v1/views.py @@ -37,6 +37,7 @@ PtrOverrideSerializer, SrvSerializer, SshfpSerializer, TxtSerializer) +from mreg.mixins import LowerCaseLookupMixin # These filtersets are used for applying generic filtering to all objects. class CnameFilterSet(ModelFilterSet): @@ -271,6 +272,7 @@ def get_queryset(self): class CnameDetail(HostPermissionsUpdateDestroy, + LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): """ get: @@ -304,6 +306,7 @@ def get_queryset(self): class HinfoDetail(HostPermissionsUpdateDestroy, + LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): """ get: @@ -409,6 +412,7 @@ def post(self, request, *args, **kwargs): class HostDetail(HostPermissionsUpdateDestroy, + LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): """ get: diff --git a/mreg/api/v1/views_hostgroups.py b/mreg/api/v1/views_hostgroups.py index 4ebaf748..50f0c1b8 100644 --- a/mreg/api/v1/views_hostgroups.py +++ b/mreg/api/v1/views_hostgroups.py @@ -10,6 +10,8 @@ IsSuperOrGroupAdminOrReadOnly) from mreg.models.host import Host, HostGroup +from mreg.mixins import LowerCaseLookupMixin + from . import serializers from .history import HistoryLog from .views import (MregListCreateAPIView, @@ -85,14 +87,15 @@ def get_queryset(self): def post(self, request, *args, **kwargs): if "name" in request.data: - if self.get_queryset().filter(name=request.data['name']).exists(): + # We need to manually use lower() here due to the overriden get_queryset() + if self.get_queryset().filter(name=request.data['name'].lower()).exists(): content = {'ERROR': 'hostgroup name already in use'} return Response(content, status=status.HTTP_409_CONFLICT) self.lookup_field = 'name' return super().post(request, *args, **kwargs) -class HostGroupDetail(HostGroupPermissionsUpdateDestroy): +class HostGroupDetail(LowerCaseLookupMixin, HostGroupPermissionsUpdateDestroy): """ get: Returns details for the specified hostgroup. Includes hostgroups that are members. diff --git a/mreg/api/v1/views_labels.py b/mreg/api/v1/views_labels.py index 51ef2e44..e0e32671 100644 --- a/mreg/api/v1/views_labels.py +++ b/mreg/api/v1/views_labels.py @@ -5,6 +5,8 @@ from .views import MregListCreateAPIView, MregRetrieveUpdateDestroyAPIView from mreg.models.base import Label from mreg.api.permissions import IsSuperOrAdminOrReadOnly + +from mreg.mixins import LowerCaseLookupMixin from . import serializers @@ -31,7 +33,7 @@ def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs) -class LabelDetail(MregRetrieveUpdateDestroyAPIView): +class LabelDetail(LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): """ get: Returns details for a Label. diff --git a/mreg/api/v1/views_zones.py b/mreg/api/v1/views_zones.py index a5a05fab..06cdea5f 100644 --- a/mreg/api/v1/views_zones.py +++ b/mreg/api/v1/views_zones.py @@ -15,6 +15,8 @@ from mreg.models.host import Host from mreg.models.zone import ForwardZone, ForwardZoneDelegation, ReverseZone, ReverseZoneDelegation +from mreg.mixins import LowerCaseLookupMixin + from mreg.api.permissions import (IsSuperGroupMember, IsAuthenticatedAndReadOnly) from .serializers import (ForwardZoneDelegationSerializer, ForwardZoneSerializer, @@ -172,7 +174,7 @@ class ReverseZoneDelegationList(ZoneDelegationList): model = ReverseZone -class ZoneDetail(MregRetrieveUpdateDestroyAPIView): +class ZoneDetail(LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): """ get: List details for a zone. @@ -239,7 +241,7 @@ class ReverseZoneDetail(ZoneDetail): queryset = ReverseZone.objects.all() -class ZoneDelegationDetail(MregRetrieveUpdateDestroyAPIView): +class ZoneDelegationDetail(LowerCaseLookupMixin, MregRetrieveUpdateDestroyAPIView): lookup_field = 'delegation' permission_classes = (IsSuperGroupMember | IsAuthenticatedAndReadOnly, ) diff --git a/mreg/fields.py b/mreg/fields.py index 5aee3813..e6cd421f 100644 --- a/mreg/fields.py +++ b/mreg/fields.py @@ -1,7 +1,15 @@ import django.contrib.postgres.fields as pgfields +import django.db.models as models from .validators import validate_hostname +class LowerCaseCharField(models.CharField): + """A CharField where the value is stored in lower case.""" + + def get_db_prep_save(self, value, connection): + if isinstance(value, str): + value = value.lower() + return super().get_db_prep_save(value, connection) class LCICharField(pgfields.CICharField): """A pgfields.CICharField where the value is stored in lower case. """ @@ -11,6 +19,13 @@ def get_db_prep_save(self, value, connection): value = value.lower() return super().get_db_prep_save(value, connection) +class LowerCaseDNSNameField(LowerCaseCharField): + """A field to hold DNS names.""" + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 253 + if 'validators' not in kwargs: + kwargs['validators'] = [validate_hostname] + super().__init__(*args, **kwargs) class DnsNameField(LCICharField): """ diff --git a/mreg/managers.py b/mreg/managers.py new file mode 100644 index 00000000..d2c5299c --- /dev/null +++ b/mreg/managers.py @@ -0,0 +1,42 @@ +from django.db import models + +from .fields import LowerCaseCharField + + +class LowerCaseManager(models.Manager): + """A manager that lowercases all values of LowerCaseCharFields in filter/exclude/get calls.""" + + @property + def lowercase_fields(self): + if not hasattr(self, "_lowercase_fields_cache"): + self._lowercase_fields_cache = [ + field.name + for field in self.model._meta.get_fields() + if isinstance(field, LowerCaseCharField) + ] + return self._lowercase_fields_cache + + def _lowercase_fields(self, **kwargs): + lower_kwargs = {} + for key, value in kwargs.items(): + field_name = key.split("__")[0] + if field_name in self.lowercase_fields and isinstance(value, str): + value = value.lower() + lower_kwargs[key] = value + return lower_kwargs + + def filter(self, **kwargs): + return super().filter(**self._lowercase_fields(**kwargs)) + + def exclude(self, **kwargs): + return super().exclude(**self._lowercase_fields(**kwargs)) + + def get(self, **kwargs): + return super().get(**self._lowercase_fields(**kwargs)) + + +def lower_case_manager_factory(base_manager): + class LowerCaseBaseManager(base_manager, LowerCaseManager): + pass + + return LowerCaseBaseManager diff --git a/mreg/migrations/0013_migrate_away_from_ci_fields.py b/mreg/migrations/0013_migrate_away_from_ci_fields.py new file mode 100644 index 00000000..16f3454f --- /dev/null +++ b/mreg/migrations/0013_migrate_away_from_ci_fields.py @@ -0,0 +1,105 @@ +# Generated by Django 3.2.18 on 2023-06-29 09:26 + +from django.db import migrations, models +import mreg.fields +import mreg.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('mreg', '0012_expiringtoken'), + ] + + operations = [ + migrations.AlterField( + model_name='cname', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='forwardzone', + name='email', + field=models.EmailField(max_length=254), + ), + migrations.AlterField( + model_name='forwardzone', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='forwardzone', + name='primary_ns', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='forwardzonedelegation', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='host', + name='contact', + field=models.EmailField(blank=True, max_length=254), + ), + migrations.AlterField( + model_name='host', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='hostgroup', + name='name', + field=mreg.fields.LowerCaseCharField(max_length=50, unique=True), + ), + migrations.AlterField( + model_name='label', + name='name', + field=mreg.fields.LowerCaseCharField(max_length=64, unique=True, validators=[mreg.validators.validate_nowhitespace]), + ), + migrations.AlterField( + model_name='mx', + name='mx', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='nameserver', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='naptr', + name='replacement', + field=mreg.fields.LowerCaseCharField(max_length=255), + ), + migrations.AlterField( + model_name='naptr', + name='service', + field=mreg.fields.LowerCaseCharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name='reversezone', + name='email', + field=models.EmailField(max_length=254), + ), + migrations.AlterField( + model_name='reversezone', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_reverse_zone_name]), + ), + migrations.AlterField( + model_name='reversezone', + name='primary_ns', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, validators=[mreg.validators.validate_hostname]), + ), + migrations.AlterField( + model_name='reversezonedelegation', + name='name', + field=mreg.fields.LowerCaseDNSNameField(max_length=253, unique=True, validators=[mreg.validators.validate_reverse_zone_name]), + ), + migrations.AlterField( + model_name='srv', + name='name', + field=mreg.fields.LowerCaseCharField(max_length=255, validators=[mreg.validators.validate_srv_service_text]), + ), + ] diff --git a/mreg/mixins.py b/mreg/mixins.py new file mode 100644 index 00000000..455785d4 --- /dev/null +++ b/mreg/mixins.py @@ -0,0 +1,21 @@ +from django.shortcuts import get_object_or_404 + + +class LowerCaseLookupMixin: + """A mixin to make DRF detail view lookup case insensitive.""" + + def get_object(self): + """Returns the object the view is displaying. + + This method is overriden to make the lookup case insensitive. + """ + + queryset = self.filter_queryset(self.get_queryset()) + filter_kwargs = {self.lookup_field: self.kwargs[self.lookup_field].lower()} + + obj = get_object_or_404(queryset, **filter_kwargs) + + # May raise a permission denied + self.check_object_permissions(self.request, obj) + + return obj diff --git a/mreg/models/base.py b/mreg/models/base.py index 3d01ea74..d32f26cd 100644 --- a/mreg/models/base.py +++ b/mreg/models/base.py @@ -8,15 +8,17 @@ from django.conf import settings from django.db import models from django.utils import timezone -from mreg.fields import DnsNameField, LCICharField +from rest_framework.authtoken.models import Token + +from mreg.fields import LowerCaseCharField, LowerCaseDNSNameField +from mreg.managers import LowerCaseManager from mreg.utils import clear_none, idna_encode, qualify from mreg.validators import validate_hostname, validate_nowhitespace, validate_ttl -from rest_framework.authtoken.models import Token MAX_UNUSED_LIST = 4096 # 12 bits for addresses. A large ipv4, but tiny ipv6 network. # To avoid circular imports, this base file is not allowed to import any other models -# from any other files. We do however want to include the model ForwardZoneMember here +# from any other files. We do however want to include the model ForwardZoneMember here # to ensure it is always available for import elsewhere. To achieve this, we use the # lazy loading feature of django models where we use a string instead of the class # reference. Here we thus use the string "ForwardZone" as the target for the foreign key @@ -24,6 +26,7 @@ # deep into the file, we use the following constant "_FORWARD_ZONE" as the reference. _FORWARD_ZONE = "ForwardZone" + class BaseModel(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -33,9 +36,11 @@ class Meta: class NameServer(BaseModel): - name = DnsNameField(unique=True) + name = LowerCaseDNSNameField(unique=True) ttl = models.IntegerField(blank=True, null=True, validators=[validate_ttl]) + objects = LowerCaseManager() + class Meta: db_table = "ns" ordering = ("name",) @@ -100,9 +105,13 @@ def remove_nameservers(self): class Label(BaseModel): - name = LCICharField(max_length=64, unique=True, validators=[validate_nowhitespace]) + name = LowerCaseCharField( + max_length=64, unique=True, validators=[validate_nowhitespace] + ) description = models.TextField(blank=False) + objects = LowerCaseManager() + class Meta: db_table = "label" ordering = ("name",) @@ -135,6 +144,7 @@ class ForwardZoneMember(BaseModel): zone = models.ForeignKey( _FORWARD_ZONE, models.DO_NOTHING, db_column="zone", blank=True, null=True ) + class Meta: abstract = True diff --git a/mreg/models/host.py b/mreg/models/host.py index 9bdaa6ff..07dee964 100644 --- a/mreg/models/host.py +++ b/mreg/models/host.py @@ -1,17 +1,20 @@ -import django.contrib.postgres.fields as pgfields from django.contrib.auth.models import Group from django.db import models -from mreg.fields import DnsNameField, LCICharField + +from mreg.fields import LowerCaseCharField, LowerCaseDNSNameField +from mreg.managers import LowerCaseManager from mreg.models.base import BaseModel, ForwardZoneMember from mreg.validators import validate_BACnetID, validate_mac_address, validate_ttl class Host(ForwardZoneMember): - name = DnsNameField(unique=True) - contact = pgfields.CIEmailField(blank=True) + name = LowerCaseDNSNameField(unique=True) + contact = models.EmailField(blank=True) ttl = models.IntegerField(blank=True, null=True, validators=[validate_ttl]) comment = models.TextField(blank=True) + objects = LowerCaseManager() + class Meta: db_table = "host" @@ -54,7 +57,7 @@ def __str__(self): class HostGroup(BaseModel): - name = LCICharField(max_length=50, unique=True) + name = LowerCaseCharField(max_length=50, unique=True) description = models.CharField(max_length=200, blank=True) owners = models.ManyToManyField(Group, blank=True) parent = models.ManyToManyField( @@ -62,6 +65,8 @@ class HostGroup(BaseModel): ) hosts = models.ManyToManyField(Host, related_name="hostgroups") + objects = LowerCaseManager() + class Meta: db_table = "hostgroup" ordering = ("name",) diff --git a/mreg/models/network.py b/mreg/models/network.py index bcf4da02..ec49a20b 100644 --- a/mreg/models/network.py +++ b/mreg/models/network.py @@ -4,9 +4,10 @@ from django.db import models from django.db.models import Q +from netfields import CidrAddressField, NetManager + from mreg.models.base import MAX_UNUSED_LIST, BaseModel, Label from mreg.validators import validate_regex -from netfields import CidrAddressField, NetManager class Network(BaseModel): @@ -98,10 +99,12 @@ def __used_ips(qs): def _used_ipaddresses(self): from mreg.models.host import Ipaddress + return self.__used(Ipaddress) def _used_ptroverrides(self): from mreg.models.host import PtrOverride + return self.__used(PtrOverride) @property diff --git a/mreg/models/resource_records.py b/mreg/models/resource_records.py index fa0ee8f4..f3aaa39c 100644 --- a/mreg/models/resource_records.py +++ b/mreg/models/resource_records.py @@ -1,5 +1,7 @@ from django.db import models -from mreg.fields import DnsNameField, LCICharField + +from mreg.fields import LowerCaseCharField, LowerCaseDNSNameField +from mreg.managers import LowerCaseManager from mreg.models.base import BaseModel, ForwardZoneMember from mreg.models.host import Host from mreg.validators import ( @@ -60,7 +62,7 @@ class Mx(BaseModel): Host, on_delete=models.CASCADE, db_column="host", related_name="mxs" ) priority = models.PositiveIntegerField(validators=[validate_16bit_uint]) - mx = DnsNameField() + mx = LowerCaseDNSNameField() class Meta: db_table = "mx" @@ -88,9 +90,11 @@ class Cname(ForwardZoneMember): host = models.ForeignKey( Host, on_delete=models.CASCADE, db_column="host", related_name="cnames" ) - name = DnsNameField(unique=True) + name = LowerCaseDNSNameField(unique=True) ttl = models.IntegerField(blank=True, null=True, validators=[validate_ttl]) + objects = LowerCaseManager + class Meta: db_table = "cname" ordering = ("name",) @@ -106,9 +110,11 @@ class Naptr(BaseModel): preference = models.IntegerField(validators=[validate_16bit_uint]) order = models.IntegerField(validators=[validate_16bit_uint]) flag = models.CharField(max_length=1, blank=True, validators=[validate_naptr_flag]) - service = LCICharField(max_length=128, blank=True) + service = LowerCaseCharField(max_length=128, blank=True) regex = models.CharField(max_length=128, blank=True) - replacement = LCICharField(max_length=255) + replacement = LowerCaseCharField(max_length=255) + + objects = LowerCaseManager() class Meta: db_table = "naptr" @@ -134,8 +140,9 @@ def __str__(self): self.replacement, ) + class Srv(ForwardZoneMember): - name = LCICharField(max_length=255, validators=[validate_srv_service_text]) + name = LowerCaseCharField(max_length=255, validators=[validate_srv_service_text]) priority = models.IntegerField(validators=[validate_16bit_uint]) weight = models.IntegerField(validators=[validate_16bit_uint]) port = models.IntegerField(validators=[validate_16bit_uint]) @@ -152,4 +159,4 @@ class Meta: ordering = ("name", "priority", "weight", "port", "host") def __str__(self): - return str(self.name) \ No newline at end of file + return str(self.name) diff --git a/mreg/models/zone.py b/mreg/models/zone.py index 7c377e72..9096df4c 100644 --- a/mreg/models/zone.py +++ b/mreg/models/zone.py @@ -2,10 +2,12 @@ from collections import defaultdict, namedtuple from datetime import timedelta -import django.contrib.postgres.fields as pgfields from django.db import DatabaseError, models, transaction from django.utils import timezone -from mreg.fields import DnsNameField +from netfields import CidrAddressField, NetManager + +from mreg.fields import LowerCaseDNSNameField +from mreg.managers import LowerCaseManager, lower_case_manager_factory from mreg.models.base import BaseModel, NameServer, ZoneHelpers from mreg.models.host import Ipaddress, PtrOverride from mreg.utils import ( @@ -20,14 +22,13 @@ validate_reverse_zone_name, validate_ttl, ) -from netfields import CidrAddressField, NetManager class BaseZone(BaseModel, ZoneHelpers): updated = models.BooleanField(default=True) - primary_ns = DnsNameField() + primary_ns = LowerCaseDNSNameField() nameservers = models.ManyToManyField(NameServer, db_column="ns") - email = pgfields.CIEmailField() + email = models.EmailField() serialno = models.BigIntegerField( default=create_serialno, validators=[validate_32bit_uint] ) @@ -38,6 +39,8 @@ class BaseZone(BaseModel, ZoneHelpers): soa_ttl = models.IntegerField(default=43200, validators=[validate_ttl]) default_ttl = models.IntegerField(default=43200, validators=[validate_ttl]) + objects = LowerCaseManager() + class Meta: abstract = True @@ -105,7 +108,9 @@ def update_serialno(self, force=False): class ForwardZone(BaseZone): - name = DnsNameField(unique=True) + name = LowerCaseDNSNameField(unique=True) + + objects = LowerCaseManager() class Meta: db_table = "forward_zone" @@ -136,12 +141,13 @@ def _get_reverse_order(data): class ReverseZone(BaseZone): - name = DnsNameField(unique=True, validators=[validate_reverse_zone_name]) + name = LowerCaseDNSNameField(unique=True, validators=[validate_reverse_zone_name]) # network can not be blank, but it will allow full_clean() to pass, even if # the network is not set. Will anyway be overridden by update() and save(). network = CidrAddressField(unique=True, blank=True) - objects = NetManager() + # We want lower case filtering and exludes for "name", but also use NetManager for the network field. + objects = lower_case_manager_factory(NetManager)() class Meta: db_table = "reverse_zone" @@ -246,10 +252,12 @@ class ForwardZoneDelegation(BaseModel, ZoneHelpers): db_column="zone", related_name="delegations", ) - name = DnsNameField(unique=True) + name = LowerCaseDNSNameField(unique=True) nameservers = models.ManyToManyField(NameServer, db_column="ns") comment = models.CharField(blank=True, max_length=200) + objects = LowerCaseManager() + class Meta: db_table = "forward_zone_delegation" @@ -264,7 +272,7 @@ class ReverseZoneDelegation(BaseModel, ZoneHelpers): db_column="zone", related_name="delegations", ) - name = DnsNameField(unique=True, validators=[validate_reverse_zone_name]) + name = LowerCaseDNSNameField(unique=True, validators=[validate_reverse_zone_name]) nameservers = models.ManyToManyField(NameServer, db_column="ns") comment = models.CharField(blank=True, max_length=200) @@ -273,5 +281,3 @@ class Meta: def __str__(self): return f"{self.zone.name} {self.name}" - -