summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-08-31 00:49:33 +0000
committerGerrit Code Review <review@openstack.org>2021-08-31 00:49:33 +0000
commit76bb79dd0d151e0c7b9d820d8d93da51c32fd037 (patch)
treea4117ad4f2c53960e2d7e24ec76248b325899dfd
parentd65faea1d540da4d0f600f5c969098eae53842cd (diff)
parente7b024660938d0b55ead1fa8556dca1d22c7bb59 (diff)
downloaddesignate-76bb79dd0d151e0c7b9d820d8d93da51c32fd037.tar.gz
Merge "CERT DNS records"
-rw-r--r--contrib/archive/backends/impl_ipa/__init__.py1
-rw-r--r--designate/conf/base.py2
-rw-r--r--designate/objects/__init__.py1
-rw-r--r--designate/objects/fields.py26
-rw-r--r--designate/objects/rrdata_cert.py106
-rw-r--r--designate/storage/impl_sqlalchemy/migrate_repo/versions/103_support_cert_records.py29
-rw-r--r--designate/storage/impl_sqlalchemy/tables.py2
-rw-r--r--designate/tests/test_central/test_service.py2
-rw-r--r--designate/tests/unit/objects/test_cert_object.py81
-rw-r--r--doc/source/contributor/sourcedoc/objects.rst8
-rw-r--r--releasenotes/notes/CERT_records-eb9b786f480851ff.yaml6
11 files changed, 261 insertions, 3 deletions
diff --git a/contrib/archive/backends/impl_ipa/__init__.py b/contrib/archive/backends/impl_ipa/__init__.py
index 4576e76d..4809373e 100644
--- a/contrib/archive/backends/impl_ipa/__init__.py
+++ b/contrib/archive/backends/impl_ipa/__init__.py
@@ -65,6 +65,7 @@ rectype2iparectype = {'A': ('arecord', '%(data)s'),
'SSHFP': ('sshfprecord', '%(data)s'),
'NAPTR': ('naptrrecord', '%(data)s'),
'CAA': ('caarecord', '%(data)s'),
+ 'CERT': ('certrecord', '%(data)s'),
}
diff --git a/designate/conf/base.py b/designate/conf/base.py
index d0a2f43e..5540f557 100644
--- a/designate/conf/base.py
+++ b/designate/conf/base.py
@@ -54,7 +54,7 @@ DESIGNATE_OPTS = [
# Supported record types
cfg.ListOpt('supported_record_type', help='Supported record types',
default=['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
- 'PTR', 'SSHFP', 'SOA', 'NAPTR', 'CAA']),
+ 'PTR', 'SSHFP', 'SOA', 'NAPTR', 'CAA', 'CERT']),
# TCP Settings
cfg.IntOpt('backlog',
diff --git a/designate/objects/__init__.py b/designate/objects/__init__.py
index 07accd43..61ea2a1e 100644
--- a/designate/objects/__init__.py
+++ b/designate/objects/__init__.py
@@ -49,6 +49,7 @@ from designate.objects.zone_export import ZoneExport, ZoneExportList # noqa
from designate.objects.rrdata_a import A, AList # noqa
from designate.objects.rrdata_aaaa import AAAA, AAAAList # noqa
from designate.objects.rrdata_caa import CAA, CAAList # noqa
+from designate.objects.rrdata_cert import CERT, CERTList # noqa
from designate.objects.rrdata_cname import CNAME, CNAMEList # noqa
from designate.objects.rrdata_mx import MX, MXList # noqa
from designate.objects.rrdata_naptr import NAPTR, NAPTRList # noqa
diff --git a/designate/objects/fields.py b/designate/objects/fields.py
index 19a52c0d..9b533438 100644
--- a/designate/objects/fields.py
+++ b/designate/objects/fields.py
@@ -102,6 +102,8 @@ class StringFields(ovoo_fields.StringField):
RE_KVP = r'^\s[A-Za-z0-9]+=[A-Za-z0-9]+'
RE_URL_MAIL = r'^mailto:[A-Za-z0-9_\-]+@.*'
RE_URL_HTTP = r'^http(s)?://.*/'
+ RE_CERT_TYPE = r'(^[A-Z]+$)|(^[0-9]+$)'
+ RE_CERT_ALGO = r'(^[A-Z]+[A-Z0-9\-]+[A-Z0-9]$)|(^[0-9]+$)'
def __init__(self, nullable=False, read_only=False,
default=ovoo_fields.UnspecifiedDefault, description='',
@@ -389,6 +391,30 @@ class CaaPropertyField(StringFields):
return value
+class CertTypeField(StringFields):
+ def __init__(self, **kwargs):
+ super(CertTypeField, self).__init__(**kwargs)
+
+ def coerce(self, obj, attr, value):
+ value = super(CertTypeField, self).coerce(obj, attr, value)
+ if not re.match(self.RE_CERT_TYPE, "%s" % value):
+ raise ValueError("Cert type %s is not a valid Mnemonic or "
+ "value" % value)
+ return value
+
+
+class CertAlgoField(StringFields):
+ def __init__(self, **kwargs):
+ super(CertAlgoField, self).__init__(**kwargs)
+
+ def coerce(self, obj, attr, value):
+ value = super(CertAlgoField, self).coerce(obj, attr, value)
+ if not re.match(self.RE_CERT_ALGO, "%s" % value):
+ raise ValueError("Cert Algo %s is not a valid Mnemonic or "
+ "value" % value)
+ return value
+
+
class Any(ovoo_fields.FieldType):
@staticmethod
def coerce(obj, attr, value):
diff --git a/designate/objects/rrdata_cert.py b/designate/objects/rrdata_cert.py
new file mode 100644
index 00000000..166d4c02
--- /dev/null
+++ b/designate/objects/rrdata_cert.py
@@ -0,0 +1,106 @@
+# Copyright 2021 Cloudification GmbH
+#
+# Author: cloudification <contact@cloudification.io>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import base64
+
+from designate.exceptions import InvalidObject
+from designate.objects.record import Record
+from designate.objects.record import RecordList
+from designate.objects import base
+from designate.objects import fields
+
+
+@base.DesignateRegistry.register
+class CERT(Record):
+ """
+ CERT Resource Record Type
+ Defined in: RFC4398
+ """
+ fields = {
+ 'cert_type': fields.CertTypeField(),
+ 'key_tag': fields.IntegerFields(minimum=0, maximum=65535),
+ 'cert_algo': fields.CertAlgoField(),
+ 'certificate': fields.StringFields(),
+ }
+
+ def validate_cert_type(self, cert_type):
+ try:
+ int_cert_type = int(cert_type)
+ if int_cert_type < 0 or int_cert_type > 65535:
+ err = ("Cert type value should be between 0 and 65535")
+ raise InvalidObject(err)
+ except ValueError:
+ # cert type is specified as Mnemonic
+ VALID_CERTS = ['PKIX', 'SPKI', 'PGP', 'IPKIX', 'ISPKI', 'IPGP',
+ 'ACPKIX', 'IACPKIX', 'URI', 'OID', 'DPKIX', 'DPTR']
+ if cert_type not in VALID_CERTS:
+ err = ("Cert type is not valid Mnemonic.")
+ raise InvalidObject(err)
+ return cert_type
+
+ def validate_cert_algo(self, cert_algo):
+ try:
+ int_cert_algo = int(cert_algo)
+ if int_cert_algo < 0 or int_cert_algo > 255:
+ err = ("Cert algorithm value should be between 0 and 255")
+ raise InvalidObject(err)
+ except ValueError:
+ # cert algo is specified as Mnemonic
+ VALID_ALGOS = ['RSAMD5', 'DSA', 'RSASHA1', 'DSA-NSEC3-SHA1',
+ 'RSASHA1-NSEC3-SHA1', 'RSASHA256', 'RSASHA512',
+ 'ECC-GOST', 'ECDSAP256SHA256', 'ECDSAP384SHA384',
+ 'ED25519', 'ED448']
+ if cert_algo not in VALID_ALGOS:
+ err = ("Cert algorithm is not valid Mnemonic.")
+ raise InvalidObject(err)
+ return cert_algo
+
+ def validate_cert_certificate(self, certificate):
+ try:
+ chunks = certificate.split(' ')
+ encoded_chunks = []
+ for chunk in chunks:
+ encoded_chunks.append(chunk.encode())
+ b64 = b''.join(encoded_chunks)
+ base64.b64decode(b64)
+ except Exception:
+ err = ("Cert certificate is not valid.")
+ raise InvalidObject(err)
+ return certificate
+
+ def _to_string(self):
+ return ("%(cert_type)s %(key_tag)s %(cert_algo)s "
+ "%(certificate)s" % self)
+
+ def _from_string(self, v):
+ cert_type, key_tag, cert_algo, certificate = v.split(' ', 3)
+
+ self.cert_type = self.validate_cert_type(cert_type)
+ self.key_tag = int(key_tag)
+ self.cert_algo = self.validate_cert_algo(cert_algo)
+ self.certificate = self.validate_cert_certificate(certificate)
+
+ RECORD_TYPE = 37
+
+
+@base.DesignateRegistry.register
+class CERTList(RecordList):
+
+ LIST_ITEM_TYPE = CERT
+
+ fields = {
+ 'objects': fields.ListOfObjectsField('CERT'),
+ }
diff --git a/designate/storage/impl_sqlalchemy/migrate_repo/versions/103_support_cert_records.py b/designate/storage/impl_sqlalchemy/migrate_repo/versions/103_support_cert_records.py
new file mode 100644
index 00000000..85ba5b67
--- /dev/null
+++ b/designate/storage/impl_sqlalchemy/migrate_repo/versions/103_support_cert_records.py
@@ -0,0 +1,29 @@
+# Copyright 2021 Cloudification GmbH
+#
+# Author: cloudification <contact@cloudification.io>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from sqlalchemy import MetaData, Table, Enum
+
+meta = MetaData()
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
+ 'PTR', 'SSHFP', 'SOA', 'NAPTR', 'CAA', 'CERT']
+
+ records_table = Table('recordsets', meta, autoload=True)
+ records_table.columns.type.alter(name='type', type=Enum(*RECORD_TYPES))
diff --git a/designate/storage/impl_sqlalchemy/tables.py b/designate/storage/impl_sqlalchemy/tables.py
index 8adb14d2..6f4c4a81 100644
--- a/designate/storage/impl_sqlalchemy/tables.py
+++ b/designate/storage/impl_sqlalchemy/tables.py
@@ -29,7 +29,7 @@ CONF = cfg.CONF
RESOURCE_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR']
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR',
- 'SSHFP', 'SOA', 'NAPTR', 'CAA']
+ 'SSHFP', 'SOA', 'NAPTR', 'CAA', 'CERT']
TASK_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR', 'COMPLETE']
TSIG_ALGORITHMS = ['hmac-md5', 'hmac-sha1', 'hmac-sha224', 'hmac-sha256',
diff --git a/designate/tests/test_central/test_service.py b/designate/tests/test_central/test_service.py
index a299946f..26694b9a 100644
--- a/designate/tests/test_central/test_service.py
+++ b/designate/tests/test_central/test_service.py
@@ -1863,7 +1863,7 @@ class CentralServiceTest(CentralTestCase):
def test_update_recordset_immutable_type(self):
zone = self.create_zone()
# ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR',
- # 'SSHFP', 'SOA', 'NAPTR', 'CAA']
+ # 'SSHFP', 'SOA', 'NAPTR', 'CAA', 'CERT']
# Create a recordset
recordset = self.create_recordset(zone)
cname_recordset = self.create_recordset(zone, type='CNAME')
diff --git a/designate/tests/unit/objects/test_cert_object.py b/designate/tests/unit/objects/test_cert_object.py
new file mode 100644
index 00000000..a2f70d8d
--- /dev/null
+++ b/designate/tests/unit/objects/test_cert_object.py
@@ -0,0 +1,81 @@
+# Copyright 2021 Cloudification GmbH
+#
+# Author: cloudification <contact@cloudification.io>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import oslotest.base
+from oslo_log import log as logging
+
+from designate import exceptions
+from designate import objects
+
+LOG = logging.getLogger(__name__)
+
+
+class CERTRecordTest(oslotest.base.BaseTestCase):
+ def test_parse_cert(self):
+ cert_record = objects.CERT()
+ cert_record._from_string(
+ 'DPKIX 1 RSASHA256 KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc=') # noqa
+
+ self.assertEqual('DPKIX', cert_record.cert_type)
+ self.assertEqual(1, cert_record.key_tag)
+ self.assertEqual('RSASHA256', cert_record.cert_algo)
+ self.assertEqual('KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc=',
+ cert_record.certificate)
+
+ def test_parse_invalid_cert_type_value(self):
+ cert_record = objects.CERT()
+ self.assertRaisesRegex(
+ exceptions.InvalidObject,
+ 'Cert type value should be between 0 and 65535',
+ cert_record._from_string,
+ '99999 1 RSASHA256 KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc='
+ )
+
+ def test_parse_invalid_cert_type_mnemonic(self):
+ cert_record = objects.CERT()
+ self.assertRaisesRegex(
+ exceptions.InvalidObject,
+ 'Cert type is not valid Mnemonic.',
+ cert_record._from_string,
+ 'FAKETYPE 1 RSASHA256 KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc='
+ )
+
+ def test_parse_invalid_cert_algo_value(self):
+ cert_record = objects.CERT()
+ self.assertRaisesRegex(
+ exceptions.InvalidObject,
+ 'Cert algorithm value should be between 0 and 255',
+ cert_record._from_string,
+ 'DPKIX 1 256 KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc='
+ )
+
+ def test_parse_invalid_cert_algo_mnemonic(self):
+ cert_record = objects.CERT()
+ self.assertRaisesRegex(
+ exceptions.InvalidObject,
+ 'Cert algorithm is not valid Mnemonic.',
+ cert_record._from_string,
+ 'DPKIX 1 FAKESHA256 KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc='
+ )
+
+ def test_parse_invalid_cert_certificate(self):
+ cert_record = objects.CERT()
+ self.assertRaisesRegex(
+ exceptions.InvalidObject,
+ 'Cert certificate is not valid.',
+ cert_record._from_string,
+ 'DPKIX 1 RSASHA256 KR1L0GbocaIOOim1+qdHtOSrDcOsGiI2NCcxuX2/Tqc'
+ )
diff --git a/doc/source/contributor/sourcedoc/objects.rst b/doc/source/contributor/sourcedoc/objects.rst
index 1934f463..fed275ec 100644
--- a/doc/source/contributor/sourcedoc/objects.rst
+++ b/doc/source/contributor/sourcedoc/objects.rst
@@ -195,3 +195,11 @@ Objects CAA Record
:members:
:undoc-members:
:show-inheritance:
+
+Objects CERT Record
+====================
+.. automodule:: designate.objects.rrdata_cert
+
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/releasenotes/notes/CERT_records-eb9b786f480851ff.yaml b/releasenotes/notes/CERT_records-eb9b786f480851ff.yaml
new file mode 100644
index 00000000..caac001b
--- /dev/null
+++ b/releasenotes/notes/CERT_records-eb9b786f480851ff.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ CERT recordset type have been added. All users should be able to use this type
+ from the API and openstack client. This can be disabled (like other record types) by
+ setting the `[DEFAULT].supported-record-type` config variable in all designate services.