From 7497eeba926ba1f4af7e39a75ae21879a73a1fdd Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Fri, 25 Feb 2022 18:09:47 +0100 Subject: [PATCH] [DRAFT]: Add support for state:fetched to user module --- .../module_utils/ansible_freeipa_module.py | 95 ++++++++++++ plugins/modules/ipauser.py | 145 ++++++++++++++++-- tests/user/test_user_fetched.yml | 108 +++++++++++++ 3 files changed, 339 insertions(+), 9 deletions(-) create mode 100644 tests/user/test_user_fetched.yml diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 3c25c7953..11e90df2b 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -1166,6 +1166,101 @@ def exception_handler(module, ex, exit_args, one_name): return changed + def execute_fetched(self, exit_args, names, prefix, name_ipa_param, + fetch_params, ipa_param_mapping, + ipa_param_converter, + show_function, find_function): + """ + Execute fetched state. + + Parameters + ---------- + exit_args: Exit args dict + This is the dict that will be resturned with + ansible_module.exit_json + 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. + fetch_params: The parameters to return + The parameters that should be returned. If fetch_params is + ["all"], all parameters in ipa_pram_names will be returned. + ipa_param_mapping: IPA param mapping + This is the mapping of the default module paramter name to + IPA option name. + Example: "uid" for user name of the user commands. + ipa_param_converter: Parameter converter + This is an extra parameter converter for parameters that + need to be converted into integers for example. + show_function: The function to show one entry + This is "user-show" for the user command. + find_function: The function to find several entries + This is "user-find" for the user command. + + Example (ipauser module): + + if state == "fetched": + changed = ansible_module.execute_fetched( + exit_args, "users", "uid", fetch_params, ipa_param_mapping, + names, user_show, user_find) + ansible_module.exit_json(changed=False, user=exit_args) + + """ + + def store_params(exit_args, name, prefix, name_ipa_param, result, + params, ipa_param_converter): + if params is None: + exit_args.setdefault(prefix, []).append( + result[name_ipa_param]) + return + for field in params: + ipa_field = ipa_param_mapping[field] + + if ipa_field in result: + value = result[ipa_field] + if ipa_param_converter and \ + field in ipa_param_converter: + if isinstance(value, list): + value = [ipa_param_converter[field](val) + for val in value] + else: + value = ipa_param_converter[field](value) + else: + if isinstance(value, list): + value = [to_text(val) for val in value] + else: + value = to_text(value) + if name is None: + exit_args[field] = value + else: + exit_args.setdefault(name, {})[field] = value + + if fetch_params == ["all"]: + fetch_params = ipa_param_mapping.keys() + + if names and isinstance(names, list): + with_name = len(names) > 1 + for name in names: + result = show_function(self, name) + if result: + store_params(exit_args, name if with_name else None, + prefix, name_ipa_param, result, + fetch_params, ipa_param_converter) + else: + results = find_function(self) + if results is not None: + for result in results: + name = result[name_ipa_param] + store_params(exit_args, name, prefix, name_ipa_param, + result, fetch_params, ipa_param_converter) + + return False + class FreeIPABaseModule(IPAAnsibleModule): """ Base class for FreeIPA Ansible modules. diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index aee71cd22..3c080213d 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -384,6 +384,9 @@ default: "always" choices: ["always", "on_create"] required: false + fetch_param: + description: The fields to fetch with state=fetched + required: false action: description: Work on user or member level default: "user" @@ -393,7 +396,8 @@ default: present choices: ["present", "absent", "enabled", "disabled", - "unlocked", "undeleted"] + "unlocked", "undeleted", + "fetched"] author: - Thomas Woerner """ @@ -481,7 +485,7 @@ unicode = str -def find_user(module, name): +def user_show(module, name): _args = { "all": True, } @@ -501,6 +505,59 @@ def find_user(module, name): return _result +def simplify_result(res): + # Transform each principal to a string + if "krbprincipalname" in res: + res["krbprincipalname"] = [ + to_text(x) for x in (res.get("krbprincipalname") or []) + ] + # Transform each certificate to a string + if "usercertificate" in res: + res["usercertificate"] = [ + encode_certificate(x) for x in + (res.get("usercertificate") or []) + ] + # All single value parameters should not be lists + for param in res: + if isinstance(res[param], list) and len(res[param]) == 1 and \ + param not in ["manager", "krbprincipalname", "usercertificate", + "ipacertmapdata"]: + res[param] = res[param][0] + if param in []: + res[param] = int(res[param]) + + +def user_show_simplified(module, name): + _args = { + "all": True, + } + + try: + _result = module.ipa_command("user_show", name, _args).get("result") + except ipalib_errors.NotFound: + return None + + simplify_result(_result) + + return _result + + +def user_find(module): + _args = { + "all": True, + } + + try: + _result = module.ipa_command_no_name("user_find", _args).get("result") + except ipalib_errors.NotFound: + return None + + for res in _result: + simplify_result(res) + + return _result + + def gen_args(first, last, fullname, displayname, initials, homedir, shell, email, principalexpiration, passwordexpiration, password, random, uid, gid, city, userstate, postalcode, phone, mobile, @@ -618,6 +675,15 @@ def check_parameters( # pylint: disable=unused-argument "certificate", "certmapdata", ]) + if state == "fetched": + invalid.append("users") + + if action == "member": + module.fail_json( + msg="Fetched is not possible with action=member") + else: + invalid.append("fetch_param") + if state != "absent" and preserve is not None: module.fail_json( msg="Preserve is only possible for state=absent") @@ -809,6 +875,51 @@ def main(): nomembers=dict(type='bool', default=None), ) + ipa_param_mapping = { + "first": "givenname", + "last": "sn", + "fullname": "cn", + "displayname": "displayname", + "initials": "initials", + "homedir": "homedirectory", + "shell": "loginshell", + "email": "mail", + "principalexpiration": "krbprincipalexpiration", + "passwordexpiration": "krbpasswordexpiration", + # "password": "userpassword", Never return passwords + # "randompassword": "randompassword", Never return passwords + "uid": "uidnumber", + "gid": "gidnumber", + "city": "l", + "userstate": "st", + "postalcode": "postalcode", + "phone": "telephonenumber", + "mobile": "mobile", + "pager": "pager", + "fax": "facsimiletelephonenumber", + "orgunit": "ou", + "title": "title", + "carlicense": "carlicense", + "sshpubkey": "ipasshpubkey", + "userauthtype": "ipauserauthtype", + "userclass": "userclass", + "radius": "ipatokenradiusconfiglink", + "radiususer": "ipatokenradiususername", + "departmentnumber": "departmentnumber", + "employeenumber": "employeenumber", + "employeetype": "employeetype", + "preferredlanguage": "preferredlanguage", + "manager": "manager", + "principal": "krbprincipalname", + "certificate": "usercertificate", + "certmapdata": "ipacertmapdata", + } + + ipa_param_converter = { + "uid": int, + "gid": int, + } + ansible_module = IPAAnsibleModule( argument_spec=dict( # general @@ -833,18 +944,24 @@ def main(): update_password=dict(type='str', default=None, no_log=False, choices=['always', 'on_create']), + # fetched + fetch_param=dict(type="list", default=None, + choices=["all"].extend(ipa_param_mapping.keys()), + 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", "fetched"]), # 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=fetched supports_check_mode=True, ) @@ -909,15 +1026,18 @@ def main(): preserve = ansible_module.params_get("preserve") # mod update_password = ansible_module.params_get("update_password") + # fetched + fetch_param = ansible_module.params_get("fetch_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 \ - (users is None or len(users) < 1): - ansible_module.fail_json(msg="One of name and users is required") + if state != "fetched": + if (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") if state == "present": if names is not None and len(names) != 1: @@ -965,6 +1085,13 @@ def main(): commands = [] user_set = set() + if state == "fetched": + changed = ansible_module.execute_fetched( + exit_args, names, "users", "uid", fetch_param, + ipa_param_mapping, ipa_param_converter, user_show_simplified, + user_find) + ansible_module.exit_json(changed=False, user=exit_args) + for user in names: if isinstance(user, dict): name = user.get("name") @@ -1074,7 +1201,7 @@ def main(): "your IPA version") # Make sure user exists - res_find = find_user(ansible_module, name) + res_find = user_show(ansible_module, name) # Create command if state == "present": @@ -1141,7 +1268,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_fetched.yml b/tests/user/test_user_fetched.yml new file mode 100644 index 000000000..3ac225f77 --- /dev/null +++ b/tests/user/test_user_fetched.yml @@ -0,0 +1,108 @@ +--- +- name: Test ipauser random password generation + hosts: ipaserver + become: true + + tasks: + - name: Users user1 and user2 absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - user1 + - user2 + state: absent + + + - name: Users user1 and user2 present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: user1 + first: first1 + last: last1 + - name: user2 + first: first2 + last: last2 + + + - name: Fetch user information + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - user100 + fetch_param: all + state: fetched + register: result + failed_when: result.changed or result.failed + + - name: Print fetched information + debug: + var: result + + + - name: Fetch user information + ipauser: + ipaadmin_password: SomeADMINpassword + state: fetched + register: result + failed_when: result.changed or result.failed + + - name: Print fetched information + debug: + var: result + + + - name: Fetch user information + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - user1 + - user2 + - user100 + state: fetched + register: result + failed_when: result.changed or result.failed + + - name: Print fetched information + debug: + var: result + + + - name: Fetch user information + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - user1 + fetch_param: all + state: fetched + register: result + failed_when: result.changed or result.failed + + - name: Print fetched information + debug: + var: result + + + - name: Fetch user information + ipauser: + ipaadmin_password: SomeADMINpassword + fetch_param: + - uid + - first + - last + state: fetched + register: result + failed_when: result.changed or result.failed + + - name: Print fetched information + debug: + var: result + + + - name: Users user1 and user2 absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: + - user1 + - user2 + state: absent