From 3a79a44d9ff0717be56e63322bef37d3cccb31f0 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Thu, 25 Aug 2022 21:10:34 +0200 Subject: [PATCH] ipagroup: Implement state:quers using IPAAnsibleModule.execute_query The query_param parameter has been added, together with the dict query_param_settings. The existing find_group function has been transformed into user_show to get the result for a single user and new convert_result and user_find function have been added. --- .../module_utils/ansible_freeipa_module.py | 10 +- plugins/modules/ipagroup.py | 138 ++++++++-- plugins/modules/ipauser.py | 8 +- tests/group/test_group_query.yml | 252 ++++++++++++++++++ 4 files changed, 379 insertions(+), 29 deletions(-) create mode 100644 tests/group/test_group_query.yml diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 377d44fa9..ce9b74923 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -1358,7 +1358,7 @@ def exception_handler(module, ex, exit_args, one_name): return changed def execute_query(self, names, prefix, name_ipa_param, - query_param, query_command, query_param_settings): + query_param, find_command, query_param_settings): """ Execute query state. Parameters @@ -1380,7 +1380,7 @@ def execute_query(self, names, prefix, name_ipa_param, mapping of the default module paramter name to IPA option name if it is not the same. Example: "uid" for user name of the user commands. - query_command: The Query function + find_command: The find function This is a module function that returns the structure(s) from the show or find command. """ @@ -1419,13 +1419,15 @@ def store_params(exit_args, name, prefix, name_ipa_param, result, if names and isinstance(names, list): with_name = len(names) > 1 for name in names: - result = query_command(self, name) + result = find_command(self, name, + pkey_only=(query_param is None)) if result: store_params(exit_args, name if with_name else None, prefix, name_ipa_param, result, query_param) else: - results = query_command(self, None) + results = find_command(self, None, + pkey_only=(query_param is None)) if results is not None: for result in results: name = result[name_ipa_param] diff --git a/plugins/modules/ipagroup.py b/plugins/modules/ipagroup.py index b80061663..b41fbac0b 100644 --- a/plugins/modules/ipagroup.py +++ b/plugins/modules/ipagroup.py @@ -193,6 +193,9 @@ required: false type: list elements: str + query_param: + description: The fields to query with state=query + required: false action: description: Work on group or member level type: str @@ -202,7 +205,8 @@ description: State to ensure type: str default: present - choices: ["present", "absent"] + choices: ["present", "absent", + "query"] author: - Thomas Woerner (@t-woerner) """ @@ -310,7 +314,7 @@ from ansible.module_utils._text import to_text from ansible.module_utils.ansible_freeipa_module import \ IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, \ - gen_add_list, gen_intersection_list, api_check_param + gen_add_list, gen_intersection_list, api_check_param, ipalib_errors from ansible.module_utils import six if six.PY3: unicode = str @@ -327,28 +331,67 @@ "deepcopy" in baseldap.LDAPObject.__json__.__code__.co_names -def find_group(module, name): +def group_show(module, name): _args = { "all": True, - "cn": name, } - _result = module.ipa_command("group_find", name, _args) + try: + _result = module.ipa_command("group_show", name, _args).get("result") + except ipalib_errors.NotFound: + return None + + # The returned services are of type ipapython.kerberos.Principal, + # also services are not case sensitive. Therefore services are + # converted to lowercase strings to be able to do the comparison. + if "member_service" in _result: + _result["member_service"] = \ + [to_text(svc).lower() for svc in _result["member_service"]] + return _result + + +def convert_result(res): + _res = {} + for key in res: + if isinstance(res[key], list): + # All single value parameters should not be lists + # This does not apply to manager, krbprincipalname, + # usercertificate and ipacertmapdata + if len(res[key]) == 1: + _res[key] = to_text(res[key][0]) + else: + _res[key] = [to_text(item) for item in res[key]] + elif key in ["gidNumber"]: + _res[key] = int(res[key]) + else: + _res[key] = to_text(res[key]) + return _res + + +def group_find(module, name, pkey_only=False, sizelimit=None, timelimit=None): + _args = {"all": True} + + if name: + _args["cn"] = name + if pkey_only: + _args["pkey_only"] = True + if sizelimit is not None: + _args["sizelimit"] = sizelimit + if timelimit is not None: + _args["timelimit"] = timelimit + + try: + _result = module.ipa_command_no_name("group_find", _args).get("result") + if _result: + if name: + _result = convert_result(_result[0]) + else: + _result = [convert_result(res) for res in _result] - if len(_result["result"]) > 1: - module.fail_json( - msg="There is more than one group '%s'" % (name)) - elif len(_result["result"]) == 1: - _res = _result["result"][0] - # The returned services are of type ipapython.kerberos.Principal, - # also services are not case sensitive. Therefore services are - # converted to lowercase strings to be able to do the comparison. - if "member_service" in _res: - _res["member_service"] = \ - [to_text(svc).lower() for svc in _res["member_service"]] - return _res + except ipalib_errors.NotFound: + return None - return None + return _result def gen_args(description, gid, nomembers): @@ -392,6 +435,11 @@ def check_parameters(module, state, action): if action == "group": invalid.extend(["user", "group", "service", "externalmember"]) + if state == "query": + invalid.append("groups") + else: + invalid.append("query_param") + module.params_fail_used_invalid(invalid, state, action) @@ -422,6 +470,31 @@ def check_objectclass_args(module, res_find, posix, external): "`non-posix`.") +query_param_settings = { + "ALL": [ + "dn", "objectclass", "ipauniqueid", "ipantsecurityidentifier", + "name", + "description", + "gid", + "nomembers", + "user", "group", "service", "external", + "idoverrideuser" + ], + "BASE": [ + "name", "description", "gid" + ], + "mapping": { + "name": "cn", + "gid": "gidnumber", + "user": "member_user", + "group": "member_group", + "service": "member_service", + "externalmember": "member_external", + "idoverrideuser": "member_idoverrideuser", + } +} + + def main(): group_spec = dict( # present @@ -466,11 +539,16 @@ def main(): ), elements='dict', required=False), + # query + query_param=dict(type="list", default=None, + choices=["ALL", "BASE"].extend( + query_param_settings["ALL"]), + required=False), # general action=dict(type="str", default="group", choices=["member", "group"]), state=dict(type="str", default="present", - choices=["present", "absent"]), + choices=["present", "absent", "query"]), # Add group specific parameters for simple use case **group_spec @@ -479,7 +557,7 @@ def main(): # same time mutually_exclusive=[['posix', 'nonposix', 'external'], ["name", "groups"]], - required_one_of=[["name", "groups"]], + # required_one_of=[["name", "groups"]] is handled below supports_check_mode=True, ) @@ -506,13 +584,17 @@ def main(): membermanager_user = ansible_module.params_get("membermanager_user") membermanager_group = ansible_module.params_get("membermanager_group") externalmember = ansible_module.params_get("externalmember") + # query + query_param = ansible_module.params_get("query_param") + # action action = ansible_module.params_get("action") # state state = ansible_module.params_get("state") # Check parameters - if (names is None or len(names) < 1) and \ + if state != "query" and \ + (names is None or len(names) < 1) and \ (groups is None or len(groups) < 1): ansible_module.fail_json(msg="At least one name or groups is required") @@ -521,6 +603,11 @@ def main(): ansible_module.fail_json( msg="Only one group can be added at a time using 'name'.") + if state == "query": + if action == "member": + ansible_module.fail_json( + msg="Query is not possible with action=query") + check_parameters(ansible_module, state, action) if external is False: @@ -567,6 +654,13 @@ def main(): # Connect to IPA API with ansible_module.ipa_connect(context=context): + if state == "query": + exit_args = ansible_module.execute_query( + names, "groups", "cn", query_param, group_find, + query_param_settings) + + ansible_module.exit_json(changed=False, group=exit_args) + has_add_member_service = ansible_module.ipa_command_param_exists( "group_add_member", "service") if service is not None and not has_add_member_service: @@ -643,7 +737,7 @@ def main(): repr(group_name)) # Make sure group exists - res_find = find_group(ansible_module, name) + res_find = group_show(ansible_module, name) user_add, user_del = [], [] group_add, group_del = [], [] diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index bd31c00ac..4eed2bcc5 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -772,17 +772,19 @@ def convert_result(res): return _res -def user_find(module, name, sizelimit=None, timelimit=None): +def user_find(module, name, pkey_only=False, sizelimit=None, timelimit=None): _args = {"all": True} + if name: + _args["uid"] = name + if pkey_only: + _args["pkey_only"] = True if sizelimit is not None: _args["sizelimit"] = sizelimit if timelimit is not None: _args["timelimit"] = timelimit try: - if name: - _args["uid"] = name _result = module.ipa_command_no_name("user_find", _args).get("result") if _result: if name: diff --git a/tests/group/test_group_query.yml b/tests/group/test_group_query.yml new file mode 100644 index 000000000..345aeeb93 --- /dev/null +++ b/tests/group/test_group_query.yml @@ -0,0 +1,252 @@ +--- +- name: Test group query + hosts: ipaserver + become: true + + tasks: + + # IPA facts + + - name: Include FreeIPA facts. + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + + # GET FQDN_AT_DOMAIN + + - name: Get fqdn_at_domain + ansible.builtin.set_fact: + fqdn_at_domain: "{{ ansible_facts['fqdn'] + '@' + ipaserver_realm }}" + + # CLEANUP + + - name: Ensure groups "testgroup1" and "testgroup2" are absent + ipagroup: + ipaadmin_password: SomeADMINpassword + name: + - testgroup1 + - testgroup2 + - non-existing-group + state: absent + + - name: Ensure users "testuser1" and "testuser2" are absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - testuser1 + - testuser2 + state: absent + + # CREATE TEST ITEMS + + - name: Ensure users "testuser1" and "testuser2" are present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: testuser1 + first: first1 + last: last1 + - name: testuser2 + first: first2 + last: last2 + + - name: Ensure groups "testgroup1" is present + ipagroup: + ipaadmin_password: SomeADMINpassword + name: testgroup1 + user: testuser1 + description: test group 1 + register: result + failed_when: not result.changed or result.failed + + - name: Ensure groups "testgroup1" has services (IPA 4.7.0+) + ipagroup: + ipaadmin_password: SomeADMINpassword + name: testgroup1 + service: + - "{{ 'HTTP/' + fqdn_at_domain }}" + - "{{ 'ldap/' + fqdn_at_domain }}" + register: result + failed_when: not result.changed or result.failed + when: ipa_version is version('4.7.0', '>=') + + - name: Ensure groups "testgroup2" is present + ipagroup: + ipaadmin_password: SomeADMINpassword + name: testgroup2 + description: test group 2 + user: testuser2 + group: testgroup1 + register: result + failed_when: not result.changed or result.failed + + # TESTS + + - name: Query group "non-existing-group" + ipagroup: + ipaadmin_password: SomeADMINpassword + name: + - non-existing-group + query_param: ALL + state: query + register: result + failed_when: result.changed or result.failed + + - name: Print query information + ansible.builtin.debug: + var: result + + - name: Fail on non empty query result + ansible.builtin.fail: + msg: "{{ result['group'] }} is not empty" + when: result['group'] | length > 0 + + - name: Query all groups + ipagroup: + ipaadmin_password: SomeADMINpassword + state: query + register: result + failed_when: result.changed or result.failed + + - name: Print query information + ansible.builtin.debug: + var: result + + - name: Fail on missing "testgroup1" in query result + ansible.builtin.fail: + msg: "'testgroup1' not in query result {{ result['group']['groups'] }}" + when: ("testgroup1" not in result["group"]["groups"]) + + - name: Fail on missing "testgroup2" in query result + ansible.builtin.fail: + msg: "'testgroup2' not in query result {{ result['group']['groups'] }}" + when: ("testgroup2" not in result["group"]["groups"]) + + - name: Fail on "non-existing-group" in query result + ansible.builtin.fail: + msg: "'non-existing-group' in query result {{ result['group']['groups'] }}" + when: ("non-existing-group" in result["group"]["groups"]) + + - name: Query groups "testgroup1", "testgroup2" and "non-existing-group" + ipagroup: + ipaadmin_password: SomeADMINpassword + name: + - testgroup1 + - testgroup2 + - non-existing-group + state: query + register: result + failed_when: result.changed or result.failed + + - name: Fail on missing "testgroup1" in query result + ansible.builtin.fail: + msg: "'testgroup1' not in query result {{ result['group']['groups'] }}" + when: ("testgroup1" not in result["group"]["groups"]) + + - name: Fail on missing "testgroup2" in query result + ansible.builtin.fail: + msg: "'testgroup2' not in query result {{ result['group']['groups'] }}" + when: ("testgroup2" not in result["group"]["groups"]) + + - name: Fail on "non-existing-group" in query result + ansible.builtin.fail: + msg: "'non-existing-group' in query result {{ result['group']['groups'] }}" + when: ("non-existing-group" in result["group"]["groups"]) + + + - name: Query all group parameters for "testgroup1" + ipagroup: + ipaadmin_password: SomeADMINpassword + name: + - testgroup1 + query_param: ALL + state: query + register: result + failed_when: result.changed or result.failed + + - name: Print query information + ansible.builtin.debug: + var: result + + - name: Fail on missing information in query result + ansible.builtin.fail: + msg: "Query result {{ result['group'] }} is incomplete" + when: (result["group"]["description"] != "test group 1" or + result["group"]["user"] != "testuser1") + + - name: Fail on missing services in query result (IPA 4.7.0+) + ansible.builtin.fail: + msg: "Query result {{ result['group'] }} is incomplete (no services)" + when: ipa_version is version('4.7.0', '>=') and + ('HTTP/'+fqdn_at_domain not in result["group"]["service"] or + 'ldap/'+fqdn_at_domain not in result["group"]["service"]) + + - name: Query all group parameters for "testgroup2" + ipagroup: + ipaadmin_password: SomeADMINpassword + name: + - testgroup2 + query_param: ALL + state: query + register: result + failed_when: result.changed or result.failed + + - name: Print query information + ansible.builtin.debug: + var: result + + - name: Fail on missing information in query result + ansible.builtin.fail: + msg: "Query result {{ result['group'] }} is incomplete" + when: (result["group"]["description"] != "test group 2" or + result["group"]["group"] != "testgroup1" or + result["group"]["user"] != "testuser2") + + - name: Query "gid", "user", "group" for all groups + ipagroup: + ipaadmin_password: SomeADMINpassword + query_param: + - gid + - user + - group + state: query + register: result + failed_when: result.changed or result.failed + + - name: Print query information + ansible.builtin.debug: + var: result + + - name: Fail on less than 3 groups in result + ansible.builtin.fail: + msg: "{{ result['group'] }} is not empty" + when: result['group'] | length < 3 + + - name: Fail on missing "testgroup1" information in query result + ansible.builtin.fail: + msg: "'testgroup1' not in query result {{ result['group'] }}" + when: ("testgroup1" not in result["group"] or + "gid" not in result["group"]["testgroup1"]) + + - name: Fail on missing "testgroup2" information in query result + ansible.builtin.fail: + msg: "'testgroup2' not in query result {{ result['group'] }}" + when: ("testgroup2" not in result["group"] or + "gid" not in result["group"]["testgroup2"] or + result["group"]["testgroup2"]["group"] != "testgroup1") + + # CLEANUP + + - name: Ensure groups "testgroup1" and "testgroup2" are absent + ipagroup: + ipaadmin_password: SomeADMINpassword + name: + - testgroup1 + - testgroup2 + state: absent + + - name: Ensure users "testuser1" and "testuser2" are absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - testuser1 + - testuser2 + state: absent