diff --git a/README-user.md b/README-user.md index e51d7af300..b80bf39d18 100644 --- a/README-user.md +++ b/README-user.md @@ -279,7 +279,6 @@ Example playbook to disable a user: This can also be done as an alternative with the `users` variable containing only names. - Example playbook to enable users: ```yaml @@ -298,6 +297,22 @@ Example playbook to enable users: This can also be done as an alternative with the `users` variable containing only names. +Example playbook to rename users: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Rename user pinky to reddy + - ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + rename: reddy + state: enabled +``` Example playbook to unlock users: @@ -401,7 +416,7 @@ Variable | Description | Required `update_password` | Set password for a user in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no `preserve` | Delete a user, keeping the entry available for future use. (bool) | no `action` | Work on user or member level. It can be on of `member` or `user` and defaults to `user`. | no -`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `unlocked` or `undeleted`, default: `present`. Only `names` or `users` with only `name` set are allowed if state is not `present`. | yes +`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `renamed`, `unlocked` or `undeleted`, default: `present`. Only `names` or `users` with only `name` set are allowed if state is not `present`. | yes @@ -458,10 +473,10 @@ Variable | Description | Required `smb_profile_path:` \| `ipantprofilepath` | SMB profile path, in UNC format. Requires FreeIPA version 4.8.0+. | no `smb_home_dir` \| `ipanthomedirectory` | SMB Home Directory, in UNC format. Requires FreeIPA version 4.8.0+. | no `smb_home_drive` \| `ipanthomedirectorydrive` | SMB Home Directory Drive, a single upercase letter (A-Z) followed by a colon (:), for example "U:". Requires FreeIPA version 4.8.0+. | no +`rename` \| `new_name` | Rename the user object to the new name string. Only usable with `state: renamed`. | no `nomembers` | Suppress processing of membership attributes. (bool) | no - Return Values ============= @@ -477,5 +492,5 @@ Variable | Description | Returned When Authors ======= -Thomas Woerner -Rafael Jeffman +- Thomas Woerner +- Rafael Jeffman diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index db39cc7b1d..a1217456d2 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -319,6 +319,11 @@ description: Suppress processing of membership attributes required: false type: bool + rename: + description: Rename the user object + required: false + type: str + aliases: ["new_name"] required: false first: description: The first name. Required if user does not exist. @@ -586,6 +591,11 @@ description: Suppress processing of membership attributes required: false type: bool + rename: + description: Rename the user object + required: false + type: str + aliases: ["new_name"] preserve: description: Delete a user, keeping the entry available for future use required: false @@ -607,7 +617,8 @@ default: present choices: ["present", "absent", "enabled", "disabled", - "unlocked", "undeleted"] + "unlocked", "undeleted", + "renamed"] author: - Thomas Woerner (@t-woerner) """ @@ -694,6 +705,13 @@ smb_profile_path: \\\\server\\profiles\\some_profile smb_home_dir: \\\\users\\home\\smbuser smb_home_drive: "U:" + +# Rename an existing user +- ipauser: + ipaadmin_password: SomeADMINpassword + name: someuser + rename: anotheruser + state: renamed """ RETURN = """ @@ -857,7 +875,7 @@ def check_parameters( # pylint: disable=unused-argument employeenumber, employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, update_password, smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, - idp, ipa_user_id, + idp, ipa_user_id, rename ): if state == "present" and action == "user": invalid = ["preserve"] @@ -885,6 +903,19 @@ def check_parameters( # pylint: disable=unused-argument module.fail_json( msg="Preserve is only possible for state=absent") + if state != "renamed": + invalid.append("rename") + else: + invalid.extend([ + "preserve", "principal", "manager", "certificate", "certmapdata", + ]) + if not rename: + module.fail_json( + msg="A value for attribute 'rename' must be provided.") + if action == "member": + module.fail_json( + msg="Action member can not be used with state: renamed.") + module.params_fail_used_invalid(invalid, state, action) if certmapdata is not None: @@ -1097,6 +1128,8 @@ def main(): idp=dict(type="str", default=None, aliases=['ipaidpconfiglink']), idp_user_id=dict(type="str", default=None, aliases=['ipaidpconfiglink']), + rename=dict(type="str", required=False, default=None, + aliases=["new_name"]), ) ansible_module = IPAAnsibleModule( @@ -1128,7 +1161,7 @@ def main(): choices=["member", "user"]), state=dict(type="str", default="present", choices=["present", "absent", "enabled", "disabled", - "unlocked", "undeleted"]), + "unlocked", "undeleted", "renamed"]), # Add user specific parameters for simple use case **user_spec @@ -1209,6 +1242,8 @@ def main(): preserve = ansible_module.params_get("preserve") # mod update_password = ansible_module.params_get("update_password") + # rename + rename = ansible_module.params_get("rename") # general action = ansible_module.params_get("action") state = ansible_module.params_get("state") @@ -1219,27 +1254,30 @@ def main(): (users is None or len(users) < 1): ansible_module.fail_json(msg="One of name and users is required") - if state == "present": + if state in ["present", "renamed"]: if names is not None and len(names) != 1: + act = "renamed" if state == "renamed" else "added" ansible_module.fail_json( - msg="Only one user can be added at a time using name.") - - check_parameters( - ansible_module, state, action, - first, last, fullname, displayname, initials, homedir, gecos, shell, - email, - principal, principalexpiration, passwordexpiration, password, random, - uid, gid, street, city, phone, mobile, pager, fax, orgunit, title, - manager, carlicense, sshpubkey, userauthtype, userclass, radius, - radiususer, departmentnumber, employeenumber, employeetype, - preferredlanguage, certificate, certmapdata, noprivate, nomembers, - preserve, update_password, smb_logon_script, smb_profile_path, - smb_home_dir, smb_home_drive, idp, idp_user_id) - certmapdata = convert_certmapdata(certmapdata) + msg="Only one user can be %s at a time using name." % (act)) # Use users if names is None if users is not None: names = users + else: + check_parameters( + ansible_module, state, action, + first, last, fullname, displayname, initials, homedir, gecos, + shell, email, + principal, principalexpiration, passwordexpiration, password, + random, + uid, gid, street, city, phone, mobile, pager, fax, orgunit, title, + manager, carlicense, sshpubkey, userauthtype, userclass, radius, + radiususer, departmentnumber, employeenumber, employeetype, + preferredlanguage, certificate, certmapdata, noprivate, nomembers, + preserve, update_password, smb_logon_script, smb_profile_path, + smb_home_dir, smb_home_drive, idp, idp_user_id, rename, + ) + certmapdata = convert_certmapdata(certmapdata) # Init @@ -1330,6 +1368,7 @@ def main(): smb_home_drive = user.get("smb_home_drive") idp = user.get("idp") idp_user_id = user.get("idp_user_id") + rename = user.get("rename") certificate = user.get("certificate") certmapdata = user.get("certmapdata") noprivate = user.get("noprivate") @@ -1346,7 +1385,8 @@ def main(): employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, update_password, smb_logon_script, smb_profile_path, - smb_home_dir, smb_home_drive, idp, idp_user_id) + smb_home_dir, smb_home_drive, idp, idp_user_id, rename, + ) certmapdata = convert_certmapdata(certmapdata) # Check API specific parameters @@ -1737,6 +1777,12 @@ def main(): else: raise ValueError("No user '%s'" % name) + elif state == "renamed": + if res_find is None: + ansible_module.fail_json(msg="No user '%s'" % name) + else: + if rename != name: + commands.append([name, 'user_mod', {"rename": rename}]) else: ansible_module.fail_json(msg="Unkown state '%s'" % state) diff --git a/tests/user/test_user.yml b/tests/user/test_user.yml index 1300b3b3ef..48e930dc6f 100644 --- a/tests/user/test_user.yml +++ b/tests/user/test_user.yml @@ -9,7 +9,7 @@ ipauser: ipaadmin_password: SomeADMINpassword ipaapi_context: "{{ ipa_context | default(omit) }}" - name: manager1,manager2,manager3,pinky,pinky2,igagarin + name: manager1,manager2,manager3,pinky,pinky2,igagarin,reddy state: absent - name: User manager1 present @@ -352,6 +352,46 @@ register: result failed_when: result.changed or result.failed + - name: Rename user pinky to reddy + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: pinky + rename: reddy + state: renamed + register: result + failed_when: not result.changed or result.failed + + - name: Rename user pinky to reddy, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: pinky + rename: reddy + state: renamed + register: result + failed_when: not result.failed or "No user 'pinky'" not in result.msg + + - name: Rename user reddy to reddy + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: reddy + rename: reddy + state: renamed + register: result + failed_when: result.changed or result.failed + + - name: Rename user reddy back to pinky + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: reddy + rename: pinky + state: renamed + register: result + failed_when: not result.changed or result.failed + - name: User pinky absent and preserved for future exclusion. ipauser: ipaadmin_password: SomeADMINpassword diff --git a/tests/user/test_users.yml b/tests/user/test_users.yml index 7c0d11e261..651e06d07b 100644 --- a/tests/user/test_users.yml +++ b/tests/user/test_users.yml @@ -5,6 +5,12 @@ gather_facts: false tasks: + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager1,manager2,manager3,pinky,pinky2,mod1,mod2 + state: absent + - name: Remove test users ipauser: ipaadmin_password: SomeADMINpassword @@ -48,7 +54,7 @@ register: result failed_when: not result.changed or result.failed - - name: Users user1..10 present + - name: Users user1..10 present, again ipauser: ipaadmin_password: SomeADMINpassword users: @@ -85,6 +91,42 @@ register: result failed_when: result.changed or result.failed + - name: Rename users user1 and user2 to mod1 and mod1 + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: user1 + rename: mod1 + - name: user2 + rename: mod2 + state: renamed + register: result + failed_when: not result.changed or result.failed + + - name: Rename users mod1 and mod2 to the same name + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: mod1 + rename: mod1 + - name: mod2 + rename: mod2 + state: renamed + register: result + failed_when: result.changed or result.failed + + - name: Rename users mod1 and mod2 back to user1 and user2 + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: mod1 + rename: user1 + - name: mod2 + rename: user2 + state: renamed + register: result + failed_when: not result.changed or result.failed + # failed_when: not result.failed has been added as this test needs to # fail because two users with the same name should be added in the same # task.