keystone/keystone/tests/test_backend_sql.py

493 lines
19 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import uuid
import sqlalchemy
from keystone.common import sql
from keystone import config
from keystone import exception
from keystone.identity.backends import sql as identity_sql
from keystone.openstack.common.db.sqlalchemy import session
from keystone.openstack.common.fixture import moxstubout
from keystone import tests
from keystone.tests import default_fixtures
from keystone.tests import test_backend
from keystone.token.backends import sql as token_sql
CONF = config.CONF
DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
class SqlTests(tests.TestCase, sql.Base):
def setUp(self):
super(SqlTests, self).setUp()
self.config([tests.dirs.etc('keystone.conf.sample'),
tests.dirs.tests('test_overrides.conf'),
tests.dirs.tests('backend_sql.conf')])
self.load_backends()
# create tables and keep an engine reference for cleanup.
# this must be done after the models are loaded by the managers.
self.engine = session.get_engine()
self.addCleanup(session.cleanup)
sql.ModelBase.metadata.create_all(bind=self.engine)
self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine)
# populate the engine with tables & fixtures
self.load_fixtures(default_fixtures)
#defaulted by the data load
self.user_foo['enabled'] = True
class SqlModels(SqlTests):
def setUp(self):
super(SqlModels, self).setUp()
self.metadata = sql.ModelBase.metadata
self.metadata.bind = self.engine
def select_table(self, name):
table = sqlalchemy.Table(name,
self.metadata,
autoload=True)
s = sqlalchemy.select([table])
return s
def assertExpectedSchema(self, table, cols):
table = self.select_table(table)
for col, type_, length in cols:
self.assertIsInstance(table.c[col].type, type_)
if length:
self.assertEqual(table.c[col].type.length, length)
def test_user_model(self):
cols = (('id', sql.String, 64),
('name', sql.String, 255),
('password', sql.String, 128),
('domain_id', sql.String, 64),
('enabled', sql.Boolean, None),
('extra', sql.JsonBlob, None))
self.assertExpectedSchema('user', cols)
def test_group_model(self):
cols = (('id', sql.String, 64),
('name', sql.String, 64),
('description', sql.Text, None),
('domain_id', sql.String, 64),
('extra', sql.JsonBlob, None))
self.assertExpectedSchema('group', cols)
def test_domain_model(self):
cols = (('id', sql.String, 64),
('name', sql.String, 64),
('enabled', sql.Boolean, None))
self.assertExpectedSchema('domain', cols)
def test_project_model(self):
cols = (('id', sql.String, 64),
('name', sql.String, 64),
('description', sql.Text, None),
('domain_id', sql.String, 64),
('enabled', sql.Boolean, None),
('extra', sql.JsonBlob, None))
self.assertExpectedSchema('project', cols)
def test_role_model(self):
cols = (('id', sql.String, 64),
('name', sql.String, 255))
self.assertExpectedSchema('role', cols)
def test_user_project_metadata_model(self):
cols = (('user_id', sql.String, 64),
('project_id', sql.String, 64),
('data', sql.JsonBlob, None))
self.assertExpectedSchema('user_project_metadata', cols)
def test_user_domain_metadata_model(self):
cols = (('user_id', sql.String, 64),
('domain_id', sql.String, 64),
('data', sql.JsonBlob, None))
self.assertExpectedSchema('user_domain_metadata', cols)
def test_group_project_metadata_model(self):
cols = (('group_id', sql.String, 64),
('project_id', sql.String, 64),
('data', sql.JsonBlob, None))
self.assertExpectedSchema('group_project_metadata', cols)
def test_group_domain_metadata_model(self):
cols = (('group_id', sql.String, 64),
('domain_id', sql.String, 64),
('data', sql.JsonBlob, None))
self.assertExpectedSchema('group_domain_metadata', cols)
def test_user_group_membership(self):
cols = (('group_id', sql.String, 64),
('user_id', sql.String, 64))
self.assertExpectedSchema('user_group_membership', cols)
class SqlIdentity(SqlTests, test_backend.IdentityTests):
def test_password_hashed(self):
session = self.identity_api.get_session()
user_ref = self.identity_api._get_user(session, self.user_foo['id'])
self.assertNotEqual(user_ref['password'], self.user_foo['password'])
def test_delete_user_with_project_association(self):
user = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'password': uuid.uuid4().hex}
self.identity_api.create_user(user['id'], user)
self.assignment_api.add_user_to_project(self.tenant_bar['id'],
user['id'])
self.identity_api.delete_user(user['id'])
self.assertRaises(exception.UserNotFound,
self.assignment_api.list_projects_for_user,
user['id'])
def test_create_null_user_name(self):
user = {'id': uuid.uuid4().hex,
'name': None,
'domain_id': DEFAULT_DOMAIN_ID,
'password': uuid.uuid4().hex}
self.assertRaises(exception.ValidationError,
self.identity_api.create_user,
user['id'],
user)
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user,
user['id'])
self.assertRaises(exception.UserNotFound,
self.identity_api.get_user_by_name,
user['name'],
DEFAULT_DOMAIN_ID)
def test_create_null_project_name(self):
tenant = {'id': uuid.uuid4().hex,
'name': None,
'domain_id': DEFAULT_DOMAIN_ID}
self.assertRaises(exception.ValidationError,
self.assignment_api.create_project,
tenant['id'],
tenant)
self.assertRaises(exception.ProjectNotFound,
self.assignment_api.get_project,
tenant['id'])
self.assertRaises(exception.ProjectNotFound,
self.assignment_api.get_project_by_name,
tenant['name'],
DEFAULT_DOMAIN_ID)
def test_create_null_role_name(self):
role = {'id': uuid.uuid4().hex,
'name': None}
self.assertRaises(exception.Conflict,
self.assignment_api.create_role,
role['id'],
role)
self.assertRaises(exception.RoleNotFound,
self.assignment_api.get_role,
role['id'])
def test_delete_project_with_user_association(self):
user = {'id': 'fake',
'name': 'fakeuser',
'domain_id': DEFAULT_DOMAIN_ID,
'password': 'passwd'}
self.identity_api.create_user('fake', user)
self.assignment_api.add_user_to_project(self.tenant_bar['id'],
user['id'])
self.assignment_api.delete_project(self.tenant_bar['id'])
tenants = self.assignment_api.list_projects_for_user(user['id'])
self.assertEqual(tenants, [])
def test_metadata_removed_on_delete_user(self):
# A test to check that the internal representation
# or roles is correctly updated when a user is deleted
user = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'password': 'passwd'}
self.identity_api.create_user(user['id'], user)
role = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex}
self.assignment_api.create_role(role['id'], role)
self.assignment_api.add_role_to_user_and_project(
user['id'],
self.tenant_bar['id'],
role['id'])
self.identity_api.delete_user(user['id'])
# Now check whether the internal representation of roles
# has been deleted
self.assertRaises(exception.MetadataNotFound,
self.assignment_api._get_metadata,
user['id'],
self.tenant_bar['id'])
def test_metadata_removed_on_delete_project(self):
# A test to check that the internal representation
# or roles is correctly updated when a project is deleted
user = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'password': 'passwd'}
self.identity_api.create_user(user['id'], user)
role = {'id': uuid.uuid4().hex,
'name': uuid.uuid4().hex}
self.assignment_api.create_role(role['id'], role)
self.assignment_api.add_role_to_user_and_project(
user['id'],
self.tenant_bar['id'],
role['id'])
self.assignment_api.delete_project(self.tenant_bar['id'])
# Now check whether the internal representation of roles
# has been deleted
self.assertRaises(exception.MetadataNotFound,
self.assignment_api._get_metadata,
user['id'],
self.tenant_bar['id'])
def test_update_project_returns_extra(self):
"""This tests for backwards-compatibility with an essex/folsom bug.
Non-indexed attributes were returned in an 'extra' attribute, instead
of on the entity itself; for consistency and backwards compatibility,
those attributes should be included twice.
This behavior is specific to the SQL driver.
"""
tenant_id = uuid.uuid4().hex
arbitrary_key = uuid.uuid4().hex
arbitrary_value = uuid.uuid4().hex
tenant = {
'id': tenant_id,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
arbitrary_key: arbitrary_value}
ref = self.assignment_api.create_project(tenant_id, tenant)
self.assertEqual(arbitrary_value, ref[arbitrary_key])
self.assertIsNone(ref.get('extra'))
tenant['name'] = uuid.uuid4().hex
ref = self.assignment_api.update_project(tenant_id, tenant)
self.assertEqual(arbitrary_value, ref[arbitrary_key])
self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key])
def test_update_user_returns_extra(self):
"""This tests for backwards-compatibility with an essex/folsom bug.
Non-indexed attributes were returned in an 'extra' attribute, instead
of on the entity itself; for consistency and backwards compatibility,
those attributes should be included twice.
This behavior is specific to the SQL driver.
"""
user_id = uuid.uuid4().hex
arbitrary_key = uuid.uuid4().hex
arbitrary_value = uuid.uuid4().hex
user = {
'id': user_id,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'password': uuid.uuid4().hex,
arbitrary_key: arbitrary_value}
ref = self.identity_api.create_user(user_id, user)
self.assertEqual(arbitrary_value, ref[arbitrary_key])
self.assertIsNone(ref.get('password'))
self.assertIsNone(ref.get('extra'))
user['name'] = uuid.uuid4().hex
user['password'] = uuid.uuid4().hex
ref = self.identity_api.update_user(user_id, user)
self.assertIsNone(ref.get('password'))
self.assertIsNone(ref['extra'].get('password'))
self.assertEqual(arbitrary_value, ref[arbitrary_key])
self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key])
def test_sql_user_to_dict_null_default_project_id(self):
user_id = uuid.uuid4().hex
user = {
'id': user_id,
'name': uuid.uuid4().hex,
'domain_id': DEFAULT_DOMAIN_ID,
'password': uuid.uuid4().hex}
self.identity_api.create_user(user_id, user)
session = self.get_session()
query = session.query(identity_sql.User)
query = query.filter_by(id=user_id)
raw_user_ref = query.one()
self.assertIsNone(raw_user_ref.default_project_id)
user_ref = raw_user_ref.to_dict()
self.assertNotIn('default_project_id', user_ref)
session.close()
class SqlTrust(SqlTests, test_backend.TrustTests):
pass
class SqlToken(SqlTests, test_backend.TokenTests):
def test_token_revocation_list_uses_right_columns(self):
# This query used to be heavy with too many columns. We want
# to make sure it is only running with the minimum columns
# necessary.
fixture = self.useFixture(moxstubout.MoxStubout())
self.mox = fixture.mox
tok = token_sql.Token()
session = tok.get_session()
q = session.query(token_sql.TokenModel.id,
token_sql.TokenModel.expires)
self.mox.StubOutWithMock(session, 'query')
session.query(token_sql.TokenModel.id,
token_sql.TokenModel.expires).AndReturn(q)
self.mox.StubOutWithMock(tok, 'get_session')
tok.get_session().AndReturn(session)
self.mox.ReplayAll()
tok.list_revoked_tokens()
def test_flush_expired_tokens_batch(self):
# This test simply executes the code under test to verify
# that the code is legal. It is not possible to test
# whether records are deleted in batches using sqlite,
# because the limit function does not seem to affect
# delete subqueries; these are, however, legal.
# After several failed attempts of using mox, it would
# seem that the use of mock objects for testing
# the target code does not seem possible, because of
# the unique way the SQLAlchemy Query class's filter
# method works.
fixture = self.useFixture(moxstubout.MoxStubout())
self.mox = fixture.mox
tok = token_sql.Token()
self.mox.StubOutWithMock(tok, 'token_flush_batch_size')
# Just need a batch larger than 0; note that the code
# path with batch_size = 0 is covered by test_backend,
# where all backends' flush_expired_tokens methods
# are tested.
tok.token_flush_batch_size('sqlite').AndReturn(1)
self.mox.ReplayAll()
tok.flush_expired_tokens()
def test_token_flush_batch_size_default(self):
tok = token_sql.Token()
sqlite_batch = tok.token_flush_batch_size('sqlite')
self.assertEqual(sqlite_batch, 0)
def test_token_flush_batch_size_db2(self):
tok = token_sql.Token()
db2_batch = tok.token_flush_batch_size('ibm_db_sa')
self.assertEqual(db2_batch, 100)
class SqlCatalog(SqlTests, test_backend.CatalogTests):
def test_malformed_catalog_throws_error(self):
service = {
'id': uuid.uuid4().hex,
'type': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'description': uuid.uuid4().hex,
}
self.catalog_api.create_service(service['id'], service.copy())
malformed_url = "http://192.168.1.104:$(compute_port)s/v2/$(tenant)s"
endpoint = {
'id': uuid.uuid4().hex,
'region': uuid.uuid4().hex,
'service_id': service['id'],
'interface': 'public',
'url': malformed_url,
}
self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy())
self.assertRaises(exception.MalformedEndpoint,
self.catalog_api.get_catalog,
'fake-user',
'fake-tenant')
def test_get_catalog_with_empty_public_url(self):
service = {
'id': uuid.uuid4().hex,
'type': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'description': uuid.uuid4().hex,
}
self.catalog_api.create_service(service['id'], service.copy())
endpoint = {
'id': uuid.uuid4().hex,
'region': uuid.uuid4().hex,
'interface': 'public',
'url': '',
'service_id': service['id'],
}
self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy())
catalog = self.catalog_api.get_catalog('user', 'tenant')
catalog_endpoint = catalog[endpoint['region']][service['type']]
self.assertEqual(catalog_endpoint['name'], service['name'])
self.assertEqual(catalog_endpoint['id'], endpoint['id'])
self.assertEqual(catalog_endpoint['publicURL'], '')
self.assertIsNone(catalog_endpoint.get('adminURL'))
self.assertIsNone(catalog_endpoint.get('internalURL'))
def test_create_endpoint_400(self):
service = {
'id': uuid.uuid4().hex,
'type': uuid.uuid4().hex,
'name': uuid.uuid4().hex,
'description': uuid.uuid4().hex,
}
self.catalog_api.create_service(service['id'], service.copy())
endpoint = {
'id': uuid.uuid4().hex,
'region': "0" * 256,
'service_id': service['id'],
'interface': 'public',
'url': uuid.uuid4().hex,
}
self.assertRaises(exception.StringLengthExceeded,
self.catalog_api.create_endpoint,
endpoint['id'],
endpoint.copy())
class SqlPolicy(SqlTests, test_backend.PolicyTests):
pass
class SqlInheritance(SqlTests, test_backend.InheritanceTests):
pass
class SqlTokenCacheInvalidation(SqlTests, test_backend.TokenCacheInvalidation):
def setUp(self):
super(SqlTokenCacheInvalidation, self).setUp()
self._create_test_data()