diff options
Diffstat (limited to 'keystone')
80 files changed, 3711 insertions, 576 deletions
diff --git a/keystone/assignment/backends/ldap.py b/keystone/assignment/backends/ldap.py index 718d38c3..45ce6432 100644 --- a/keystone/assignment/backends/ldap.py +++ b/keystone/assignment/backends/ldap.py @@ -23,11 +23,11 @@ from keystone import assignment from keystone import clean from keystone.common import dependency from keystone.common import ldap as common_ldap -from keystone.common import logging from keystone.common import models from keystone import config from keystone import exception from keystone.identity.backends import ldap as ldap_identity +from keystone.openstack.common import log as logging CONF = config.CONF diff --git a/keystone/assignment/core.py b/keystone/assignment/core.py index 0a2ee681..d78d3485 100644 --- a/keystone/assignment/core.py +++ b/keystone/assignment/core.py @@ -17,10 +17,10 @@ """Main entry point into the assignment service.""" from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import log as logging CONF = config.CONF diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py index d1bd764f..9f6f1972 100644 --- a/keystone/auth/controllers.py +++ b/keystone/auth/controllers.py @@ -17,12 +17,12 @@ from keystone.common import controller from keystone.common import dependency -from keystone.common import logging from keystone.common import wsgi from keystone import config from keystone import exception from keystone import identity from keystone.openstack.common import importutils +from keystone.openstack.common import log as logging from keystone import token from keystone import trust @@ -285,6 +285,8 @@ class Auth(controller.V3Controller): auth_info = AuthInfo(context, auth=auth) auth_context = {'extras': {}, 'method_names': [], 'bind': {}} self.authenticate(context, auth_info, auth_context) + if auth_context.get('access_token_id'): + auth_info.set_scope(None, auth_context['project_id'], None) self._check_and_set_default_scoping(auth_info, auth_context) (domain_id, project_id, trust) = auth_info.get_scope() method_names = auth_info.get_method_names() @@ -328,7 +330,7 @@ class Auth(controller.V3Controller): def authenticate(self, context, auth_info, auth_context): """Authenticate user.""" - # user have been authenticated externally + # user has been authenticated externally if 'REMOTE_USER' in context: external = get_auth_method('external') external.authenticate(context, auth_info, auth_context) diff --git a/keystone/auth/core.py b/keystone/auth/core.py index b7bdb7c6..26e7a470 100644 --- a/keystone/auth/core.py +++ b/keystone/auth/core.py @@ -35,46 +35,52 @@ class AuthMethodHandler(object): by default. "method_names" is a list and "extras" is a dictionary. - If successful, plugin must set "user_id" in "auth_context". - "method_name" is used to convey any additional authentication methods - in case authentication is for re-scoping. For example, - if the authentication is for re-scoping, plugin must append the - previous method names into "method_names". Also, plugin may add - any additional information into "extras". Anything in "extras" - will be conveyed in the token's "extras" field. Here's an example of - "auth_context" on successful authentication. + If successful, plugin must set ``user_id`` in ``auth_context``. + ``method_name`` is used to convey any additional authentication methods + in case authentication is for re-scoping. For example, if the + authentication is for re-scoping, plugin must append the previous + method names into ``method_names``. Also, plugin may add any additional + information into ``extras``. Anything in ``extras`` will be conveyed in + the token's ``extras`` attribute. Here's an example of ``auth_context`` + on successful authentication:: - {"user_id": "abc123", - "methods": ["password", "token"], - "extras": {}} + { + "extras": {}, + "methods": [ + "password", + "token" + ], + "user_id": "abc123" + } Plugins are invoked in the order in which they are specified in the - "methods" attribute of the "identity" object. - For example, with the following authentication request, + ``methods`` attribute of the ``identity`` object. For example, + ``custom-plugin`` is invoked before ``password``, which is invoked + before ``token`` in the following authentication request:: - {"auth": { - "identity": { - "methods": ["custom-plugin", "password", "token"], - "token": { - "id": "sdfafasdfsfasfasdfds" - }, - "custom-plugin": { - "custom-data": "sdfdfsfsfsdfsf" - }, - "password": { - "user": { - "id": "s23sfad1", - "password": "secrete" + { + "auth": { + "identity": { + "custom-plugin": { + "custom-data": "sdfdfsfsfsdfsf" + }, + "methods": [ + "custom-plugin", + "password", + "token" + ], + "password": { + "user": { + "id": "s23sfad1", + "password": "secrete" + } + }, + "token": { + "id": "sdfafasdfsfasfasdfds" + } } } } - }} - - plugins will be invoked in this order: - - 1. custom-plugin - 2. password - 3. token :returns: None if authentication is successful. Authentication payload in the form of a dictionary for the diff --git a/keystone/auth/plugins/oauth1.py b/keystone/auth/plugins/oauth1.py new file mode 100644 index 00000000..ffebd365 --- /dev/null +++ b/keystone/auth/plugins/oauth1.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from keystone import auth +from keystone.common import dependency +from keystone.common import logging +from keystone.contrib import oauth1 +from keystone.contrib.oauth1 import core as oauth +from keystone import exception +from keystone.openstack.common import timeutils + + +METHOD_NAME = 'oauth1' +LOG = logging.getLogger(__name__) + + +@dependency.requires('oauth_api') +class OAuth(auth.AuthMethodHandler): + def __init__(self): + self.oauth_api = oauth1.Manager() + + def authenticate(self, context, auth_info, auth_context): + """Turn a signed request with an access key into a keystone token.""" + headers = context['headers'] + oauth_headers = oauth.get_oauth_headers(headers) + consumer_id = oauth_headers.get('oauth_consumer_key') + access_token_id = oauth_headers.get('oauth_token') + + if not access_token_id: + raise exception.ValidationError( + attribute='oauth_token', target='request') + + acc_token = self.oauth_api.get_access_token(access_token_id) + consumer = self.oauth_api._get_consumer(consumer_id) + + expires_at = acc_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Access token is expired')) + + consumer_obj = oauth1.Consumer(key=consumer['id'], + secret=consumer['secret']) + acc_token_obj = oauth1.Token(key=acc_token['id'], + secret=acc_token['access_secret']) + + url = oauth.rebuild_url(context['path']) + oauth_request = oauth1.Request.from_request( + http_method='POST', + http_url=url, + headers=context['headers'], + query_string=context['query_string']) + oauth_server = oauth1.Server() + oauth_server.add_signature_method(oauth1.SignatureMethod_HMAC_SHA1()) + params = oauth_server.verify_request(oauth_request, + consumer_obj, + token=acc_token_obj) + + if len(params) != 0: + msg = _('There should not be any non-oauth parameters') + raise exception.Unauthorized(message=msg) + + auth_context['user_id'] = acc_token['authorizing_user_id'] + auth_context['access_token_id'] = access_token_id + auth_context['project_id'] = acc_token['project_id'] diff --git a/keystone/auth/plugins/password.py b/keystone/auth/plugins/password.py index f3cfeba8..b069f4d9 100644 --- a/keystone/auth/plugins/password.py +++ b/keystone/auth/plugins/password.py @@ -15,9 +15,9 @@ # under the License. from keystone import auth -from keystone.common import logging from keystone import exception from keystone import identity +from keystone.openstack.common import log as logging METHOD_NAME = 'password' @@ -94,6 +94,7 @@ class UserAuthInfo(object): self._assert_user_is_enabled(user_ref) self.user_ref = user_ref self.user_id = user_ref['id'] + self.domain_id = domain_ref['id'] class Password(auth.AuthMethodHandler): @@ -106,7 +107,8 @@ class Password(auth.AuthMethodHandler): try: self.identity_api.authenticate( user_id=user_info.user_id, - password=user_info.password) + password=user_info.password, + domain_scope=user_info.domain_id) except AssertionError: # authentication failed because of invalid username or password msg = _('Invalid username or password') diff --git a/keystone/auth/plugins/token.py b/keystone/auth/plugins/token.py index 720eccac..bc7cb1ba 100644 --- a/keystone/auth/plugins/token.py +++ b/keystone/auth/plugins/token.py @@ -15,9 +15,9 @@ # under the License. from keystone import auth -from keystone.common import logging from keystone.common import wsgi from keystone import exception +from keystone.openstack.common import log as logging from keystone import token @@ -37,6 +37,12 @@ class Token(auth.AuthMethodHandler): target=METHOD_NAME) token_id = auth_payload['id'] token_ref = self.token_api.get_token(token_id) + if ('OS-TRUST:trust' in token_ref['token_data']['token'] or + 'trust' in token_ref['token_data']['token']): + raise exception.Forbidden() + if 'OS-OAUTH1' in token_ref['token_data']['token']: + raise exception.Forbidden() + wsgi.validate_token_bind(context, token_ref) user_context.setdefault( 'user_id', token_ref['token_data']['token']['user']['id']) @@ -48,9 +54,6 @@ class Token(auth.AuthMethodHandler): token_ref['token_data']['token']['extras']) user_context['method_names'].extend( token_ref['token_data']['token']['methods']) - if ('OS-TRUST:trust' in token_ref['token_data']['token'] or - 'trust' in token_ref['token_data']['token']): - raise exception.Forbidden() except AssertionError as e: LOG.error(e) raise exception.Unauthorized(e) diff --git a/keystone/catalog/backends/templated.py b/keystone/catalog/backends/templated.py index a96902d3..db99110b 100644 --- a/keystone/catalog/backends/templated.py +++ b/keystone/catalog/backends/templated.py @@ -18,16 +18,13 @@ import os.path from keystone.catalog.backends import kvs from keystone.catalog import core -from keystone.common import logging from keystone import config +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) CONF = config.CONF -config.register_str('template_file', - default='default_catalog.templates', - group='catalog') def parse_templates(template_lines): diff --git a/keystone/catalog/core.py b/keystone/catalog/core.py index b8a081ac..61b7e8ac 100644 --- a/keystone/catalog/core.py +++ b/keystone/catalog/core.py @@ -18,10 +18,10 @@ """Main entry point into the Catalog service.""" from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import log as logging CONF = config.CONF diff --git a/keystone/clean.py b/keystone/clean.py index c1d01ec8..cb6c69c0 100644 --- a/keystone/clean.py +++ b/keystone/clean.py @@ -23,18 +23,23 @@ def check_length(property_name, value, min_length=1, max_length=64): msg = _("%s cannot be empty.") % property_name else: msg = (_("%(property_name)s cannot be less than " - "%(min_length)s characters.")) % locals() + "%(min_length)s characters.") % dict( + property_name=property_name, min_length=min_length)) raise exception.ValidationError(msg) if len(value) > max_length: msg = (_("%(property_name)s should not be greater than " - "%(max_length)s characters.")) % locals() + "%(max_length)s characters.") % dict( + property_name=property_name, max_length=max_length)) + raise exception.ValidationError(msg) def check_type(property_name, value, expected_type, display_expected_type): if not isinstance(value, expected_type): - msg = _("%(property_name)s is not a " - "%(display_expected_type)s") % locals() + msg = (_("%(property_name)s is not a " + "%(display_expected_type)s") % dict( + property_name=property_name, + display_expected_type=display_expected_type)) raise exception.ValidationError(msg) @@ -44,10 +49,11 @@ def check_enabled(property_name, enabled): return bool(enabled) -def check_name(property_name, name): +def check_name(property_name, name, min_length=1, max_length=64): check_type('%s name' % property_name, name, basestring, 'str or unicode') name = name.strip() - check_length('%s name' % property_name, name) + check_length('%s name' % property_name, name, + min_length=min_length, max_length=max_length) return name @@ -64,7 +70,7 @@ def project_enabled(enabled): def user_name(name): - return check_name('User', name) + return check_name('User', name, max_length=255) def user_enabled(enabled): diff --git a/keystone/cli.py b/keystone/cli.py index 18c095ce..6575f2e9 100644 --- a/keystone/cli.py +++ b/keystone/cli.py @@ -79,7 +79,7 @@ class DbSync(BaseApp): package = importutils.import_module(package_name) repo_path = os.path.abspath(os.path.dirname(package.__file__)) except ImportError: - print _("This extension does not provide migrations.") + print(_("This extension does not provide migrations.")) exit(0) try: # Register the repo with the version control API @@ -115,7 +115,7 @@ class DbVersion(BaseApp): repo_path = os.path.abspath(os.path.dirname(package.__file__)) print(migration.db_version(repo_path)) except ImportError: - print _("This extension does not provide migrations.") + print(_("This extension does not provide migrations.")) exit(1) else: print(migration.db_version()) diff --git a/keystone/common/cms.py b/keystone/common/cms.py index 6ec740f8..09a98cdc 100644 --- a/keystone/common/cms.py +++ b/keystone/common/cms.py @@ -1,7 +1,7 @@ import hashlib from keystone.common import environment -from keystone.common import logging +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) diff --git a/keystone/common/config.py b/keystone/common/config.py index 5a961d4a..34ab0988 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -24,6 +24,223 @@ _DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" _DEFAULT_AUTH_METHODS = ['external', 'password', 'token'] +FILE_OPTIONS = { + '': [ + cfg.StrOpt('admin_token', secret=True, default='ADMIN'), + cfg.StrOpt('bind_host', default='0.0.0.0'), + cfg.IntOpt('compute_port', default=8774), + cfg.IntOpt('admin_port', default=35357), + cfg.IntOpt('public_port', default=5000), + cfg.StrOpt('public_endpoint', + default='http://localhost:%(public_port)s/'), + cfg.StrOpt('admin_endpoint', + default='http://localhost:%(admin_port)s/'), + cfg.StrOpt('onready'), + cfg.StrOpt('auth_admin_prefix', default=''), + cfg.StrOpt('policy_file', default='policy.json'), + cfg.StrOpt('policy_default_rule', default=None), + # default max request size is 112k + cfg.IntOpt('max_request_body_size', default=114688), + cfg.IntOpt('max_param_size', default=64), + # we allow tokens to be a bit larger to accommodate PKI + cfg.IntOpt('max_token_size', default=8192), + cfg.StrOpt('member_role_id', + default='9fe2ff9ee4384b1894a90878d3e92bab'), + cfg.StrOpt('member_role_name', default='_member_'), + cfg.IntOpt('crypt_strength', default=40000)], + 'identity': [ + cfg.StrOpt('default_domain_id', default='default'), + cfg.BoolOpt('domain_specific_drivers_enabled', + default=False), + cfg.StrOpt('domain_config_dir', + default='/etc/keystone/domains'), + cfg.StrOpt('driver', + default=('keystone.identity.backends' + '.sql.Identity')), + cfg.IntOpt('max_password_length', default=4096)], + 'trust': [ + cfg.BoolOpt('enabled', default=True), + cfg.StrOpt('driver', + default='keystone.trust.backends.sql.Trust')], + 'os_inherit': [ + cfg.BoolOpt('enabled', default=False)], + 'token': [ + cfg.ListOpt('bind', default=[]), + cfg.StrOpt('enforce_token_bind', default='permissive'), + cfg.IntOpt('expiration', default=86400), + cfg.StrOpt('provider', default=None), + cfg.StrOpt('driver', + default='keystone.token.backends.sql.Token')], + 'ssl': [ + cfg.BoolOpt('enable', default=False), + cfg.StrOpt('certfile', + default="/etc/keystone/ssl/certs/keystone.pem"), + cfg.StrOpt('keyfile', + default="/etc/keystone/ssl/private/keystonekey.pem"), + cfg.StrOpt('ca_certs', + default="/etc/keystone/ssl/certs/ca.pem"), + cfg.StrOpt('ca_key', + default="/etc/keystone/ssl/certs/cakey.pem"), + cfg.BoolOpt('cert_required', default=False), + cfg.IntOpt('key_size', default=1024), + cfg.IntOpt('valid_days', default=3650), + cfg.StrOpt('ca_password', default=None), + cfg.StrOpt('cert_subject', + default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost')], + 'signing': [ + cfg.StrOpt('token_format', default=None), + cfg.StrOpt('certfile', + default="/etc/keystone/ssl/certs/signing_cert.pem"), + cfg.StrOpt('keyfile', + default="/etc/keystone/ssl/private/signing_key.pem"), + cfg.StrOpt('ca_certs', + default="/etc/keystone/ssl/certs/ca.pem"), + cfg.StrOpt('ca_key', + default="/etc/keystone/ssl/certs/cakey.pem"), + cfg.IntOpt('key_size', default=2048), + cfg.IntOpt('valid_days', default=3650), + cfg.StrOpt('ca_password', default=None), + cfg.StrOpt('cert_subject', + default=('/C=US/ST=Unset/L=Unset/O=Unset/' + 'CN=www.example.com'))], + 'sql': [ + cfg.StrOpt('connection', secret=True, + default='sqlite:///keystone.db'), + cfg.IntOpt('idle_timeout', default=200)], + 'assignment': [ + # assignment has no default for backward compatibility reasons. + # If assignment driver is not specified, the identity driver chooses + # the backend + cfg.StrOpt('driver', default=None)], + 'credential': [ + cfg.StrOpt('driver', + default=('keystone.credential.backends' + '.sql.Credential'))], + 'oauth1': [ + cfg.StrOpt('driver', + default='keystone.contrib.oauth1.backends.sql.OAuth1'), + cfg.IntOpt('request_token_duration', default=28800), + cfg.IntOpt('access_token_duration', default=86400)], + 'policy': [ + cfg.StrOpt('driver', + default='keystone.policy.backends.sql.Policy')], + 'ec2': [ + cfg.StrOpt('driver', + default='keystone.contrib.ec2.backends.kvs.Ec2')], + 'stats': [ + cfg.StrOpt('driver', + default=('keystone.contrib.stats.backends' + '.kvs.Stats'))], + 'ldap': [ + cfg.StrOpt('url', default='ldap://localhost'), + cfg.StrOpt('user', default=None), + cfg.StrOpt('password', secret=True, default=None), + cfg.StrOpt('suffix', default='cn=example,cn=com'), + cfg.BoolOpt('use_dumb_member', default=False), + cfg.StrOpt('dumb_member', default='cn=dumb,dc=nonexistent'), + cfg.BoolOpt('allow_subtree_delete', default=False), + cfg.StrOpt('query_scope', default='one'), + cfg.IntOpt('page_size', default=0), + cfg.StrOpt('alias_dereferencing', default='default'), + + cfg.StrOpt('user_tree_dn', default=None), + cfg.StrOpt('user_filter', default=None), + cfg.StrOpt('user_objectclass', default='inetOrgPerson'), + cfg.StrOpt('user_id_attribute', default='cn'), + cfg.StrOpt('user_name_attribute', default='sn'), + cfg.StrOpt('user_mail_attribute', default='email'), + cfg.StrOpt('user_pass_attribute', default='userPassword'), + cfg.StrOpt('user_enabled_attribute', default='enabled'), + cfg.StrOpt('user_domain_id_attribute', + default='businessCategory'), + cfg.IntOpt('user_enabled_mask', default=0), + cfg.StrOpt('user_enabled_default', default='True'), + cfg.ListOpt('user_attribute_ignore', + default='tenant_id,tenants'), + cfg.BoolOpt('user_allow_create', default=True), + cfg.BoolOpt('user_allow_update', default=True), + cfg.BoolOpt('user_allow_delete', default=True), + cfg.BoolOpt('user_enabled_emulation', default=False), + cfg.StrOpt('user_enabled_emulation_dn', default=None), + cfg.ListOpt('user_additional_attribute_mapping', + default=None), + + cfg.StrOpt('tenant_tree_dn', default=None), + cfg.StrOpt('tenant_filter', default=None), + cfg.StrOpt('tenant_objectclass', default='groupOfNames'), + cfg.StrOpt('tenant_id_attribute', default='cn'), + cfg.StrOpt('tenant_member_attribute', default='member'), + cfg.StrOpt('tenant_name_attribute', default='ou'), + cfg.StrOpt('tenant_desc_attribute', default='description'), + cfg.StrOpt('tenant_enabled_attribute', default='enabled'), + cfg.StrOpt('tenant_domain_id_attribute', + default='businessCategory'), + cfg.ListOpt('tenant_attribute_ignore', default=''), + cfg.BoolOpt('tenant_allow_create', default=True), + cfg.BoolOpt('tenant_allow_update', default=True), + cfg.BoolOpt('tenant_allow_delete', default=True), + cfg.BoolOpt('tenant_enabled_emulation', default=False), + cfg.StrOpt('tenant_enabled_emulation_dn', default=None), + cfg.ListOpt('tenant_additional_attribute_mapping', + default=None), + + cfg.StrOpt('role_tree_dn', default=None), + cfg.StrOpt('role_filter', default=None), + cfg.StrOpt('role_objectclass', default='organizationalRole'), + cfg.StrOpt('role_id_attribute', default='cn'), + cfg.StrOpt('role_name_attribute', default='ou'), + cfg.StrOpt('role_member_attribute', default='roleOccupant'), + cfg.ListOpt('role_attribute_ignore', default=''), + cfg.BoolOpt('role_allow_create', default=True), + cfg.BoolOpt('role_allow_update', default=True), + cfg.BoolOpt('role_allow_delete', default=True), + cfg.ListOpt('role_additional_attribute_mapping', + default=None), + + cfg.StrOpt('group_tree_dn', default=None), + cfg.StrOpt('group_filter', default=None), + cfg.StrOpt('group_objectclass', default='groupOfNames'), + cfg.StrOpt('group_id_attribute', default='cn'), + cfg.StrOpt('group_name_attribute', default='ou'), + cfg.StrOpt('group_member_attribute', default='member'), + cfg.StrOpt('group_desc_attribute', default='description'), + cfg.StrOpt('group_domain_id_attribute', + default='businessCategory'), + cfg.ListOpt('group_attribute_ignore', default=''), + cfg.BoolOpt('group_allow_create', default=True), + cfg.BoolOpt('group_allow_update', default=True), + cfg.BoolOpt('group_allow_delete', default=True), + cfg.ListOpt('group_additional_attribute_mapping', + default=None), + + cfg.StrOpt('tls_cacertfile', default=None), + cfg.StrOpt('tls_cacertdir', default=None), + cfg.BoolOpt('use_tls', default=False), + cfg.StrOpt('tls_req_cert', default='demand')], + 'pam': [ + cfg.StrOpt('userid', default=None), + cfg.StrOpt('password', default=None)], + 'auth': [ + cfg.ListOpt('methods', default=_DEFAULT_AUTH_METHODS), + cfg.StrOpt('password', + default='keystone.auth.plugins.token.Token'), + cfg.StrOpt('token', + default='keystone.auth.plugins.password.Password'), + #deals with REMOTE_USER authentication + cfg.StrOpt('external', + default='keystone.auth.plugins.external.ExternalDefault')], + 'paste_deploy': [ + cfg.StrOpt('config_file', default=None)], + 'memcache': [ + cfg.StrOpt('servers', default='localhost:11211'), + cfg.IntOpt('max_compare_and_set_retry', default=16)], + 'catalog': [ + cfg.StrOpt('template_file', + default='default_catalog.templates'), + cfg.StrOpt('driver', + default='keystone.catalog.backends.sql.Catalog')]} + + CONF = cfg.CONF @@ -40,297 +257,35 @@ def setup_logging(conf, product_name='keystone'): logging.setup(product_name) -def setup_authentication(): +def setup_authentication(conf=None): # register any non-default auth methods here (used by extensions, etc) - for method_name in CONF.auth.methods: + if conf is None: + conf = CONF + for method_name in conf.auth.methods: if method_name not in _DEFAULT_AUTH_METHODS: - register_str(method_name, group="auth") - - -def register_str(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.StrOpt(*args, **kw), group=group) - - -def register_cli_str(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.StrOpt(*args, **kw), group=group) - - -def register_list(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.ListOpt(*args, **kw), group=group) - - -def register_cli_list(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.ListOpt(*args, **kw), group=group) - - -def register_bool(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.BoolOpt(*args, **kw), group=group) - - -def register_cli_bool(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.BoolOpt(*args, **kw), group=group) - - -def register_int(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_opt(cfg.IntOpt(*args, **kw), group=group) - - -def register_cli_int(*args, **kw): - conf = kw.pop('conf', CONF) - group = kw.pop('group', None) - return conf.register_cli_opt(cfg.IntOpt(*args, **kw), group=group) - - -def configure(): - register_cli_bool('standard-threads', default=False, - help='Do not monkey-patch threading system modules.') + conf.register_opt(cfg.StrOpt(method_name), group='auth') + + +def configure(conf=None): + if conf is None: + conf = CONF + + conf.register_cli_opt( + cfg.BoolOpt('standard-threads', default=False, + help='Do not monkey-patch threading system modules.')) + conf.register_cli_opt( + cfg.StrOpt('pydev-debug-host', default=None, + help='Host to connect to for remote debugger.')) + conf.register_cli_opt( + cfg.IntOpt('pydev-debug-port', default=None, + help='Port to connect to for remote debugger.')) + + for section in FILE_OPTIONS: + for option in FILE_OPTIONS[section]: + if section: + conf.register_opt(option, group=section) + else: + conf.register_opt(option) - register_cli_str('pydev-debug-host', default=None, - help='Host to connect to for remote debugger.') - register_cli_int('pydev-debug-port', default=None, - help='Port to connect to for remote debugger.') - - register_str('admin_token', secret=True, default='ADMIN') - register_str('bind_host', default='0.0.0.0') - register_int('compute_port', default=8774) - register_int('admin_port', default=35357) - register_int('public_port', default=5000) - register_str( - 'public_endpoint', default='http://localhost:%(public_port)s/') - register_str('admin_endpoint', default='http://localhost:%(admin_port)s/') - register_str('onready') - register_str('auth_admin_prefix', default='') - register_str('policy_file', default='policy.json') - register_str('policy_default_rule', default=None) - # default max request size is 112k - register_int('max_request_body_size', default=114688) - register_int('max_param_size', default=64) - # we allow tokens to be a bit larger to accommodate PKI - register_int('max_token_size', default=8192) - register_str( - 'member_role_id', default='9fe2ff9ee4384b1894a90878d3e92bab') - register_str('member_role_name', default='_member_') - - # identity - register_str('default_domain_id', group='identity', default='default') - register_int('max_password_length', group='identity', default=4096) - - # trust - register_bool('enabled', group='trust', default=True) - - # os_inherit - register_bool('enabled', group='os_inherit', default=False) - - # binding - register_list('bind', group='token', default=[]) - register_str('enforce_token_bind', group='token', default='permissive') - - # ssl - register_bool('enable', group='ssl', default=False) - register_str('certfile', group='ssl', - default="/etc/keystone/ssl/certs/keystone.pem") - register_str('keyfile', group='ssl', - default="/etc/keystone/ssl/private/keystonekey.pem") - register_str('ca_certs', group='ssl', - default="/etc/keystone/ssl/certs/ca.pem") - register_str('ca_key', group='ssl', - default="/etc/keystone/ssl/certs/cakey.pem") - register_bool('cert_required', group='ssl', default=False) - register_int('key_size', group='ssl', default=1024) - register_int('valid_days', group='ssl', default=3650) - register_str('ca_password', group='ssl', default=None) - register_str('cert_subject', group='ssl', - default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost') - - # signing - register_str( - 'token_format', group='signing', default=None) - register_str( - 'certfile', - group='signing', - default="/etc/keystone/ssl/certs/signing_cert.pem") - register_str( - 'keyfile', - group='signing', - default="/etc/keystone/ssl/private/signing_key.pem") - register_str( - 'ca_certs', - group='signing', - default="/etc/keystone/ssl/certs/ca.pem") - register_str('ca_key', group='signing', - default="/etc/keystone/ssl/certs/cakey.pem") - register_int('key_size', group='signing', default=2048) - register_int('valid_days', group='signing', default=3650) - register_str('ca_password', group='signing', default=None) - register_str('cert_subject', group='signing', - default='/C=US/ST=Unset/L=Unset/O=Unset/CN=www.example.com') - - # sql - register_str('connection', group='sql', secret=True, - default='sqlite:///keystone.db') - register_int('idle_timeout', group='sql', default=200) - - #assignment has no default for backward compatibility reasons. - #If assignment is not specified, the identity driver chooses the backend - register_str( - 'driver', - group='assignment', - default=None) - register_str( - 'driver', - group='catalog', - default='keystone.catalog.backends.sql.Catalog') - register_str( - 'driver', - group='identity', - default='keystone.identity.backends.sql.Identity') - register_str( - 'driver', - group='credential', - default='keystone.credential.backends.sql.Credential') - register_str( - 'driver', - group='policy', - default='keystone.policy.backends.sql.Policy') - register_str( - 'driver', group='token', default='keystone.token.backends.sql.Token') - register_str( - 'driver', group='trust', default='keystone.trust.backends.sql.Trust') - register_str( - 'driver', group='ec2', default='keystone.contrib.ec2.backends.kvs.Ec2') - register_str( - 'driver', - group='stats', - default='keystone.contrib.stats.backends.kvs.Stats') - - # ldap - register_str('url', group='ldap', default='ldap://localhost') - register_str('user', group='ldap', default=None) - register_str('password', group='ldap', secret=True, default=None) - register_str('suffix', group='ldap', default='cn=example,cn=com') - register_bool('use_dumb_member', group='ldap', default=False) - register_str('dumb_member', group='ldap', default='cn=dumb,dc=nonexistent') - register_bool('allow_subtree_delete', group='ldap', default=False) - register_str('query_scope', group='ldap', default='one') - register_int('page_size', group='ldap', default=0) - register_str('alias_dereferencing', group='ldap', default='default') - - register_str('user_tree_dn', group='ldap', default=None) - register_str('user_filter', group='ldap', default=None) - register_str('user_objectclass', group='ldap', default='inetOrgPerson') - register_str('user_id_attribute', group='ldap', default='cn') - 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='businessCategory') - 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', default='tenant_id,tenants') - register_bool('user_allow_create', group='ldap', default=True) - register_bool('user_allow_update', group='ldap', default=True) - register_bool('user_allow_delete', group='ldap', default=True) - register_bool('user_enabled_emulation', group='ldap', default=False) - register_str('user_enabled_emulation_dn', group='ldap', default=None) - register_list( - 'user_additional_attribute_mapping', group='ldap', default=None) - - register_str('tenant_tree_dn', group='ldap', default=None) - register_str('tenant_filter', 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('tenant_desc_attribute', group='ldap', default='description') - register_str('tenant_enabled_attribute', group='ldap', default='enabled') - register_str( - 'tenant_domain_id_attribute', group='ldap', default='businessCategory') - 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) - register_bool('tenant_allow_delete', group='ldap', default=True) - register_bool('tenant_enabled_emulation', group='ldap', default=False) - register_str('tenant_enabled_emulation_dn', group='ldap', default=None) - register_list( - 'tenant_additional_attribute_mapping', group='ldap', default=None) - - register_str('role_tree_dn', group='ldap', default=None) - register_str('role_filter', group='ldap', default=None) - register_str( - 'role_objectclass', group='ldap', default='organizationalRole') - register_str('role_id_attribute', group='ldap', default='cn') - register_str('role_name_attribute', group='ldap', default='ou') - register_str('role_member_attribute', group='ldap', default='roleOccupant') - register_list('role_attribute_ignore', group='ldap', default='') - register_bool('role_allow_create', group='ldap', default=True) - register_bool('role_allow_update', group='ldap', default=True) - register_bool('role_allow_delete', group='ldap', default=True) - register_list( - 'role_additional_attribute_mapping', group='ldap', default=None) - - register_str('group_tree_dn', group='ldap', default=None) - register_str('group_filter', group='ldap', default=None) - register_str('group_objectclass', group='ldap', default='groupOfNames') - 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='description') - register_str( - 'group_domain_id_attribute', group='ldap', default='businessCategory') - 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) - register_bool('group_allow_delete', group='ldap', default=True) - register_list( - 'group_additional_attribute_mapping', group='ldap', default=None) - - register_str('tls_cacertfile', group='ldap', default=None) - register_str('tls_cacertdir', group='ldap', default=None) - register_bool('use_tls', group='ldap', default=False) - register_str('tls_req_cert', group='ldap', default='demand') - - # pam - register_str('userid', group='pam', default=None) - register_str('password', group='pam', default=None) - - # default authentication methods - register_list('methods', group='auth', default=_DEFAULT_AUTH_METHODS) - register_str( - 'password', group='auth', default='keystone.auth.plugins.token.Token') - register_str( - 'token', group='auth', - default='keystone.auth.plugins.password.Password') - #deals with REMOTE_USER authentication - register_str( - 'external', - group='auth', - default='keystone.auth.plugins.external.ExternalDefault') # register any non-default auth methods here (used by extensions, etc) - for method_name in CONF.auth.methods: - if method_name not in _DEFAULT_AUTH_METHODS: - register_str(method_name, group='auth') - - # PasteDeploy config file - register_str('config_file', group='paste_deploy', default=None) - - # token provider - register_str( - 'provider', - group='token', - default=None) + setup_authentication(conf) diff --git a/keystone/common/controller.py b/keystone/common/controller.py index affc34de..90818fb4 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -3,11 +3,10 @@ import functools import uuid from keystone.common import dependency -from keystone.common import logging from keystone.common import wsgi from keystone import config from keystone import exception - +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) CONF = config.CONF @@ -169,6 +168,10 @@ class V2Controller(wsgi.Application): self._delete_tokens_for_trust(trust['trustee_user_id'], trust['id']) + def _delete_tokens_for_project(self, project_id): + for user_ref in self.identity_api.get_project_users(project_id): + self._delete_tokens_for_user(user_ref['id'], project_id=project_id) + def _require_attribute(self, ref, attr): """Ensures the reference contains the specified attribute.""" if ref.get(attr) is None or ref.get(attr) == '': @@ -300,34 +303,35 @@ class V3Controller(V2Controller): ref['id'] = uuid.uuid4().hex return ref + def _get_domain_id_for_request(self, context): + """Get the domain_id for a v3 call.""" + + if context['is_admin']: + return DEFAULT_DOMAIN_ID + + # 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, this optimization is probably not + # worth the duplication of state + try: + token_ref = self.token_api.get_token( + token_id=context['token_id']) + except exception.TokenNotFound: + LOG.warning(_('Invalid token in _get_domain_id_for_request')) + raise exception.Unauthorized() + + if 'domain' in token_ref: + return token_ref['domain']['id'] + else: + return DEFAULT_DOMAIN_ID + 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( - 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 + ref['domain_id'] = self._get_domain_id_for_request(context) return ref def _filter_domain_id(self, ref): diff --git a/keystone/common/environment/__init__.py b/keystone/common/environment/__init__.py index 2993536a..7ec82002 100644 --- a/keystone/common/environment/__init__.py +++ b/keystone/common/environment/__init__.py @@ -2,7 +2,7 @@ import functools import os from keystone.common import config -from keystone.common import logging +from keystone.openstack.common import log as logging CONF = config.CONF LOG = logging.getLogger(__name__) diff --git a/keystone/common/environment/eventlet_server.py b/keystone/common/environment/eventlet_server.py index 18987d26..874c4831 100644 --- a/keystone/common/environment/eventlet_server.py +++ b/keystone/common/environment/eventlet_server.py @@ -26,8 +26,7 @@ import eventlet import eventlet.wsgi import greenlet -from keystone.common import logging -from keystone.common import wsgi +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) @@ -48,10 +47,10 @@ class Server(object): def start(self, key=None, backlog=128): """Run a WSGI server with the given application.""" - LOG.debug(_('Starting %(arg0)s on %(host)s:%(port)s') % - {'arg0': sys.argv[0], - 'host': self.host, - 'port': self.port}) + LOG.info(_('Starting %(arg0)s on %(host)s:%(port)s') % + {'arg0': sys.argv[0], + 'host': self.host, + 'port': self.port}) # TODO(dims): eventlet's green dns/socket module does not actually # support IPv6 in getaddrinfo(). We need to get around this in the @@ -108,7 +107,7 @@ class Server(object): log = logging.getLogger('eventlet.wsgi.server') try: eventlet.wsgi.server(socket, application, custom_pool=self.pool, - log=wsgi.WritableLogger(log)) + log=logging.WritableLogger(log)) except Exception: LOG.exception(_('Server error')) raise diff --git a/keystone/common/ldap/core.py b/keystone/common/ldap/core.py index 39ea78de..48e4121f 100644 --- a/keystone/common/ldap/core.py +++ b/keystone/common/ldap/core.py @@ -20,9 +20,8 @@ import ldap from ldap import filter as ldap_filter from keystone.common.ldap import fakeldap -from keystone.common import logging from keystone import exception - +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) @@ -509,7 +508,7 @@ class LdapWrapper(object): def add_s(self, dn, attrs): ldap_attrs = [(kind, [py2ldap(x) for x in safe_iter(values)]) for kind, values in attrs] - if LOG.isEnabledFor(logging.DEBUG): + if LOG.isEnabledFor(LOG.debug): sane_attrs = [(kind, values if kind != 'userPassword' else ['****']) @@ -519,7 +518,7 @@ class LdapWrapper(object): return self.conn.add_s(dn, ldap_attrs) def search_s(self, dn, scope, query, attrlist=None): - if LOG.isEnabledFor(logging.DEBUG): + if LOG.isEnabledFor(LOG.debug): LOG.debug(_( 'LDAP search: dn=%(dn)s, scope=%(scope)s, query=%(query)s, ' 'attrs=%(attrlist)s') % { @@ -586,7 +585,7 @@ class LdapWrapper(object): else [py2ldap(x) for x in safe_iter(values)])) for op, kind, values in modlist] - if LOG.isEnabledFor(logging.DEBUG): + if LOG.isEnabledFor(LOG.debug): sane_modlist = [(op, kind, (values if kind != 'userPassword' else ['****'])) for op, kind, values in ldap_modlist] diff --git a/keystone/common/ldap/fakeldap.py b/keystone/common/ldap/fakeldap.py index f6c95895..e4458874 100644 --- a/keystone/common/ldap/fakeldap.py +++ b/keystone/common/ldap/fakeldap.py @@ -29,8 +29,8 @@ import shelve import ldap -from keystone.common import logging from keystone.common import utils +from keystone.openstack.common import log as logging SCOPE_NAMES = { @@ -41,8 +41,6 @@ SCOPE_NAMES = { LOG = logging.getLogger(__name__) -#Only enable a lower level than WARN if you are actively debugging -LOG.level = logging.WARN def _match_query(query, attrs): @@ -125,18 +123,14 @@ server_fail = False class FakeShelve(dict): - @classmethod - def get_instance(cls): - try: - return cls.__instance - except AttributeError: - cls.__instance = cls() - return cls.__instance def sync(self): pass +FakeShelves = {} + + class FakeLdap(object): """Fake LDAP connection.""" @@ -144,8 +138,10 @@ class FakeLdap(object): def __init__(self, url): LOG.debug(_('FakeLdap initialize url=%s'), url) - if url == 'fake://memory': - self.db = FakeShelve.get_instance() + if url.startswith('fake://memory'): + if url not in FakeShelves: + FakeShelves[url] = FakeShelve() + self.db = FakeShelves[url] else: self.db = shelve.open(url[7:]) diff --git a/keystone/common/openssl.py b/keystone/common/openssl.py index 90484505..280815ae 100644 --- a/keystone/common/openssl.py +++ b/keystone/common/openssl.py @@ -19,9 +19,8 @@ import os import stat from keystone.common import environment -from keystone.common import logging from keystone import config - +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) CONF = config.CONF diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py index 67863588..fdb45c74 100644 --- a/keystone/common/sql/core.py +++ b/keystone/common/sql/core.py @@ -26,10 +26,10 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute import sqlalchemy.pool from sqlalchemy import types as sql_types -from keystone.common import logging from keystone import config from keystone import exception from keystone.openstack.common import jsonutils +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) diff --git a/keystone/common/sql/legacy.py b/keystone/common/sql/legacy.py index c8adc900..d88e5a46 100644 --- a/keystone/common/sql/legacy.py +++ b/keystone/common/sql/legacy.py @@ -21,10 +21,10 @@ from sqlalchemy import exc from keystone.assignment.backends import sql as assignment_sql -from keystone.common import logging from keystone import config from keystone.contrib.ec2.backends import sql as ec2_sql from keystone.identity.backends import sql as identity_sql +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) diff --git a/keystone/common/sql/migrate_repo/versions/032_username_length.py b/keystone/common/sql/migrate_repo/versions/032_username_length.py new file mode 100644 index 00000000..636ebd75 --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/032_username_length.py @@ -0,0 +1,31 @@ +import sqlalchemy as sql +from sqlalchemy.orm import sessionmaker + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + user_table = sql.Table('user', meta, autoload=True) + user_table.c.name.alter(type=sql.String(255)) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + user_table = sql.Table('user', meta, autoload=True) + if migrate_engine.name != 'mysql': + # NOTE(aloga): sqlite does not enforce length on the + # VARCHAR types: http://www.sqlite.org/faq.html#q9 + # postgresql and DB2 do not truncate. + maker = sessionmaker(bind=migrate_engine) + session = maker() + for user in session.query(user_table).all(): + values = {'name': user.name[:64]} + update = (user_table.update(). + where(user_table.c.id == user.id). + values(values)) + migrate_engine.execute(update) + + session.commit() + session.close() + user_table.c.name.alter(type=sql.String(64)) diff --git a/keystone/common/sql/nova.py b/keystone/common/sql/nova.py index fd8d2481..c7abfb81 100644 --- a/keystone/common/sql/nova.py +++ b/keystone/common/sql/nova.py @@ -19,10 +19,10 @@ import uuid from keystone import assignment -from keystone.common import logging from keystone import config from keystone.contrib.ec2.backends import sql as ec2_sql from keystone import identity +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) diff --git a/keystone/common/utils.py b/keystone/common/utils.py index 9966ee67..27968efc 100644 --- a/keystone/common/utils.py +++ b/keystone/common/utils.py @@ -27,12 +27,11 @@ import passlib.hash from keystone.common import config from keystone.common import environment -from keystone.common import logging from keystone import exception +from keystone.openstack.common import log as logging CONF = config.CONF -config.register_int('crypt_strength', default=40000) LOG = logging.getLogger(__name__) diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index ae199d74..646bb4c4 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -27,12 +27,12 @@ import webob.dec import webob.exc from keystone.common import config -from keystone.common import logging from keystone.common import utils from keystone import exception from keystone.openstack.common import gettextutils from keystone.openstack.common import importutils from keystone.openstack.common import jsonutils +from keystone.openstack.common import log as logging CONF = config.CONF @@ -123,17 +123,6 @@ def validate_token_bind(context, token_ref): raise exception.Unauthorized() -class WritableLogger(object): - """A thin wrapper that responds to `write` and logs.""" - - def __init__(self, logger, level=logging.DEBUG): - self.logger = logger - self.level = level - - def write(self, msg): - self.logger.log(self.level, msg) - - class Request(webob.Request): def best_match_language(self): """Determines the best available locale from the Accept-Language @@ -407,7 +396,7 @@ class Debug(Middleware): @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): - if LOG.isEnabledFor(logging.DEBUG): + if LOG.isEnabledFor(LOG.debug): LOG.debug('%s %s %s', ('*' * 20), 'REQUEST ENVIRON', ('*' * 20)) for key, value in req.environ.items(): LOG.debug('%s = %s', key, mask_password(value, @@ -419,7 +408,7 @@ class Debug(Middleware): LOG.debug('') resp = req.get_response(self.application) - if LOG.isEnabledFor(logging.DEBUG): + if LOG.isEnabledFor(LOG.debug): LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE HEADERS', ('*' * 20)) for (key, value) in resp.headers.iteritems(): LOG.debug('%s = %s', key, value) @@ -468,7 +457,7 @@ class Router(object): # if we're only running in debug, bump routes' internal logging up a # notch, as it's very spammy if CONF.debug: - logging.getLogger('routes.middleware').setLevel(logging.INFO) + logging.getLogger('routes.middleware') self.map = mapper self._router = routes.middleware.RoutesMiddleware(self._dispatch, diff --git a/keystone/config.py b/keystone/config.py index 28f1cf2c..c4a43b47 100644 --- a/keystone/config.py +++ b/keystone/config.py @@ -25,15 +25,8 @@ config.configure() CONF = config.CONF setup_logging = config.setup_logging -register_str = config.register_str -register_cli_str = config.register_cli_str -register_list = config.register_list -register_cli_list = config.register_cli_list -register_bool = config.register_bool -register_cli_bool = config.register_cli_bool -register_int = config.register_int -register_cli_int = config.register_cli_int setup_authentication = config.setup_authentication +configure = config.configure def find_paste_config(): diff --git a/keystone/contrib/access/core.py b/keystone/contrib/access/core.py index f0221200..fbe09a24 100644 --- a/keystone/contrib/access/core.py +++ b/keystone/contrib/access/core.py @@ -14,12 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. -import webob import webob.dec -from keystone.common import logging from keystone.common import wsgi from keystone import config +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils diff --git a/keystone/contrib/oauth1/__init__.py b/keystone/contrib/oauth1/__init__.py new file mode 100644 index 00000000..fdb8dc4b --- /dev/null +++ b/keystone/contrib/oauth1/__init__.py @@ -0,0 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from keystone.contrib.oauth1.core import * # flake8: noqa diff --git a/keystone/contrib/oauth1/backends/__init__.py b/keystone/contrib/oauth1/backends/__init__.py new file mode 100644 index 00000000..3f393b26 --- /dev/null +++ b/keystone/contrib/oauth1/backends/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/keystone/contrib/oauth1/backends/kvs.py b/keystone/contrib/oauth1/backends/kvs.py new file mode 100644 index 00000000..09e31741 --- /dev/null +++ b/keystone/contrib/oauth1/backends/kvs.py @@ -0,0 +1,222 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 datetime +import random +import uuid + +from keystone.common import kvs +from keystone.common import logging +from keystone.contrib.oauth1 import core +from keystone import exception +from keystone.openstack.common import timeutils + + +LOG = logging.getLogger(__name__) + + +class OAuth1(kvs.Base): + """kvs backend for oauth is deprecated. + Deprecated in Havana and will be removed in Icehouse, as this backend + is not production grade. + """ + + def __init__(self, *args, **kw): + super(OAuth1, self).__init__(*args, **kw) + LOG.warn(_("kvs token backend is DEPRECATED. Use " + "keystone.contrib.oauth1.sql instead.")) + + def _get_consumer(self, consumer_id): + return self.db.get('consumer-%s' % consumer_id) + + def get_consumer(self, consumer_id): + consumer_ref = self.db.get('consumer-%s' % consumer_id) + return core.filter_consumer(consumer_ref) + + def create_consumer(self, consumer): + consumer_id = consumer['id'] + consumer['secret'] = uuid.uuid4().hex + if not consumer.get('description'): + consumer['description'] = None + self.db.set('consumer-%s' % consumer_id, consumer) + consumer_list = set(self.db.get('consumer_list', [])) + consumer_list.add(consumer_id) + self.db.set('consumer_list', list(consumer_list)) + return consumer + + def _delete_consumer(self, consumer_id): + # call get to make sure it exists + self.db.get('consumer-%s' % consumer_id) + self.db.delete('consumer-%s' % consumer_id) + consumer_list = set(self.db.get('consumer_list', [])) + consumer_list.remove(consumer_id) + self.db.set('consumer_list', list(consumer_list)) + + def _delete_request_tokens(self, consumer_id): + consumer_requests = set(self.db.get('consumer-%s-requests' % + consumer_id, [])) + for token in consumer_requests: + self.db.get('request_token-%s' % token) + self.db.delete('request_token-%s' % token) + + if len(consumer_requests) > 0: + self.db.delete('consumer-%s-requests' % consumer_id) + + def _delete_access_tokens(self, consumer_id): + consumer_accesses = set(self.db.get('consumer-%s-accesses' % + consumer_id, [])) + for token in consumer_accesses: + access_token = self.db.get('access_token-%s' % token) + self.db.delete('access_token-%s' % token) + + # kind of a hack, but I needed to update the auth_list + user_id = access_token['authorizing_user_id'] + user_auth_list = set(self.db.get('auth_list-%s' % user_id, [])) + user_auth_list.remove(token) + self.db.set('auth_list-%s' % user_id, list(user_auth_list)) + + if len(consumer_accesses) > 0: + self.db.delete('consumer-%s-accesses' % consumer_id) + + def delete_consumer(self, consumer_id): + self._delete_consumer(consumer_id) + self._delete_request_tokens(consumer_id) + self._delete_access_tokens(consumer_id) + + def list_consumers(self): + consumer_ids = self.db.get('consumer_list', []) + return [self.get_consumer(x) for x in consumer_ids] + + def update_consumer(self, consumer_id, consumer): + # call get to make sure it exists + old_consumer_ref = self.db.get('consumer-%s' % consumer_id) + new_consumer_ref = old_consumer_ref.copy() + new_consumer_ref['description'] = consumer['description'] + new_consumer_ref['id'] = consumer_id + self.db.set('consumer-%s' % consumer_id, new_consumer_ref) + return new_consumer_ref + + def create_request_token(self, consumer_id, roles, + project_id, token_duration): + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = timeutils.isotime(future, subsecond=True) + + ref = {} + request_token_id = uuid.uuid4().hex + ref['id'] = request_token_id + ref['request_secret'] = uuid.uuid4().hex + ref['verifier'] = None + ref['authorizing_user_id'] = None + ref['requested_project_id'] = project_id + ref['requested_roles'] = roles + ref['consumer_id'] = consumer_id + ref['expires_at'] = expiry_date + self.db.set('request_token-%s' % request_token_id, ref) + + # add req token to the list that containers the consumers req tokens + consumer_requests = set(self.db.get('consumer-%s-requests' % + consumer_id, [])) + consumer_requests.add(request_token_id) + self.db.set('consumer-%s-requests' % + consumer_id, list(consumer_requests)) + return ref + + def get_request_token(self, request_token_id): + return self.db.get('request_token-%s' % request_token_id) + + def authorize_request_token(self, request_token_id, user_id): + request_token = self.db.get('request_token-%s' % request_token_id) + request_token['authorizing_user_id'] = user_id + request_token['verifier'] = str(random.randint(1000, 9999)) + self.db.set('request_token-%s' % request_token_id, request_token) + return request_token + + def create_access_token(self, request_id, token_duration): + request_token = self.db.get('request_token-%s' % request_id) + + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = timeutils.isotime(future, subsecond=True) + + ref = {} + access_token_id = uuid.uuid4().hex + ref['id'] = access_token_id + ref['access_secret'] = uuid.uuid4().hex + ref['authorizing_user_id'] = request_token['authorizing_user_id'] + ref['project_id'] = request_token['requested_project_id'] + ref['requested_roles'] = request_token['requested_roles'] + ref['consumer_id'] = request_token['consumer_id'] + ref['expires_at'] = expiry_date + self.db.set('access_token-%s' % access_token_id, ref) + + #add access token id to user authorizations list too + user_id = request_token['authorizing_user_id'] + user_auth_list = set(self.db.get('auth_list-%s' % user_id, [])) + user_auth_list.add(access_token_id) + self.db.set('auth_list-%s' % user_id, list(user_auth_list)) + + #delete request token from table, it has been exchanged + self.db.get('request_token-%s' % request_id) + self.db.delete('request_token-%s' % request_id) + + #add access token to the list that containers the consumers acc tokens + consumer_id = request_token['consumer_id'] + consumer_accesses = set(self.db.get('consumer-%s-accesses' % + consumer_id, [])) + consumer_accesses.add(access_token_id) + self.db.set('consumer-%s-accesses' % + consumer_id, list(consumer_accesses)) + + # remove the used up request token id from consumer req list + consumer_requests = set(self.db.get('consumer-%s-requests' % + consumer_id, [])) + consumer_requests.remove(request_id) + self.db.set('consumer-%s-requests' % + consumer_id, list(consumer_requests)) + + return ref + + def get_access_token(self, access_token_id): + return self.db.get('access_token-%s' % access_token_id) + + def list_access_tokens(self, user_id): + user_auth_list = self.db.get('auth_list-%s' % user_id, []) + return [self.get_access_token(x) for x in user_auth_list] + + def delete_access_token(self, user_id, access_token_id): + access_token = self.get_access_token(access_token_id) + consumer_id = access_token['consumer_id'] + if access_token['authorizing_user_id'] != user_id: + raise exception.Unauthorized(_('User IDs do not match')) + self.db.get('access_token-%s' % access_token_id) + self.db.delete('access_token-%s' % access_token_id) + + # remove access token id from user authz list + user_auth_list = set(self.db.get('auth_list-%s' % user_id, [])) + user_auth_list.remove(access_token_id) + self.db.set('auth_list-%s' % user_id, list(user_auth_list)) + + # remove this token id from the consumer access list + consumer_accesses = set(self.db.get('consumer-%s-accesses' % + consumer_id, [])) + consumer_accesses.remove(access_token_id) + self.db.set('consumer-%s-accesses' % + consumer_id, list(consumer_accesses)) diff --git a/keystone/contrib/oauth1/backends/sql.py b/keystone/contrib/oauth1/backends/sql.py new file mode 100644 index 00000000..9dc0665c --- /dev/null +++ b/keystone/contrib/oauth1/backends/sql.py @@ -0,0 +1,284 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 datetime +import random +import uuid + +from keystone.common import sql +from keystone.common.sql import migration +from keystone.contrib.oauth1 import core +from keystone import exception +from keystone.openstack.common import timeutils + + +class Consumer(sql.ModelBase, sql.DictBase): + __tablename__ = 'consumer' + attributes = ['id', 'description', 'secret'] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + description = sql.Column(sql.String(64), nullable=False) + secret = sql.Column(sql.String(64), nullable=False) + extra = sql.Column(sql.JsonBlob(), nullable=False) + + +class RequestToken(sql.ModelBase, sql.DictBase): + __tablename__ = 'request_token' + attributes = ['id', 'request_secret', + 'verifier', 'authorizing_user_id', 'requested_project_id', + 'requested_roles', 'consumer_id', 'expires_at'] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + request_secret = sql.Column(sql.String(64), nullable=False) + verifier = sql.Column(sql.String(64), nullable=True) + authorizing_user_id = sql.Column(sql.String(64), nullable=True) + requested_project_id = sql.Column(sql.String(64), nullable=False) + requested_roles = sql.Column(sql.Text(), nullable=False) + consumer_id = sql.Column(sql.String(64), nullable=False, index=True) + expires_at = sql.Column(sql.String(64), nullable=True) + + @classmethod + def from_dict(cls, user_dict): + return cls(**user_dict) + + def to_dict(self): + return dict(self.iteritems()) + + +class AccessToken(sql.ModelBase, sql.DictBase): + __tablename__ = 'access_token' + attributes = ['id', 'access_secret', 'authorizing_user_id', + 'project_id', 'requested_roles', 'consumer_id', + 'expires_at'] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + access_secret = sql.Column(sql.String(64), nullable=False) + authorizing_user_id = sql.Column(sql.String(64), nullable=False, + index=True) + project_id = sql.Column(sql.String(64), nullable=False) + requested_roles = sql.Column(sql.Text(), nullable=False) + consumer_id = sql.Column(sql.String(64), nullable=False) + expires_at = sql.Column(sql.String(64), nullable=True) + + @classmethod + def from_dict(cls, user_dict): + return cls(**user_dict) + + def to_dict(self): + return dict(self.iteritems()) + + +class OAuth1(sql.Base): + def db_sync(self): + migration.db_sync() + + def _get_consumer(self, consumer_id): + session = self.get_session() + consumer_ref = session.query(Consumer).get(consumer_id) + if consumer_ref is None: + raise exception.NotFound(_('Consumer not found')) + return consumer_ref + + def get_consumer(self, consumer_id): + session = self.get_session() + consumer_ref = session.query(Consumer).get(consumer_id) + if consumer_ref is None: + raise exception.NotFound(_('Consumer not found')) + return core.filter_consumer(consumer_ref.to_dict()) + + def create_consumer(self, consumer): + consumer['secret'] = uuid.uuid4().hex + if not consumer.get('description'): + consumer['description'] = None + session = self.get_session() + with session.begin(): + consumer_ref = Consumer.from_dict(consumer) + session.add(consumer_ref) + session.flush() + return consumer_ref.to_dict() + + def _delete_consumer(self, session, consumer_id): + consumer_ref = self._get_consumer(session, consumer_id) + q = session.query(Consumer) + q = q.filter_by(id=consumer_id) + q.delete(False) + session.delete(consumer_ref) + + def _delete_request_tokens(self, session, consumer_id): + q = session.query(RequestToken) + req_tokens = q.filter_by(consumer_id=consumer_id) + req_tokens_list = set([x.id for x in req_tokens]) + for token_id in req_tokens_list: + token_ref = self._get_request_token(session, token_id) + q = session.query(RequestToken) + q = q.filter_by(id=token_id) + q.delete(False) + session.delete(token_ref) + + def _delete_access_tokens(self, session, consumer_id): + q = session.query(AccessToken) + acc_tokens = q.filter_by(consumer_id=consumer_id) + acc_tokens_list = set([x.id for x in acc_tokens]) + for token_id in acc_tokens_list: + token_ref = self._get_access_token(session, token_id) + q = session.query(AccessToken) + q = q.filter_by(id=token_id) + q.delete(False) + session.delete(token_ref) + + def delete_consumer(self, consumer_id): + session = self.get_session() + with session.begin(): + self._delete_consumer(session, consumer_id) + self._delete_request_tokens(session, consumer_id) + self._delete_access_tokens(session, consumer_id) + session.flush() + + def list_consumers(self): + session = self.get_session() + cons = session.query(Consumer) + return [core.filter_consumer(x.to_dict()) for x in cons] + + def update_consumer(self, consumer_id, consumer): + session = self.get_session() + with session.begin(): + consumer_ref = self._get_consumer(consumer_id) + old_consumer_dict = consumer_ref.to_dict() + old_consumer_dict.update(consumer) + new_consumer = Consumer.from_dict(old_consumer_dict) + for attr in Consumer.attributes: + if (attr != 'id' or attr != 'secret'): + setattr(consumer_ref, + attr, + getattr(new_consumer, attr)) + consumer_ref.extra = new_consumer.extra + session.flush() + return core.filter_consumer(consumer_ref.to_dict()) + + def create_request_token(self, consumer_id, roles, + project_id, token_duration): + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = timeutils.isotime(future, subsecond=True) + + ref = {} + request_token_id = uuid.uuid4().hex + ref['id'] = request_token_id + ref['request_secret'] = uuid.uuid4().hex + ref['verifier'] = None + ref['authorizing_user_id'] = None + ref['requested_project_id'] = project_id + ref['requested_roles'] = roles + ref['consumer_id'] = consumer_id + ref['expires_at'] = expiry_date + session = self.get_session() + with session.begin(): + token_ref = RequestToken.from_dict(ref) + session.add(token_ref) + session.flush() + return token_ref.to_dict() + + def _get_request_token(self, session, request_token_id): + token_ref = session.query(RequestToken).get(request_token_id) + if token_ref is None: + raise exception.NotFound(_('Request token not found')) + return token_ref + + def get_request_token(self, request_token_id): + session = self.get_session() + token_ref = self._get_request_token(session, request_token_id) + return token_ref.to_dict() + + def authorize_request_token(self, request_token_id, user_id): + session = self.get_session() + with session.begin(): + token_ref = self._get_request_token(session, request_token_id) + token_dict = token_ref.to_dict() + token_dict['authorizing_user_id'] = user_id + token_dict['verifier'] = str(random.randint(1000, 9999)) + + new_token = RequestToken.from_dict(token_dict) + for attr in RequestToken.attributes: + if (attr == 'authorizing_user_id' or attr == 'verifier'): + setattr(token_ref, attr, getattr(new_token, attr)) + + session.flush() + return token_ref.to_dict() + + def create_access_token(self, request_token_id, token_duration): + session = self.get_session() + with session.begin(): + req_token_ref = self._get_request_token(session, request_token_id) + token_dict = req_token_ref.to_dict() + + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = timeutils.isotime(future, subsecond=True) + + # add Access Token + ref = {} + access_token_id = uuid.uuid4().hex + ref['id'] = access_token_id + ref['access_secret'] = uuid.uuid4().hex + ref['authorizing_user_id'] = token_dict['authorizing_user_id'] + ref['project_id'] = token_dict['requested_project_id'] + ref['requested_roles'] = token_dict['requested_roles'] + ref['consumer_id'] = token_dict['consumer_id'] + ref['expires_at'] = expiry_date + token_ref = AccessToken.from_dict(ref) + session.add(token_ref) + + # remove request token, it's been used + q = session.query(RequestToken) + q = q.filter_by(id=request_token_id) + q.delete(False) + session.delete(req_token_ref) + + session.flush() + return token_ref.to_dict() + + def _get_access_token(self, session, access_token_id): + token_ref = session.query(AccessToken).get(access_token_id) + if token_ref is None: + raise exception.NotFound(_('Access token not found')) + return token_ref + + def get_access_token(self, access_token_id): + session = self.get_session() + token_ref = self._get_access_token(session, access_token_id) + return token_ref.to_dict() + + def list_access_tokens(self, user_id): + session = self.get_session() + q = session.query(AccessToken) + user_auths = q.filter_by(authorizing_user_id=user_id) + return [core.filter_token(x.to_dict()) for x in user_auths] + + def delete_access_token(self, user_id, access_token_id): + session = self.get_session() + with session.begin(): + token_ref = self._get_access_token(session, access_token_id) + token_dict = token_ref.to_dict() + if token_dict['authorizing_user_id'] != user_id: + raise exception.Unauthorized(_('User IDs do not match')) + + q = session.query(AccessToken) + q = q.filter_by(id=access_token_id) + q.delete(False) + + session.delete(token_ref) + session.flush() diff --git a/keystone/contrib/oauth1/controllers.py b/keystone/contrib/oauth1/controllers.py new file mode 100644 index 00000000..69522e0c --- /dev/null +++ b/keystone/contrib/oauth1/controllers.py @@ -0,0 +1,377 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +"""Extensions supporting OAuth1.""" + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import wsgi +from keystone import config +from keystone.contrib.oauth1 import core as oauth1 +from keystone import exception +from keystone.openstack.common import jsonutils +from keystone.openstack.common import timeutils + + +CONF = config.CONF + + +@dependency.requires('oauth_api', 'token_api') +class ConsumerCrudV3(controller.V3Controller): + collection_name = 'consumers' + member_name = 'consumer' + + def create_consumer(self, context, consumer): + ref = self._assign_unique_id(self._normalize_dict(consumer)) + consumer_ref = self.oauth_api.create_consumer(ref) + return ConsumerCrudV3.wrap_member(context, consumer_ref) + + def update_consumer(self, context, consumer_id, consumer): + self._require_matching_id(consumer_id, consumer) + ref = self._normalize_dict(consumer) + self._validate_consumer_ref(consumer) + ref = self.oauth_api.update_consumer(consumer_id, consumer) + return ConsumerCrudV3.wrap_member(context, ref) + + def list_consumers(self, context): + ref = self.oauth_api.list_consumers() + return ConsumerCrudV3.wrap_collection(context, ref) + + def get_consumer(self, context, consumer_id): + ref = self.oauth_api.get_consumer(consumer_id) + return ConsumerCrudV3.wrap_member(context, ref) + + def delete_consumer(self, context, consumer_id): + user_token_ref = self.token_api.get_token(context['token_id']) + user_id = user_token_ref['user'].get('id') + self.token_api.delete_tokens(user_id, consumer_id=consumer_id) + self.oauth_api.delete_consumer(consumer_id) + + def _validate_consumer_ref(self, consumer): + if 'secret' in consumer: + msg = _('Cannot change consumer secret') + raise exception.ValidationError(message=msg) + + +@dependency.requires('oauth_api') +class AccessTokenCrudV3(controller.V3Controller): + collection_name = 'access_tokens' + member_name = 'access_token' + + def get_access_token(self, context, user_id, access_token_id): + access_token = self.oauth_api.get_access_token(access_token_id) + if access_token['authorizing_user_id'] != user_id: + raise exception.NotFound() + access_token = self._format_token_entity(access_token) + return AccessTokenCrudV3.wrap_member(context, access_token) + + def list_access_tokens(self, context, user_id): + refs = self.oauth_api.list_access_tokens(user_id) + formatted_refs = ([self._format_token_entity(x) for x in refs]) + return AccessTokenCrudV3.wrap_collection(context, formatted_refs) + + def delete_access_token(self, context, user_id, access_token_id): + access_token = self.oauth_api.get_access_token(access_token_id) + consumer_id = access_token['consumer_id'] + self.token_api.delete_tokens(user_id, consumer_id=consumer_id) + return self.oauth_api.delete_access_token( + user_id, access_token_id) + + def _format_token_entity(self, entity): + + formatted_entity = entity.copy() + access_token_id = formatted_entity['id'] + user_id = "" + if 'requested_roles' in entity: + formatted_entity.pop('requested_roles') + if 'access_secret' in entity: + formatted_entity.pop('access_secret') + if 'authorizing_user_id' in entity: + user_id = formatted_entity['authorizing_user_id'] + + url = ('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(access_token_id)s' + '/roles' % {'user_id': user_id, + 'access_token_id': access_token_id}) + + formatted_entity.setdefault('links', {}) + formatted_entity['links']['roles'] = (self.base_url(url)) + + return formatted_entity + + +@dependency.requires('oauth_api') +class AccessTokenRolesV3(controller.V3Controller): + collection_name = 'roles' + member_name = 'role' + + def list_access_token_roles(self, context, user_id, access_token_id): + access_token = self.oauth_api.get_access_token(access_token_id) + if access_token['authorizing_user_id'] != user_id: + raise exception.NotFound() + roles = access_token['requested_roles'] + roles_refs = jsonutils.loads(roles) + formatted_refs = ([self._format_role_entity(x) for x in roles_refs]) + return AccessTokenRolesV3.wrap_collection(context, formatted_refs) + + def get_access_token_role(self, context, user_id, + access_token_id, role_id): + access_token = self.oauth_api.get_access_token(access_token_id) + if access_token['authorizing_user_id'] != user_id: + raise exception.Unauthorized(_('User IDs do not match')) + roles = access_token['requested_roles'] + roles_dict = jsonutils.loads(roles) + for role in roles_dict: + if role['id'] == role_id: + role = self._format_role_entity(role) + return AccessTokenRolesV3.wrap_member(context, role) + raise exception.RoleNotFound(_('Could not find role')) + + def _format_role_entity(self, entity): + + formatted_entity = entity.copy() + if 'description' in entity: + formatted_entity.pop('description') + if 'enabled' in entity: + formatted_entity.pop('enabled') + return formatted_entity + + +@dependency.requires('oauth_api', 'token_api', 'identity_api', + 'token_provider_api', 'assignment_api') +class OAuthControllerV3(controller.V3Controller): + collection_name = 'not_used' + member_name = 'not_used' + + def create_request_token(self, context): + headers = context['headers'] + oauth_headers = oauth1.get_oauth_headers(headers) + consumer_id = oauth_headers.get('oauth_consumer_key') + requested_role_ids = headers.get('Requested-Role-Ids') + requested_project_id = headers.get('Requested-Project-Id') + if not consumer_id: + raise exception.ValidationError( + attribute='oauth_consumer_key', target='request') + if not requested_role_ids: + raise exception.ValidationError( + attribute='requested_role_ids', target='request') + if not requested_project_id: + raise exception.ValidationError( + attribute='requested_project_id', target='request') + + req_role_ids = requested_role_ids.split(',') + consumer_ref = self.oauth_api._get_consumer(consumer_id) + consumer = oauth1.Consumer(key=consumer_ref['id'], + secret=consumer_ref['secret']) + + url = oauth1.rebuild_url(context['path']) + oauth_request = oauth1.Request.from_request( + http_method='POST', + http_url=url, + headers=context['headers'], + query_string=context['query_string'], + parameters={'requested_role_ids': requested_role_ids, + 'requested_project_id': requested_project_id}) + oauth_server = oauth1.Server() + oauth_server.add_signature_method(oauth1.SignatureMethod_HMAC_SHA1()) + params = oauth_server.verify_request(oauth_request, + consumer, + token=None) + + project_params = params['requested_project_id'] + if project_params != requested_project_id: + msg = _('Non-oauth parameter - project, do not match') + raise exception.Unauthorized(message=msg) + + roles_params = params['requested_role_ids'] + roles_params_list = roles_params.split(',') + if roles_params_list != req_role_ids: + msg = _('Non-oauth parameter - roles, do not match') + raise exception.Unauthorized(message=msg) + + req_role_list = list() + all_roles = self.identity_api.list_roles() + for role in all_roles: + for req_role in req_role_ids: + if role['id'] == req_role: + req_role_list.append(role) + + if len(req_role_list) == 0: + msg = _('could not find matching roles for provided role ids') + raise exception.Unauthorized(message=msg) + + json_roles = jsonutils.dumps(req_role_list) + request_token_duration = CONF.oauth1.request_token_duration + token_ref = self.oauth_api.create_request_token(consumer_id, + json_roles, + requested_project_id, + request_token_duration) + + result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' + % {'key': token_ref['id'], + 'secret': token_ref['request_secret']}) + + if CONF.oauth1.request_token_duration: + expiry_bit = '&oauth_expires_at=%s' % token_ref['expires_at'] + result += expiry_bit + + headers = [('Content-Type', 'application/x-www-urlformencoded')] + response = wsgi.render_response(result, + status=(201, 'Created'), + headers=headers) + + return response + + def create_access_token(self, context): + headers = context['headers'] + oauth_headers = oauth1.get_oauth_headers(headers) + consumer_id = oauth_headers.get('oauth_consumer_key') + request_token_id = oauth_headers.get('oauth_token') + oauth_verifier = oauth_headers.get('oauth_verifier') + + if not consumer_id: + raise exception.ValidationError( + attribute='oauth_consumer_key', target='request') + if not request_token_id: + raise exception.ValidationError( + attribute='oauth_token', target='request') + if not oauth_verifier: + raise exception.ValidationError( + attribute='oauth_verifier', target='request') + + consumer = self.oauth_api._get_consumer(consumer_id) + req_token = self.oauth_api.get_request_token( + request_token_id) + + expires_at = req_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Request token is expired')) + + consumer_obj = oauth1.Consumer(key=consumer['id'], + secret=consumer['secret']) + req_token_obj = oauth1.Token(key=req_token['id'], + secret=req_token['request_secret']) + req_token_obj.set_verifier(oauth_verifier) + + url = oauth1.rebuild_url(context['path']) + oauth_request = oauth1.Request.from_request( + http_method='POST', + http_url=url, + headers=context['headers'], + query_string=context['query_string']) + oauth_server = oauth1.Server() + oauth_server.add_signature_method(oauth1.SignatureMethod_HMAC_SHA1()) + params = oauth_server.verify_request(oauth_request, + consumer_obj, + token=req_token_obj) + + if len(params) != 0: + msg = _('There should not be any non-oauth parameters') + raise exception.Unauthorized(message=msg) + + if req_token['consumer_id'] != consumer_id: + msg = _('provided consumer key does not match stored consumer key') + raise exception.Unauthorized(message=msg) + + if req_token['verifier'] != oauth_verifier: + msg = _('provided verifier does not match stored verifier') + raise exception.Unauthorized(message=msg) + + if req_token['id'] != request_token_id: + msg = _('provided request key does not match stored request key') + raise exception.Unauthorized(message=msg) + + if not req_token.get('authorizing_user_id'): + msg = _('Request Token does not have an authorizing user id') + raise exception.Unauthorized(message=msg) + + access_token_duration = CONF.oauth1.access_token_duration + token_ref = self.oauth_api.create_access_token(request_token_id, + access_token_duration) + + result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' + % {'key': token_ref['id'], + 'secret': token_ref['access_secret']}) + + if CONF.oauth1.access_token_duration: + expiry_bit = '&oauth_expires_at=%s' % (token_ref['expires_at']) + result += expiry_bit + + headers = [('Content-Type', 'application/x-www-urlformencoded')] + response = wsgi.render_response(result, + status=(201, 'Created'), + headers=headers) + + return response + + def authorize(self, context, request_token_id): + """An authenticated user is going to authorize a request token. + + As a security precaution, the requested roles must match those in + the request token. Because this is in a CLI-only world at the moment, + there is not another easy way to make sure the user knows which roles + are being requested before authorizing. + """ + + req_token = self.oauth_api.get_request_token(request_token_id) + + expires_at = req_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Request token is expired')) + + req_roles = req_token['requested_roles'] + req_roles_list = jsonutils.loads(req_roles) + + req_set = set() + for x in req_roles_list: + req_set.add(x['id']) + + # verify the authorizing user has the roles + user_token = self.token_api.get_token(token_id=context['token_id']) + credentials = user_token['metadata'].copy() + user_roles = credentials.get('roles') + user_id = user_token['user'].get('id') + cred_set = set(user_roles) + + if not cred_set.issuperset(req_set): + msg = _('authorizing user does not have role required') + raise exception.Unauthorized(message=msg) + + # verify the user has the project too + req_project_id = req_token['requested_project_id'] + user_projects = self.assignment_api.list_user_projects(user_id) + found = False + for user_project in user_projects: + if user_project['id'] == req_project_id: + found = True + break + if not found: + msg = _("User is not a member of the requested project") + raise exception.Unauthorized(message=msg) + + # finally authorize the token + authed_token = self.oauth_api.authorize_request_token( + request_token_id, user_id) + + to_return = {'token': {'oauth_verifier': authed_token['verifier']}} + return to_return diff --git a/keystone/contrib/oauth1/core.py b/keystone/contrib/oauth1/core.py new file mode 100644 index 00000000..eb4bf959 --- /dev/null +++ b/keystone/contrib/oauth1/core.py @@ -0,0 +1,272 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +"""Extensions supporting OAuth1.""" + +from __future__ import absolute_import + +import oauth2 as oauth + +from keystone.common import dependency +from keystone.common import extension +from keystone.common import manager +from keystone import config +from keystone import exception + + +Consumer = oauth.Consumer +Request = oauth.Request +Server = oauth.Server +SignatureMethod = oauth.SignatureMethod +SignatureMethod_HMAC_SHA1 = oauth.SignatureMethod_HMAC_SHA1 +SignatureMethod_PLAINTEXT = oauth.SignatureMethod_PLAINTEXT +Token = oauth.Token +Client = oauth.Client + + +CONF = config.CONF + + +EXTENSION_DATA = { + 'name': 'OpenStack OAUTH1 API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-OAUTH1/v1.0', + 'alias': 'OS-OAUTH1', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack OAuth 1.0a Delegated Auth Mechanism.', + 'links': [ + { + 'rel': 'describedby', + # TODO(dolph): link needs to be revised after + # bug 928059 merges + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + + +def filter_consumer(consumer_ref): + """Filter out private items in a consumer dict. + + 'secret' is never returned. + + :returns: consumer_ref + + """ + if consumer_ref: + consumer_ref = consumer_ref.copy() + consumer_ref.pop('secret', None) + return consumer_ref + + +def filter_token(access_token_ref): + """Filter out private items in an access token dict. + + 'access_secret' is never returned. + + :returns: access_token_ref + + """ + if access_token_ref: + access_token_ref = access_token_ref.copy() + access_token_ref.pop('access_secret', None) + return access_token_ref + + +def rebuild_url(path): + endpoint = CONF.public_endpoint % CONF + + # allow a missing trailing slash in the config + if endpoint[-1] != '/': + endpoint += '/' + + url = endpoint + 'v3' + return url + path + + +def get_oauth_headers(headers): + parameters = {} + + # The incoming headers variable is your usual heading from context + # In an OAuth signed req, where the oauth variables are in the header, + # they with the key 'Authorization'. + + if headers and 'Authorization' in headers: + # A typical value for Authorization is seen below + # 'OAuth realm="", oauth_body_hash="2jm%3D", oauth_nonce="14475435" + # along with other oauth variables, the 'OAuth ' part is trimmed + # to split the rest of the headers. + + auth_header = headers['Authorization'] + # Check that the authorization header is OAuth. + if auth_header[:6] == 'OAuth ': + auth_header = auth_header[6:] + # Get the parameters from the header. + header_params = oauth.Request._split_header(auth_header) + parameters.update(header_params) + return parameters + + +@dependency.provider('oauth_api') +class Manager(manager.Manager): + """Default pivot point for the OAuth1 backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.oauth1.driver) + + +class Driver(object): + """Interface description for an OAuth1 driver.""" + + def create_consumer(self, consumer_ref): + """Create consumer. + + :param consumer_ref: consumer ref with consumer name + :type consumer_ref: dict + :returns: consumer_ref + + """ + raise exception.NotImplemented() + + def update_consumer(self, consumer_id, consumer_ref): + """Update consumer. + + :param consumer_id: id of consumer to update + :type consumer_ref: string + :param consumer_ref: new consumer ref with consumer name + :type consumer_ref: dict + :returns: consumer_ref + + """ + raise exception.NotImplemented() + + def list_consumers(self): + """List consumers. + + returns: list of consumers + + """ + raise exception.NotImplemented() + + def get_consumer(self, consumer_id): + """Get consumer. + + :param consumer_id: id of consumer to get + :type consumer_ref: string + :returns: consumer_ref + + """ + raise exception.NotImplemented() + + def delete_consumer(self, consumer_id): + """Delete consumer. + + :param consumer_id: id of consumer to get + :type consumer_ref: string + :returns: None. + + """ + raise exception.NotImplemented() + + def list_access_tokens(self, user_id): + """List access tokens. + + :param user_id: search for access tokens authorized by given user id + :type user_id: string + returns: list of access tokens the user has authorized + + """ + raise exception.NotImplemented() + + def delete_access_token(self, user_id, access_token_id): + """Delete access token. + + :param user_id: authorizing user id + :type user_id: string + :param access_token_id: access token to delete + :type access_token_id: string + returns: None + + """ + raise exception.NotImplemented() + + def create_request_token(self, consumer_id, requested_roles, + requested_project, request_token_duration): + """Create request token. + + :param consumer_id: the id of the consumer + :type consumer_id: string + :param requested_roles: requested roles + :type requested_roles: string + :param requested_project_id: requested project id + :type requested_project_id: string + :param request_token_duration: duration of request token + :type request_token_duration: string + returns: request_token_ref + + """ + raise exception.NotImplemented() + + def get_request_token(self, request_token_id): + """Get request token. + + :param request_token_id: the id of the request token + :type request_token_id: string + returns: request_token_ref + + """ + raise exception.NotImplemented() + + def get_access_token(self, access_token_id): + """Get access token. + + :param access_token_id: the id of the access token + :type access_token_id: string + returns: access_token_ref + + """ + raise exception.NotImplemented() + + def authorize_request_token(self, request_id, user_id): + """Authorize request token. + + :param request_id: the id of the request token, to be authorized + :type request_id: string + :param user_id: the id of the authorizing user + :type user_id: string + returns: verifier + + """ + raise exception.NotImplemented() + + def create_access_token(self, request_id, access_token_duration): + """Create access token. + + :param request_id: the id of the request token, to be deleted + :type request_id: string + :param access_token_duration: duration of an access token + :type access_token_duration: string + returns: access_token_ref + + """ + raise exception.NotImplemented() diff --git a/keystone/contrib/oauth1/migrate_repo/__init__.py b/keystone/contrib/oauth1/migrate_repo/__init__.py new file mode 100644 index 00000000..3f393b26 --- /dev/null +++ b/keystone/contrib/oauth1/migrate_repo/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/keystone/contrib/oauth1/migrate_repo/migrate.cfg b/keystone/contrib/oauth1/migrate_repo/migrate.cfg new file mode 100644 index 00000000..97ca7810 --- /dev/null +++ b/keystone/contrib/oauth1/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=oauth1 + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py b/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py new file mode 100644 index 00000000..d3ed9033 --- /dev/null +++ b/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + consumer_table = sql.Table( + 'consumer', + meta, + sql.Column('id', sql.String(64), primary_key=True, nullable=False), + sql.Column('description', sql.String(64), nullable=False), + sql.Column('secret', sql.String(64), nullable=False), + sql.Column('extra', sql.Text(), nullable=False)) + consumer_table.create(migrate_engine, checkfirst=True) + + request_token_table = sql.Table( + 'request_token', + meta, + sql.Column('id', sql.String(64), primary_key=True, nullable=False), + sql.Column('request_secret', sql.String(64), nullable=False), + sql.Column('verifier', sql.String(64), nullable=True), + sql.Column('authorizing_user_id', sql.String(64), nullable=True), + sql.Column('requested_project_id', sql.String(64), nullable=False), + sql.Column('requested_roles', sql.Text(), nullable=False), + sql.Column('consumer_id', sql.String(64), nullable=False, index=True), + sql.Column('expires_at', sql.String(64), nullable=True)) + request_token_table.create(migrate_engine, checkfirst=True) + + access_token_table = sql.Table( + 'access_token', + meta, + sql.Column('id', sql.String(64), primary_key=True, nullable=False), + sql.Column('access_secret', sql.String(64), nullable=False), + sql.Column('authorizing_user_id', sql.String(64), + nullable=False, index=True), + sql.Column('project_id', sql.String(64), nullable=False), + sql.Column('requested_roles', sql.Text(), nullable=False), + sql.Column('consumer_id', sql.String(64), nullable=False), + sql.Column('expires_at', sql.String(64), nullable=True)) + access_token_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Operations to reverse the above upgrade go here. + tables = ['consumer', 'request_token', 'access_token'] + for table_name in tables: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone/contrib/oauth1/migrate_repo/versions/__init__.py b/keystone/contrib/oauth1/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..3f393b26 --- /dev/null +++ b/keystone/contrib/oauth1/migrate_repo/versions/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/keystone/contrib/oauth1/routers.py b/keystone/contrib/oauth1/routers.py new file mode 100644 index 00000000..0d9123b1 --- /dev/null +++ b/keystone/contrib/oauth1/routers.py @@ -0,0 +1,129 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from keystone.common import wsgi +from keystone.contrib.oauth1 import controllers + + +class OAuth1Extension(wsgi.ExtensionRouter): + """API Endpoints for the OAuth1 extension. + + The goal of this extension is to allow third-party service providers + to acquire tokens with a limited subset of a user's roles for acting + on behalf of that user. This is done using an oauth-similar flow and + api. + + The API looks like: + + # Basic admin-only consumer crud + POST /OS-OAUTH1/consumers + GET /OS-OAUTH1/consumers + PATCH /OS-OAUTH1/consumers/$consumer_id + GET /OS-OAUTH1/consumers/$consumer_id + DELETE /OS-OAUTH1/consumers/$consumer_id + + # User access token crud + GET /users/$user_id/OS-OAUTH1/access_tokens + GET /users/$user_id/OS-OAUTH1/access_tokens/$access_token_id + GET /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/roles + GET /users/{user_id}/OS-OAUTH1/access_tokens + /{access_token_id}/roles/{role_id} + DELETE /users/$user_id/OS-OAUTH1/access_tokens/$access_token_id + + # OAuth interfaces + POST /OS-OAUTH1/request_token # create a request token + PUT /OS-OAUTH1/authorize # authorize a request token + POST /OS-OAUTH1/access_token # create an access token + + """ + + def add_routes(self, mapper): + consumer_controller = controllers.ConsumerCrudV3() + access_token_controller = controllers.AccessTokenCrudV3() + access_token_roles_controller = controllers.AccessTokenRolesV3() + oauth_controller = controllers.OAuthControllerV3() + + # basic admin-only consumer crud + mapper.connect( + '/OS-OAUTH1/consumers', + controller=consumer_controller, + action='create_consumer', + conditions=dict(method=['POST'])) + mapper.connect( + '/OS-OAUTH1/consumers/{consumer_id}', + controller=consumer_controller, + action='get_consumer', + conditions=dict(method=['GET'])) + mapper.connect( + '/OS-OAUTH1/consumers/{consumer_id}', + controller=consumer_controller, + action='update_consumer', + conditions=dict(method=['PATCH'])) + mapper.connect( + '/OS-OAUTH1/consumers/{consumer_id}', + controller=consumer_controller, + action='delete_consumer', + conditions=dict(method=['DELETE'])) + mapper.connect( + '/OS-OAUTH1/consumers', + controller=consumer_controller, + action='list_consumers', + conditions=dict(method=['GET'])) + + # user accesss token crud + mapper.connect( + '/users/{user_id}/OS-OAUTH1/access_tokens', + controller=access_token_controller, + action='list_access_tokens', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}', + controller=access_token_controller, + action='get_access_token', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}', + controller=access_token_controller, + action='delete_access_token', + conditions=dict(method=['DELETE'])) + mapper.connect( + '/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/roles', + controller=access_token_roles_controller, + action='list_access_token_roles', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/OS-OAUTH1/access_tokens/' + '{access_token_id}/roles/{role_id}', + controller=access_token_roles_controller, + action='get_access_token_role', + conditions=dict(method=['GET'])) + + # oauth flow calls + mapper.connect( + '/OS-OAUTH1/request_token', + controller=oauth_controller, + action='create_request_token', + conditions=dict(method=['POST'])) + mapper.connect( + '/OS-OAUTH1/access_token', + controller=oauth_controller, + action='create_access_token', + conditions=dict(method=['POST'])) + mapper.connect( + '/OS-OAUTH1/authorize/{request_token_id}', + controller=oauth_controller, + action='authorize', + conditions=dict(method=['PUT'])) diff --git a/keystone/contrib/stats/core.py b/keystone/contrib/stats/core.py index 1d7b2cdf..9e6538db 100644 --- a/keystone/contrib/stats/core.py +++ b/keystone/contrib/stats/core.py @@ -15,12 +15,12 @@ # under the License. from keystone.common import extension -from keystone.common import logging from keystone.common import manager from keystone.common import wsgi from keystone import config from keystone import exception from keystone import identity +from keystone.openstack.common import log as logging from keystone import policy from keystone import token diff --git a/keystone/contrib/user_crud/core.py b/keystone/contrib/user_crud/core.py index f9f09b89..2129af40 100644 --- a/keystone/contrib/user_crud/core.py +++ b/keystone/contrib/user_crud/core.py @@ -18,10 +18,10 @@ import copy import uuid from keystone.common import extension -from keystone.common import logging from keystone.common import wsgi from keystone import exception from keystone import identity +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) @@ -82,7 +82,7 @@ class UserController(identity.controllers.User): new_token_ref = copy.copy(token_ref) new_token_ref['id'] = token_id self.token_api.create_token(token_id, new_token_ref) - logging.debug('TOKEN_REF %s', new_token_ref) + LOG.debug('TOKEN_REF %s', new_token_ref) return {'access': {'token': new_token_ref}} diff --git a/keystone/controllers.py b/keystone/controllers.py index 8ffa073a..be3c57fa 100644 --- a/keystone/controllers.py +++ b/keystone/controllers.py @@ -15,10 +15,10 @@ # under the License. from keystone.common import extension -from keystone.common import logging from keystone.common import wsgi from keystone import config from keystone import exception +from keystone.openstack.common import log as logging LOG = logging.getLogger(__name__) diff --git a/keystone/credential/core.py b/keystone/credential/core.py index a8921ba0..97cfc1c1 100644 --- a/keystone/credential/core.py +++ b/keystone/credential/core.py @@ -17,10 +17,10 @@ """Main entry point into the Credentials service.""" from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import log as logging CONF = config.CONF diff --git a/keystone/exception.py b/keystone/exception.py index 5e1defba..c0edc263 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -15,8 +15,8 @@ # under the License. from keystone.common import config -from keystone.common import logging from keystone.openstack.common.gettextutils import _ # noqa +from keystone.openstack.common import log as logging CONF = config.CONF diff --git a/keystone/identity/backends/kvs.py b/keystone/identity/backends/kvs.py index 0323d3d0..bcfb777b 100644 --- a/keystone/identity/backends/kvs.py +++ b/keystone/identity/backends/kvs.py @@ -27,6 +27,9 @@ class Identity(kvs.Base, identity.Driver): def default_assignment_driver(self): return "keystone.assignment.backends.kvs.Assignment" + def is_domain_aware(self): + return True + # Public interface def authenticate(self, user_id, password): user_ref = None diff --git a/keystone/identity/backends/ldap.py b/keystone/identity/backends/ldap.py index a359c63f..5898da1f 100644 --- a/keystone/identity/backends/ldap.py +++ b/keystone/identity/backends/ldap.py @@ -21,12 +21,12 @@ import ldap from keystone import clean from keystone.common import dependency from keystone.common import ldap as common_ldap -from keystone.common import logging from keystone.common import models from keystone.common import utils from keystone import config from keystone import exception from keystone import identity +from keystone.openstack.common import log as logging CONF = config.CONF @@ -41,14 +41,19 @@ DEFAULT_DOMAIN = { @dependency.requires('assignment_api') class Identity(identity.Driver): - def __init__(self): + def __init__(self, conf=None): super(Identity, self).__init__() - self.user = UserApi(CONF) - self.group = GroupApi(CONF) + if conf is None: + conf = CONF + self.user = UserApi(conf) + self.group = GroupApi(conf) def default_assignment_driver(self): return "keystone.assignment.backends.ldap.Assignment" + def is_domain_aware(self): + return False + # Identity interface def create_project(self, project_id, project): @@ -68,37 +73,31 @@ class Identity(identity.Driver): raise AssertionError('Invalid user / password') except Exception: raise AssertionError('Invalid user / password') - return self.assignment_api._set_default_domain( - identity.filter_user(user_ref)) + return identity.filter_user(user_ref) def _get_user(self, user_id): return self.user.get(user_id) def get_user(self, user_id): - ref = identity.filter_user(self._get_user(user_id)) - return self.assignment_api._set_default_domain(ref) + return identity.filter_user(self._get_user(user_id)) def list_users(self): - return (self.assignment_api._set_default_domain - (self.user.get_all_filtered())) + return self.user.get_all_filtered() def get_user_by_name(self, user_name, domain_id): - self.assignment_api._validate_default_domain_id(domain_id) - ref = identity.filter_user(self.user.get_by_name(user_name)) - return self.assignment_api._set_default_domain(ref) + # domain_id will already have been handled in the Manager layer, + # parameter left in so this matches the Driver specification + return identity.filter_user(self.user.get_by_name(user_name)) # CRUD def create_user(self, user_id, user): - user = self.assignment_api._validate_default_domain(user) user_ref = self.user.create(user) tenant_id = user.get('tenant_id') if tenant_id is not None: self.assignment_api.add_user_to_project(tenant_id, user_id) - return (self.assignment_api._set_default_domain - (identity.filter_user(user_ref))) + return identity.filter_user(user_ref) def update_user(self, user_id, user): - user = self.assignment_api._validate_default_domain(user) if 'id' in user and user['id'] != user_id: raise exception.ValidationError('Cannot change user ID') old_obj = self.user.get(user_id) @@ -121,8 +120,7 @@ class Identity(identity.Driver): user['enabled_nomask'] = old_obj['enabled_nomask'] self.user.mask_enabled_attribute(user) self.user.update(user_id, user, old_obj) - return (self.assignment_api._set_default_domain - (self.user.get_filtered(user_id))) + return self.user.get_filtered(user_id) def delete_user(self, user_id): self.assignment_api.delete_user(user_id) @@ -138,21 +136,16 @@ class Identity(identity.Driver): self.user.delete(user_id) def create_group(self, group_id, group): - group = self.assignment_api._validate_default_domain(group) group['name'] = clean.group_name(group['name']) - return self.assignment_api._set_default_domain( - self.group.create(group)) + return self.group.create(group) def get_group(self, group_id): - return self.assignment_api._set_default_domain( - self.group.get(group_id)) + return self.group.get(group_id) def update_group(self, group_id, group): - group = self.assignment_api._validate_default_domain(group) if 'name' in group: group['name'] = clean.group_name(group['name']) - return (self.assignment_api._set_default_domain - (self.group.update(group_id, group))) + return self.group.update(group_id, group) def delete_group(self, group_id): return self.group.delete(group_id) @@ -172,11 +165,10 @@ class Identity(identity.Driver): def list_groups_for_user(self, user_id): self.get_user(user_id) user_dn = self.user._id_to_dn(user_id) - return (self.assignment_api._set_default_domain - (self.group.list_user_groups(user_dn))) + return self.group.list_user_groups(user_dn) def list_groups(self): - return self.assignment_api._set_default_domain(self.group.get_all()) + return self.group.get_all() def list_users_in_group(self, group_id): self.get_group(group_id) @@ -190,7 +182,7 @@ class Identity(identity.Driver): " '%(group_id)s'. The user should be removed" " from the group. The user will be ignored.") % dict(user_dn=user_dn, group_id=group_id)) - return self.assignment_api._set_default_domain(users) + return users def check_user_in_group(self, user_id, group_id): self.get_user(user_id) @@ -228,14 +220,15 @@ class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap): def _ldap_res_to_model(self, res): obj = super(UserApi, self)._ldap_res_to_model(res) if self.enabled_mask != 0: - obj['enabled_nomask'] = obj['enabled'] - obj['enabled'] = ((obj['enabled'] & self.enabled_mask) != + enabled = int(obj.get('enabled', self.enabled_default)) + obj['enabled_nomask'] = enabled + obj['enabled'] = ((enabled & self.enabled_mask) != self.enabled_mask) return obj def mask_enabled_attribute(self, values): value = values['enabled'] - values.setdefault('enabled_nomask', self.enabled_default) + values.setdefault('enabled_nomask', int(self.enabled_default)) if value != ((values['enabled_nomask'] & self.enabled_mask) != self.enabled_mask): values['enabled_nomask'] ^= self.enabled_mask diff --git a/keystone/identity/backends/pam.py b/keystone/identity/backends/pam.py index 2a6ee621..a5459694 100644 --- a/keystone/identity/backends/pam.py +++ b/keystone/identity/backends/pam.py @@ -58,6 +58,9 @@ class PamIdentity(identity.Driver): Tenant is always the same as User, root user has admin role. """ + def is_domain_aware(self): + return False + def authenticate(self, user_id, password): auth = pam.authenticate if pam else PAM_authenticate if not auth(user_id, password): diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index bff41106..84026a58 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -26,7 +26,7 @@ class User(sql.ModelBase, sql.DictBase): __tablename__ = 'user' attributes = ['id', 'name', 'domain_id', 'password', 'enabled'] id = sql.Column(sql.String(64), primary_key=True) - name = sql.Column(sql.String(64), nullable=False) + name = sql.Column(sql.String(255), nullable=False) domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), nullable=False) password = sql.Column(sql.String(128)) @@ -85,6 +85,9 @@ class Identity(sql.Base, identity.Driver): """ return utils.check_password(password, user_ref.password) + def is_domain_aware(self): + return True + # Identity interface def authenticate(self, user_id, password): session = self.get_session() diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 7ca1f8bf..281e3f1b 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -22,10 +22,9 @@ import urlparse import uuid from keystone.common import controller -from keystone.common import logging from keystone import config from keystone import exception - +from keystone.openstack.common import log as logging CONF = config.CONF DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id @@ -109,12 +108,20 @@ class Tenant(controller.V2Controller): # be specifying that clean_tenant = tenant.copy() clean_tenant.pop('domain_id', None) + + # If the project has been disabled (or enabled=False) we are + # deleting the tokens for that project. + if not tenant.get('enabled', True): + self._delete_tokens_for_project(tenant_id) + tenant_ref = self.identity_api.update_project( tenant_id, clean_tenant) return {'tenant': tenant_ref} def delete_project(self, context, tenant_id): self.assert_admin(context) + # Delete all tokens belonging to the users for that project + self._delete_tokens_for_project(tenant_id) self.identity_api.delete_project(tenant_id) def get_project_users(self, context, tenant_id, **kw): @@ -572,6 +579,10 @@ class ProjectV3(controller.V3Controller): def update_project(self, context, project_id, project): self._require_matching_id(project_id, project) + # The project was disabled so we delete the tokens + if not project.get('enabled', True): + self._delete_tokens_for_project(project_id) + ref = self.identity_api.update_project(project_id, project) return ProjectV3.wrap_member(context, ref) @@ -580,6 +591,10 @@ class ProjectV3(controller.V3Controller): for cred in self.credential_api.list_credentials(): if cred['project_id'] == project_id: self.credential_api.delete_credential(cred['id']) + + # Delete all tokens belonging to the users for that project + self._delete_tokens_for_project(project_id) + # Finally delete the project itself - the backend is # responsible for deleting any role assignments related # to this project @@ -605,23 +620,30 @@ class UserV3(controller.V3Controller): @controller.filterprotected('domain_id', 'email', 'enabled', 'name') def list_users(self, context, filters): - refs = self.identity_api.list_users() + refs = self.identity_api.list_users( + domain_scope=self._get_domain_id_for_request(context)) return UserV3.wrap_collection(context, refs, filters) @controller.filterprotected('domain_id', 'email', 'enabled', 'name') def list_users_in_group(self, context, filters, group_id): - refs = self.identity_api.list_users_in_group(group_id) + refs = self.identity_api.list_users_in_group( + group_id, + domain_scope=self._get_domain_id_for_request(context)) return UserV3.wrap_collection(context, refs, filters) @controller.protected def get_user(self, context, user_id): - ref = self.identity_api.get_user(user_id) + ref = self.identity_api.get_user( + user_id, + domain_scope=self._get_domain_id_for_request(context)) return UserV3.wrap_member(context, ref) @controller.protected def update_user(self, context, user_id, user): self._require_matching_id(user_id, user) - ref = self.identity_api.update_user(user_id, user) + ref = self.identity_api.update_user( + user_id, user, + domain_scope=self._get_domain_id_for_request(context)) if user.get('password') or not user.get('enabled', True): # revoke all tokens owned by this user @@ -631,18 +653,24 @@ class UserV3(controller.V3Controller): @controller.protected def add_user_to_group(self, context, user_id, group_id): - self.identity_api.add_user_to_group(user_id, group_id) + self.identity_api.add_user_to_group( + user_id, group_id, + domain_scope=self._get_domain_id_for_request(context)) # Delete any tokens so that group membership can have an # immediate effect self._delete_tokens_for_user(user_id) @controller.protected def check_user_in_group(self, context, user_id, group_id): - return self.identity_api.check_user_in_group(user_id, group_id) + return self.identity_api.check_user_in_group( + user_id, group_id, + domain_scope=self._get_domain_id_for_request(context)) @controller.protected def remove_user_from_group(self, context, user_id, group_id): - self.identity_api.remove_user_from_group(user_id, group_id) + self.identity_api.remove_user_from_group( + user_id, group_id, + domain_scope=self._get_domain_id_for_request(context)) self._delete_tokens_for_user(user_id) def _delete_user(self, context, user_id): @@ -652,11 +680,13 @@ class UserV3(controller.V3Controller): self.credential_api.delete_credential(cred['id']) # Make sure any tokens are marked as deleted + domain_id = self._get_domain_id_for_request(context) self._delete_tokens_for_user(user_id) # Finally delete the user itself - the backend is # responsible for deleting any role assignments related # to this user - return self.identity_api.delete_user(user_id) + return self.identity_api.delete_user( + user_id, domain_scope=domain_id) @controller.protected def delete_user(self, context, user_id): @@ -678,24 +708,31 @@ class GroupV3(controller.V3Controller): @controller.filterprotected('domain_id', 'name') def list_groups(self, context, filters): - refs = self.identity_api.list_groups() + refs = self.identity_api.list_groups( + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_collection(context, refs, filters) @controller.filterprotected('name') def list_groups_for_user(self, context, filters, user_id): - refs = self.identity_api.list_groups_for_user(user_id) + refs = self.identity_api.list_groups_for_user( + user_id, + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_collection(context, refs, filters) @controller.protected def get_group(self, context, group_id): - ref = self.identity_api.get_group(group_id) + ref = self.identity_api.get_group( + group_id, + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_member(context, ref) @controller.protected def update_group(self, context, group_id, group): self._require_matching_id(group_id, group) - ref = self.identity_api.update_group(group_id, group) + ref = self.identity_api.update_group( + group_id, group, + domain_scope=self._get_domain_id_for_request(context)) return GroupV3.wrap_member(context, ref) def _delete_group(self, context, group_id): @@ -705,8 +742,10 @@ class GroupV3(controller.V3Controller): # deletion, so that we can remove these tokens after we know # the group deletion succeeded. - user_refs = self.identity_api.list_users_in_group(group_id) - self.identity_api.delete_group(group_id) + domain_id = self._get_domain_id_for_request(context) + user_refs = self.identity_api.list_users_in_group( + group_id, domain_scope=domain_id) + self.identity_api.delete_group(group_id, domain_scope=domain_id) for user in user_refs: self._delete_tokens_for_user(user['id']) diff --git a/keystone/identity/core.py b/keystone/identity/core.py index b2b3eaf0..7d5882e3 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -16,12 +16,18 @@ """Main entry point into the Identity service.""" +import functools +import os + +from oslo.config import cfg + from keystone import clean from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import importutils +from keystone.openstack.common import log as logging CONF = config.CONF @@ -51,6 +57,121 @@ def filter_user(user_ref): return user_ref +class DomainConfigs(dict): + """Discover, store and provide access to domain specifc configs. + + The setup_domain_drives() call will be made via the wrapper from + the first call to any driver function handled by this manager. This + setup call it will scan the domain config directory for files of the form + + keystone.<domain_name>.conf + + For each file, the domain_name will be turned into a domain_id and then + this class will: + - Create a new config structure, adding in the specific additional options + defined in this config file + - Initialise a new instance of the required driver with this new config. + + """ + configured = False + driver = None + + def _load_driver(self, assignment_api, domain_id): + domain_config = self[domain_id] + domain_config['driver'] = ( + importutils.import_object( + domain_config['cfg'].identity.driver, domain_config['cfg'])) + domain_config['driver'].assignment_api = assignment_api + + def _load_config(self, assignment_api, file_list, domain_name): + try: + domain_ref = assignment_api.get_domain_by_name(domain_name) + except exception.DomainNotFound: + msg = (_('Invalid domain name (%s) found in config file name') + % domain_name) + LOG.warning(msg) + + if domain_ref: + # Create a new entry in the domain config dict, which contains + # a new instance of both the conf environment and driver using + # options defined in this set of config files. Later, when we + # service calls via this Manager, we'll index via this domain + # config dict to make sure we call the right driver + domain = domain_ref['id'] + self[domain] = {} + self[domain]['cfg'] = cfg.ConfigOpts() + config.configure(conf=self[domain]['cfg']) + self[domain]['cfg'](args=[], project='keystone', + default_config_files=file_list) + self._load_driver(assignment_api, domain) + + def setup_domain_drivers(self, standard_driver, assignment_api): + # This is called by the api call wrapper + self.configured = True + self.driver = standard_driver + + conf_dir = CONF.identity.domain_config_dir + if not os.path.exists(conf_dir): + msg = _('Unable to locate domain config directory: %s') % conf_dir + LOG.warning(msg) + return + + for r, d, f in os.walk(conf_dir): + for file in f: + if file.startswith('keystone.') and file.endswith('.conf'): + names = file.split('.') + if len(names) == 3: + self._load_config(assignment_api, + [os.path.join(r, file)], + names[1]) + else: + msg = (_('Ignoring file (%s) while scanning domain ' + 'config directory') % file) + LOG.debug(msg) + + def get_domain_driver(self, domain_id): + if domain_id in self: + return self[domain_id]['driver'] + + def get_domain_conf(self, domain_id): + if domain_id in self: + return self[domain_id]['cfg'] + + def reload_domain_driver(self, assignment_api, domain_id): + # Only used to support unit tests that want to set + # new config values. This should only be called once + # the domains have been configured, since it relies on + # the fact that the configuration files have already been + # read. + if self.configured: + if domain_id in self: + self._load_driver(assignment_api, domain_id) + else: + # The standard driver + self.driver = self.driver() + self.driver.assignment_api = assignment_api + + +def domains_configured(f): + """Wraps API calls to lazy load domain configs after init. + + This is required since the assignment manager needs to be initialized + before this manager, and yet this manager's init wants to be + able to make assignment calls (to build the domain configs). So + instead, we check if the domains have been initialized on entry + to each call, and if requires load them, + + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if (not self.domain_configs.configured and + CONF.identity.domain_specific_drivers_enabled): + self.domain_configs.setup_domain_drivers( + self.driver, self.assignment_api) + return f(self, *args, **kwargs) + return wrapper + + @dependency.provider('identity_api') @dependency.requires('assignment_api') class Manager(manager.Manager): @@ -59,30 +180,228 @@ class Manager(manager.Manager): See :mod:`keystone.common.manager.Manager` for more details on how this dynamically calls the backend. + This class also handles the support of domain specific backends, by using + the DomainConfigs class. The setup call for DomainConfigs is called + from with the @domains_configured wrapper in a lazy loading fashion + to get around the fact that we can't satisfy the assignment api it needs + from within our __init__() function since the assignment driver is not + itself yet intitalized. + + Each of the identity calls are pre-processed here to choose, based on + domain, which of the drivers should be called. The non-domain-specific + driver is still in place, and is used if there is no specific driver for + the domain in question. + """ def __init__(self): super(Manager, self).__init__(CONF.identity.driver) - + self.domain_configs = DomainConfigs() + + # Domain ID normalization methods + + def _set_domain_id(self, ref, domain_id): + if isinstance(ref, dict): + ref = ref.copy() + ref['domain_id'] = domain_id + return ref + elif isinstance(ref, list): + return [self._set_domain_id(x, domain_id) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + + def _clear_domain_id(self, ref): + # Clear the domain_id, and then check to ensure that if this + # was not the default domain, it is being handled by its own + # backend driver. + ref = ref.copy() + domain_id = ref.pop('domain_id', CONF.identity.default_domain_id) + if (domain_id != CONF.identity.default_domain_id and + domain_id not in self.domain_configs): + raise exception.DomainNotFound(domain_id=domain_id) + return ref + + def _normalize_scope(self, domain_scope): + if domain_scope is None: + return CONF.identity.default_domain_id + else: + return domain_scope + + def _select_identity_driver(self, domain_id): + driver = self.domain_configs.get_domain_driver(domain_id) + if driver: + return driver + else: + return self.driver + + def _get_domain_conf(self, domain_id): + conf = self.domain_configs.get_domain_conf(domain_id) + if conf: + return conf + else: + return CONF + + def _get_domain_id_and_driver(self, domain_scope): + domain_id = self._normalize_scope(domain_scope) + driver = self._select_identity_driver(domain_id) + return (domain_id, driver) + + # The actual driver calls - these are pre/post processed here as + # part of the Manager layer to make sure we: + # + # - select the right driver for this domain + # - clear/set domain_ids for drivers that do not support domains + + @domains_configured + def authenticate(self, user_id, password, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + ref = driver.authenticate(user_id, password) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured def create_user(self, user_id, user_ref): user = user_ref.copy() user['name'] = clean.user_name(user['name']) user.setdefault('enabled', True) user['enabled'] = clean.user_enabled(user['enabled']) - return self.driver.create_user(user_id, user) - def update_user(self, user_id, user_ref): + # For creating a user, the domain is in the object itself + domain_id = user_ref['domain_id'] + driver = self._select_identity_driver(domain_id) + if not driver.is_domain_aware(): + user = self._clear_domain_id(user) + ref = driver.create_user(user_id, user) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def get_user(self, user_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + ref = driver.get_user(user_id) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def get_user_by_name(self, user_name, domain_id): + driver = self._select_identity_driver(domain_id) + ref = driver.get_user_by_name(user_name, domain_id) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def list_users(self, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + user_list = driver.list_users() + if not driver.is_domain_aware(): + user_list = self._set_domain_id(user_list, domain_id) + return user_list + + @domains_configured + def update_user(self, user_id, user_ref, domain_scope=None): user = user_ref.copy() if 'name' in user: user['name'] = clean.user_name(user['name']) if 'enabled' in user: user['enabled'] = clean.user_enabled(user['enabled']) - return self.driver.update_user(user_id, user) + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + if not driver.is_domain_aware(): + user = self._clear_domain_id(user) + ref = driver.update_user(user_id, user) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def delete_user(self, user_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.delete_user(user_id) + + @domains_configured def create_group(self, group_id, group_ref): group = group_ref.copy() group.setdefault('description', '') - return self.driver.create_group(group_id, group) + + # For creating a group, the domain is in the object itself + domain_id = group_ref['domain_id'] + driver = self._select_identity_driver(domain_id) + if not driver.is_domain_aware(): + group = self._clear_domain_id(group) + ref = driver.create_group(group_id, group) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def get_group(self, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + ref = driver.get_group(group_id) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def update_group(self, group_id, group, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + if not driver.is_domain_aware(): + group = self._clear_domain_id(group) + ref = driver.update_group(group_id, group) + if not driver.is_domain_aware(): + ref = self._set_domain_id(ref, domain_id) + return ref + + @domains_configured + def delete_group(self, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.delete_group(group_id) + + @domains_configured + def add_user_to_group(self, user_id, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.add_user_to_group(user_id, group_id) + + @domains_configured + def remove_user_from_group(self, user_id, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + driver.remove_user_from_group(user_id, group_id) + + @domains_configured + def list_groups_for_user(self, user_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + group_list = driver.list_groups_for_user(user_id) + if not driver.is_domain_aware(): + group_list = self._set_domain_id(group_list, domain_id) + return group_list + + @domains_configured + def list_groups(self, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + group_list = driver.list_groups() + if not driver.is_domain_aware(): + group_list = self._set_domain_id(group_list, domain_id) + return group_list + + @domains_configured + def list_users_in_group(self, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + user_list = driver.list_users_in_group(group_id) + if not driver.is_domain_aware(): + user_list = self._set_domain_id(user_list, domain_id) + return user_list + + @domains_configured + def check_user_in_group(self, user_id, group_id, domain_scope=None): + domain_id, driver = self._get_domain_id_and_driver(domain_scope) + return driver.check_user_in_group(user_id, group_id) + + # TODO(henry-nash, ayoung) The following cross calls to the assignment + # API should be removed, with the controller and tests making the correct + # calls direct to assignment. def create_project(self, tenant_id, tenant_ref): tenant = tenant_ref.copy() @@ -358,6 +677,8 @@ class Driver(object): """ raise exception.NotImplemented() - #end of identity + def is_domain_aware(self): + """Indicates if Driver supports domains.""" + raise exception.NotImplemented() - # Assignments + #end of identity diff --git a/keystone/middleware/core.py b/keystone/middleware/core.py index 863ef948..92b179c3 100644 --- a/keystone/middleware/core.py +++ b/keystone/middleware/core.py @@ -17,13 +17,12 @@ import webob.dec from keystone.common import config -from keystone.common import logging from keystone.common import serializer from keystone.common import utils from keystone.common import wsgi from keystone import exception from keystone.openstack.common import jsonutils - +from keystone.openstack.common import log as logging CONF = config.CONF LOG = logging.getLogger(__name__) diff --git a/keystone/middleware/s3_token.py b/keystone/middleware/s3_token.py index b346893b..39678591 100644 --- a/keystone/middleware/s3_token.py +++ b/keystone/middleware/s3_token.py @@ -37,8 +37,8 @@ import httplib import urllib import webob -from keystone.common import logging from keystone.openstack.common import jsonutils +from keystone.openstack.common import log as logging PROTOCOL_NAME = 'S3 Token Authentication' diff --git a/keystone/policy/backends/rules.py b/keystone/policy/backends/rules.py index 63110e69..31a26d88 100644 --- a/keystone/policy/backends/rules.py +++ b/keystone/policy/backends/rules.py @@ -19,10 +19,10 @@ import os.path -from keystone.common import logging from keystone.common import utils from keystone import config from keystone import exception +from keystone.openstack.common import log as logging from keystone.openstack.common import policy as common_policy from keystone import policy diff --git a/keystone/service.py b/keystone/service.py index ce64aba8..e3633865 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -14,19 +14,21 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import routes from keystone import assignment from keystone import auth from keystone import catalog from keystone.common import dependency -from keystone.common import logging from keystone.common import wsgi from keystone import config from keystone.contrib import ec2 +from keystone.contrib import oauth1 from keystone import controllers from keystone import credential from keystone import identity +from keystone.openstack.common import log as logging from keystone import policy from keystone import routers from keystone import token @@ -48,6 +50,7 @@ DRIVERS = dict( credentials_api=credential.Manager(), ec2_api=ec2.Manager(), identity_api=_IDENTITY_API, + oauth1_api=oauth1.Manager(), policy_api=policy.Manager(), token_api=token.Manager(), trust_api=trust.Manager(), @@ -56,7 +59,23 @@ DRIVERS = dict( dependency.resolve_future_dependencies() -@logging.fail_gracefully +def fail_gracefully(f): + """Logs exceptions and aborts.""" + @functools.wraps(f) + def wrapper(*args, **kw): + try: + return f(*args, **kw) + except Exception as e: + LOG.debug(e, exc_info=True) + + # exception message is printed to all logs + LOG.critical(e) + + exit(1) + return wrapper + + +@fail_gracefully def public_app_factory(global_conf, **local_conf): controllers.register_version('v2.0') conf = global_conf.copy() @@ -68,7 +87,7 @@ def public_app_factory(global_conf, **local_conf): routers.Extension(False)]) -@logging.fail_gracefully +@fail_gracefully def admin_app_factory(global_conf, **local_conf): conf = global_conf.copy() conf.update(local_conf) @@ -79,7 +98,7 @@ def admin_app_factory(global_conf, **local_conf): routers.Extension()]) -@logging.fail_gracefully +@fail_gracefully def public_version_app_factory(global_conf, **local_conf): conf = global_conf.copy() conf.update(local_conf) @@ -87,7 +106,7 @@ def public_version_app_factory(global_conf, **local_conf): [routers.Versions('public')]) -@logging.fail_gracefully +@fail_gracefully def admin_version_app_factory(global_conf, **local_conf): conf = global_conf.copy() conf.update(local_conf) @@ -95,7 +114,7 @@ def admin_version_app_factory(global_conf, **local_conf): [routers.Versions('admin')]) -@logging.fail_gracefully +@fail_gracefully def v3_app_factory(global_conf, **local_conf): controllers.register_version('v3') conf = global_conf.copy() diff --git a/keystone/tests/_ldap_livetest.py b/keystone/tests/_ldap_livetest.py index 59da4e66..4562ccb6 100644 --- a/keystone/tests/_ldap_livetest.py +++ b/keystone/tests/_ldap_livetest.py @@ -87,9 +87,6 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): def tearDown(self): test.TestCase.tearDown(self) - def test_user_enable_attribute_mask(self): - self.skipTest('Test is for Active Directory Only') - def test_ldap_dereferencing(self): alt_users_ldif = {'objectclass': ['top', 'organizationalUnit'], 'ou': 'alt_users'} @@ -158,3 +155,11 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): alias_dereferencing=deref) self.assertEqual(ldap.DEREF_SEARCHING, ldap_wrapper.conn.get_option(ldap.OPT_DEREF)) + + def test_user_enable_attribute_mask(self): + CONF.ldap.user_enabled_emulation = False + CONF.ldap.user_enabled_attribute = 'employeeType' + super(LiveLDAPIdentity, self).test_user_enable_attribute_mask() + + def test_create_unicode_user_name(self): + self.skipTest('Addressed by bug #1172106') diff --git a/keystone/tests/backend_multi_ldap_sql.conf b/keystone/tests/backend_multi_ldap_sql.conf new file mode 100644 index 00000000..59cff761 --- /dev/null +++ b/keystone/tests/backend_multi_ldap_sql.conf @@ -0,0 +1,35 @@ +[sql] +connection = sqlite:// +#For a file based sqlite use +#connection = sqlite:////tmp/keystone.db +#To Test MySQL: +#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#To Test PostgreSQL: +#connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 +idle_timeout = 200 + +[identity] +# common identity backend is SQL, domain specific configs will +# set their backends to ldap +driver = keystone.identity.backends.sql.Identity +# The test setup will set this to True, to allow easier creation +# of initial domain data +# domain_specific_drivers_enabled = True + +[assignment] +driver = keystone.assignment.backends.sql.Assignment + +[token] +driver = keystone.token.backends.sql.Token + +[ec2] +driver = keystone.contrib.ec2.backends.sql.Ec2 + +[catalog] +driver = keystone.catalog.backends.sql.Catalog + +[policy] +driver = keystone.policy.backends.sql.Policy + +[trust] +driver = keystone.trust.backends.sql.Trust diff --git a/keystone/tests/core.py b/keystone/tests/core.py index 21dc61dc..cba6cbf8 100644 --- a/keystone/tests/core.py +++ b/keystone/tests/core.py @@ -40,15 +40,16 @@ from keystone import assignment from keystone import catalog from keystone.common import dependency from keystone.common import kvs -from keystone.common import logging from keystone.common import sql from keystone.common import utils from keystone.common import wsgi from keystone import config from keystone.contrib import ec2 +from keystone.contrib import oauth1 from keystone import credential from keystone import exception from keystone import identity +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils from keystone import policy from keystone import token @@ -68,9 +69,6 @@ CONF = config.CONF cd = os.chdir -logging.getLogger('routes.middleware').level = logging.WARN - - def rootdir(*p): return os.path.join(ROOTDIR, *p) @@ -271,7 +269,7 @@ class TestCase(NoModule, unittest.TestCase): # assignment manager gets the default assignment driver from the # identity driver. for manager in [identity, assignment, catalog, credential, ec2, policy, - token, token_provider, trust]: + token, token_provider, trust, oauth1]: # manager.__name__ is like keystone.xxx[.yyy], # converted to xxx[_yyy] manager_name = ('%s_api' % @@ -295,9 +293,11 @@ class TestCase(NoModule, unittest.TestCase): for domain in fixtures.DOMAINS: try: rv = self.identity_api.create_domain(domain['id'], domain) - except (exception.Conflict, exception.NotImplemented): - pass - setattr(self, 'domain_%s' % domain['id'], domain) + except exception.Conflict: + rv = self.identity_api.get_domain(domain['id']) + except exception.NotImplemented: + rv = domain + setattr(self, 'domain_%s' % domain['id'], rv) for tenant in fixtures.TENANTS: try: diff --git a/keystone/tests/keystone.Default.conf b/keystone/tests/keystone.Default.conf new file mode 100644 index 00000000..7049afed --- /dev/null +++ b/keystone/tests/keystone.Default.conf @@ -0,0 +1,14 @@ +# The domain-specific configuration file for the default domain for +# use with unit tests. +# +# The domain_name of the default domain is 'Default', hence the +# strange mix of upper/lower case in the file name. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file diff --git a/keystone/tests/keystone.domain1.conf b/keystone/tests/keystone.domain1.conf new file mode 100644 index 00000000..6b7e2488 --- /dev/null +++ b/keystone/tests/keystone.domain1.conf @@ -0,0 +1,11 @@ +# The domain-specific configuration file for the test domain +# 'domain1' for use with unit tests. + +[ldap] +url = fake://memory1 +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file diff --git a/keystone/tests/keystone.domain2.conf b/keystone/tests/keystone.domain2.conf new file mode 100644 index 00000000..0ed68eb9 --- /dev/null +++ b/keystone/tests/keystone.domain2.conf @@ -0,0 +1,13 @@ +# The domain-specific configuration file for the test domain +# 'domain2' for use with unit tests. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=myroot,cn=com +group_tree_dn = ou=UserGroups,dc=myroot,dc=org +user_tree_dn = ou=Users,dc=myroot,dc=org + +[identity] +driver = keystone.identity.backends.ldap.Identity
\ No newline at end of file diff --git a/keystone/tests/test_backend.py b/keystone/tests/test_backend.py index 8622b10d..8013deec 100644 --- a/keystone/tests/test_backend.py +++ b/keystone/tests/test_backend.py @@ -105,7 +105,9 @@ class IdentityTests(object): self.assertIn(CONF.member_role_id, role_list) def test_password_hashed(self): - user_ref = self.identity_api._get_user(self.user_foo['id']) + driver = self.identity_api._select_identity_driver( + self.user_foo['domain_id']) + user_ref = driver._get_user(self.user_foo['id']) self.assertNotEqual(user_ref['password'], self.user_foo['password']) def test_create_unicode_user_name(self): @@ -1521,7 +1523,8 @@ class IdentityTests(object): self.assertRaises(exception.UserNotFound, self.identity_api.update_user, user_id, - {'id': user_id}) + {'id': user_id, + 'domain_id': DEFAULT_DOMAIN_ID}) def test_delete_user_with_project_association(self): user = {'id': uuid.uuid4().hex, @@ -1628,7 +1631,7 @@ class IdentityTests(object): tenant) def test_create_user_long_name_fails(self): - user = {'id': 'fake1', 'name': 'a' * 65, + user = {'id': 'fake1', 'name': 'a' * 256, 'domain_id': DEFAULT_DOMAIN_ID} self.assertRaises(exception.ValidationError, self.identity_api.create_user, @@ -1701,7 +1704,7 @@ class IdentityTests(object): user = {'id': 'fake1', 'name': 'fake1', 'domain_id': DEFAULT_DOMAIN_ID} self.identity_api.create_user('fake1', user) - user['name'] = 'a' * 65 + user['name'] = 'a' * 256 self.assertRaises(exception.ValidationError, self.identity_api.update_user, 'fake1', diff --git a/keystone/tests/test_backend_ldap.py b/keystone/tests/test_backend_ldap.py index 9c1c98d5..23379712 100644 --- a/keystone/tests/test_backend_ldap.py +++ b/keystone/tests/test_backend_ldap.py @@ -17,6 +17,8 @@ import uuid +import ldap + from keystone import assignment from keystone.common.ldap import fakeldap from keystone.common import sql @@ -38,8 +40,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): return self.identity_api.get_domain(CONF.identity.default_domain_id) def clear_database(self): - db = fakeldap.FakeShelve().get_instance() - db.clear() + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() + + def reload_backends(self, domain_id): + # Only one backend unless we are using separate domain backends + self.load_backends() + + def get_config(self, domain_id): + # Only one conf structure unless we are using separate domain backends + return CONF def _set_config(self): self.config([test.etcdir('keystone.conf.sample'), @@ -57,6 +67,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): user = {'id': 'fake1', 'name': 'fake1', 'password': 'fakepass1', + 'domain_id': CONF.identity.default_domain_id, 'tenants': ['bar']} self.identity_api.create_user('fake1', user) user_ref = self.identity_api.get_user('fake1') @@ -71,14 +82,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): 'fake1') def test_configurable_forbidden_user_actions(self): - CONF.ldap.user_allow_create = False - CONF.ldap.user_allow_update = False - CONF.ldap.user_allow_delete = False - self.load_backends() + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_allow_create = False + conf.ldap.user_allow_update = False + conf.ldap.user_allow_delete = False + self.reload_backends(CONF.identity.default_domain_id) user = {'id': 'fake1', 'name': 'fake1', 'password': 'fakepass1', + 'domain_id': CONF.identity.default_domain_id, 'tenants': ['bar']} self.assertRaises(exception.ForbiddenAction, self.identity_api.create_user, @@ -100,8 +113,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.user_foo.pop('password') self.assertDictEqual(user_ref, self.user_foo) - CONF.ldap.user_filter = '(CN=DOES_NOT_MATCH)' - self.load_backends() + conf = self.get_config(user_ref['domain_id']) + conf.ldap.user_filter = '(CN=DOES_NOT_MATCH)' + self.reload_backends(user_ref['domain_id']) self.assertRaises(exception.UserNotFound, self.identity_api.get_user, self.user_foo['id']) @@ -205,18 +219,21 @@ class BaseLDAPIdentity(test_backend.IdentityTests): # Create a group group_id = None - group = dict(name=uuid.uuid4().hex) + group = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) group_id = self.identity_api.create_group(group_id, group)['id'] # Create a couple of users and add them to the group. user_id = None - user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex) + user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) user_1_id = self.identity_api.create_user(user_id, user)['id'] self.identity_api.add_user_to_group(user_1_id, group_id) user_id = None - user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex) + user = dict(name=uuid.uuid4().hex, id=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) user_2_id = self.identity_api.create_user(user_id, user)['id'] self.identity_api.add_user_to_group(user_2_id, group_id) @@ -224,7 +241,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): # Delete user 2 # NOTE(blk-u): need to go directly to user interface to keep from # updating the group. - self.identity_api.driver.user.delete(user_2_id) + driver = self.identity_api._select_identity_driver( + user['domain_id']) + driver.user.delete(user_2_id) # List group users and verify only user 1. res = self.identity_api.list_users_in_group(group_id) @@ -249,13 +268,16 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.identity_api.create_user(user['id'], user) self.identity_api.add_user_to_project(self.tenant_baz['id'], user['id']) - self.identity_api.driver.user.LDAP_USER = None - self.identity_api.driver.user.LDAP_PASSWORD = None + driver = self.identity_api._select_identity_driver( + user['domain_id']) + driver.user.LDAP_USER = None + driver.user.LDAP_PASSWORD = None self.assertRaises(AssertionError, self.identity_api.authenticate, user_id=user['id'], - password=None) + password=None, + domain_scope=user['domain_id']) # (spzala)The group and domain crud tests below override the standard ones # in test_backend.py so that we can exclude the update name test, since we @@ -454,24 +476,56 @@ class LDAPIdentity(test.TestCase, BaseLDAPIdentity): self.assertNotIn('name', role_ref) def test_user_enable_attribute_mask(self): - CONF.ldap.user_enabled_attribute = 'enabled' CONF.ldap.user_enabled_mask = 2 - CONF.ldap.user_enabled_default = 512 + CONF.ldap.user_enabled_default = '512' self.clear_database() - user = {'id': 'fake1', 'name': 'fake1', 'enabled': True} - self.identity_api.create_user('fake1', user) + self.load_backends() + self.load_fixtures(default_fixtures) + + ldap_ = self.identity_api.driver.user.get_connection() + + def get_enabled_vals(): + user_dn = self.identity_api.driver.user._id_to_dn_string('fake1') + enabled_attr_name = CONF.ldap.user_enabled_attribute + + res = ldap_.search_s(user_dn, + ldap.SCOPE_BASE, + query='(sn=fake1)') + return res[0][1][enabled_attr_name] + + user = {'id': 'fake1', 'name': 'fake1', 'enabled': True, + 'domain_id': CONF.identity.default_domain_id} + + user_ref = self.identity_api.create_user('fake1', user) + + self.assertEqual(user_ref['enabled'], 512) + # TODO(blk-u): 512 seems wrong, should it be True? + + enabled_vals = get_enabled_vals() + self.assertEqual(enabled_vals, [512]) + user_ref = self.identity_api.get_user('fake1') - self.assertEqual(user_ref['enabled'], True) + self.assertIs(user_ref['enabled'], True) user['enabled'] = False - self.identity_api.update_user('fake1', user) + user_ref = self.identity_api.update_user('fake1', user) + self.assertIs(user_ref['enabled'], False) + + enabled_vals = get_enabled_vals() + self.assertEqual(enabled_vals, [514]) + user_ref = self.identity_api.get_user('fake1') - self.assertEqual(user_ref['enabled'], False) + self.assertIs(user_ref['enabled'], False) user['enabled'] = True - self.identity_api.update_user('fake1', user) + user_ref = self.identity_api.update_user('fake1', user) + self.assertIs(user_ref['enabled'], True) + + enabled_vals = get_enabled_vals() + self.assertEqual(enabled_vals, [512]) + user_ref = self.identity_api.get_user('fake1') - self.assertEqual(user_ref['enabled'], True) + self.assertIs(user_ref['enabled'], True) def test_user_api_get_connection_no_user_password(self): """Don't bind in case the user and password are blank.""" @@ -510,6 +564,7 @@ class LDAPIdentity(test.TestCase, BaseLDAPIdentity): 'id': 'extra_attributes', 'name': 'EXTRA_ATTRIBUTES', 'password': 'extra', + 'domain_id': CONF.identity.default_domain_id } self.identity_api.create_user(user['id'], user) dn, attrs = self.identity_api.driver.user._ldap_get(user['id']) @@ -743,3 +798,230 @@ class LdapIdentitySqlAssignment(sql.Base, test.TestCase, BaseLDAPIdentity): def test_role_filter(self): self.skipTest( 'N/A: Not part of SQL backend') + + +class MultiLDAPandSQLIdentity(sql.Base, test.TestCase, BaseLDAPIdentity): + """Class to test common SQL plus individual LDAP backends. + + We define a set of domains and domain-specific backends: + + - A separate LDAP backend for the default domain + - A separate LDAP backend for domain1 + - domain2 shares the same LDAP as domain1, but uses a different + tree attach point + - An SQL backend for all other domains (which will include domain3 + and domain4) + + Normally one would expect that the default domain would be handled as + part of the "other domains" - however the above provides better + test coverage since most of the existing backend tests use the default + domain. + + """ + def setUp(self): + super(MultiLDAPandSQLIdentity, self).setUp() + + self._set_config() + self.load_backends() + self.engine = self.get_engine() + sql.ModelBase.metadata.create_all(bind=self.engine) + self._setup_domain_test_data() + + # All initial domain data setup complete, time to switch on support + # for separate backends per domain. + + self.orig_config_domains_enabled = ( + config.CONF.identity.domain_specific_drivers_enabled) + self.opt_in_group('identity', domain_specific_drivers_enabled=True) + self.orig_config_dir = ( + config.CONF.identity.domain_config_dir) + self.opt_in_group('identity', domain_config_dir=test.TESTSDIR) + self._set_domain_configs() + self.clear_database() + self.load_fixtures(default_fixtures) + + def tearDown(self): + super(MultiLDAPandSQLIdentity, self).tearDown() + self.opt_in_group( + 'identity', + domain_config_dir=self.orig_config_dir) + self.opt_in_group( + 'identity', + domain_specific_drivers_enabled=self.orig_config_domains_enabled) + sql.ModelBase.metadata.drop_all(bind=self.engine) + self.engine.dispose() + sql.set_global_engine(None) + + def _set_config(self): + self.config([test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf')]) + + def _setup_domain_test_data(self): + + def create_domain(domain): + try: + ref = self.assignment_api.create_domain( + domain['id'], domain) + except exception.Conflict: + ref = ( + self.assignment_api.get_domain_by_name(domain['name'])) + return ref + + self.domain_default = create_domain(assignment.DEFAULT_DOMAIN) + self.domain1 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain1'}) + self.domain2 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain2'}) + self.domain3 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain3'}) + self.domain4 = create_domain( + {'id': uuid.uuid4().hex, 'name': 'domain4'}) + + def _set_domain_configs(self): + # We need to load the domain configs explicitly to ensure the + # test overrides are included. + self.identity_api.domain_configs._load_config( + self.identity_api.assignment_api, + [test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf'), + test.testsdir('keystone.Default.conf')], + 'Default') + self.identity_api.domain_configs._load_config( + self.identity_api.assignment_api, + [test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf'), + test.testsdir('keystone.domain1.conf')], + 'domain1') + self.identity_api.domain_configs._load_config( + self.identity_api.assignment_api, + [test.etcdir('keystone.conf.sample'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_multi_ldap_sql.conf'), + test.testsdir('keystone.domain2.conf')], + 'domain2') + + def reload_backends(self, domain_id): + # Just reload the driver for this domain - which will pickup + # any updated cfg + self.identity_api.domain_configs.reload_domain_driver( + self.identity_api.assignment_api, domain_id) + + def get_config(self, domain_id): + # Get the config for this domain, will return CONF + # if no specific config defined for this domain + return self.identity_api.domain_configs.get_domain_conf(domain_id) + + def test_list_domains(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_domain_segregation(self): + """Test that separate configs have segregated the domain. + + Test Plan: + - Create a user in each of the domains + - Make sure that you can only find a given user in its + relevant domain + - Make sure that for a backend that supports multiple domains + you can get the users via any of the domain scopes + + """ + def create_user(domain_id): + user = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain_id, + 'password': uuid.uuid4().hex, + 'enabled': True} + self.identity_api.create_user(user['id'], user) + return user + + userd = create_user(CONF.identity.default_domain_id) + user1 = create_user(self.domain1['id']) + user2 = create_user(self.domain2['id']) + user3 = create_user(self.domain3['id']) + user4 = create_user(self.domain4['id']) + + # Now check that I can read user1 with the appropriate domain + # scope, but won't find it if the wrong scope is used + + ref = self.identity_api.get_user( + userd['id'], domain_scope=CONF.identity.default_domain_id) + del userd['password'] + self.assertDictEqual(ref, userd) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain1['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain2['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain3['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + userd['id'], + domain_scope=self.domain4['id']) + + ref = self.identity_api.get_user( + user1['id'], domain_scope=self.domain1['id']) + del user1['password'] + self.assertDictEqual(ref, user1) + ref = self.identity_api.get_user( + user2['id'], domain_scope=self.domain2['id']) + del user2['password'] + self.assertDictEqual(ref, user2) + + # Domains 3 and 4 share the same backend, so you should be + # able to see user3 and 4 from either + + ref = self.identity_api.get_user( + user3['id'], domain_scope=self.domain3['id']) + del user3['password'] + self.assertDictEqual(ref, user3) + ref = self.identity_api.get_user( + user4['id'], domain_scope=self.domain4['id']) + del user4['password'] + self.assertDictEqual(ref, user4) + ref = self.identity_api.get_user( + user3['id'], domain_scope=self.domain4['id']) + self.assertDictEqual(ref, user3) + ref = self.identity_api.get_user( + user4['id'], domain_scope=self.domain3['id']) + self.assertDictEqual(ref, user4) + + def test_scanning_of_config_dir(self): + """Test the Manager class scans the config directory. + + The setup for the main tests above load the domain configs directly + so that the test overrides can be included. This test just makes sure + that the standard config directory scanning does pick up the relevant + domain config files. + + """ + # Confirm that config has drivers_enabled as True, which we will + # check has been set to False later in this test + self.assertTrue(config.CONF.identity.domain_specific_drivers_enabled) + self.load_backends() + # Execute any command to trigger the lazy loading of domain configs + self.identity_api.list_users(domain_scope=self.domain1['id']) + # ...and now check the domain configs have been set up + self.assertIn('default', self.identity_api.domain_configs) + self.assertIn(self.domain1['id'], self.identity_api.domain_configs) + self.assertIn(self.domain2['id'], self.identity_api.domain_configs) + self.assertNotIn(self.domain3['id'], self.identity_api.domain_configs) + self.assertNotIn(self.domain4['id'], self.identity_api.domain_configs) + + # Finally check that a domain specific config contains items from both + # the primary config and the domain specific config + conf = self.identity_api.domain_configs.get_domain_conf( + self.domain1['id']) + # This should now be false, as is the default, since this is not + # set in the standard primary config file + self.assertFalse(conf.identity.domain_specific_drivers_enabled) + # ..and make sure a domain-specifc options is also set + self.assertEqual(conf.ldap.url, 'fake://memory1') diff --git a/keystone/tests/test_backend_sql.py b/keystone/tests/test_backend_sql.py index 773ae862..24159eb6 100644 --- a/keystone/tests/test_backend_sql.py +++ b/keystone/tests/test_backend_sql.py @@ -81,7 +81,7 @@ class SqlModels(SqlTests): def test_user_model(self): cols = (('id', sql.String, 64), - ('name', sql.String, 64), + ('name', sql.String, 255), ('password', sql.String, 128), ('domain_id', sql.String, 64), ('enabled', sql.Boolean, None), diff --git a/keystone/tests/test_drivers.py b/keystone/tests/test_drivers.py index c83c1a89..888b365c 100644 --- a/keystone/tests/test_drivers.py +++ b/keystone/tests/test_drivers.py @@ -3,6 +3,7 @@ import unittest2 as unittest from keystone import assignment from keystone import catalog +from keystone.contrib import oauth1 from keystone import exception from keystone import identity from keystone import policy @@ -55,3 +56,7 @@ class TestDrivers(unittest.TestCase): def test_token_driver_unimplemented(self): interface = token.Driver() self.assertInterfaceNotImplemented(interface) + + def test_oauth1_driver_unimplemented(self): + interface = oauth1.Driver() + self.assertInterfaceNotImplemented(interface) diff --git a/keystone/tests/test_keystoneclient.py b/keystone/tests/test_keystoneclient.py index 7e59885d..ff2462f4 100644 --- a/keystone/tests/test_keystoneclient.py +++ b/keystone/tests/test_keystoneclient.py @@ -378,6 +378,46 @@ class KeystoneClientTests(object): client.tokens.authenticate, token=token_id) + def test_disable_tenant_invalidates_token(self): + from keystoneclient import exceptions as client_exceptions + + admin_client = self.get_client(admin=True) + foo_client = self.get_client(self.user_foo) + tenant_bar = admin_client.tenants.get(self.tenant_bar['id']) + + # Disable the tenant. + tenant_bar.update(enabled=False) + + # Test that the token has been removed. + self.assertRaises(client_exceptions.Unauthorized, + foo_client.tokens.authenticate, + token=foo_client.auth_token) + + # Test that the user access has been disabled. + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, + self.user_foo) + + def test_delete_tenant_invalidates_token(self): + from keystoneclient import exceptions as client_exceptions + + admin_client = self.get_client(admin=True) + foo_client = self.get_client(self.user_foo) + tenant_bar = admin_client.tenants.get(self.tenant_bar['id']) + + # Delete the tenant. + tenant_bar.delete() + + # Test that the token has been removed. + self.assertRaises(client_exceptions.Unauthorized, + foo_client.tokens.authenticate, + token=foo_client.auth_token) + + # Test that the user access has been disabled. + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, + self.user_foo) + def test_disable_user_invalidates_token(self): from keystoneclient import exceptions as client_exceptions @@ -495,6 +535,15 @@ class KeystoneClientTests(object): user = client.users.update_tenant( user=user, tenant=self.tenant_bar['id']) + def test_user_create_no_string_password(self): + from keystoneclient import exceptions as client_exceptions + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.users.create, + name='test_user', + password=12345, + email=uuid.uuid4().hex) + def test_user_create_no_name(self): from keystoneclient import exceptions as client_exceptions client = self.get_client(admin=True) @@ -1165,6 +1214,12 @@ class KcEssex3TestCase(CompatTestCase, KeystoneClientTests): def test_policy_crud(self): self.skipTest('N/A due to lack of endpoint CRUD') + def test_disable_tenant_invalidates_token(self): + self.skipTest('N/A') + + def test_delete_tenant_invalidates_token(self): + self.skipTest('N/A') + class Kc11TestCase(CompatTestCase, KeystoneClientTests): def get_checkout(self): diff --git a/keystone/tests/test_overrides.conf b/keystone/tests/test_overrides.conf index aac29f26..5cd522b2 100644 --- a/keystone/tests/test_overrides.conf +++ b/keystone/tests/test_overrides.conf @@ -14,6 +14,9 @@ driver = keystone.trust.backends.kvs.Trust [token] driver = keystone.token.backends.kvs.Token +[oauth1] +driver = keystone.contrib.oauth1.backends.kvs.OAuth1 + [signing] certfile = ../../examples/pki/certs/signing_cert.pem keyfile = ../../examples/pki/private/signing_key.pem diff --git a/keystone/tests/test_s3_token_middleware.py b/keystone/tests/test_s3_token_middleware.py index ec31f2ac..2d561c10 100644 --- a/keystone/tests/test_s3_token_middleware.py +++ b/keystone/tests/test_s3_token_middleware.py @@ -225,9 +225,9 @@ class S3TokenMiddlewareTestUtil(unittest.TestCase): def test_split_path_invalid_path(self): try: s3_token.split_path('o\nn e', 2) - except ValueError, err: + except ValueError as err: self.assertEquals(str(err), 'Invalid path: o%0An%20e') try: s3_token.split_path('o\nn e', 2, 3, True) - except ValueError, err: + except ValueError as err: self.assertEquals(str(err), 'Invalid path: o%0An%20e') diff --git a/keystone/tests/test_sql_migrate_extensions.py b/keystone/tests/test_sql_migrate_extensions.py index 4a529559..f9393cbe 100644 --- a/keystone/tests/test_sql_migrate_extensions.py +++ b/keystone/tests/test_sql_migrate_extensions.py @@ -27,6 +27,7 @@ To run these tests against a live database: """ from keystone.contrib import example +from keystone.contrib import oauth1 import test_sql_upgrade @@ -45,3 +46,65 @@ class SqlUpgradeExampleExtension(test_sql_upgrade.SqlMigrateBase): self.assertTableColumns('example', ['id', 'type', 'extra']) self.downgrade(0, repository=self.repo_path) self.assertTableDoesNotExist('example') + + +class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): + def repo_package(self): + return oauth1 + + def test_upgrade(self): + self.assertTableDoesNotExist('consumer') + self.assertTableDoesNotExist('request_token') + self.assertTableDoesNotExist('access_token') + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('consumer', + ['id', + 'description', + 'secret', + 'extra']) + self.assertTableColumns('request_token', + ['id', + 'request_secret', + 'verifier', + 'authorizing_user_id', + 'requested_project_id', + 'requested_roles', + 'consumer_id', + 'expires_at']) + self.assertTableColumns('access_token', + ['id', + 'access_secret', + 'authorizing_user_id', + 'project_id', + 'requested_roles', + 'consumer_id', + 'expires_at']) + + def test_downgrade(self): + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('consumer', + ['id', + 'description', + 'secret', + 'extra']) + self.assertTableColumns('request_token', + ['id', + 'request_secret', + 'verifier', + 'authorizing_user_id', + 'requested_project_id', + 'requested_roles', + 'consumer_id', + 'expires_at']) + self.assertTableColumns('access_token', + ['id', + 'access_secret', + 'authorizing_user_id', + 'project_id', + 'requested_roles', + 'consumer_id', + 'expires_at']) + self.downgrade(0, repository=self.repo_path) + self.assertTableDoesNotExist('consumer') + self.assertTableDoesNotExist('request_token') + self.assertTableDoesNotExist('access_token') diff --git a/keystone/tests/test_sql_upgrade.py b/keystone/tests/test_sql_upgrade.py index e904d6a7..0ee63433 100644 --- a/keystone/tests/test_sql_upgrade.py +++ b/keystone/tests/test_sql_upgrade.py @@ -556,6 +556,42 @@ class SqlUpgradeTests(SqlMigrateBase): insert.execute(d) session.commit() + def test_upgrade_31_to_32(self): + self.upgrade(32) + + user_table = self.select_table("user") + self.assertEquals(user_table.c.name.type.length, 255) + + def test_downgrade_32_to_31(self): + self.upgrade(32) + session = self.Session() + # NOTE(aloga): we need a different metadata object + user_table = sqlalchemy.Table('user', + sqlalchemy.MetaData(), + autoload=True, + autoload_with=self.engine) + user_id = uuid.uuid4().hex + ins = user_table.insert().values( + {'id': user_id, + 'name': 'a' * 255, + 'password': uuid.uuid4().hex, + 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID, + 'extra': '{}'}) + session.execute(ins) + session.commit() + + self.downgrade(31) + # Check that username has been truncated + q = session.query(user_table.c.name) + q = q.filter(user_table.c.id == user_id) + r = q.one() + user_name = r[0] + self.assertEquals(len(user_name), 64) + + user_table = self.select_table("user") + self.assertEquals(user_table.c.name.type.length, 64) + def test_downgrade_to_0(self): self.upgrade(self.max_version) @@ -1362,7 +1398,7 @@ class SqlUpgradeTests(SqlMigrateBase): total = connection.execute("SELECT count(*) " "from information_schema.TABLES " "where TABLE_SCHEMA='%(database)s'" % - locals()) + dict(database=database)) self.assertTrue(total.scalar() > 0, "No tables found. Wrong schema?") noninnodb = connection.execute("SELECT table_name " @@ -1370,7 +1406,7 @@ class SqlUpgradeTests(SqlMigrateBase): "where TABLE_SCHEMA='%(database)s' " "and ENGINE!='InnoDB' " "and TABLE_NAME!='migrate_version'" % - locals()) + dict(database=database)) names = [x[0] for x in noninnodb] self.assertEqual(names, [], "Non-InnoDB tables exist") diff --git a/keystone/tests/test_v3_auth.py b/keystone/tests/test_v3_auth.py index 43f87d98..1f4425ce 100644 --- a/keystone/tests/test_v3_auth.py +++ b/keystone/tests/test_v3_auth.py @@ -545,6 +545,67 @@ class TestTokenRevoking(test_v3.RestfulTestCase): headers={'X-Subject-Token': token}, expected_status=204) + def test_disabling_project_revokes_token(self): + resp = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id'])) + token = resp.headers.get('X-Subject-Token') + + # confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=204) + + # disable the project, which should invalidate the token + self.patch( + '/projects/%(project_id)s' % {'project_id': self.projectA['id']}, + body={'project': {'enabled': False}}) + + # user should no longer have access to the project + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=401) + resp = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id']), + expected_status=401) + + def test_deleting_project_revokes_token(self): + resp = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id'])) + token = resp.headers.get('X-Subject-Token') + + # confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=204) + + # delete the project, which should invalidate the token + self.delete( + '/projects/%(project_id)s' % {'project_id': self.projectA['id']}) + + # user should no longer have access to the project + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=401) + resp = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id']), + expected_status=401) + def test_deleting_group_grant_revokes_tokens(self): """Test deleting a group grant revokes tokens. diff --git a/keystone/tests/test_v3_oauth1.py b/keystone/tests/test_v3_oauth1.py new file mode 100644 index 00000000..a0ae5fc6 --- /dev/null +++ b/keystone/tests/test_v3_oauth1.py @@ -0,0 +1,574 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 copy +import os +import urlparse +import uuid + +import webtest + +from keystone.common import cms +from keystone import config +from keystone.contrib import oauth1 +from keystone.contrib.oauth1 import controllers +from keystone.tests import core + +import test_v3 + + +OAUTH_PASTE_FILE = 'v3_oauth1-paste.ini' +CONF = config.CONF + + +class OAuth1Tests(test_v3.RestfulTestCase): + def setUp(self): + super(OAuth1Tests, self).setUp() + self.controller = controllers.OAuthControllerV3() + self.base_url = CONF.public_endpoint % CONF + "v3" + self._generate_paste_config() + self.load_backends() + self.admin_app = webtest.TestApp( + self.loadapp('v3_oauth1', name='admin')) + self.public_app = webtest.TestApp( + self.loadapp('v3_oauth1', name='admin')) + + def tearDown(self): + os.remove(OAUTH_PASTE_FILE) + + def _generate_paste_config(self): + # Generate a file, based on keystone-paste.ini, + # that includes oauth_extension in the pipeline + old_pipeline = " ec2_extension " + new_pipeline = " oauth_extension ec2_extension " + + with open(core.etcdir('keystone-paste.ini'), 'r') as f: + contents = f.read() + new_contents = contents.replace(old_pipeline, new_pipeline) + with open(OAUTH_PASTE_FILE, 'w') as f: + f.write(new_contents) + + def _create_single_consumer(self): + ref = {'description': uuid.uuid4().hex} + resp = self.post( + '/OS-OAUTH1/consumers', + body={'consumer': ref}) + return resp.result.get('consumer') + + def _oauth_request(self, consumer, token=None, **kw): + return oauth1.Request.from_consumer_and_token(consumer=consumer, + token=token, + **kw) + + def _create_request_token(self, consumer, role, project_id): + params = {'requested_role_ids': role, + 'requested_project_id': project_id} + headers = {'Content-Type': 'application/json'} + url = '/OS-OAUTH1/request_token' + oreq = self._oauth_request( + consumer=consumer, + http_url=self.base_url + url, + http_method='POST', + parameters=params) + + hmac = oauth1.SignatureMethod_HMAC_SHA1() + oreq.sign_request(hmac, consumer, None) + headers.update(oreq.to_header()) + headers.update(params) + return url, headers + + def _create_access_token(self, consumer, token): + headers = {'Content-Type': 'application/json'} + url = '/OS-OAUTH1/access_token' + oreq = self._oauth_request( + consumer=consumer, token=token, + http_method='POST', + http_url=self.base_url + url) + hmac = oauth1.SignatureMethod_HMAC_SHA1() + oreq.sign_request(hmac, consumer, token) + headers.update(oreq.to_header()) + return url, headers + + def _get_oauth_token(self, consumer, token): + headers = {'Content-Type': 'application/json'} + body = {'auth': {'identity': {'methods': ['oauth1'], 'oauth1': {}}}} + url = '/auth/tokens' + oreq = self._oauth_request( + consumer=consumer, token=token, + http_method='POST', + http_url=self.base_url + url) + hmac = oauth1.SignatureMethod_HMAC_SHA1() + oreq.sign_request(hmac, consumer, token) + headers.update(oreq.to_header()) + return url, headers, body + + def _authorize_request_token(self, request_id): + return '/OS-OAUTH1/authorize/%s' % (request_id) + + +class ConsumerCRUDTests(OAuth1Tests): + + def test_consumer_create(self): + description = uuid.uuid4().hex + ref = {'description': description} + resp = self.post( + '/OS-OAUTH1/consumers', + body={'consumer': ref}) + consumer = resp.result.get('consumer') + consumer_id = consumer.get('id') + self.assertEqual(consumer.get('description'), description) + self.assertIsNotNone(consumer_id) + self.assertIsNotNone(consumer.get('secret')) + + def test_consumer_delete(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + resp = self.delete('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': consumer_id}) + self.assertResponseStatus(resp, 204) + + def test_consumer_get(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + resp = self.get('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': consumer_id}) + self.assertTrue(resp.result.get('consumer').get('id'), consumer_id) + + def test_consumer_list(self): + resp = self.get('/OS-OAUTH1/consumers') + entities = resp.result.get('consumers') + self.assertIsNotNone(entities) + self.assertValidListLinks(resp.result.get('links')) + + def test_consumer_update(self): + consumer = self._create_single_consumer() + original_id = consumer.get('id') + original_description = consumer.get('description') + original_secret = consumer.get('secret') + update_description = original_description + "_new" + + update_ref = {'description': update_description} + update_resp = self.patch('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': original_id}, + body={'consumer': update_ref}) + consumer = update_resp.result.get('consumer') + self.assertEqual(consumer.get('description'), update_description) + self.assertEqual(consumer.get('id'), original_id) + self.assertEqual(consumer.get('secret'), original_secret) + + def test_consumer_update_bad_secret(self): + consumer = self._create_single_consumer() + original_id = consumer.get('id') + update_ref = copy.deepcopy(consumer) + update_ref['description'] = uuid.uuid4().hex + update_ref['secret'] = uuid.uuid4().hex + self.patch('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': original_id}, + body={'consumer': update_ref}, + expected_status=400) + + def test_consumer_update_bad_id(self): + consumer = self._create_single_consumer() + original_id = consumer.get('id') + original_description = consumer.get('description') + update_description = original_description + "_new" + + update_ref = copy.deepcopy(consumer) + update_ref['description'] = update_description + update_ref['id'] = update_description + self.patch('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': original_id}, + body={'consumer': update_ref}, + expected_status=400) + + def test_consumer_create_no_description(self): + resp = self.post('/OS-OAUTH1/consumers', body={'consumer': {}}) + consumer = resp.result.get('consumer') + consumer_id = consumer.get('id') + self.assertEqual(consumer.get('description'), None) + self.assertIsNotNone(consumer_id) + self.assertIsNotNone(consumer.get('secret')) + + def test_consumer_get_bad_id(self): + self.get('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': uuid.uuid4().hex}, + expected_status=404) + + +class OAuthFlowTests(OAuth1Tests): + + def test_oauth_flow(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + self.consumer = oauth1.Consumer(consumer_id, consumer_secret) + self.assertIsNotNone(self.consumer.key) + + url, headers = self._create_request_token(self.consumer, + self.role_id, + self.project_id) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + request_key = credentials.get('oauth_token')[0] + request_secret = credentials.get('oauth_token_secret')[0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + resp = self.put(url, expected_status=200) + self.verifier = resp.result['token']['oauth_verifier'] + + self.request_token.set_verifier(self.verifier) + url, headers = self._create_access_token(self.consumer, + self.request_token) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + access_key = credentials.get('oauth_token')[0] + access_secret = credentials.get('oauth_token_secret')[0] + self.access_token = oauth1.Token(access_key, access_secret) + self.assertIsNotNone(self.access_token.key) + + url, headers, body = self._get_oauth_token(self.consumer, + self.access_token) + content = self.post(url, headers=headers, body=body) + self.keystone_token_id = content.headers.get('X-Subject-Token') + self.keystone_token = content.result.get('token') + self.assertIsNotNone(self.keystone_token_id) + + +class AccessTokenCRUDTests(OAuthFlowTests): + def test_delete_access_token_dne(self): + self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': uuid.uuid4().hex}, + expected_status=404) + + def test_list_no_access_tokens(self): + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result.get('access_tokens') + self.assertTrue(len(entities) == 0) + self.assertValidListLinks(resp.result.get('links')) + + def test_get_single_access_token(self): + self.test_oauth_flow() + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(key)s' + % {'user_id': self.user_id, + 'key': self.access_token.key}) + entity = resp.result.get('access_token') + self.assertTrue(entity['id'], self.access_token.key) + self.assertTrue(entity['consumer_id'], self.consumer.key) + + def test_get_access_token_dne(self): + self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(key)s' + % {'user_id': self.user_id, + 'key': uuid.uuid4().hex}, + expected_status=404) + + def test_list_all_roles_in_access_token(self): + self.test_oauth_flow() + resp = self.get('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles' + % {'id': self.user_id, + 'key': self.access_token.key}) + entities = resp.result.get('roles') + self.assertTrue(len(entities) > 0) + self.assertValidListLinks(resp.result.get('links')) + + def test_get_role_in_access_token(self): + self.test_oauth_flow() + url = ('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles/%(role)s' + % {'id': self.user_id, 'key': self.access_token.key, + 'role': self.role_id}) + resp = self.get(url) + entity = resp.result.get('role') + self.assertTrue(entity['id'], self.role_id) + + def test_get_role_in_access_token_dne(self): + self.test_oauth_flow() + url = ('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles/%(role)s' + % {'id': self.user_id, 'key': self.access_token.key, + 'role': uuid.uuid4().hex}) + self.get(url, expected_status=404) + + def test_list_and_delete_access_tokens(self): + self.test_oauth_flow() + # List access_tokens should be > 0 + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result.get('access_tokens') + self.assertTrue(len(entities) > 0) + self.assertValidListLinks(resp.result.get('links')) + + # Delete access_token + resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': self.access_token.key}) + self.assertResponseStatus(resp, 204) + + # List access_token should be 0 + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result.get('access_tokens') + self.assertTrue(len(entities) == 0) + self.assertValidListLinks(resp.result.get('links')) + + +class AuthTokenTests(OAuthFlowTests): + + def test_keystone_token_is_valid(self): + self.test_oauth_flow() + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidTokenResponse(r, self.user) + + # now verify the oauth section + oauth_section = r.result['token']['OS-OAUTH1'] + self.assertEquals(oauth_section['access_token_id'], + self.access_token.key) + self.assertEquals(oauth_section['consumer_id'], self.consumer.key) + + def test_delete_access_token_also_revokes_token(self): + self.test_oauth_flow() + + # Delete access token + resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': self.access_token.key}) + self.assertResponseStatus(resp, 204) + + # Check Keystone Token no longer exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.get('/auth/tokens', headers=headers, + expected_status=401) + + def test_deleting_consumer_also_deletes_tokens(self): + self.test_oauth_flow() + + # Delete consumer + consumer_id = self.consumer.key + resp = self.delete('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': consumer_id}) + self.assertResponseStatus(resp, 204) + + # List access_token should be 0 + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result.get('access_tokens') + self.assertEqual(len(entities), 0) + + # Check Keystone Token no longer exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.head('/auth/tokens', headers=headers, + expected_status=401) + + def test_change_user_password_also_deletes_tokens(self): + self.test_oauth_flow() + + # delegated keystone token exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidTokenResponse(r, self.user) + + user = {'password': uuid.uuid4().hex} + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body={'user': user}) + + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.admin_request(path='/auth/tokens', headers=headers, + method='GET', expected_status=404) + + def test_deleting_project_also_invalidates_tokens(self): + self.test_oauth_flow() + + # delegated keystone token exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidTokenResponse(r, self.user) + + r = self.delete('/projects/%(project_id)s' % { + 'project_id': self.project_id}) + + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.admin_request(path='/auth/tokens', headers=headers, + method='GET', expected_status=404) + + def test_token_chaining_is_not_allowed(self): + self.test_oauth_flow() + + #attempt to re-authenticate (token chain) with the given token + path = '/v3/auth/tokens/' + auth_data = self.build_authentication_request( + token=self.keystone_token_id) + + self.admin_request( + path=path, + body=auth_data, + token=self.keystone_token_id, + method='POST', + expected_status=403) + + def test_list_keystone_tokens_by_consumer(self): + self.test_oauth_flow() + tokens = self.token_api.list_tokens(self.user_id, + consumer_id=self.consumer.key) + keystone_token_uuid = cms.cms_hash_token(self.keystone_token_id) + self.assertTrue(len(tokens) > 0) + self.assertTrue(keystone_token_uuid in tokens) + + +class MaliciousOAuth1Tests(OAuth1Tests): + + def test_bad_consumer_secret(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer = oauth1.Consumer(consumer_id, "bad_secret") + url, headers = self._create_request_token(consumer, + self.role_id, + self.project_id) + self.post(url, headers=headers, expected_status=500) + + def test_bad_request_token_key(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + consumer = oauth1.Consumer(consumer_id, consumer_secret) + url, headers = self._create_request_token(consumer, + self.role_id, + self.project_id) + self.post(url, headers=headers) + url = self._authorize_request_token("bad_key") + self.put(url, expected_status=404) + + def test_bad_verifier(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + consumer = oauth1.Consumer(consumer_id, consumer_secret) + + url, headers = self._create_request_token(consumer, + self.role_id, + self.project_id) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + request_key = credentials.get('oauth_token')[0] + request_secret = credentials.get('oauth_token_secret')[0] + request_token = oauth1.Token(request_key, request_secret) + + url = self._authorize_request_token(request_key) + resp = self.put(url, expected_status=200) + verifier = resp.result['token']['oauth_verifier'] + self.assertIsNotNone(verifier) + + request_token.set_verifier("bad verifier") + url, headers = self._create_access_token(consumer, + request_token) + self.post(url, headers=headers, expected_status=401) + + def test_bad_requested_roles(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + consumer = oauth1.Consumer(consumer_id, consumer_secret) + + url, headers = self._create_request_token(consumer, + "bad_role", + self.project_id) + self.post(url, headers=headers, expected_status=401) + + def test_bad_authorizing_roles(self): + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + consumer = oauth1.Consumer(consumer_id, consumer_secret) + + url, headers = self._create_request_token(consumer, + self.role_id, + self.project_id) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + request_key = credentials.get('oauth_token')[0] + + self.identity_api.remove_role_from_user_and_project(self.user_id, + self.project_id, + self.role_id) + url = self._authorize_request_token(request_key) + self.admin_request(path=url, method='PUT', expected_status=404) + + def test_expired_authorizing_request_token(self): + CONF.oauth1.request_token_duration = -1 + + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + self.consumer = oauth1.Consumer(consumer_id, consumer_secret) + self.assertIsNotNone(self.consumer.key) + + url, headers = self._create_request_token(self.consumer, + self.role_id, + self.project_id) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + request_key = credentials.get('oauth_token')[0] + request_secret = credentials.get('oauth_token_secret')[0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + self.put(url, expected_status=401) + + def test_expired_creating_keystone_token(self): + CONF.oauth1.access_token_duration = -1 + consumer = self._create_single_consumer() + consumer_id = consumer.get('id') + consumer_secret = consumer.get('secret') + self.consumer = oauth1.Consumer(consumer_id, consumer_secret) + self.assertIsNotNone(self.consumer.key) + + url, headers = self._create_request_token(self.consumer, + self.role_id, + self.project_id) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + request_key = credentials.get('oauth_token')[0] + request_secret = credentials.get('oauth_token_secret')[0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + resp = self.put(url, expected_status=200) + self.verifier = resp.result['token']['oauth_verifier'] + + self.request_token.set_verifier(self.verifier) + url, headers = self._create_access_token(self.consumer, + self.request_token) + content = self.post(url, headers=headers) + credentials = urlparse.parse_qs(content.result) + access_key = credentials.get('oauth_token')[0] + access_secret = credentials.get('oauth_token_secret')[0] + self.access_token = oauth1.Token(access_key, access_secret) + self.assertIsNotNone(self.access_token.key) + + url, headers, body = self._get_oauth_token(self.consumer, + self.access_token) + self.post(url, headers=headers, body=body, expected_status=401) diff --git a/keystone/token/backends/kvs.py b/keystone/token/backends/kvs.py index c3c3e769..b2c6ed30 100644 --- a/keystone/token/backends/kvs.py +++ b/keystone/token/backends/kvs.py @@ -17,8 +17,8 @@ import copy from keystone.common import kvs -from keystone.common import logging from keystone import exception +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils from keystone import token @@ -90,6 +90,29 @@ class Token(kvs.Base, token.Driver): tokens.append(token.split('-', 1)[1]) return tokens + def _consumer_matches(self, consumer_id, token_ref_dict): + if consumer_id is None: + return True + else: + if 'token_data' in token_ref_dict: + token_data = token_ref_dict.get('token_data') + if 'token' in token_data: + token = token_data.get('token') + oauth = token.get('OS-OAUTH1') + if oauth and oauth.get('consumer_id') == consumer_id: + return True + return False + + def _list_tokens_for_consumer(self, consumer_id): + tokens = [] + now = timeutils.utcnow() + for token, ref in self.db.items(): + if not token.startswith('token-') or self.is_expired(now, ref): + continue + if self._consumer_matches(consumer_id, ref): + tokens.append(token.split('-', 1)[1]) + return tokens + def _list_tokens_for_user(self, user_id, tenant_id=None): def user_matches(user_id, ref): return ref.get('user') and ref['user'].get('id') == user_id @@ -110,9 +133,12 @@ class Token(kvs.Base, token.Driver): tokens.append(token.split('-', 1)[1]) return tokens - def list_tokens(self, user_id, tenant_id=None, trust_id=None): + def list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): if trust_id: return self._list_tokens_for_trust(trust_id) + if consumer_id: + return self._list_tokens_for_consumer(consumer_id) else: return self._list_tokens_for_user(user_id, tenant_id) diff --git a/keystone/token/backends/memcache.py b/keystone/token/backends/memcache.py index 06e89d60..b80d01bc 100644 --- a/keystone/token/backends/memcache.py +++ b/keystone/token/backends/memcache.py @@ -19,18 +19,16 @@ import copy import memcache -from keystone.common import logging from keystone.common import utils from keystone import config from keystone import exception from keystone.openstack.common import jsonutils +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils from keystone import token CONF = config.CONF -config.register_str('servers', group='memcache', default='localhost:11211') -config.register_int('max_compare_and_set_retry', group='memcache', default=16) LOG = logging.getLogger(__name__) @@ -180,7 +178,8 @@ class Token(token.Driver): self._add_to_revocation_list(data) return result - def list_tokens(self, user_id, tenant_id=None, trust_id=None): + def list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): tokens = [] user_key = self._prefix_user_id(user_id) user_record = self.client.get(user_key) or "" @@ -201,6 +200,13 @@ class Token(token.Driver): continue if trust != trust_id: continue + if consumer_id is not None: + try: + oauth = token_ref['token_data']['token']['OS-OAUTH1'] + if oauth.get('consumer_id') != consumer_id: + continue + except KeyError: + continue tokens.append(token_id) return tokens diff --git a/keystone/token/backends/sql.py b/keystone/token/backends/sql.py index 82eab651..5d24fb4f 100644 --- a/keystone/token/backends/sql.py +++ b/keystone/token/backends/sql.py @@ -78,7 +78,8 @@ class Token(sql.Base, token.Driver): token_ref.valid = False session.flush() - def delete_tokens(self, user_id, tenant_id=None, trust_id=None): + def delete_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): """Deletes all tokens in one session The user_id will be ignored if the trust_id is specified. user_id @@ -103,6 +104,11 @@ class Token(sql.Base, token.Driver): token_ref_dict = token_ref.to_dict() if not self._tenant_matches(tenant_id, token_ref_dict): continue + if consumer_id: + token_ref_dict = token_ref.to_dict() + if not self._consumer_matches(consumer_id, token_ref_dict): + continue + token_ref.valid = False session.flush() @@ -112,6 +118,13 @@ class Token(sql.Base, token.Driver): (token_ref_dict.get('tenant') and token_ref_dict['tenant'].get('id') == tenant_id)) + def _consumer_matches(self, consumer_id, token_ref_dict): + if consumer_id is None: + return True + else: + oauth = token_ref_dict['token_data']['token'].get('OS-OAUTH1', {}) + return oauth and oauth['consumer_id'] == consumer_id + def _list_tokens_for_trust(self, trust_id): session = self.get_session() tokens = [] @@ -141,9 +154,29 @@ class Token(sql.Base, token.Driver): tokens.append(token_ref['id']) return tokens - def list_tokens(self, user_id, tenant_id=None, trust_id=None): + def _list_tokens_for_consumer(self, user_id, consumer_id): + tokens = [] + session = self.get_session() + with session.begin(): + now = timeutils.utcnow() + query = session.query(TokenModel) + query = query.filter(TokenModel.expires > now) + query = query.filter(TokenModel.user_id == user_id) + token_references = query.filter_by(valid=True) + + for token_ref in token_references: + token_ref_dict = token_ref.to_dict() + if self._consumer_matches(consumer_id, token_ref_dict): + tokens.append(token_ref_dict['id']) + session.flush() + return tokens + + def list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): if trust_id: return self._list_tokens_for_trust(trust_id) + if consumer_id: + return self._list_tokens_for_consumer(user_id, consumer_id) else: return self._list_tokens_for_user(user_id, tenant_id) diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py index 91514493..954ff8e8 100644 --- a/keystone/token/controllers.py +++ b/keystone/token/controllers.py @@ -3,10 +3,10 @@ import json from keystone.common import cms from keystone.common import controller from keystone.common import dependency -from keystone.common import logging from keystone.common import wsgi from keystone import config from keystone import exception +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils from keystone.token import core from keystone.token import provider as token_provider diff --git a/keystone/token/core.py b/keystone/token/core.py index bc27b80d..7eadbe63 100644 --- a/keystone/token/core.py +++ b/keystone/token/core.py @@ -21,15 +21,15 @@ import datetime from keystone.common import cms from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils CONF = config.CONF -config.register_int('expiration', group='token', default=86400) + LOG = logging.getLogger(__name__) @@ -174,41 +174,51 @@ class Driver(object): """ raise exception.NotImplemented() - def delete_tokens(self, user_id, tenant_id=None, trust_id=None): + def delete_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): """Deletes tokens by user. If the tenant_id is not None, only delete the tokens by user id under the specified tenant. If the trust_id is not None, it will be used to query tokens and the user_id will be ignored. + If the consumer_id is not None, only delete the tokens by consumer id + that match the specified consumer id :param user_id: identity of user :type user_id: string :param tenant_id: identity of the tenant :type tenant_id: string - :param trust_id: identified of the trust + :param trust_id: identity of the trust :type trust_id: string + :param consumer_id: identity of the consumer + :type consumer_id: string :returns: None. :raises: keystone.exception.TokenNotFound """ token_list = self.list_tokens(user_id, tenant_id=tenant_id, - trust_id=trust_id) + trust_id=trust_id, + consumer_id=consumer_id) + for token in token_list: try: self.delete_token(token) except exception.NotFound: pass - def list_tokens(self, user_id, tenant_id=None, trust_id=None): + def list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): """Returns a list of current token_id's for a user :param user_id: identity of the user :type user_id: string :param tenant_id: identity of the tenant :type tenant_id: string - :param trust_id: identified of the trust + :param trust_id: identity of the trust :type trust_id: string + :param consumer_id: identity of the consumer + :type consumer_id: string :returns: list of token_id's """ diff --git a/keystone/token/provider.py b/keystone/token/provider.py index 2864be6f..f2acb0e1 100644 --- a/keystone/token/provider.py +++ b/keystone/token/provider.py @@ -18,10 +18,10 @@ from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import log as logging CONF = config.CONF diff --git a/keystone/token/providers/pki.py b/keystone/token/providers/pki.py index 81abe5d4..64dde473 100644 --- a/keystone/token/providers/pki.py +++ b/keystone/token/providers/pki.py @@ -20,9 +20,9 @@ import json from keystone.common import cms from keystone.common import environment -from keystone.common import logging from keystone import config from keystone import exception +from keystone.openstack.common import log as logging from keystone.token.providers import uuid diff --git a/keystone/token/providers/uuid.py b/keystone/token/providers/uuid.py index acfa9372..612df999 100644 --- a/keystone/token/providers/uuid.py +++ b/keystone/token/providers/uuid.py @@ -18,6 +18,7 @@ from __future__ import absolute_import +import json import sys import uuid @@ -206,12 +207,23 @@ class V3TokenDataHelper(object): 'domain': self._get_filtered_domain(user_ref['domain_id'])} token_data['user'] = filtered_user + def _populate_oauth_section(self, token_data, access_token): + if access_token: + access_token_id = access_token['id'] + consumer_id = access_token['consumer_id'] + token_data['OS-OAUTH1'] = ({'access_token_id': access_token_id, + 'consumer_id': consumer_id}) + def _populate_roles(self, token_data, user_id, domain_id, project_id, - trust): + trust, access_token): if 'roles' in token_data: # no need to repopulate roles return + if access_token: + token_data['roles'] = json.loads(access_token['requested_roles']) + return + if CONF.trust.enabled and trust: token_user_id = trust['trustor_user_id'] token_project_id = trust['project_id'] @@ -288,7 +300,7 @@ class V3TokenDataHelper(object): def get_token_data(self, user_id, method_names, extras, domain_id=None, project_id=None, expires=None, trust=None, token=None, include_catalog=True, - bind=None): + bind=None, access_token=None): token_data = {'methods': method_names, 'extras': extras} @@ -307,15 +319,17 @@ class V3TokenDataHelper(object): self._populate_scope(token_data, domain_id, project_id) self._populate_user(token_data, user_id, domain_id, project_id, trust) - self._populate_roles(token_data, user_id, domain_id, project_id, trust) + self._populate_roles(token_data, user_id, domain_id, project_id, trust, + access_token) if include_catalog: self._populate_service_catalog(token_data, user_id, domain_id, project_id, trust) self._populate_token_dates(token_data, expires=expires, trust=trust) + self._populate_oauth_section(token_data, access_token) return {'token': token_data} -@dependency.requires('token_api', 'identity_api', 'catalog_api') +@dependency.requires('token_api', 'identity_api', 'catalog_api', 'oauth_api') class Provider(token.provider.Provider): def __init__(self, *args, **kwargs): super(Provider, self).__init__(*args, **kwargs) @@ -380,6 +394,12 @@ class Provider(token.provider.Provider): if (CONF.trust.enabled and not trust and metadata_ref and 'trust_id' in metadata_ref): trust = self.trust_api.get_trust(metadata_ref['trust_id']) + + access_token = None + if 'oauth1' in method_names: + access_token_id = auth_context['access_token_id'] + access_token = self.oauth_api.get_access_token(access_token_id) + token_data = self.v3_token_data_helper.get_token_data( user_id, method_names, @@ -389,7 +409,8 @@ class Provider(token.provider.Provider): expires=expires_at, trust=trust, bind=auth_context.get('bind') if auth_context else None, - include_catalog=include_catalog) + include_catalog=include_catalog, + access_token=access_token) token_id = self._get_token_id(token_data) try: diff --git a/keystone/trust/controllers.py b/keystone/trust/controllers.py index 7a94fe29..3d8df459 100644 --- a/keystone/trust/controllers.py +++ b/keystone/trust/controllers.py @@ -2,10 +2,10 @@ import uuid from keystone.common import controller from keystone.common import dependency -from keystone.common import logging from keystone import config from keystone import exception from keystone import identity +from keystone.openstack.common import log as logging from keystone.openstack.common import timeutils diff --git a/keystone/trust/core.py b/keystone/trust/core.py index 5c4fc90f..e4ff74de 100644 --- a/keystone/trust/core.py +++ b/keystone/trust/core.py @@ -17,10 +17,10 @@ """Main entry point into the Identity service.""" from keystone.common import dependency -from keystone.common import logging from keystone.common import manager from keystone import config from keystone import exception +from keystone.openstack.common import log as logging CONF = config.CONF |