Blob Blame History Raw
From 0355be5205c8fa0645c7d7552654a331a4727821 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Wed, 6 Jul 2022 16:59:40 -0400
Subject: [PATCH] Add support for the DNS URI type

URI records are not required but if they exist they are
validated.

https://github.com/freeipa/freeipa-healthcheck/issues/222

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
 src/ipahealthcheck/ipa/idns.py |  64 ++++++++-
 tests/test_ipa_dns.py          | 228 +++++++++++++++++++++++++++------
 2 files changed, 246 insertions(+), 46 deletions(-)

diff --git a/src/ipahealthcheck/ipa/idns.py b/src/ipahealthcheck/ipa/idns.py
index 3282e2c..9f1b158 100644
--- a/src/ipahealthcheck/ipa/idns.py
+++ b/src/ipahealthcheck/ipa/idns.py
@@ -11,12 +11,25 @@ from ipahealthcheck.core.plugin import Result, duration
 from ipahealthcheck.core import constants
 
 from ipalib import api
-from dns import resolver
+
+try:
+    from dns.resolver import resolve
+except ImportError:
+    from dns.resolver import query as resolve
 
 
 logger = logging.getLogger()
 
 
+def query_uri(uri):
+    try:
+        answers = resolve(uri, rdatatype.URI)
+    except DNSException as e:
+        logger.debug("DNS record not found: %s", e.__class__.__name__)
+        answers = []
+    return answers
+
+
 @registry
 class IPADNSSystemRecordsCheck(IPAPlugin):
     """
@@ -32,6 +45,10 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
         """Combine the SRV record and target into a unique name."""
         return srv + ":" + target
 
+    def uri_to_name(self, uri, target):
+        """Combine the SRV record and target into a unique name."""
+        return uri + ":" + target
+
     @duration
     def check(self):
         # pylint: disable=import-outside-toplevel
@@ -45,6 +62,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
         # collect the list of expected values
         txt_rec = dict()
         srv_rec = dict()
+        uri_rec = dict()
         a_rec = list()
         aaaa_rec = list()
 
@@ -65,6 +83,15 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
                         a_rec.append(rd.to_text())
                     elif rd.rdtype == rdatatype.AAAA:
                         aaaa_rec.append(rd.to_text())
+                    elif rd.rdtype == rdatatype.URI:
+                        if name.ToASCII() in uri_rec:
+                            uri_rec[name.ToASCII()].append(
+                                rd.target.decode('utf-8')
+                            )
+                        else:
+                            uri_rec[name.ToASCII()] = [
+                                rd.target.decode('utf-8')
+                            ]
                     else:
                         logger.error("Unhandler rdtype %d", rd.rdtype)
 
@@ -97,10 +124,39 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
                     msg='Expected SRV record missing',
                     key=self.srv_to_name(srv, host))
 
+        for uri in uri_rec:
+            logger.debug("Search DNS for URI record of %s", uri)
+            answers = query_uri(uri)
+            hosts = uri_rec[uri]
+            for answer in answers:
+                logger.debug("DNS record found: %s", answer)
+                try:
+                    hosts.remove(answer.target.decode('utf-8'))
+                    yield Result(
+                         self, constants.SUCCESS,
+                         key=self.uri_to_name(
+                             uri, answer.target.decode('utf-8')
+                         )
+                    )
+                except ValueError:
+                    yield Result(
+                        self, constants.WARNING,
+                        msg='Unexpected URI entry in DNS',
+                        key=self.uri_to_name(
+                            uri, answer.target.decode('utf-8')
+                        )
+                    )
+            for host in hosts:
+                yield Result(
+                    self, constants.WARNING,
+                    msg='Expected URI record missing',
+                    key=self.uri_to_name(uri, host)
+                )
+
         for txt in txt_rec:
             logger.debug("Search DNS for TXT record of %s", txt)
             try:
-                answers = resolver.query(txt, rdatatype.TXT)
+                answers = resolve(txt, rdatatype.TXT)
             except DNSException as e:
                 logger.debug("DNS record not found: %s", e.__class__.__name__)
                 answers = []
@@ -123,7 +179,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
             qname = "ipa-ca." + api.env.domain + "."
             logger.debug("Search DNS for A record of %s", qname)
             try:
-                answers = resolver.query(qname, rdatatype.A)
+                answers = resolve(qname, rdatatype.A)
             except DNSException as e:
                 logger.debug("DNS record not found: %s", e.__class__.__name__)
                 answers = []
@@ -157,7 +213,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
             qname = "ipa-ca." + api.env.domain + "."
             logger.debug("Search DNS for AAAA record of %s", qname)
             try:
-                answers = resolver.query(qname, rdatatype.AAAA)
+                answers = resolve(qname, rdatatype.AAAA)
             except DNSException as e:
                 logger.debug("DNS record not found: %s", e.__class__.__name__)
                 answers = []
diff --git a/tests/test_ipa_dns.py b/tests/test_ipa_dns.py
index 11e1aa9..43ddcb9 100644
--- a/tests/test_ipa_dns.py
+++ b/tests/test_ipa_dns.py
@@ -27,6 +27,15 @@ from ipaserver.dns_data_management import (
     IPA_DEFAULT_ADTRUST_SRV_REC
 )
 
+try:
+    # pylint: disable=unused-import
+    from ipaserver.dns_data_management import IPA_DEFAULT_MASTER_URI_REC  # noqa
+except ImportError:
+    has_uri_support = False
+else:
+    has_uri_support = True
+
+
 try:
     # pylint: disable=unused-import
     from ipaserver.install.installutils import resolve_rrsets_nss  # noqa: F401
@@ -79,6 +88,45 @@ def query_srv(qname, ad_records=False):
     return rdlist
 
 
+def query_uri(hosts):
+    """
+    Return a list containing two answers, one for each uri type
+    """
+    answers = []
+    if version.MAJOR < 2 or (version.MAJOR == 2 and version.MINOR == 0):
+        m = message.Message()
+    elif version.MAJOR == 2 and version.MINOR > 0:
+        m = message.QueryMessage()  # pylint: disable=E1101
+        m = message.make_response(m)  # pylint: disable=E1101
+
+    rdtype = rdatatype.URI
+    for name in ('_kerberos.', '_kpasswd.'):
+        qname = DNSName(name + m_api.env.domain)
+        qname = qname.make_absolute()
+        if version.MAJOR < 2:
+            # pylint: disable=unexpected-keyword-arg
+            answer = Answer(qname, rdataclass.IN, rdtype, m,
+                            raise_on_no_answer=False)
+            # pylint: enable=unexpected-keyword-arg
+        else:
+            if version.MAJOR == 2 and version.MINOR > 0:
+                question = rrset.RRset(qname, rdataclass.IN, rdtype)
+                m.question = [question]
+            answer = Answer(qname, rdataclass.IN, rdtype, m)
+
+        rl = []
+        for host in hosts:
+            rlist = rrset.from_text_list(
+                qname, 86400, rdataclass.IN,
+                rdatatype.URI, ['0 100 "krb5srv:m:tcp:%s."' % host,
+                                '0 100 "krb5srv:m:udp:%s."' % host, ]
+            )
+            rl.extend(rlist)
+        answer.rrset = rl
+        answers.append(answer)
+    return answers
+
+
 def gen_addrs(rdtype=rdatatype.A, num=1):
     """Generate sequential IP addresses for the ipa-ca A record lookup"""
     ips = []
@@ -202,16 +250,19 @@ class TestDNSSystemRecords(BaseTest):
 
        1. The query_srv() override returns the set of configured
           servers for each type of SRV record.
-       2. fake_query() overrides dns.resolver.query to simulate
+       2. fake_query() overrides ipahealthcheck.ipa.idns.resolve to simulate
           A, AAAA and TXT record lookups.
     """
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_single(self, mock_query, mock_query_srv, mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_single(self, mock_query, mock_query_uri,
+                               mock_query_srv, mock_rrset):
         """Test single CA master, all SRV records"""
         mock_query.side_effect = fake_query_one
         mock_query_srv.side_effect = query_srv([m_api.env.host])
+        mock_query_uri.side_effect = query_uri([m_api.env.host])
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
         ]
@@ -233,7 +284,11 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 10
+        if has_uri_support:
+            expected = 14
+        else:
+            expected = 10
+        assert len(self.results) == expected
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
@@ -242,13 +297,19 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_two(self, mock_query, mock_query_srv, mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_two(self, mock_query, mock_query_uri,
+                            mock_query_srv, mock_rrset):
         """Test two CA masters, all SRV records"""
         mock_query_srv.side_effect = query_srv([
             m_api.env.host,
             'replica.' + m_api.env.domain
         ])
+        mock_query_uri.side_effect = query_uri([
+            m_api.env.host,
+            'replica.' + m_api.env.domain
+        ])
         mock_query.side_effect = fake_query_two
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -281,7 +342,11 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 19
+        if has_uri_support:
+            expected = 27
+        else:
+            expected = 19
+        assert len(self.results) == expected
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
@@ -290,14 +355,21 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_three(self, mock_query, mock_query_srv, mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_three(self, mock_query, mock_query_uri,
+                              mock_query_srv, mock_rrset):
         """Test three CA masters, all SRV records"""
         mock_query_srv.side_effect = query_srv([
             m_api.env.host,
             'replica.' + m_api.env.domain,
             'replica2.' + m_api.env.domain
         ])
+        mock_query_uri.side_effect = query_uri([
+            m_api.env.host,
+            'replica.' + m_api.env.domain,
+            'replica2.' + m_api.env.domain
+        ])
         mock_query.side_effect = fake_query_three
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -339,7 +411,11 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 28
+        if has_uri_support:
+            expected = 40
+        else:
+            expected = 28
+        assert len(self.results) == expected
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
@@ -348,15 +424,21 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_three_mixed(self, mock_query, mock_query_srv,
-                                    mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_three_mixed(self, mock_query, mock_query_uri,
+                                    mock_query_srv, mock_rrset):
         """Test three masters, only one with a CA, all SRV records"""
         mock_query_srv.side_effect = query_srv([
             m_api.env.host,
             'replica.' + m_api.env.domain,
             'replica2.' + m_api.env.domain
         ])
+        mock_query_uri.side_effect = query_uri([
+            m_api.env.host,
+            'replica.' + m_api.env.domain,
+            'replica2.' + m_api.env.domain
+        ])
         mock_query.side_effect = fake_query_one
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -396,7 +478,11 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 24
+        if has_uri_support:
+            expected = 36
+        else:
+            expected = 24
+        assert len(self.results) == expected
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
@@ -404,9 +490,10 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_missing_server(self, mock_query, mock_query_srv,
-                                       mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_missing_server(self, mock_query, mock_query_uri,
+                                       mock_query_srv, mock_rrset):
         """Drop one of the masters from query_srv
 
            This will simulate missing SRV records and cause a number of
@@ -417,6 +504,11 @@ class TestDNSSystemRecords(BaseTest):
             'replica.' + m_api.env.domain
             # replica2 is missing
         ])
+        mock_query_uri.side_effect = query_uri([
+            m_api.env.host,
+            'replica.' + m_api.env.domain,
+            'replica2.' + m_api.env.domain
+        ])
         mock_query.side_effect = fake_query_three
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -458,21 +550,30 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 28
+        if has_uri_support:
+            expected = 40
+        else:
+            expected = 28
+        assert len(self.results) == expected
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 21
-        assert len(warn) == 7
+        if has_uri_support:
+            assert len(ok) == 33
+            assert len(warn) == 7
+        else:
+            assert len(ok) == 21
+            assert len(warn) == 7
 
         for result in warn:
             assert result.kw.get('msg') == 'Expected SRV record missing'
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_srv,
-                                       mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_uri,
+                                       mock_query_srv, mock_rrset):
         """Drop one of the masters from query_srv
 
            This will simulate missing SRV records and cause a number of
@@ -483,6 +584,11 @@ class TestDNSSystemRecords(BaseTest):
             'replica.' + m_api.env.domain,
             'replica2.' + m_api.env.domain
         ])
+        mock_query_uri.side_effect = query_uri([
+            m_api.env.host,
+            'replica.' + m_api.env.domain,
+            'replica2.' + m_api.env.domain
+        ])
         mock_query.side_effect = fake_query_two
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -524,12 +630,20 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 28
+        if has_uri_support:
+            expected = 40
+        else:
+            expected = 28
+        assert len(self.results) == expected
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 26
-        assert len(warn) == 2
+        if has_uri_support:
+            assert len(ok) == 38
+            assert len(warn) == 2
+        else:
+            assert len(ok) == 26
+            assert len(warn) == 2
 
         for result in warn:
             assert re.match(
@@ -541,9 +655,10 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_extra_srv(self, mock_query, mock_query_srv,
-                                  mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_extra_srv(self, mock_query, mock_query_uri,
+                                  mock_query_srv, mock_rrset):
         """An extra SRV record set exists, report it.
 
            Add an extra master to the query_srv() which will generate
@@ -555,6 +670,11 @@ class TestDNSSystemRecords(BaseTest):
             'replica2.' + m_api.env.domain,
             'replica3.' + m_api.env.domain
         ])
+        mock_query_uri.side_effect = query_uri([
+            m_api.env.host,
+            'replica.' + m_api.env.domain,
+            'replica2.' + m_api.env.domain,
+        ])
         mock_query.side_effect = fake_query_three
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -598,12 +718,20 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 35
+        if has_uri_support:
+            expected = 47
+        else:
+            expected = 35
+        assert len(self.results) == expected
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 28
-        assert len(warn) == 7
+        if has_uri_support:
+            assert len(ok) == 40
+            assert len(warn) == 7
+        else:
+            assert len(ok) == 28
+            assert len(warn) == 7
 
         for result in warn:
             assert result.kw.get('msg') == \
@@ -611,12 +739,14 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_bad_realm(self, mock_query, mock_query_srv,
-                                  mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_bad_realm(self, mock_query, mock_query_uri,
+                                  mock_query_srv, mock_rrset):
         """Unexpected Kerberos TXT record"""
         mock_query.side_effect = fake_query_one_txt
         mock_query_srv.side_effect = query_srv([m_api.env.host])
+        mock_query_uri.side_effect = query_uri([m_api.env.host])
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
         ]
@@ -638,12 +768,20 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 10
+        if has_uri_support:
+            expected = 14
+        else:
+            expected = 10
+        assert len(self.results) == expected
 
         ok = get_results_by_severity(self.results.results, constants.SUCCESS)
         warn = get_results_by_severity(self.results.results, constants.WARNING)
-        assert len(ok) == 9
-        assert len(warn) == 1
+        if has_uri_support:
+            assert len(ok) == 13
+            assert len(warn) == 1
+        else:
+            assert len(ok) == 9
+            assert len(warn) == 1
 
         result = warn[0]
         assert result.kw.get('msg') == 'expected realm missing'
@@ -651,11 +789,13 @@ class TestDNSSystemRecords(BaseTest):
 
     @patch(resolve_rrsets_import)
     @patch('ipapython.dnsutil.query_srv')
-    @patch('dns.resolver.query')
-    def test_dnsrecords_one_with_ad(self, mock_query, mock_query_srv,
-                                    mock_rrset):
+    @patch('ipahealthcheck.ipa.idns.query_uri')
+    @patch('ipahealthcheck.ipa.idns.resolve')
+    def test_dnsrecords_one_with_ad(self, mock_query, mock_query_uri,
+                                    mock_query_srv, mock_rrset):
         mock_query.side_effect = fake_query_one
         mock_query_srv.side_effect = query_srv([m_api.env.host], True)
+        mock_query_uri.side_effect = query_uri([m_api.env.host])
         mock_rrset.side_effect = [
             resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
         ]
@@ -678,7 +818,11 @@ class TestDNSSystemRecords(BaseTest):
 
         self.results = capture_results(f)
 
-        assert len(self.results) == 16
+        if has_uri_support:
+            expected = 20
+        else:
+            expected = 16
+        assert len(self.results) == expected
 
         for result in self.results.results:
             assert result.result == constants.SUCCESS
-- 
2.31.1