diff options
author | WuKong <rebirthmonkey@gmail.com> | 2015-06-30 18:47:29 +0200 |
---|---|---|
committer | WuKong <rebirthmonkey@gmail.com> | 2015-06-30 18:47:29 +0200 |
commit | b8c756ecdd7cced1db4300935484e8c83701c82e (patch) | |
tree | 87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/assignment/backends | |
parent | c304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff) |
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604
Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/assignment/backends')
-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 |
3 files changed, 946 insertions, 0 deletions
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)) |