summaryrefslogtreecommitdiffstats
path: root/keystone
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-02-12 22:16:46 +0000
committerGerrit Code Review <review@openstack.org>2013-02-12 22:16:46 +0000
commit337d2b8748f346484db410c6815c484d3dda8989 (patch)
treeb01d9d869b190e20aef95c852dc915e61d7211c6 /keystone
parent086e40101a9fb26a0330505434b447497de1c514 (diff)
parent8a89464d62e9c81a1ba15c0a3aa695456fc6fd33 (diff)
downloadkeystone-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.py50
-rw-r--r--keystone/common/models.py13
-rw-r--r--keystone/common/sql/legacy.py9
-rw-r--r--keystone/common/sql/migrate_repo/versions/008_create_default_domain.py2
-rw-r--r--keystone/common/sql/migrate_repo/versions/010_normalize_identity_migration.py35
-rw-r--r--keystone/common/sql/migrate_repo/versions/012_populate_endpoint_type.py61
-rw-r--r--keystone/common/sql/migrate_repo/versions/014_add_group_tables.py3
-rw-r--r--keystone/common/sql/migrate_repo/versions/015_tenant_to_project.py1
-rw-r--r--keystone/common/sql/migrate_repo/versions/016_normalize_domain_ids.py414
-rw-r--r--keystone/common/sql/nova.py5
-rw-r--r--keystone/config.py3
-rw-r--r--keystone/identity/backends/kvs.py80
-rw-r--r--keystone/identity/backends/ldap/core.py23
-rw-r--r--keystone/identity/backends/pam.py8
-rw-r--r--keystone/identity/backends/sql.py53
-rw-r--r--keystone/identity/controllers.py83
-rw-r--r--keystone/identity/core.py19
-rw-r--r--keystone/token/controllers.py127
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