From 54cb545993a21454305c6b1adba70ac3fbe9aaa2 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 25 Feb 2022 18:09:47 +0100 Subject: [PATCH 1/3] ansible_freeipa_module.IPAAnsibleModule: New execute_query method This method enables to execute query state within IPAAnsibleModule. The parameter query_param canbe used to select the returned parameters. def execute_query(self, names, prefix, name_ipa_param, query_param, query_command, query_param_settings): """ Execute query state. Parameters ---------- names: The main items to return It names is not None and not an empty list then all items found with "item_find" are returned, else the items in names. prefix: The prefix for use with several main items The prefix is "users" for the "user" module. It is used if only the list of main items (example: users) is returned. name_ipa_param: The IPA param name of the name parameter This is for example "uid" that is used for the user name in the user module. query_param: The parameters to return The parameters that should be returned. If query_param is ["ALL"], all parameters in ipa_pram_names will be returned. query_param_settings: IPA base parameters, all and mapping The dict provides all parameters the "ALL" list and the 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 This is a module function that returns the structure(s) from the show or find command. """ Add to DOCUMENTATION: query_param: description: The fields to query with state=query required: false state: description: State to ensure default: present choices: ["present", "absent", "query"] Add to the code: query_param_settings = { "ALL": [ "dn", "objectclass", "ipauniqueid", "ipantsecurityidentifier", "name", ... ], "BASE": [ "name", ... "disabled" ], "mapping": { "name": "uid", ... "disabled": "nsaccountock" } } def main(): ... invalid = [] if state == "present": ... else: ... if state == "query": if action == "member": module.fail_json( msg="Query is not possible with action=member") else: invalid.append("query_param") ... # Connect to IPA API with ansible_module.ipa_connect(): if state == "query": exit_args = ansible_module.execute_query( names, "users", "uid", query_param, user_find, query_param_settings) ansible_module.exit_json(changed=False, user=exit_args) # remainaing module code follows here --- .../module_utils/ansible_freeipa_module.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index dfec4c58f1..9276b2a325 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -1356,3 +1356,82 @@ def exception_handler(module, ex, exit_args, one_name): self.fail_json(msg=", ".join(_errors)) return changed + + def execute_query(self, names, prefix, name_ipa_param, + query_param, query_command, query_param_settings): + """ + Execute query state. + + Parameters + ---------- + names: The main items to return + It names is not None and not an empty list then all items + found with "item_find" are returned, else the items in names. + prefix: The prefix for use with several main items + The prefix is "users" for the "user" module. It is used + if only the list of main items (example: users) is returned. + name_ipa_param: The IPA param name of the name parameter + This is for example "uid" that is used for the user name in + the user module. + query_param: The parameters to return + The parameters that should be returned. If query_param is + ["ALL"], all parameters in ipa_pram_names will be returned. + query_param_settings: IPA base parameters, all and mapping + The dict provides all parameters the "ALL" list and the + 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 + This is a module function that returns the structure(s) from + the show or find command. + + """ + + def store_params(exit_args, name, prefix, name_ipa_param, result, + params): + if params is None: + exit_args.setdefault(prefix, []).append( + result[name_ipa_param]) + return + for field in params: + if field not in query_param_settings["ALL"]: + self.fail_json( + msg="query_param '%s' is not supported" % field) + if "mapping" in query_param_settings and \ + field in query_param_settings["mapping"]: + ipa_field = query_param_settings["mapping"][field] + else: + ipa_field = field + + if ipa_field in result: + value = result[ipa_field] + if name is None: + exit_args[field] = value + else: + exit_args.setdefault(name, {})[field] = value + + # Create exit_args + exit_args = {} + + if query_param == ["BASE"]: + query_param = query_param_settings["BASE"] + elif query_param == ["ALL"]: + query_param = query_param_settings["ALL"] + + if names and isinstance(names, list): + with_name = len(names) > 1 + for name in names: + result = query_command(self, name) + 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) + if results is not None: + for result in results: + name = result[name_ipa_param] + store_params(exit_args, name, prefix, name_ipa_param, + result, query_param) + + return exit_args From 15b96491d515b5fe084f5a7b9f813753b038568a Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Thu, 25 Aug 2022 21:06:43 +0200 Subject: [PATCH 2/3] ipauser: Implement state:quers using IPAAnsibleModule.execute_query The query_param parameter has been added, together with the dict query_param_settings. Also a new convert_result and user_find function has been added. --- plugins/modules/ipauser.py | 140 ++++++++++++++++++++++++-- tests/user/test_user_query.yml | 173 +++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 tests/user/test_user_query.yml diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index dcea92f467..bd31c00aca 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -596,6 +596,9 @@ type: str choices: ["always", "on_create"] required: false + query_param: + description: The fields to query with state=query + required: false action: description: Work on user or member level type: str @@ -607,7 +610,8 @@ default: present choices: ["present", "absent", "enabled", "disabled", - "unlocked", "undeleted"] + "unlocked", "undeleted", + "query"] author: - Thomas Woerner (@t-woerner) """ @@ -729,7 +733,7 @@ unicode = str -def find_user(module, name): +def user_show(module, name): _args = { "all": True, } @@ -749,6 +753,49 @@ def find_user(module, name): return _result +def convert_result(res): + _res = {} + for key in res: + if key in ["manager", "krbprincipalname", "ipacertmapdata"]: + _res[key] = [to_text(x) for x in (res.get(key) or [])] + elif key == "usercertificate": + _res[key] = [encode_certificate(x) for x in (res.get(key) or [])] + elif isinstance(res[key], list) and len(res[key]) == 1: + # All single value parameters should not be lists + # This does not apply to manager, krbprincipalname, + # usercertificate and ipacertmapdata + _res[key] = to_text(res[key][0]) + elif key in ["uidNumber", "gidNumber"]: + _res[key] = int(res[key]) + else: + _res[key] = to_text(res[key]) + return _res + + +def user_find(module, name, sizelimit=None, timelimit=None): + _args = {"all": 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: + _result = convert_result(_result[0]) + else: + _result = [convert_result(res) for res in _result] + + except ipalib_errors.NotFound: + return None + + return _result + + def gen_args(first, last, fullname, displayname, initials, homedir, gecos, shell, email, principalexpiration, passwordexpiration, password, random, uid, gid, street, city, userstate, postalcode, phone, @@ -881,6 +928,15 @@ def check_parameters( # pylint: disable=unused-argument invalid.extend( ["principal", "manager", "certificate", "certmapdata"]) + if state == "query": + invalid.append("users") + + if action == "member": + module.fail_json( + msg="Query is not possible with action=member") + else: + invalid.append("query_param") + if state != "absent" and preserve is not None: module.fail_json( msg="Preserve is only possible for state=absent") @@ -1014,6 +1070,59 @@ def exception_handler(module, ex, errors, exit_args, single_user): return False +query_param_settings = { + # password, randompassword and krbprincipalkey may not be in the returned + # information even in server context. + "ALL": [ + "dn", "objectclass", "ipauniqueid", "ipantsecurityidentifier", "name", + "first", "last", "fullname", "displayname", "initials", "homedir", + "shell", "email", "principalexpiration", "passwordexpiration", "uid", + "gid", "city", "userstate", "postalcode", "phone", "mobile", "pager", + "fax", "orgunit", "title", "carlicense", "sshpubkey", "userauthtype", + "userclass", "radius", "radiususer", "departmentnumber", + "employeenumber", "employeetype", "preferredlanguage", "manager", + "principal", "certificate", "certmapdata", "gecos", "krblastpwdchange", + "krblastadminunlock", "krbextradata", "krbticketflags", + "krbloginfailedcount", "krblastsuccessfulauth", "has_password", + "has_keytab", "preserved", "memberof_group", "disabled" + ], + "BASE": [ + "name", "first", "last", "shell", "principal", "uid", "gid", + "disabled" + ], + "mapping": { + "name": "uid", + "first": "givenname", + "last": "sn", + "fullname": "cn", + "homedir": "homedirectory", + "shell": "loginshell", + "email": "mail", + "principalexpiration": "krbprincipalexpiration", + "passwordexpiration": "krbpasswordexpiration", + "uid": "uidnumber", + "gid": "gidnumber", + "city": "l", + "userstate": "st", + "postalcode": "postalcode", + "phone": "telephonenumber", + "mobile": "mobile", + "pager": "pager", + "fax": "facsimiletelephonenumber", + "orgunit": "ou", + "sshpubkey": "ipasshpubkey", + "userauthtype": "ipauserauthtype", + "radius": "ipatokenradiusconfiglink", + "radiususer": "ipatokenradiususername", + "preferredlanguage": "preferredlanguage", + "principal": "krbprincipalname", + "certificate": "usercertificate", + "certmapdata": "ipacertmapdata", + "disabled": "nsaccountock" + } +} + + def main(): user_spec = dict( # present @@ -1123,18 +1232,25 @@ def main(): update_password=dict(type='str', default=None, no_log=False, choices=['always', 'on_create']), + # query + query_param=dict(type="list", default=None, + choices=["ALL", "BASE"].extend( + query_param_settings["ALL"]), + required=False), + # general action=dict(type="str", default="user", choices=["member", "user"]), state=dict(type="str", default="present", choices=["present", "absent", "enabled", "disabled", - "unlocked", "undeleted"]), + "unlocked", "undeleted", "query"]), # Add user specific parameters for simple use case **user_spec ), mutually_exclusive=[["name", "users"]], - required_one_of=[["name", "users"]], + # Required one of [["name", "users"]] has been removed as there is + # an extra test below and it is not working with state=query supports_check_mode=True, ) @@ -1209,13 +1325,16 @@ def main(): preserve = ansible_module.params_get("preserve") # mod update_password = ansible_module.params_get("update_password") + # query + query_param = ansible_module.params_get("query_param") # general action = ansible_module.params_get("action") 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 \ (users is None or len(users) < 1): ansible_module.fail_json(msg="One of name and users is required") @@ -1249,6 +1368,13 @@ def main(): # Connect to IPA API with ansible_module.ipa_connect(): + if state == "query": + exit_args = ansible_module.execute_query( + names, "users", "uid", query_param, user_find, + query_param_settings) + + ansible_module.exit_json(changed=False, user=exit_args) + # Check version specific settings server_realm = ansible_module.ipa_get_realm() @@ -1421,7 +1547,7 @@ def main(): msg="Your IPA version does not support External IdP.") # Make sure user exists - res_find = find_user(ansible_module, name) + res_find = user_show(ansible_module, name) # Create command if state == "present": @@ -1498,7 +1624,7 @@ def main(): principal_add, principal_del = gen_add_del_lists( principal, res_find.get("krbprincipalname")) # Principals are not returned as utf8 for IPA using - # python2 using user_find, therefore we need to + # python2 using user_show, therefore we need to # convert the principals that we should remove. principal_del = [to_text(x) for x in principal_del] diff --git a/tests/user/test_user_query.yml b/tests/user/test_user_query.yml new file mode 100644 index 0000000000..82eb810892 --- /dev/null +++ b/tests/user/test_user_query.yml @@ -0,0 +1,173 @@ +--- +- name: Test user query + hosts: ipaserver + become: true + + tasks: + + # CLEANUP + + - name: Ensure users "testuser1" and "testuser2" are absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - testuser1 + - testuser2 + - non-existing-user + 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: Query user "non-existing-user" + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - non-existing-user + 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['user'] }} is not empty" + when: result['user'] | length > 0 + + - name: Query all users + ipauser: + 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 "testuser1" in query result + ansible.builtin.fail: + msg: "'testuser1' not in query result {{ result['user']['users'] }}" + when: ("testuser1" not in result["user"]["users"]) + + - name: Fail on missing "testuser2" in query result + ansible.builtin.fail: + msg: "'testuser2' not in query result {{ result['user']['users'] }}" + when: ("testuser2" not in result["user"]["users"]) + + - name: Fail on "non-existing-user" in query result + ansible.builtin.fail: + msg: "'non-existing-user' in query result {{ result['user']['users'] }}" + when: ("non-existing-user" in result["user"]["users"]) + + - name: Query users "testuser1", "testuser2" and "non-existing-user" + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - testuser1 + - testuser2 + - non-existing-user + state: query + register: result + failed_when: result.changed or result.failed + + - name: Fail on missing "testuser1" in query result + ansible.builtin.fail: + msg: "'testuser1' not in query result {{ result['user']['users'] }}" + when: ("testuser1" not in result["user"]["users"]) + + - name: Fail on missing "testuser2" in query result + ansible.builtin.fail: + msg: "'testuser2' not in query result {{ result['user']['users'] }}" + when: ("testuser2" not in result["user"]["users"]) + + - name: Fail on "non-existing-user" in query result + ansible.builtin.fail: + msg: "'non-existing-user' in query result {{ result['user']['users'] }}" + when: ("non-existing-user" in result["user"]["users"]) + + - name: Query all user parameters for "testuser1" + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - testuser1 + 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['user'] }} is incomplete" + when: (result["user"]["displayname"] != "first1 last1" or + result["user"]["first"] != "first1" or + result["user"]["fullname"] != "first1 last1" or + result["user"]["initials"] != "fl" or + result["user"]["last"] != "last1" or + result["user"]["name"] != "testuser1") + + - name: Query "uid", "first" and "last" parameters for all users + ipauser: + ipaadmin_password: SomeADMINpassword + query_param: + - uid + - first + - last + 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 users in result + ansible.builtin.fail: + msg: "{{ result['user'] }} is not empty" + when: result['user'] | length < 3 + + - name: Fail on missing "testuser1" information in query result + ansible.builtin.fail: + msg: "'testuser1' not in query result {{ result['user'] }}" + when: ("testuser1" not in result["user"] or + result["user"]["testuser1"]["first"] != "first1" or + result["user"]["testuser1"]["last"] != "last1" or + "uid" not in result["user"]["testuser1"] or + result["user"]["testuser1"] | length != 3) + + - name: Fail on missing "testuser2" information in query result + ansible.builtin.fail: + msg: "'testuser2' not in query result {{ result['user'] }}" + when: ("testuser2" not in result["user"] or + result["user"]["testuser2"]["first"] != "first2" or + result["user"]["testuser2"]["last"] != "last2" or + "uid" not in result["user"]["testuser2"] or + result["user"]["testuser2"] | length != 3) + + # CLEANUP + + - name: Ensure users "testuser1" and "testuser2" are absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - testuser1 + - testuser2 + state: absent From f5dc8431d217f89962506a25beb9287f39b581ee Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Thu, 25 Aug 2022 21:10:34 +0200 Subject: [PATCH 3/3] 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 ++++++++++++++++++ tests/group/test_groups.yml | 2 +- 5 files changed, 380 insertions(+), 30 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 9276b2a325..1d773e5626 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. @@ -1381,7 +1381,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. @@ -1421,13 +1421,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 b80061663c..b41fbac0b2 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 bd31c00aca..4eed2bcc51 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 0000000000..345aeeb933 --- /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 diff --git a/tests/group/test_groups.yml b/tests/group/test_groups.yml index 648b35b878..a3a79608a7 100644 --- a/tests/group/test_groups.yml +++ b/tests/group/test_groups.yml @@ -114,7 +114,7 @@ ipagroup: ipaadmin_password: SomeADMINpassword register: result - failed_when: result.changed or not result.failed or "one of the following is required" not in result.msg + failed_when: result.changed or not result.failed or "At least one name or groups is required" not in result.msg - name: Name is absent ipagroup: