Skip to content

Commit

Permalink
Rationalize the Assignment Grant Tables
Browse files Browse the repository at this point in the history
Currently there are four different tables in the assignment sql
backend, one for each type of grant.  This leads to significantly
more complex code.

This patch rationalizes these four tables into a single Assignment
table, with the minimal of rework to calling functions of the
sql backend.  A subsequent patch will improve efficiency of the
calling code by taking advantage of a single Assignment table.

Blueprint: role-assignments-unified-sql

Change-Id: I7e604d718b29ad28b4d59458c7a528c01ff9804d
  • Loading branch information
henrynash authored and dolph committed Feb 19, 2014
1 parent 359ef53 commit ec995b3
Show file tree
Hide file tree
Showing 7 changed files with 937 additions and 441 deletions.
625 changes: 207 additions & 418 deletions keystone/assignment/backends/sql.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions keystone/common/sql/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@
Column = sql.Column
Index = sql.Index
String = sql.String
Integer = sql.Integer
Enum = sql.Enum
ForeignKey = sql.ForeignKey
DateTime = sql.DateTime
IntegrityError = sql.exc.IntegrityError
DBDuplicateEntry = db_exception.DBDuplicateEntry
OperationalError = sql.exc.OperationalError
NotFound = sql.orm.exc.NoResultFound
Boolean = sql.Boolean
Text = sql.Text
UniqueConstraint = sql.UniqueConstraint
PrimaryKeyConstraint = sql.PrimaryKeyConstraint
relationship = sql.orm.relationship
joinedload = sql.orm.joinedload
# Suppress flake8's unused import warning for flag_modified:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2014 IBM Corp.
#
# 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.

import sqlalchemy as sql

from keystone.assignment.backends import sql as assignment_sql

ASSIGNMENT_TABLE = 'assignment'


def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine

sql.Table('role', meta, autoload=True)
assignment_table = sql.Table(
ASSIGNMENT_TABLE,
meta,
sql.Column('type', sql.Enum(
assignment_sql.AssignmentType.USER_PROJECT,
assignment_sql.AssignmentType.GROUP_PROJECT,
assignment_sql.AssignmentType.USER_DOMAIN,
assignment_sql.AssignmentType.GROUP_DOMAIN,
name='type'),
nullable=False),
sql.Column('actor_id', sql.String(64), nullable=False),
sql.Column('target_id', sql.String(64), nullable=False),
sql.Column('role_id', sql.String(64), sql.ForeignKey('role.id'),
nullable=False),
sql.Column('inherited', sql.Boolean, default=False, nullable=False),
sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', 'role_id'))
assignment_table.create(migrate_engine, checkfirst=True)


def downgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine

assignment = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True)
assignment.drop(migrate_engine, checkfirst=True)
231 changes: 231 additions & 0 deletions keystone/common/sql/migrate_repo/versions/039_grant_to_assignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Copyright 2014 IBM Corp.
#
# 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.

import json

import sqlalchemy as sql

from keystone.assignment.backends import sql as assignment_sql

USER_PROJECT_TABLE = 'user_project_metadata'
GROUP_PROJECT_TABLE = 'group_project_metadata'
USER_DOMAIN_TABLE = 'user_domain_metadata'
GROUP_DOMAIN_TABLE = 'group_domain_metadata'

ASSIGNMENT_TABLE = 'assignment'

GRANT_TABLES = [USER_PROJECT_TABLE, USER_DOMAIN_TABLE,
GROUP_PROJECT_TABLE, GROUP_DOMAIN_TABLE]


def migrate_grant_table(meta, migrate_engine, session, table_name):

def extract_actor_and_target(table_name, composite_grant):
if table_name == USER_PROJECT_TABLE:
return {'type': assignment_sql.AssignmentType.USER_PROJECT,
'actor_id': composite_grant.user_id,
'target_id': composite_grant.project_id}
elif table_name == GROUP_PROJECT_TABLE:
return {'type': assignment_sql.AssignmentType.GROUP_PROJECT,
'actor_id': composite_grant.group_id,
'target_id': composite_grant.project_id}
elif table_name == USER_DOMAIN_TABLE:
return {'type': assignment_sql.AssignmentType.USER_DOMAIN,
'actor_id': composite_grant.user_id,
'target_id': composite_grant.domain_id}
else:
return {'type': assignment_sql.AssignmentType.GROUP_DOMAIN,
'actor_id': composite_grant.group_id,
'target_id': composite_grant.domain_id}

def grant_to_grant_dict_list(table_name, composite_grant):
"""Make each role in the list of this entry a separate assignment."""
json_metadata = json.loads(composite_grant.data)
role_dict_list = []
if 'roles' in json_metadata:
for x in json_metadata['roles']:
if x.get('id') is None:
# Looks like an invalid role, drop it
break
grant = extract_actor_and_target(table_name, composite_grant)
grant['role_id'] = x.get('id')
grant['inherited'] = False
if x.get('inherited_to') == 'projects':
grant['inherited'] = True
role_dict_list.append(grant)
return role_dict_list

upgrade_table = sql.Table(table_name, meta, autoload=True)
assignment_table = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True)

