diff --git a/ci/roles/identity_user/defaults/main.yml b/ci/roles/identity_user/defaults/main.yml
index 53954500..d7ae1079 100644
--- a/ci/roles/identity_user/defaults/main.yml
+++ b/ci/roles/identity_user/defaults/main.yml
@@ -1,4 +1,4 @@
-os_identity_user_fields:
+expected_fields:
   - default_project_id
   - description
   - domain_id
diff --git a/ci/roles/identity_user/tasks/main.yml b/ci/roles/identity_user/tasks/main.yml
index 7696fdbd..37f123e0 100644
--- a/ci/roles/identity_user/tasks/main.yml
+++ b/ci/roles/identity_user/tasks/main.yml
@@ -1,197 +1,218 @@
 ---
-- name: setup
-  block:
-  - name: Delete user before running tests
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: absent
-      name: "{{ item }}"
-    loop:
-      - ansible_user
-      - ansible_user2
-    register: user
+- name: Create a user without a password
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    email: ansible.user@nowhere.net
+    domain: default
+    description: "ansible user"
+    default_project: demo
+  register: user
 
-- block:
-  - name: Delete unexistent user
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: absent
-      name: ansible_user
-    register: user
+- name: Assert return values of identity_user module
+  assert:
+    that:
+      - user.user.name == 'ansible_user'
+      - user.user.description == 'ansible user'
+      # allow new fields to be introduced but prevent fields from being removed
+      - expected_fields|difference(user.user.keys())|length == 0
 
-  - name: Ensure user was not changed
-    assert:
-      that: user is not changed
+- name: Fail when update_password is always but no password specified
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    update_password: always
+    email: ansible.user@nowhere.net
+    domain: default
+    default_project: demo
+  register: user
+  ignore_errors: yes
 
-- block:
-  - name: Create a user without a password
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      email: ansible.user@nowhere.net
-      domain: default
-      default_project: demo
-    register: user
+- name: Assert that update failed
+  assert:
+    that:
+      - user is failed
+      - user.msg == "update_password is 'always' but password is missing"
 
-  - name: Ensure user was changed
-    assert:
-      that: user is changed
+- name: Delete user
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_user
 
-  - name: Ensure user has fields
-    assert:
-      that: item in user['user']
-    loop: "{{ os_identity_user_fields }}"
 
-  - name: Fail when update_password is always but no password specified
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      update_password: always
-      email: ansible.user@nowhere.net
-      domain: default
-      default_project: demo
-    register: user
-    ignore_errors: yes
+- name: Create user with a password
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    password: secret
+    email: ansible.user@nowhere.net
+    update_password: on_create
+    domain: default
+    default_project: demo
 
-  - assert:
-      that: user.msg == "update_password is always but a password value is missing"
+- name: Create user with a password again
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    password: secret
+    email: ansible.user@nowhere.net
+    update_password: on_create
+    domain: default
+    default_project: demo
+  register: user
 
-  - name: Delete user
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: absent
-      name: ansible_user
+- name: Assert user was not changed
+  assert:
+    that:
+      - user is not changed
 
-- block:
-  - name: Create user with a password
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      password: secret
-      email: ansible.user@nowhere.net
-      update_password: on_create
-      domain: default
-      default_project: demo
-    register: user
+- name: Update user with password
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    password: secret2
+    email: updated.ansible.user@nowhere.net
+  register: user
 
-  - name: Assert user has fields
-    assert:
-      that: item in user['user']
-    loop: "{{ os_identity_user_fields }}"
-
-- block:
-  - name: Create identical user
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      password: secret
-      email: ansible.user@nowhere.net
-      update_password: on_create
-      domain: default
-      default_project: demo
-    register: user
-
-  - name: Assert user was not changed
-    assert:
-      that: user is not changed
-
-  - name: Assert user has fields
-    assert:
-      that: item in user['user']
-    loop: "{{ os_identity_user_fields }}"
-
-- block:
-  - name: Update user with password
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      password: secret2
-      email: updated.ansible.user@nowhere.net
-    register: user
-
-  - name: Ensure user was changed
-    assert:
-      that: user is changed
-
-  - name: Ensure user has fields
-    assert:
-      that: item in user['user']
-    loop: "{{ os_identity_user_fields }}"
+- name: Ensure user was changed
+  assert:
+    that:
+      - user is changed
 
 - name: Update user without password and update_password set to always
