Add Keystone User/Group RoleAssignment resources
This is the second patch of two to allow roles to be added to existing users. This patch adds two new resources: OS::Keystone::UserRoleAssignment OS::Keystone::GroupRoleAssignment These resources will grant a user or group role(s) within the given project or domain. Change-Id: I4002fc245b1b21d99d95740b7d15642fd8f9e26d Closes-Bug: #1477218
This commit is contained in:
@@ -15,6 +15,8 @@ from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.engine import constraints
|
||||
from heat.engine import properties
|
||||
from heat.engine import resource
|
||||
from heat.engine import support
|
||||
|
||||
|
||||
class KeystoneRoleAssignmentMixin(object):
|
||||
@@ -306,3 +308,112 @@ class KeystoneRoleAssignmentMixin(object):
|
||||
msg = _('Either project or domain must be specified for'
|
||||
' role %s') % role_assignment.get(self.ROLE)
|
||||
raise exception.StackValidationFailed(message=msg)
|
||||
|
||||
|
||||
class KeystoneUserRoleAssignment(resource.Resource,
|
||||
KeystoneRoleAssignmentMixin):
|
||||
'''Resource for granting roles to a user.'''
|
||||
|
||||
support_status = support.SupportStatus(
|
||||
version='5.0.0',
|
||||
message=_('Supported versions: keystone v3'))
|
||||
|
||||
default_client_name = 'keystone'
|
||||
|
||||
PROPERTIES = (
|
||||
USER,
|
||||
) = (
|
||||
'user',
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
USER: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Name or id of keystone user.'),
|
||||
required=True,
|
||||
update_allowed=True,
|
||||
constraints=[constraints.CustomConstraint('keystone.user')]
|
||||
)
|
||||
}
|
||||
|
||||
properties_schema.update(
|
||||
KeystoneRoleAssignmentMixin.mixin_properties_schema)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KeystoneUserRoleAssignment, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
return (self.client_plugin().get_user_id(
|
||||
self.properties.get(self.USER)))
|
||||
|
||||
def handle_create(self):
|
||||
self.create_assignment(user_id=self.user_id)
|
||||
|
||||
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
||||
self.update_assignment(user_id=self.user_id, prop_diff=prop_diff)
|
||||
|
||||
def handle_delete(self):
|
||||
self.delete_assignment(user_id=self.user_id)
|
||||
|
||||
def validate(self):
|
||||
super(KeystoneUserRoleAssignment, self).validate()
|
||||
self.validate_assignment_properties()
|
||||
|
||||
|
||||
class KeystoneGroupRoleAssignment(resource.Resource,
|
||||
KeystoneRoleAssignmentMixin):
|
||||
'''Resource for granting roles to a group.'''
|
||||
|
||||
support_status = support.SupportStatus(
|
||||
version='5.0.0',
|
||||
message=_('Supported versions: keystone v3'))
|
||||
|
||||
default_client_name = 'keystone'
|
||||
|
||||
PROPERTIES = (
|
||||
GROUP,
|
||||
) = (
|
||||
'group',
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
GROUP: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Name or id of keystone group.'),
|
||||
required=True,
|
||||
update_allowed=True,
|
||||
constraints=[constraints.CustomConstraint('keystone.group')]
|
||||
)
|
||||
}
|
||||
|
||||
properties_schema.update(
|
||||
KeystoneRoleAssignmentMixin.mixin_properties_schema)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KeystoneGroupRoleAssignment, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
return (self.client_plugin().get_group_id(
|
||||
self.properties.get(self.GROUP)))
|
||||
|
||||
def handle_create(self):
|
||||
self.create_assignment(group_id=self.group_id)
|
||||
|
||||
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
||||
self.update_assignment(group_id=self.group_id, prop_diff=prop_diff)
|
||||
|
||||
def handle_delete(self):
|
||||
self.delete_assignment(group_id=self.group_id)
|
||||
|
||||
def validate(self):
|
||||
super(KeystoneGroupRoleAssignment, self).validate()
|
||||
self.validate_assignment_properties()
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
'OS::Keystone::UserRoleAssignment': KeystoneUserRoleAssignment,
|
||||
'OS::Keystone::GroupRoleAssignment': KeystoneGroupRoleAssignment
|
||||
}
|
||||
|
||||
@@ -250,12 +250,12 @@ class KeystoneUser(resource.Resource,
|
||||
try:
|
||||
self.delete_assignment(user_id=self.resource_id)
|
||||
|
||||
if self._stored_properties_data[self.GROUPS] is not None:
|
||||
if self._stored_properties_data.get(self.GROUPS) is not None:
|
||||
self._remove_user_from_groups(
|
||||
self.resource_id,
|
||||
[self.client_plugin().get_group_id(group)
|
||||
for group in
|
||||
self._stored_properties_data[self.GROUPS]])
|
||||
self._stored_properties_data.get(self.GROUPS)])
|
||||
|
||||
self._delete_user(user_id=self.resource_id)
|
||||
except Exception as ex:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import mock
|
||||
|
||||
from heat.common import exception
|
||||
@@ -26,7 +27,7 @@ from heat.tests import utils
|
||||
RESOURCE_TYPE = 'OS::Keystone::DummyRoleAssignment'
|
||||
|
||||
keystone_role_assignment_template = {
|
||||
'heat_template_version': '2013-05-23',
|
||||
'heat_template_version': '2015-10-15',
|
||||
'resources': {
|
||||
'test_role_assignment': {
|
||||
'type': RESOURCE_TYPE,
|
||||
@@ -41,7 +42,6 @@ keystone_role_assignment_template = {
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,14 @@ class KeystoneRoleAssignmentMixinTest(common.HeatTestCase):
|
||||
(self.test_role_assignment.client_plugin.
|
||||
return_value) = self.keystone_client_plugin
|
||||
|
||||
def test_resource_mapping(self):
|
||||
mapping = role_assignments.resource_mapping()
|
||||
self.assertEqual(2, len(mapping))
|
||||
self.assertEqual(role_assignments.KeystoneUserRoleAssignment,
|
||||
mapping['OS::Keystone::UserRoleAssignment'])
|
||||
self.assertEqual(role_assignments.KeystoneGroupRoleAssignment,
|
||||
mapping['OS::Keystone::GroupRoleAssignment'])
|
||||
|
||||
def test_properties_title(self):
|
||||
property_title_map = {MixinClass.ROLES: 'roles'}
|
||||
|
||||
@@ -380,3 +388,279 @@ class KeystoneRoleAssignmentMixinTest(common.HeatTestCase):
|
||||
]
|
||||
self.assertRaises(exception.ResourcePropertyConflict,
|
||||
self.test_role_assignment.validate)
|
||||
|
||||
|
||||
class KeystoneUserRoleAssignmentTest(common.HeatTestCase):
|
||||
|
||||
role_assignment_template = copy.deepcopy(keystone_role_assignment_template)
|
||||
role = role_assignment_template['resources']['test_role_assignment']
|
||||
role['properties']['user'] = 'user_1'
|
||||
role['type'] = 'OS::Keystone::UserRoleAssignment'
|
||||
|
||||
def setUp(self):
|
||||
super(KeystoneUserRoleAssignmentTest, self).setUp()
|
||||
|
||||
self.ctx = utils.dummy_context()
|
||||
|
||||
self.stack = stack.Stack(
|
||||
self.ctx, 'test_stack_keystone_user_role_add',
|
||||
template.Template(self.role_assignment_template)
|
||||
)
|
||||
self.test_role_assignment = self.stack['test_role_assignment']
|
||||
|
||||
# Mock client
|
||||
self.keystoneclient = mock.MagicMock()
|
||||
self.test_role_assignment.client = mock.MagicMock()
|
||||
self.test_role_assignment.client.return_value = self.keystoneclient
|
||||
self.roles = self.keystoneclient.client.roles
|
||||
|
||||
# Mock client plugin
|
||||
def _side_effect(value):
|
||||
return value
|
||||
|
||||
self.keystone_client_plugin = mock.MagicMock()
|
||||
self.keystone_client_plugin.get_user_id.side_effect = _side_effect
|
||||
self.keystone_client_plugin.get_domain_id.side_effect = _side_effect
|
||||
self.keystone_client_plugin.get_role_id.side_effect = _side_effect
|
||||
self.keystone_client_plugin.get_project_id.side_effect = _side_effect
|
||||
self.test_role_assignment.client_plugin = mock.MagicMock()
|
||||
(self.test_role_assignment.client_plugin.
|
||||
return_value) = self.keystone_client_plugin
|
||||
|
||||
def test_user_role_assignment_handle_create(self):
|
||||
self.test_role_assignment.handle_create()
|
||||
|
||||
# role-user-domain created
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_1',
|
||||
user='user_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-user-project created
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_1',
|
||||
user='user_1',
|
||||
project='project_1')
|
||||
|
||||
def test_user_role_assignment_handle_update(self):
|
||||
self.test_role_assignment._stored_properties_data = {
|
||||
'roles': [
|
||||
{
|
||||
'role': 'role_1',
|
||||
'project': 'project_1'
|
||||
},
|
||||
{
|
||||
'role': 'role_1',
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
prop_diff = {
|
||||
MixinClass.ROLES: [
|
||||
{
|
||||
'role': 'role_2',
|
||||
'project': 'project_1'
|
||||
},
|
||||
{
|
||||
'role': 'role_2',
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.test_role_assignment.handle_update(json_snippet=None,
|
||||
tmpl_diff=None,
|
||||
prop_diff=prop_diff)
|
||||
|
||||
# Add role2-project1-domain1
|
||||
# role-user-domain
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_2',
|
||||
user='user_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-user-project
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_2',
|
||||
user='user_1',
|
||||
project='project_1')
|
||||
|
||||
# Remove role1-project1-domain1
|
||||
# role-user-domain
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
user='user_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-user-project
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
user='user_1',
|
||||
project='project_1')
|
||||
|
||||
def test_user_role_assignment_handle_delete(self):
|
||||
self.test_role_assignment._stored_properties_data = {
|
||||
'roles': [
|
||||
{
|
||||
'role': 'role_1',
|
||||
'project': 'project_1'
|
||||
},
|
||||
{
|
||||
'role': 'role_1',
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
self.assertIsNone(self.test_role_assignment.handle_delete())
|
||||
|
||||
# Remove role1-project1-domain1
|
||||
# role-user-domain
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
user='user_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-user-project
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
user='user_1',
|
||||
project='project_1')
|
||||
|
||||
|
||||
class KeystoneGroupRoleAssignmentTest(common.HeatTestCase):
|
||||
|
||||
role_assignment_template = copy.deepcopy(keystone_role_assignment_template)
|
||||
role = role_assignment_template['resources']['test_role_assignment']
|
||||
role['properties']['group'] = 'group_1'
|
||||
role['type'] = 'OS::Keystone::GroupRoleAssignment'
|
||||
|
||||
def setUp(self):
|
||||
super(KeystoneGroupRoleAssignmentTest, self).setUp()
|
||||
|
||||
self.ctx = utils.dummy_context()
|
||||
|
||||
self.stack = stack.Stack(
|
||||
self.ctx, 'test_stack_keystone_group_role_add',
|
||||
template.Template(self.role_assignment_template)
|
||||
)
|
||||
self.test_role_assignment = self.stack['test_role_assignment']
|
||||
|
||||
# Mock client
|
||||
self.keystoneclient = mock.MagicMock()
|
||||
self.test_role_assignment.client = mock.MagicMock()
|
||||
self.test_role_assignment.client.return_value = self.keystoneclient
|
||||
self.roles = self.keystoneclient.client.roles
|
||||
|
||||
# Mock client plugin
|
||||
def _side_effect(value):
|
||||
return value
|
||||
|
||||
self.keystone_client_plugin = mock.MagicMock()
|
||||
self.keystone_client_plugin.get_group_id.side_effect = _side_effect
|
||||
self.keystone_client_plugin.get_domain_id.side_effect = _side_effect
|
||||
self.keystone_client_plugin.get_role_id.side_effect = _side_effect
|
||||
self.keystone_client_plugin.get_project_id.side_effect = _side_effect
|
||||
self.test_role_assignment.client_plugin = mock.MagicMock()
|
||||
(self.test_role_assignment.client_plugin.
|
||||
return_value) = self.keystone_client_plugin
|
||||
|
||||
def test_group_role_assignment_handle_create(self):
|
||||
self.test_role_assignment.handle_create()
|
||||
|
||||
# role-group-domain created
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_1',
|
||||
group='group_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-group-project created
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_1',
|
||||
group='group_1',
|
||||
project='project_1')
|
||||
|
||||
def test_group_role_assignment_handle_update(self):
|
||||
self.test_role_assignment._stored_properties_data = {
|
||||
'roles': [
|
||||
{
|
||||
'role': 'role_1',
|
||||
'project': 'project_1'
|
||||
},
|
||||
{
|
||||
'role': 'role_1',
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
prop_diff = {
|
||||
MixinClass.ROLES: [
|
||||
{
|
||||
'role': 'role_2',
|
||||
'project': 'project_1'
|
||||
},
|
||||
{
|
||||
'role': 'role_2',
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.test_role_assignment.handle_update(json_snippet=None,
|
||||
tmpl_diff=None,
|
||||
prop_diff=prop_diff)
|
||||
|
||||
# Add role2-project1-domain1
|
||||
# role-group-domain
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_2',
|
||||
group='group_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-group-project
|
||||
self.roles.grant.assert_any_call(
|
||||
role='role_2',
|
||||
group='group_1',
|
||||
project='project_1')
|
||||
|
||||
# Remove role1-project1-domain1
|
||||
# role-group-domain
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
group='group_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-group-project
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
group='group_1',
|
||||
project='project_1')
|
||||
|
||||
def test_group_role_assignment_handle_delete(self):
|
||||
self.test_role_assignment._stored_properties_data = {
|
||||
'roles': [
|
||||
{
|
||||
'role': 'role_1',
|
||||
'project': 'project_1'
|
||||
},
|
||||
{
|
||||
'role': 'role_1',
|
||||
'domain': 'domain_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
self.assertIsNone(self.test_role_assignment.handle_delete())
|
||||
|
||||
# Remove role1-project1-domain1
|
||||
# role-group-domain
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
group='group_1',
|
||||
domain='domain_1')
|
||||
|
||||
# role-group-project
|
||||
self.roles.revoke.assert_any_call(
|
||||
role='role_1',
|
||||
group='group_1',
|
||||
project='project_1')
|
||||
|
||||
Reference in New Issue
Block a user