From 20608cf343b0dd54139d266de614e7ee39852f8b Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Wed, 27 Sep 2023 16:06:44 -0300 Subject: [PATCH] ipadnszone: Add support for per-zone privilege delegation IPA DNS Zones management can be delegated by adding a "Manage DNS zone" permission. The CLI commands that manage these permissions are dnszone-add-delegation and dnszone-remove-delegation. The ansible-freeipa module ipadnszone did not have this capability, and it now support dnszone per-zone management delegation by setting the module parameter 'permission'. If set to 'true' the permission will be assigned to the zone, if set to false the permission will be removed. --- README-dnszone.md | 21 ++++++++- plugins/modules/ipadnszone.py | 33 +++++++++++-- tests/dnszone/test_dnszone.yml | 86 +++++++++++++--------------------- 3 files changed, 80 insertions(+), 60 deletions(-) diff --git a/README-dnszone.md b/README-dnszone.md index 607e77bc21..e643096cdd 100644 --- a/README-dnszone.md +++ b/README-dnszone.md @@ -133,6 +133,22 @@ Example playbook to enable a zone: state: enabled ``` +Example playbook to allow per-zone privilege delegation: + +``` yaml +--- +- name: Playbook to enable per-zone privilege delegation + hosts: ipaserver + become: true + + tasks: + - name: Enable privilege delegation. + ipadnszone: + ipaadmin_password: SomeADMINpassword + name: testzone.local + permission: true +``` + Example playbook to remove a zone: ```yaml @@ -223,6 +239,7 @@ Variable | Description | Required `ttl`| Time to live for records at zone apex | no `default_ttl`| Time to live for records without explicit TTL definition | no `nsec3param_rec`| NSEC3PARAM record for zone in format: hash_algorithm flags iterations salt | no +`permission` | Set per-zone access delegation permission. | no `skip_overlap_check`| Force DNS zone creation even if it will overlap with an existing zone | no `skip_nameserver_check` | Force DNS zone creation even if nameserver is not resolvable | no @@ -238,4 +255,6 @@ Variable | Description | Returned When Authors ======= -Sergio Oliveira Campos +- Sergio Oliveira Campos +- Thomas Woerner +- Rafael Jeffman diff --git a/plugins/modules/ipadnszone.py b/plugins/modules/ipadnszone.py index a44e8cfb83..cead7d84e8 100644 --- a/plugins/modules/ipadnszone.py +++ b/plugins/modules/ipadnszone.py @@ -142,6 +142,10 @@ salt. required: false type: str + permission: + description: Set per-zone access delegation permission. + required: false + type: bool skip_overlap_check: description: | Force DNS zone creation even if it will overlap with an existing zone @@ -154,6 +158,7 @@ author: - Sergio Oliveira Campos (@seocam) - Thomas Woerner (@t-woerner) + - Rafael Jeffman (@rjeffman) """ # noqa: E501 EXAMPLES = """ @@ -253,6 +258,7 @@ def __init__(self, *args, **kwargs): "idnsallowdynupdate": "dynamic_update", "idnssecinlinesigning": "dnssec", "idnsupdatepolicy": "update_policy", + "managedby": "permission", # Mapping by method "idnsforwarders": self.get_ipa_idnsforwarders, "idnsallowtransfer": self.get_ipa_idnsallowtransfer, @@ -434,7 +440,7 @@ def get_zone(self, zone_name): is_zone_active = False else: zone = response["result"] - # FreeIPA 4.9.10+ and 4.10 use proper mapping for boolean vaalues. + # FreeIPA 4.9.10+ and 4.10 use proper mapping for boolean values. # See: https://github.com/freeipa/freeipa/pull/6294 is_zone_active = ( str(zone.get("idnszoneactive")[0]).upper() == "TRUE" @@ -462,18 +468,24 @@ def check_ipa_params(self): self.fail_json( msg="Either `name` or `name_from_ip` must be provided." ) + # check invalid parameters + invalid = [] if self.ipa_params.state != "present": - invalid = ["name_from_ip"] - - self.params_fail_used_invalid(invalid, self.ipa_params.state) + invalid .extend(["name_from_ip"]) + if self.ipa_params.state == "absent": + invalid.extend(["permission"]) + self.params_fail_used_invalid(invalid, self.ipa_params.state) def define_ipa_commands(self): for zone_name in self.get_zone_names(): # Look for existing zone in IPA zone, is_zone_active = self.get_zone(zone_name) - args = self.ipa_params.get_ipa_command_args(zone=zone) if self.ipa_params.state in ["present", "enabled", "disabled"]: + args = self.ipa_params.get_ipa_command_args(zone=zone) + # We'll handle "managedby" after dnszone add/mod. + args.pop("managedby", None) + if not zone: # Since the zone doesn't exist we just create it # with given args @@ -487,6 +499,16 @@ def define_ipa_commands(self): if not compare_args_ipa(self, args, zone): self.commands.append((zone_name, "dnszone_mod", args)) + # Permissions must be set on existing zones. + if self.ipa_params.permission is not None: + is_managed = zone.get("managedby") + if self.ipa_params.permission and not is_managed: + self.commands.append( + (zone_name, "dnszone_add_permission", {})) + if not self.ipa_params.permission and is_managed: + self.commands.append( + (zone_name, "dnszone_remove_permission", {})) + if self.ipa_params.state == "enabled" and not is_zone_active: self.commands.append((zone_name, "dnszone_enable", {})) @@ -555,6 +577,7 @@ def get_argument_spec(): ttl=dict(type="int", required=False, default=None), default_ttl=dict(type="int", required=False, default=None), nsec3param_rec=dict(type="str", required=False, default=None), + permission=dict(type="bool", required=False, default=None), skip_nameserver_check=dict(type="bool", required=False, default=None), skip_overlap_check=dict(type="bool", required=False, default=None), ) diff --git a/tests/dnszone/test_dnszone.yml b/tests/dnszone/test_dnszone.yml index 0491e26344..3d74d43afd 100644 --- a/tests/dnszone/test_dnszone.yml +++ b/tests/dnszone/test_dnszone.yml @@ -3,6 +3,10 @@ hosts: "{{ ipa_test_host | default('ipaserver') }}" become: true gather_facts: true + module_defaults: + ipadnszone: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" tasks: @@ -13,8 +17,6 @@ # Tests - name: Check if zone is present, when it shouldn't be. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: present check_mode: yes @@ -23,8 +25,6 @@ - name: Check if zone is present again, when it shouldn't be. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: present check_mode: yes @@ -33,8 +33,6 @@ - name: Ensure zone is present. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: present register: result @@ -42,8 +40,6 @@ - name: Check if zone is present, when it should be. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: present check_mode: yes @@ -52,8 +48,6 @@ - name: Ensure zone is present, again. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: present register: result @@ -61,8 +55,6 @@ - name: Ensure zone is disabled. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: disabled register: result @@ -70,8 +62,6 @@ - name: Ensure zone is disabled, again. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: disabled register: result @@ -79,8 +69,6 @@ - name: Ensure zone is enabled. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: enabled register: result @@ -88,8 +76,6 @@ - name: Ensure zone is enabled, again. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local state: enabled register: result @@ -97,8 +83,6 @@ - name: Ensure forward_policy is none. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forward_policy: none register: result @@ -106,8 +90,6 @@ - name: Ensure forward_policy is none, again. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forward_policy: none register: result @@ -115,8 +97,6 @@ - name: Ensure forward_policy is first. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forward_policy: first register: result @@ -124,8 +104,6 @@ - name: Ensure forward_policy is first, again. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forward_policy: first register: result @@ -133,8 +111,6 @@ - name: Ensure first forwarder is set. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forwarders: - ip_address: 8.8.8.8 @@ -144,8 +120,6 @@ - name: Ensure first and second forwarder are set. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forwarders: - ip_address: 8.8.8.8 @@ -156,8 +130,6 @@ - name: Ensure first and second forwarder are set, again. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forwarders: - ip_address: 8.8.8.8 @@ -168,8 +140,6 @@ - name: Ensure only second forwarder is set. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forwarders: - ip_address: 2001:4860:4860::8888 @@ -178,16 +148,12 @@ - name: Nothing changes. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local register: result failed_when: result.changed or result.failed - name: Ensure no forwarders are set. ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: testzone.local forwarders: [] register: result @@ -195,56 +161,70 @@ - name: Create zones test1 ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: test1.testzone.local register: result failed_when: not result.changed or result.failed - name: Create zones test1, again ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: test1.testzone.local register: result failed_when: result.changed or result.failed - name: Create zones test2 ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: test2.testzone.local register: result failed_when: not result.changed or result.failed - name: Create zones test2, again ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: test2.testzone.local register: result failed_when: result.changed or result.failed - name: Create zones test3 ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: test3.testzone.local register: result failed_when: not result.changed or result.failed - name: Create zones test3, again ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: test3.testzone.local register: result failed_when: result.changed or result.failed + - name: Ensure zone test1.testzone.local has management permissioon + ipadnszone: + name: test1.testzone.local + permission: true + register: result + failed_when: not result.changed or result.failed + + - name: Ensure zone test1.testzone.local has management permissioon + ipadnszone: + name: test1.testzone.local + permission: true + register: result + failed_when: result.changed or result.failed + + - name: Ensure zone test1.testzone.local don't have management permissioon + ipadnszone: + name: test1.testzone.local + permission: false + register: result + failed_when: not result.changed or result.failed + + - name: Ensure zone test1.testzone.local don't have management permissioon + ipadnszone: + name: test1.testzone.local + permission: false + register: result + failed_when: result.changed or result.failed + - name: Ensure multiple zones are absent ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: - test1.testzone.local - test2.testzone.local @@ -255,8 +235,6 @@ - name: Ensure multiple zones are absent, again ipadnszone: - ipaadmin_password: SomeADMINpassword - ipaapi_context: "{{ ipa_context | default(omit) }}" name: - test1.testzone.local - test2.testzone.local