-  block:
-  - openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      update_password: always
-      email: updated.ansible.user@nowhere.net
-    register: user
-    ignore_errors: yes
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    update_password: always
+    email: updated.ansible.user@nowhere.net
+  register: user
+  ignore_errors: yes
 
-  - assert:
-      that: user.msg == "update_password is always but a password value is missing"
+- name: Assert user update failed
+  assert:
+    that:
+      - user is failed
+      - user.msg == "update_password is 'always' but password is missing"
 
-- block:
-  - name: Ensure user with update_password set to on_create
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      update_password: on_create
-      password: secret3
-      email: updated.ansible.user@nowhere.net
-    register: user
+- name: Ensure user with update_password set to on_create
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    update_password: on_create
+    password: secret3
+    email: updated.ansible.user@nowhere.net
+  register: user
 
-  - name: Ensure user was not changed
-    assert:
-      that: user is not changed
+- name: Ensure user was not changed
+  assert:
+    that:
+      - user is not changed
 
-- block:
-  - name: Ensure user with update_password set to always
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user
-      update_password: always
-      password: secret3
-      email: updated.ansible.user@nowhere.net
-    register: user
+- name: Ensure user with update_password set to always
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user
+    update_password: always
+    password: secret3
+    email: updated.ansible.user@nowhere.net
+  register: user
 
-  - name: Ensure user was changed
-    assert:
-      that: user is changed
+- name: Ensure user was changed
+  assert:
+    that:
+      - user is changed
 
-- block:
-  - name: Create user without a password
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user2
-      password: secret
-      email: ansible.user2@nowhere.net
-      update_password: on_create
-      domain: default
-      default_project: demo
-    register: user
+- name: Create user without a password
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_user2
+    password: secret
+    email: ansible.user2@nowhere.net
+    update_password: on_create
+    domain: default
+    default_project: demo
+  register: user
 
-  - name: Assert user has fields
-    assert:
-      that: item in user['user']
-    loop: "{{ os_identity_user_fields }}"
+- name: Fetch users
+  openstack.cloud.identity_user_info:
+    cloud: "{{ cloud }}"
+  register: users
 
-- block:
-  - name: Delete user
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: absent
-      name: ansible_user
+- name: Assert return values of identity_user_info module
+  assert:
+    that:
+      - users.users | length > 0
+      # allow new fields to be introduced but prevent fields from being removed
+      - expected_fields|difference(users.users.0.keys())|length == 0
 
