Browse Source

further work, mostly functional name checking

jsonschema
Ross McFarland 6 months ago
parent
commit
4edd6340fd
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
3 changed files with 101 additions and 63 deletions
  1. +83
    -26
      octodns/record/base.py
  2. +1
    -1
      octodns/record/srv.py
  3. +17
    -36
      tests/test_octodns_record.py

+ 83
- 26
octodns/record/base.py View File

@ -27,8 +27,7 @@ class Record(EqualityTupleMixin):
log = getLogger('Record')
_CLASSES = {}
_NAME_SCHEMAS = {}
_DATA_VALIDATORS = {}
_VALIDATORS = {}
@classmethod
def register_type(cls, _class, _type=None):
@ -42,21 +41,14 @@ class Record(EqualityTupleMixin):
raise RecordException(msg)
cls._CLASSES[_type] = _class
# see if there's a record schema
try:
cls._NAME_SCHEMAS[_type] = Draft202012Validator(
schema=_class.name_schema(),
format_checker=Draft202012Validator.FORMAT_CHECKER,
)
except AttributeError:
pass
# see if there's a data schema
schema = _class.data_schema()
if schema:
cls._DATA_VALIDATORS[_type] = Draft202012Validator(
schema=schema,
format_checker=Draft202012Validator.FORMAT_CHECKER,
# data_schema is a flag for using the updated validation mechinism,
# so use the schema based name validation as well
cls._VALIDATORS[_type] = (
Draft202012Validator(schema=_class.name_schema()),
Draft202012Validator(schema=schema),
)
@classmethod
@ -93,19 +85,34 @@ class Record(EqualityTupleMixin):
msg += f', {context}'
raise Exception(msg)
# name_validator = cls._NAME_VALIDATORS.get(_type)
data_validator = cls._DATA_VALIDATORS.get(_type)
if data_validator:
if _type in cls._VALIDATORS:
# TODO: this should live somewhere else
# new jsonschema based validaton
for e in chain(iter([]), data_validator.iter_errors(data)):
name_validator, data_validator = cls._VALIDATORS.get(_type)
errors = chain(
name_validator.iter_errors({'name': name, 'fqdn': fqdn}),
data_validator.iter_errors(data),
)
for error in errors:
print('error:')
# some of the jsonschema error messages are opaque and useless
# to end uses, this provides a mechinism to translate them.
schema = error.schema
print(f'schema={schema}')
try:
translations = e.schema['translations']
path = '.'.join(e.schema_path)
message = translations[path]
message = schema['$error_message']
print(f'$error_message: message={message}')
except KeyError:
message = e.message
path = '.'.join(str(p) for p in error.schema_path)
print(f'$error_messages: path={path}')
try:
messages = schema['$error_messages']
print(f'$error_messages: messages={messages}')
message = messages[path]
print(f'$error_messages: message={message}')
except KeyError:
message = error.message
print(f'error.messages: message={message}')
reasons.append(message)
else:
# original .validate
@ -124,6 +131,54 @@ class Record(EqualityTupleMixin):
raise ValidationError(fqdn, reasons, context)
return _class(zone, name, data, source=source, context=context)
@classmethod
def name_schema(cls):
# https://github.com/ypcrts/fqdn?tab=readme-ov-file#ietf-specification
# https://datatracker.ietf.org/doc/html/rfc1034
# https://datatracker.ietf.org/doc/html/rfc1035
return {
'properties': {
'name': {'type': 'string'},
'fqdn': {'type': 'string', 'maxLength': 253},
},
'allOf': [
{
'properties': {
'name': {
'not': {'const': '@'},
# TODO: quote name
'$error_message': 'invalid name "@", use "" instead',
}
}
},
{
'properties': {
'name': {
'not': {'pattern': r'[^\.]{63}'},
'$error_message': 'invalid label, too long, max is 63',
}
}
},
{
'properties': {
'name': {
'not': {'pattern': r'\.\.'},
'$error_message': 'invalid name, double `.`',
}
}
},
{
'properties': {
'name': {
'not': {'pattern': r'\.$'},
'$error_message': 'invalid name, double `.`',
}
}
},
],
'required': ['name', 'fqdn'],
}
def TODO_remove(cls):
class_schemas = []
for _type, schema in cls._SCHEMAS.items():
@ -385,8 +440,10 @@ class ValuesMixin(object):
'$defs': {'schema': value_type.schema()},
'title': cls._type,
'properties': {
# TODO: what schema validations make sense for octodns?
# TODO: what schema validations make sense for these
'octodns': {},
'geo': {},
'dynamic': {},
'type': {'const': cls._type},
'ttl': {'type': 'integer', 'minimum': 0, 'maximum': 86400},
'value': {'$ref': '#/$defs/schema'},
@ -397,10 +454,10 @@ class ValuesMixin(object):
},
},
'required': ['ttl', 'type'],
'oneOf': [{'required': ['value']}, {'required': ['values']}],
'anyOf': [{'required': ['value']}, {'required': ['values']}],
"unevaluatedProperties": False,
'translations': {
'oneOf': "one of 'value' or 'values' is a required property"
'$error_messages': {
'anyOf': "one of 'value' or 'values' is a required property"
},
}


