diff --git a/contrib/archive/backends/impl_ipa/__init__.py b/contrib/archive/backends/impl_ipa/__init__.py index 4576e76d1..4809373e5 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 d0a2f43ea..5540f5570 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 07accd432..61ea2a1e6 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 19a52c0d0..9b533438d 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 000000000..166d4c02c --- /dev/null +++ b/designate/objects/rrdata_cert.py @@ -0,0 +1,106 @@ +# Copyright 2021 Cloudification GmbH +# +# Author: cloudification +# +# 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 000000000..85ba5b67e --- /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 +# +# 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 8adb14d21..6f4c4a814 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 a299946f0..26694b9ad 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 000000000..a2f70d8d8 --- /dev/null +++ b/designate/tests/unit/objects/test_cert_object.py @@ -0,0 +1,81 @@ +# Copyright 2021 Cloudification GmbH +# +# Author: cloudification +# +# 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 1934f4634..fed275ec2 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 000000000..caac001be --- /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.