Unverified Commit c0b54602 authored by Tytus Kurek's avatar Tytus Kurek Committed by Graham Hayes

CAA DNS records

This patchset adds support for DNS CAA (Certification Authority
Authorization) Resource Record which is described in RFC 6844
(https://tools.ietf.org/html/rfc6844)

Change-Id: If9619096f1706d1123895b63b9129b9ffd4fb320
Closes-Bug: 1787552
parent 44d9c02c
......@@ -63,7 +63,10 @@ rectype2iparectype = {'A': ('arecord', '%(data)s'),
'PTR': ('ptrrecord', '%(data)s'),
'SPF': ('spfrecord', '%(data)s'),
'SSHFP': ('sshfprecord', '%(data)s'),
'NAPTR': ('naptrrecord', '%(data)s')}
'NAPTR': ('naptrrecord', '%(data)s'),
'CAA': ('caarecord', '%(data)s'),
}
IPA_INVALID_DATA = 3009
IPA_NOT_FOUND = 4001
......
......@@ -69,7 +69,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']),
'PTR', 'SSHFP', 'SOA', 'NAPTR', 'CAA']),
]
# Set some Oslo Log defaults
......
......@@ -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_cname import CNAME, CNAMEList # noqa
from designate.objects.rrdata_mx import MX, MXList # noqa
from designate.objects.rrdata_naptr import NAPTR, NAPTRList # noqa
......
......@@ -101,6 +101,9 @@ class StringFields(ovoo_fields.StringField):
RE_NAPTR_FLAGS = r'^(?!.*(.).*\1)[APSU]+$'
RE_NAPTR_SERVICE = r'^([A-Za-z]([A-Za-z0-9]*)(\+[A-Za-z]([A-Za-z0-9]{0,31}))*)?' # noqa
RE_NAPTR_REGEXP = r'^([^0-9i\\])(.*)\1((.+)|(\\[1-9]))\1(i?)'
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)?://.*/'
def __init__(self, nullable=False, read_only=False,
default=ovoo_fields.UnspecifiedDefault, description='',
......@@ -337,6 +340,57 @@ class NaptrRegexpField(StringFields):
return value
class CaaPropertyField(StringFields):
def __init__(self, **kwargs):
super(CaaPropertyField, self).__init__(**kwargs)
def coerce(self, obj, attr, value):
value = super(CaaPropertyField, self).coerce(obj, attr, value)
prpt = value.split(' ', 1)
tag = prpt[0]
val = prpt[1]
if (tag == 'issue' or tag == 'issuewild'):
entries = val.split(';')
idn = entries.pop(0)
domain = idn.split('.')
for host in domain:
if len(host) > 63:
raise ValueError("Host %s is too long" % host)
idn_with_dot = idn + '.'
if not re.match(self.RE_ZONENAME, idn_with_dot):
raise ValueError("Domain %s does not match" % idn)
for entry in entries:
if not re.match(self.RE_KVP, entry):
raise ValueError("%s is not valid key-value pair" % entry)
elif tag == 'iodef':
if re.match(self.RE_URL_MAIL, val):
parts = val.split('@')
idn = parts[1]
domain = idn.split('.')
for host in domain:
if len(host) > 63:
raise ValueError("Host %s is too long" % host)
idn_with_dot = idn + '.'
if not re.match(self.RE_ZONENAME, idn_with_dot):
raise ValueError("Domain %s does not match" % idn)
elif re.match(self.RE_URL_HTTP, val):
parts = val.split('/')
idn = parts[2]
domain = idn.split('.')
for host in domain:
if len(host) > 63:
raise ValueError("Host %s is too long" % host)
idn_with_dot = idn + '.'
if not re.match(self.RE_ZONENAME, idn_with_dot):
raise ValueError("Domain %s does not match" % idn)
else:
raise ValueError("%s is not valid URL" % val)
else:
raise ValueError("Property tag %s must be 'issue', 'issuewild'"
" or 'iodef'" % value)
return value
class Any(ovoo_fields.FieldType):
@staticmethod
def coerce(obj, attr, value):
......
# Copyright 2018 Canonical Ltd.
#
# Author: Tytus Kurek <tytus.kurek@canonical.com>
#
# 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 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 CAA(Record):
"""
CAA Resource Record Type
Defined in: RFC6844
"""
fields = {
'flags': fields.IntegerFields(minimum=0, maximum=1),
'prpt': fields.CaaPropertyField()
}
def _to_string(self):
return ("%(flag)s %(prpt)s" % self)
def _from_string(self, v):
flags, prpt = v.split(' ', 1)
self.flags = int(flags)
self.prpt = prpt
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 257
@base.DesignateRegistry.register
class CAAList(RecordList):
LIST_ITEM_TYPE = CAA
fields = {
'objects': fields.ListOfObjectsField('CAA'),
}
# Copyright 2018 Canonical Ltd.
#
# Author: Tytus Kurek <tytus.kurek@canonical.com>
#
# 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', 'CAA']
records_table = Table('recordsets', meta, autoload=True)
records_table.columns.type.alter(name='type', type=Enum(*RECORD_TYPES))
def downgrade(migrate_engine):
meta.bind = migrate_engine
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
'PTR', 'SSHFP', 'SOA']
records_table = Table('recordsets', meta, autoload=True)
# Delete all CAA records
records_table.filter_by(name='type', type='CAA').delete()
# Remove CAA from the ENUM
records_table.columns.type.alter(type=Enum(*RECORD_TYPES))
......@@ -29,7 +29,8 @@ CONF = cfg.CONF
RESOURCE_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR']
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR',
'SSHFP', 'SOA', 'NAPTR']
'SSHFP', 'SOA', 'NAPTR', 'CAA']
TASK_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR', 'COMPLETE']
TSIG_ALGORITHMS = ['hmac-md5', 'hmac-sha1', 'hmac-sha224', 'hmac-sha256',
'hmac-sha384', 'hmac-sha512']
......
......@@ -1823,7 +1823,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']
# 'SSHFP', 'SOA', 'NAPTR', 'CAA']
# Create a recordset
recordset = self.create_recordset(zone)
cname_recordset = self.create_recordset(zone, type='CNAME')
......
# Copyright 2018 Canonical Ltd.
#
# Author: Tytus Kurek <tytus.kurek@canonical.com>
#
# 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 oslo_log import log as logging
import oslotest.base
from designate import objects
LOG = logging.getLogger(__name__)
def debug(*a, **kw):
for v in a:
LOG.debug(repr(v))
for k in sorted(kw):
LOG.debug("%s: %s", k, repr(kw[k]))
class CAARecordTest(oslotest.base.BaseTestCase):
def test_parse_caa_issue(self):
caa_record = objects.CAA()
caa_record._from_string('0 issue ca.example.net')
self.assertEqual(0, caa_record.flags)
self.assertEqual('issue ca.example.net', caa_record.prpt)
def test_parse_caa_issuewild(self):
caa_record = objects.CAA()
caa_record._from_string('1 issuewild ca.example.net; policy=ev')
self.assertEqual(1, caa_record.flags)
self.assertEqual('issuewild ca.example.net; policy=ev',
caa_record.prpt)
def test_parse_caa_iodef(self):
caa_record = objects.CAA()
caa_record._from_string('0 iodef https://example.net/')
self.assertEqual(0, caa_record.flags)
self.assertEqual('iodef https://example.net/', caa_record.prpt)
......@@ -186,3 +186,12 @@ Objects NAPTR Record
:members:
:undoc-members:
:show-inheritance:
Objects CAA Record
====================
.. automodule:: designate.objects.rrdata_caa
:members:
:undoc-members:
:show-inheritance:
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment