From 0355be5205c8fa0645c7d7552654a331a4727821 Mon Sep 17 00:00:00 2001 From: Rob Crittenden 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 --- 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