# 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))