From 78e256273a72410f11cfacde09a65eb3246d6b3c Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 14 Oct 2015 02:47:25 -0400 Subject: [PATCH] Move oauth1 extension into core Remove oauth1 as an extension and move it to a core resource. For now we leave the database migrations in the extension directory until we have a general policy for merging these into core. DocImpact: update keystone-paste and remove oauth1 from pipeline Change-Id: I0ed1ec44d42c3b379a5c2a40e3e6298842dfc01d Implements: bp move-extensions --- etc/keystone-paste.ini | 5 +- keystone/auth/plugins/oauth1.py | 4 +- keystone/contrib/oauth1/__init__.py | 15 - keystone/contrib/oauth1/backends/sql.py | 262 +----------------- keystone/contrib/oauth1/routers.py | 145 +--------- keystone/oauth1/__init__.py | 16 ++ keystone/oauth1/backends/__init__.py | 0 keystone/oauth1/backends/sql.py | 272 +++++++++++++++++++ keystone/{contrib => }/oauth1/controllers.py | 4 +- keystone/{contrib => }/oauth1/core.py | 0 keystone/oauth1/routers.py | 154 +++++++++++ keystone/{contrib => }/oauth1/validator.py | 2 +- keystone/server/backends.py | 2 +- keystone/tests/unit/test_v3_oauth1.py | 22 +- keystone/version/service.py | 4 +- setup.cfg | 2 +- 16 files changed, 491 insertions(+), 418 deletions(-) create mode 100644 keystone/oauth1/__init__.py create mode 100644 keystone/oauth1/backends/__init__.py create mode 100644 keystone/oauth1/backends/sql.py rename keystone/{contrib => }/oauth1/controllers.py (99%) rename keystone/{contrib => }/oauth1/core.py (100%) create mode 100644 keystone/oauth1/routers.py rename keystone/{contrib => }/oauth1/validator.py (99%) diff --git a/etc/keystone-paste.ini b/etc/keystone-paste.ini index 888e005101..11229aa714 100644 --- a/etc/keystone-paste.ini +++ b/etc/keystone-paste.ini @@ -30,9 +30,6 @@ use = egg:keystone#ec2_extension [filter:ec2_extension_v3] use = egg:keystone#ec2_extension_v3 -[filter:oauth1_extension] -use = egg:keystone#oauth1_extension - [filter:s3_extension] use = egg:keystone#s3_extension @@ -73,7 +70,7 @@ pipeline = sizelimit url_normalize request_id build_auth_context token_auth admi [pipeline:api_v3] # The last item in this pipeline must be service_v3 or an equivalent # application. It cannot be a filter. -pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension oauth1_extension endpoint_filter_extension service_v3 +pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension endpoint_filter_extension service_v3 [app:public_version_service] use = egg:keystone#public_version_service diff --git a/keystone/auth/plugins/oauth1.py b/keystone/auth/plugins/oauth1.py index 4ca97df562..e0da938abb 100644 --- a/keystone/auth/plugins/oauth1.py +++ b/keystone/auth/plugins/oauth1.py @@ -18,10 +18,10 @@ from oslo_utils import timeutils from keystone import auth from keystone.common import controller from keystone.common import dependency -from keystone.contrib.oauth1 import core as oauth -from keystone.contrib.oauth1 import validator from keystone import exception from keystone.i18n import _ +from keystone.oauth1 import core as oauth +from keystone.oauth1 import validator LOG = log.getLogger(__name__) diff --git a/keystone/contrib/oauth1/__init__.py b/keystone/contrib/oauth1/__init__.py index 8cab24987d..e69de29bb2 100644 --- a/keystone/contrib/oauth1/__init__.py +++ b/keystone/contrib/oauth1/__init__.py @@ -1,15 +0,0 @@ -# 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 * # noqa diff --git a/keystone/contrib/oauth1/backends/sql.py b/keystone/contrib/oauth1/backends/sql.py index a787675618..31b6ce3b83 100644 --- a/keystone/contrib/oauth1/backends/sql.py +++ b/keystone/contrib/oauth1/backends/sql.py @@ -12,261 +12,19 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime -import random as _random -import uuid +from oslo_log import versionutils -from oslo_serialization import jsonutils -from oslo_utils import timeutils - -from keystone.common import sql -from keystone.common import utils -from keystone.contrib.oauth1 import core -from keystone import exception -from keystone.i18n import _ +from keystone.oauth1.backends import sql -random = _random.SystemRandom() +_OLD = "keystone.contrib.oauth1.backends.sql.OAuth1" +_NEW = "sql" -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=True) - secret = sql.Column(sql.String(64), nullable=False) - extra = sql.Column(sql.JsonBlob(), nullable=False) +class OAuth1(sql.OAuth1): - -class RequestToken(sql.ModelBase, sql.DictBase): - __tablename__ = 'request_token' - attributes = ['id', 'request_secret', - 'verifier', 'authorizing_user_id', 'requested_project_id', - 'role_ids', '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) - role_ids = sql.Column(sql.Text(), nullable=True) - consumer_id = sql.Column(sql.String(64), sql.ForeignKey('consumer.id'), - 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.items()) - - -class AccessToken(sql.ModelBase, sql.DictBase): - __tablename__ = 'access_token' - attributes = ['id', 'access_secret', 'authorizing_user_id', - 'project_id', 'role_ids', '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) - role_ids = sql.Column(sql.Text(), nullable=False) - consumer_id = sql.Column(sql.String(64), sql.ForeignKey('consumer.id'), - 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.items()) - - -class OAuth1(object): - def _get_consumer(self, session, consumer_id): - 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_with_secret(self, consumer_id): - session = sql.get_session() - consumer_ref = self._get_consumer(session, consumer_id) - return consumer_ref.to_dict() - - def get_consumer(self, consumer_id): - return core.filter_consumer( - self.get_consumer_with_secret(consumer_id)) - - def create_consumer(self, consumer): - consumer['secret'] = uuid.uuid4().hex - if not consumer.get('description'): - consumer['description'] = None - session = sql.get_session() - with session.begin(): - consumer_ref = Consumer.from_dict(consumer) - session.add(consumer_ref) - return consumer_ref.to_dict() - - def _delete_consumer(self, session, consumer_id): - consumer_ref = self._get_consumer(session, consumer_id) - 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) - 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) - session.delete(token_ref) - - def delete_consumer(self, consumer_id): - session = sql.get_session() - with session.begin(): - self._delete_request_tokens(session, consumer_id) - self._delete_access_tokens(session, consumer_id) - self._delete_consumer(session, consumer_id) - - def list_consumers(self): - session = sql.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 = sql.get_session() - with session.begin(): - consumer_ref = self._get_consumer(session, consumer_id) - old_consumer_dict = consumer_ref.to_dict() - old_consumer_dict.update(consumer) - new_consumer = Consumer.from_dict(old_consumer_dict) - consumer_ref.description = new_consumer.description - consumer_ref.extra = new_consumer.extra - return core.filter_consumer(consumer_ref.to_dict()) - - def create_request_token(self, consumer_id, project_id, token_duration, - request_token_id=None, request_token_secret=None): - if request_token_id is None: - request_token_id = uuid.uuid4().hex - if request_token_secret is None: - request_token_secret = uuid.uuid4().hex - expiry_date = None - if token_duration: - now = timeutils.utcnow() - future = now + datetime.timedelta(seconds=token_duration) - expiry_date = utils.isotime(future, subsecond=True) - - ref = {} - ref['id'] = request_token_id - ref['request_secret'] = request_token_secret - ref['verifier'] = None - ref['authorizing_user_id'] = None - ref['requested_project_id'] = project_id - ref['role_ids'] = None - ref['consumer_id'] = consumer_id - ref['expires_at'] = expiry_date - session = sql.get_session() - with session.begin(): - token_ref = RequestToken.from_dict(ref) - session.add(token_ref) - 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 = sql.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, - role_ids): - session = sql.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'] = ''.join(random.sample(core.VERIFIER_CHARS, - 8)) - token_dict['role_ids'] = jsonutils.dumps(role_ids) - - new_token = RequestToken.from_dict(token_dict) - for attr in RequestToken.attributes: - if (attr == 'authorizing_user_id' or attr == 'verifier' - or attr == 'role_ids'): - setattr(token_ref, attr, getattr(new_token, attr)) - - return token_ref.to_dict() - - def create_access_token(self, request_token_id, token_duration, - access_token_id=None, access_token_secret=None): - if access_token_id is None: - access_token_id = uuid.uuid4().hex - if access_token_secret is None: - access_token_secret = uuid.uuid4().hex - session = sql.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 = utils.isotime(future, subsecond=True) - - # add Access Token - ref = {} - ref['id'] = access_token_id - ref['access_secret'] = access_token_secret - ref['authorizing_user_id'] = token_dict['authorizing_user_id'] - ref['project_id'] = token_dict['requested_project_id'] - ref['role_ids'] = token_dict['role_ids'] - 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 - session.delete(req_token_ref) - - 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 = sql.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 = sql.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 = sql.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')) - - session.delete(token_ref) + @versionutils.deprecated(versionutils.deprecated.MITAKA, + in_favor_of=_NEW, + what=_OLD) + def __init__(self, *args, **kwargs): + super(OAuth1, self).__init__(*args, **kwargs) diff --git a/keystone/contrib/oauth1/routers.py b/keystone/contrib/oauth1/routers.py index 4b772eb545..42a26c1099 100644 --- a/keystone/contrib/oauth1/routers.py +++ b/keystone/contrib/oauth1/routers.py @@ -12,143 +12,22 @@ # License for the specific language governing permissions and limitations # under the License. -import functools +from oslo_log import log +from oslo_log import versionutils -from keystone.common import json_home from keystone.common import wsgi -from keystone.contrib.oauth1 import controllers +from keystone.i18n import _ -build_resource_relation = functools.partial( - json_home.build_v3_extension_resource_relation, - extension_name='OS-OAUTH1', extension_version='1.0') - -build_parameter_relation = functools.partial( - json_home.build_v3_extension_parameter_relation, - extension_name='OS-OAUTH1', extension_version='1.0') - -ACCESS_TOKEN_ID_PARAMETER_RELATION = build_parameter_relation( - parameter_name='access_token_id') +LOG = log.getLogger(__name__) -class OAuth1Extension(wsgi.V3ExtensionRouter): - """API Endpoints for the OAuth1 extension. +class OAuth1Extension(wsgi.Middleware): - 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 - self._add_resource( - mapper, consumer_controller, - path='/OS-OAUTH1/consumers', - get_action='list_consumers', - post_action='create_consumer', - rel=build_resource_relation(resource_name='consumers')) - self._add_resource( - mapper, consumer_controller, - path='/OS-OAUTH1/consumers/{consumer_id}', - get_action='get_consumer', - patch_action='update_consumer', - delete_action='delete_consumer', - rel=build_resource_relation(resource_name='consumer'), - path_vars={ - 'consumer_id': - build_parameter_relation(parameter_name='consumer_id'), - }) - - # user access token crud - self._add_resource( - mapper, access_token_controller, - path='/users/{user_id}/OS-OAUTH1/access_tokens', - get_action='list_access_tokens', - rel=build_resource_relation(resource_name='user_access_tokens'), - path_vars={ - 'user_id': json_home.Parameters.USER_ID, - }) - self._add_resource( - mapper, access_token_controller, - path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}', - get_action='get_access_token', - delete_action='delete_access_token', - rel=build_resource_relation(resource_name='user_access_token'), - path_vars={ - 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, - 'user_id': json_home.Parameters.USER_ID, - }) - self._add_resource( - mapper, access_token_roles_controller, - path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/' - 'roles', - get_action='list_access_token_roles', - rel=build_resource_relation( - resource_name='user_access_token_roles'), - path_vars={ - 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, - 'user_id': json_home.Parameters.USER_ID, - }) - self._add_resource( - mapper, access_token_roles_controller, - path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/' - 'roles/{role_id}', - get_action='get_access_token_role', - rel=build_resource_relation( - resource_name='user_access_token_role'), - path_vars={ - 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, - 'role_id': json_home.Parameters.ROLE_ID, - 'user_id': json_home.Parameters.USER_ID, - }) - - # oauth flow calls - self._add_resource( - mapper, oauth_controller, - path='/OS-OAUTH1/request_token', - post_action='create_request_token', - rel=build_resource_relation(resource_name='request_tokens')) - self._add_resource( - mapper, oauth_controller, - path='/OS-OAUTH1/access_token', - post_action='create_access_token', - rel=build_resource_relation(resource_name='access_tokens')) - self._add_resource( - mapper, oauth_controller, - path='/OS-OAUTH1/authorize/{request_token_id}', - path_vars={ - 'request_token_id': - build_parameter_relation(parameter_name='request_token_id') - }, - put_action='authorize_request_token', - rel=build_resource_relation( - resource_name='authorize_request_token')) + def __init__(self, *args, **kwargs): + super(OAuth1Extension, self).__init__(*args, **kwargs) + msg = _("Remove oauth1_extension from the paste pipeline, the " + "oauth1 extension is now always available. Update the " + "[pipeline:api_v3] section in keystone-paste.ini accordingly, " + "as it will be removed in the O release.") + versionutils.report_deprecated_feature(LOG, msg) diff --git a/keystone/oauth1/__init__.py b/keystone/oauth1/__init__.py new file mode 100644 index 0000000000..1065f9ef28 --- /dev/null +++ b/keystone/oauth1/__init__.py @@ -0,0 +1,16 @@ +# 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.oauth1.core import * # noqa +from keystone.oauth1 import routers # noqa diff --git a/keystone/oauth1/backends/__init__.py b/keystone/oauth1/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/oauth1/backends/sql.py b/keystone/oauth1/backends/sql.py new file mode 100644 index 0000000000..999446f192 --- /dev/null +++ b/keystone/oauth1/backends/sql.py @@ -0,0 +1,272 @@ +# 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 as _random +import uuid + +from oslo_serialization import jsonutils +from oslo_utils import timeutils + +from keystone.common import sql +from keystone.common import utils +from keystone import exception +from keystone.i18n import _ +from keystone.oauth1 import core + + +random = _random.SystemRandom() + + +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=True) + 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', + 'role_ids', '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) + role_ids = sql.Column(sql.Text(), nullable=True) + consumer_id = sql.Column(sql.String(64), sql.ForeignKey('consumer.id'), + 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.items()) + + +class AccessToken(sql.ModelBase, sql.DictBase): + __tablename__ = 'access_token' + attributes = ['id', 'access_secret', 'authorizing_user_id', + 'project_id', 'role_ids', '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) + role_ids = sql.Column(sql.Text(), nullable=False) + consumer_id = sql.Column(sql.String(64), sql.ForeignKey('consumer.id'), + 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.items()) + + +class OAuth1(object): + def _get_consumer(self, session, consumer_id): + 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_with_secret(self, consumer_id): + session = sql.get_session() + consumer_ref = self._get_consumer(session, consumer_id) + return consumer_ref.to_dict() + + def get_consumer(self, consumer_id): + return core.filter_consumer( + self.get_consumer_with_secret(consumer_id)) + + def create_consumer(self, consumer): + consumer['secret'] = uuid.uuid4().hex + if not consumer.get('description'): + consumer['description'] = None + session = sql.get_session() + with session.begin(): + consumer_ref = Consumer.from_dict(consumer) + session.add(consumer_ref) + return consumer_ref.to_dict() + + def _delete_consumer(self, session, consumer_id): + consumer_ref = self._get_consumer(session, consumer_id) + 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) + 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) + session.delete(token_ref) + + def delete_consumer(self, consumer_id): + session = sql.get_session() + with session.begin(): + self._delete_request_tokens(session, consumer_id) + self._delete_access_tokens(session, consumer_id) + self._delete_consumer(session, consumer_id) + + def list_consumers(self): + session = sql.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 = sql.get_session() + with session.begin(): + consumer_ref = self._get_consumer(session, consumer_id) + old_consumer_dict = consumer_ref.to_dict() + old_consumer_dict.update(consumer) + new_consumer = Consumer.from_dict(old_consumer_dict) + consumer_ref.description = new_consumer.description + consumer_ref.extra = new_consumer.extra + return core.filter_consumer(consumer_ref.to_dict()) + + def create_request_token(self, consumer_id, project_id, token_duration, + request_token_id=None, request_token_secret=None): + if request_token_id is None: + request_token_id = uuid.uuid4().hex + if request_token_secret is None: + request_token_secret = uuid.uuid4().hex + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = utils.isotime(future, subsecond=True) + + ref = {} + ref['id'] = request_token_id + ref['request_secret'] = request_token_secret + ref['verifier'] = None + ref['authorizing_user_id'] = None + ref['requested_project_id'] = project_id + ref['role_ids'] = None + ref['consumer_id'] = consumer_id + ref['expires_at'] = expiry_date + session = sql.get_session() + with session.begin(): + token_ref = RequestToken.from_dict(ref) + session.add(token_ref) + 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 = sql.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, + role_ids): + session = sql.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'] = ''.join(random.sample(core.VERIFIER_CHARS, + 8)) + token_dict['role_ids'] = jsonutils.dumps(role_ids) + + new_token = RequestToken.from_dict(token_dict) + for attr in RequestToken.attributes: + if (attr == 'authorizing_user_id' or attr == 'verifier' + or attr == 'role_ids'): + setattr(token_ref, attr, getattr(new_token, attr)) + + return token_ref.to_dict() + + def create_access_token(self, request_token_id, token_duration, + access_token_id=None, access_token_secret=None): + if access_token_id is None: + access_token_id = uuid.uuid4().hex + if access_token_secret is None: + access_token_secret = uuid.uuid4().hex + session = sql.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 = utils.isotime(future, subsecond=True) + + # add Access Token + ref = {} + ref['id'] = access_token_id + ref['access_secret'] = access_token_secret + ref['authorizing_user_id'] = token_dict['authorizing_user_id'] + ref['project_id'] = token_dict['requested_project_id'] + ref['role_ids'] = token_dict['role_ids'] + 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 + session.delete(req_token_ref) + + 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 = sql.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 = sql.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 = sql.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')) + + session.delete(token_ref) diff --git a/keystone/contrib/oauth1/controllers.py b/keystone/oauth1/controllers.py similarity index 99% rename from keystone/contrib/oauth1/controllers.py rename to keystone/oauth1/controllers.py index 70e778ec7e..1b30e0ff8d 100644 --- a/keystone/contrib/oauth1/controllers.py +++ b/keystone/oauth1/controllers.py @@ -22,11 +22,11 @@ from keystone.common import controller from keystone.common import dependency from keystone.common import utils from keystone.common import wsgi -from keystone.contrib.oauth1 import core as oauth1 -from keystone.contrib.oauth1 import validator from keystone import exception from keystone.i18n import _ from keystone import notifications +from keystone.oauth1 import core as oauth1 +from keystone.oauth1 import validator CONF = cfg.CONF diff --git a/keystone/contrib/oauth1/core.py b/keystone/oauth1/core.py similarity index 100% rename from keystone/contrib/oauth1/core.py rename to keystone/oauth1/core.py diff --git a/keystone/oauth1/routers.py b/keystone/oauth1/routers.py new file mode 100644 index 0000000000..0575b1076b --- /dev/null +++ b/keystone/oauth1/routers.py @@ -0,0 +1,154 @@ +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.oauth1 import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-OAUTH1', extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, + extension_name='OS-OAUTH1', extension_version='1.0') + +ACCESS_TOKEN_ID_PARAMETER_RELATION = build_parameter_relation( + parameter_name='access_token_id') + + +class Routers(wsgi.RoutersBase): + """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 append_v3_routers(self, mapper, routers): + consumer_controller = controllers.ConsumerCrudV3() + access_token_controller = controllers.AccessTokenCrudV3() + access_token_roles_controller = controllers.AccessTokenRolesV3() + oauth_controller = controllers.OAuthControllerV3() + + # basic admin-only consumer crud + self._add_resource( + mapper, consumer_controller, + path='/OS-OAUTH1/consumers', + get_action='list_consumers', + post_action='create_consumer', + rel=build_resource_relation(resource_name='consumers')) + self._add_resource( + mapper, consumer_controller, + path='/OS-OAUTH1/consumers/{consumer_id}', + get_action='get_consumer', + patch_action='update_consumer', + delete_action='delete_consumer', + rel=build_resource_relation(resource_name='consumer'), + path_vars={ + 'consumer_id': + build_parameter_relation(parameter_name='consumer_id'), + }) + + # user access token crud + self._add_resource( + mapper, access_token_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens', + get_action='list_access_tokens', + rel=build_resource_relation(resource_name='user_access_tokens'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, access_token_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}', + get_action='get_access_token', + delete_action='delete_access_token', + rel=build_resource_relation(resource_name='user_access_token'), + path_vars={ + 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, access_token_roles_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/' + 'roles', + get_action='list_access_token_roles', + rel=build_resource_relation( + resource_name='user_access_token_roles'), + path_vars={ + 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, access_token_roles_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/' + 'roles/{role_id}', + get_action='get_access_token_role', + rel=build_resource_relation( + resource_name='user_access_token_role'), + path_vars={ + 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + + # oauth flow calls + self._add_resource( + mapper, oauth_controller, + path='/OS-OAUTH1/request_token', + post_action='create_request_token', + rel=build_resource_relation(resource_name='request_tokens')) + self._add_resource( + mapper, oauth_controller, + path='/OS-OAUTH1/access_token', + post_action='create_access_token', + rel=build_resource_relation(resource_name='access_tokens')) + self._add_resource( + mapper, oauth_controller, + path='/OS-OAUTH1/authorize/{request_token_id}', + path_vars={ + 'request_token_id': + build_parameter_relation(parameter_name='request_token_id') + }, + put_action='authorize_request_token', + rel=build_resource_relation( + resource_name='authorize_request_token')) diff --git a/keystone/contrib/oauth1/validator.py b/keystone/oauth1/validator.py similarity index 99% rename from keystone/contrib/oauth1/validator.py rename to keystone/oauth1/validator.py index 8f44059e3f..8023abf4ce 100644 --- a/keystone/contrib/oauth1/validator.py +++ b/keystone/oauth1/validator.py @@ -18,8 +18,8 @@ from oslo_log import log import six from keystone.common import dependency -from keystone.contrib.oauth1 import core as oauth1 from keystone import exception +from keystone.oauth1 import core as oauth1 METHOD_NAME = 'oauth_validator' diff --git a/keystone/server/backends.py b/keystone/server/backends.py index a2dc347c0f..c71f747826 100644 --- a/keystone/server/backends.py +++ b/keystone/server/backends.py @@ -15,12 +15,12 @@ from keystone import auth from keystone import catalog from keystone.common import cache from keystone.contrib import endpoint_filter -from keystone.contrib import oauth1 from keystone.contrib import revoke from keystone import credential from keystone import endpoint_policy from keystone import federation from keystone import identity +from keystone import oauth1 from keystone import policy from keystone import resource from keystone import token diff --git a/keystone/tests/unit/test_v3_oauth1.py b/keystone/tests/unit/test_v3_oauth1.py index a73b116d72..ad1b114d60 100644 --- a/keystone/tests/unit/test_v3_oauth1.py +++ b/keystone/tests/unit/test_v3_oauth1.py @@ -15,16 +15,19 @@ import copy import uuid +import mock from oslo_config import cfg +from oslo_log import versionutils from oslo_serialization import jsonutils from pycadf import cadftaxonomy from six.moves import http_client from six.moves import urllib -from keystone.contrib import oauth1 -from keystone.contrib.oauth1 import controllers -from keystone.contrib.oauth1 import core +from keystone.contrib.oauth1 import routers from keystone import exception +from keystone import oauth1 +from keystone.oauth1 import controllers +from keystone.oauth1 import core from keystone.tests import unit from keystone.tests.unit.common import test_notifications from keystone.tests.unit.ksfixtures import temporaryfile @@ -34,10 +37,17 @@ from keystone.tests.unit import test_v3 CONF = cfg.CONF -class OAuth1Tests(test_v3.RestfulTestCase): +class OAuth1ContribTests(test_v3.RestfulTestCase): - EXTENSION_NAME = 'oauth1' - EXTENSION_TO_ADD = 'oauth1_extension' + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_exception_happens(self, mock_deprecator): + routers.OAuth1Extension(mock.ANY) + mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY) + args, _kwargs = mock_deprecator.call_args + self.assertIn("Remove oauth1_extension from", args[1]) + + +class OAuth1Tests(test_v3.RestfulTestCase): CONSUMER_URL = '/OS-OAUTH1/consumers' diff --git a/keystone/version/service.py b/keystone/version/service.py index c485af2c81..789fab7f6f 100644 --- a/keystone/version/service.py +++ b/keystone/version/service.py @@ -29,6 +29,7 @@ from keystone import endpoint_policy from keystone import federation from keystone.i18n import _LW from keystone import identity +from keystone import oauth1 from keystone import policy from keystone import resource from keystone import token @@ -131,7 +132,8 @@ def v3_app_factory(global_conf, **local_conf): identity, policy, resource, - federation] + federation, + oauth1] if CONF.trust.enabled: router_modules.append(trust) diff --git a/setup.cfg b/setup.cfg index b2b1b6ac82..eada13d53e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -165,7 +165,7 @@ keystone.federation = sql = keystone.federation.backends.sql:Federation keystone.oauth1 = - sql = keystone.contrib.oauth1.backends.sql:OAuth1 + sql = keystone.oauth1.backends.sql:OAuth1 keystone.revoke = kvs = keystone.contrib.revoke.backends.kvs:Revoke