diff options
Diffstat (limited to 'keystone/assignment/backends/sql.py')
-rw-r--r-- | keystone/assignment/backends/sql.py | 751 |
1 files changed, 751 insertions, 0 deletions
diff --git a/keystone/assignment/backends/sql.py b/keystone/assignment/backends/sql.py new file mode 100644 index 00000000..cd2a0a5c --- /dev/null +++ b/keystone/assignment/backends/sql.py @@ -0,0 +1,751 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012-13 OpenStack LLC +# +# 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 import clean +from keystone.common import dependency +from keystone.common import sql +from keystone.common.sql import migration +from keystone import exception + + +@dependency.requires('identity_api') +class Assignment(sql.Base, assignment.Driver): + + # Internal interface to manage the database + def db_sync(self, version=None): + migration.db_sync(version=version) + + def _get_project(self, session, project_id): + project_ref = session.query(Project).get(project_id) + if project_ref is None: + raise exception.ProjectNotFound(project_id=project_id) + return project_ref + + def get_project(self, tenant_id): + session = self.get_session() + return self._get_project(session, tenant_id).to_dict() + + def get_project_by_name(self, tenant_name, domain_id): + session = self.get_session() + query = session.query(Project) + query = query.filter_by(name=tenant_name) + query = query.filter_by(domain_id=domain_id) + try: + project_ref = query.one() + except sql.NotFound: + raise exception.ProjectNotFound(project_id=tenant_name) + return project_ref.to_dict() + + def get_project_user_ids(self, tenant_id): + session = self.get_session() + self.get_project(tenant_id) + query = session.query(UserProjectGrant) + query = query.filter(UserProjectGrant.project_id == + tenant_id) + project_refs = query.all() + return [project_ref.user_id for project_ref in project_refs] + + def get_project_users(self, tenant_id): + self.get_session() + self.get_project(tenant_id) + user_refs = [] + #TODO(ayoung): Move to controller or manager + for user_id in self.get_project_user_ids(tenant_id): + user_ref = self.identity_api.get_user(user_id) + user_refs.append(user_ref) + return user_refs + + def _get_metadata(self, user_id=None, tenant_id=None, + domain_id=None, group_id=None): + session = self.get_session() + + if user_id: + if tenant_id: + q = session.query(UserProjectGrant) + q = q.filter_by(project_id=tenant_id) + elif domain_id: + q = session.query(UserDomainGrant) + q = q.filter_by(domain_id=domain_id) + q = q.filter_by(user_id=user_id) + elif group_id: + if tenant_id: + q = session.query(GroupProjectGrant) + q = q.filter_by(project_id=tenant_id) + elif domain_id: + q = session.query(GroupDomainGrant) + q = q.filter_by(domain_id=domain_id) + q = q.filter_by(group_id=group_id) + try: + return q.one().data + except sql.NotFound: + raise exception.MetadataNotFound() + + def create_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + if user_id: + self.identity_api.get_user(user_id) + if group_id: + self.identity_api.get_group(group_id) + + session = self.get_session() + self._get_role(session, role_id) + + if domain_id: + self._get_domain(session, domain_id) + if project_id: + self._get_project(session, project_id) + + if project_id and inherited_to_projects: + msg = _('Inherited roles can only be assigned to domains') + raise exception.Conflict(type='role grant', details=msg) + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + is_new = False + except exception.MetadataNotFound: + metadata_ref = {} + is_new = True + + metadata_ref['roles'] = self._add_role_to_role_dicts( + role_id, inherited_to_projects, metadata_ref.get('roles', [])) + + if is_new: + self._create_metadata(user_id, project_id, metadata_ref, + domain_id, group_id) + else: + self._update_metadata(user_id, project_id, metadata_ref, + domain_id, group_id) + + def list_grants(self, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + if user_id: + self.identity_api.get_user(user_id) + if group_id: + self.identity_api.get_group(group_id) + session = self.get_session() + if domain_id: + self._get_domain(session, domain_id) + if project_id: + self._get_project(session, project_id) + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + except exception.MetadataNotFound: + metadata_ref = {} + + return [self.get_role(x) for x in + self._roles_from_role_dicts(metadata_ref.get('roles', []), + 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): + if user_id: + self.identity_api.get_user(user_id) + if group_id: + self.identity_api.get_group(group_id) + + session = self.get_session() + role_ref = self._get_role(session, role_id) + + if domain_id: + self._get_domain(session, domain_id) + if project_id: + self._get_project(session, project_id) + + 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: + raise exception.RoleNotFound(role_id=role_id) + return role_ref.to_dict() + + def delete_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + if user_id: + self.identity_api.get_user(user_id) + if group_id: + self.identity_api.get_group(group_id) + + session = self.get_session() + self._get_role(session, role_id) + + if domain_id: + self._get_domain(session, domain_id) + if project_id: + self._get_project(session, project_id) + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + is_new = False + except exception.MetadataNotFound: + metadata_ref = {} + is_new = True + + try: + metadata_ref['roles'] = self._remove_role_from_role_dicts( + role_id, inherited_to_projects, metadata_ref.get('roles', [])) + except KeyError: + raise exception.RoleNotFound(role_id=role_id) + + if is_new: + # TODO(henry-nash) It seems odd that you would create a new + # entry in response to trying to delete a role that was not + # assigned. Although benign, this should probably be removed. + self._create_metadata(user_id, project_id, metadata_ref, + domain_id, group_id) + else: + self._update_metadata(user_id, project_id, metadata_ref, + domain_id, group_id) + + def list_projects(self, domain_id=None): + session = self.get_session() + if domain_id: + self._get_domain(session, domain_id) + + query = session.query(Project) + if domain_id: + query = query.filter_by(domain_id=domain_id) + project_refs = query.all() + return [project_ref.to_dict() for project_ref in project_refs] + + def get_projects_for_user(self, user_id): + + # FIXME(henry-nash) The following should take into account + # both group and inherited roles. In fact, I don't see why this + # call can't be handled at the controller level like we do + # with 'get_roles_for_user_and_project()'. Further, this + # call seems essentially the same as 'list_user_projects()' + # later in this driver. Both should be removed. + + self.identity_api.get_user(user_id) + session = self.get_session() + query = session.query(UserProjectGrant) + query = query.filter_by(user_id=user_id) + membership_refs = query.all() + return [x.project_id for x in membership_refs] + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + self.identity_api.get_user(user_id) + session = self.get_session() + self._get_project(session, tenant_id) + self._get_role(session, role_id) + try: + metadata_ref = self._get_metadata(user_id, tenant_id) + is_new = False + except exception.MetadataNotFound: + metadata_ref = {} + is_new = True + + try: + metadata_ref['roles'] = self._add_role_to_role_dicts( + role_id, False, metadata_ref.get('roles', []), + allow_existing=False) + except KeyError: + msg = ('User %s already has role %s in tenant %s' + % (user_id, role_id, tenant_id)) + raise exception.Conflict(type='role grant', details=msg) + + if is_new: + self._create_metadata(user_id, tenant_id, metadata_ref) + else: + self._update_metadata(user_id, tenant_id, metadata_ref) + + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + try: + metadata_ref = self._get_metadata(user_id, tenant_id) + try: + metadata_ref['roles'] = self._remove_role_from_role_dicts( + role_id, False, metadata_ref.get('roles', [])) + except KeyError: + raise exception.RoleNotFound(message=_( + 'Cannot remove role that has not been granted, %s') % + role_id) + + if len(metadata_ref['roles']): + self._update_metadata(user_id, tenant_id, metadata_ref) + else: + session = self.get_session() + q = session.query(UserProjectGrant) + q = q.filter_by(user_id=user_id) + q = q.filter_by(project_id=tenant_id) + q.delete() + except exception.MetadataNotFound: + msg = 'Cannot remove role that has not been granted, %s' % role_id + raise exception.RoleNotFound(message=msg) + + def list_role_assignments(self): + + # TODO(henry-nash): The current implementation is really simulating + # us having a common role assignment table, rather than having the + # four different grant tables we have today. When we move to role + # assignment as a first class entity, we should create the single + # assignment table, simplifying the logic of this (and many other) + # functions. + + session = self.get_session() + assignment_list = [] + refs = session.query(UserDomainGrant).all() + for x in refs: + for r in self._roles_from_role_dicts( + x.data.get('roles', {}), False): + assignment_list.append({'user_id': x.user_id, + 'domain_id': x.domain_id, + 'role_id': r}) + for r in self._roles_from_role_dicts( + x.data.get('roles', {}), True): + assignment_list.append({'user_id': x.user_id, + 'domain_id': x.domain_id, + 'role_id': r, + 'inherited_to_projects': True}) + refs = session.query(UserProjectGrant).all() + for x in refs: + for r in self._roles_from_role_dicts( + x.data.get('roles', {}), False): + assignment_list.append({'user_id': x.user_id, + 'project_id': x.project_id, + 'role_id': r}) + refs = session.query(GroupDomainGrant).all() + for x in refs: + for r in self._roles_from_role_dicts( + x.data.get('roles', {}), False): + assignment_list.append({'group_id': x.group_id, + 'domain_id': x.domain_id, + 'role_id': r}) + for r in self._roles_from_role_dicts( + x.data.get('roles', {}), True): + assignment_list.append({'group_id': x.group_id, + 'domain_id': x.domain_id, + 'role_id': r, + 'inherited_to_projects': True}) + refs = session.query(GroupProjectGrant).all() + for x in refs: + for r in self._roles_from_role_dicts( + x.data.get('roles', {}), False): + assignment_list.append({'group_id': x.group_id, + 'project_id': x.project_id, + 'role_id': r}) + return assignment_list + + # CRUD + @sql.handle_conflicts(type='project') + def create_project(self, tenant_id, tenant): + tenant['name'] = clean.project_name(tenant['name']) + session = self.get_session() + with session.begin(): + tenant_ref = Project.from_dict(tenant) + session.add(tenant_ref) + session.flush() + return tenant_ref.to_dict() + + @sql.handle_conflicts(type='project') + def update_project(self, tenant_id, tenant): + session = self.get_session() + + if 'name' in tenant: + tenant['name'] = clean.project_name(tenant['name']) + + with session.begin(): + tenant_ref = self._get_project(session, tenant_id) + old_project_dict = tenant_ref.to_dict() + for k in tenant: + old_project_dict[k] = tenant[k] + new_project = Project.from_dict(old_project_dict) + for attr in Project.attributes: + if attr != 'id': + setattr(tenant_ref, attr, getattr(new_project, attr)) + tenant_ref.extra = new_project.extra + session.flush() + return tenant_ref.to_dict(include_extra_dict=True) + + @sql.handle_conflicts(type='project') + def delete_project(self, tenant_id): + session = self.get_session() + + with session.begin(): + tenant_ref = self._get_project(session, tenant_id) + + q = session.query(UserProjectGrant) + q = q.filter_by(project_id=tenant_id) + q.delete(False) + + q = session.query(UserProjectGrant) + q = q.filter_by(project_id=tenant_id) + q.delete(False) + + q = session.query(GroupProjectGrant) + q = q.filter_by(project_id=tenant_id) + q.delete(False) + + session.delete(tenant_ref) + session.flush() + + @sql.handle_conflicts(type='metadata') + def _create_metadata(self, user_id, tenant_id, metadata, + domain_id=None, group_id=None): + session = self.get_session() + with session.begin(): + if user_id: + if tenant_id: + session.add(UserProjectGrant + (user_id=user_id, + project_id=tenant_id, + data=metadata)) + elif domain_id: + session.add(UserDomainGrant + (user_id=user_id, + domain_id=domain_id, + data=metadata)) + elif group_id: + if tenant_id: + session.add(GroupProjectGrant + (group_id=group_id, + project_id=tenant_id, + data=metadata)) + elif domain_id: + session.add(GroupDomainGrant + (group_id=group_id, + domain_id=domain_id, + data=metadata)) + session.flush() + return metadata + + @sql.handle_conflicts(type='metadata') + def _update_metadata(self, user_id, tenant_id, metadata, + domain_id=None, group_id=None): + session = self.get_session() + with session.begin(): + if user_id: + if tenant_id: + q = session.query(UserProjectGrant) + q = q.filter_by(user_id=user_id) + q = q.filter_by(project_id=tenant_id) + elif domain_id: + q = session.query(UserDomainGrant) + q = q.filter_by(user_id=user_id) + q = q.filter_by(domain_id=domain_id) + elif group_id: + if tenant_id: + q = session.query(GroupProjectGrant) + q = q.filter_by(group_id=group_id) + q = q.filter_by(project_id=tenant_id) + elif domain_id: + q = session.query(GroupDomainGrant) + q = q.filter_by(group_id=group_id) + q = q.filter_by(domain_id=domain_id) + metadata_ref = q.first() + data = metadata_ref.data.copy() + data.update(metadata) + metadata_ref.data = data + session.flush() + return metadata_ref + + # domain crud + + @sql.handle_conflicts(type='domain') + def create_domain(self, domain_id, domain): + session = self.get_session() + with session.begin(): + ref = Domain.from_dict(domain) + session.add(ref) + session.flush() + return ref.to_dict() + + def list_domains(self): + session = self.get_session() + refs = session.query(Domain).all() + return [ref.to_dict() for ref in refs] + + def _get_domain(self, session, domain_id): + ref = session.query(Domain).get(domain_id) + if ref is None: + raise exception.DomainNotFound(domain_id=domain_id) + return ref + + def get_domain(self, domain_id): + session = self.get_session() + return self._get_domain(session, domain_id).to_dict() + + def get_domain_by_name(self, domain_name): + session = self.get_session() + try: + ref = (session.query(Domain). + filter_by(name=domain_name).one()) + except sql.NotFound: + raise exception.DomainNotFound(domain_id=domain_name) + return ref.to_dict() + + @sql.handle_conflicts(type='domain') + def update_domain(self, domain_id, domain): + session = self.get_session() + with session.begin(): + ref = self._get_domain(session, domain_id) + old_dict = ref.to_dict() + for k in domain: + old_dict[k] = domain[k] + new_domain = Domain.from_dict(old_dict) + for attr in Domain.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_domain, attr)) + ref.extra = new_domain.extra + session.flush() + return ref.to_dict() + + def delete_domain(self, domain_id): + session = self.get_session() + with session.begin(): + ref = self._get_domain(session, domain_id) + session.delete(ref) + session.flush() + + def list_user_projects(self, user_id): + + # FIXME(henry-nash) The following should take into account + # both group and inherited roles. In fact, I don't see why this + # call can't be handled at the controller level like we do + # with 'get_roles_for_user_and_project()'. Further, this + # call seems essentially the same as 'get_projects_for_user()' + # earlier in this driver. Both should be removed. + + session = self.get_session() + user = self.identity_api.get_user(user_id) + metadata_refs = session\ + .query(UserProjectGrant)\ + .filter_by(user_id=user_id) + project_ids = set([x.project_id for x in metadata_refs + if x.data.get('roles')]) + if user.get('project_id'): + project_ids.add(user['project_id']) + + # FIXME(dolph): this should be removed with proper migrations + if user.get('tenant_id'): + project_ids.add(user['tenant_id']) + + return [self.get_project(x) for x in project_ids] + + # role crud + + @sql.handle_conflicts(type='role') + def create_role(self, role_id, role): + session = self.get_session() + with session.begin(): + ref = Role.from_dict(role) + session.add(ref) + session.flush() + return ref.to_dict() + + def list_roles(self): + session = self.get_session() + refs = session.query(Role).all() + return [ref.to_dict() for ref in refs] + + def _get_role(self, session, role_id): + ref = session.query(Role).get(role_id) + if ref is None: + raise exception.RoleNotFound(role_id=role_id) + return ref + + def get_role(self, role_id): + session = self.get_session() + return self._get_role(session, role_id).to_dict() + + @sql.handle_conflicts(type='role') + def update_role(self, role_id, role): + session = self.get_session() + with session.begin(): + ref = self._get_role(session, role_id) + old_dict = ref.to_dict() + for k in role: + old_dict[k] = role[k] + new_role = Role.from_dict(old_dict) + for attr in Role.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_role, attr)) + ref.extra = new_role.extra + session.flush() + return ref.to_dict() + + def delete_role(self, role_id): + session = self.get_session() + + with session.begin(): + ref = self._get_role(session, role_id) + for metadata_ref in session.query(UserProjectGrant): + try: + self.delete_grant(role_id, user_id=metadata_ref.user_id, + project_id=metadata_ref.project_id) + except exception.RoleNotFound: + pass + for metadata_ref in session.query(UserDomainGrant): + try: + self.delete_grant(role_id, user_id=metadata_ref.user_id, + domain_id=metadata_ref.domain_id) + except exception.RoleNotFound: + pass + for metadata_ref in session.query(GroupProjectGrant): + try: + self.delete_grant(role_id, group_id=metadata_ref.group_id, + project_id=metadata_ref.project_id) + except exception.RoleNotFound: + pass + for metadata_ref in session.query(GroupDomainGrant): + try: + self.delete_grant(role_id, group_id=metadata_ref.group_id, + domain_id=metadata_ref.domain_id) + except exception.RoleNotFound: + pass + + session.delete(ref) + session.flush() + + def delete_user(self, user_id): + session = self.get_session() + + with session.begin(): + q = session.query(UserProjectGrant) + q = q.filter_by(user_id=user_id) + q.delete(False) + + q = session.query(UserDomainGrant) + q = q.filter_by(user_id=user_id) + q.delete(False) + + session.flush() + + def delete_group(self, group_id): + session = self.get_session() + + with session.begin(): + + q = session.query(GroupProjectGrant) + q = q.filter_by(group_id=group_id) + q.delete(False) + + q = session.query(GroupDomainGrant) + q = q.filter_by(group_id=group_id) + q.delete(False) + + session.flush() + + +class Domain(sql.ModelBase, sql.DictBase): + __tablename__ = 'domain' + attributes = ['id', 'name', 'enabled'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(64), nullable=False) + enabled = sql.Column(sql.Boolean, default=True, nullable=False) + extra = sql.Column(sql.JsonBlob()) + __table_args__ = (sql.UniqueConstraint('name'), {}) + + +class Project(sql.ModelBase, sql.DictBase): + __tablename__ = 'project' + attributes = ['id', 'name', 'domain_id', 'description', 'enabled'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(64), nullable=False) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + nullable=False) + description = sql.Column(sql.Text()) + enabled = sql.Column(sql.Boolean) + extra = sql.Column(sql.JsonBlob()) + # Unique constraint across two columns to create the separation + # rather than just only 'name' being unique + __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) + + +class Role(sql.ModelBase, sql.DictBase): + __tablename__ = 'role' + attributes = ['id', 'name'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(255), nullable=False) + extra = sql.Column(sql.JsonBlob()) + __table_args__ = (sql.UniqueConstraint('name'), {}) + + +class BaseGrant(sql.DictBase): + """Base Grant class. + + There are four grant tables in the current implementation, one for + each type of grant: + + - User for Project + - User for Domain + - Group for Project + - Group for Domain + + Each is a table with the two attributes above as a combined primary key, + with the data field holding all roles for that combination. The data + field is a list of dicts. For regular role assignments each dict in + the list of of the form: + + {'id': role_id} + + If the OS-INHERIT extension is enabled and the role on a domain is an + inherited role, the dict will be of the form: + + {'id': role_id, 'inherited_to': 'projects'} + + """ + def to_dict(self): + """Override parent to_dict() method with a simpler implementation. + + Grant tables don't have non-indexed 'extra' attributes, so the + parent implementation is not applicable. + """ + return dict(self.iteritems()) + + +class UserProjectGrant(sql.ModelBase, BaseGrant): + __tablename__ = 'user_project_metadata' + user_id = sql.Column(sql.String(64), primary_key=True) + project_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'), + primary_key=True) + data = sql.Column(sql.JsonBlob()) + + +class UserDomainGrant(sql.ModelBase, BaseGrant): + __tablename__ = 'user_domain_metadata' + user_id = sql.Column(sql.String(64), primary_key=True) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + primary_key=True) + data = sql.Column(sql.JsonBlob()) + + +class GroupProjectGrant(sql.ModelBase, BaseGrant): + __tablename__ = 'group_project_metadata' + group_id = sql.Column(sql.String(64), primary_key=True) + project_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'), + primary_key=True) + data = sql.Column(sql.JsonBlob()) + + +class GroupDomainGrant(sql.ModelBase, BaseGrant): + __tablename__ = 'group_domain_metadata' + group_id = sql.Column(sql.String(64), primary_key=True) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + primary_key=True) + data = sql.Column(sql.JsonBlob()) |