-  - name: Ensure user was changed
-    assert:
-      that: user is changed
+- name: Fetch user by name
+  openstack.cloud.identity_user_info:
+    cloud: "{{ cloud }}"
+    name: ansible_user
+  register: users
+
+- name: Assert named user
+  assert:
+    that:
+      - users.users | length == 1
+
+- name: Delete user
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_user2
+
+- name: Delete user
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_user
+
+- name: Ensure user was changed
+  assert:
+    that:
+      - user is changed
+
+- name: Delete user again
+  openstack.cloud.identity_user:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_user
+  register: user
+
+- name: Ensure user was not changed
+  assert:
+    that:
+      - user is not changed
+
+- name: Fetch ansible_user
+  openstack.cloud.identity_user_info:
+    cloud: "{{ cloud }}"
+    name: ansible_user
+  register: users
+
+- name: Assert ansible_user does not exist
+  assert:
+    that:
+      - users.users | length == 0
+
+- name: Fetch ansible_user2
+  openstack.cloud.identity_user_info:
+    cloud: "{{ cloud }}"
+    name: ansible_user2
+  register: users
+
+- name: Assert ansible_user2 does not exist
+  assert:
+    that:
+      - users.users | length == 0
diff --git a/ci/roles/identity_user_info/defaults/main.yml b/ci/roles/identity_user_info/defaults/main.yml
deleted file mode 100644
index 5f2f1447..00000000
--- a/ci/roles/identity_user_info/defaults/main.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-os_expected_user_info_fields:
-  - default_project_id
-  - description
-  - domain_id
-  - email
-  - id
-  - is_enabled
-  - links
-  - name
-  - password
-  - password_expires_at
diff --git a/ci/roles/identity_user_info/tasks/main.yml b/ci/roles/identity_user_info/tasks/main.yml
deleted file mode 100644
index b03fc37c..00000000
--- a/ci/roles/identity_user_info/tasks/main.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-- name: Ensure user does not exist before tests
-  openstack.cloud.identity_user:
-     cloud: "{{ cloud }}"
-     state: absent
-     name: ansible_user
-
-- block:
-  - name: Get unexistent user
-    openstack.cloud.identity_user_info:
-      cloud: "{{ cloud }}"
-      name: ansible_user
-    register: userinfo
-  - name: Ensure nothing was returned
-    assert:
-      that: not userinfo.users
-
-- block:
-  - name: Create user
-    openstack.cloud.identity_user:
-       cloud: "{{ cloud }}"
-       state: present
-       name: ansible_user
-       password: secret
-       email: ansible.user@nowhere.net
-       domain: default
-       default_project: demo
-    register: user
-  - name: Create second user
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: present
-      name: ansible_user2
-      password: secret
-      email: ansible.user2@nowhere.net
-      domain: default
-      default_project: demo
-    register: user
-  - name: Get first user info
-    openstack.cloud.identity_user_info:
-      cloud: "{{ cloud }}"
-      name: ansible_user
-    register: userinfo
-  - name: Assert only one result exists
-    assert:
-      that: "{{ userinfo.users | length }} == 1"
-  - name: Assert userinfo has fields
-    assert:
-      that: item in userinfo.users[0]
-    loop: "{{ os_expected_user_info_fields }}"
-
-- block:
-  - name: Get all users
-    openstack.cloud.identity_user_info:
-      cloud: "{{ cloud }}"
-    register: userinfo
-  - name: Assert results were returned
-    assert:
-      that: "{{ userinfo.users | length }} > 0"
-
-- name: Post-test cleanup
-  block:
-  - name: Ensure users do not exist
-    openstack.cloud.identity_user:
-      cloud: "{{ cloud }}"
-      state: absent
-      name: "{{ item }}"
-    loop:
-      - ansible_user
-      - ansible_user2
diff --git a/ci/run-collection.yml b/ci/run-collection.yml
index ad28879e..338f1182 100644
--- a/ci/run-collection.yml
+++ b/ci/run-collection.yml
@@ -21,7 +21,6 @@
     - { role: identity_domain, tags: identity_domain }
     - { role: identity_group, tags: identity_group }
     - { role: identity_user, tags: identity_user }
-    - { role: identity_user_info, tags: identity_user_info }
     - { role: identity_role, tags: identity_role }
     - { role: image, tags: image }
     - { role: keypair, tags: keypair }
diff --git a/plugins/modules/identity_user.py b/plugins/modules/identity_user.py
index ca6feb2c..c4e7dff6 100644
--- a/plugins/modules/identity_user.py
+++ b/plugins/modules/identity_user.py
@@ -4,69 +4,68 @@
 # Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
-DOCUMENTATION = '''
+DOCUMENTATION = r'''
 ---
 module: identity_user
-short_description: Manage OpenStack Identity Users
+short_description: Manage a OpenStack identity (Keystone) user
 author: OpenStack Ansible SIG
 description:
-    - Manage OpenStack Identity users. Users can be created,
-      updated or deleted using this module. A user will be updated
-      if I(name) matches an existing user and I(state) is present.
-      The value for I(name) cannot be updated without deleting and
-      re-creating the user.
+  - Create, update or delete a OpenStack identity (Keystone) user.
 options:
