diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 3c25c7953c..ff0c5d97d4 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -1166,6 +1166,78 @@ def exception_handler(module, ex, exit_args, one_name): return changed + def execute_fetched(self, names, prefix, name_ipa_param, + fetch_param, fetch_command, fetch_param_mapping): + """ + Execute fetched 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. + fetch_param: The parameters to return + The parameters that should be returned. If fetch_params is + ["all"], all parameters in ipa_pram_names will be returned. + fetch_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. + fetch_command: The Fetch 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 fetch_param_mapping[field] is True: + ipa_field = field + else: + ipa_field = fetch_param_mapping[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 fetch_param == ["all"]: + fetch_param = fetch_param_mapping.keys() + + if names and isinstance(names, list): + with_name = len(names) > 1 + for name in names: + result = fetch_command(self, name) + if result: + store_params(exit_args, name if with_name else None, + prefix, name_ipa_param, result, + fetch_param) + else: + results = fetch_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, fetch_param) + + return exit_args + class FreeIPABaseModule(IPAAnsibleModule): """ Base class for FreeIPA Ansible modules. diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index aee71cd22a..fe394bb904 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,50 @@ 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 fetch_command(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 + else: + 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 +666,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") @@ -742,6 +799,70 @@ def exception_handler(module, ex, errors, exit_args, one_name): return False +fetch_param_base = [ + "login", "first", "last", "shell", "principal", "uid", "gid", "disabled" +] + + +fetch_param_mapping = { + # password, randompassword and krbprincipalkey may not be in the returned + # information even in server context. + + "objectclass": True, + "ipauniqueid": True, + + "login": "uid", + "first": "givenname", + "last": "sn", + "fullname": "cn", + "displayname": True, + "initials": True, + "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", + "title": True, + "carlicense": True, + "sshpubkey": "ipasshpubkey", + "userauthtype": "ipauserauthtype", + "userclass": True, + "radius": "ipatokenradiusconfiglink", + "radiususer": "ipatokenradiususername", + "departmentnumber": True, + "employeenumber": True, + "employeetype": True, + "preferredlanguage": "preferredlanguage", + "manager": True, + "principal": "krbprincipalname", + "certificate": "usercertificate", + "certmapdata": "ipacertmapdata", + + "gecos": True, + "krblastpwdchange": True, + "krblastadminunlock": True, + "krbextradata": True, + "krbticketflags": True, + "krbloginfailedcount": True, + "krblastsuccessfulauth": True, + "has_password": True, + "has_keytab": True, + "preserved": True, + "memberof_group": True, + "disabled": "nsaccountock" +} + + def main(): user_spec = dict( # present @@ -833,18 +954,25 @@ 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( + fetch_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 +1037,19 @@ 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 +1097,13 @@ def main(): commands = [] user_set = set() + if state == "fetched": + exit_args = ansible_module.execute_fetched( + names, "users", "uid", fetch_param, fetch_command, + fetch_param_mapping) + + ansible_module.exit_json(changed=False, user=exit_args) + for user in names: if isinstance(user, dict): name = user.get("name") @@ -1074,7 +1213,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 +1280,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 0000000000..3ac225f775 --- /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