From 3ae8b103974d7e6aca4e2c5f093e63b67f25a6dd Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 18 Mar 2022 16:53:20 -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 | 60 ++++++++- tests/test_ipa_dns.py | 228 +++++++++++++++++++++++++++------ 2 files changed, 242 insertions(+), 46 deletions(-) diff --git a/src/ipahealthcheck/ipa/idns.py b/src/ipahealthcheck/ipa/idns.py index 4aa7008..64d5ddd 100644 --- a/src/ipahealthcheck/ipa/idns.py +++ b/src/ipahealthcheck/ipa/idns.py @@ -11,12 +11,21 @@ from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants from ipalib import api -from dns import resolver +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 +41,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): from ipapython.dnsutil import query_srv @@ -43,6 +56,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() @@ -63,6 +77,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) @@ -95,10 +118,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 = [] @@ -121,7 +173,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 = [] @@ -155,7 +207,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 91b15c2..28243b6 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 = [] @@ -188,16 +236,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)) ] @@ -219,7 +270,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 @@ -228,13 +283,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)), @@ -267,7 +328,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 @@ -276,14 +341,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)), @@ -325,7 +397,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 @@ -334,15 +410,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)), @@ -382,7 +464,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 @@ -390,9 +476,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 @@ -403,6 +490,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)), @@ -444,21 +536,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 @@ -469,6 +570,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)), @@ -510,12 +616,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( @@ -527,9 +641,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 @@ -541,6 +656,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)), @@ -584,12 +704,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') == \ @@ -597,12 +725,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)) ] @@ -624,12 +754,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' @@ -637,11 +775,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)) ] @@ -664,7 +804,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