-   name:
-     description:
-        - Username for the user
-     required: true
-     type: str
-   password:
-     description:
-        - Password for the user
-     type: str
-   update_password:
-     required: false
-     choices: ['always', 'on_create']
-     default: on_create
-     description:
-        - C(always) will attempt to update password.  C(on_create) will only
-          set the password for newly created users.
-     type: str
-   email:
-     description:
-        - Email address for the user
-     type: str
-   description:
-     description:
-        - Description about the user
-     type: str
-   default_project:
-     description:
-        - Project name or ID that the user should be associated with by default
-     type: str
-   domain:
-     description:
-        - Domain to create the user in if the cloud supports domains
-     type: str
-   enabled:
-     description:
-        - Is the user enabled
-     type: bool
-     default: 'yes'
-   state:
-     description:
-       - Should the resource be present or absent.
-     choices: [present, absent]
-     default: present
-     type: str
+  default_project:
+    description:
+      - Name or ID of the project, the user should be created in.
+    type: str
+  description:
+    description:
+      - Description about the user.
+    type: str
+  domain:
+    description:
+      - Domain to create the user in if the cloud supports domains.
+    type: str
+  email:
+    description:
+      - Email address for the user.
+    type: str
+  is_enabled:
+    description:
+      - Whether the user is enabled or not.
+    type: bool
+    default: 'yes'
+    aliases: ['enabled']
+  name:
+    description:
+      - Name of the user.
+      - I(name) cannot be updated without deleting and re-creating the user.
+    required: true
+    type: str
+  password:
+    description:
+      - Password for the user.
+    type: str
+  state:
+    description:
+      - Should the resource be present or absent.
+    choices: [present, absent]
+    default: present
+    type: str
+  update_password:
+    choices: ['always', 'on_create']
+    default: on_create
+    description:
+      - When I(update_password) is C(always), then the password will always be
+        updated.
+      - When I(update_password) is C(on_create), the the password is only set
+        when creating a user.
+    type: str
 extends_documentation_fragment:
-- openstack.cloud.openstack
+  - openstack.cloud.openstack
 '''
 
-EXAMPLES = '''
-# Create a user
-- openstack.cloud.identity_user:
+EXAMPLES = r'''
+- name: Create a user
+  openstack.cloud.identity_user:
     cloud: mycloud
     state: present
     name: demouser
@@ -75,14 +74,14 @@ EXAMPLES = '''
     domain: default
     default_project: demo
 
-# Delete a user
-- openstack.cloud.identity_user:
+- name: Delete a user
+  openstack.cloud.identity_user:
     cloud: mycloud
     state: absent
     name: demouser
 
-# Create a user but don't update password if user exists
-- openstack.cloud.identity_user:
+- name: Create a user but don't update password if user exists
+  openstack.cloud.identity_user:
     cloud: mycloud
     state: present
     name: demouser
@@ -92,8 +91,8 @@ EXAMPLES = '''
     domain: default
     default_project: demo
 
-# Create a user without password
-- openstack.cloud.identity_user:
+- name: Create a user without password
+  openstack.cloud.identity_user:
     cloud: mycloud
     state: present
     name: demouser
@@ -102,158 +101,137 @@ EXAMPLES = '''
     default_project: demo
 '''
 
-
-RETURN = '''
+RETURN = r'''
 user:
-    description: Dictionary describing the user.
-    returned: On success when I(state) is 'present'
-    type: dict
-    contains:
-        default_project_id:
-            description: User default project ID. Only present with Keystone >= v3.
-            returned: success
-            type: str
-            sample: "4427115787be45f08f0ec22a03bfc735"
-        description:
-            description: The description of this user
-            returned: success
-            type: str
-            sample: "a user"
-        domain_id:
-            description: User domain ID. Only present with Keystone >= v3.
-            returned: success
-            type: str
-            sample: "default"
-        email:
-            description: User email address
-            returned: success
-            type: str
-            sample: "demo@example.com"
-        id:
-            description: User ID
-            returned: success
-            type: str
-            sample: "f59382db809c43139982ca4189404650"
-        is_enabled:
-            description: Indicates whether the user is enabled
-            type: bool
-        links:
-            description: The links for the user resource
-            returned: success
-            type: dict
-            elements: str
-        name:
-            description: Unique user name, within the owning domain
-            returned: success
-            type: str
-            sample: "demouser"
-        password:
-            description: Credential used during authentication
-            returned: success
-            type: str
-        password_expires_at:
-            description: The date and time when the password expires. The time zone is UTC. A none value means the password never expires
-            returned: success
-            type: str
-
+  description: Dictionary describing the identity user.
+  returned: On success when I(state) is C(present).
+  type: dict
+  contains:
+    default_project_id:
+      description: User default project ID. Only present with Keystone >= v3.
+      type: str
+      sample: "4427115787be45f08f0ec22a03bfc735"
+    description:
+      description: The description of this user
+      type: str
+      sample: "a user"
+    domain_id:
+      description: User domain ID. Only present with Keystone >= v3.
+      type: str
+      sample: "default"
+    email:
+      description: User email address
+      type: str
+      sample: "demo@example.com"
+    id:
+      description: User ID
+      type: str
+      sample: "f59382db809c43139982ca4189404650"
+    is_enabled:
+      description: Indicates whether the user is enabled
+      type: bool
+    links:
+      description: The links for the user resource
+      type: dict
+      elements: str
+    name:
+      description: Unique user name, within the owning domain
+      type: str
+      sample: "demouser"
+    password:
+      description: Credential used during authentication
+      type: str
+    password_expires_at:
+      description: The date and time when the password expires. The time zone
+                   is UTC. A none value means the password never expires
+      type: str
 '''
 
 from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
