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:
Bryan Jones
2015-07-23 21:13:13 +00:00
parent ac7b96ea3d
commit b418669ce0
3 changed files with 399 additions and 4 deletions

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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')