From 437438e33c836d50c1f6e3ef22a88226fdac239d Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Tue, 15 Apr 2025 13:26:25 -0500 Subject: [PATCH] Allow role_assignment module to work cross domain The role_assignment module always looks up the user, group and project so to support cross-domain assignments we should add extra parameters like OSC to look them up from the correct domains. Switch to using the service proxy interface to grant or revoke the roles as well. Partial-Bug: #2052448 Partial-Bug: #2047151 Partial-Bug: #2097203 Change-Id: Id023cb9e7017c749bc39bba2091921154a413723 --- ci/roles/role_assignment/tasks/main.yml | 53 ++++++++-- plugins/modules/role_assignment.py | 132 +++++++++++++++++++++--- 2 files changed, 162 insertions(+), 23 deletions(-) diff --git a/ci/roles/role_assignment/tasks/main.yml b/ci/roles/role_assignment/tasks/main.yml index cff79e4d..e0e59815 100644 --- a/ci/roles/role_assignment/tasks/main.yml +++ b/ci/roles/role_assignment/tasks/main.yml @@ -45,12 +45,6 @@ state: absent user: admin -- name: Delete project - openstack.cloud.project: - cloud: "{{ cloud }}" - state: absent - name: ansible_project - - name: Create domain openstack.cloud.identity_domain: cloud: "{{ cloud }}" @@ -78,6 +72,7 @@ state: present name: ansible_user domain: default + register: specific_user - name: Create user in specific domain openstack.cloud.identity_user: @@ -138,6 +133,45 @@ that: - role_assignment is changed +- name: Assign role to user in specific domain on default domain project + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + user: "{{ specific_user.user.id }}" + domain: default + project: ansible_project + register: role_assignment + +- name: Assert role assignment + assert: + that: + - role_assignment is changed + +- name: Revoke role to user in specific domain + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + user: "{{ specific_user.user.id }}" + domain: default + project: ansible_project + state: absent + register: role_assignment + +- name: Assert role assignment revoked + assert: + that: + - role_assignment is changed + +- name: Assign role to user in specific domain on default domain project + openstack.cloud.role_assignment: + cloud: "{{ cloud }}" + role: anotherrole + user: ansible_user + user_domain: "{{ specific_user.user.domain_id }}" + project: ansible_project + project_domain: default + register: role_assignment + - name: Delete group in default domain openstack.cloud.identity_group: cloud: "{{ cloud }}" @@ -171,3 +205,10 @@ cloud: "{{ cloud }}" state: absent name: ansible_domain + +- name: Delete project + openstack.cloud.project: + cloud: "{{ cloud }}" + state: absent + name: ansible_project + diff --git a/plugins/modules/role_assignment.py b/plugins/modules/role_assignment.py index 5ec69923..b97d80fb 100644 --- a/plugins/modules/role_assignment.py +++ b/plugins/modules/role_assignment.py @@ -19,7 +19,9 @@ options: - 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. + scoping the role association, only for finding resources. Deprecated + for finding resources, please use I(group_domain), I(project_domain), + I(role_domain), or I(user_domain). - "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 @@ -32,24 +34,45 @@ options: - 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. + - You can supply I(group_domain) or the deprecated usage of I(domain) to + find group resources. + type: str + group_domain: + description: + - Name or ID for the domain. + - Valid only with keystone version 3. + - Only valid for finding group resources. 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. + scoping the role association, only for finding resources. Prefer + I(group_domain) over I(domain). - "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 + project_domain: + description: + - Name or ID for the domain. + - Valid only with keystone version 3. + - Only valid for finding project resources. + type: str role: description: - Name or ID for the role. required: true type: str + role_domain: + description: + - Name or ID for the domain. + - Valid only with keystone version 3. + - Only valid for finding role resources. + type: str state: description: - Should the roles be present or absent on the user. @@ -73,6 +96,12 @@ options: - If I(user) is not specified, then I(group) is required. Both may not be specified at the same time. type: str + user_domain: + description: + - Name or ID for the domain. + - Valid only with keystone version 3. + - Only valid for finding user resources. + type: str extends_documentation_fragment: - openstack.cloud.openstack ''' @@ -101,11 +130,15 @@ class IdentityRoleAssignmentModule(OpenStackModule): argument_spec = dict( domain=dict(), group=dict(), + group_domain=dict(type='str'), project=dict(), + project_domain=dict(type='str'), role=dict(required=True), + role_domain=dict(type='str'), state=dict(default='present', choices=['absent', 'present']), system=dict(), user=dict(), + user_domain=dict(type='str'), ) module_kwargs = dict( @@ -113,17 +146,33 @@ class IdentityRoleAssignmentModule(OpenStackModule): ('user', 'group'), ('domain', 'project', 'system'), ], + mutually_exclusive=[ + ('user', 'group'), + ('project', 'system'), # domain should be part of this + ], supports_check_mode=True ) + def _find_domain_id(self, domain): + if domain is not None: + domain = self.conn.identity.find_domain(domain, + ignore_missing=False) + return dict(domain_id=domain['id']) + return dict() + def run(self): filters = {} - find_filters = {} - kwargs = {} + group_find_filters = {} + project_find_filters = {} + role_find_filters = {} + user_find_filters = {} + role_find_filters.update(self._find_domain_id( + self.params['role_domain'])) role_name_or_id = self.params['role'] role = self.conn.identity.find_role(role_name_or_id, - ignore_missing=False) + ignore_missing=False, + **role_find_filters) filters['role_id'] = role['id'] domain_name_or_id = self.params['domain'] @@ -131,22 +180,31 @@ class IdentityRoleAssignmentModule(OpenStackModule): 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'] + group_find_filters['domain_id'] = domain['id'] + project_find_filters['domain_id'] = domain['id'] + user_find_filters['domain_id'] = domain['id'] user_name_or_id = self.params['user'] if user_name_or_id is not None: + user_find_filters.update(self._find_domain_id( + self.params['user_domain'])) user = self.conn.identity.find_user( - user_name_or_id, ignore_missing=False, **find_filters) + user_name_or_id, ignore_missing=False, + **user_find_filters) filters['user_id'] = user['id'] - kwargs['user'] = user['id'] + else: + user = None group_name_or_id = self.params['group'] if group_name_or_id is not None: + group_find_filters.update(self._find_domain_id( + self.params['group_domain'])) group = self.conn.identity.find_group( - group_name_or_id, ignore_missing=False, **find_filters) + group_name_or_id, ignore_missing=False, + **group_find_filters) filters['group_id'] = group['id'] - kwargs['group'] = group['id'] + else: + group = None system_name = self.params['system'] if system_name is not None: @@ -154,14 +212,14 @@ class IdentityRoleAssignmentModule(OpenStackModule): 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_find_filters.update(self._find_domain_id( + self.params['project_domain'])) project = self.conn.identity.find_project( - project_name_or_id, ignore_missing=False, **find_filters) + project_name_or_id, ignore_missing=False, + **project_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) @@ -176,10 +234,50 @@ class IdentityRoleAssignmentModule(OpenStackModule): or (state == 'absent' and role_assignments))) if state == 'present' and not role_assignments: - self.conn.grant_role(role['id'], **kwargs) + if 'scope_domain_id' in filters: + if user is not None: + self.conn.identity.assign_domain_role_to_user( + filters['scope_domain_id'], user, role) + else: + self.conn.identity.assign_domain_role_to_group( + filters['scope_domain_id'], group, role) + elif 'scope_project_id' in filters: + if user is not None: + self.conn.identity.assign_project_role_to_user( + filters['scope_project_id'], user, role) + else: + self.conn.identity.assign_project_role_to_group( + filters['scope_project_id'], group, role) + elif 'scope.system' in filters: + if user is not None: + self.conn.identity.assign_system_role_to_user( + user, role, filters['scope.system']) + else: + self.conn.identity.assign_system_role_to_group( + group, role, filters['scope.system']) self.exit_json(changed=True) elif state == 'absent' and role_assignments: - self.conn.revoke_role(role['id'], **kwargs) + if 'scope_domain_id' in filters: + if user is not None: + self.conn.identity.unassign_domain_role_from_user( + filters['scope_domain_id'], user, role) + else: + self.conn.identity.unassign_domain_role_from_group( + filters['scope_domain_id'], group, role) + elif 'scope_project_id' in filters: + if user is not None: + self.conn.identity.unassign_project_role_from_user( + filters['scope_project_id'], user, role) + else: + self.conn.identity.unassign_project_role_from_group( + filters['scope_project_id'], group, role) + elif 'scope.system' in filters: + if user is not None: + self.conn.identity.unassign_system_role_from_user( + user, role, filters['scope.system']) + else: + self.conn.identity.unassign_system_role_from_group( + group, role, filters['scope.system']) self.exit_json(changed=True) else: self.exit_json(changed=False)