diff options
Diffstat (limited to 'keystone-moon/keystone/assignment')
-rw-r--r-- | keystone-moon/keystone/assignment/__init__.py | 17 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/backends/__init__.py | 0 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/backends/ldap.py | 531 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/backends/sql.py | 415 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/controllers.py | 816 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/core.py | 1019 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/role_backends/__init__.py | 0 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/role_backends/ldap.py | 125 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/role_backends/sql.py | 80 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/routers.py | 246 | ||||
-rw-r--r-- | keystone-moon/keystone/assignment/schema.py | 32 |
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 +} |