From d859ddc7fea047fab0f69817a18083bb2b5a43ff Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman <rjeffman@redhat.com> Date: Mon, 26 Sep 2022 16:09:15 -0300 Subject: [PATCH] sudorule: Add support for 'hostmask' parameter The hostmask parameter allows matching a sudorule against a network address, and was missing from ipasudorule module. Documentation and tests were updated to reflect changes. Two new example playbooks are available: playbooks/sudorule/ensure-sudorule-hostmask-member-is-absent.yml playbooks/sudorule/ensure-sudorule-hostmask-member-is-present.yml --- README-sudorule.md | 1 + ...ure-sudorule-hostmask-member-is-absent.yml | 14 +++ ...re-sudorule-hostmask-member-is-present.yml | 13 ++ plugins/modules/ipasudorule.py | 63 +++++++--- tests/sudorule/test_sudorule.yml | 112 ++++++++++++++++++ 5 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 playbooks/sudorule/ensure-sudorule-hostmask-member-is-absent.yml create mode 100644 playbooks/sudorule/ensure-sudorule-hostmask-member-is-present.yml diff --git a/README-sudorule.md b/README-sudorule.md index 089adab2..bd23c178 100644 --- a/README-sudorule.md +++ b/README-sudorule.md @@ -129,6 +129,7 @@ Variable | Description | Required `nomembers` | Suppress processing of membership attributes. (bool) | no `host` | List of host name strings assigned to this sudorule. | no `hostgroup` | List of host group name strings assigned to this sudorule. | no +`hostmask` | List of host masks of allowed hosts | no `user` | List of user name strings assigned to this sudorule. | no `group` | List of user group name strings assigned to this sudorule. | no `allow_sudocmd` | List of sudocmd name strings assigned to the allow group of this sudorule. | no diff --git a/playbooks/sudorule/ensure-sudorule-hostmask-member-is-absent.yml b/playbooks/sudorule/ensure-sudorule-hostmask-member-is-absent.yml new file mode 100644 index 00000000..32f8bc36 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-hostmask-member-is-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to manage sudorule + hosts: ipaserver + become: no + gather_facts: no + + tasks: + - name: Ensure hostmask network is absent in sudorule + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule1 + hostmask: 192.168.122.37/24 + action: member + state: absent diff --git a/playbooks/sudorule/ensure-sudorule-hostmask-member-is-present.yml b/playbooks/sudorule/ensure-sudorule-hostmask-member-is-present.yml new file mode 100644 index 00000000..51cb968d --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-hostmask-member-is-present.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to manage sudorule + hosts: ipaserver + become: no + gather_facts: no + + tasks: + - name: Ensure hostmask network is present in sudorule + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule1 + hostmask: 192.168.122.37/24 + action: member diff --git a/plugins/modules/ipasudorule.py b/plugins/modules/ipasudorule.py index bd00ae1d..2be49c22 100644 --- a/plugins/modules/ipasudorule.py +++ b/plugins/modules/ipasudorule.py @@ -143,6 +143,11 @@ options: required: false type: list elements: str + hostmask: + description: Host masks of allowed hosts. + required: false + type: list + elements: str action: description: Work on sudorule or member level type: str @@ -202,6 +207,15 @@ EXAMPLES = """ hostcategory: all state: enabled +# Ensure sudo rule applies for hosts with hostmasks +- ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule1 + hostmask: + - 192.168.122.1/24 + - 192.168.120.1/24 + action: member + # Ensure Sudo Rule tesrule1 is absent - ipasudorule: ipaadmin_password: SomeADMINpassword @@ -214,7 +228,7 @@ RETURN = """ from ansible.module_utils.ansible_freeipa_module import \ IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, gen_add_list, \ - gen_intersection_list, api_get_domain, ensure_fqdn + gen_intersection_list, api_get_domain, ensure_fqdn, netaddr, to_text def find_sudorule(module, name): @@ -275,6 +289,8 @@ def main(): default=None), hostgroup=dict(required=False, type='list', elements="str", default=None), + hostmask=dict(required=False, type='list', elements="str", + default=None), user=dict(required=False, type='list', elements="str", default=None), group=dict(required=False, type='list', elements="str", @@ -334,6 +350,7 @@ def main(): nomembers = ansible_module.params_get("nomembers") # noqa host = ansible_module.params_get("host") hostgroup = ansible_module.params_get_lowercase("hostgroup") + hostmask = ansible_module.params_get("hostmask") user = ansible_module.params_get_lowercase("user") group = ansible_module.params_get_lowercase("group") allow_sudocmd = ansible_module.params_get('allow_sudocmd') @@ -351,6 +368,10 @@ def main(): # state state = ansible_module.params_get("state") + # ensure hostmasks are network cidr + if hostmask is not None: + hostmask = [to_text(netaddr.IPNetwork(x).cidr) for x in hostmask] + # Check parameters invalid = [] @@ -382,7 +403,7 @@ def main(): "cmdcategory", "runasusercategory", "runasgroupcategory", "nomembers", "order"] if action == "sudorule": - invalid.extend(["host", "hostgroup", "user", "group", + invalid.extend(["host", "hostgroup", "hostmask", "user", "group", "runasuser", "runasgroup", "allow_sudocmd", "allow_sudocmdgroup", "deny_sudocmd", "deny_sudocmdgroup", "sudooption"]) @@ -396,7 +417,7 @@ def main(): "disabled") invalid = ["description", "usercategory", "hostcategory", "cmdcategory", "runasusercategory", "runasgroupcategory", - "nomembers", "nomembers", "host", "hostgroup", + "nomembers", "nomembers", "host", "hostgroup", "hostmask", "user", "group", "allow_sudocmd", "allow_sudocmdgroup", "deny_sudocmd", "deny_sudocmdgroup", "runasuser", "runasgroup", "order", "sudooption"] @@ -425,6 +446,7 @@ def main(): user_add, user_del = [], [] group_add, group_del = [], [] hostgroup_add, hostgroup_del = [], [] + hostmask_add, hostmask_del = [], [] allow_cmd_add, allow_cmd_del = [], [] allow_cmdgroup_add, allow_cmdgroup_del = [], [] deny_cmd_add, deny_cmd_del = [], [] @@ -490,6 +512,9 @@ def main(): hostgroup_add, hostgroup_del = gen_add_del_lists( hostgroup, res_find.get('memberhost_hostgroup', [])) + hostmask_add, hostmask_del = gen_add_del_lists( + hostmask, res_find.get('hostmask', [])) + user_add, user_del = gen_add_del_lists( user, res_find.get('memberuser_user', [])) @@ -556,6 +581,9 @@ def main(): if hostgroup is not None: hostgroup_add = gen_add_list( hostgroup, res_find.get("memberhost_hostgroup")) + if hostmask is not None: + hostmask_add = gen_add_list( + hostmask, res_find.get("hostmask")) if user is not None: user_add = gen_add_list( user, res_find.get("memberuser_user")) @@ -628,6 +656,10 @@ def main(): hostgroup_del = gen_intersection_list( hostgroup, res_find.get("memberhost_hostgroup")) + if hostmask is not None: + hostmask_del = gen_intersection_list( + hostmask, res_find.get("hostmask")) + if user is not None: user_del = gen_intersection_list( user, res_find.get("memberuser_user")) @@ -719,18 +751,19 @@ def main(): # Manage members. # Manage hosts and hostgroups - if host_add or hostgroup_add: - commands.append([name, "sudorule_add_host", - { - "host": host_add, - "hostgroup": hostgroup_add, - }]) - if host_del or hostgroup_del: - commands.append([name, "sudorule_remove_host", - { - "host": host_del, - "hostgroup": hostgroup_del, - }]) + if any([host_add, hostgroup_add, hostmask_add]): + params = {"host": host_add, "hostgroup": hostgroup_add} + # An empty Hostmask cannot be used, or IPA API will fail. + if hostmask_add: + params["hostmask"] = hostmask_add + commands.append([name, "sudorule_add_host", params]) + + if any([host_del, hostgroup_del, hostmask_del]): + params = {"host": host_del, "hostgroup": hostgroup_del} + # An empty Hostmask cannot be used, or IPA API will fail. + if hostmask_del: + params["hostmask"] = hostmask_del + commands.append([name, "sudorule_remove_host", params]) # Manage users and groups if user_add or group_add: diff --git a/tests/sudorule/test_sudorule.yml b/tests/sudorule/test_sudorule.yml index 0ba8d8fe..622438cd 100644 --- a/tests/sudorule/test_sudorule.yml +++ b/tests/sudorule/test_sudorule.yml @@ -83,6 +83,7 @@ ipaapi_context: "{{ ipa_context | default(omit) }}" name: - test_upstream_issue_664 + - testrule_hostmask - testrule1 - allusers - allhosts @@ -1005,6 +1006,116 @@ register: result failed_when: not result.changed or result.failed + - name: Ensure sudorule is present with hostmask + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule_hostmask + hostmask: + - 192.168.122.1/24 + - 192.168.120.1/24 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure sudorule is present with hostmask, again + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule_hostmask + hostmask: + - 192.168.122.1/24 + - 192.168.120.1/24 + register: result + failed_when: result.changed or result.failed + + - name: Ensure sudorule hostmask member is absent + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule_hostmask + hostmask: 192.168.122.0/24 + action: member + state: absent + register: result + failed_when: not result.changed or result.failed + + - name: Ensure sudorule hostmask member is absent, again + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule_hostmask + hostmask: 192.168.122.0/24 + action: member + state: absent + register: result + failed_when: result.changed or result.failed + + - name: Ensure sudorule is present with another hostmask + ipasudorule: + ipaadmin_password: SomeADMINpassword + name: testrule_hostmask + hostmask: 192.168.122.0/24 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure sudorule is present with another hostmask, again + ipasudorule: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testrule_hostmask + hostmask: 192.168.122.0/24 + register: result + failed_when: result.changed + + - name: Check sudorule with hostmask is absent + ipasudorule: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testrule_hostmask + hostmask: 192.168.120.0/24 + action: member + register: result + check_mode: yes + failed_when: not result.changed or result.failed + + - name: Ensure sudorule hostmask member is present + ipasudorule: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testrule_hostmask + hostmask: 192.168.120.0/24 + action: member + register: result + failed_when: not result.changed or result.failed + + - name: Ensure sudorule hostmask member is present, again + ipasudorule: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testrule_hostmask + hostmask: 192.168.120.0/24 + action: member + register: result + failed_when: result.changed or result.failed + + - name: Ensure sudorule hostmask member is absent + ipasudorule: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testrule_hostmask + hostmask: 192.168.120.0/24 + action: member + state: absent + register: result + failed_when: not result.changed or result.failed + + - name: Ensure sudorule hostmask member is absent, again + ipasudorule: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testrule_hostmask + hostmask: 192.168.120.0/24 + action: member + state: absent + register: result + failed_when: result.changed or result.failed + # cleanup - name: Ensure sudocmdgroup is absent ipasudocmdgroup: @@ -1013,6 +1124,7 @@ name: - test_sudorule - test_sudorule2 + - testrule_hostmask state: absent - name: Ensure sudocmds are absent -- GitLab