diff options
author | Jenkins <jenkins@review.openstack.org> | 2013-02-12 22:16:46 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2013-02-12 22:16:46 +0000 |
commit | 337d2b8748f346484db410c6815c484d3dda8989 (patch) | |
tree | b01d9d869b190e20aef95c852dc915e61d7211c6 /keystone | |
parent | 086e40101a9fb26a0330505434b447497de1c514 (diff) | |
parent | 8a89464d62e9c81a1ba15c0a3aa695456fc6fd33 (diff) | |
download | keystone-337d2b8748f346484db410c6815c484d3dda8989.tar.gz keystone-337d2b8748f346484db410c6815c484d3dda8989.tar.xz keystone-337d2b8748f346484db410c6815c484d3dda8989.zip |
Merge "Keystone backend preparation for domain-scoping"
Diffstat (limited to 'keystone')
-rw-r--r-- | keystone/common/controller.py | 50 | ||||
-rw-r--r-- | keystone/common/models.py | 13 | ||||
-rw-r--r-- | keystone/common/sql/legacy.py | 9 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/008_create_default_domain.py | 2 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py | 35 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py | 61 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/014_add_group_tables.py | 3 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py | 1 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/016_normalize_domain_ids.py | 414 | ||||
-rw-r--r-- | keystone/common/sql/nova.py | 5 | ||||
-rw-r--r-- | keystone/config.py | 3 | ||||
-rw-r--r-- | keystone/identity/backends/kvs.py | 80 | ||||
-rw-r--r-- | keystone/identity/backends/ldap/core.py | 23 | ||||
-rw-r--r-- | keystone/identity/backends/pam.py | 8 | ||||
-rw-r--r-- | keystone/identity/backends/sql.py | 53 | ||||
-rw-r--r-- | keystone/identity/controllers.py | 83 | ||||
-rw-r--r-- | keystone/identity/core.py | 19 | ||||
-rw-r--r-- | keystone/token/controllers.py | 127 |
18 files changed, 800 insertions, 189 deletions
diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 3112fc0c..83400d3e 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -10,6 +10,7 @@ from keystone import exception LOG = logging.getLogger(__name__) CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id def protected(f): @@ -68,6 +69,21 @@ class V2Controller(wsgi.Application): msg = '%s field is required and cannot be empty' % attr raise exception.ValidationError(message=msg) + def _normalize_domain_id(self, context, ref): + """Fill in domain_id since v2 calls are not domain-aware. + + This will overwrite any domain_id that was inadvertently + specified in the v2 call. + + """ + ref['domain_id'] = DEFAULT_DOMAIN_ID + return ref + + def _filter_domain_id(self, ref): + """Remove domain_id since v2 calls are not domain-aware.""" + ref.pop('domain_id', None) + return ref + class V3Controller(V2Controller): """Base controller class for Identity API v3. @@ -148,3 +164,37 @@ class V3Controller(V2Controller): value = context['query_string'][attr] return [r for r in refs if r[attr] == value] return refs + + def _normalize_domain_id(self, context, ref): + """Fill in domain_id if not specified in a v3 call.""" + + if 'domain_id' not in ref: + if context['is_admin']: + ref['domain_id'] = DEFAULT_DOMAIN_ID + else: + # Fish the domain_id out of the token + # + # We could make this more efficient by loading the domain_id + # into the context in the wrapper function above (since + # this version of normalize_domain will only be called inside + # a v3 protected call). However, given that we only use this + # for creating entities, this optimization is probably not + # worth the duplication of state + try: + token_ref = self.token_api.get_token( + context=context, token_id=context['token_id']) + except exception.TokenNotFound: + LOG.warning(_('Invalid token in normalize_domain_id')) + raise exception.Unauthorized() + + if 'domain' in token_ref: + ref['domain_id'] = token_ref['domain']['id'] + else: + # FIXME(henry-nash) Revisit this once v3 token scoping + # across domains has been hashed out + ref['domain_id'] = DEFAULT_DOMAIN_ID + return ref + + def _filter_domain_id(self, ref): + """Override v2 filter to let domain_id out for v3 calls.""" + return ref diff --git a/keystone/common/models.py b/keystone/common/models.py index 72818111..f572d382 100644 --- a/keystone/common/models.py +++ b/keystone/common/models.py @@ -87,6 +87,7 @@ class User(Model): Required keys: id name + domain_id Optional keys: password @@ -95,7 +96,7 @@ class User(Model): enabled (bool, default True) """ - required_keys = ('id', 'name') + required_keys = ('id', 'name', 'domain_id') optional_keys = ('password', 'description', 'email', 'enabled') @@ -105,15 +106,16 @@ class Group(Model): Required keys: id name + domain_id Optional keys: - domain_id + description """ - required_keys = ('id', 'name') - optional_keys = ('domain_id', 'description') + required_keys = ('id', 'name', 'domain_id') + optional_keys = ('description') class Project(Model): @@ -122,6 +124,7 @@ class Project(Model): Required keys: id name + domain_id Optional Keys: description @@ -129,7 +132,7 @@ class Project(Model): """ - required_keys = ('id', 'name') + required_keys = ('id', 'name', 'domain_id') optional_keys = ('description', 'enabled') diff --git a/keystone/common/sql/legacy.py b/keystone/common/sql/legacy.py index 4d742456..82dda2cf 100644 --- a/keystone/common/sql/legacy.py +++ b/keystone/common/sql/legacy.py @@ -22,9 +22,12 @@ from sqlalchemy import exc from keystone.common import logging from keystone.contrib.ec2.backends import sql as ec2_sql from keystone.identity.backends import sql as identity_sql +from keystone import config LOG = logging.getLogger(__name__) +CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id def export_db(db): @@ -103,7 +106,8 @@ class LegacyMigration(object): # map new_dict = {'description': x.get('desc', ''), 'id': x.get('uid', x.get('id')), - 'enabled': x.get('enabled', True)} + 'enabled': x.get('enabled', True), + 'domain_id': x.get('domain_id', DEFAULT_DOMAIN_ID)} new_dict['name'] = x.get('name', new_dict.get('id')) # track internal ids self._project_map[x.get('id')] = new_dict['id'] @@ -117,7 +121,8 @@ class LegacyMigration(object): new_dict = {'email': x.get('email', ''), 'password': x.get('password', None), 'id': x.get('uid', x.get('id')), - 'enabled': x.get('enabled', True)} + 'enabled': x.get('enabled', True), + 'domain_id': x.get('domain_id', DEFAULT_DOMAIN_ID)} if x.get('tenant_id'): new_dict['tenant_id'] = self._project_map.get(x['tenant_id']) new_dict['name'] = x.get('name', new_dict.get('id')) diff --git a/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py b/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py index fb1da77b..3a88f897 100644 --- a/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py +++ b/keystone/common/sql/migrate_repo/versions/008_create_default_domain.py @@ -22,7 +22,7 @@ from keystone import config CONF = config.CONF -DEFAULT_DOMAIN_ID = CONF['identity']['default_domain_id'] +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id def upgrade(migrate_engine): diff --git a/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py b/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py index 01c153ce..7f0ee379 100644 --- a/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py +++ b/keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py @@ -32,26 +32,21 @@ def is_enabled(enabled): return bool(enabled) -def downgrade_user_table(meta, migrate_engine): +def downgrade_user_table(meta, migrate_engine, session): user_table = Table('user', meta, autoload=True) - maker = sessionmaker(bind=migrate_engine) - session = maker() for user in session.query(user_table).all(): extra = json.loads(user.extra) extra['password'] = user.password extra['enabled'] = '%r' % user.enabled - values = {'extra': json.dumps(extra)} + values = {'extra': json.dumps(extra)} update = user_table.update().\ where(user_table.c.id == user.id).\ values(values) migrate_engine.execute(update) - session.commit() -def downgrade_tenant_table(meta, migrate_engine): +def downgrade_tenant_table(meta, migrate_engine, session): tenant_table = Table('tenant', meta, autoload=True) - maker = sessionmaker(bind=migrate_engine) - session = maker() for tenant in session.query(tenant_table).all(): extra = json.loads(tenant.extra) extra['description'] = tenant.description @@ -61,13 +56,10 @@ def downgrade_tenant_table(meta, migrate_engine): where(tenant_table.c.id == tenant.id).\ values(values) migrate_engine.execute(update) - session.commit() -def upgrade_user_table(meta, migrate_engine): +def upgrade_user_table(meta, migrate_engine, session): user_table = Table('user', meta, autoload=True) - maker = sessionmaker(bind=migrate_engine) - session = maker() for user in session.query(user_table).all(): extra = json.loads(user.extra) values = {'password': extra.pop('password', None), @@ -77,14 +69,10 @@ def upgrade_user_table(meta, migrate_engine): where(user_table.c.id == user.id).\ values(values) migrate_engine.execute(update) - session.commit() -def upgrade_tenant_table(meta, migrate_engine): +def upgrade_tenant_table(meta, migrate_engine, session): tenant_table = Table('tenant', meta, autoload=True) - - maker = sessionmaker(bind=migrate_engine) - session = maker() for tenant in session.query(tenant_table): extra = json.loads(tenant.extra) values = {'description': extra.pop('description', None), @@ -94,18 +82,21 @@ def upgrade_tenant_table(meta, migrate_engine): where(tenant_table.c.id == tenant.id).\ values(values) migrate_engine.execute(update) - session.commit() def upgrade(migrate_engine): meta = MetaData() meta.bind = migrate_engine - upgrade_user_table(meta, migrate_engine) - upgrade_tenant_table(meta, migrate_engine) + session = sessionmaker(bind=migrate_engine)() + upgrade_user_table(meta, migrate_engine, session) + upgrade_tenant_table(meta, migrate_engine, session) + session.commit() def downgrade(migrate_engine): meta = MetaData() meta.bind = migrate_engine - downgrade_user_table(meta, migrate_engine) - downgrade_tenant_table(meta, migrate_engine) + session = sessionmaker(bind=migrate_engine)() + downgrade_user_table(meta, migrate_engine, session) + downgrade_tenant_table(meta, migrate_engine, session) + session.commit() diff --git a/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py b/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py index abfe728a..cede906d 100644 --- a/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py +++ b/keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py @@ -48,13 +48,10 @@ def upgrade(migrate_engine): 'url': urls[interface], 'extra': json.dumps(extra), } - session.execute( - 'INSERT INTO `%s` (%s) VALUES (%s)' % ( - new_table.name, - ', '.join('%s' % k for k in endpoint.keys()), - ', '.join([':%s' % k for k in endpoint.keys()])), - endpoint) + insert = new_table.insert().values(endpoint) + migrate_engine.execute(insert) session.commit() + session.close() def downgrade(migrate_engine): @@ -67,31 +64,31 @@ def downgrade(migrate_engine): session = orm.sessionmaker(bind=migrate_engine)() for ref in session.query(new_table).all(): - extra = json.loads(ref.extra) - extra['%surl' % ref.interface] = ref.url - endpoint = { - 'id': ref.legacy_endpoint_id, - 'region': ref.region, - 'service_id': ref.service_id, - 'extra': json.dumps(extra), - } - - try: - session.execute( - 'INSERT INTO `%s` (%s) VALUES (%s)' % ( - legacy_table.name, - ', '.join('%s' % k for k in endpoint.keys()), - ', '.join([':%s' % k for k in endpoint.keys()])), - endpoint) - except sql.exc.IntegrityError: - q = session.query(legacy_table) - q = q.filter_by(id=ref.legacy_endpoint_id) - legacy_ref = q.one() + q = session.query(legacy_table) + q = q.filter_by(id=ref.legacy_endpoint_id) + legacy_ref = q.first() + if legacy_ref: + # We already have one, so just update the extra + # attribute with the urls. extra = json.loads(legacy_ref.extra) extra['%surl' % ref.interface] = ref.url - - session.execute( - 'UPDATE `%s` SET extra=:extra WHERE id=:id' % ( - legacy_table.name), - {'extra': json.dumps(extra), 'id': legacy_ref.id}) - session.commit() + values = {'extra': json.dumps(extra)} + update = legacy_table.update().\ + where(legacy_table.c.id == legacy_ref.id).\ + values(values) + migrate_engine.execute(update) + else: + # This is the first one of this legacy ID, so + # we can insert instead. + extra = json.loads(ref.extra) + extra['%surl' % ref.interface] = ref.url + endpoint = { + 'id': ref.legacy_endpoint_id, + 'region': ref.region, + 'service_id': ref.service_id, + 'extra': json.dumps(extra), + } + insert = legacy_table.insert().values(endpoint) + migrate_engine.execute(insert) + session.commit() + session.close() diff --git a/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py b/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py index a42b5772..668bca2d 100644 --- a/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py +++ b/keystone/common/sql/migrate_repo/versions/014_add_group_tables.py @@ -26,7 +26,8 @@ def upgrade(migrate_engine): 'group', meta, sql.Column('id', sql.String(64), primary_key=True), - sql.Column('domain_id', sql.String(64), sql.ForeignKey('domain.id')), + sql.Column('domain_id', sql.String(64), sql.ForeignKey('domain.id'), + nullable=False), sql.Column('name', sql.String(64), unique=True, nullable=False), sql.Column('description', sql.Text()), sql.Column('extra', sql.Text())) diff --git a/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py b/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py index 9b413338..4ac0d612 100644 --- a/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py +++ b/keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py @@ -11,7 +11,6 @@ def upgrade(migrate_engine): def downgrade(migrate_engine): - """Replace API-version specific endpoint tables with one based on v2.""" meta = sql.MetaData() meta.bind = migrate_engine upgrade_table = sql.Table('project', meta, autoload=True) diff --git a/keystone/common/sql/migrate_repo/versions/016_normalize_domain_ids.py b/keystone/common/sql/migrate_repo/versions/016_normalize_domain_ids.py new file mode 100644 index 00000000..4705daf0 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/016_normalize_domain_ids.py @@ -0,0 +1,414 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# Copyright 2013 IBM +# +# 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. + +""" +Normalize for domain_id, i.e. ensure User and Project entities have the +domain_id as a first class attribute. + +Both User and Project (as well as Group) entities are owned by a +domain, which is implemented as each having a domain_id foreign key +in their sql representation that points back to the respective +domain in the domain table. This domain_id attribute should also +be required (i.e. not nullable) + +Adding a non_nullable foreign key attribute to a table with existing +data causes a few problems since not all DB engines support the +ability to either control the triggering of integrity constraints +or the ability to modify columns after they are created. + +To get round the above inconsistencies, two versions of the +upgrade/downgrade functions are supplied, one for those engines +that support dropping columns, and one for those that don't. For +the latter we are forced to do table copy AND control the triggering +of integrity constraints. +""" + +import sqlalchemy as sql +from sqlalchemy.orm import sessionmaker +from keystone import config + + +CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + + +def _disable_foreign_constraints(session, migrate_engine): + if migrate_engine.name == 'mysql': + session.execute('SET foreign_key_checks = 0;') + + +def _enable_foreign_constraints(session, migrate_engine): + if migrate_engine.name == 'mysql': + session.execute('SET foreign_key_checks = 1;') + + +def upgrade_user_table_with_copy(meta, migrate_engine, session): + # We want to add the domain_id attribute to the user table. Since + # it is non nullable and the table may have data, easiest way is + # a table copy. Further, in order to keep foreign key constraints + # pointing at the right table, we need to be able and do a table + # DROP then CREATE, rather than ALTERing the name of the table. + + # First make a copy of the user table + temp_user_table = sql.Table( + 'temp_user', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('extra', sql.Text()), + sql.Column("password", sql.String(128)), + sql.Column("enabled", sql.Boolean, default=True)) + temp_user_table.create(migrate_engine, checkfirst=True) + + user_table = sql.Table('user', meta, autoload=True) + for user in session.query(user_table): + session.execute("insert into temp_user (id, name, extra, " + "password, enabled) " + "values ( :id, :name, :extra, " + ":password, :enabled);", + {'id': user.id, + 'name': user.name, + 'extra': user.extra, + 'password': user.password, + 'enabled': user.enabled}) + + # Now switch off constraints while we drop and then re-create the + # user table, with the additional domain_id column + _disable_foreign_constraints(session, migrate_engine) + session.execute('drop table user;') + # Need to create a new metadata stream since we are going to load a + # different version of the user table + meta2 = sql.MetaData() + meta2.bind = migrate_engine + domain_table = sql.Table('domain', meta2, autoload=True) + user_table = sql.Table( + 'user', + meta2, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('extra', sql.Text()), + sql.Column("password", sql.String(128)), + sql.Column("enabled", sql.Boolean, default=True), + sql.Column('domain_id', sql.String(64), sql.ForeignKey('domain.id'), + nullable=False)) + user_table.create(migrate_engine, checkfirst=True) + + # Finally copy in the data from our temp table and then clean + # up by deleting our temp table + for user in session.query(temp_user_table): + session.execute("insert into user (id, name, extra, " + "password, enabled, domain_id) " + "values ( :id, :name, :extra, " + ":password, :enabled, :domain_id);", + {'id': user.id, + 'name': user.name, + 'extra': user.extra, + 'password': user.password, + 'enabled': user.enabled, + 'domain_id': DEFAULT_DOMAIN_ID}) + _enable_foreign_constraints(session, migrate_engine) + session.execute("drop table temp_user;") + + +def upgrade_project_table_with_copy(meta, migrate_engine, session): + # We want to add the domain_id attribute to the project table. Since + # it is non nullable and the table may have data, easiest way is + # a table copy. Further, in order to keep foreign key constraints + # pointing at the right table, we need to be able and do a table + # DROP then CREATE, rather than ALTERing the name of the table. + + # Fist make a copy of the project table + temp_project_table = sql.Table( + 'temp_project', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('extra', sql.Text()), + sql.Column("description", sql.Text()), + sql.Column("enabled", sql.Boolean, default=True)) + temp_project_table.create(migrate_engine, checkfirst=True) + + project_table = sql.Table('project', meta, autoload=True) + for project in session.query(project_table): + session.execute("insert into temp_project (id, name, extra, " + "description, enabled) " + "values ( :id, :name, :extra, " + ":description, :enabled);", + {'id': project.id, + 'name': project.name, + 'extra': project.extra, + 'description': project.description, + 'enabled': project.enabled}) + + # Now switch off constraints while we drop and then re-create the + # project table, with the additional domain_id column + _disable_foreign_constraints(session, migrate_engine) + session.execute("drop table project;") + # Need to create a new metadata stream since we are going to load a + # different version of the project table + meta2 = sql.MetaData() + meta2.bind = migrate_engine + domain_table = sql.Table('domain', meta2, autoload=True) + project_table = sql.Table( + 'project', + meta2, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('extra', sql.Text()), + sql.Column('description', sql.Text()), + sql.Column("enabled", sql.Boolean, default=True), + sql.Column('domain_id', sql.String(64), sql.ForeignKey('domain.id'), + nullable=False)) + project_table.create(migrate_engine, checkfirst=True) + + # Finally copy in the data from our temp table and then clean + # up by deleting our temp table + for project in session.query(temp_project_table): + session.execute("insert into project (id, name, extra, " + "description, enabled, domain_id) " + "values ( :id, :name, :extra, " + ":description, :enabled, :domain_id);", + {'id': project.id, + 'name': project.name, + 'extra': project.extra, + 'description': project.description, + 'enabled': project.enabled, + 'domain_id': DEFAULT_DOMAIN_ID}) + _enable_foreign_constraints(session, migrate_engine) + session.execute("drop table temp_project;") + + +def downgrade_user_table_with_copy(meta, migrate_engine, session): + # For engines that don't support dropping columns, we need to do this + # as a table copy. Further, in order to keep foreign key constraints + # pointing at the right table, we need to be able and do a table + # DROP then CREATE, rather than ALTERing the name of the table. + + # Fist make a copy of the user table + temp_user_table = sql.Table( + 'temp_user', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + # Temporary table, so no need to make it a foreign key + sql.Column('domain_id', sql.String(64), nullable=False), + sql.Column("password", sql.String(128)), + sql.Column("enabled", sql.Boolean, default=True), + sql.Column('extra', sql.Text())) + temp_user_table.create(migrate_engine, checkfirst=True) + + user_table = sql.Table('user', meta, autoload=True) + for user in session.query(user_table): + session.execute("insert into temp_user (id, name, domain_id, " + "password, enabled, extra) " + "values ( :id, :name, :domain_id, " + ":password, :enabled, :extra);", + {'id': user.id, + 'name': user.name, + 'domain_id': user.domain_id, + 'password': user.password, + 'enabled': user.enabled, + 'extra': user.extra}) + + # Now switch off constraints while we drop and then re-create the + # user table, less the columns we wanted to drop + _disable_foreign_constraints(session, migrate_engine) + session.execute("drop table user;") + # Need to create a new metadata stream since we are going to load a + # different version of the user table + meta2 = sql.MetaData() + meta2.bind = migrate_engine + user_table = sql.Table( + 'user', + meta2, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('extra', sql.Text()), + sql.Column("password", sql.String(128)), + sql.Column("enabled", sql.Boolean, default=True)) + user_table.create(migrate_engine, checkfirst=True) + _enable_foreign_constraints(session, migrate_engine) + + # Finally copy in the data from our temp table and then clean + # up by deleting our temp table + for user in session.query(temp_user_table): + session.execute("insert into user (id, name, extra, " + "password, enabled) " + "values ( :id, :name, :extra, " + ":password, :enabled);", + {'id': user.id, + 'name': user.name, + 'extra': user.extra, + 'password': user.password, + 'enabled': user.enabled}) + session.execute("drop table temp_user;") + + +def downgrade_project_table_with_copy(meta, migrate_engine, session): + # For engines that don't support dropping columns, we need to do this + # as a table copy. Further, in order to keep foreign key constraints + # pointing at the right table, we need to be able and do a table + # DROP then CREATE, rather than ALTERing the name of the table. + + # Fist make a copy of the project table + temp_project_table = sql.Table( + 'temp_project', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + # Temporary table, so no need to make it a foreign key + sql.Column('domain_id', sql.String(64), nullable=False), + sql.Column('description', sql.Text()), + sql.Column("enabled", sql.Boolean, default=True), + sql.Column('extra', sql.Text())) + temp_project_table.create(migrate_engine, checkfirst=True) + + project_table = sql.Table('project', meta, autoload=True) + for project in session.query(project_table): + session.execute("insert into temp_project (id, name, domain_id, " + "description, enabled, extra) " + "values ( :id, :name, :domain_id, " + ":description, :enabled, :extra);", + {'id': project.id, + 'name': project.name, + 'domain_id': project.domain_id, + 'description': project.description, + 'enabled': project.enabled, + 'extra': project.extra}) + + # Now switch off constraints while we drop and then re-create the + # project table, less the columns we wanted to drop + _disable_foreign_constraints(session, migrate_engine) + session.execute("drop table project;") + # Need to create a new metadata stream since we are going to load a + # different version of the project table + meta2 = sql.MetaData() + meta2.bind = migrate_engine + project_table = sql.Table( + 'project', + meta2, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), unique=True, nullable=False), + sql.Column('extra', sql.Text()), + sql.Column("description", sql.Text()), + sql.Column("enabled", sql.Boolean, default=True)) + project_table.create(migrate_engine, checkfirst=True) + _enable_foreign_constraints(session, migrate_engine) + + # Finally copy in the data from our temp table and then clean + # up by deleting our temp table + for project in session.query(temp_project_table): + session.execute("insert into project (id, name, extra, " + "description, enabled) " + "values ( :id, :name, :extra, " + ":description, :enabled);", + {'id': project.id, + 'name': project.name, + 'extra': project.extra, + 'description': project.description, + 'enabled': project.enabled}) + session.execute("drop table temp_project;") + + +def upgrade_user_table_with_col_create(meta, migrate_engine, session): + # Create the domain_id column. We want this to be not nullable + # but also a foreign key. We can't create this right off the + # bat since any existing rows would cause an Integrity Error. + # We therefore create it nullable, fill the column with the + # default data and then set it to non nullable. + domain_table = sql.Table('domain', meta, autoload=True) + user_table = sql.Table('user', meta, autoload=True) + user_table.create_column( + sql.Column('domain_id', sql.String(64), + sql.ForeignKey('domain.id'), nullable=True)) + for user in session.query(user_table).all(): + values = {'domain_id': DEFAULT_DOMAIN_ID} + update = user_table.update().\ + where(user_table.c.id == user.id).\ + values(values) + migrate_engine.execute(update) + # Need to commit this or setting nullable to False will fail + session.commit() + user_table.columns.domain_id.alter(nullable=False) + + +def upgrade_project_table_with_col_create(meta, migrate_engine, session): + # Create the domain_id column. We want this to be not nullable + # but also a foreign key. We can't create this right off the + # bat since any existing rows would cause an Integrity Error. + # We therefore create it nullable, fill the column with the + # default data and then set it to non nullable. + domain_table = sql.Table('domain', meta, autoload=True) + project_table = sql.Table('project', meta, autoload=True) + project_table.create_column( + sql.Column('domain_id', sql.String(64), + sql.ForeignKey('domain.id'), nullable=True)) + for project in session.query(project_table).all(): + values = {'domain_id': DEFAULT_DOMAIN_ID} + update = project_table.update().\ + where(project_table.c.id == project.id).\ + values(values) + migrate_engine.execute(update) + # Need to commit this or setting nullable to False will fail + session.commit() + project_table.columns.domain_id.alter(nullable=False) + + +def downgrade_user_table_with_col_drop(meta, migrate_engine): + domain_table = sql.Table('domain', meta, autoload=True) + user_table = sql.Table('user', meta, autoload=True) + column = sql.Column('domain_id', sql.String(64), + sql.ForeignKey('domain.id'), nullable=False) + column.drop(user_table) + + +def downgrade_project_table_with_col_drop(meta, migrate_engine): + domain_table = sql.Table('domain', meta, autoload=True) + project_table = sql.Table('project', meta, autoload=True) + column = sql.Column('domain_id', sql.String(64), + sql.ForeignKey('domain.id'), nullable=False) + column.drop(project_table) + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + session = sessionmaker(bind=migrate_engine)() + if migrate_engine.name in ['sqlite', 'mysql']: + upgrade_user_table_with_copy(meta, migrate_engine, session) + upgrade_project_table_with_copy(meta, migrate_engine, session) + else: + upgrade_user_table_with_col_create(meta, migrate_engine, session) + upgrade_project_table_with_col_create(meta, migrate_engine, session) + session.commit() + session.close() + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + session = sessionmaker(bind=migrate_engine)() + if migrate_engine.name in ['sqlite', 'mysql']: + downgrade_user_table_with_copy(meta, migrate_engine, session) + downgrade_project_table_with_copy(meta, migrate_engine, session) + else: + # MySQL should in theory be able to use this path, but seems to + # have problems dropping columns which are foreign keys + downgrade_user_table_with_col_drop(meta, migrate_engine) + downgrade_project_table_with_col_drop(meta, migrate_engine) + session.commit() + session.close() diff --git a/keystone/common/sql/nova.py b/keystone/common/sql/nova.py index c7fc4725..968c542f 100644 --- a/keystone/common/sql/nova.py +++ b/keystone/common/sql/nova.py @@ -18,12 +18,15 @@ import uuid +from keystone import config from keystone.common import logging from keystone.contrib.ec2.backends import sql as ec2_sql from keystone.identity.backends import sql as identity_sql LOG = logging.getLogger(__name__) +CONF = config.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id def import_auth(data): @@ -51,6 +54,7 @@ def _create_projects(api, tenants): tenant_dict = { 'id': _generate_uuid(), 'name': tenant['id'], + 'domain_id': tenant.get('domain_id', DEFAULT_DOMAIN_ID), 'description': tenant['description'], 'enabled': True, } @@ -66,6 +70,7 @@ def _create_users(api, users): user_dict = { 'id': _generate_uuid(), 'name': user['id'], + 'domain_id': user.get('domain_id', DEFAULT_DOMAIN_ID), 'email': '', 'password': user['password'], 'enabled': True, diff --git a/keystone/config.py b/keystone/config.py index cedb5d57..53fd1756 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -256,6 +256,7 @@ register_str('user_name_attribute', group='ldap', default='sn') register_str('user_mail_attribute', group='ldap', default='email') register_str('user_pass_attribute', group='ldap', default='userPassword') register_str('user_enabled_attribute', group='ldap', default='enabled') +register_str('user_domain_id_attribute', group='ldap', default='domain_id') register_int('user_enabled_mask', group='ldap', default=0) register_str('user_enabled_default', group='ldap', default='True') register_list('user_attribute_ignore', group='ldap', @@ -272,6 +273,7 @@ register_str('tenant_member_attribute', group='ldap', default='member') register_str('tenant_name_attribute', group='ldap', default='ou') register_str('tenant_desc_attribute', group='ldap', default='desc') register_str('tenant_enabled_attribute', group='ldap', default='enabled') +register_str('tenant_domain_id_attribute', group='ldap', default='domain_id') register_list('tenant_attribute_ignore', group='ldap', default='') register_bool('tenant_allow_create', group='ldap', default=True) register_bool('tenant_allow_update', group='ldap', default=True) @@ -295,6 +297,7 @@ register_str('group_id_attribute', group='ldap', default='cn') register_str('group_name_attribute', group='ldap', default='ou') register_str('group_member_attribute', group='ldap', default='member') register_str('group_desc_attribute', group='ldap', default='desc') +register_str('group_domain_id_attribute', group='ldap', default='domain_id') register_list('group_attribute_ignore', group='ldap', default='') register_bool('group_allow_create', group='ldap', default=True) register_bool('group_allow_update', group='ldap', default=True) diff --git a/keystone/identity/backends/kvs.py b/keystone/identity/backends/kvs.py index 8eef7df5..6922a1c1 100644 --- a/keystone/identity/backends/kvs.py +++ b/keystone/identity/backends/kvs.py @@ -67,7 +67,7 @@ class Identity(kvs.Base, identity.Driver): tenant_keys = filter(lambda x: x.startswith("tenant-"), self.db.keys()) return [self.db.get(key) for key in tenant_keys] - def get_project_by_name(self, tenant_name): + def get_project_by_name(self, tenant_name, domain_id): try: return self.db.get('tenant_name-%s' % tenant_name) except exception.NotFound: @@ -85,7 +85,7 @@ class Identity(kvs.Base, identity.Driver): except exception.NotFound: raise exception.UserNotFound(user_id=user_id) - def _get_user_by_name(self, user_name): + def _get_user_by_name(self, user_name, domain_id): try: return self.db.get('user_name-%s' % user_name) except exception.NotFound: @@ -94,16 +94,27 @@ class Identity(kvs.Base, identity.Driver): def get_user(self, user_id): return identity.filter_user(self._get_user(user_id)) - def get_user_by_name(self, user_name): - return identity.filter_user(self._get_user_by_name(user_name)) + def get_user_by_name(self, user_name, domain_id): + return identity.filter_user( + self._get_user_by_name(user_name, domain_id)) def get_metadata(self, user_id=None, tenant_id=None, domain_id=None, group_id=None): try: if user_id: - return self.db.get('metadata-%s-%s' % (tenant_id, user_id)) + if tenant_id: + return self.db.get('metadata-%s-%s' % (tenant_id, + user_id)) + else: + return self.db.get('metadata-%s-%s' % (domain_id, + user_id)) else: - return self.db.get('metadata-%s-%s' % (tenant_id, group_id)) + if tenant_id: + return self.db.get('metadata-%s-%s' % (tenant_id, + group_id)) + else: + return self.db.get('metadata-%s-%s' % (domain_id, + group_id)) except exception.NotFound: raise exception.MetadataNotFound() @@ -195,7 +206,7 @@ class Identity(kvs.Base, identity.Driver): raise exception.Conflict(type='user', details=msg) try: - self.get_user_by_name(user['name']) + self.get_user_by_name(user['name'], user['domain_id']) except exception.UserNotFound: pass else: @@ -294,7 +305,7 @@ class Identity(kvs.Base, identity.Driver): raise exception.Conflict(type='tenant', details=msg) try: - self.get_project_by_name(tenant['name']) + self.get_project_by_name(tenant['name'], tenant['domain_id']) except exception.ProjectNotFound: pass else: @@ -338,18 +349,22 @@ class Identity(kvs.Base, identity.Driver): def create_metadata(self, user_id, tenant_id, metadata, domain_id=None, group_id=None): - if user_id: - self.db.set('metadata-%s-%s' % (tenant_id, user_id), metadata) - else: - self.db.set('metadata-%s-%s' % (tenant_id, group_id), metadata) - return metadata + + return self.update_metadata(user_id, tenant_id, metadata, + domain_id, group_id) def update_metadata(self, user_id, tenant_id, metadata, domain_id=None, group_id=None): if user_id: - self.db.set('metadata-%s-%s' % (tenant_id, user_id), metadata) + if tenant_id: + self.db.set('metadata-%s-%s' % (tenant_id, user_id), metadata) + else: + self.db.set('metadata-%s-%s' % (domain_id, user_id), metadata) else: - self.db.set('metadata-%s-%s' % (tenant_id, group_id), metadata) + if tenant_id: + self.db.set('metadata-%s-%s' % (tenant_id, group_id), metadata) + else: + self.db.set('metadata-%s-%s' % (domain_id, group_id), metadata) return metadata def create_role(self, role_id, role): @@ -500,7 +515,24 @@ class Identity(kvs.Base, identity.Driver): # domain crud def create_domain(self, domain_id, domain): + try: + self.get_domain(domain_id) + except exception.DomainNotFound: + pass + else: + msg = 'Duplicate ID, %s.' % domain_id + raise exception.Conflict(type='domain', details=msg) + + try: + self.get_domain_by_name(domain['name']) + except exception.DomainNotFound: + pass + else: + msg = 'Duplicate name, %s.' % domain['name'] + raise exception.Conflict(type='domain', details=msg) + self.db.set('domain-%s' % domain_id, domain) + self.db.set('domain_name-%s' % domain['name'], domain) domain_list = set(self.db.get('domain_list', [])) domain_list.add(domain_id) self.db.set('domain_list', list(domain_list)) @@ -510,14 +542,30 @@ class Identity(kvs.Base, identity.Driver): return self.db.get('domain_list', []) def get_domain(self, domain_id): - return self.db.get('domain-%s' % domain_id) + try: + return self.db.get('domain-%s' % domain_id) + except exception.NotFound: + raise exception.DomainNotFound(domain_id=domain_id) + + def get_domain_by_name(self, domain_name): + try: + return self.db.get('domain_name-%s' % domain_name) + except exception.NotFound: + raise exception.DomainNotFound(domain_id=domain_name) def update_domain(self, domain_id, domain): + orig_domain = self.get_domain(domain_id) + domain['id'] = domain_id self.db.set('domain-%s' % domain_id, domain) + self.db.set('domain_name-%s' % domain['name'], domain) + if domain['name'] != orig_domain['name']: + self.db.delete('domain_name-%s' % orig_domain['name']) return domain def delete_domain(self, domain_id): + domain = self.get_domain(domain_id) self.db.delete('domain-%s' % domain_id) + self.db.delete('domain_name-%s' % domain['name']) domain_list = set(self.db.get('domain_list', [])) domain_list.remove(domain_id) self.db.set('domain_list', list(domain_list)) diff --git a/keystone/identity/backends/ldap/core.py b/keystone/identity/backends/ldap/core.py index b403abff..177dd026 100644 --- a/keystone/identity/backends/ldap/core.py +++ b/keystone/identity/backends/ldap/core.py @@ -106,7 +106,9 @@ class Identity(identity.Driver): def get_projects(self): return self.project.get_all() - def get_project_by_name(self, tenant_name): + def get_project_by_name(self, tenant_name, domain_id): + # TODO(henry-nash): Use domain_id once domains are implemented + # in LDAP backend try: return self.project.get_by_name(tenant_name) except exception.NotFound: @@ -124,7 +126,9 @@ class Identity(identity.Driver): def list_users(self): return self.user.get_all() - def get_user_by_name(self, user_name): + def get_user_by_name(self, user_name, domain_id): + # TODO(henry-nash): Use domain_id once domains are implemented + # in LDAP backend try: return identity.filter_user(self.user.get_by_name(user_name)) except exception.NotFound: @@ -353,7 +357,8 @@ class UserApi(common_ldap.BaseLdap, ApiShimMixin): attribute_mapping = {'password': 'userPassword', 'email': 'mail', 'name': 'sn', - 'enabled': 'enabled'} + 'enabled': 'enabled', + 'domain_id': 'domain_id'} model = models.User @@ -363,6 +368,8 @@ class UserApi(common_ldap.BaseLdap, ApiShimMixin): self.attribute_mapping['email'] = conf.ldap.user_mail_attribute self.attribute_mapping['password'] = conf.ldap.user_pass_attribute self.attribute_mapping['enabled'] = conf.ldap.user_enabled_attribute + self.attribute_mapping['domain_id'] = ( + conf.ldap.user_domain_id_attribute) self.enabled_mask = conf.ldap.user_enabled_mask self.enabled_default = conf.ldap.user_enabled_default self.attribute_ignore = (getattr(conf.ldap, 'user_attribute_ignore') @@ -510,7 +517,8 @@ class ProjectApi(common_ldap.BaseLdap, ApiShimMixin): attribute_mapping = {'name': 'ou', 'description': 'desc', 'tenantId': 'cn', - 'enabled': 'enabled'} + 'enabled': 'enabled', + 'domain_id': 'domain_id'} model = models.Project def __init__(self, conf): @@ -519,6 +527,8 @@ class ProjectApi(common_ldap.BaseLdap, ApiShimMixin): self.attribute_mapping['name'] = conf.ldap.tenant_name_attribute self.attribute_mapping['description'] = conf.ldap.tenant_desc_attribute self.attribute_mapping['enabled'] = conf.ldap.tenant_enabled_attribute + self.attribute_mapping['domain_id'] = ( + conf.ldap.tenant_domain_id_attribute) self.member_attribute = (getattr(conf.ldap, 'tenant_member_attribute') or self.DEFAULT_MEMBER_ATTRIBUTE) self.attribute_ignore = (getattr(conf.ldap, 'tenant_attribute_ignore') @@ -1070,7 +1080,8 @@ class GroupApi(common_ldap.BaseLdap, ApiShimMixin): options_name = 'group' attribute_mapping = {'name': 'ou', 'description': 'desc', - 'groupId': 'cn'} + 'groupId': 'cn', + 'domain_id': 'domain_id'} model = models.Group def __init__(self, conf): @@ -1078,6 +1089,8 @@ class GroupApi(common_ldap.BaseLdap, ApiShimMixin): self.api = ApiShim(conf) self.attribute_mapping['name'] = conf.ldap.group_name_attribute self.attribute_mapping['description'] = conf.ldap.group_desc_attribute + self.attribute_mapping['domain_id'] = ( + conf.ldap.group_domain_id_attribute) self.member_attribute = (getattr(conf.ldap, 'group_member_attribute') or self.DEFAULT_MEMBER_ATTRIBUTE) self.attribute_ignore = (getattr(conf.ldap, 'group_attribute_ignore') diff --git a/keystone/identity/backends/pam.py b/keystone/identity/backends/pam.py index bc345424..3aa87c40 100644 --- a/keystone/identity/backends/pam.py +++ b/keystone/identity/backends/pam.py @@ -74,13 +74,17 @@ class PamIdentity(identity.Driver): def get_project(self, tenant_id): return {'id': tenant_id, 'name': tenant_id} - def get_project_by_name(self, tenant_name): + def get_project_by_name(self, tenant_name, domain_id): + # TODO(henry-nash): Used domain_id once domains are implemented + # in LDAP backend return {'id': tenant_name, 'name': tenant_name} def get_user(self, user_id): return {'id': user_id, 'name': user_id} - def get_user_by_name(self, user_name): + def get_user_by_name(self, user_name, domain_id): + # TODO(henry-nash): Used domain_id once domains are implemented + # in LDAP backend return {'id': user_name, 'name': user_name} def get_role(self, role_id): diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index a880995f..8004a416 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -39,9 +39,11 @@ def handle_conflicts(type='object'): class User(sql.ModelBase, sql.DictBase): __tablename__ = 'user' - attributes = ['id', 'name', 'password', 'enabled'] + attributes = ['id', 'name', 'domain_id', 'password', 'enabled'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), unique=True, nullable=False) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + nullable=False) password = sql.Column(sql.String(128)) enabled = sql.Column(sql.Boolean) extra = sql.Column(sql.JsonBlob()) @@ -52,7 +54,8 @@ class Group(sql.ModelBase, sql.DictBase): attributes = ['id', 'name', 'domain_id'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), unique=True, nullable=False) - domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id')) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + nullable=False) description = sql.Column(sql.Text()) extra = sql.Column(sql.JsonBlob()) @@ -82,9 +85,11 @@ class Domain(sql.ModelBase, sql.DictBase): # TODO(dolph): rename to Project class Project(sql.ModelBase, sql.DictBase): __tablename__ = 'project' - attributes = ['id', 'name'] + attributes = ['id', 'name', 'domain_id'] id = sql.Column(sql.String(64), primary_key=True) name = sql.Column(sql.String(64), unique=True, 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()) @@ -222,12 +227,16 @@ class Identity(sql.Base, identity.Driver): raise exception.ProjectNotFound(project_id=tenant_id) return tenant_ref.to_dict() - def get_project_by_name(self, tenant_name): + def get_project_by_name(self, tenant_name, domain_id): session = self.get_session() - tenant_ref = session.query(Project).filter_by(name=tenant_name).first() - if not tenant_ref: + 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 tenant_ref.to_dict() + return project_ref.to_dict() def get_project_users(self, tenant_id): session = self.get_session() @@ -484,7 +493,8 @@ class Identity(sql.Base, identity.Driver): tenant_ref = session.query(Project).filter_by(id=tenant_id).one() except sql.NotFound: raise exception.ProjectNotFound(project_id=tenant_id) - + # FIXME(henry-nash) Think about how we detect potential name clash + # when we move domains with session.begin(): old_project_dict = tenant_ref.to_dict() for k in tenant: @@ -603,6 +613,14 @@ class Identity(sql.Base, identity.Driver): raise exception.DomainNotFound(domain_id=domain_id) return ref.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() + @handle_conflicts(type='domain') def update_domain(self, domain_id, domain): session = self.get_session() @@ -674,18 +692,23 @@ class Identity(sql.Base, identity.Driver): raise exception.UserNotFound(user_id=user_id) return user_ref.to_dict() - def _get_user_by_name(self, user_name): + def _get_user_by_name(self, user_name, domain_id): session = self.get_session() - user_ref = session.query(User).filter_by(name=user_name).first() - if not user_ref: + query = session.query(User) + query = query.filter_by(name=user_name) + query = query.filter_by(domain_id=domain_id) + try: + user_ref = query.one() + except sql.NotFound: raise exception.UserNotFound(user_id=user_name) return user_ref.to_dict() def get_user(self, user_id): return identity.filter_user(self._get_user(user_id)) - def get_user_by_name(self, user_name): - return identity.filter_user(self._get_user_by_name(user_name)) + def get_user_by_name(self, user_name, domain_id): + return identity.filter_user( + self._get_user_by_name(user_name, domain_id)) @handle_conflicts(type='user') def update_user(self, user_id, user): @@ -694,6 +717,8 @@ class Identity(sql.Base, identity.Driver): session = self.get_session() if 'id' in user and user_id != user['id']: raise exception.ValidationError('Cannot change user ID') + # FIXME(henry-nash) Think about how we detect potential name clash + # when we move domains with session.begin(): user_ref = session.query(User).filter_by(id=user_id).first() if user_ref is None: @@ -826,6 +851,8 @@ class Identity(sql.Base, identity.Driver): @handle_conflicts(type='group') def update_group(self, group_id, group): session = self.get_session() + # FIXME(henry-nash) Think about how we detect potential name clash + # when we move domains with session.begin(): ref = session.query(Group).filter_by(id=group_id).first() if ref is None: diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 1b7180bb..c34a25b6 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -27,7 +27,7 @@ from keystone import exception CONF = config.CONF -DEFAULT_DOMAIN_ID = CONF['identity']['default_domain_id'] +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id LOG = logging.getLogger(__name__) @@ -40,6 +40,8 @@ class Tenant(controller.V2Controller): self.assert_admin(context) tenant_refs = self.identity_api.get_projects(context) + for tenant_ref in tenant_refs: + tenant_ref = self._filter_domain_id(tenant_ref) params = { 'limit': context['query_string'].get('limit'), 'marker': context['query_string'].get('marker'), @@ -67,9 +69,9 @@ class Tenant(controller.V2Controller): context, user_ref['id']) tenant_refs = [] for tenant_id in tenant_ids: - tenant_refs.append(self.identity_api.get_project( - context=context, - tenant_id=tenant_id)) + ref = self.identity_api.get_project( + context=context, tenant_id=tenant_id) + tenant_refs.append(self._filter_domain_id(ref)) params = { 'limit': context['query_string'].get('limit'), 'marker': context['query_string'].get('marker'), @@ -79,12 +81,14 @@ class Tenant(controller.V2Controller): def get_project(self, context, tenant_id): # TODO(termie): this stuff should probably be moved to middleware self.assert_admin(context) - return {'tenant': self.identity_api.get_project(context, tenant_id)} + ref = self.identity_api.get_project(context, tenant_id) + return {'tenant': self._filter_domain_id(ref)} def get_project_by_name(self, context, tenant_name): self.assert_admin(context) - return {'tenant': self.identity_api.get_project_by_name( - context, tenant_name)} + ref = self.identity_api.get_project_by_name( + context, tenant_name, DEFAULT_DOMAIN_ID) + return {'tenant': self._filter_domain_id(ref)} # CRUD Extension def create_project(self, context, tenant): @@ -97,13 +101,18 @@ class Tenant(controller.V2Controller): self.assert_admin(context) tenant_ref['id'] = tenant_ref.get('id', uuid.uuid4().hex) tenant = self.identity_api.create_project( - context, tenant_ref['id'], tenant_ref) - return {'tenant': tenant} + context, tenant_ref['id'], + self._normalize_domain_id(context, tenant_ref)) + return {'tenant': self._filter_domain_id(tenant)} def update_project(self, context, tenant_id, tenant): self.assert_admin(context) + # Remove domain_id if specified - a v2 api caller should not + # be specifying that + clean_tenant = tenant.copy() + clean_tenant.pop('domain_id', None) tenant_ref = self.identity_api.update_project( - context, tenant_id, tenant) + context, tenant_id, clean_tenant) return {'tenant': tenant_ref} def delete_project(self, context, tenant_id): @@ -113,6 +122,8 @@ class Tenant(controller.V2Controller): def get_project_users(self, context, tenant_id, **kw): self.assert_admin(context) user_refs = self.identity_api.get_project_users(context, tenant_id) + for user_ref in user_refs: + self._filter_domain_id(user_ref) return {'users': user_refs} def _format_project_list(self, tenant_refs, **kwargs): @@ -153,7 +164,8 @@ class Tenant(controller.V2Controller): class User(controller.V2Controller): def get_user(self, context, user_id): self.assert_admin(context) - return {'user': self.identity_api.get_user(context, user_id)} + ref = self.identity_api.get_user(context, user_id) + return {'user': self._filter_domain_id(ref)} def get_users(self, context): # NOTE(termie): i can't imagine that this really wants all the data @@ -163,11 +175,16 @@ class User(controller.V2Controller): context, context['query_string'].get('name')) self.assert_admin(context) - return {'users': self.identity_api.list_users(context)} + user_list = self.identity_api.list_users(context) + for x in user_list: + self._filter_domain_id(x) + return {'users': user_list} def get_user_by_name(self, context, user_name): self.assert_admin(context) - return {'user': self.identity_api.get_user_by_name(context, user_name)} + ref = self.identity_api.get_user_by_name( + context, user_name, DEFAULT_DOMAIN_ID) + return {'user': self._filter_domain_id(ref)} # CRUD extension def create_user(self, context, user): @@ -178,18 +195,20 @@ class User(controller.V2Controller): msg = 'Name field is required and cannot be empty' raise exception.ValidationError(message=msg) - tenant_id = user.get('tenantId', None) - if (tenant_id is not None - and self.identity_api.get_project(context, tenant_id) is None): - raise exception.ProjectNotFound(project_id=tenant_id) + default_tenant_id = user.get('tenantId', None) + if (default_tenant_id is not None + and self.identity_api.get_project(context, + default_tenant_id) is None): + raise exception.ProjectNotFound(project_id=default_tenant_id) user_id = uuid.uuid4().hex - user_ref = user.copy() + user_ref = self._normalize_domain_id(context, user.copy()) user_ref['id'] = user_id new_user_ref = self.identity_api.create_user( context, user_id, user_ref) - if tenant_id: - self.identity_api.add_user_to_project(context, tenant_id, user_id) - return {'user': new_user_ref} + if default_tenant_id: + self.identity_api.add_user_to_project(context, + default_tenant_id, user_id) + return {'user': self._filter_domain_id(new_user_ref)} def update_user(self, context, user_id, user): # NOTE(termie): this is really more of a patch than a put @@ -206,7 +225,7 @@ class User(controller.V2Controller): # backends that can't list tokens for users LOG.warning('User %s status has changed, but existing tokens ' 'remain valid' % user_id) - return {'user': user_ref} + return {'user': self._filter_domain_id(user_ref)} def delete_user(self, context, user_id): self.assert_admin(context) @@ -222,8 +241,9 @@ class User(controller.V2Controller): """Update the default tenant.""" self.assert_admin(context) # ensure that we're a member of that tenant - tenant_id = user.get('tenantId') - self.identity_api.add_user_to_project(context, tenant_id, user_id) + default_tenant_id = user.get('tenantId') + self.identity_api.add_user_to_project(context, + default_tenant_id, user_id) return self.update_user(context, user_id, user) @@ -403,6 +423,7 @@ class DomainV3(controller.V3Controller): @controller.protected def list_domains(self, context): refs = self.identity_api.list_domains(context) + refs = self._filter_by_attribute(context, refs, 'name') return DomainV3.wrap_collection(context, refs) @controller.protected @@ -456,6 +477,17 @@ class DomainV3(controller.V3Controller): return self.identity_api.delete_domain(context, domain_id) + def _get_domain_by_name(self, context, domain_name): + """Get the domain via its unique name. + + For use by token authentication - not for hooking to the identity + router as a public api. + + """ + ref = self.identity_api.get_domain_by_name( + context, domain_name) + return {'domain': ref} + class ProjectV3(controller.V3Controller): collection_name = 'projects' @@ -464,6 +496,7 @@ class ProjectV3(controller.V3Controller): @controller.protected def create_project(self, context, project): ref = self._assign_unique_id(self._normalize_dict(project)) + ref = self._normalize_domain_id(context, ref) ref = self.identity_api.create_project(context, ref['id'], ref) return ProjectV3.wrap_member(context, ref) @@ -501,6 +534,7 @@ class UserV3(controller.V3Controller): @controller.protected def create_user(self, context, user): ref = self._assign_unique_id(self._normalize_dict(user)) + ref = self._normalize_domain_id(context, ref) ref = self.identity_api.create_user(context, ref['id'], ref) return UserV3.wrap_member(context, ref) @@ -560,6 +594,7 @@ class GroupV3(controller.V3Controller): @controller.protected def create_group(self, context, group): ref = self._assign_unique_id(self._normalize_dict(group)) + ref = self._normalize_domain_id(context, ref) ref = self.identity_api.create_group(context, ref['id'], ref) return GroupV3.wrap_member(context, ref) diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 8c3c82d6..90587307 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -29,7 +29,9 @@ LOG = logging.getLogger(__name__) def filter_user(user_ref): - """Filter out private items in a user dict ('password' and 'tenants') + """Filter out private items in a user dict. + + 'password', 'tenants' and 'groups' are never returned. :returns: user_ref @@ -81,7 +83,7 @@ class Driver(object): """ raise exception.NotImplemented() - def get_project_by_name(self, tenant_name): + def get_project_by_name(self, tenant_name, domain_id): """Get a tenant by name. :returns: tenant_ref @@ -90,7 +92,7 @@ class Driver(object): """ raise exception.NotImplemented() - def get_user_by_name(self, user_name): + def get_user_by_name(self, user_name, domain_id): """Get a user by name. :returns: user_ref @@ -252,7 +254,16 @@ class Driver(object): def get_domain(self, domain_id): """Get a domain by ID. - :returns: user_ref + :returns: domain_ref + :raises: keystone.exception.DomainNotFound + + """ + raise exception.NotImplemented() + + def get_domain_by_name(self, domain_name): + """Get a domain by name. + + :returns: domain_ref :raises: keystone.exception.DomainNotFound """ diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py index 62134028..5dbfc0c3 100644 --- a/keystone/token/controllers.py +++ b/keystone/token/controllers.py @@ -13,6 +13,7 @@ from keystone.token import core CONF = config.CONF LOG = logging.getLogger(__name__) +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id class ExternalAuthNotApplicable(Exception): @@ -64,19 +65,26 @@ class Auth(controller.V2Controller): if "token" in auth: # Try to authenticate using a token - auth_token_data, auth_info = self._authenticate_token( + auth_info = self._authenticate_token( context, auth) else: # Try external authentication try: - auth_token_data, auth_info = self._authenticate_external( + auth_info = self._authenticate_external( context, auth) except ExternalAuthNotApplicable: # Try local authentication - auth_token_data, auth_info = self._authenticate_local( + auth_info = self._authenticate_local( context, auth) - user_ref, tenant_ref, metadata_ref = auth_info + user_ref, tenant_ref, metadata_ref, expiry = auth_info + user_ref = self._filter_domain_id(user_ref) + if tenant_ref: + tenant_ref = self._filter_domain_id(tenant_ref) + auth_token_data = self._get_auth_token_data(user_ref, + tenant_ref, + metadata_ref, + expiry) # If the user is disabled don't allow them to authenticate if not user_ref.get('enabled', True): @@ -202,21 +210,15 @@ class Auth(controller.V2Controller): tenant_ref = self._get_project_ref(context, user_id, tenant_id) metadata_ref = self._get_metadata_ref(context, user_id, tenant_id) - self._append_roles(metadata_ref, - self._get_group_metadata_ref( - context, user_id, tenant_id)) + # TODO (henry-nash) If no tenant was specified, instead check + # for a domain and find any related user/group roles self._append_roles(metadata_ref, - self._get_domain_metadata_ref( + self._get_group_metadata_ref( context, user_id, tenant_id)) expiry = old_token_ref['expires'] - auth_token_data = self._get_auth_token_data(current_user_ref, - tenant_ref, - metadata_ref, - expiry) - - return auth_token_data, (current_user_ref, tenant_ref, metadata_ref) + return (current_user_ref, tenant_ref, metadata_ref, expiry) def _authenticate_local(self, context, auth): """Try to authenticate against the identity backend. @@ -256,7 +258,8 @@ class Auth(controller.V2Controller): if username: try: user_ref = self.identity_api.get_user_by_name( - context=context, user_name=username) + context=context, user_name=username, + domain_id=DEFAULT_DOMAIN_ID) user_id = user_ref['id'] except exception.UserNotFound as e: raise exception.Unauthorized(e) @@ -273,21 +276,19 @@ class Auth(controller.V2Controller): raise exception.Unauthorized(e) (user_ref, tenant_ref, metadata_ref) = auth_info - self._append_roles(metadata_ref, - self._get_group_metadata_ref( - context, user_id, tenant_id)) + # By now we will have authorized and if a tenant/project was + # specified, we will have obtained its metadata. In this case + # we just need to add in any group roles. + # + # TODO (henry-nash) If no tenant was specified, instead check + # for a domain and find any related user/group roles self._append_roles(metadata_ref, - self._get_domain_metadata_ref( + self._get_group_metadata_ref( context, user_id, tenant_id)) expiry = core.default_expire_time() - auth_token_data = self._get_auth_token_data(user_ref, - tenant_ref, - metadata_ref, - expiry) - - return auth_token_data, (user_ref, tenant_ref, metadata_ref) + return (user_ref, tenant_ref, metadata_ref, expiry) def _authenticate_external(self, context, auth): """Try to authenticate an external user via REMOTE_USER variable. @@ -300,7 +301,8 @@ class Auth(controller.V2Controller): username = context['REMOTE_USER'] try: user_ref = self.identity_api.get_user_by_name( - context=context, user_name=username) + context=context, user_name=username, + domain_id=DEFAULT_DOMAIN_ID) user_id = user_ref['id'] except exception.UserNotFound as e: raise exception.Unauthorized(e) @@ -310,21 +312,15 @@ class Auth(controller.V2Controller): tenant_ref = self._get_project_ref(context, user_id, tenant_id) metadata_ref = self._get_metadata_ref(context, user_id, tenant_id) - self._append_roles(metadata_ref, - self._get_group_metadata_ref( - context, user_id, tenant_id)) + # TODO (henry-nash) If no tenant was specified, instead check + # for a domain and find any related user/group roles self._append_roles(metadata_ref, - self._get_domain_metadata_ref( + self._get_group_metadata_ref( context, user_id, tenant_id)) expiry = core.default_expire_time() - auth_token_data = self._get_auth_token_data(user_ref, - tenant_ref, - metadata_ref, - expiry) - - return auth_token_data, (user_ref, tenant_ref, metadata_ref) + return (user_ref, tenant_ref, metadata_ref, expiry) def _get_auth_token_data(self, user, tenant, metadata, expiry): return dict(dict(user=user, @@ -350,12 +346,32 @@ class Auth(controller.V2Controller): if tenant_name: try: tenant_ref = self.identity_api.get_project_by_name( - context=context, tenant_name=tenant_name) + context=context, tenant_name=tenant_name, + domain_id=DEFAULT_DOMAIN_ID) tenant_id = tenant_ref['id'] except exception.ProjectNotFound as e: raise exception.Unauthorized(e) return tenant_id + def _get_domain_id_from_auth(self, context, auth): + """Extract domain information from v3 auth dict. + + Returns a valid domain_id if it exists, or None if not specified. + """ + # FIXME(henry-nash): This is a placeholder that needs to be + # only called in the v3 context, and the auth.get calls + # converted to the v3 format + domain_id = auth.get('domainId', None) + domain_name = auth.get('domainName', None) + if domain_name: + try: + domain_ref = self.identity_api._get_domain_by_name( + context=context, domain_name=domain_name) + domain_id = domain_ref['id'] + except exception.DomainNotFound as e: + raise exception.Unauthorized(e) + return domain_id + def _get_project_ref(self, context, user_id, tenant_id): """Returns the tenant_ref for the user's tenant""" tenant_ref = None @@ -375,43 +391,32 @@ class Auth(controller.V2Controller): return tenant_ref def _get_metadata_ref(self, context, user_id=None, tenant_id=None, - group_id=None): - """Returns the metadata_ref for a user or group in a tenant""" + domain_id=None, group_id=None): + """Returns metadata_ref for a user or group in a tenant or domain""" + metadata_ref = {} - if tenant_id: + if (user_id or group_id) and (tenant_id or domain_id): try: - if user_id: - metadata_ref = self.identity_api.get_metadata( - context=context, - user_id=user_id, - tenant_id=tenant_id) - elif group_id: - metadata_ref = self.identity_api.get_metadata( - context=context, - group_id=group_id, - tenant_id=tenant_id) + metadata_ref = self.identity_api.get_metadata( + context=context, user_id=user_id, tenant_id=tenant_id, + domain_id=domain_id, group_id=group_id) except exception.MetadataNotFound: - metadata_ref = {} - + pass return metadata_ref - def _get_group_metadata_ref(self, context, user_id, tenant_id): - """Return any metadata for this project due to group grants""" + def _get_group_metadata_ref(self, context, user_id, + tenant_id=None, domain_id=None): + """Return any metadata for this project/domain due to group grants""" group_refs = self.identity_api.list_groups_for_user(context=context, user_id=user_id) metadata_ref = {} for x in group_refs: metadata_ref.update(self._get_metadata_ref(context, group_id=x['id'], - tenant_id=tenant_id)) + tenant_id=tenant_id, + domain_id=domain_id)) return metadata_ref - def _get_domain_metadata_ref(self, context, user_id, tenant_id): - """Return any metadata for this project due to domain grants""" - # TODO (henry-nashe) Get the domain for this tenant...and then see if - # any domain grants apply. Bug #1093248 - return {} - def _append_roles(self, metadata, additional_metadata): """ Update the roles in metadata to be the union of the roles from |