+from ansible_collections.openstack.cloud.plugins.module_utils.resource import StateMachine
 
 
 class IdentityUserModule(OpenStackModule):
     argument_spec = dict(
-        name=dict(required=True),
-        password=dict(no_log=True),
-        email=dict(),
         default_project=dict(),
         description=dict(),
         domain=dict(),
-        enabled=dict(default=True, type='bool'),
+        email=dict(),
+        is_enabled=dict(default=True, type='bool', aliases=['enabled']),
+        name=dict(required=True),
+        password=dict(no_log=True),
         state=dict(default='present', choices=['absent', 'present']),
-        update_password=dict(default='on_create', choices=['always', 'on_create']),
+        update_password=dict(default='on_create',
+                             choices=['always', 'on_create']),
     )
 
     module_kwargs = dict()
 
-    def _needs_update(self, params_dict, user):
-        for k in params_dict:
-            # We don't get password back in the user object, so assume any supplied
-            # password is a change.
-            if k == 'password':
-                return True
-            if user[k] != params_dict[k]:
-                return True
-        return False
+    class _StateMachine(StateMachine):
+        def _build_update(self, resource, attributes, updateable_attributes,
+                          non_updateable_attributes,
+                          update_password='on_create', **kwargs):
+            if update_password == 'always' and 'password' not in attributes:
+                self.ansible.fail_json(msg="update_password is 'always'"
+                                           " but password is missing")
+            elif update_password == 'on_create' and 'password' in attributes:
+                attributes.pop('password')
 
-    def _get_domain_id(self, domain):
-        dom_obj = self.conn.identity.find_domain(domain)
-        if dom_obj is None:
-            # Ok, let's hope the user is non-admin and passing a sane id
-            return domain
-        return dom_obj.id
+            return super()._build_update(resource, attributes,
+                                         updateable_attributes,
+                                         non_updateable_attributes, **kwargs)
 
-    def _get_default_project_id(self, default_project, domain_id):
-        project = self.conn.identity.find_project(default_project, domain_id=domain_id)
-        if not project:
-            self.fail_json(msg='Default project %s is not valid' % default_project)
-        return project['id']
+        def _find(self, attributes, **kwargs):
+            query_args = dict((k, attributes[k])
+                              for k in ['domain_id']
+                              if k in attributes and attributes[k] is not None)
+
+            return self.find_function(attributes['name'], **query_args)
 
     def run(self):
-        name = self.params['name']
-        password = self.params.get('password')
-        email = self.params['email']
-        default_project = self.params['default_project']
-        domain = self.params['domain']
-        enabled = self.params['enabled']
-        state = self.params['state']
-        update_password = self.params['update_password']
-        description = self.params['description']
+        sm = self._StateMachine(connection=self.conn,
+                                service_name='identity',
+                                type_name='user',
+                                sdk=self.sdk,
+                                ansible=self.ansible)
 
-        domain_id = None
-        if domain:
-            domain_id = self._get_domain_id(domain)
-        user = self.conn.identity.find_user(name, domain_id=domain_id)
+        kwargs = dict((k, self.params[k])
+                      for k in ['state', 'timeout', 'update_password']
+                      if self.params[k] is not None)
 
-        changed = False
-        if state == 'present':
-            user_args = {
-                'name': name,
-                'email': email,
-                'domain_id': domain_id,
-                'description': description,
-                'is_enabled': enabled,
-            }
-            if default_project:
-                default_project_id = self._get_default_project_id(
-                    default_project, domain_id)
-                user_args['default_project_id'] = default_project_id
-            user_args = {k: v for k, v in user_args.items() if v is not None}
+        kwargs['attributes'] = \
+            dict((k, self.params[k])
+                 for k in ['description', 'email', 'is_enabled', 'name',
+                           'password']
+                 if self.params[k] is not None)
 
