summaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/assignment
diff options
context:
space:
mode:
authorWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
committerWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
commitb8c756ecdd7cced1db4300935484e8c83701c82e (patch)
tree87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/assignment
parentc304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff)
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604 Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/assignment')
-rw-r--r--keystone-moon/keystone/assignment/__init__.py17
-rw-r--r--keystone-moon/keystone/assignment/backends/__init__.py0
-rw-r--r--keystone-moon/keystone/assignment/backends/ldap.py531
-rw-r--r--keystone-moon/keystone/assignment/backends/sql.py415
-rw-r--r--keystone-moon/keystone/assignment/controllers.py816
-rw-r--r--keystone-moon/keystone/assignment/core.py1019
-rw-r--r--keystone-moon/keystone/assignment/role_backends/__init__.py0
-rw-r--r--keystone-moon/keystone/assignment/role_backends/ldap.py125
-rw-r--r--keystone-moon/keystone/assignment/role_backends/sql.py80
-rw-r--r--keystone-moon/keystone/assignment/routers.py246
-rw-r--r--keystone-moon/keystone/assignment/schema.py32
11 files changed, 3281 insertions, 0 deletions
diff --git a/keystone-moon/keystone/assignment/__init__.py b/keystone-moon/keystone/assignment/__init__.py
new file mode 100644
index 00000000..49ad7594
--- /dev/null
+++ b/keystone-moon/keystone/assignment/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2013 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystone.assignment import controllers # noqa
+from keystone.assignment.core import * # noqa
+from keystone.assignment import routers # noqa
diff --git a/keystone-moon/keystone/assignment/backends/__init__.py b/keystone-moon/keystone/assignment/backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/assignment/backends/__init__.py
diff --git a/keystone-moon/keystone/assignment/backends/ldap.py b/keystone-moon/keystone/assignment/backends/ldap.py
new file mode 100644
index 00000000..f93e989f
--- /dev/null
+++ b/keystone-moon/keystone/assignment/backends/ldap.py
@@ -0,0 +1,531 @@
+# Copyright 2012-2013 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+from __future__ import absolute_import
+
+import ldap as ldap
+import ldap.filter
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone import assignment
+from keystone.assignment.role_backends import ldap as ldap_role
+from keystone.common import ldap as common_ldap
+from keystone.common import models
+from keystone import exception
+from keystone.i18n import _
+from keystone.identity.backends import ldap as ldap_identity
+from keystone.openstack.common import versionutils
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+class Assignment(assignment.Driver):
+ @versionutils.deprecated(
+ versionutils.deprecated.KILO,
+ remove_in=+2,
+ what='keystone.assignment.backends.ldap.Assignment')
+ def __init__(self):
+ super(Assignment, self).__init__()
+ self.LDAP_URL = CONF.ldap.url
+ self.LDAP_USER = CONF.ldap.user
+ self.LDAP_PASSWORD = CONF.ldap.password
+ self.suffix = CONF.ldap.suffix
+
+ # This is the only deep dependency from assignment back to identity.
+ # This is safe to do since if you are using LDAP for assignment, it is
+ # required that you are using it for identity as well.
+ self.user = ldap_identity.UserApi(CONF)
+ self.group = ldap_identity.GroupApi(CONF)
+
+ self.project = ProjectApi(CONF)
+ self.role = RoleApi(CONF, self.user)
+
+ def default_role_driver(self):
+ return 'keystone.assignment.role_backends.ldap.Role'
+
+ def default_resource_driver(self):
+ return 'keystone.resource.backends.ldap.Resource'
+
+ def list_role_ids_for_groups_on_project(
+ self, groups, project_id, project_domain_id, project_parents):
+ group_dns = [self.group._id_to_dn(group_id) for group_id in groups]
+ role_list = [self.role._dn_to_id(role_assignment.role_dn)
+ for role_assignment in self.role.get_role_assignments
+ (self.project._id_to_dn(project_id))
+ if role_assignment.user_dn.upper() in group_dns]
+ # NOTE(morganfainberg): Does not support OS-INHERIT as domain
+ # metadata/roles are not supported by LDAP backend. Skip OS-INHERIT
+ # logic.
+ return role_list
+
+ def _get_metadata(self, user_id=None, tenant_id=None,
+ domain_id=None, group_id=None):
+
+ def _get_roles_for_just_user_and_project(user_id, tenant_id):
+ user_dn = self.user._id_to_dn(user_id)
+ return [self.role._dn_to_id(a.role_dn)
+ for a in self.role.get_role_assignments
+ (self.project._id_to_dn(tenant_id))
+ if common_ldap.is_dn_equal(a.user_dn, user_dn)]
+
+ def _get_roles_for_group_and_project(group_id, project_id):
+ group_dn = self.group._id_to_dn(group_id)
+ return [self.role._dn_to_id(a.role_dn)
+ for a in self.role.get_role_assignments
+ (self.project._id_to_dn(project_id))
+ if common_ldap.is_dn_equal(a.user_dn, group_dn)]
+
+ if domain_id is not None:
+ msg = _('Domain metadata not supported by LDAP')
+ raise exception.NotImplemented(message=msg)
+ if group_id is None and user_id is None:
+ return {}
+
+ if tenant_id is None:
+ return {}
+ if user_id is None:
+ metadata_ref = _get_roles_for_group_and_project(group_id,
+ tenant_id)
+ else:
+ metadata_ref = _get_roles_for_just_user_and_project(user_id,
+ tenant_id)
+ if not metadata_ref:
+ return {}
+ return {'roles': [self._role_to_dict(r, False) for r in metadata_ref]}
+
+ def list_project_ids_for_user(self, user_id, group_ids, hints,
+ inherited=False):
+ # TODO(henry-nash): The ldap driver does not support inherited
+ # assignments, so the inherited parameter is unused.
+ # See bug #1404273.
+ user_dn = self.user._id_to_dn(user_id)
+ associations = (self.role.list_project_roles_for_user
+ (user_dn, self.project.tree_dn))
+
+ for group_id in group_ids:
+ group_dn = self.group._id_to_dn(group_id)
+ for group_role in self.role.list_project_roles_for_group(
+ group_dn, self.project.tree_dn):
+ associations.append(group_role)
+
+ return list(set(
+ [self.project._dn_to_id(x.project_dn) for x in associations]))
+
+ def list_role_ids_for_groups_on_domain(self, group_ids, domain_id):
+ raise exception.NotImplemented()
+
+ def list_project_ids_for_groups(self, group_ids, hints,
+ inherited=False):
+ raise exception.NotImplemented()
+
+ def list_domain_ids_for_user(self, user_id, group_ids, hints):
+ raise exception.NotImplemented()
+
+ def list_domain_ids_for_groups(self, group_ids, inherited=False):
+ raise exception.NotImplemented()
+
+ def list_user_ids_for_project(self, tenant_id):
+ tenant_dn = self.project._id_to_dn(tenant_id)
+ rolegrants = self.role.get_role_assignments(tenant_dn)
+ return [self.user._dn_to_id(user_dn) for user_dn in
+ self.project.get_user_dns(tenant_id, rolegrants)]
+
+ def _subrole_id_to_dn(self, role_id, tenant_id):
+ if tenant_id is None:
+ return self.role._id_to_dn(role_id)
+ else:
+ return '%s=%s,%s' % (self.role.id_attr,
+ ldap.dn.escape_dn_chars(role_id),
+ self.project._id_to_dn(tenant_id))
+
+ def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
+ user_dn = self.user._id_to_dn(user_id)
+ role_dn = self._subrole_id_to_dn(role_id, tenant_id)
+ self.role.add_user(role_id, role_dn, user_dn, user_id, tenant_id)
+ tenant_dn = self.project._id_to_dn(tenant_id)
+ return UserRoleAssociation(role_dn=role_dn,
+ user_dn=user_dn,
+ tenant_dn=tenant_dn)
+
+ def _add_role_to_group_and_project(self, group_id, tenant_id, role_id):
+ group_dn = self.group._id_to_dn(group_id)
+ role_dn = self._subrole_id_to_dn(role_id, tenant_id)
+ self.role.add_user(role_id, role_dn, group_dn, group_id, tenant_id)
+ tenant_dn = self.project._id_to_dn(tenant_id)
+ return GroupRoleAssociation(group_dn=group_dn,
+ role_dn=role_dn,
+ tenant_dn=tenant_dn)
+
+ def remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
+ role_dn = self._subrole_id_to_dn(role_id, tenant_id)
+ return self.role.delete_user(role_dn,
+ self.user._id_to_dn(user_id), role_id)
+
+ def _remove_role_from_group_and_project(self, group_id, tenant_id,
+ role_id):
+ role_dn = self._subrole_id_to_dn(role_id, tenant_id)
+ return self.role.delete_user(role_dn,
+ self.group._id_to_dn(group_id), role_id)
+
+# Bulk actions on User From identity
+ def delete_user(self, user_id):
+ user_dn = self.user._id_to_dn(user_id)
+ for ref in self.role.list_global_roles_for_user(user_dn):
+ self.role.delete_user(ref.role_dn, ref.user_dn,
+ self.role._dn_to_id(ref.role_dn))
+ for ref in self.role.list_project_roles_for_user(user_dn,
+ self.project.tree_dn):
+ self.role.delete_user(ref.role_dn, ref.user_dn,
+ self.role._dn_to_id(ref.role_dn))
+
+ def delete_group(self, group_id):
+ """Called when the group was deleted.
+
+ Any role assignments for the group should be cleaned up.
+
+ """
+ group_dn = self.group._id_to_dn(group_id)
+ group_role_assignments = self.role.list_project_roles_for_group(
+ group_dn, self.project.tree_dn)
+ for ref in group_role_assignments:
+ self.role.delete_user(ref.role_dn, ref.group_dn,
+ self.role._dn_to_id(ref.role_dn))
+
+ def create_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+
+ try:
+ metadata_ref = self._get_metadata(user_id, project_id,
+ domain_id, group_id)
+ except exception.MetadataNotFound:
+ metadata_ref = {}
+
+ if user_id is None:
+ metadata_ref['roles'] = self._add_role_to_group_and_project(
+ group_id, project_id, role_id)
+ else:
+ metadata_ref['roles'] = self.add_role_to_user_and_project(
+ user_id, project_id, role_id)
+
+ def check_grant_role_id(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+
+ try:
+ metadata_ref = self._get_metadata(user_id, project_id,
+ domain_id, group_id)
+ except exception.MetadataNotFound:
+ metadata_ref = {}
+ role_ids = set(self._roles_from_role_dicts(
+ metadata_ref.get('roles', []), inherited_to_projects))
+ if role_id not in role_ids:
+ actor_id = user_id or group_id
+ target_id = domain_id or project_id
+ raise exception.RoleAssignmentNotFound(role_id=role_id,
+ actor_id=actor_id,
+ target_id=target_id)
+
+ def delete_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+
+ try:
+ metadata_ref = self._get_metadata(user_id, project_id,
+ domain_id, group_id)
+ except exception.MetadataNotFound:
+ metadata_ref = {}
+
+ try:
+ if user_id is None:
+ metadata_ref['roles'] = (
+ self._remove_role_from_group_and_project(
+ group_id, project_id, role_id))
+ else:
+ metadata_ref['roles'] = self.remove_role_from_user_and_project(
+ user_id, project_id, role_id)
+ except (exception.RoleNotFound, KeyError):
+ actor_id = user_id or group_id
+ target_id = domain_id or project_id
+ raise exception.RoleAssignmentNotFound(role_id=role_id,
+ actor_id=actor_id,
+ target_id=target_id)
+
+ def list_grant_role_ids(self, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+
+ try:
+ metadata_ref = self._get_metadata(user_id, project_id,
+ domain_id, group_id)
+ except exception.MetadataNotFound:
+ metadata_ref = {}
+
+ return self._roles_from_role_dicts(metadata_ref.get('roles', []),
+ inherited_to_projects)
+
+ def list_role_assignments(self):
+ role_assignments = []
+ for a in self.role.list_role_assignments(self.project.tree_dn):
+ if isinstance(a, UserRoleAssociation):
+ assignment = {
+ 'role_id': self.role._dn_to_id(a.role_dn),
+ 'user_id': self.user._dn_to_id(a.user_dn),
+ 'project_id': self.project._dn_to_id(a.project_dn)}
+ else:
+ assignment = {
+ 'role_id': self.role._dn_to_id(a.role_dn),
+ 'group_id': self.group._dn_to_id(a.group_dn),
+ 'project_id': self.project._dn_to_id(a.project_dn)}
+ role_assignments.append(assignment)
+ return role_assignments
+
+ def delete_project_assignments(self, project_id):
+ tenant_dn = self.project._id_to_dn(project_id)
+ self.role.roles_delete_subtree_by_project(tenant_dn)
+
+ def delete_role_assignments(self, role_id):
+ self.role.roles_delete_subtree_by_role(role_id, self.project.tree_dn)
+
+
+# TODO(termie): turn this into a data object and move logic to driver
+class ProjectApi(common_ldap.ProjectLdapStructureMixin,
+ common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
+
+ model = models.Project
+
+ def __init__(self, conf):
+ super(ProjectApi, self).__init__(conf)
+ self.member_attribute = (conf.ldap.project_member_attribute
+ or self.DEFAULT_MEMBER_ATTRIBUTE)
+
+ def get_user_projects(self, user_dn, associations):
+ """Returns list of tenants a user has access to
+ """
+
+ project_ids = set()
+ for assoc in associations:
+ project_ids.add(self._dn_to_id(assoc.project_dn))
+ projects = []
+ for project_id in project_ids:
+ # slower to get them one at a time, but a huge list could blow out
+ # the connection. This is the safer way
+ projects.append(self.get(project_id))
+ return projects
+
+ def get_user_dns(self, tenant_id, rolegrants, role_dn=None):
+ tenant = self._ldap_get(tenant_id)
+ res = set()
+ if not role_dn:
+ # Get users who have default tenant mapping
+ for user_dn in tenant[1].get(self.member_attribute, []):
+ if self._is_dumb_member(user_dn):
+ continue
+ res.add(user_dn)
+
+ # Get users who are explicitly mapped via a tenant
+ for rolegrant in rolegrants:
+ if role_dn is None or rolegrant.role_dn == role_dn:
+ res.add(rolegrant.user_dn)
+ return list(res)
+
+
+class UserRoleAssociation(object):
+ """Role Grant model."""
+
+ def __init__(self, user_dn=None, role_dn=None, tenant_dn=None,
+ *args, **kw):
+ self.user_dn = user_dn
+ self.role_dn = role_dn
+ self.project_dn = tenant_dn
+
+
+class GroupRoleAssociation(object):
+ """Role Grant model."""
+
+ def __init__(self, group_dn=None, role_dn=None, tenant_dn=None,
+ *args, **kw):
+ self.group_dn = group_dn
+ self.role_dn = role_dn
+ self.project_dn = tenant_dn
+
+
+# TODO(termie): turn this into a data object and move logic to driver
+# NOTE(heny-nash): The RoleLdapStructureMixin class enables the sharing of the
+# LDAP structure between here and the role backend LDAP, no methods are shared.
+class RoleApi(ldap_role.RoleLdapStructureMixin, common_ldap.BaseLdap):
+
+ def __init__(self, conf, user_api):
+ super(RoleApi, self).__init__(conf)
+ self.member_attribute = (conf.ldap.role_member_attribute
+ or self.DEFAULT_MEMBER_ATTRIBUTE)
+ self._user_api = user_api
+
+ def add_user(self, role_id, role_dn, user_dn, user_id, tenant_id=None):
+ try:
+ super(RoleApi, self).add_member(user_dn, role_dn)
+ except exception.Conflict:
+ msg = (_('User %(user_id)s already has role %(role_id)s in '
+ 'tenant %(tenant_id)s') %
+ dict(user_id=user_id, role_id=role_id, tenant_id=tenant_id))
+ raise exception.Conflict(type='role grant', details=msg)
+ except self.NotFound:
+ if tenant_id is None or self.get(role_id) is None:
+ raise Exception(_("Role %s not found") % (role_id,))
+
+ attrs = [('objectClass', [self.object_class]),
+ (self.member_attribute, [user_dn]),
+ (self.id_attr, [role_id])]
+
+ if self.use_dumb_member:
+ attrs[1][1].append(self.dumb_member)
+ with self.get_connection() as conn:
+ conn.add_s(role_dn, attrs)
+
+ def delete_user(self, role_dn, user_dn, role_id):
+ try:
+ super(RoleApi, self).remove_member(user_dn, role_dn)
+ except (self.NotFound, ldap.NO_SUCH_ATTRIBUTE):
+ raise exception.RoleNotFound(message=_(
+ 'Cannot remove role that has not been granted, %s') %
+ role_id)
+
+ def get_role_assignments(self, tenant_dn):
+ try:
+ roles = self._ldap_get_list(tenant_dn, ldap.SCOPE_ONELEVEL,
+ attrlist=[self.member_attribute])
+ except ldap.NO_SUCH_OBJECT:
+ roles = []
+ res = []
+ for role_dn, attrs in roles:
+ try:
+ user_dns = attrs[self.member_attribute]
+ except KeyError:
+ continue
+ for user_dn in user_dns:
+ if self._is_dumb_member(user_dn):
+ continue
+ res.append(UserRoleAssociation(
+ user_dn=user_dn,
+ role_dn=role_dn,
+ tenant_dn=tenant_dn))
+
+ return res
+
+ def list_global_roles_for_user(self, user_dn):
+ user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
+ roles = self.get_all('(%s=%s)' % (self.member_attribute, user_dn_esc))
+ return [UserRoleAssociation(
+ role_dn=role.dn,
+ user_dn=user_dn) for role in roles]
+
+ def list_project_roles_for_user(self, user_dn, project_subtree):
+ try:
+ roles = self._ldap_get_list(project_subtree, ldap.SCOPE_SUBTREE,
+ query_params={
+ self.member_attribute: user_dn},
+ attrlist=common_ldap.DN_ONLY)
+ except ldap.NO_SUCH_OBJECT:
+ roles = []
+ res = []
+ for role_dn, _role_attrs in roles:
+ # ldap.dn.dn2str returns an array, where the first
+ # element is the first segment.
+ # For a role assignment, this contains the role ID,
+ # The remainder is the DN of the tenant.
+ # role_dn is already utf8 encoded since it came from LDAP.
+ tenant = ldap.dn.str2dn(role_dn)
+ tenant.pop(0)
+ tenant_dn = ldap.dn.dn2str(tenant)
+ res.append(UserRoleAssociation(
+ user_dn=user_dn,
+ role_dn=role_dn,
+ tenant_dn=tenant_dn))
+ return res
+
+ def list_project_roles_for_group(self, group_dn, project_subtree):
+ group_dn_esc = ldap.filter.escape_filter_chars(group_dn)
+ query = '(&(objectClass=%s)(%s=%s))' % (self.object_class,
+ self.member_attribute,
+ group_dn_esc)
+ with self.get_connection() as conn:
+ try:
+ roles = conn.search_s(project_subtree,
+ ldap.SCOPE_SUBTREE,
+ query,
+ attrlist=common_ldap.DN_ONLY)
+ except ldap.NO_SUCH_OBJECT:
+ # Return no roles rather than raise an exception if the project
+ # subtree entry doesn't exist because an empty subtree is not
+ # an error.
+ return []
+
+ res = []
+ for role_dn, _role_attrs in roles:
+ # ldap.dn.str2dn returns a list, where the first
+ # element is the first RDN.
+ # For a role assignment, this contains the role ID,
+ # the remainder is the DN of the project.
+ # role_dn is already utf8 encoded since it came from LDAP.
+ project = ldap.dn.str2dn(role_dn)
+ project.pop(0)
+ project_dn = ldap.dn.dn2str(project)
+ res.append(GroupRoleAssociation(
+ group_dn=group_dn,
+ role_dn=role_dn,
+ tenant_dn=project_dn))
+ return res
+
+ def roles_delete_subtree_by_project(self, tenant_dn):
+ self._delete_tree_nodes(tenant_dn, ldap.SCOPE_ONELEVEL)
+
+ def roles_delete_subtree_by_role(self, role_id, tree_dn):
+ self._delete_tree_nodes(tree_dn, ldap.SCOPE_SUBTREE, query_params={
+ self.id_attr: role_id})
+
+ def list_role_assignments(self, project_tree_dn):
+ """Returns a list of all the role assignments linked to project_tree_dn
+ attribute.
+ """
+ try:
+ roles = self._ldap_get_list(project_tree_dn, ldap.SCOPE_SUBTREE,
+ attrlist=[self.member_attribute])
+ except ldap.NO_SUCH_OBJECT:
+ roles = []
+ res = []
+ for role_dn, role in roles:
+ # role_dn is already utf8 encoded since it came from LDAP.
+ tenant = ldap.dn.str2dn(role_dn)
+ tenant.pop(0)
+ # It obtains the tenant DN to construct the UserRoleAssociation
+ # object.
+ tenant_dn = ldap.dn.dn2str(tenant)
+ for occupant_dn in role[self.member_attribute]:
+ if self._is_dumb_member(occupant_dn):
+ continue
+ if self._user_api.is_user(occupant_dn):
+ association = UserRoleAssociation(
+ user_dn=occupant_dn,
+ role_dn=role_dn,
+ tenant_dn=tenant_dn)
+ else:
+ # occupant_dn is a group.
+ association = GroupRoleAssociation(
+ group_dn=occupant_dn,
+ role_dn=role_dn,
+ tenant_dn=tenant_dn)
+ res.append(association)
+ return res
diff --git a/keystone-moon/keystone/assignment/backends/sql.py b/keystone-moon/keystone/assignment/backends/sql.py
new file mode 100644
index 00000000..2de6ca60
--- /dev/null
+++ b/keystone-moon/keystone/assignment/backends/sql.py
@@ -0,0 +1,415 @@
+# Copyright 2012-13 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_config import cfg
+from oslo_log import log
+import six
+import sqlalchemy
+from sqlalchemy.sql.expression import false
+
+from keystone import assignment as keystone_assignment
+from keystone.common import sql
+from keystone import exception
+from keystone.i18n import _
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+class AssignmentType(object):
+ USER_PROJECT = 'UserProject'
+ GROUP_PROJECT = 'GroupProject'
+ USER_DOMAIN = 'UserDomain'
+ GROUP_DOMAIN = 'GroupDomain'
+
+ @classmethod
+ def calculate_type(cls, user_id, group_id, project_id, domain_id):
+ if user_id:
+ if project_id:
+ return cls.USER_PROJECT
+ if domain_id:
+ return cls.USER_DOMAIN
+ if group_id:
+ if project_id:
+ return cls.GROUP_PROJECT
+ if domain_id:
+ return cls.GROUP_DOMAIN
+ # Invalid parameters combination
+ raise exception.AssignmentTypeCalculationError(**locals())
+
+
+class Assignment(keystone_assignment.Driver):
+
+ def default_role_driver(self):
+ return "keystone.assignment.role_backends.sql.Role"
+
+ def default_resource_driver(self):
+ return 'keystone.resource.backends.sql.Resource'
+
+ def list_user_ids_for_project(self, tenant_id):
+ with sql.transaction() as session:
+ query = session.query(RoleAssignment.actor_id)
+ query = query.filter_by(type=AssignmentType.USER_PROJECT)
+ query = query.filter_by(target_id=tenant_id)
+ query = query.distinct('actor_id')
+ assignments = query.all()
+ return [assignment.actor_id for assignment in assignments]
+
+ def _get_metadata(self, user_id=None, tenant_id=None,
+ domain_id=None, group_id=None, session=None):
+ # TODO(henry-nash): This method represents the last vestiges of the old
+ # metadata concept in this driver. Although we no longer need it here,
+ # since the Manager layer uses the metadata concept across all
+ # assignment drivers, we need to remove it from all of them in order to
+ # finally remove this method.
+
+ # We aren't given a session when called by the manager directly.
+ if session is None:
+ session = sql.get_session()
+
+ q = session.query(RoleAssignment)
+
+ def _calc_assignment_type():
+ # Figure out the assignment type we're checking for from the args.
+ if user_id:
+ if tenant_id:
+ return AssignmentType.USER_PROJECT
+ else:
+ return AssignmentType.USER_DOMAIN
+ else:
+ if tenant_id:
+ return AssignmentType.GROUP_PROJECT
+ else:
+ return AssignmentType.GROUP_DOMAIN
+
+ q = q.filter_by(type=_calc_assignment_type())
+ q = q.filter_by(actor_id=user_id or group_id)
+ q = q.filter_by(target_id=tenant_id or domain_id)
+ refs = q.all()
+ if not refs:
+ raise exception.MetadataNotFound()
+
+ metadata_ref = {}
+ metadata_ref['roles'] = []
+ for assignment in refs:
+ role_ref = {}
+ role_ref['id'] = assignment.role_id
+ if assignment.inherited:
+ role_ref['inherited_to'] = 'projects'
+ metadata_ref['roles'].append(role_ref)
+
+ return metadata_ref
+
+ def create_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+
+ assignment_type = AssignmentType.calculate_type(
+ user_id, group_id, project_id, domain_id)
+ try:
+ with sql.transaction() as session:
+ session.add(RoleAssignment(
+ type=assignment_type,
+ actor_id=user_id or group_id,
+ target_id=project_id or domain_id,
+ role_id=role_id,
+ inherited=inherited_to_projects))
+ except sql.DBDuplicateEntry:
+ # The v3 grant APIs are silent if the assignment already exists
+ pass
+
+ def list_grant_role_ids(self, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ with sql.transaction() as session:
+ q = session.query(RoleAssignment.role_id)
+ q = q.filter(RoleAssignment.actor_id == (user_id or group_id))
+ q = q.filter(RoleAssignment.target_id == (project_id or domain_id))
+ q = q.filter(RoleAssignment.inherited == inherited_to_projects)
+ return [x.role_id for x in q.all()]
+
+ def _build_grant_filter(self, session, role_id, user_id, group_id,
+ domain_id, project_id, inherited_to_projects):
+ q = session.query(RoleAssignment)
+ q = q.filter_by(actor_id=user_id or group_id)
+ q = q.filter_by(target_id=project_id or domain_id)
+ q = q.filter_by(role_id=role_id)
+ q = q.filter_by(inherited=inherited_to_projects)
+ return q
+
+ def check_grant_role_id(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ with sql.transaction() as session:
+ try:
+ q = self._build_grant_filter(
+ session, role_id, user_id, group_id, domain_id, project_id,
+ inherited_to_projects)
+ q.one()
+ except sql.NotFound:
+ actor_id = user_id or group_id
+ target_id = domain_id or project_id
+ raise exception.RoleAssignmentNotFound(role_id=role_id,
+ actor_id=actor_id,
+ target_id=target_id)
+
+ def delete_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ with sql.transaction() as session:
+ q = self._build_grant_filter(
+ session, role_id, user_id, group_id, domain_id, project_id,
+ inherited_to_projects)
+ if not q.delete(False):
+ actor_id = user_id or group_id
+ target_id = domain_id or project_id
+ raise exception.RoleAssignmentNotFound(role_id=role_id,
+ actor_id=actor_id,
+ target_id=target_id)
+
+ def _list_project_ids_for_actor(self, actors, hints, inherited,
+ group_only=False):
+ # TODO(henry-nash): Now that we have a single assignment table, we
+ # should be able to honor the hints list that is provided.
+
+ assignment_type = [AssignmentType.GROUP_PROJECT]
+ if not group_only:
+ assignment_type.append(AssignmentType.USER_PROJECT)
+
+ sql_constraints = sqlalchemy.and_(
+ RoleAssignment.type.in_(assignment_type),
+ RoleAssignment.inherited == inherited,
+ RoleAssignment.actor_id.in_(actors))
+
+ with sql.transaction() as session:
+ query = session.query(RoleAssignment.target_id).filter(
+ sql_constraints).distinct()
+
+ return [x.target_id for x in query.all()]
+
+ def list_project_ids_for_user(self, user_id, group_ids, hints,
+ inherited=False):
+ actor_list = [user_id]
+ if group_ids:
+ actor_list = actor_list + group_ids
+
+ return self._list_project_ids_for_actor(actor_list, hints, inherited)
+
+ def list_domain_ids_for_user(self, user_id, group_ids, hints,
+ inherited=False):
+ with sql.transaction() as session:
+ query = session.query(RoleAssignment.target_id)
+ filters = []
+
+ if user_id:
+ sql_constraints = sqlalchemy.and_(
+ RoleAssignment.actor_id == user_id,
+ RoleAssignment.inherited == inherited,
+ RoleAssignment.type == AssignmentType.USER_DOMAIN)
+ filters.append(sql_constraints)
+
+ if group_ids:
+ sql_constraints = sqlalchemy.and_(
+ RoleAssignment.actor_id.in_(group_ids),
+ RoleAssignment.inherited == inherited,
+ RoleAssignment.type == AssignmentType.GROUP_DOMAIN)
+ filters.append(sql_constraints)
+
+ if not filters:
+ return []
+
+ query = query.filter(sqlalchemy.or_(*filters)).distinct()
+
+ return [assignment.target_id for assignment in query.all()]
+
+ def list_role_ids_for_groups_on_domain(self, group_ids, domain_id):
+ if not group_ids:
+ # If there's no groups then there will be no domain roles.
+ return []
+
+ sql_constraints = sqlalchemy.and_(
+ RoleAssignment.type == AssignmentType.GROUP_DOMAIN,
+ RoleAssignment.target_id == domain_id,
+ RoleAssignment.inherited == false(),
+ RoleAssignment.actor_id.in_(group_ids))
+
+ with sql.transaction() as session:
+ query = session.query(RoleAssignment.role_id).filter(
+ sql_constraints).distinct()
+ return [role.role_id for role in query.all()]
+
+ def list_role_ids_for_groups_on_project(
+ self, group_ids, project_id, project_domain_id, project_parents):
+
+ if not group_ids:
+ # If there's no groups then there will be no project roles.
+ return []
+
+ # NOTE(rodrigods): First, we always include projects with
+ # non-inherited assignments
+ sql_constraints = sqlalchemy.and_(
+ RoleAssignment.type == AssignmentType.GROUP_PROJECT,
+ RoleAssignment.inherited == false(),
+ RoleAssignment.target_id == project_id)
+
+ if CONF.os_inherit.enabled:
+ # Inherited roles from domains
+ sql_constraints = sqlalchemy.or_(
+ sql_constraints,
+ sqlalchemy.and_(
+ RoleAssignment.type == AssignmentType.GROUP_DOMAIN,
+ RoleAssignment.inherited,
+ RoleAssignment.target_id == project_domain_id))
+
+ # Inherited roles from projects
+ if project_parents:
+ sql_constraints = sqlalchemy.or_(
+ sql_constraints,
+ sqlalchemy.and_(
+ RoleAssignment.type == AssignmentType.GROUP_PROJECT,
+ RoleAssignment.inherited,
+ RoleAssignment.target_id.in_(project_parents)))
+
+ sql_constraints = sqlalchemy.and_(
+ sql_constraints, RoleAssignment.actor_id.in_(group_ids))
+
+ with sql.transaction() as session:
+ # NOTE(morganfainberg): Only select the columns we actually care
+ # about here, in this case role_id.
+ query = session.query(RoleAssignment.role_id).filter(
+ sql_constraints).distinct()
+
+ return [result.role_id for result in query.all()]
+
+ def list_project_ids_for_groups(self, group_ids, hints,
+ inherited=False):
+ return self._list_project_ids_for_actor(
+ group_ids, hints, inherited, group_only=True)
+
+ def list_domain_ids_for_groups(self, group_ids, inherited=False):
+ if not group_ids:
+ # If there's no groups then there will be no domains.
+ return []
+
+ group_sql_conditions = sqlalchemy.and_(
+ RoleAssignment.type == AssignmentType.GROUP_DOMAIN,
+ RoleAssignment.inherited == inherited,
+ RoleAssignment.actor_id.in_(group_ids))
+
+ with sql.transaction() as session:
+ query = session.query(RoleAssignment.target_id).filter(
+ group_sql_conditions).distinct()
+ return [x.target_id for x in query.all()]
+
+ def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
+ try:
+ with sql.transaction() as session:
+ session.add(RoleAssignment(
+ type=AssignmentType.USER_PROJECT,
+ actor_id=user_id, target_id=tenant_id,
+ role_id=role_id, inherited=False))
+ except sql.DBDuplicateEntry:
+ msg = ('User %s already has role %s in tenant %s'
+ % (user_id, role_id, tenant_id))
+ raise exception.Conflict(type='role grant', details=msg)
+
+ def remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
+ with sql.transaction() as session:
+ q = session.query(RoleAssignment)
+ q = q.filter_by(actor_id=user_id)
+ q = q.filter_by(target_id=tenant_id)
+ q = q.filter_by(role_id=role_id)
+ if q.delete() == 0:
+ raise exception.RoleNotFound(message=_(
+ 'Cannot remove role that has not been granted, %s') %
+ role_id)
+
+ def list_role_assignments(self):
+
+ def denormalize_role(ref):
+ assignment = {}
+ if ref.type == AssignmentType.USER_PROJECT:
+ assignment['user_id'] = ref.actor_id
+ assignment['project_id'] = ref.target_id
+ elif ref.type == AssignmentType.USER_DOMAIN:
+ assignment['user_id'] = ref.actor_id
+ assignment['domain_id'] = ref.target_id
+ elif ref.type == AssignmentType.GROUP_PROJECT:
+ assignment['group_id'] = ref.actor_id
+ assignment['project_id'] = ref.target_id
+ elif ref.type == AssignmentType.GROUP_DOMAIN:
+ assignment['group_id'] = ref.actor_id
+ assignment['domain_id'] = ref.target_id
+ else:
+ raise exception.Error(message=_(
+ 'Unexpected assignment type encountered, %s') %
+ ref.type)
+ assignment['role_id'] = ref.role_id
+ if ref.inherited:
+ assignment['inherited_to_projects'] = 'projects'
+ return assignment
+
+ with sql.transaction() as session:
+ refs = session.query(RoleAssignment).all()
+ return [denormalize_role(ref) for ref in refs]
+
+ def delete_project_assignments(self, project_id):
+ with sql.transaction() as session:
+ q = session.query(RoleAssignment)
+ q = q.filter_by(target_id=project_id)
+ q.delete(False)
+
+ def delete_role_assignments(self, role_id):
+ with sql.transaction() as session:
+ q = session.query(RoleAssignment)
+ q = q.filter_by(role_id=role_id)
+ q.delete(False)
+
+ def delete_user(self, user_id):
+ with sql.transaction() as session:
+ q = session.query(RoleAssignment)
+ q = q.filter_by(actor_id=user_id)
+ q.delete(False)
+
+ def delete_group(self, group_id):
+ with sql.transaction() as session:
+ q = session.query(RoleAssignment)
+ q = q.filter_by(actor_id=group_id)
+ q.delete(False)
+
+
+class RoleAssignment(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'assignment'
+ attributes = ['type', 'actor_id', 'target_id', 'role_id', 'inherited']
+ # NOTE(henry-nash); Postgres requires a name to be defined for an Enum
+ type = sql.Column(
+ sql.Enum(AssignmentType.USER_PROJECT, AssignmentType.GROUP_PROJECT,
+ AssignmentType.USER_DOMAIN, AssignmentType.GROUP_DOMAIN,
+ name='type'),
+ nullable=False)
+ actor_id = sql.Column(sql.String(64), nullable=False, index=True)
+ target_id = sql.Column(sql.String(64), nullable=False)
+ role_id = sql.Column(sql.String(64), nullable=False)
+ inherited = sql.Column(sql.Boolean, default=False, nullable=False)
+ __table_args__ = (sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id',
+ 'role_id'), {})
+
+ def to_dict(self):
+ """Override parent to_dict() method with a simpler implementation.
+
+ RoleAssignment doesn't have non-indexed 'extra' attributes, so the
+ parent implementation is not applicable.
+ """
+ return dict(six.iteritems(self))
diff --git a/keystone-moon/keystone/assignment/controllers.py b/keystone-moon/keystone/assignment/controllers.py
new file mode 100644
index 00000000..ff27fd36
--- /dev/null
+++ b/keystone-moon/keystone/assignment/controllers.py
@@ -0,0 +1,816 @@
+# Copyright 2013 Metacloud, Inc.
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Workflow Logic the Assignment service."""
+
+import copy
+import functools
+import uuid
+
+from oslo_config import cfg
+from oslo_log import log
+from six.moves import urllib
+
+from keystone.assignment import schema
+from keystone.common import controller
+from keystone.common import dependency
+from keystone.common import validation
+from keystone import exception
+from keystone.i18n import _, _LW
+from keystone.models import token_model
+from keystone import notifications
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('assignment_api', 'identity_api', 'token_provider_api')
+class TenantAssignment(controller.V2Controller):
+ """The V2 Project APIs that are processing assignments."""
+
+ @controller.v2_deprecated
+ def get_projects_for_token(self, context, **kw):
+ """Get valid tenants for token based on token used to authenticate.
+
+ Pulls the token from the context, validates it and gets the valid
+ tenants for the user in the token.
+
+ Doesn't care about token scopedness.
+
+ """
+ try:
+ token_data = self.token_provider_api.validate_token(
+ context['token_id'])
+ token_ref = token_model.KeystoneToken(token_id=context['token_id'],
+ token_data=token_data)
+ except exception.NotFound as e:
+ LOG.warning(_LW('Authentication failed: %s'), e)
+ raise exception.Unauthorized(e)
+
+ tenant_refs = (
+ self.assignment_api.list_projects_for_user(token_ref.user_id))
+ tenant_refs = [self.filter_domain_id(ref) for ref in tenant_refs
+ if ref['domain_id'] == CONF.identity.default_domain_id]
+ params = {
+ 'limit': context['query_string'].get('limit'),
+ 'marker': context['query_string'].get('marker'),
+ }
+ return self.format_project_list(tenant_refs, **params)
+
+ @controller.v2_deprecated
+ def get_project_users(self, context, tenant_id, **kw):
+ self.assert_admin(context)
+ user_refs = []
+ user_ids = self.assignment_api.list_user_ids_for_project(tenant_id)
+ for user_id in user_ids:
+ try:
+ user_ref = self.identity_api.get_user(user_id)
+ except exception.UserNotFound:
+ # Log that user is missing and continue on.
+ message = ("User %(user_id)s in project %(project_id)s "
+ "doesn't exist.")
+ LOG.debug(message,
+ {'user_id': user_id, 'project_id': tenant_id})
+ else:
+ user_refs.append(self.v3_to_v2_user(user_ref))
+ return {'users': user_refs}
+
+
+@dependency.requires('assignment_api', 'role_api')
+class Role(controller.V2Controller):
+ """The Role management APIs."""
+
+ @controller.v2_deprecated
+ def get_role(self, context, role_id):
+ self.assert_admin(context)
+ return {'role': self.role_api.get_role(role_id)}
+
+ @controller.v2_deprecated
+ def create_role(self, context, role):
+ role = self._normalize_dict(role)
+ self.assert_admin(context)
+
+ if 'name' not in role or not role['name']:
+ msg = _('Name field is required and cannot be empty')
+ raise exception.ValidationError(message=msg)
+
+ role_id = uuid.uuid4().hex
+ role['id'] = role_id
+ role_ref = self.role_api.create_role(role_id, role)
+ return {'role': role_ref}
+
+ @controller.v2_deprecated
+ def delete_role(self, context, role_id):
+ self.assert_admin(context)
+ self.role_api.delete_role(role_id)
+
+ @controller.v2_deprecated
+ def get_roles(self, context):
+ self.assert_admin(context)
+ return {'roles': self.role_api.list_roles()}
+
+
+@dependency.requires('assignment_api', 'resource_api', 'role_api')
+class RoleAssignmentV2(controller.V2Controller):
+ """The V2 Role APIs that are processing assignments."""
+
+ # COMPAT(essex-3)
+ @controller.v2_deprecated
+ def get_user_roles(self, context, user_id, tenant_id=None):
+ """Get the roles for a user and tenant pair.
+
+ Since we're trying to ignore the idea of user-only roles we're
+ not implementing them in hopes that the idea will die off.
+
+ """
+ self.assert_admin(context)
+ roles = self.assignment_api.get_roles_for_user_and_project(
+ user_id, tenant_id)
+ return {'roles': [self.role_api.get_role(x)
+ for x in roles]}
+
+ @controller.v2_deprecated
+ def add_role_to_user(self, context, user_id, role_id, tenant_id=None):
+ """Add a role to a user and tenant pair.
+
+ Since we're trying to ignore the idea of user-only roles we're
+ not implementing them in hopes that the idea will die off.
+
+ """
+ self.assert_admin(context)
+ if tenant_id is None:
+ raise exception.NotImplemented(message='User roles not supported: '
+ 'tenant_id required')
+
+ self.assignment_api.add_role_to_user_and_project(
+ user_id, tenant_id, role_id)
+
+ role_ref = self.role_api.get_role(role_id)
+ return {'role': role_ref}
+
+ @controller.v2_deprecated
+ def remove_role_from_user(self, context, user_id, role_id, tenant_id=None):
+ """Remove a role from a user and tenant pair.
+
+ Since we're trying to ignore the idea of user-only roles we're
+ not implementing them in hopes that the idea will die off.
+
+ """
+ self.assert_admin(context)
+ if tenant_id is None:
+ raise exception.NotImplemented(message='User roles not supported: '
+ 'tenant_id required')
+
+ # This still has the weird legacy semantics that adding a role to
+ # a user also adds them to a tenant, so we must follow up on that
+ self.assignment_api.remove_role_from_user_and_project(
+ user_id, tenant_id, role_id)
+
+ # COMPAT(diablo): CRUD extension
+ @controller.v2_deprecated
+ def get_role_refs(self, context, user_id):
+ """Ultimate hack to get around having to make role_refs first-class.
+
+ This will basically iterate over the various roles the user has in
+ all tenants the user is a member of and create fake role_refs where
+ the id encodes the user-tenant-role information so we can look
+ up the appropriate data when we need to delete them.
+
+ """
+ self.assert_admin(context)
+ tenants = self.assignment_api.list_projects_for_user(user_id)
+ o = []
+ for tenant in tenants:
+ # As a v2 call, we should limit the response to those projects in
+ # the default domain.
+ if tenant['domain_id'] != CONF.identity.default_domain_id:
+ continue
+ role_ids = self.assignment_api.get_roles_for_user_and_project(
+ user_id, tenant['id'])
+ for role_id in role_ids:
+ ref = {'roleId': role_id,
+ 'tenantId': tenant['id'],
+ 'userId': user_id}
+ ref['id'] = urllib.parse.urlencode(ref)
+ o.append(ref)
+ return {'roles': o}
+
+ # COMPAT(diablo): CRUD extension
+ @controller.v2_deprecated
+ def create_role_ref(self, context, user_id, role):
+ """This is actually used for adding a user to a tenant.
+
+ In the legacy data model adding a user to a tenant required setting
+ a role.
+
+ """
+ self.assert_admin(context)
+ # TODO(termie): for now we're ignoring the actual role
+ tenant_id = role.get('tenantId')
+ role_id = role.get('roleId')
+ self.assignment_api.add_role_to_user_and_project(
+ user_id, tenant_id, role_id)
+
+ role_ref = self.role_api.get_role(role_id)
+ return {'role': role_ref}
+
+ # COMPAT(diablo): CRUD extension
+ @controller.v2_deprecated
+ def delete_role_ref(self, context, user_id, role_ref_id):
+ """This is actually used for deleting a user from a tenant.
+
+ In the legacy data model removing a user from a tenant required
+ deleting a role.
+
+ To emulate this, we encode the tenant and role in the role_ref_id,
+ and if this happens to be the last role for the user-tenant pair,
+ we remove the user from the tenant.
+
+ """
+ self.assert_admin(context)
+ # TODO(termie): for now we're ignoring the actual role
+ role_ref_ref = urllib.parse.parse_qs(role_ref_id)
+ tenant_id = role_ref_ref.get('tenantId')[0]
+ role_id = role_ref_ref.get('roleId')[0]
+ self.assignment_api.remove_role_from_user_and_project(
+ user_id, tenant_id, role_id)
+
+
+@dependency.requires('assignment_api', 'resource_api')
+class ProjectAssignmentV3(controller.V3Controller):
+ """The V3 Project APIs that are processing assignments."""
+
+ collection_name = 'projects'
+ member_name = 'project'
+
+ def __init__(self):
+ super(ProjectAssignmentV3, self).__init__()
+ self.get_member_from_driver = self.resource_api.get_project
+
+ @controller.filterprotected('enabled', 'name')
+ def list_user_projects(self, context, filters, user_id):
+ hints = ProjectAssignmentV3.build_driver_hints(context, filters)
+ refs = self.assignment_api.list_projects_for_user(user_id,
+ hints=hints)
+ return ProjectAssignmentV3.wrap_collection(context, refs, hints=hints)
+
+
+@dependency.requires('role_api')
+class RoleV3(controller.V3Controller):
+ """The V3 Role CRUD APIs."""
+
+ collection_name = 'roles'
+ member_name = 'role'
+
+ def __init__(self):
+ super(RoleV3, self).__init__()
+ self.get_member_from_driver = self.role_api.get_role
+
+ @controller.protected()
+ @validation.validated(schema.role_create, 'role')
+ def create_role(self, context, role):
+ ref = self._assign_unique_id(self._normalize_dict(role))
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.role_api.create_role(ref['id'], ref, initiator)
+ return RoleV3.wrap_member(context, ref)
+
+ @controller.filterprotected('name')
+ def list_roles(self, context, filters):
+ hints = RoleV3.build_driver_hints(context, filters)
+ refs = self.role_api.list_roles(
+ hints=hints)
+ return RoleV3.wrap_collection(context, refs, hints=hints)
+
+ @controller.protected()
+ def get_role(self, context, role_id):
+ ref = self.role_api.get_role(role_id)
+ return RoleV3.wrap_member(context, ref)
+
+ @controller.protected()
+ @validation.validated(schema.role_update, 'role')
+ def update_role(self, context, role_id, role):
+ self._require_matching_id(role_id, role)
+ initiator = notifications._get_request_audit_info(context)
+ ref = self.role_api.update_role(role_id, role, initiator)
+ return RoleV3.wrap_member(context, ref)
+
+ @controller.protected()
+ def delete_role(self, context, role_id):
+ initiator = notifications._get_request_audit_info(context)
+ self.role_api.delete_role(role_id, initiator)
+
+
+@dependency.requires('assignment_api', 'identity_api', 'resource_api',
+ 'role_api')
+class GrantAssignmentV3(controller.V3Controller):
+ """The V3 Grant Assignment APIs."""
+
+ collection_name = 'roles'
+ member_name = 'role'
+
+ def __init__(self):
+ super(GrantAssignmentV3, self).__init__()
+ self.get_member_from_driver = self.role_api.get_role
+
+ def _require_domain_xor_project(self, domain_id, project_id):
+ if domain_id and project_id:
+ msg = _('Specify a domain or project, not both')
+ raise exception.ValidationError(msg)
+ if not domain_id and not project_id:
+ msg = _('Specify one of domain or project')
+ raise exception.ValidationError(msg)
+
+ def _require_user_xor_group(self, user_id, group_id):
+ if user_id and group_id:
+ msg = _('Specify a user or group, not both')
+ raise exception.ValidationError(msg)
+ if not user_id and not group_id:
+ msg = _('Specify one of user or group')
+ raise exception.ValidationError(msg)
+
+ def _check_if_inherited(self, context):
+ return (CONF.os_inherit.enabled and
+ context['path'].startswith('/OS-INHERIT') and
+ context['path'].endswith('/inherited_to_projects'))
+
+ def _check_grant_protection(self, context, protection, role_id=None,
+ user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ allow_no_user=False):
+ """Check protection for role grant APIs.
+
+ The policy rule might want to inspect attributes of any of the entities
+ involved in the grant. So we get these and pass them to the
+ check_protection() handler in the controller.
+
+ """
+ ref = {}
+ if role_id:
+ ref['role'] = self.role_api.get_role(role_id)
+ if user_id:
+ try:
+ ref['user'] = self.identity_api.get_user(user_id)
+ except exception.UserNotFound:
+ if not allow_no_user:
+ raise
+ else:
+ ref['group'] = self.identity_api.get_group(group_id)
+
+ if domain_id:
+ ref['domain'] = self.resource_api.get_domain(domain_id)
+ else:
+ ref['project'] = self.resource_api.get_project(project_id)
+
+ self.check_protection(context, protection, ref)
+
+ @controller.protected(callback=_check_grant_protection)
+ def create_grant(self, context, role_id, user_id=None,
+ group_id=None, domain_id=None, project_id=None):
+ """Grants a role to a user or group on either a domain or project."""
+ self._require_domain_xor_project(domain_id, project_id)
+ self._require_user_xor_group(user_id, group_id)
+
+ self.assignment_api.create_grant(
+ role_id, user_id, group_id, domain_id, project_id,
+ self._check_if_inherited(context), context)
+
+ @controller.protected(callback=_check_grant_protection)
+ def list_grants(self, context, user_id=None,
+ group_id=None, domain_id=None, project_id=None):
+ """Lists roles granted to user/group on either a domain or project."""
+ self._require_domain_xor_project(domain_id, project_id)
+ self._require_user_xor_group(user_id, group_id)
+
+ refs = self.assignment_api.list_grants(
+ user_id, group_id, domain_id, project_id,
+ self._check_if_inherited(context))
+ return GrantAssignmentV3.wrap_collection(context, refs)
+
+ @controller.protected(callback=_check_grant_protection)
+ def check_grant(self, context, role_id, user_id=None,
+ group_id=None, domain_id=None, project_id=None):
+ """Checks if a role has been granted on either a domain or project."""
+ self._require_domain_xor_project(domain_id, project_id)
+ self._require_user_xor_group(user_id, group_id)
+
+ self.assignment_api.get_grant(
+ role_id, user_id, group_id, domain_id, project_id,
+ self._check_if_inherited(context))
+
+ # NOTE(lbragstad): This will allow users to clean up role assignments
+ # from the backend in the event the user was removed prior to the role
+ # assignment being removed.
+ @controller.protected(callback=functools.partial(
+ _check_grant_protection, allow_no_user=True))
+ def revoke_grant(self, context, role_id, user_id=None,
+ group_id=None, domain_id=None, project_id=None):
+ """Revokes a role from user/group on either a domain or project."""
+ self._require_domain_xor_project(domain_id, project_id)
+ self._require_user_xor_group(user_id, group_id)
+
+ self.assignment_api.delete_grant(
+ role_id, user_id, group_id, domain_id, project_id,
+ self._check_if_inherited(context), context)
+
+
+@dependency.requires('assignment_api', 'identity_api', 'resource_api')
+class RoleAssignmentV3(controller.V3Controller):
+ """The V3 Role Assignment APIs, really just list_role_assignment()."""
+
+ # TODO(henry-nash): The current implementation does not provide a full
+ # first class entity for role-assignment. There is no role_assignment_id
+ # and only the list_role_assignment call is supported. Further, since it
+ # is not a first class entity, the links for the individual entities
+ # reference the individual role grant APIs.
+
+ collection_name = 'role_assignments'
+ member_name = 'role_assignment'
+
+ @classmethod
+ def wrap_member(cls, context, ref):
+ # NOTE(henry-nash): Since we are not yet a true collection, we override
+ # the wrapper as have already included the links in the entities
+ pass
+
+ def _format_entity(self, context, entity):
+ """Format an assignment entity for API response.
+
+ The driver layer returns entities as dicts containing the ids of the
+ actor (e.g. user or group), target (e.g. domain or project) and role.
+ If it is an inherited role, then this is also indicated. Examples:
+
+ {'user_id': user_id,
+ 'project_id': domain_id,
+ 'role_id': role_id}
+
+ or, for an inherited role:
+
+ {'user_id': user_id,
+ 'domain_id': domain_id,
+ 'role_id': role_id,
+ 'inherited_to_projects': true}
+
+ This function maps this into the format to be returned via the API,
+ e.g. for the second example above:
+
+ {
+ 'user': {
+ {'id': user_id}
+ },
+ 'scope': {
+ 'domain': {
+ {'id': domain_id}
+ },
+ 'OS-INHERIT:inherited_to': 'projects
+ },
+ 'role': {
+ {'id': role_id}
+ },
+ 'links': {
+ 'assignment': '/domains/domain_id/users/user_id/roles/'
+ 'role_id/inherited_to_projects'
+ }
+ }
+
+ """
+
+ formatted_entity = {}
+ suffix = ""
+ if 'user_id' in entity:
+ formatted_entity['user'] = {'id': entity['user_id']}
+ actor_link = 'users/%s' % entity['user_id']
+ if 'group_id' in entity:
+ formatted_entity['group'] = {'id': entity['group_id']}
+ actor_link = 'groups/%s' % entity['group_id']
+ if 'role_id' in entity:
+ formatted_entity['role'] = {'id': entity['role_id']}
+ if 'project_id' in entity:
+ formatted_entity['scope'] = (
+ {'project': {'id': entity['project_id']}})
+ if 'inherited_to_projects' in entity:
+ formatted_entity['scope']['OS-INHERIT:inherited_to'] = (
+ 'projects')
+ target_link = '/OS-INHERIT/projects/%s' % entity['project_id']
+ suffix = '/inherited_to_projects'
+ else:
+ target_link = '/projects/%s' % entity['project_id']
+ if 'domain_id' in entity:
+ formatted_entity['scope'] = (
+ {'domain': {'id': entity['domain_id']}})
+ if 'inherited_to_projects' in entity:
+ formatted_entity['scope']['OS-INHERIT:inherited_to'] = (
+ 'projects')
+ target_link = '/OS-INHERIT/domains/%s' % entity['domain_id']
+ suffix = '/inherited_to_projects'
+ else:
+ target_link = '/domains/%s' % entity['domain_id']
+ formatted_entity.setdefault('links', {})
+
+ path = '%(target)s/%(actor)s/roles/%(role)s%(suffix)s' % {
+ 'target': target_link,
+ 'actor': actor_link,
+ 'role': entity['role_id'],
+ 'suffix': suffix}
+ formatted_entity['links']['assignment'] = self.base_url(context, path)
+
+ return formatted_entity
+
+ def _expand_indirect_assignments(self, context, refs):
+ """Processes entity list into all-direct assignments.
+
+ For any group role assignments in the list, create a role assignment
+ entity for each member of that group, and then remove the group
+ assignment entity itself from the list.
+
+ If the OS-INHERIT extension is enabled, then honor any inherited
+ roles on the domain by creating the equivalent on all projects
+ owned by the domain.
+
+ For any new entity created by virtue of group membership, add in an
+ additional link to that membership.
+
+ """
+ def _get_group_members(ref):
+ """Get a list of group members.
+
+ Get the list of group members. If this fails with
+ GroupNotFound, then log this as a warning, but allow
+ overall processing to continue.
+
+ """
+ try:
+ members = self.identity_api.list_users_in_group(
+ ref['group']['id'])
+ except exception.GroupNotFound:
+ members = []
+ # The group is missing, which should not happen since
+ # group deletion should remove any related assignments, so
+ # log a warning
+ target = 'Unknown'
+ # Should always be a domain or project, but since to get
+ # here things have gone astray, let's be cautious.
+ if 'scope' in ref:
+ if 'domain' in ref['scope']:
+ dom_id = ref['scope']['domain'].get('id', 'Unknown')
+ target = 'Domain: %s' % dom_id
+ elif 'project' in ref['scope']:
+ proj_id = ref['scope']['project'].get('id', 'Unknown')
+ target = 'Project: %s' % proj_id
+ role_id = 'Unknown'
+ if 'role' in ref and 'id' in ref['role']:
+ role_id = ref['role']['id']
+ LOG.warning(
+ _LW('Group %(group)s not found for role-assignment - '
+ '%(target)s with Role: %(role)s'), {
+ 'group': ref['group']['id'], 'target': target,
+ 'role': role_id})
+ return members
+
+ def _build_user_assignment_equivalent_of_group(
+ user, group_id, template):
+ """Create a user assignment equivalent to the group one.
+
+ The template has had the 'group' entity removed, so
+ substitute a 'user' one. The 'assignment' link stays as it is,
+ referring to the group assignment that led to this role.
+ A 'membership' link is added that refers to this particular
+ user's membership of this group.
+
+ """
+ user_entry = copy.deepcopy(template)
+ user_entry['user'] = {'id': user['id']}
+ user_entry['links']['membership'] = (
+ self.base_url(context, '/groups/%s/users/%s' %
+ (group_id, user['id'])))
+ return user_entry
+
+ def _build_project_equivalent_of_user_target_role(
+ project_id, target_id, target_type, template):
+ """Create a user project assignment equivalent to the domain one.
+
+ The template has had the 'domain' entity removed, so
+ substitute a 'project' one, modifying the 'assignment' link
+ to match.
+
+ """
+ project_entry = copy.deepcopy(template)
+ project_entry['scope']['project'] = {'id': project_id}
+ project_entry['links']['assignment'] = (
+ self.base_url(
+ context,
+ '/OS-INHERIT/%s/%s/users/%s/roles/%s'
+ '/inherited_to_projects' % (
+ target_type, target_id, project_entry['user']['id'],
+ project_entry['role']['id'])))
+ return project_entry
+
+ def _build_project_equivalent_of_group_target_role(
+ user_id, group_id, project_id,
+ target_id, target_type, template):
+ """Create a user project equivalent to the domain group one.
+
+ The template has had the 'domain' and 'group' entities removed, so
+ substitute a 'user-project' one, modifying the 'assignment' link
+ to match.
+
+ """
+ project_entry = copy.deepcopy(template)
+ project_entry['user'] = {'id': user_id}
+ project_entry['scope']['project'] = {'id': project_id}
+ project_entry['links']['assignment'] = (
+ self.base_url(context,
+ '/OS-INHERIT/%s/%s/groups/%s/roles/%s'
+ '/inherited_to_projects' % (
+ target_type, target_id, group_id,
+ project_entry['role']['id'])))
+ project_entry['links']['membership'] = (
+ self.base_url(context, '/groups/%s/users/%s' %
+ (group_id, user_id)))
+ return project_entry
+
+ # Scan the list of entities for any assignments that need to be
+ # expanded.
+ #
+ # If the OS-INERIT extension is enabled, the refs lists may
+ # contain roles to be inherited from domain to project, so expand
+ # these as well into project equivalents
+ #
+ # For any regular group entries, expand these into user entries based
+ # on membership of that group.
+ #
+ # Due to the potentially large expansions, rather than modify the
+ # list we are enumerating, we build a new one as we go.
+ #
+
+ new_refs = []
+ for r in refs:
+ if 'OS-INHERIT:inherited_to' in r['scope']:
+ if 'domain' in r['scope']:
+ # It's an inherited domain role - so get the list of
+ # projects owned by this domain.
+ project_ids = (
+ [x['id'] for x in
+ self.resource_api.list_projects_in_domain(
+ r['scope']['domain']['id'])])
+ base_entry = copy.deepcopy(r)
+ target_type = 'domains'
+ target_id = base_entry['scope']['domain']['id']
+ base_entry['scope'].pop('domain')
+ else:
+ # It's an inherited project role - so get the list of
+ # projects in this project subtree.
+ project_id = r['scope']['project']['id']
+ project_ids = (
+ [x['id'] for x in
+ self.resource_api.list_projects_in_subtree(
+ project_id)])
+ base_entry = copy.deepcopy(r)
+ target_type = 'projects'
+ target_id = base_entry['scope']['project']['id']
+ base_entry['scope'].pop('project')
+
+ # For each project, create an equivalent role assignment
+ for p in project_ids:
+ # If it's a group assignment, then create equivalent user
+ # roles based on membership of the group
+ if 'group' in base_entry:
+ members = _get_group_members(base_entry)
+ sub_entry = copy.deepcopy(base_entry)
+ group_id = sub_entry['group']['id']
+ sub_entry.pop('group')
+ for m in members:
+ new_entry = (
+ _build_project_equivalent_of_group_target_role(
+ m['id'], group_id, p,
+ target_id, target_type, sub_entry))
+ new_refs.append(new_entry)
+ else:
+ new_entry = (
+ _build_project_equivalent_of_user_target_role(
+ p, target_id, target_type, base_entry))
+ new_refs.append(new_entry)
+ elif 'group' in r:
+ # It's a non-inherited group role assignment, so get the list
+ # of members.
+ members = _get_group_members(r)
+
+ # Now replace that group role assignment entry with an
+ # equivalent user role assignment for each of the group members
+ base_entry = copy.deepcopy(r)
+ group_id = base_entry['group']['id']
+ base_entry.pop('group')
+ for m in members:
+ user_entry = _build_user_assignment_equivalent_of_group(
+ m, group_id, base_entry)
+ new_refs.append(user_entry)
+ else:
+ new_refs.append(r)
+
+ return new_refs
+
+ def _filter_inherited(self, entry):
+ if ('inherited_to_projects' in entry and
+ not CONF.os_inherit.enabled):
+ return False
+ else:
+ return True
+
+ def _assert_effective_filters(self, inherited, group, domain):
+ """Assert that useless filter combinations are avoided.
+
+ In effective mode, the following filter combinations are useless, since
+ they would always return an empty list of role assignments:
+ - group id, since no group assignment is returned in effective mode;
+ - domain id and inherited, since no domain inherited assignment is
+ returned in effective mode.
+
+ """
+ if group:
+ msg = _('Combining effective and group filter will always '
+ 'result in an empty list.')
+ raise exception.ValidationError(msg)
+
+ if inherited and domain:
+ msg = _('Combining effective, domain and inherited filters will '
+ 'always result in an empty list.')
+ raise exception.ValidationError(msg)
+
+ def _assert_domain_nand_project(self, domain_id, project_id):
+ if domain_id and project_id:
+ msg = _('Specify a domain or project, not both')
+ raise exception.ValidationError(msg)
+
+ def _assert_user_nand_group(self, user_id, group_id):
+ if user_id and group_id:
+ msg = _('Specify a user or group, not both')
+ raise exception.ValidationError(msg)
+
+ @controller.filterprotected('group.id', 'role.id',
+ 'scope.domain.id', 'scope.project.id',
+ 'scope.OS-INHERIT:inherited_to', 'user.id')
+ def list_role_assignments(self, context, filters):
+
+ # TODO(henry-nash): This implementation uses the standard filtering
+ # in the V3.wrap_collection. Given the large number of individual
+ # assignments, this is pretty inefficient. An alternative would be
+ # to pass the filters into the driver call, so that the list size is
+ # kept a minimum.
+
+ params = context['query_string']
+ effective = 'effective' in params and (
+ self.query_filter_is_true(params['effective']))
+
+ if 'scope.OS-INHERIT:inherited_to' in params:
+ inherited = (
+ params['scope.OS-INHERIT:inherited_to'] == 'projects')
+ else:
+ # None means querying both inherited and direct assignments
+ inherited = None
+
+ self._assert_domain_nand_project(params.get('scope.domain.id'),
+ params.get('scope.project.id'))
+ self._assert_user_nand_group(params.get('user.id'),
+ params.get('group.id'))
+
+ if effective:
+ self._assert_effective_filters(inherited=inherited,
+ group=params.get('group.id'),
+ domain=params.get(
+ 'scope.domain.id'))
+
+ hints = self.build_driver_hints(context, filters)
+ refs = self.assignment_api.list_role_assignments()
+ formatted_refs = (
+ [self._format_entity(context, x) for x in refs
+ if self._filter_inherited(x)])
+
+ if effective:
+ formatted_refs = self._expand_indirect_assignments(context,
+ formatted_refs)
+
+ return self.wrap_collection(context, formatted_refs, hints=hints)
+
+ @controller.protected()
+ def get_role_assignment(self, context):
+ raise exception.NotImplemented()
+
+ @controller.protected()
+ def update_role_assignment(self, context):
+ raise exception.NotImplemented()
+
+ @controller.protected()
+ def delete_role_assignment(self, context):
+ raise exception.NotImplemented()
diff --git a/keystone-moon/keystone/assignment/core.py b/keystone-moon/keystone/assignment/core.py
new file mode 100644
index 00000000..0f9c03e9
--- /dev/null
+++ b/keystone-moon/keystone/assignment/core.py
@@ -0,0 +1,1019 @@
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Main entry point into the assignment service."""
+
+import abc
+
+from oslo_config import cfg
+from oslo_log import log
+import six
+
+from keystone.common import cache
+from keystone.common import dependency
+from keystone.common import driver_hints
+from keystone.common import manager
+from keystone import exception
+from keystone.i18n import _
+from keystone.i18n import _LI
+from keystone import notifications
+from keystone.openstack.common import versionutils
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+MEMOIZE = cache.get_memoization_decorator(section='role')
+
+
+def deprecated_to_role_api(f):
+ """Specialized deprecation wrapper for assignment to role api.
+
+ This wraps the standard deprecation wrapper and fills in the method
+ names automatically.
+
+ """
+ @six.wraps(f)
+ def wrapper(*args, **kwargs):
+ x = versionutils.deprecated(
+ what='assignment.' + f.__name__ + '()',
+ as_of=versionutils.deprecated.KILO,
+ in_favor_of='role.' + f.__name__ + '()')
+ return x(f)
+ return wrapper()
+
+
+def deprecated_to_resource_api(f):
+ """Specialized deprecation wrapper for assignment to resource api.
+
+ This wraps the standard deprecation wrapper and fills in the method
+ names automatically.
+
+ """
+ @six.wraps(f)
+ def wrapper(*args, **kwargs):
+ x = versionutils.deprecated(
+ what='assignment.' + f.__name__ + '()',
+ as_of=versionutils.deprecated.KILO,
+ in_favor_of='resource.' + f.__name__ + '()')
+ return x(f)
+ return wrapper()
+
+
+@dependency.provider('assignment_api')
+@dependency.requires('credential_api', 'identity_api', 'resource_api',
+ 'revoke_api', 'role_api')
+class Manager(manager.Manager):
+ """Default pivot point for the Assignment backend.
+
+ See :mod:`keystone.common.manager.Manager` for more details on how this
+ dynamically calls the backend.
+
+ """
+ _PROJECT = 'project'
+ _ROLE_REMOVED_FROM_USER = 'role_removed_from_user'
+ _INVALIDATION_USER_PROJECT_TOKENS = 'invalidate_user_project_tokens'
+
+ def __init__(self):
+ assignment_driver = CONF.assignment.driver
+
+ # If there is no explicit assignment driver specified, we let the
+ # identity driver tell us what to use. This is for backward
+ # compatibility reasons from the time when identity, resource and
+ # assignment were all part of identity.
+ if assignment_driver is None:
+ identity_driver = dependency.get_provider('identity_api').driver
+ assignment_driver = identity_driver.default_assignment_driver()
+
+ super(Manager, self).__init__(assignment_driver)
+
+ def _get_group_ids_for_user_id(self, user_id):
+ # TODO(morganfainberg): Implement a way to get only group_ids
+ # instead of the more expensive to_dict() call for each record.
+ return [x['id'] for
+ x in self.identity_api.list_groups_for_user(user_id)]
+
+ def list_user_ids_for_project(self, tenant_id):
+ self.resource_api.get_project(tenant_id)
+ return self.driver.list_user_ids_for_project(tenant_id)
+
+ def _list_parent_ids_of_project(self, project_id):
+ if CONF.os_inherit.enabled:
+ return [x['id'] for x in (
+ self.resource_api.list_project_parents(project_id))]
+ else:
+ return []
+
+ def get_roles_for_user_and_project(self, user_id, tenant_id):
+ """Get the roles associated with a user within given project.
+
+ This includes roles directly assigned to the user on the
+ project, as well as those by virtue of group membership. If
+ the OS-INHERIT extension is enabled, then this will also
+ include roles inherited from the domain.
+
+ :returns: a list of role ids.
+ :raises: keystone.exception.UserNotFound,
+ keystone.exception.ProjectNotFound
+
+ """
+ def _get_group_project_roles(user_id, project_ref):
+ group_ids = self._get_group_ids_for_user_id(user_id)
+ return self.driver.list_role_ids_for_groups_on_project(
+ group_ids,
+ project_ref['id'],
+ project_ref['domain_id'],
+ self._list_parent_ids_of_project(project_ref['id']))
+
+ def _get_user_project_roles(user_id, project_ref):
+ role_list = []
+ try:
+ metadata_ref = self._get_metadata(user_id=user_id,
+ tenant_id=project_ref['id'])
+ role_list = self._roles_from_role_dicts(
+ metadata_ref.get('roles', {}), False)
+ except exception.MetadataNotFound:
+ pass
+
+ if CONF.os_inherit.enabled:
+ # Now get any inherited roles for the owning domain
+ try:
+ metadata_ref = self._get_metadata(
+ user_id=user_id, domain_id=project_ref['domain_id'])
+ role_list += self._roles_from_role_dicts(
+ metadata_ref.get('roles', {}), True)
+ except (exception.MetadataNotFound, exception.NotImplemented):
+ pass
+ # As well inherited roles from parent projects
+ for p in self.list_project_parents(project_ref['id']):
+ p_roles = self.list_grants(
+ user_id=user_id, project_id=p['id'],
+ inherited_to_projects=True)
+ role_list += [x['id'] for x in p_roles]
+
+ return role_list
+
+ project_ref = self.resource_api.get_project(tenant_id)
+ user_role_list = _get_user_project_roles(user_id, project_ref)
+ group_role_list = _get_group_project_roles(user_id, project_ref)
+ # Use set() to process the list to remove any duplicates
+ return list(set(user_role_list + group_role_list))
+
+ def get_roles_for_user_and_domain(self, user_id, domain_id):
+ """Get the roles associated with a user within given domain.
+
+ :returns: a list of role ids.
+ :raises: keystone.exception.UserNotFound,
+ keystone.exception.DomainNotFound
+
+ """
+
+ def _get_group_domain_roles(user_id, domain_id):
+ role_list = []
+ group_ids = self._get_group_ids_for_user_id(user_id)
+ for group_id in group_ids:
+ try:
+ metadata_ref = self._get_metadata(group_id=group_id,
+ domain_id=domain_id)
+ role_list += self._roles_from_role_dicts(
+ metadata_ref.get('roles', {}), False)
+ except (exception.MetadataNotFound, exception.NotImplemented):
+ # MetadataNotFound implies no group grant, so skip.
+ # Ignore NotImplemented since not all backends support
+ # domains.
+ pass
+ return role_list
+
+ def _get_user_domain_roles(user_id, domain_id):
+ metadata_ref = {}
+ try:
+ metadata_ref = self._get_metadata(user_id=user_id,
+ domain_id=domain_id)
+ except (exception.MetadataNotFound, exception.NotImplemented):
+ # MetadataNotFound implies no user grants.
+ # Ignore NotImplemented since not all backends support
+ # domains
+ pass
+ return self._roles_from_role_dicts(
+ metadata_ref.get('roles', {}), False)
+
+ self.get_domain(domain_id)
+ user_role_list = _get_user_domain_roles(user_id, domain_id)
+ group_role_list = _get_group_domain_roles(user_id, domain_id)
+ # Use set() to process the list to remove any duplicates
+ return list(set(user_role_list + group_role_list))
+
+ def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
+ """Get a list of roles for this group on domain and/or project."""
+
+ if project_id is not None:
+ project = self.resource_api.get_project(project_id)
+ role_ids = self.driver.list_role_ids_for_groups_on_project(
+ group_ids, project_id, project['domain_id'],
+ self._list_parent_ids_of_project(project_id))
+ elif domain_id is not None:
+ role_ids = self.driver.list_role_ids_for_groups_on_domain(
+ group_ids, domain_id)
+ else:
+ raise AttributeError(_("Must specify either domain or project"))
+
+ return self.role_api.list_roles_from_ids(role_ids)
+
+ def add_user_to_project(self, tenant_id, user_id):
+ """Add user to a tenant by creating a default role relationship.
+
+ :raises: keystone.exception.ProjectNotFound,
+ keystone.exception.UserNotFound
+
+ """
+ self.resource_api.get_project(tenant_id)
+ try:
+ self.role_api.get_role(CONF.member_role_id)
+ self.driver.add_role_to_user_and_project(
+ user_id,
+ tenant_id,
+ CONF.member_role_id)
+ except exception.RoleNotFound:
+ LOG.info(_LI("Creating the default role %s "
+ "because it does not exist."),
+ CONF.member_role_id)
+ role = {'id': CONF.member_role_id,
+ 'name': CONF.member_role_name}
+ try:
+ self.role_api.create_role(CONF.member_role_id, role)
+ except exception.Conflict:
+ LOG.info(_LI("Creating the default role %s failed because it "
+ "was already created"),
+ CONF.member_role_id)
+ # now that default role exists, the add should succeed
+ self.driver.add_role_to_user_and_project(
+ user_id,
+ tenant_id,
+ CONF.member_role_id)
+
+ def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
+ self.resource_api.get_project(tenant_id)
+ self.role_api.get_role(role_id)
+ self.driver.add_role_to_user_and_project(user_id, tenant_id, role_id)
+
+ def remove_user_from_project(self, tenant_id, user_id):
+ """Remove user from a tenant
+
+ :raises: keystone.exception.ProjectNotFound,
+ keystone.exception.UserNotFound
+
+ """
+ roles = self.get_roles_for_user_and_project(user_id, tenant_id)
+ if not roles:
+ raise exception.NotFound(tenant_id)
+ for role_id in roles:
+ try:
+ self.driver.remove_role_from_user_and_project(user_id,
+ tenant_id,
+ role_id)
+ self.revoke_api.revoke_by_grant(role_id, user_id=user_id,
+ project_id=tenant_id)
+
+ except exception.RoleNotFound:
+ LOG.debug("Removing role %s failed because it does not exist.",
+ role_id)
+
+ # TODO(henry-nash): We might want to consider list limiting this at some
+ # point in the future.
+ def list_projects_for_user(self, user_id, hints=None):
+ # NOTE(henry-nash): In order to get a complete list of user projects,
+ # the driver will need to look at group assignments. To avoid cross
+ # calling between the assignment and identity driver we get the group
+ # list here and pass it in. The rest of the detailed logic of listing
+ # projects for a user is pushed down into the driver to enable
+ # optimization with the various backend technologies (SQL, LDAP etc.).
+
+ group_ids = self._get_group_ids_for_user_id(user_id)
+ project_ids = self.driver.list_project_ids_for_user(
+ user_id, group_ids, hints or driver_hints.Hints())
+
+ if not CONF.os_inherit.enabled:
+ return self.resource_api.list_projects_from_ids(project_ids)
+
+ # Inherited roles are enabled, so check to see if this user has any
+ # inherited role (direct or group) on any parent project, in which
+ # case we must add in all the projects in that parent's subtree.
+ project_ids = set(project_ids)
+ project_ids_inherited = self.driver.list_project_ids_for_user(
+ user_id, group_ids, hints or driver_hints.Hints(), inherited=True)
+ for proj_id in project_ids_inherited:
+ project_ids.update(
+ (x['id'] for x in
+ self.resource_api.list_projects_in_subtree(proj_id)))
+
+ # Now do the same for any domain inherited roles
+ domain_ids = self.driver.list_domain_ids_for_user(
+ user_id, group_ids, hints or driver_hints.Hints(),
+ inherited=True)
+ project_ids.update(
+ self.resource_api.list_project_ids_from_domain_ids(domain_ids))
+
+ return self.resource_api.list_projects_from_ids(list(project_ids))
+
+ # TODO(henry-nash): We might want to consider list limiting this at some
+ # point in the future.
+ def list_domains_for_user(self, user_id, hints=None):
+ # NOTE(henry-nash): In order to get a complete list of user domains,
+ # the driver will need to look at group assignments. To avoid cross
+ # calling between the assignment and identity driver we get the group
+ # list here and pass it in. The rest of the detailed logic of listing
+ # projects for a user is pushed down into the driver to enable
+ # optimization with the various backend technologies (SQL, LDAP etc.).
+ group_ids = self._get_group_ids_for_user_id(user_id)
+ domain_ids = self.driver.list_domain_ids_for_user(
+ user_id, group_ids, hints or driver_hints.Hints())
+ return self.resource_api.list_domains_from_ids(domain_ids)
+
+ def list_domains_for_groups(self, group_ids):
+ domain_ids = self.driver.list_domain_ids_for_groups(group_ids)
+ return self.resource_api.list_domains_from_ids(domain_ids)
+
+ def list_projects_for_groups(self, group_ids):
+ project_ids = (
+ self.driver.list_project_ids_for_groups(group_ids,
+ driver_hints.Hints()))
+ if not CONF.os_inherit.enabled:
+ return self.resource_api.list_projects_from_ids(project_ids)
+
+ # Inherited roles are enabled, so check to see if these groups have any
+ # roles on any domain, in which case we must add in all the projects
+ # in that domain.
+
+ domain_ids = self.driver.list_domain_ids_for_groups(
+ group_ids, inherited=True)
+
+ project_ids_from_domains = (
+ self.resource_api.list_project_ids_from_domain_ids(domain_ids))
+
+ return self.resource_api.list_projects_from_ids(
+ list(set(project_ids + project_ids_from_domains)))
+
+ def list_role_assignments_for_role(self, role_id=None):
+ # NOTE(henry-nash): Currently the efficiency of the key driver
+ # implementation (SQL) of list_role_assignments is severely hampered by
+ # the existence of the multiple grant tables - hence there is little
+ # advantage in pushing the logic of this method down into the driver.
+ # Once the single assignment table is implemented, then this situation
+ # will be different, and this method should have its own driver
+ # implementation.
+ return [r for r in self.driver.list_role_assignments()
+ if r['role_id'] == role_id]
+
+ def remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
+ self.driver.remove_role_from_user_and_project(user_id, tenant_id,
+ role_id)
+ self.identity_api.emit_invalidate_user_token_persistence(user_id)
+ self.revoke_api.revoke_by_grant(role_id, user_id=user_id,
+ project_id=tenant_id)
+
+ @notifications.internal(notifications.INVALIDATE_USER_TOKEN_PERSISTENCE)
+ def _emit_invalidate_user_token_persistence(self, user_id):
+ self.identity_api.emit_invalidate_user_token_persistence(user_id)
+
+ @notifications.role_assignment('created')
+ def create_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False, context=None):
+ self.role_api.get_role(role_id)
+ if domain_id:
+ self.resource_api.get_domain(domain_id)
+ if project_id:
+ self.resource_api.get_project(project_id)
+ self.driver.create_grant(role_id, user_id, group_id, domain_id,
+ project_id, inherited_to_projects)
+
+ def get_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ role_ref = self.role_api.get_role(role_id)
+ if domain_id:
+ self.resource_api.get_domain(domain_id)
+ if project_id:
+ self.resource_api.get_project(project_id)
+ self.driver.check_grant_role_id(
+ role_id, user_id, group_id, domain_id, project_id,
+ inherited_to_projects)
+ return role_ref
+
+ def list_grants(self, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ if domain_id:
+ self.resource_api.get_domain(domain_id)
+ if project_id:
+ self.resource_api.get_project(project_id)
+ grant_ids = self.driver.list_grant_role_ids(
+ user_id, group_id, domain_id, project_id, inherited_to_projects)
+ return self.role_api.list_roles_from_ids(grant_ids)
+
+ @notifications.role_assignment('deleted')
+ def delete_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False, context=None):
+ if group_id is None:
+ self.revoke_api.revoke_by_grant(user_id=user_id,
+ role_id=role_id,
+ domain_id=domain_id,
+ project_id=project_id)
+ else:
+ try:
+ # NOTE(morganfainberg): The user ids are the important part
+ # for invalidating tokens below, so extract them here.
+ for user in self.identity_api.list_users_in_group(group_id):
+ if user['id'] != user_id:
+ self._emit_invalidate_user_token_persistence(
+ user['id'])
+ self.revoke_api.revoke_by_grant(
+ user_id=user['id'], role_id=role_id,
+ domain_id=domain_id, project_id=project_id)
+ except exception.GroupNotFound:
+ LOG.debug('Group %s not found, no tokens to invalidate.',
+ group_id)
+
+ # TODO(henry-nash): While having the call to get_role here mimics the
+ # previous behavior (when it was buried inside the driver delete call),
+ # this seems an odd place to have this check, given what we have
+ # already done so far in this method. See Bug #1406776.
+ self.role_api.get_role(role_id)
+
+ if domain_id:
+ self.resource_api.get_domain(domain_id)
+ if project_id:
+ self.resource_api.get_project(project_id)
+ self.driver.delete_grant(role_id, user_id, group_id, domain_id,
+ project_id, inherited_to_projects)
+ if user_id is not None:
+ self._emit_invalidate_user_token_persistence(user_id)
+
+ def delete_tokens_for_role_assignments(self, role_id):
+ assignments = self.list_role_assignments_for_role(role_id=role_id)
+
+ # Iterate over the assignments for this role and build the list of
+ # user or user+project IDs for the tokens we need to delete
+ user_ids = set()
+ user_and_project_ids = list()
+ for assignment in assignments:
+ # If we have a project assignment, then record both the user and
+ # project IDs so we can target the right token to delete. If it is
+ # a domain assignment, we might as well kill all the tokens for
+ # the user, since in the vast majority of cases all the tokens
+ # for a user will be within one domain anyway, so not worth
+ # trying to delete tokens for each project in the domain.
+ if 'user_id' in assignment:
+ if 'project_id' in assignment:
+ user_and_project_ids.append(
+ (assignment['user_id'], assignment['project_id']))
+ elif 'domain_id' in assignment:
+ self._emit_invalidate_user_token_persistence(
+ assignment['user_id'])
+ elif 'group_id' in assignment:
+ # Add in any users for this group, being tolerant of any
+ # cross-driver database integrity errors.
+ try:
+ users = self.identity_api.list_users_in_group(
+ assignment['group_id'])
+ except exception.GroupNotFound:
+ # Ignore it, but log a debug message
+ if 'project_id' in assignment:
+ target = _('Project (%s)') % assignment['project_id']
+ elif 'domain_id' in assignment:
+ target = _('Domain (%s)') % assignment['domain_id']
+ else:
+ target = _('Unknown Target')
+ msg = ('Group (%(group)s), referenced in assignment '
+ 'for %(target)s, not found - ignoring.')
+ LOG.debug(msg, {'group': assignment['group_id'],
+ 'target': target})
+ continue
+
+ if 'project_id' in assignment:
+ for user in users:
+ user_and_project_ids.append(
+ (user['id'], assignment['project_id']))
+ elif 'domain_id' in assignment:
+ for user in users:
+ self._emit_invalidate_user_token_persistence(
+ user['id'])
+
+ # Now process the built up lists. Before issuing calls to delete any
+ # tokens, let's try and minimize the number of calls by pruning out
+ # any user+project deletions where a general token deletion for that
+ # same user is also planned.
+ user_and_project_ids_to_action = []
+ for user_and_project_id in user_and_project_ids:
+ if user_and_project_id[0] not in user_ids:
+ user_and_project_ids_to_action.append(user_and_project_id)
+
+ for user_id, project_id in user_and_project_ids_to_action:
+ self._emit_invalidate_user_project_tokens_notification(
+ {'user_id': user_id,
+ 'project_id': project_id})
+
+ @notifications.internal(
+ notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE)
+ def _emit_invalidate_user_project_tokens_notification(self, payload):
+ # This notification's payload is a dict of user_id and
+ # project_id so the token provider can invalidate the tokens
+ # from persistence if persistence is enabled.
+ pass
+
+ @deprecated_to_role_api
+ def create_role(self, role_id, role):
+ return self.role_api.create_role(role_id, role)
+
+ @deprecated_to_role_api
+ def get_role(self, role_id):
+ return self.role_api.get_role(role_id)
+
+ @deprecated_to_role_api
+ def update_role(self, role_id, role):
+ return self.role_api.update_role(role_id, role)
+
+ @deprecated_to_role_api
+ def delete_role(self, role_id):
+ return self.role_api.delete_role(role_id)
+
+ @deprecated_to_role_api
+ def list_roles(self, hints=None):
+ return self.role_api.list_roles(hints=hints)
+
+ @deprecated_to_resource_api
+ def create_project(self, project_id, project):
+ return self.resource_api.create_project(project_id, project)
+
+ @deprecated_to_resource_api
+ def get_project_by_name(self, tenant_name, domain_id):
+ return self.resource_api.get_project_by_name(tenant_name, domain_id)
+
+ @deprecated_to_resource_api
+ def get_project(self, project_id):
+ return self.resource_api.get_project(project_id)
+
+ @deprecated_to_resource_api
+ def update_project(self, project_id, project):
+ return self.resource_api.update_project(project_id, project)
+
+ @deprecated_to_resource_api
+ def delete_project(self, project_id):
+ return self.resource_api.delete_project(project_id)
+
+ @deprecated_to_resource_api
+ def list_projects(self, hints=None):
+ return self.resource_api.list_projects(hints=hints)
+
+ @deprecated_to_resource_api
+ def list_projects_in_domain(self, domain_id):
+ return self.resource_api.list_projects_in_domain(domain_id)
+
+ @deprecated_to_resource_api
+ def create_domain(self, domain_id, domain):
+ return self.resource_api.create_domain(domain_id, domain)
+
+ @deprecated_to_resource_api
+ def get_domain_by_name(self, domain_name):
+ return self.resource_api.get_domain_by_name(domain_name)
+
+ @deprecated_to_resource_api
+ def get_domain(self, domain_id):
+ return self.resource_api.get_domain(domain_id)
+
+ @deprecated_to_resource_api
+ def update_domain(self, domain_id, domain):
+ return self.resource_api.update_domain(domain_id, domain)
+
+ @deprecated_to_resource_api
+ def delete_domain(self, domain_id):
+ return self.resource_api.delete_domain(domain_id)
+
+ @deprecated_to_resource_api
+ def list_domains(self, hints=None):
+ return self.resource_api.list_domains(hints=hints)
+
+ @deprecated_to_resource_api
+ def assert_domain_enabled(self, domain_id, domain=None):
+ return self.resource_api.assert_domain_enabled(domain_id, domain)
+
+ @deprecated_to_resource_api
+ def assert_project_enabled(self, project_id, project=None):
+ return self.resource_api.assert_project_enabled(project_id, project)
+
+ @deprecated_to_resource_api
+ def is_leaf_project(self, project_id):
+ return self.resource_api.is_leaf_project(project_id)
+
+ @deprecated_to_resource_api
+ def list_project_parents(self, project_id, user_id=None):
+ return self.resource_api.list_project_parents(project_id, user_id)
+
+ @deprecated_to_resource_api
+ def list_projects_in_subtree(self, project_id, user_id=None):
+ return self.resource_api.list_projects_in_subtree(project_id, user_id)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Driver(object):
+
+ def _role_to_dict(self, role_id, inherited):
+ role_dict = {'id': role_id}
+ if inherited:
+ role_dict['inherited_to'] = 'projects'
+ return role_dict
+
+ def _roles_from_role_dicts(self, dict_list, inherited):
+ role_list = []
+ for d in dict_list:
+ if ((not d.get('inherited_to') and not inherited) or
+ (d.get('inherited_to') == 'projects' and inherited)):
+ role_list.append(d['id'])
+ return role_list
+
+ def _add_role_to_role_dicts(self, role_id, inherited, dict_list,
+ allow_existing=True):
+ # There is a difference in error semantics when trying to
+ # assign a role that already exists between the coded v2 and v3
+ # API calls. v2 will error if the assignment already exists,
+ # while v3 is silent. Setting the 'allow_existing' parameter
+ # appropriately lets this call be used for both.
+ role_set = set([frozenset(r.items()) for r in dict_list])
+ key = frozenset(self._role_to_dict(role_id, inherited).items())
+ if not allow_existing and key in role_set:
+ raise KeyError
+ role_set.add(key)
+ return [dict(r) for r in role_set]
+
+ def _remove_role_from_role_dicts(self, role_id, inherited, dict_list):
+ role_set = set([frozenset(r.items()) for r in dict_list])
+ role_set.remove(frozenset(self._role_to_dict(role_id,
+ inherited).items()))
+ return [dict(r) for r in role_set]
+
+ def _get_list_limit(self):
+ return CONF.assignment.list_limit or CONF.list_limit
+
+ @abc.abstractmethod
+ def list_user_ids_for_project(self, tenant_id):
+ """Lists all user IDs with a role assignment in the specified project.
+
+ :returns: a list of user_ids or an empty set.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
+ """Add a role to a user within given tenant.
+
+ :raises: keystone.exception.Conflict
+
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def remove_role_from_user_and_project(self, user_id, tenant_id, role_id):
+ """Remove a role from a user within given tenant.
+
+ :raises: keystone.exception.RoleNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ # assignment/grant crud
+
+ @abc.abstractmethod
+ def create_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ """Creates a new assignment/grant.
+
+ If the assignment is to a domain, then optionally it may be
+ specified as inherited to owned projects (this requires
+ the OS-INHERIT extension to be enabled).
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_grant_role_ids(self, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ """Lists role ids for assignments/grants."""
+
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def check_grant_role_id(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ """Checks an assignment/grant role id.
+
+ :raises: keystone.exception.RoleAssignmentNotFound
+ :returns: None or raises an exception if grant not found
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_grant(self, role_id, user_id=None, group_id=None,
+ domain_id=None, project_id=None,
+ inherited_to_projects=False):
+ """Deletes assignments/grants.
+
+ :raises: keystone.exception.RoleAssignmentNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_role_assignments(self):
+
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_project_ids_for_user(self, user_id, group_ids, hints,
+ inherited=False):
+ """List all project ids associated with a given user.
+
+ :param user_id: the user in question
+ :param group_ids: the groups this user is a member of. This list is
+ built in the Manager, so that the driver itself
+ does not have to call across to identity.
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+ :param inherited: whether assignments marked as inherited should
+ be included.
+
+ :returns: a list of project ids or an empty list.
+
+ This method should not try and expand any inherited assignments,
+ just report the projects that have the role for this user. The manager
+ method is responsible for expanding out inherited assignments.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_project_ids_for_groups(self, group_ids, hints,
+ inherited=False):
+ """List project ids accessible to specified groups.
+
+ :param group_ids: List of group ids.
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+ :param inherited: whether assignments marked as inherited should
+ be included.
+ :returns: List of project ids accessible to specified groups.
+
+ This method should not try and expand any inherited assignments,
+ just report the projects that have the role for this group. The manager
+ method is responsible for expanding out inherited assignments.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_domain_ids_for_user(self, user_id, group_ids, hints,
+ inherited=False):
+ """List all domain ids associated with a given user.
+
+ :param user_id: the user in question
+ :param group_ids: the groups this user is a member of. This list is
+ built in the Manager, so that the driver itself
+ does not have to call across to identity.
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+ :param inherited: whether to return domain_ids that have inherited
+ assignments or not.
+
+ :returns: a list of domain ids or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_domain_ids_for_groups(self, group_ids, inherited=False):
+ """List domain ids accessible to specified groups.
+
+ :param group_ids: List of group ids.
+ :param inherited: whether to return domain_ids that have inherited
+ assignments or not.
+ :returns: List of domain ids accessible to specified groups.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_role_ids_for_groups_on_project(
+ self, group_ids, project_id, project_domain_id, project_parents):
+ """List the group role ids for a specific project.
+
+ Supports the ``OS-INHERIT`` role inheritance from the project's domain
+ if supported by the assignment driver.
+
+ :param group_ids: list of group ids
+ :type group_ids: list
+ :param project_id: project identifier
+ :type project_id: str
+ :param project_domain_id: project's domain identifier
+ :type project_domain_id: str
+ :param project_parents: list of parent ids of this project
+ :type project_parents: list
+ :returns: list of role ids for the project
+ :rtype: list
+ """
+ raise exception.NotImplemented()
+
+ @abc.abstractmethod
+ def list_role_ids_for_groups_on_domain(self, group_ids, domain_id):
+ """List the group role ids for a specific domain.
+
+ :param group_ids: list of group ids
+ :type group_ids: list
+ :param domain_id: domain identifier
+ :type domain_id: str
+ :returns: list of role ids for the project
+ :rtype: list
+ """
+ raise exception.NotImplemented()
+
+ @abc.abstractmethod
+ def delete_project_assignments(self, project_id):
+ """Deletes all assignments for a project.
+
+ :raises: keystone.exception.ProjectNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_role_assignments(self, role_id):
+ """Deletes all assignments for a role."""
+
+ raise exception.NotImplemented() # pragma: no cover
+
+ # TODO(henry-nash): Rename the following two methods to match the more
+ # meaningfully named ones above.
+
+# TODO(ayoung): determine what else these two functions raise
+ @abc.abstractmethod
+ def delete_user(self, user_id):
+ """Deletes all assignments for a user.
+
+ :raises: keystone.exception.RoleNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_group(self, group_id):
+ """Deletes all assignments for a group.
+
+ :raises: keystone.exception.RoleNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+
+@dependency.provider('role_api')
+@dependency.requires('assignment_api')
+class RoleManager(manager.Manager):
+ """Default pivot point for the Role backend."""
+
+ _ROLE = 'role'
+
+ def __init__(self):
+ # If there is a specific driver specified for role, then use it.
+ # Otherwise retrieve the driver type from the assignment driver.
+ role_driver = CONF.role.driver
+
+ if role_driver is None:
+ assignment_driver = (
+ dependency.get_provider('assignment_api').driver)
+ role_driver = assignment_driver.default_role_driver()
+
+ super(RoleManager, self).__init__(role_driver)
+
+ @MEMOIZE
+ def get_role(self, role_id):
+ return self.driver.get_role(role_id)
+
+ def create_role(self, role_id, role, initiator=None):
+ ret = self.driver.create_role(role_id, role)
+ notifications.Audit.created(self._ROLE, role_id, initiator)
+ if MEMOIZE.should_cache(ret):
+ self.get_role.set(ret, self, role_id)
+ return ret
+
+ @manager.response_truncated
+ def list_roles(self, hints=None):
+ return self.driver.list_roles(hints or driver_hints.Hints())
+
+ def update_role(self, role_id, role, initiator=None):
+ ret = self.driver.update_role(role_id, role)
+ notifications.Audit.updated(self._ROLE, role_id, initiator)
+ self.get_role.invalidate(self, role_id)
+ return ret
+
+ def delete_role(self, role_id, initiator=None):
+ try:
+ self.assignment_api.delete_tokens_for_role_assignments(role_id)
+ except exception.NotImplemented:
+ # FIXME(morganfainberg): Not all backends (ldap) implement
+ # `list_role_assignments_for_role` which would have previously
+ # caused a NotImplmented error to be raised when called through
+ # the controller. Now error or proper action will always come from
+ # the `delete_role` method logic. Work needs to be done to make
+ # the behavior between drivers consistent (capable of revoking
+ # tokens for the same circumstances). This is related to the bug
+ # https://bugs.launchpad.net/keystone/+bug/1221805
+ pass
+ self.assignment_api.delete_role_assignments(role_id)
+ self.driver.delete_role(role_id)
+ notifications.Audit.deleted(self._ROLE, role_id, initiator)
+ self.get_role.invalidate(self, role_id)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class RoleDriver(object):
+
+ def _get_list_limit(self):
+ return CONF.role.list_limit or CONF.list_limit
+
+ @abc.abstractmethod
+ def create_role(self, role_id, role):
+ """Creates a new role.
+
+ :raises: keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_roles(self, hints):
+ """List roles in the system.
+
+ :param hints: filter hints which the driver should
+ implement if at all possible.
+
+ :returns: a list of role_refs or an empty list.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def list_roles_from_ids(self, role_ids):
+ """List roles for the provided list of ids.
+
+ :param role_ids: list of ids
+
+ :returns: a list of role_refs.
+
+ This method is used internally by the assignment manager to bulk read
+ a set of roles given their ids.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_role(self, role_id):
+ """Get a role by ID.
+
+ :returns: role_ref
+ :raises: keystone.exception.RoleNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def update_role(self, role_id, role):
+ """Updates an existing role.
+
+ :raises: keystone.exception.RoleNotFound,
+ keystone.exception.Conflict
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def delete_role(self, role_id):
+ """Deletes an existing role.
+
+ :raises: keystone.exception.RoleNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
diff --git a/keystone-moon/keystone/assignment/role_backends/__init__.py b/keystone-moon/keystone/assignment/role_backends/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/keystone-moon/keystone/assignment/role_backends/__init__.py
diff --git a/keystone-moon/keystone/assignment/role_backends/ldap.py b/keystone-moon/keystone/assignment/role_backends/ldap.py
new file mode 100644
index 00000000..d5a06a4c
--- /dev/null
+++ b/keystone-moon/keystone/assignment/role_backends/ldap.py
@@ -0,0 +1,125 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from __future__ import absolute_import
+
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone import assignment
+from keystone.common import ldap as common_ldap
+from keystone.common import models
+from keystone import exception
+from keystone.i18n import _
+from keystone.identity.backends import ldap as ldap_identity
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+class Role(assignment.RoleDriver):
+
+ def __init__(self):
+ super(Role, self).__init__()
+ self.LDAP_URL = CONF.ldap.url
+ self.LDAP_USER = CONF.ldap.user
+ self.LDAP_PASSWORD = CONF.ldap.password
+ self.suffix = CONF.ldap.suffix
+
+ # This is the only deep dependency from resource back
+ # to identity. The assumption is that if you are using
+ # LDAP for resource, you are using it for identity as well.
+ self.user = ldap_identity.UserApi(CONF)
+ self.role = RoleApi(CONF, self.user)
+
+ def get_role(self, role_id):
+ return self.role.get(role_id)
+
+ def list_roles(self, hints):
+ return self.role.get_all()
+
+ def list_roles_from_ids(self, ids):
+ return [self.get_role(id) for id in ids]
+
+ def create_role(self, role_id, role):
+ self.role.check_allow_create()
+ try:
+ self.get_role(role_id)
+ except exception.NotFound:
+ pass
+ else:
+ msg = _('Duplicate ID, %s.') % role_id
+ raise exception.Conflict(type='role', details=msg)
+
+ try:
+ self.role.get_by_name(role['name'])
+ except exception.NotFound:
+ pass
+ else:
+ msg = _('Duplicate name, %s.') % role['name']
+ raise exception.Conflict(type='role', details=msg)
+
+ return self.role.create(role)
+
+ def delete_role(self, role_id):
+ self.role.check_allow_delete()
+ return self.role.delete(role_id)
+
+ def update_role(self, role_id, role):
+ self.role.check_allow_update()
+ self.get_role(role_id)
+ return self.role.update(role_id, role)
+
+
+# NOTE(heny-nash): A mixin class to enable the sharing of the LDAP structure
+# between here and the assignment LDAP.
+class RoleLdapStructureMixin(object):
+ DEFAULT_OU = 'ou=Roles'
+ DEFAULT_STRUCTURAL_CLASSES = []
+ DEFAULT_OBJECTCLASS = 'organizationalRole'
+ DEFAULT_MEMBER_ATTRIBUTE = 'roleOccupant'
+ NotFound = exception.RoleNotFound
+ options_name = 'role'
+ attribute_options_names = {'name': 'name'}
+ immutable_attrs = ['id']
+ model = models.Role
+
+
+# TODO(termie): turn this into a data object and move logic to driver
+class RoleApi(RoleLdapStructureMixin, common_ldap.BaseLdap):
+
+ def __init__(self, conf, user_api):
+ super(RoleApi, self).__init__(conf)
+ self._user_api = user_api
+
+ def get(self, role_id, role_filter=None):
+ model = super(RoleApi, self).get(role_id, role_filter)
+ return model
+
+ def create(self, values):
+ return super(RoleApi, self).create(values)
+
+ def update(self, role_id, role):
+ new_name = role.get('name')
+ if new_name is not None:
+ try:
+ old_role = self.get_by_name(new_name)
+ if old_role['id'] != role_id:
+ raise exception.Conflict(
+ _('Cannot duplicate name %s') % old_role)
+ except exception.NotFound:
+ pass
+ return super(RoleApi, self).update(role_id, role)
+
+ def delete(self, role_id):
+ super(RoleApi, self).delete(role_id)
diff --git a/keystone-moon/keystone/assignment/role_backends/sql.py b/keystone-moon/keystone/assignment/role_backends/sql.py
new file mode 100644
index 00000000..f19d1827
--- /dev/null
+++ b/keystone-moon/keystone/assignment/role_backends/sql.py
@@ -0,0 +1,80 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystone import assignment
+from keystone.common import sql
+from keystone import exception
+
+
+class Role(assignment.RoleDriver):
+
+ @sql.handle_conflicts(conflict_type='role')
+ def create_role(self, role_id, role):
+ with sql.transaction() as session:
+ ref = RoleTable.from_dict(role)
+ session.add(ref)
+ return ref.to_dict()
+
+ @sql.truncated
+ def list_roles(self, hints):
+ with sql.transaction() as session:
+ query = session.query(RoleTable)
+ refs = sql.filter_limit_query(RoleTable, query, hints)
+ return [ref.to_dict() for ref in refs]
+
+ def list_roles_from_ids(self, ids):
+ if not ids:
+ return []
+ else:
+ with sql.transaction() as session:
+ query = session.query(RoleTable)
+ query = query.filter(RoleTable.id.in_(ids))
+ role_refs = query.all()
+ return [role_ref.to_dict() for role_ref in role_refs]
+
+ def _get_role(self, session, role_id):
+ ref = session.query(RoleTable).get(role_id)
+ if ref is None:
+ raise exception.RoleNotFound(role_id=role_id)
+ return ref
+
+ def get_role(self, role_id):
+ with sql.transaction() as session:
+ return self._get_role(session, role_id).to_dict()
+
+ @sql.handle_conflicts(conflict_type='role')
+ def update_role(self, role_id, role):
+ with sql.transaction() as session:
+ ref = self._get_role(session, role_id)
+ old_dict = ref.to_dict()
+ for k in role:
+ old_dict[k] = role[k]
+ new_role = RoleTable.from_dict(old_dict)
+ for attr in RoleTable.attributes:
+ if attr != 'id':
+ setattr(ref, attr, getattr(new_role, attr))
+ ref.extra = new_role.extra
+ return ref.to_dict()
+
+ def delete_role(self, role_id):
+ with sql.transaction() as session:
+ ref = self._get_role(session, role_id)
+ session.delete(ref)
+
+
+class RoleTable(sql.ModelBase, sql.DictBase):
+ __tablename__ = 'role'
+ attributes = ['id', 'name']
+ id = sql.Column(sql.String(64), primary_key=True)
+ name = sql.Column(sql.String(255), unique=True, nullable=False)
+ extra = sql.Column(sql.JsonBlob())
+ __table_args__ = (sql.UniqueConstraint('name'), {})
diff --git a/keystone-moon/keystone/assignment/routers.py b/keystone-moon/keystone/assignment/routers.py
new file mode 100644
index 00000000..49549a0b
--- /dev/null
+++ b/keystone-moon/keystone/assignment/routers.py
@@ -0,0 +1,246 @@
+# Copyright 2013 Metacloud, Inc.
+# Copyright 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""WSGI Routers for the Assignment service."""
+
+import functools
+
+from oslo_config import cfg
+
+from keystone.assignment import controllers
+from keystone.common import json_home
+from keystone.common import router
+from keystone.common import wsgi
+
+
+CONF = cfg.CONF
+
+build_os_inherit_relation = functools.partial(
+ json_home.build_v3_extension_resource_relation,
+ extension_name='OS-INHERIT', extension_version='1.0')
+
+
+class Public(wsgi.ComposableRouter):
+ def add_routes(self, mapper):
+ tenant_controller = controllers.TenantAssignment()
+ mapper.connect('/tenants',
+ controller=tenant_controller,
+ action='get_projects_for_token',
+ conditions=dict(method=['GET']))
+
+
+class Admin(wsgi.ComposableRouter):
+ def add_routes(self, mapper):
+ # Role Operations
+ roles_controller = controllers.RoleAssignmentV2()
+ mapper.connect('/tenants/{tenant_id}/users/{user_id}/roles',
+ controller=roles_controller,
+ action='get_user_roles',
+ conditions=dict(method=['GET']))
+ mapper.connect('/users/{user_id}/roles',
+ controller=roles_controller,
+ action='get_user_roles',
+ conditions=dict(method=['GET']))
+
+
+class Routers(wsgi.RoutersBase):
+
+ def append_v3_routers(self, mapper, routers):
+
+ project_controller = controllers.ProjectAssignmentV3()
+ self._add_resource(
+ mapper, project_controller,
+ path='/users/{user_id}/projects',
+ get_action='list_user_projects',
+ rel=json_home.build_v3_resource_relation('user_projects'),
+ path_vars={
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+
+ routers.append(
+ router.Router(controllers.RoleV3(), 'roles', 'role',
+ resource_descriptions=self.v3_resources))
+
+ grant_controller = controllers.GrantAssignmentV3()
+ self._add_resource(
+ mapper, grant_controller,
+ path='/projects/{project_id}/users/{user_id}/roles/{role_id}',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=json_home.build_v3_resource_relation('project_user_role'),
+ path_vars={
+ 'project_id': json_home.Parameters.PROJECT_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/projects/{project_id}/groups/{group_id}/roles/{role_id}',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=json_home.build_v3_resource_relation('project_group_role'),
+ path_vars={
+ 'group_id': json_home.Parameters.GROUP_ID,
+ 'project_id': json_home.Parameters.PROJECT_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/projects/{project_id}/users/{user_id}/roles',
+ get_action='list_grants',
+ rel=json_home.build_v3_resource_relation('project_user_roles'),
+ path_vars={
+ 'project_id': json_home.Parameters.PROJECT_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/projects/{project_id}/groups/{group_id}/roles',
+ get_action='list_grants',
+ rel=json_home.build_v3_resource_relation('project_group_roles'),
+ path_vars={
+ 'group_id': json_home.Parameters.GROUP_ID,
+ 'project_id': json_home.Parameters.PROJECT_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/domains/{domain_id}/users/{user_id}/roles/{role_id}',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=json_home.build_v3_resource_relation('domain_user_role'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/domains/{domain_id}/groups/{group_id}/roles/{role_id}',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=json_home.build_v3_resource_relation('domain_group_role'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'group_id': json_home.Parameters.GROUP_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/domains/{domain_id}/users/{user_id}/roles',
+ get_action='list_grants',
+ rel=json_home.build_v3_resource_relation('domain_user_roles'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/domains/{domain_id}/groups/{group_id}/roles',
+ get_action='list_grants',
+ rel=json_home.build_v3_resource_relation('domain_group_roles'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'group_id': json_home.Parameters.GROUP_ID,
+ })
+
+ routers.append(
+ router.Router(controllers.RoleAssignmentV3(),
+ 'role_assignments', 'role_assignment',
+ resource_descriptions=self.v3_resources,
+ is_entity_implemented=False))
+
+ if CONF.os_inherit.enabled:
+ self._add_resource(
+ mapper, grant_controller,
+ path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/'
+ '{role_id}/inherited_to_projects',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=build_os_inherit_relation(
+ resource_name='domain_user_role_inherited_to_projects'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/'
+ '{role_id}/inherited_to_projects',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=build_os_inherit_relation(
+ resource_name='domain_group_role_inherited_to_projects'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'group_id': json_home.Parameters.GROUP_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/'
+ 'inherited_to_projects',
+ get_action='list_grants',
+ rel=build_os_inherit_relation(
+ resource_name='domain_group_roles_inherited_to_projects'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'group_id': json_home.Parameters.GROUP_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/'
+ 'inherited_to_projects',
+ get_action='list_grants',
+ rel=build_os_inherit_relation(
+ resource_name='domain_user_roles_inherited_to_projects'),
+ path_vars={
+ 'domain_id': json_home.Parameters.DOMAIN_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/OS-INHERIT/projects/{project_id}/users/{user_id}/roles/'
+ '{role_id}/inherited_to_projects',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=build_os_inherit_relation(
+ resource_name='project_user_role_inherited_to_projects'),
+ path_vars={
+ 'project_id': json_home.Parameters.PROJECT_ID,
+ 'user_id': json_home.Parameters.USER_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ })
+ self._add_resource(
+ mapper, grant_controller,
+ path='/OS-INHERIT/projects/{project_id}/groups/{group_id}/'
+ 'roles/{role_id}/inherited_to_projects',
+ get_head_action='check_grant',
+ put_action='create_grant',
+ delete_action='revoke_grant',
+ rel=build_os_inherit_relation(
+ resource_name='project_group_role_inherited_to_projects'),
+ path_vars={
+ 'project_id': json_home.Parameters.PROJECT_ID,
+ 'group_id': json_home.Parameters.GROUP_ID,
+ 'role_id': json_home.Parameters.ROLE_ID,
+ })
diff --git a/keystone-moon/keystone/assignment/schema.py b/keystone-moon/keystone/assignment/schema.py
new file mode 100644
index 00000000..f4d1b08a
--- /dev/null
+++ b/keystone-moon/keystone/assignment/schema.py
@@ -0,0 +1,32 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from keystone.common.validation import parameter_types
+
+
+_role_properties = {
+ 'name': parameter_types.name
+}
+
+role_create = {
+ 'type': 'object',
+ 'properties': _role_properties,
+ 'required': ['name'],
+ 'additionalProperties': True
+}
+
+role_update = {
+ 'type': 'object',
+ 'properties': _role_properties,
+ 'minProperties': 1,
+ 'additionalProperties': True
+}