diff --git a/ci/roles/group_assignment/tasks/main.yml b/ci/roles/group_assignment/tasks/main.yml new file mode 100644 index 00000000..ccd66c60 --- /dev/null +++ b/ci/roles/group_assignment/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- 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 + +- name: Assign user to nonadmins group + openstack.cloud.group_assignment: + cloud: "{{ cloud }}" + state: present + user: ansible_user + group: nonadmins + register: group_assignment + +- name: Assert group assignment + assert: + that: + - group_assignment is changed + +- name: Assign user to nonadmins group again + openstack.cloud.group_assignment: + cloud: "{{ cloud }}" + state: present + user: ansible_user + group: nonadmins + register: group_assignment + +- name: Assert group assignment + assert: + that: + - group_assignment is not changed + +- name: Remove user from nonadmins group + openstack.cloud.group_assignment: + cloud: "{{ cloud }}" + state: absent + user: ansible_user + group: nonadmins + register: group_assignment + +- name: Assert group assignment + assert: + that: + - group_assignment is changed + +- name: Remove user from nonadmins group again + openstack.cloud.group_assignment: + cloud: "{{ cloud }}" + state: absent + user: ansible_user + group: nonadmins + register: group_assignment + +- name: Assert group assignment + assert: + that: + - group_assignment is not changed + +- name: Delete user + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user diff --git a/ci/roles/role_assignment/tasks/main.yml b/ci/roles/role_assignment/tasks/main.yml index 0ea9e6cf..cff79e4d 100644 --- a/ci/roles/role_assignment/tasks/main.yml +++ b/ci/roles/role_assignment/tasks/main.yml @@ -15,6 +15,12 @@ project: ansible_project role: admin user: admin + register: role_assignment + +- name: Assert role assignment + assert: + that: + - role_assignment is changed - name: Grant an admin role on the user admin in the project ansible_project again openstack.cloud.role_assignment: @@ -23,12 +29,12 @@ project: ansible_project role: admin user: admin - register: grant_again + register: role_assignment -- name: Ensure grant again doesn't change anything +- name: Ensure grant again did not change anything assert: that: - - not grant_again.changed + - role_assignment is not changed - name: Revoke the admin role on the user admin in the project ansible_project openstack.cloud.role_assignment: @@ -44,3 +50,124 @@ cloud: "{{ cloud }}" state: absent name: ansible_project + +- name: Create domain + openstack.cloud.identity_domain: + cloud: "{{ cloud }}" + state: present + name: ansible_domain + register: domain + +- name: Create group in default domain + openstack.cloud.identity_group: + cloud: "{{ cloud }}" + state: present + name: ansible_group + domain_id: default + +- name: Create group in specific domain + openstack.cloud.identity_group: + cloud: "{{ cloud }}" + state: present + name: ansible_group + domain_id: "{{ domain.domain.id }}" + +- name: Create user in default domain + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + domain: default + +- name: Create user in specific domain + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + domain: "{{ domain.domain.id }}" + +- name: Assign role to group in default domain + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + group: ansible_group + domain: default + register: role_assignment + +- name: Assert role assignment + assert: + that: + - role_assignment is changed + +- name: Assign role to group in specific domain + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + group: ansible_group + domain: "{{ domain.domain.id }}" + register: role_assignment + +- name: Assert role assignment + assert: + that: + - role_assignment is changed + +- name: Assign role to user in default domain + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + user: ansible_user + domain: default + register: role_assignment + +- name: Assert role assignment + assert: + that: + - role_assignment is changed + +- name: Assign role to user in specific domain + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + user: ansible_user + domain: "{{ domain.domain.id }}" + register: role_assignment + +- name: Assert role assignment + assert: + that: + - role_assignment is changed + +- name: Delete group in default domain + openstack.cloud.identity_group: + cloud: "{{ cloud }}" + state: absent + name: ansible_group + domain_id: default + +- name: Delete group in specific domain + openstack.cloud.identity_group: + cloud: "{{ cloud }}" + state: absent + name: ansible_group + domain_id: "{{ domain.domain.id }}" + +- name: Delete user in default domain + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user + domain: default + +- name: Delete user in specific domain + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user + domain: "{{ domain.domain.id }}" + +- name: Delete domain + openstack.cloud.identity_domain: + cloud: "{{ cloud }}" + state: absent + name: ansible_domain diff --git a/ci/roles/user_group/tasks/main.yml b/ci/roles/user_group/tasks/main.yml deleted file mode 100644 index a638bf9a..00000000 --- a/ci/roles/user_group/tasks/main.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -- 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: Assign user to nonadmins group - openstack.cloud.group_assignment: - cloud: "{{ cloud }}" - state: present - user: ansible_user - group: nonadmins - -- name: Remove user from nonadmins group - openstack.cloud.group_assignment: - cloud: "{{ cloud }}" - state: absent - user: ansible_user - group: nonadmins - -- name: Delete user - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: absent - name: ansible_user diff --git a/ci/roles/user_role/defaults/main.yaml b/ci/roles/user_role/defaults/main.yaml deleted file mode 100644 index 0c09cb7f..00000000 --- a/ci/roles/user_role/defaults/main.yaml +++ /dev/null @@ -1,4 +0,0 @@ -domain_name: ansible_domain -user_name: ansible_user -group_name: ansible_group -keystone_role_name: anotherrole diff --git a/ci/roles/user_role/tasks/main.yaml b/ci/roles/user_role/tasks/main.yaml deleted file mode 100644 index fe340b15..00000000 --- a/ci/roles/user_role/tasks/main.yaml +++ /dev/null @@ -1,96 +0,0 @@ -- name: Create domain - openstack.cloud.identity_domain: - cloud: "{{ cloud }}" - state: present - name: "{{ domain_name }}" - register: domain - -- name: Create group in default domain - openstack.cloud.identity_group: - cloud: "{{ cloud }}" - state: present - name: "{{ group_name }}" - domain_id: default - -- name: Create group in specific domain - openstack.cloud.identity_group: - cloud: "{{ cloud }}" - state: present - name: "{{ group_name }}" - domain_id: "{{ domain.domain.id }}" - -- name: Create user in default domain - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: present - name: "{{ user_name }}" - domain: default - -- name: Create user in specific domain - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: present - name: "{{ user_name }}" - domain: "{{ domain.domain.id }}" - -- name: Assign role to group in default domain - openstack.cloud.role_assignment: - cloud: "{{ cloud }}" - role: "{{ keystone_role_name }}" - group: "{{ group_name }}" - domain: default - -- name: Assign role to group in specific domain - openstack.cloud.role_assignment: - cloud: "{{ cloud }}" - role: "{{ keystone_role_name }}" - group: "{{ group_name }}" - domain: "{{ domain.domain.id }}" - -- name: Assign role to user in default domain - openstack.cloud.role_assignment: - cloud: "{{ cloud }}" - role: "{{ keystone_role_name }}" - user: "{{ user_name }}" - domain: default - -- name: Assign role to user in specific domain - openstack.cloud.role_assignment: - cloud: "{{ cloud }}" - role: "{{ keystone_role_name }}" - user: "{{ user_name }}" - domain: "{{ domain.domain.id }}" - -- name: Delete group in default domain - openstack.cloud.identity_group: - cloud: "{{ cloud }}" - state: absent - name: "{{ group_name }}" - domain_id: default - -- name: Delete group in specific domain - openstack.cloud.identity_group: - cloud: "{{ cloud }}" - state: absent - name: "{{ group_name }}" - domain_id: "{{ domain.domain.id }}" - -- name: Delete user in default domain - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: absent - name: "{{ user_name }}" - domain: default - -- name: Delete user in specific domain - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: absent - name: "{{ user_name }}" - domain: "{{ domain.domain.id }}" - -- name: Delete domain - openstack.cloud.identity_domain: - cloud: "{{ cloud }}" - state: absent - name: "{{ domain_name }}" diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 9069e03c..ab27b5bb 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -17,6 +17,7 @@ - { role: endpoint, tags: endpoint } - { role: federation_mapping, tags: federation_mapping } - { role: floating_ip, tags: floating_ip } + - { role: group_assignment, tags: group_assignment } - { role: host_aggregate, tags: host_aggregate } - { role: identity_domain, tags: identity_domain } - { role: identity_group, tags: identity_group } @@ -50,8 +51,6 @@ - { role: stack, tags: stack } - { role: subnet, tags: subnet } - { role: subnet_pool, tags: subnet_pool } - - { role: user_group, tags: user_group } - - { role: user_role, tags: user_role } - { role: volume, tags: volume } - { role: volume_backup, tags: volume_backup } - { role: volume_snapshot, tags: volume_snapshot } diff --git a/plugins/modules/group_assignment.py b/plugins/modules/group_assignment.py index 478a7d88..19e9e21b 100644 --- a/plugins/modules/group_assignment.py +++ b/plugins/modules/group_assignment.py @@ -4,40 +4,40 @@ # 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: group_assignment -short_description: Associate OpenStack Identity users and groups +short_description: Assign OpenStack identity users to groups author: OpenStack Ansible SIG description: - - Add and remove users from groups + - Add and remove OpenStack identity (Keystone) users to/from groups. options: - user: - description: - - Name or id for the user - required: true - type: str - group: - description: - - Name or id for the group. - required: true - type: str - state: - description: - - Should the user be present or absent in the group - choices: [present, absent] - default: present - type: str + group: + description: + - Name or ID for the group. + required: true + type: str + state: + description: + - Should the user be present or absent in the group. + choices: [present, absent] + default: present + type: str + user: + description: + - Name or ID for the user. + required: true + type: str extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Add the demo user to the demo group -- openstack.cloud.group_assignment: - cloud: mycloud - user: demo - group: demo +EXAMPLES = r''' +- name: Add demo_user user to demo_group group + openstack.cloud.group_assignment: + cloud: mycloud + user: demo_user + group: demo_group ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule @@ -45,44 +45,42 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class IdentityGroupAssignment(OpenStackModule): argument_spec = dict( - user=dict(required=True), group=dict(required=True), state=dict(default='present', choices=['absent', 'present']), + user=dict(required=True), ) module_kwargs = dict( supports_check_mode=True ) - def _system_state_change(self, state, in_group): - if state == 'present' and not in_group: - return True - if state == 'absent' and in_group: - return True - return False - def run(self): - user = self.params['user'] - group = self.params['group'] + user_name_or_id = self.params['user'] + user = self.conn.identity.find_user(user_name_or_id, + ignore_missing=False) + + group_name_or_id = self.params['group'] + group = self.conn.identity.find_group(group_name_or_id, + ignore_missing=False) + + is_user_in_group = \ + self.conn.identity.check_user_in_group(user, group) + state = self.params['state'] - - in_group = self.conn.is_user_in_group(user, group) - if self.ansible.check_mode: - self.exit_json(changed=self._system_state_change(state, in_group)) + self.exit_json( + changed=( + (state == 'present' and not is_user_in_group) + or (state == 'absent' and is_user_in_group))) - changed = False - if state == 'present': - if not in_group: - self.conn.add_user_to_group(user, group) - changed = True - - elif state == 'absent': - if in_group: - self.conn.remove_user_from_group(user, group) - changed = True - - self.exit_json(changed=changed) + if state == 'present' and not is_user_in_group: + self.conn.identity.add_user_to_group(user, group) + self.exit_json(changed=True) + elif state == 'absent' and is_user_in_group: + self.conn.identity.remove_user_from_group(user, group) + self.exit_json(changed=True) + else: + self.exit_json(changed=False) def main(): diff --git a/plugins/modules/role_assignment.py b/plugins/modules/role_assignment.py index 8bae40b3..5ec69923 100644 --- a/plugins/modules/role_assignment.py +++ b/plugins/modules/role_assignment.py @@ -4,67 +4,89 @@ # Copyright (c) 2016 IBM # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: role_assignment -short_description: Associate OpenStack Identity users and roles +short_description: Assign OpenStack identity groups and users to roles author: OpenStack Ansible SIG description: - - Grant and revoke roles in either project or domain context for - OpenStack Identity Users. + - Grant and revoke roles in either project or domain context for + OpenStack identity (Keystone) users and groups. options: - role: - description: - - Name or ID for the role. - required: true - type: str - user: - description: - - Name or ID for the user. If I(user) is not specified, then - I(group) is required. Both may not be specified. - type: str - group: - description: - - Name or ID for the group. Valid only with keystone version 3. - If I(group) is not specified, then I(user) is required. Both - may not be specified. - type: str - project: - description: - - Name or ID of the project to scope the role association to. - If you are using keystone version 2, then this value is required. - type: str - domain: - description: - - Name or ID of the domain to scope the role association to. Valid only - with keystone version 3, and required if I(project) is not specified. - type: str - system: - description: - - Name of system to scope the role association to. Valid only with - keystone version 3, and required if I(project) and I(domain) - are not specified. - type: str - state: - description: - - Should the roles be present or absent on the user. - choices: [present, absent] - default: present - type: str + domain: + description: + - Name or ID of the domain to scope the role association to. + - Valid only with keystone version 3. + - Required if I(project) is not specified. + - When I(project) is specified, then I(domain) will not be used for + scoping the role association, only for finding resources. + - "When scoping the role association, I(project) has precedence over + I(domain) and I(domain) has precedence over I(system): When I(project) + is specified, then I(domain) and I(system) are not used for role + association. When I(domain) is specified, then I(system) will not be + used for role association." + type: str + group: + description: + - Name or ID for the group. + - Valid only with keystone version 3. + - If I(group) is not specified, then I(user) is required. Both may not be + specified at the same time. + type: str + project: + description: + - Name or ID of the project to scope the role association to. + - If you are using keystone version 2, then this value is required. + - When I(project) is specified, then I(domain) will not be used for + scoping the role association, only for finding resources. + - "When scoping the role association, I(project) has precedence over + I(domain) and I(domain) has precedence over I(system): When I(project) + is specified, then I(domain) and I(system) are not used for role + association. When I(domain) is specified, then I(system) will not be + used for role association." + type: str + role: + description: + - Name or ID for the role. + required: true + type: str + state: + description: + - Should the roles be present or absent on the user. + choices: [present, absent] + default: present + type: str + system: + description: + - Name of system to scope the role association to. + - Valid only with keystone version 3. + - Required if I(project) and I(domain) are not specified. + - "When scoping the role association, I(project) has precedence over + I(domain) and I(domain) has precedence over I(system): When I(project) + is specified, then I(domain) and I(system) are not used for role + association. When I(domain) is specified, then I(system) will not be + used for role association." + type: str + user: + description: + - Name or ID for the user. + - If I(user) is not specified, then I(group) is required. Both may not be + specified at the same time. + type: str extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Grant an admin role on the user admin in the project project1 -- openstack.cloud.role_assignment: +EXAMPLES = r''' +- name: Grant an admin role on the user admin in the project project1 + openstack.cloud.role_assignment: cloud: mycloud user: admin role: admin project: project1 -# Revoke the admin role from the user barney in the newyork domain -- openstack.cloud.role_assignment: +- name: Revoke the admin role from the user barney in the newyork domain + openstack.cloud.role_assignment: cloud: mycloud state: absent user: barney @@ -72,123 +94,95 @@ EXAMPLES = ''' domain: newyork ''' -RETURN = ''' -# -''' - from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule class IdentityRoleAssignmentModule(OpenStackModule): argument_spec = dict( - role=dict(required=True), - user=dict(), + domain=dict(), group=dict(), project=dict(), - domain=dict(), - system=dict(), + role=dict(required=True), state=dict(default='present', choices=['absent', 'present']), + system=dict(), + user=dict(), ) module_kwargs = dict( required_one_of=[ - ['user', 'group'] + ('user', 'group'), + ('domain', 'project', 'system'), ], supports_check_mode=True ) - def _system_state_change(self, state, assignment): - if state == 'present' and not assignment: - return True - elif state == 'absent' and assignment: - return True - return False - - def _build_kwargs(self, user, group, project, domain, system): - kwargs = {} - if user: - kwargs['user'] = user - if group: - kwargs['group'] = group - if project: - kwargs['project'] = project - if domain: - kwargs['domain'] = domain - if system: - kwargs['system'] = system - return kwargs - def run(self): - role = self.params.get('role') - user = self.params.get('user') - group = self.params.get('group') - project = self.params.get('project') - domain = self.params.get('domain') - system = self.params.get('system') - state = self.params.get('state') - filters = {} find_filters = {} - domain_id = None + kwargs = {} - r = self.conn.identity.find_role(role) - if r is None: - self.fail_json(msg="Role %s is not valid" % role) - filters['role'] = r['id'] + role_name_or_id = self.params['role'] + role = self.conn.identity.find_role(role_name_or_id, + ignore_missing=False) + filters['role_id'] = role['id'] - if domain: - d = self.conn.identity.find_domain(domain) - if d is None: - self.fail_json(msg="Domain %s is not valid" % domain) - domain_id = d['id'] - find_filters['domain_id'] = domain_id - if user: - u = self.conn.identity.find_user(user, **find_filters) - if u is None: - self.fail_json(msg="User %s is not valid" % user) - filters['user'] = u['id'] + 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) + filters['scope_domain_id'] = domain['id'] + find_filters['domain_id'] = domain['id'] + kwargs['domain'] = domain['id'] - if group: - g = self.conn.identity.find_group(group, **find_filters) - if g is None: - self.fail_json(msg="Group %s is not valid" % group) - filters['group'] = g['id'] - if project: - p = self.conn.identity.find_project(project, **find_filters) - if p is None: - self.fail_json(msg="Project %s is not valid" % project) - filters['project'] = p['id'] - if system: - # the system role name is the argument. list_role_assignments will - # fail if the system role name is not valid - filters['system'] = system + user_name_or_id = self.params['user'] + if user_name_or_id is not None: + user = self.conn.identity.find_user( + user_name_or_id, ignore_missing=False, **find_filters) + filters['user_id'] = user['id'] + kwargs['user'] = user['id'] - # Keeping the self.conn.list_role_assignments because it calls directly - # the identity.role_assignments and there are some logics for the - # filters that won't worth rewrite here. - assignment = self.conn.list_role_assignments(filters=filters) + group_name_or_id = self.params['group'] + if group_name_or_id is not None: + group = self.conn.identity.find_group( + group_name_or_id, ignore_missing=False, **find_filters) + filters['group_id'] = group['id'] + kwargs['group'] = group['id'] + system_name = self.params['system'] + if system_name is not None: + # domain has precedence over system + if 'scope_domain_id' not in filters: + filters['scope.system'] = system_name + + kwargs['system'] = system_name + + project_name_or_id = self.params['project'] + if project_name_or_id is not None: + project = self.conn.identity.find_project( + project_name_or_id, ignore_missing=False, **find_filters) + filters['scope_project_id'] = project['id'] + kwargs['project'] = project['id'] + + # project has precedence over domain and system + filters.pop('scope_domain_id', None) + filters.pop('scope.system', None) + + role_assignments = list(self.conn.identity.role_assignments(**filters)) + + state = self.params['state'] if self.ansible.check_mode: - self.exit_json(changed=self._system_state_change(state, assignment)) + self.exit_json( + changed=((state == 'present' and not role_assignments) + or (state == 'absent' and role_assignments))) - changed = False - - # Both grant_role and revoke_role calls directly the proxy layer, and - # has some logic that won't worth to rewrite here so keeping it is a - # good idea - if state == 'present': - if not assignment: - kwargs = self._build_kwargs(user, group, project, domain_id, system) - self.conn.grant_role(role, **kwargs) - changed = True - - elif state == 'absent': - if assignment: - kwargs = self._build_kwargs(user, group, project, domain_id, system) - self.conn.revoke_role(role, **kwargs) - changed = True - - self.exit_json(changed=changed) + if state == 'present' and not role_assignments: + self.conn.grant_role(role['id'], **kwargs) + self.exit_json(changed=True) + elif state == 'absent' and role_assignments: + self.conn.revoke_role(role['id'], **kwargs) + self.exit_json(changed=True) + else: + self.exit_json(changed=False) def main():