-            changed = False
-            if user is None:
-                if password:
-                    user_args['password'] = password
+        domain_name_or_id = self.params['domain']
+        if domain_name_or_id is not None:
+            domain = self.conn.identity.find_domain(domain_name_or_id,
+                                                    ignore_missing=False)
+            kwargs['attributes']['domain_id'] = domain.id
 
-                user = self.conn.identity.create_user(**user_args)
-                changed = True
-            else:
-                if update_password == 'always':
-                    if not password:
-                        self.fail_json(msg="update_password is always but a password value is missing")
-                    user_args['password'] = password
-                # else we do not want to update the password
+        default_project_name_or_id = self.params['default_project']
+        if default_project_name_or_id is not None:
+            query_args = dict((k, kwargs['attributes'][k])
+                              for k in ['domain_id']
+                              if k in kwargs['attributes']
+                              and kwargs['attributes'][k] is not None)
+            project = self.conn.identity.find_project(
+                default_project_name_or_id, ignore_missing=False, **query_args)
+            kwargs['attributes']['default_project_id'] = project.id
 
-                if self._needs_update(user_args, user):
-                    user = self.conn.identity.update_user(user['id'], **user_args)
-                    changed = True
+        user, is_changed = sm(check_mode=self.ansible.check_mode,
+                              updateable_attributes=None,
+                              non_updateable_attributes=['domain_id'],
+                              wait=False,
+                              **kwargs)
 
-            user = user.to_dict(computed=False)
-            self.exit_json(changed=changed, user=user)
-        elif state == 'absent' and user is not None:
-            self.conn.identity.delete_user(user)
-            changed = True
-        self.exit_json(changed=changed)
+        if user is None:
+            self.exit_json(changed=is_changed)
+        else:
+            self.exit_json(changed=is_changed,
+                           user=user.to_dict(computed=False))
 
 
 def main():
diff --git a/plugins/modules/identity_user_info.py b/plugins/modules/identity_user_info.py
index 1a0f3b7a..b55b9fbd 100644
--- a/plugins/modules/identity_user_info.py
+++ b/plugins/modules/identity_user_info.py
@@ -4,126 +4,98 @@
 # Copyright (c) 2016 Hewlett-Packard Enterprise Corporation
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
-DOCUMENTATION = '''
+DOCUMENTATION = r'''
 ---
 module: identity_user_info
-short_description: Retrieve information about one or more OpenStack users
+short_description: Fetch OpenStack identity (Keystone) users
 author: OpenStack Ansible SIG
 description:
-    - Retrieve information about a one or more OpenStack users
+  - Fetch OpenStack identity (Keystone) users.
 options:
-   name:
-     description:
-        - Name or ID of the user
-     type: str
-   domain:
-     description:
-        - Name or ID of the domain containing the user if the cloud supports domains
-     type: str
-   filters:
-     description:
-        - A dictionary of meta data to use for further filtering.  Elements of
-          this dictionary may be additional dictionaries.
-     type: dict
-     default: {}
+  domain:
+    description:
+      - Name or ID of the domain containing the user.
+    type: str
+  filters:
+    description:
+      - A dictionary of meta data to use for further filtering. Elements of
+        this dictionary may be additional dictionaries.
+    type: dict
+  name:
+    description:
+      - Name or ID of the user.
+    type: str
 extends_documentation_fragment:
-- openstack.cloud.openstack
+  - openstack.cloud.openstack
 '''
 
-EXAMPLES = '''
-# Gather information about previously created users
-- openstack.cloud.identity_user_info:
+EXAMPLES = r'''
+- name: Gather previously created users
+  openstack.cloud.identity_user_info:
     cloud: awesomecloud
-  register: result
-- debug:
-    msg: "{{ result.users }}"
 
-# Gather information about a previously created user by name
-- openstack.cloud.identity_user_info:
+- name: Gather previously created user by name
+  openstack.cloud.identity_user_info:
     cloud: awesomecloud
     name: demouser
-  register: result
-- debug:
-    msg: "{{ result.users }}"
 
-# Gather information about a previously created user in a specific domain
-- openstack.cloud.identity_user_info:
+- name: Gather previously created user in a specific domain
+  openstack.cloud.identity_user_info:
     cloud: awesomecloud
     name: demouser
     domain: admindomain
-  register: result
-- debug:
-    msg: "{{ result.users }}"
 
-# Gather information about a previously created user in a specific domain with filter
-- openstack.cloud.identity_user_info:
+- name: Gather previously created user with filters
+  openstack.cloud.identity_user_info:
     cloud: awesomecloud
     name: demouser
     domain: admindomain
     filters:
-      enabled: False
-  register: result
-- debug:
-    msg: "{{ result.users }}"
+      is_enabled: False
 '''
 
