diff --git a/domainconnectzone/DomainConnectImpl.py b/domainconnectzone/DomainConnectImpl.py index 9ade03b..9e531cc 100644 --- a/domainconnectzone/DomainConnectImpl.py +++ b/domainconnectzone/DomainConnectImpl.py @@ -554,6 +554,7 @@ def process_custom_record(template_record, zone_records): 'AAAA': ['A', 'AAAA', 'CNAME', 'REDIR301', 'REDIR302'], 'MX': ['MX', 'CNAME'], 'CNAME': ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'REDIR301', 'REDIR302'], + 'APEXCNAME': ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'REDIR301', 'REDIR302'], 'REDIR301': ['A', 'AAAA', 'CNAME'], 'REDIR302': ['A', 'AAAA', 'CNAME'], } @@ -604,13 +605,43 @@ def process_other_record(template_record, zone_records): return new_record +def process_apexcname_record(template_record, zone_records): + """ + Process an APEXCNAME record from a template. APEXCNAME always targets the + zone apex (@) and behaves like a CNAME for conflict resolution purposes. + + :param template_record: The record from the template to process. + :type template_record: dict + - keys: 'type', 'pointsTo', 'ttl'; optional 'host' (must be '@' if present) + + :param zone_records: A list of all records in the current zone. + :type zone_records: list + + :return: The new APEXCNAME record. + :rtype: dict + """ + new_record = {'type': 'APEXCNAME', + 'name': '@', + 'data': template_record['pointsTo'], + 'ttl': int(template_record['ttl'])} + + for zone_record in zone_records: + zone_record_type = zone_record['type'].upper() + if zone_record_type in _delete_map['APEXCNAME'] and \ + zone_record['name'] == '@' and \ + '_replace' not in zone_record: + zone_record['_delete'] = 1 + + return new_record + + def check_conflict_with_self(new_record, new_records): - # Mark records that conflict with self (affects only CNAME and NS) + # Mark records that conflict with self (affects only CNAME, APEXCNAME and NS) for zone_record in new_records: zone_record_type = zone_record['type'].upper() error = False - if (new_record['type'] == 'CNAME' or zone_record['type'] == 'CNAME') \ + if (new_record['type'] in ('CNAME', 'APEXCNAME') or zone_record['type'] in ('CNAME', 'APEXCNAME')) \ and zone_record['name'] == new_record['name']: error = True @@ -629,7 +660,7 @@ def check_conflict_with_self(new_record, new_records): if error: raise InvalidData(f"Template record {new_record['type']} {new_record['name']} conflicts with other tempate record {zone_record['type']} {zone_record['name']}") -_CORE_TYPES = {'A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'TXT', 'SPFM', +_CORE_TYPES = {'A', 'AAAA', 'CNAME', 'APEXCNAME', 'MX', 'NS', 'SRV', 'TXT', 'SPFM', 'REDIR301', 'REDIR302'} # Maps RR type → list of fields whose string values must NOT be lowercased @@ -838,7 +869,7 @@ def process_records(template_records, zone_records, domain, host, params, template_record_type = template_record['type'].upper() # We can only handle certain record types - supported = ['A', 'AAAA', 'MX', 'CNAME', 'TXT', 'SRV', 'SPFM', 'NS'] + supported = ['A', 'AAAA', 'MX', 'CNAME', 'APEXCNAME', 'TXT', 'SRV', 'SPFM', 'NS'] if redirect_records is not None: supported += ['REDIR301', 'REDIR302'] is_custom = not template_record_type in supported and is_custom_record_type(template_record_type) @@ -848,7 +879,7 @@ def process_records(template_records, zone_records, domain, host, params, # Deal with the variables and validation - # Deal with the host/name + # Deal with the host/name if template_record_type == 'SRV': template_record['name'] = resolve_variables( template_record['name'], domain, host, params, 'name') @@ -861,6 +892,14 @@ def process_records(template_records, zone_records, domain, host, params, raise InvalidData('Invalid data for SRV host: ' + srvhost) + elif template_record_type == 'APEXCNAME': + # host is optional for APEXCNAME; if present it must be '@' + apex_host = template_record.get('host', '@') + if apex_host != '@': + raise InvalidData('Invalid data for APEXCNAME host: ' + + apex_host + ' (must be @ or omitted)') + template_record['host'] = '@' + else: orig_host = template_record['host'] template_record['host'] = resolve_variables( @@ -889,14 +928,14 @@ def process_records(template_records, zone_records, domain, host, params, raise InvalidData(err_msg) # Points To / Target - if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'NS']: + if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'APEXCNAME', 'NS']: orig_pointsto = template_record['pointsTo'] if template_record_type == 'NS' and orig_pointsto == '@': raise InvalidData('Invalid data for NS pointsTo: @ would create a circular delegation') template_record['pointsTo'] = resolve_variables( template_record['pointsTo'], domain, host, params, 'pointsTo') - if template_record_type in ['MX', 'CNAME', 'NS']: + if template_record_type in ['MX', 'CNAME', 'APEXCNAME', 'NS']: if not is_valid_pointsTo_host( template_record['pointsTo']): raise InvalidData('Invalid data for ' + @@ -1010,6 +1049,8 @@ def process_records(template_records, zone_records, domain, host, params, new_record = process_srv_record(template_record, zone_records) elif template_record_type in ['REDIR301', 'REDIR302']: new_record = process_redir_record(template_record, zone_records) + elif template_record_type == 'APEXCNAME': + new_record = process_apexcname_record(template_record, zone_records) elif is_custom: new_record = process_custom_record(template_record, zone_records) else: diff --git a/test/test_definitions/process_records_apexcname_tests.yaml b/test/test_definitions/process_records_apexcname_tests.yaml new file mode 100644 index 0000000..602c55d --- /dev/null +++ b/test/test_definitions/process_records_apexcname_tests.yaml @@ -0,0 +1,119 @@ +version: "1.0" +suite_type: process_records +description: "Domain Connect process_records compliance tests — APEXCNAME" + +tests: + + # --------------------------------------------------------------------------- + # APEXCNAME + # --------------------------------------------------------------------------- + + - id: apexcname_basic + description: "APEXCNAME creates a record at the apex (@) with no host field" + input: + zone_records: [] + template_records: + - {type: APEXCNAME, pointsTo: target.example.com, ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + new_count: 1 + delete_count: 0 + records: + - {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600} + + - id: apexcname_host_at + description: "APEXCNAME with explicit host '@' is accepted" + input: + zone_records: [] + template_records: + - {type: APEXCNAME, host: "@", pointsTo: target.example.com, ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + new_count: 1 + delete_count: 0 + records: + - {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600} + + - id: apexcname_host_non_apex_rejected + description: "APEXCNAME with host other than '@' raises InvalidData" + input: + zone_records: [] + template_records: + - {type: APEXCNAME, host: "sub", pointsTo: target.example.com, ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + exception: InvalidData + + - id: apexcname_delete_conflicts + description: "APEXCNAME deletes conflicting records at @ (A, AAAA, CNAME, MX, TXT)" + input: + zone_records: + - {type: A, name: "@", data: 1.2.3.4, ttl: 300} + - {type: AAAA, name: "@", data: "::1", ttl: 300} + - {type: CNAME, name: "@", data: old.example.com, ttl: 300} + - {type: MX, name: "@", data: mail.example.com, ttl: 300, priority: 10} + - {type: TXT, name: "@", data: "v=spf1 ~all", ttl: 300} + - {type: NS, name: "@", data: ns1.example.com, ttl: 300} + template_records: + - {type: APEXCNAME, pointsTo: target.example.com, ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + new_count: 1 + delete_count: 5 + records: + - {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600} + - {type: NS, name: "@", data: ns1.example.com, ttl: 300} + + - id: apexcname_no_delete_other_hosts + description: "APEXCNAME only deletes records at @, not at other hosts" + input: + zone_records: + - {type: A, name: "@", data: 1.2.3.4, ttl: 300} + - {type: A, name: "sub", data: 5.6.7.8, ttl: 300} + template_records: + - {type: APEXCNAME, pointsTo: target.example.com, ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + new_count: 1 + delete_count: 1 + records: + - {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600} + - {type: A, name: "sub", data: 5.6.7.8, ttl: 300} + + - id: apexcname_conflict_itself + description: "Two APEXCNAME records in same template raise InvalidData" + input: + zone_records: [] + template_records: + - {type: APEXCNAME, pointsTo: target1.example.com, ttl: 600} + - {type: APEXCNAME, pointsTo: target2.example.com, ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + exception: InvalidData + + - id: apexcname_variable_substitution + description: "APEXCNAME supports %variable% substitution in pointsTo" + input: + zone_records: [] + template_records: + - {type: APEXCNAME, pointsTo: "%target%", ttl: 600} + domain: foo.com + host: "" + params: {target: target.example.com} + expect: + new_count: 1 + delete_count: 0 + records: + - {type: APEXCNAME, name: "@", data: target.example.com, ttl: 600} diff --git a/test/test_definitions/process_records_redir_tests.yaml b/test/test_definitions/process_records_redir_tests.yaml new file mode 100644 index 0000000..ff4db79 --- /dev/null +++ b/test/test_definitions/process_records_redir_tests.yaml @@ -0,0 +1,146 @@ +version: "1.0" +suite_type: process_records +description: "Domain Connect process_records compliance tests — REDIR301/REDIR302" + +tests: + + # --------------------------------------------------------------------------- + # REDIR + # --------------------------------------------------------------------------- + - id: redir301_basic + description: "REDIR301 replaces A/AAAA/CNAME at host and adds redirect record" + input: + zone_records: + - {type: A, name: bar, data: abc, ttl: 400} + - {type: AAAA, name: bar, data: abc, ttl: 400} + - {type: CNAME, name: bar, data: abc, ttl: 400} + - {type: A, name: random.value, data: abc, ttl: 400} + template_records: + - {type: REDIR301, host: "@", target: "http://%target%"} + domain: foo.com + host: bar + params: {target: example.com} + redirect_records: + - {type: A, pointsTo: "127.0.0.1", ttl: 600} + - {type: AAAA, pointsTo: "::1", ttl: 600} + expect: + new_count: 3 + delete_count: 3 + records: + - {type: A, name: bar, data: "127.0.0.1", ttl: 600} + - {type: AAAA, name: bar, data: "::1", ttl: 600} + - {type: A, name: random.value, data: abc, ttl: 400} + - {type: REDIR301, name: bar, data: "http://example.com"} + + - id: redir301_double + description: "Two REDIR301 records at different hosts both get redirect records" + input: + zone_records: [] + template_records: + - {type: REDIR301, host: www, target: "http://%target%"} + - {type: REDIR301, host: "@", target: "http://www.%fqdn%"} + domain: foo.com + host: "" + params: {target: example.com} + redirect_records: + - {type: A, pointsTo: "127.0.0.1", ttl: 600} + - {type: AAAA, pointsTo: "::1", ttl: 600} + expect: + new_count: 6 + delete_count: 0 + records: + - {type: A, name: "@", data: "127.0.0.1", ttl: 600} + - {type: AAAA, name: "@", data: "::1", ttl: 600} + - {type: REDIR301, name: "@", data: "http://www.foo.com"} + - {type: A, name: www, data: "127.0.0.1", ttl: 600} + - {type: AAAA, name: www, data: "::1", ttl: 600} + - {type: REDIR301, name: www, data: "http://example.com"} + + - id: redir301_with_groupid_filtered_out + description: "REDIR301 filtered out by group_ids does nothing" + input: + zone_records: + - {type: A, name: bar, data: abc, ttl: 400} + - {type: A, name: random.value, data: abc, ttl: 400} + template_records: + - {type: REDIR301, host: "@", target: "http://example.com", groupId: b} + domain: foo.com + host: bar + params: {} + group_ids: [a] + redirect_records: + - {type: A, pointsTo: "127.0.0.1", ttl: 600} + - {type: AAAA, pointsTo: "::1", ttl: 600} + expect: + new_count: 0 + delete_count: 0 + records: + - {type: A, name: bar, data: abc, ttl: 400} + - {type: A, name: random.value, data: abc, ttl: 400} + + - id: redir302_basic + description: "REDIR302 replaces A/AAAA/CNAME at host and adds redirect record" + input: + zone_records: + - {type: A, name: bar, data: abc, ttl: 400} + - {type: AAAA, name: bar, data: abc, ttl: 400} + - {type: CNAME, name: bar, data: abc, ttl: 400} + - {type: A, name: random.value, data: abc, ttl: 400} + template_records: + - {type: REDIR302, host: "@", target: "http://example.com"} + domain: foo.com + host: bar + params: {} + redirect_records: + - {type: A, pointsTo: "127.0.0.1", ttl: 600} + - {type: AAAA, pointsTo: "::1", ttl: 600} + expect: + new_count: 3 + delete_count: 3 + records: + - {type: A, name: bar, data: "127.0.0.1", ttl: 600} + - {type: AAAA, name: bar, data: "::1", ttl: 600} + - {type: A, name: random.value, data: abc, ttl: 400} + - {type: REDIR302, name: bar, data: "http://example.com"} + + - id: exception_redir301_empty_target + description: "REDIR301 with empty target raises InvalidData" + input: + zone_records: [] + template_records: + - {type: REDIR301, host: "@", target: "", ttl: 600} + domain: foo.com + host: "" + params: {} + redirect_records: + - {type: A, pointsTo: "127.0.0.1", ttl: 600} + - {type: AAAA, pointsTo: "::1", ttl: 600} + expect: + exception: InvalidData + + - id: exception_redir302_invalid_target + description: "REDIR302 with an invalid URL target raises InvalidData" + input: + zone_records: [] + template_records: + - {type: REDIR302, host: "@", target: "http://ijfjiör@@@a:43244434::", ttl: 600} + domain: foo.com + host: "" + params: {} + redirect_records: + - {type: A, pointsTo: "127.0.0.1", ttl: 600} + - {type: AAAA, pointsTo: "::1", ttl: 600} + expect: + exception: InvalidData + + - id: exception_redir301_missing_redirect_records + description: "REDIR301 without redirect_records raises InvalidTemplate" + input: + zone_records: [] + template_records: + - {type: REDIR301, host: "@", target: "", ttl: 600} + domain: foo.com + host: "" + params: {} + expect: + exception: InvalidTemplate diff --git a/test/test_definitions/process_records_tests.yaml b/test/test_definitions/process_records_tests.yaml index 490e8c1..e12bce3 100644 --- a/test/test_definitions/process_records_tests.yaml +++ b/test/test_definitions/process_records_tests.yaml @@ -1391,105 +1391,6 @@ tests: - {type: A, name: bar, data: "127.0.0.1", ttl: 600} - {type: TXT, name: bar, data: testdata, ttl: 600} - # --------------------------------------------------------------------------- - # REDIR - # --------------------------------------------------------------------------- - - id: redir301_basic - description: "REDIR301 replaces A/AAAA/CNAME at host and adds redirect record" - input: - zone_records: - - {type: A, name: bar, data: abc, ttl: 400} - - {type: AAAA, name: bar, data: abc, ttl: 400} - - {type: CNAME, name: bar, data: abc, ttl: 400} - - {type: A, name: random.value, data: abc, ttl: 400} - template_records: - - {type: REDIR301, host: "@", target: "http://%target%"} - domain: foo.com - host: bar - params: {target: example.com} - redirect_records: - - {type: A, pointsTo: "127.0.0.1", ttl: 600} - - {type: AAAA, pointsTo: "::1", ttl: 600} - expect: - new_count: 3 - delete_count: 3 - records: - - {type: A, name: bar, data: "127.0.0.1", ttl: 600} - - {type: AAAA, name: bar, data: "::1", ttl: 600} - - {type: A, name: random.value, data: abc, ttl: 400} - - {type: REDIR301, name: bar, data: "http://example.com"} - - - id: redir301_double - description: "Two REDIR301 records at different hosts both get redirect records" - input: - zone_records: [] - template_records: - - {type: REDIR301, host: www, target: "http://%target%"} - - {type: REDIR301, host: "@", target: "http://www.%fqdn%"} - domain: foo.com - host: "" - params: {target: example.com} - redirect_records: - - {type: A, pointsTo: "127.0.0.1", ttl: 600} - - {type: AAAA, pointsTo: "::1", ttl: 600} - expect: - new_count: 6 - delete_count: 0 - records: - - {type: A, name: "@", data: "127.0.0.1", ttl: 600} - - {type: AAAA, name: "@", data: "::1", ttl: 600} - - {type: REDIR301, name: "@", data: "http://www.foo.com"} - - {type: A, name: www, data: "127.0.0.1", ttl: 600} - - {type: AAAA, name: www, data: "::1", ttl: 600} - - {type: REDIR301, name: www, data: "http://example.com"} - - - id: redir301_with_groupid_filtered_out - description: "REDIR301 filtered out by group_ids does nothing" - input: - zone_records: - - {type: A, name: bar, data: abc, ttl: 400} - - {type: A, name: random.value, data: abc, ttl: 400} - template_records: - - {type: REDIR301, host: "@", target: "http://example.com", groupId: b} - domain: foo.com - host: bar - params: {} - group_ids: [a] - redirect_records: - - {type: A, pointsTo: "127.0.0.1", ttl: 600} - - {type: AAAA, pointsTo: "::1", ttl: 600} - expect: - new_count: 0 - delete_count: 0 - records: - - {type: A, name: bar, data: abc, ttl: 400} - - {type: A, name: random.value, data: abc, ttl: 400} - - - id: redir302_basic - description: "REDIR302 replaces A/AAAA/CNAME at host and adds redirect record" - input: - zone_records: - - {type: A, name: bar, data: abc, ttl: 400} - - {type: AAAA, name: bar, data: abc, ttl: 400} - - {type: CNAME, name: bar, data: abc, ttl: 400} - - {type: A, name: random.value, data: abc, ttl: 400} - template_records: - - {type: REDIR302, host: "@", target: "http://example.com"} - domain: foo.com - host: bar - params: {} - redirect_records: - - {type: A, pointsTo: "127.0.0.1", ttl: 600} - - {type: AAAA, pointsTo: "::1", ttl: 600} - expect: - new_count: 3 - delete_count: 3 - records: - - {type: A, name: bar, data: "127.0.0.1", ttl: 600} - - {type: AAAA, name: bar, data: "::1", ttl: 600} - - {type: A, name: random.value, data: abc, ttl: 400} - - {type: REDIR302, name: bar, data: "http://example.com"} - # --------------------------------------------------------------------------- # Multi-aware / multi-instance # --------------------------------------------------------------------------- @@ -1912,48 +1813,6 @@ tests: expect: exception: InvalidData - - id: exception_redir301_empty_target - description: "REDIR301 with empty target raises InvalidData" - input: - zone_records: [] - template_records: - - {type: REDIR301, host: "@", target: "", ttl: 600} - domain: foo.com - host: "" - params: {} - redirect_records: - - {type: A, pointsTo: "127.0.0.1", ttl: 600} - - {type: AAAA, pointsTo: "::1", ttl: 600} - expect: - exception: InvalidData - - - id: exception_redir302_invalid_target - description: "REDIR302 with an invalid URL target raises InvalidData" - input: - zone_records: [] - template_records: - - {type: REDIR302, host: "@", target: "http://ijfjiör@@@a:43244434::", ttl: 600} - domain: foo.com - host: "" - params: {} - redirect_records: - - {type: A, pointsTo: "127.0.0.1", ttl: 600} - - {type: AAAA, pointsTo: "::1", ttl: 600} - expect: - exception: InvalidData - - - id: exception_redir301_missing_redirect_records - description: "REDIR301 without redirect_records raises InvalidTemplate" - input: - zone_records: [] - template_records: - - {type: REDIR301, host: "@", target: "", ttl: 600} - domain: foo.com - host: "" - params: {} - expect: - exception: InvalidTemplate - - id: exception_invalid_record_type description: "An entirely invalid record type string raises TypeError" input: @@ -4439,3 +4298,4 @@ tests: delete_count: 2 records: - {type: A, name: "www", data: "5.6.7.8", ttl: 300} +