summaryrefslogtreecommitdiffstats
path: root/keystone/contrib/oauth1
diff options
context:
space:
mode:
Diffstat (limited to 'keystone/contrib/oauth1')
-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
11 files changed, 1440 insertions, 0 deletions
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']))