# For each grant in this table, expand it out to be an assignment entry for
# each role in the metadata
for grant in session.query(upgrade_table).all():
for grant_role in grant_to_grant_dict_list(table_name, grant):
new_entry = assignment_table.insert().values(
type=grant_role['type'],
actor_id=grant_role['actor_id'],
target_id=grant_role['target_id'],
role_id=grant_role['role_id'],
inherited=grant_role['inherited'])
migrate_engine.execute(new_entry)

# Delete all the rows
migrate_engine.execute(upgrade_table.delete())


def downgrade_assignment_table(meta, migrate_engine):

def add_to_dict_list(metadata, assignment_row):
"""Update a metadata dict list with the role.
For the assignment row supplied, we need to append the role_id into
the metadata list of dicts. If the row is inherited, then we mark
it so in the dict we append.
"""
new_entry = {'id': assignment_row.role_id}
if assignment_row.inherited and (
assignment_row.type ==
assignment_sql.AssignmentType.USER_DOMAIN or
assignment_row.type ==
assignment_sql.AssignmentType.GROUP_DOMAIN):
new_entry['inherited_to'] = 'projects'

if metadata is not None:
json_metadata = json.loads(metadata)
else:
json_metadata = {}

if json_metadata.get('roles') is None:
json_metadata['roles'] = []

json_metadata['roles'].append(new_entry)
return json.dumps(json_metadata)

def build_user_project_entry(meta, session, row):
update_table = sql.Table(USER_PROJECT_TABLE, meta, autoload=True)
q = session.query(update_table)
q = q.filter_by(user_id=row.actor_id)
q = q.filter_by(project_id=row.target_id)
ref = q.first()
if ref is not None:
values = {'data': add_to_dict_list(ref.data, row)}
update = update_table.update().where(
update_table.c.user_id == ref.user_id).where(
update_table.c.project_id == ref.project_id).values(values)
else:
values = {'user_id': row.actor_id,
'project_id': row.target_id,
'data': add_to_dict_list(None, row)}
update = update_table.insert().values(values)
return update

def build_group_project_entry(meta, session, row):
update_table = sql.Table(GROUP_PROJECT_TABLE, meta, autoload=True)
q = session.query(update_table)
q = q.filter_by(group_id=row.actor_id)
q = q.filter_by(project_id=row.target_id)
ref = q.first()
if ref is not None:
values = {'data': add_to_dict_list(ref.data, row)}
update = update_table.update().where(
update_table.c.group_id == ref.group_id).where(
update_table.c.project_id == ref.project_id).values(values)
else:
values = {'group_id': row.actor_id,
'project_id': row.target_id,
'data': add_to_dict_list(None, row)}
update = update_table.insert().values(values)
return update

def build_user_domain_entry(meta, session, row):
update_table = sql.Table(USER_DOMAIN_TABLE, meta, autoload=True)
q = session.query(update_table)
q = q.filter_by(user_id=row.actor_id)
q = q.filter_by(domain_id=row.target_id)
ref = q.first()
if ref is not None:
values = {'data': add_to_dict_list(ref.data, row)}
update = update_table.update().where(
update_table.c.user_id == ref.user_id).where(
update_table.c.domain_id == ref.domain_id).values(values)
else:
values = {'user_id': row.actor_id,
'domain_id': row.target_id,
'data': add_to_dict_list(None, row)}
update = update_table.insert().values(values)
return update

def build_group_domain_entry(meta, session, row):
update_table = sql.Table(GROUP_DOMAIN_TABLE, meta, autoload=True)
q = session.query(update_table)
q = q.filter_by(group_id=row.actor_id)
q = q.filter_by(domain_id=row.target_id)
ref = q.first()
if ref is not None:
values = {'data': add_to_dict_list(ref.data, row)}
update = update_table.update().where(
update_table.c.group_id == ref.group_id).where(
update_table.c.domain_id == ref.domain_id).values(values)
else:
values = {'group_id': row.actor_id,
'domain_id': row.target_id,
'data': add_to_dict_list(None, row)}
update = update_table.insert().values(values)
return update

def build_update(meta, session, row):
"""Build an update or an insert to the correct metadata table."""
if row.type == assignment_sql.AssignmentType.USER_PROJECT:
return build_user_project_entry(meta, session, row)
elif row.type == assignment_sql.AssignmentType.GROUP_PROJECT:
return build_group_project_entry(meta, session, row)
elif row.type == assignment_sql.AssignmentType.USER_DOMAIN:
return build_user_domain_entry(meta, session, row)
elif row.type == assignment_sql.AssignmentType.GROUP_DOMAIN:
return build_group_domain_entry(meta, session, row)
# If the row type doesn't match any that we understand we drop
# the data.

session = sql.orm.sessionmaker(bind=migrate_engine)()
downgrade_table = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True)
for assignment in session.query(downgrade_table).all():
update = build_update(meta, session, assignment)
if update is not None:
migrate_engine.execute(update)

# Delete all the rows
migrate_engine.execute(downgrade_table.delete())

session.commit()
session.close()


def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine

session = sql.orm.sessionmaker(bind=migrate_engine)()
for table_name in GRANT_TABLES:
migrate_grant_table(meta, migrate_engine, session, table_name)
session.commit()
session.close()


def downgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine

downgrade_assignment_table(meta, migrate_engine)
Loading

0 comments on commit ec995b3

Please sign in to comment.