summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-08-17 04:27:21 +0000
committerGerrit Code Review <review@openstack.org>2013-08-17 04:27:21 +0000
commit14cba15fcb9a03adb82bbe586f6431791077f1e8 (patch)
tree7fb8487ce511a853da2b8abe9e6662dab9487b5a
parent781c65b72b78bd1e2b1d93db029d6b0c6fbc2050 (diff)
parentbcaa3072f37d3af3f9d526f18f311411ceeae160 (diff)
downloadkeystone-14cba15fcb9a03adb82bbe586f6431791077f1e8.tar.gz
keystone-14cba15fcb9a03adb82bbe586f6431791077f1e8.tar.xz
keystone-14cba15fcb9a03adb82bbe586f6431791077f1e8.zip
Merge "Add delegated_auth support for keystone"
-rw-r--r--doc/source/configuration.rst1
-rw-r--r--etc/keystone-paste.ini3
-rw-r--r--etc/keystone.conf.sample13
-rw-r--r--keystone/auth/controllers.py2
-rw-r--r--keystone/auth/plugins/oauth1.py80
-rw-r--r--keystone/auth/plugins/token.py9
-rw-r--r--keystone/common/config.py5
-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/service.py2
-rw-r--r--keystone/tests/core.py3
-rw-r--r--keystone/tests/test_drivers.py5
-rw-r--r--keystone/tests/test_overrides.conf3
-rw-r--r--keystone/tests/test_sql_migrate_extensions.py63
-rw-r--r--keystone/tests/test_v3_oauth1.py574
-rw-r--r--keystone/token/backends/kvs.py28
-rw-r--r--keystone/token/backends/memcache.py10
-rw-r--r--keystone/token/backends/sql.py37
-rw-r--r--keystone/token/core.py20
-rw-r--r--keystone/token/providers/uuid.py31
-rw-r--r--requirements.txt1
30 files changed, 2311 insertions, 19 deletions
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 2b802c83..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
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 922d90c6..13d14317 100644
--- a/etc/keystone.conf.sample
+++ b/etc/keystone.conf.sample
@@ -163,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
@@ -289,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/auth/controllers.py b/keystone/auth/controllers.py
index e6557be5..9f6f1972 100644
--- a/keystone/auth/controllers.py
+++ b/keystone/auth/controllers.py
@@ -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()
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/token.py b/keystone/auth/plugins/token.py
index b82c0311..bc7cb1ba 100644
--- a/keystone/auth/plugins/token.py
+++ b/keystone/auth/plugins/token.py
@@ -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/common/config.py b/keystone/common/config.py
index 61eeac92..34ab0988 100644
--- a/keystone/common/config.py
+++ b/keystone/common/config.py
@@ -116,6 +116,11 @@ FILE_OPTIONS = {
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')],
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/service.py b/keystone/service.py
index f2c95f78..e3633865 100644
--- a/keystone/service.py
+++ b/keystone/service.py
@@ -24,6 +24,7 @@ from keystone.common import dependency
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
@@ -49,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(),
diff --git a/keystone/tests/core.py b/keystone/tests/core.py
index b42a8709..cba6cbf8 100644
--- a/keystone/tests/core.py
+++ b/keystone/tests/core.py
@@ -45,6 +45,7 @@ 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
@@ -268,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' %
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_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_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_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 171d77df..b2c6ed30 100644
--- a/keystone/token/backends/kvs.py
+++ b/keystone/token/backends/kvs.py
@@ -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 d0d59eef..b80d01bc 100644
--- a/keystone/token/backends/memcache.py
+++ b/keystone/token/backends/memcache.py
@@ -178,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 ""
@@ -199,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/core.py b/keystone/token/core.py
index e8d04a7e..7eadbe63 100644
--- a/keystone/token/core.py
+++ b/keystone/token/core.py
@@ -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/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/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