+ 1
- 1
octodns/record/srv.py View File

@ -189,7 +189,7 @@ class SrvRecord(ValuesMixin, Record):
@classmethod
def name_schema(cls):
schema = super().jsonschema()
schema = super().name_schema()
# we'll have _
schema['pattern'] = r'^(\*|_[^\.]+)\.[^\.]+'
return schema


+ 17
- 36
tests/test_octodns_record.py View File

@ -583,12 +583,8 @@ class TestRecordValidation(TestCase):
self.zone, name, {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'}
)
reason = ctx.exception.reasons[0]
self.assertTrue(reason.startswith('invalid fqdn, "xxxx'))
self.assertTrue(
reason.endswith(
'.unit.tests." is too long at 254 chars, max is 253'
)
)
self.assertTrue(reason.startswith("'xxxx"))
self.assertTrue(reason.endswith("xxxx.unit.tests.' is too long"))
# label length, DNS defines max as 63
with self.assertRaises(ValidationError) as ctx:
@ -597,10 +593,8 @@ class TestRecordValidation(TestCase):
Record.new(
self.zone, name, {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'}
)
reason = ctx.exception.reasons[0]
self.assertTrue(reason.startswith('invalid label, "xxxx'))
self.assertTrue(
reason.endswith('xxx" is too long at 64 chars, max is 63')
self.assertEqual(
'invalid label, too long, max is 63', ctx.exception.reasons[0]
)
with self.assertRaises(ValidationError) as ctx:
@ -608,10 +602,8 @@ class TestRecordValidation(TestCase):
Record.new(
self.zone, name, {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'}
)
reason = ctx.exception.reasons[0]
self.assertTrue(reason.startswith('invalid label, "xxxx'))
self.assertTrue(
reason.endswith('xxx" is too long at 64 chars, max is 63')
self.assertEqual(
'invalid label, too long, max is 63', ctx.exception.reasons[0]
)
# should not raise with dots
@ -634,12 +626,8 @@ class TestRecordValidation(TestCase):
{'ttl': 300, 'type': 'A', 'value': '1.2.3.4'},
)
reason = ctx.exception.reasons[0]
self.assertTrue(reason.startswith('invalid fqdn, "déjà-vu'))
self.assertTrue(
reason.endswith(
'.unit.tests." is too long at 259' ' chars, max is 253'
)
)
self.assertTrue(reason.startswith("'xn--dj-vu-sqa5d.xxxxx"))
self.assertTrue(reason.endswith(".unit.tests.' is too long"))
# same, but with ascii version of things
plain = 'deja-vu'
@ -673,11 +661,7 @@ class TestRecordValidation(TestCase):
'this.ends.with.a.dot.',
{'ttl': 301, 'type': 'A', 'value': '1.2.3.4'},
)
reason = ctx.exception.reasons[0]
self.assertEqual(
'invalid name, double `.` in "this.ends.with.a.dot..unit.tests."',
reason,
)
self.assertEqual('invalid name, double `.`', ctx.exception.reasons[0])
# double dots are not valid when eplxicit
with self.assertRaises(ValidationError) as ctx:
@ -686,11 +670,7 @@ class TestRecordValidation(TestCase):
'this.has.double..dots',
{'ttl': 301, 'type': 'A', 'value': '1.2.3.4'},
)
reason = ctx.exception.reasons[0]
self.assertEqual(
'invalid name, double `.` in "this.has.double..dots.unit.tests."',
reason,
)
self.assertEqual('invalid name, double `.`', ctx.exception.reasons[0])
# double dots in idna names
with self.assertRaises(ValidationError) as ctx:
@ -699,15 +679,14 @@ class TestRecordValidation(TestCase):
'niño.',
{'ttl': 301, 'type': 'A', 'value': '1.2.3.4'},
)
reason = ctx.exception.reasons[0]
self.assertEqual(
'invalid name, double `.` in "niño..unit.tests."', reason
)
self.assertEqual('invalid name, double `.`', ctx.exception.reasons[0])
# no ttl
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {'type': 'A', 'value': '1.2.3.4'})
self.assertEqual(['missing ttl'], ctx.exception.reasons)
self.assertEqual(
["'ttl' is a required property"], ctx.exception.reasons
)
# invalid ttl
with self.assertRaises(ValidationError) as ctx:
@ -715,7 +694,9 @@ class TestRecordValidation(TestCase):
self.zone, 'www', {'type': 'A', 'ttl': -1, 'value': '1.2.3.4'}
)
self.assertEqual('www.unit.tests.', ctx.exception.fqdn)
self.assertEqual(['invalid ttl'], ctx.exception.reasons)
self.assertEqual(
['-1 is less than the minimum of 0'], ctx.exception.reasons
)
# no exception if we're in lenient mode
Record.new(


Loading…
Cancel
Save