-
-RETURN = '''
+RETURN = r'''
 users:
-    description: has all the OpenStack information about users
-    returned: always
-    type: list
-    elements: dict
-    contains:
-        id:
-            description: Unique UUID.
-            returned: success
-            type: str
-        name:
-            description: Username of the user.
-            returned: success
-            type: str
-        default_project_id:
-            description: Default project ID of the user
-            returned: success
-            type: str
-        description:
-            description: The description of this user
-            returned: success
-            type: str
-        domain_id:
-            description: Domain ID containing the user
-            returned: success
-            type: str
-        email:
-            description: Email of the user
-            returned: success
-            type: str
-        is_enabled:
-            description: Flag to indicate if the user is enabled
-            returned: success
-            type: bool
-        links:
-            description: The links for the user resource
-            returned: success
-            type: complex
-            contains:
-                self:
-                    description: Link to this user resource
-                    returned: success
-                    type: str
-        password:
-            description: The default form of credential used during authentication.
-            returned: success
-            type: str
-        password_expires_at:
-            description: The date and time when the password expires. The time zone is UTC. A Null value means the password never expires.
-            returned: success
-            type: str
-        username:
-            description: Username with Identity API v2 (OpenStack Pike or earlier) else Null
-            returned: success
-            type: str
+  description: Dictionary describing all matching identity users.
+  returned: always
+  type: list
+  elements: dict
+  contains:
+    id:
+      description: Unique UUID.
+      type: str
+    name:
+      description: Username of the user.
+      type: str
+    default_project_id:
+      description: Default project ID of the user
+      type: str
+    description:
+      description: The description of this user
+      type: str
+    domain_id:
+      description: Domain ID containing the user
+      type: str
+    email:
+      description: Email of the user
+      type: str
+    is_enabled:
+      description: Flag to indicate if the user is enabled
+      type: bool
+    links:
+      description: The links for the user resource
+      type: dict
+    password:
+      description: The default form of credential used during authentication.
+      type: str
+    password_expires_at:
+      description: The date and time when the password expires. The time zone
+                   is UTC. A Null value means the password never expires.
+      type: str
+    username:
+      description: Username with Identity API v2 (OpenStack Pike or earlier)
+                   else Null.
+      type: str
 '''
 
 from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
@@ -131,9 +103,9 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O
 
 class IdentityUserInfoModule(OpenStackModule):
     argument_spec = dict(
-        name=dict(),
         domain=dict(),
-        filters=dict(type='dict', default={}),
+        filters=dict(type='dict'),
+        name=dict(),
     )
     module_kwargs = dict(
         supports_check_mode=True
@@ -141,19 +113,20 @@ class IdentityUserInfoModule(OpenStackModule):
 
     def run(self):
         name = self.params['name']
-        domain = self.params['domain']
-        filters = self.params['filters']
+        filters = self.params['filters'] or {}
 
-        args = {}
-        if domain:
-            dom_obj = self.conn.identity.find_domain(domain)
-            if dom_obj is None:
-                self.fail_json(
-                    msg="Domain name or ID '{0}' does not exist".format(domain))
-            args['domain_id'] = dom_obj.id
+        kwargs = {}
+        domain_name_or_id = self.params['domain']
+        if domain_name_or_id:
+            domain = self.conn.identity.find_domain(domain_name_or_id)
+            if domain is None:
+                self.exit_json(changed=False, groups=[])
+            kwargs['domain_id'] = domain['id']
 
-        users = [user.to_dict(computed=False) for user in self.conn.search_users(name, filters, **args)]
-        self.exit_json(changed=False, users=users)
+        self.exit_json(changed=False,
+                       users=[u.to_dict(computed=False)
+                              for u in self.conn.search_users(name, filters,
+                                                              **kwargs)])
 
 
 def main():