diff options
-rw-r--r-- | doc/source/configuration.rst | 15 | ||||
-rw-r--r-- | etc/keystone.conf.sample | 2 | ||||
-rw-r--r-- | keystone/catalog/backends/sql.py | 2 | ||||
-rw-r--r-- | keystone/common/cms.py | 12 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/002_mysql_downgrade.sql | 3 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/002_mysql_upgrade.sql | 2 | ||||
-rw-r--r-- | keystone/common/sql/migrate_repo/versions/002_token_id_hash.py | 43 | ||||
-rw-r--r-- | keystone/config.py | 4 | ||||
-rw-r--r-- | keystone/identity/backends/ldap/core.py | 2 | ||||
-rw-r--r-- | keystone/middleware/auth_token.py | 16 | ||||
-rw-r--r-- | keystone/service.py | 85 | ||||
-rw-r--r-- | keystone/token/backends/sql.py | 4 | ||||
-rw-r--r-- | tests/test_backend.py | 12 | ||||
-rw-r--r-- | tests/test_backend_sql.py | 34 |
14 files changed, 147 insertions, 89 deletions
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index da184641..917e8659 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -773,7 +773,7 @@ example:: Configuring the LDAP Identity Provider =========================================================== -As an alternative to the SQL Databse backing store, Keystone can use a +As an alternative to the SQL Database backing store, Keystone can use a directory server to provide the Identity service. An example Schema for openstack would look like this:: @@ -805,3 +805,16 @@ The corresponding entries in the Keystone configuration file are:: suffix = dc=openstack,dc=org user = dc=Manager,dc=openstack,dc=org password = badpassword + +The default object classes and attributes are intentionally simplistic. They +reflect the common standard objects according to the LDAP RFCs. However, +in a live deployment, the correct attributes can be overridden to support a +preexisting, more complex schema. For example, in the user object, the +objectClass posixAccount from RFC2307 is very common. If this is the +underlying objectclass, then the *uid* field should probably be *uidNumber* and +*username* field either *uid* or *cn*. To change these two fields, the +corresponding entries in the Keystone configuration file are:: + + [ldap] + user_id_attribute = uidNumber + user_name_attribute = cn diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index e98c22d1..3f37c713 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -109,11 +109,13 @@ # user_tree_dn = ou=Users,dc=example,dc=com # user_objectclass = inetOrgPerson # user_id_attribute = cn +# user_name_attribute = sn # tenant_tree_dn = ou=Groups,dc=example,dc=com # tenant_objectclass = groupOfNames # tenant_id_attribute = cn # tenant_member_attribute = member +# tenant_name_attribute = ou # role_tree_dn = ou=Roles,dc=example,dc=com # role_objectclass = organizationalRole diff --git a/keystone/catalog/backends/sql.py b/keystone/catalog/backends/sql.py index 785baeaf..c47d337a 100644 --- a/keystone/catalog/backends/sql.py +++ b/keystone/catalog/backends/sql.py @@ -127,6 +127,8 @@ class Catalog(sql.Base, catalog.Driver): session = self.get_session() endpoint_ref = session.query(Endpoint) endpoint_ref = endpoint_ref.filter_by(id=endpoint_id).first() + if not endpoint_ref: + raise exception.EndpointNotFound(endpoint_id=endpoint_id) return endpoint_ref.to_dict() def list_endpoints(self): diff --git a/keystone/common/cms.py b/keystone/common/cms.py index 1f0b8fc0..22dadfcc 100644 --- a/keystone/common/cms.py +++ b/keystone/common/cms.py @@ -2,13 +2,16 @@ import os import stat import subprocess +from keystone.common import logging + +LOG = logging.getLogger(__name__) UUID_TOKEN_LENGTH = 32 def cms_verify(formatted, signing_cert_file_name, ca_file_name): """ - verifies the signature of the contensts IAW CMS syntax + verifies the signature of the contents IAW CMS syntax """ process = subprocess.Popen(["openssl", "cms", "-verify", "-certfile", signing_cert_file_name, @@ -22,6 +25,7 @@ def cms_verify(formatted, signing_cert_file_name, ca_file_name): output, err = process.communicate(formatted) retcode = process.poll() if retcode: + LOG.error('Verify error: %s' % err) raise subprocess.CalledProcessError(retcode, "openssl", output=err) return output @@ -64,10 +68,12 @@ def cms_sign_text(text, signing_cert_file_name, signing_key_file_name): "-nosmimecap", "-nodetach", "-nocerts", "-noattr"], stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - output, unused_err = process.communicate(text) + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, err = process.communicate(text) retcode = process.poll() if retcode: + LOG.error('Signing error: %s' % err) raise subprocess.CalledProcessError(retcode, "openssl", output=output) return cms_to_token(output) diff --git a/keystone/common/sql/migrate_repo/versions/002_mysql_downgrade.sql b/keystone/common/sql/migrate_repo/versions/002_mysql_downgrade.sql deleted file mode 100644 index f1337acd..00000000 --- a/keystone/common/sql/migrate_repo/versions/002_mysql_downgrade.sql +++ /dev/null @@ -1,3 +0,0 @@ -alter table token drop id; -alter table token change id_hash id varchar(64); - diff --git a/keystone/common/sql/migrate_repo/versions/002_mysql_upgrade.sql b/keystone/common/sql/migrate_repo/versions/002_mysql_upgrade.sql deleted file mode 100644 index 1cb8d747..00000000 --- a/keystone/common/sql/migrate_repo/versions/002_mysql_upgrade.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table token change id id_hash varchar(64); -alter table token add id varchar(2048); diff --git a/keystone/common/sql/migrate_repo/versions/002_token_id_hash.py b/keystone/common/sql/migrate_repo/versions/002_token_id_hash.py new file mode 100644 index 00000000..4d38b525 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/002_token_id_hash.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + token = Table('token', meta, autoload=True) + old_id_col = token.c.id + old_id_col.alter(name='id_hash') + # Note: We obtain a new metadata reference to avoid + # sqlalchemy.exc.ArgumentError: + # Trying to redefine primary-key column 'id' as a non-primary-key... + meta = MetaData() + meta.bind = migrate_engine + token = Table('token', meta, autoload=True) + new_id = Column("id", String(2048)) + token.create_column(new_id) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + token = Table('token', meta, autoload=True) + token.drop_column('id') + token = Table('token', meta, autoload=True) + id_col = token.c.id_hash + id_col.alter(name='id') diff --git a/keystone/config.py b/keystone/config.py index 8954d36b..33f1e3ba 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -162,6 +162,8 @@ register_str('user', group='ldap', default='dc=Manager,dc=example,dc=com') register_str('password', group='ldap', default='freeipa4all') register_str('suffix', group='ldap', default='cn=example,cn=com') register_bool('use_dumb_member', group='ldap', default=False) +register_str('user_name_attribute', group='ldap', default='sn') + register_str('user_tree_dn', group='ldap', default=None) register_str('user_objectclass', group='ldap', default='inetOrgPerson') @@ -171,7 +173,7 @@ register_str('tenant_tree_dn', group='ldap', default=None) register_str('tenant_objectclass', group='ldap', default='groupOfNames') register_str('tenant_id_attribute', group='ldap', default='cn') register_str('tenant_member_attribute', group='ldap', default='member') - +register_str('tenant_name_attribute', group='ldap', default='ou') register_str('role_tree_dn', group='ldap', default=None) register_str('role_objectclass', group='ldap', default='organizationalRole') diff --git a/keystone/identity/backends/ldap/core.py b/keystone/identity/backends/ldap/core.py index 166f7f58..25aaebae 100644 --- a/keystone/identity/backends/ldap/core.py +++ b/keystone/identity/backends/ldap/core.py @@ -337,6 +337,7 @@ class UserApi(common_ldap.BaseLdap, ApiShimMixin): def __init__(self, conf): super(UserApi, self).__init__(conf) + self.attribute_mapping['name'] = conf.ldap.user_name_attribute self.api = ApiShim(conf) def get(self, id, filter=None): @@ -462,6 +463,7 @@ class TenantApi(common_ldap.BaseLdap, ApiShimMixin): def __init__(self, conf): super(TenantApi, self).__init__(conf) self.api = ApiShim(conf) + self.attribute_mapping['name'] = conf.ldap.tenant_name_attribute self.member_attribute = (getattr(conf.ldap, 'tenant_member_attribute') or self.DEFAULT_MEMBER_ATTRIBUTE) diff --git a/keystone/middleware/auth_token.py b/keystone/middleware/auth_token.py index c82e5ef5..75ab67c7 100644 --- a/keystone/middleware/auth_token.py +++ b/keystone/middleware/auth_token.py @@ -117,6 +117,10 @@ class ServiceError(Exception): pass +class ConfigurationError(Exception): + pass + + class AuthProtocol(object): """Auth Middleware that handles authenticating client calls.""" @@ -150,10 +154,14 @@ class AuthProtocol(object): self.key_file = conf.get('keyfile') #signing - self.signing_dirname = conf.get('signing_dir', '/tmp/keystone-signing') + default_signing_dir = '%s/keystone-signing' % os.environ['HOME'] + self.signing_dirname = conf.get('signing_dir', default_signing_dir) + LOG.info('Using %s as cache directory for signing certificate' % + self.signing_dirname) if (os.path.exists(self.signing_dirname) and not os.access(self.signing_dirname, os.W_OK)): - raise "TODO: Need to find an Exception to raise here." + raise ConfigurationError("unable to access signing dir %s" % + self.signing_dirname) if not os.path.exists(self.signing_dirname): os.makedirs(self.signing_dirname) @@ -565,8 +573,8 @@ class AuthProtocol(object): time=self.token_cache_time) def cert_file_missing(self, called_proc_err, file_name): - return (called_proc_err.output.find(self.signing_cert_file_name) - and not os.path.exists(self.signing_cert_file_name)) + return (called_proc_err.output.find(file_name) + and not os.path.exists(file_name)) def verify_uuid_token(self, user_token, retry=True): """Authenticate user token with keystone. diff --git a/keystone/service.py b/keystone/service.py index 2119e873..0ee34e88 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -20,6 +20,7 @@ import json from keystone import config from keystone import catalog +from keystone.common import cms from keystone.common import logging from keystone.common import wsgi from keystone import exception @@ -28,11 +29,6 @@ from keystone.openstack.common import timeutils from keystone import policy from keystone import token -from keystone.common import cms -from keystone.common import logging -from keystone.common import utils -from keystone.common import wsgi - LOG = logging.getLogger(__name__) @@ -328,7 +324,7 @@ class TokenController(wsgi.Application): raise exception.Unauthorized() except AssertionError as e: raise exception.Unauthorized(e.message) - auth_token_data = dict(zip(["user", "tenant", "metadata"], + auth_token_data = dict(zip(['user', 'tenant', 'metadata'], auth_info)) expiry = self.token_api._get_default_expire_time(context=context) @@ -340,9 +336,7 @@ class TokenController(wsgi.Application): metadata=metadata_ref) else: catalog_ref = {} - elif 'token' in auth: - old_token = auth['token'].get('id', None) tenant_name = auth.get('tenantName') @@ -351,19 +345,22 @@ class TokenController(wsgi.Application): token_id=old_token) except exception.NotFound: raise exception.Unauthorized() + user_ref = old_token_ref['user'] user_id = user_ref['id'] current_user_ref = self.identity_api.get_user(context=context, user_id=user_id) + + # If the user is disabled don't allow them to authenticate if not current_user_ref.get('enabled', True): LOG.warning('User %s is disabled' % user_id) raise exception.Unauthorized() if tenant_name: - tenant_ref = self.identity_api.\ - get_tenant_by_name(context=context, - tenant_name=tenant_name) + tenant_ref = self.identity_api.get_tenant_by_name( + context=context, + tenant_name=tenant_name) tenant_id = tenant_ref['id'] else: tenant_id = auth.get('tenantId', None) @@ -375,17 +372,17 @@ class TokenController(wsgi.Application): % (user_id, tenant_id)) raise exception.Unauthorized() - #if the old token is sufficient unpack and return it. - if (old_token_ref['tenant']) and \ - (tenant_id == old_token_ref['tenant']['id']) and\ - len(old_token) > cms.UUID_TOKEN_LENGTH: - return_data = \ - json.loads(cms.verify_token - (old_token, - config.CONF.signing.certfile, - config.CONF.signing.ca_certs)) - return_data['access']['token']['id'] = old_token - return return_data + # if the old token is sufficient unpack and return it + if (old_token_ref['tenant'] + and tenant_id == old_token_ref['tenant']['id'] + and len(old_token) > cms.UUID_TOKEN_LENGTH): + json_data = cms.verify_token( + old_token, + config.CONF.signing.certfile, + config.CONF.signing.ca_certs) + return_data = json.loads(json_data) + return_data['access']['token']['id'] = old_token + return return_data expiry = old_token_ref['expires'] try: @@ -395,7 +392,6 @@ class TokenController(wsgi.Application): tenant_ref = None metadata_ref = {} catalog_ref = {} - except exception.MetadataNotFound: metadata_ref = {} catalog_ref = {} @@ -435,29 +431,28 @@ class TokenController(wsgi.Application): if config.CONF.signing.disable_pki: token_id = uuid.uuid4().hex - signed = token_id else: - signed = cms.cms_sign_text(json.dumps(token_data), - config.CONF.signing.certfile, - config.CONF.signing.keyfile) - token_id = signed + token_id = cms.cms_sign_text(json.dumps(token_data), + config.CONF.signing.certfile, + config.CONF.signing.keyfile) + try: - token_ref = self.token_api.create_token( + self.token_api.create_token( context, token_id, dict(key=token_id, - id=signed, + id=token_id, user=user_ref, tenant=tenant_ref, metadata=metadata_ref)) - except Exception as ex: - #an identical token may have been created already. - #if so, return the token_data as it is also identical + except Exception as e: + # an identical token may have been created already. + # if so, return the token_data as it is also identical try: - exist_token = self.token_api.get_token(context=context, - token_id=token_id) + self.token_api.get_token(context=context, + token_id=token_id) except exception.TokenNotFound: - raise ex + raise e - token_data['access']['token']['id'] = signed + token_data['access']['token']['id'] = token_id return token_data @@ -468,19 +463,17 @@ class TokenController(wsgi.Application): """ # TODO(termie): this stuff should probably be moved to middleware + self.assert_admin(context) + if len(token_id) > cms.UUID_TOKEN_LENGTH: - self.assert_admin(context) data = json.loads(cms.cms_verify(cms.token_to_cms(token_id), config.CONF.signing.certfile, config.CONF.signing.ca_certs)) - access_data = data['access'] - token_ref = access_data['token'] - user_data = access_data['user'] - token_ref['metadata'] = access_data['metadata'] - token_ref['user'] = user_data + data['access']['token']['user'] = data['access']['user'] + data['access']['token']['metadata'] = data['access']['metadata'] if belongs_to: - assert token_ref['tenant']['id'] == belongs_to - token_ref['expires'] + assert data['access']['token']['tenant']['id'] == belongs_to + token_ref = data['access']['token'] else: token_ref = self.token_api.get_token(context=context, token_id=token_id) @@ -495,7 +488,7 @@ class TokenController(wsgi.Application): Identical to ``validate_token``, except does not return a response. """ - belongs_to = context['query_string'].get("belongsTo") + belongs_to = context['query_string'].get('belongsTo') assert self._get_token_ref(context, token_id, belongs_to) # admin only diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py index fd31eeef..fa0dbb76 100644 --- a/keystone/token/backends/sql.py +++ b/keystone/token/backends/sql.py @@ -17,9 +17,9 @@ import copy import datetime import hashlib -import uuid -from keystone.common import sql, cms +from keystone.common import cms +from keystone.common import sql from keystone import exception from keystone.openstack.common import timeutils from keystone import token diff --git a/tests/test_backend.py b/tests/test_backend.py index 9aace406..66d2019f 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -651,12 +651,14 @@ class TokenTests(object): class CatalogTests(object): def test_service_crud(self): new_service = { - 'id': 'MY_SERVICE', - 'type': 'myservice', - 'name': 'My Service', - 'description': 'My description' + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, } - res = self.catalog_api.create_service(new_service['id'], new_service) + res = self.catalog_api.create_service( + new_service['id'], + new_service.copy()) self.assertDictEqual(res, new_service) service_id = new_service['id'] diff --git a/tests/test_backend_sql.py b/tests/test_backend_sql.py index 9a0bcc00..ee181dee 100644 --- a/tests/test_backend_sql.py +++ b/tests/test_backend_sql.py @@ -16,6 +16,8 @@ import uuid +from keystone import catalog +from keystone.catalog.backends import sql as catalog_sql from keystone.common.sql import util as sql_util from keystone import config from keystone import exception @@ -142,25 +144,13 @@ class SqlToken(test.TestCase, test_backend.TokenTests): self.token_api = token_sql.Token() -#class SqlCatalog(test_backend_kvs.KvsCatalog): -# def setUp(self): -# super(SqlCatalog, self).setUp() -# self.catalog_api = sql.SqlCatalog() -# self._load_fixtures() - -# def _load_fixtures(self): -# self.catalog_foobar = self.catalog_api._create_catalog( -# 'foo', 'bar', -# {'RegionFoo': {'service_bar': {'foo': 'bar'}}}) - -# def test_get_catalog_bad_user(self): -# catalog_ref = self.catalog_api.get_catalog('foo' + 'WRONG', 'bar') -# self.assert_(catalog_ref is None) - -# def test_get_catalog_bad_tenant(self): -# catalog_ref = self.catalog_api.get_catalog('foo', 'bar' + 'WRONG') -# self.assert_(catalog_ref is None) - -# def test_get_catalog(self): -# catalog_ref = self.catalog_api.get_catalog('foo', 'bar') -# self.assertDictEqual(catalog_ref, self.catalog_foobar) +class SqlCatalog(test.TestCase, test_backend.CatalogTests): + def setUp(self): + super(SqlCatalog, self).setUp() + self.config([test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_sql.conf')]) + sql_util.setup_test_database() + self.catalog_api = catalog_sql.Catalog() + self.catalog_man = catalog.Manager() + self.load_fixtures(default_fixtures) |