From 73e5671c02490a4e33285d034f35de018d4d7836 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 2 Dec 2021 06:43:37 -0800 Subject: [PATCH] infrastructure needed for CNAME-and-other-data check in txn --- dns/node.py | 12 ++++++ dns/rdataset.py | 18 ++++++--- dns/transaction.py | 93 ++++++++++++++++++++++++++++++++++++++++------ dns/zone.py | 6 +++ 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/dns/node.py b/dns/node.py index 7f172dd4..3267de77 100644 --- a/dns/node.py +++ b/dns/node.py @@ -226,3 +226,15 @@ class Node: self.delete_rdataset(replacement.rdclass, replacement.rdtype, replacement.covers) self._append_rdataset(replacement) + + def is_cname(self): + """Is this a CNAME node? + + If the node has a CNAME or an RRSIG(CNAME) it is considered a CNAME + node for CNAME-and-other-data purposes, and ``True`` is returned. + Otherwise the node is an "other data" node, and ``False`` is returned. + """ + for rdataset in self.rdatasets: + if rdataset.implies_cname(): + return True + return False diff --git a/dns/rdataset.py b/dns/rdataset.py index 868c1fc3..242e30c7 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -48,11 +48,6 @@ _ok_for_cname = { dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 } -_delete_for_other_data = { - (dns.rdatatype.CNAME, dns.rdatatype.NONE), - (dns.rdatatype.RRSIG, dns.rdatatype.CNAME), -} - class Rdataset(dns.set.Set): @@ -342,10 +337,21 @@ class Rdataset(dns.set.Set): (self.rdtype == dns.rdatatype.RRSIG and self.covers in _ok_for_cname) + def implies_cname(self): + """Does this rdataset imply a node is a CNAME node? + + If the rdataset's type is CNAME or RRSIG(CNAME) then it implies a + node is a CNAME node, and ``True`` is returned. Otherwise it implies + the node is an an "other data" node, and ``False`` is returned. + """ + return self.rdtype == dns.rdatatype.CNAME or \ + (self.rdtype == dns.rdatatype.RRSIG and + self.covers == dns.rdatatype.CNAME) + def ok_for_other_data(self): """Is this rdataset compatible with an 'other data' (i.e. not CNAME) node?""" - return (self.rdtype, self.covers) not in _delete_for_other_data + return not self.implies_cname() @dns.immutable.immutable diff --git a/dns/transaction.py b/dns/transaction.py index 8aec2e8d..22c63a71 100644 --- a/dns/transaction.py +++ b/dns/transaction.py @@ -79,6 +79,12 @@ class AlreadyEnded(dns.exception.DNSException): """Tried to use an already-ended transaction.""" +def _ensure_immutable(rdataset): + if rdataset is None or isinstance(rdataset, dns.rdataset.ImmutableRdataset): + return rdataset + return dns.rdataset.ImmutableRdataset(rdataset) + + class Transaction: def __init__(self, manager, replacement=False, read_only=False): @@ -86,6 +92,9 @@ class Transaction: self.replacement = replacement self.read_only = read_only self._ended = False + self._check_put_rdataset = [] + self._check_delete_rdataset = [] + self._check_delete_name = [] # # This is the high level API @@ -102,10 +111,15 @@ class Transaction: name = dns.name.from_text(name, None) rdtype = dns.rdatatype.RdataType.make(rdtype) rdataset = self._get_rdataset(name, rdtype, covers) - if rdataset is not None and \ - not isinstance(rdataset, dns.rdataset.ImmutableRdataset): - rdataset = dns.rdataset.ImmutableRdataset(rdataset) - return rdataset + return _ensure_immutable(rdataset) + + def get_rdatasets(self, name): + """Return the rdatasets at *name*, if any. + + The returned rdatasets are immutable. + An empty tuple is returned if the name doesn't exist. + """ + return [_ensure_immutable(rds) for rds in self._get_rdatasets(name)] def _check_read_only(self): if self.read_only: @@ -271,6 +285,43 @@ class Transaction: """ self._end(False) + def check_put_rdataset(self, check): + """Call *check* before putting (storing) an rdataset. + + The function is called with the transaction, the name, and the rdataset. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_put_rdataset.append(check) + + def check_delete_rdataset(self, check): + """Call *check* before deleting an rdataset. + + The function is called with the transaction, the name, the rdatatype, + covered rdatatype. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_delete_rdataset.append(check) + + def check_delete_name(self, check): + """Call *check* before putting (storing) an rdataset. + + The function is called with the transaction and the name. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_delete_name.append(check) + # # Helper methods # @@ -349,7 +400,7 @@ class Transaction: trds.update(existing) existing = trds rdataset = existing.union(rdataset) - self._put_rdataset(name, rdataset) + self._checked_put_rdataset(name, rdataset) except IndexError: raise TypeError(f'not enough parameters to {method}') @@ -403,16 +454,16 @@ class Transaction: raise DeleteNotExact(f'{method}: missing rdatas') rdataset = existing.difference(rdataset) if len(rdataset) == 0: - self._delete_rdataset(name, rdataset.rdtype, - rdataset.covers) + self._checked_delete_rdataset(name, rdataset.rdtype, + rdataset.covers) else: - self._put_rdataset(name, rdataset) + self._checked_put_rdataset(name, rdataset) elif exact: raise DeleteNotExact(f'{method}: missing rdataset') else: if exact and not self._name_exists(name): raise DeleteNotExact(f'{method}: name not known') - self._delete_name(name) + self._checked_delete_name(name) except IndexError: raise TypeError(f'not enough parameters to {method}') @@ -429,6 +480,21 @@ class Transaction: finally: self._ended = True + def _checked_put_rdataset(self, name, rdataset): + for check in self._check_put_rdataset: + check(self, name, rdataset) + self._put_rdataset(name, rdataset) + + def _checked_delete_rdataset(self, name, rdtype, covers): + for check in self._check_delete_rdataset: + check(self, name, rdtype, covers) + self._delete_rdataset(name, rdtype, covers) + + def _checked_delete_name(self, name): + for check in self._check_delete_name: + check(self, name) + self._delete_name(name) + # # Transactions are context managers. # @@ -462,7 +528,7 @@ class Transaction: def _delete_name(self, name): """Delete all data associated with *name*. - It is not an error if the rdataset does not exist. + It is not an error if the name does not exist. """ raise NotImplementedError # pragma: no cover @@ -506,7 +572,12 @@ class Transaction: def _iterate_rdatasets(self): """Return an iterator that yields (name, rdataset) tuples. + """ + raise NotImplementedError # pragma: no cover + + def _get_rdatasets(self, name): + """Return the rdatasets at *name*, if any. - Not all Transaction subclasses implement this. + An empty list is returned if the name doesn't exist. """ raise NotImplementedError # pragma: no cover diff --git a/dns/zone.py b/dns/zone.py index 510be2df..9d999c74 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -1024,6 +1024,12 @@ class Transaction(dns.transaction.Transaction): for rdataset in node: yield (name, rdataset) + def _get_rdatasets(self, name): + node = self.version.get_node(name) + if node is None: + return [] + return node.rdatasets + def from_text(text, origin=None, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, filename=None, -- 2.47.3