summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.rst13
-rw-r--r--doc/source/configuration.rst20
-rw-r--r--etc/keystone-paste.ini3
-rw-r--r--etc/keystone.conf.sample21
-rw-r--r--keystone/assignment/backends/ldap.py2
-rw-r--r--keystone/assignment/core.py2
-rw-r--r--keystone/auth/controllers.py6
-rw-r--r--keystone/auth/core.py72
-rw-r--r--keystone/auth/plugins/oauth1.py80
-rw-r--r--keystone/auth/plugins/password.py6
-rw-r--r--keystone/auth/plugins/token.py11
-rw-r--r--keystone/catalog/backends/templated.py5
-rw-r--r--keystone/catalog/core.py2
-rw-r--r--keystone/clean.py20
-rw-r--r--keystone/cli.py4
-rw-r--r--keystone/common/cms.py2
-rw-r--r--keystone/common/config.py535
-rw-r--r--keystone/common/controller.py58
-rw-r--r--keystone/common/environment/__init__.py2
-rw-r--r--keystone/common/environment/eventlet_server.py13
-rw-r--r--keystone/common/ldap/core.py9
-rw-r--r--keystone/common/ldap/fakeldap.py20
-rw-r--r--keystone/common/openssl.py3
-rw-r--r--keystone/common/sql/core.py2
-rw-r--r--keystone/common/sql/legacy.py2
-rw-r--r--keystone/common/sql/migrate_repo/versions/032_username_length.py31
-rw-r--r--keystone/common/sql/nova.py2
-rw-r--r--keystone/common/utils.py3
-rw-r--r--keystone/common/wsgi.py19
-rw-r--r--keystone/config.py9
-rw-r--r--keystone/contrib/access/core.py3
-rw-r--r--keystone/contrib/oauth1/__init__.py17
-rw-r--r--keystone/contrib/oauth1/backends/__init__.py15
-rw-r--r--keystone/contrib/oauth1/backends/kvs.py222
-rw-r--r--keystone/contrib/oauth1/backends/sql.py284
-rw-r--r--keystone/contrib/oauth1/controllers.py377
-rw-r--r--keystone/contrib/oauth1/core.py272
-rw-r--r--keystone/contrib/oauth1/migrate_repo/__init__.py15
-rw-r--r--keystone/contrib/oauth1/migrate_repo/migrate.cfg25
-rw-r--r--keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py69
-rw-r--r--keystone/contrib/oauth1/migrate_repo/versions/__init__.py15
-rw-r--r--keystone/contrib/oauth1/routers.py129
-rw-r--r--keystone/contrib/stats/core.py2
-rw-r--r--keystone/contrib/user_crud/core.py4
-rw-r--r--keystone/controllers.py2
-rw-r--r--keystone/credential/core.py2
-rw-r--r--keystone/exception.py2
-rw-r--r--keystone/identity/backends/kvs.py3
-rw-r--r--keystone/identity/backends/ldap.py61
-rw-r--r--keystone/identity/backends/pam.py3
-rw-r--r--keystone/identity/backends/sql.py5
-rw-r--r--keystone/identity/controllers.py71
-rw-r--r--keystone/identity/core.py337
-rw-r--r--keystone/middleware/core.py3
-rw-r--r--keystone/middleware/s3_token.py2
-rw-r--r--keystone/policy/backends/rules.py2
-rw-r--r--keystone/service.py31
-rw-r--r--keystone/tests/_ldap_livetest.py11
-rw-r--r--keystone/tests/backend_multi_ldap_sql.conf35
-rw-r--r--keystone/tests/core.py16
-rw-r--r--keystone/tests/keystone.Default.conf14
-rw-r--r--keystone/tests/keystone.domain1.conf11
-rw-r--r--keystone/tests/keystone.domain2.conf13
-rw-r--r--keystone/tests/test_backend.py11
-rw-r--r--keystone/tests/test_backend_ldap.py330
-rw-r--r--keystone/tests/test_backend_sql.py2
-rw-r--r--keystone/tests/test_drivers.py5
-rw-r--r--keystone/tests/test_keystoneclient.py55
-rw-r--r--keystone/tests/test_overrides.conf3
-rw-r--r--keystone/tests/test_s3_token_middleware.py4
-rw-r--r--keystone/tests/test_sql_migrate_extensions.py63
-rw-r--r--keystone/tests/test_sql_upgrade.py40
-rw-r--r--keystone/tests/test_v3_auth.py61
-rw-r--r--keystone/tests/test_v3_oauth1.py574
-rw-r--r--keystone/token/backends/kvs.py30
-rw-r--r--keystone/token/backends/memcache.py14
-rw-r--r--keystone/token/backends/sql.py37
-rw-r--r--keystone/token/controllers.py2
-rw-r--r--keystone/token/core.py24
-rw-r--r--keystone/token/provider.py2
-rw-r--r--keystone/token/providers/pki.py2
-rw-r--r--keystone/token/providers/uuid.py31
-rw-r--r--keystone/trust/controllers.py2
-rw-r--r--keystone/trust/core.py2
-rw-r--r--requirements.txt1
-rwxr-xr-xrun_tests.sh5
-rw-r--r--tox.ini2
87 files changed, 3763 insertions, 589 deletions
diff --git a/README.rst b/README.rst
index c0b3e722..9e7e8ce8 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,6 @@
-.. image:: http://term.ie/data/medium_ksl.png
- :alt: Keystone
-
-.. toctree::
- :maxdepth 2
+========
+Keystone
+========
Keystone is an OpenStack project that provides Identity, Token, Catalog and
Policy services for use specifically by projects in the OpenStack family.
@@ -30,10 +28,7 @@ Building the Documentation
The documentation is all generated with Sphinx from within the doc directory.
To generate the full set of HTML documentation::
- cd doc
- make autodoc
- make html
- make man
+ tox -evenv python setup.py build_sphinx
the results are in the ``doc/build/html`` and ``doc/build/man`` directories
respectively.
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 1c339490..b4af31f8 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -72,6 +72,7 @@ following sections:
* ``[sql]`` - optional storage backend configuration
* ``[ec2]`` - Amazon EC2 authentication driver configuration
* ``[s3]`` - Amazon S3 authentication driver configuration.
+* ``[oauth1]`` - Oauth 1.0a system driver configuration
* ``[identity]`` - identity system driver configuration
* ``[catalog]`` - service catalog driver configuration
* ``[token]`` - token driver & token provider configuration
@@ -96,6 +97,25 @@ order:
PasteDeploy configuration file is specified by the ``config_file`` parameter in ``[paste_deploy]`` section of the primary configuration file. If the parameter
is not an absolute path, then Keystone looks for it in the same directories as above. If not specified, WSGI pipeline definitions are loaded from the primary configuration file.
+Keystone supports the option (disabled by default) to specify identity driver
+configurations on a domain by domain basis, allowing, for example, a specific
+domain to have its own LDAP or SQL server. This is configured by specifying the
+following options::
+
+ [identity]
+ domain_specific_drivers_enabled = True
+ domain_config_dir = /etc/keystone/domains
+
+Setting ``domain_specific_drivers_enabled`` to True will enable this feature, causing
+keystone to look in the ``domain_config_dir`` for config files of the form::
+
+ keystone.<domain_name>.conf
+
+Options given in the domain specific configuration file will override those in the
+primary configuration file for the specified domain only. Domains without a specific
+configuration file will continue to use the options from the primary configuration
+file.
+
Authentication Plugins
----------------------
diff --git a/etc/keystone-paste.ini b/etc/keystone-paste.ini
index 0f4590a2..9c5545db 100644
--- a/etc/keystone-paste.ini
+++ b/etc/keystone-paste.ini
@@ -24,6 +24,9 @@ paste.filter_factory = keystone.contrib.admin_crud:CrudExtension.factory
[filter:ec2_extension]
paste.filter_factory = keystone.contrib.ec2:Ec2Extension.factory
+[filter:oauth_extension]
+paste.filter_factory = keystone.contrib.oauth1.routers:OAuth1Extension.factory
+
[filter:s3_extension]
paste.filter_factory = keystone.contrib.s3:S3Extension.factory
diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample
index 90efe5f6..13d14317 100644
--- a/etc/keystone.conf.sample
+++ b/etc/keystone.conf.sample
@@ -99,6 +99,14 @@
# There is nothing special about this domain, other than the fact that it must
# exist to order to maintain support for your v2 clients.
# default_domain_id = default
+#
+# A subset (or all) of domains can have their own identity driver, each with
+# their own partial configuration file in a domain configuration directory.
+# Only values specific to the domain need to be placed in the domain specific
+# configuration file. This feature is disabled by default; set
+# domain_specific_drivers_enabled to True to enable.
+# domain_specific_drivers_enabled = False
+# domain_config_dir = /etc/keystone/domains
# Maximum supported length for user passwords; decrease to improve performance.
# max_password_length = 4096
@@ -155,6 +163,16 @@
[assignment]
# driver =
+[oauth1]
+# driver = keystone.contrib.oauth1.backends.sql.OAuth1
+
+# The Identity service may include expire attributes.
+# If no such attribute is included, then the token lasts indefinitely.
+# Specify how quickly the request token will expire (in seconds)
+# request_token_duration = 28800
+# Specify how quickly the access token will expire (in seconds)
+# access_token_duration = 86400
+
[ssl]
#enable = True
#certfile = /etc/keystone/pki/certs/ssl_cert.pem
@@ -281,10 +299,11 @@
# user_additional_attribute_mapping =
[auth]
-methods = external,password,token
+methods = external,password,token,oauth1
#external = keystone.auth.plugins.external.ExternalDefault
password = keystone.auth.plugins.password.Password
token = keystone.auth.plugins.token.Token
+oauth1 = keystone.auth.plugins.oauth1.OAuth
[paste_deploy]
# Name of the paste configuration file that defines the available pipelines
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
diff --git a/requirements.txt b/requirements.txt
index b57a91fe..7b6190d8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,3 +17,4 @@ iso8601>=0.1.4
python-keystoneclient>=0.3.0
oslo.config>=1.1.0
Babel>=0.9.6
+oauth2
diff --git a/run_tests.sh b/run_tests.sh
index 19f3f0f4..7916b4d3 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -27,8 +27,9 @@ function usage {
echo " -x, --stop Stop running tests after the first error or failure."
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -u, --update Update the virtual environment with any newer package versions"
- echo " -p, --pep8 Just run pep8"
- echo " -P, --no-pep8 Don't run pep8"
+ echo " -p, --pep8 Just run flake8"
+ echo " -8, --8 Just run flake8, don't show PEP8 text for each error"
+ echo " -P, --no-pep8 Don't run flake8"
echo " -c, --coverage Generate coverage report"
echo " -h, --help Print this usage message"
echo " -xintegration Ignore all keystoneclient test cases (integration tests)"
diff --git a/tox.ini b/tox.ini
index 1fe184ea..125b7845 100644
--- a/tox.ini
+++ b/tox.ini
@@ -34,7 +34,7 @@ commands = {posargs}
[flake8]
show-source = true
-# H304: no relative imports.
+# H304 no relative imports.
ignore = H304
builtins = _