From cb130ffae86dcec0c7a61502005c7b3b483d3579 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Thu, 12 Jul 2018 19:07:00 -0700 Subject: [PATCH] Move Credentials API to Flask Native Move the Credentials API to Flask Native dispatching. This change fixes some circular importing in the conversion. Change-Id: I5e2485ba471d09c3454e78ca2c9dfa19aaf0e4e2 Partial-Bug: #1776504 --- keystone/api/__init__.py | 5 +- keystone/api/credentials.py | 134 +++++++++++++++++++++++ keystone/credential/__init__.py | 1 - keystone/credential/controllers.py | 112 ------------------- keystone/credential/routers.py | 30 ----- keystone/server/flask/__init__.py | 7 +- keystone/server/flask/application.py | 4 +- keystone/server/wsgi.py | 3 +- keystone/tests/unit/common/test_utils.py | 4 +- keystone/tests/unit/core.py | 5 +- 10 files changed, 145 insertions(+), 160 deletions(-) create mode 100644 keystone/api/credentials.py delete mode 100644 keystone/credential/controllers.py delete mode 100644 keystone/credential/routers.py diff --git a/keystone/api/__init__.py b/keystone/api/__init__.py index 69be5d6c4b..d78b91fb70 100644 --- a/keystone/api/__init__.py +++ b/keystone/api/__init__.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from keystone.api import credentials from keystone.api import discovery -__all__ = ('discovery',) -__apis__ = (discovery,) +__all__ = ('discovery', 'credentials') +__apis__ = (discovery, credentials) diff --git a/keystone/api/credentials.py b/keystone/api/credentials.py new file mode 100644 index 0000000000..8f6fda38ca --- /dev/null +++ b/keystone/api/credentials.py @@ -0,0 +1,134 @@ +# 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. + +# This file handles all flask-restful resources for /v3/credentials + +import hashlib + +import flask +from oslo_serialization import jsonutils +from six.moves import http_client + +from keystone.common import provider_api +from keystone.common import rbac_enforcer +from keystone.common import validation +from keystone.credential import schema +from keystone import exception +from keystone.i18n import _ +from keystone.server import flask as ks_flask + + +PROVIDERS = provider_api.ProviderAPIs +ENFORCER = rbac_enforcer.RBACEnforcer + + +class CredentialResource(ks_flask.ResourceBase): + collection_key = 'credentials' + member_key = 'credential' + + @staticmethod + def _blob_to_json(ref): + # credentials stored via ec2tokens before the fix for #1259584 + # need json_serailzing, as that's the documented API format + blob = ref.get('blob') + if isinstance(blob, dict): + ref = ref.copy() + ref['blob'] = jsonutils.dumps(blob) + return ref + + def _assign_unique_id(self, ref, trust_id=None): + # Generates an assigns a unique identifier to a credential reference. + if ref.get('type', '').lower() == 'ec2': + try: + blob = jsonutils.loads(ref.get('blob')) + except (ValueError, TabError): + raise exception.ValidationError( + message=_('Invalid blob in credential')) + if not blob or not isinstance(blob, dict): + raise exception.ValidationError(attribute='blob', + target='credential') + if blob.get('access') is None: + raise exception.ValidationError(attribute='access', + target='credential') + + ref = ref.copy() + ref['id'] = hashlib.sha256( + blob['access'].encode('utf8')).hexdigest() + # update the blob with the trust_id, so credentials created with + # a trust scoped token will result in trust scoped tokens when + # authentication via ec2tokens happens + if trust_id is not None: + blob['trust_id'] = trust_id + ref['blob'] = jsonutils.dumps(blob) + return ref + else: + return super(CredentialResource, self)._assign_unique_id(ref) + + def _list_credentials(self): + filters = ['user_id', 'type'] + ENFORCER.enforce_call(action='identity:list_credentials', + filters=filters) + hints = self.build_driver_hints(filters) + refs = PROVIDERS.credential_api.list_credentials(hints) + refs = [self._blob_to_json(r) for r in refs] + return self.wrap_collection(refs, hints=hints) + + def _get_credential(self, credential_id): + ENFORCER.enforce_call(action='identity:get_credential') + ref = PROVIDERS.credential_api.get_credential(credential_id) + return self.wrap_member(self._blob_to_json(ref)) + + def get(self, credential_id=None): + # Get Credential or List of credentials. + if credential_id is None: + # No Parameter passed means that we're doing a LIST action. + return self._list_credentials() + else: + return self._get_credential(credential_id) + + def post(self): + # Create a new credential + ENFORCER.enforce_call(action='identity:create_credential') + credential = flask.request.json.get('credential', {}) + validation.lazy_validate(schema.credential_create, credential) + trust_id = getattr(self.oslo_context, 'trust_id', None) + ref = self._assign_unique_id( + self._normalize_dict(credential), trust_id=trust_id) + ref = PROVIDERS.credential_api.create_credential(ref['id'], ref) + return self.wrap_member(ref), http_client.CREATED + + def patch(self, credential_id): + # Update Credential + ENFORCER.enforce_call(action='identity:update_credential') + credential = flask.request.json.get('credential', {}) + validation.lazy_validate(schema.credential_update, credential) + self._require_matching_id(credential) + ref = PROVIDERS.credential_api.update_credential( + credential_id, credential) + return self.wrap_member(ref) + + def delete(self, credential_id): + # Delete credentials + ENFORCER.enforce_call(action='identity:delete_credential') + return (PROVIDERS.credential_api.delete_credential(credential_id), + http_client.NO_CONTENT) + + +class CredentialAPI(ks_flask.APIBase): + + _name = 'credentials' + _import_name = __name__ + resource_mapping = [] + resources = [CredentialResource] + + +APIs = (CredentialAPI,) diff --git a/keystone/credential/__init__.py b/keystone/credential/__init__.py index 73f29e20ef..c2357e4c2a 100644 --- a/keystone/credential/__init__.py +++ b/keystone/credential/__init__.py @@ -12,6 +12,5 @@ # License for the specific language governing permissions and limitations # under the License. -from keystone.credential import controllers # noqa from keystone.credential.core import * # noqa from keystone.credential import provider # noqa diff --git a/keystone/credential/controllers.py b/keystone/credential/controllers.py deleted file mode 100644 index f9f6f9911e..0000000000 --- a/keystone/credential/controllers.py +++ /dev/null @@ -1,112 +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. - -import hashlib - -from oslo_serialization import jsonutils - -from keystone.common import controller -from keystone.common import provider_api -from keystone.common import validation -from keystone.credential import schema -from keystone import exception -from keystone.i18n import _ - - -PROVIDERS = provider_api.ProviderAPIs - - -class CredentialV3(controller.V3Controller): - collection_name = 'credentials' - member_name = 'credential' - - def __init__(self): - super(CredentialV3, self).__init__() - self.get_member_from_driver = PROVIDERS.credential_api.get_credential - - def _assign_unique_id(self, ref, trust_id=None): - # Generates and assigns a unique identifier to - # a credential reference. - if ref.get('type', '').lower() == 'ec2': - try: - blob = jsonutils.loads(ref.get('blob')) - except (ValueError, TypeError): - raise exception.ValidationError( - message=_('Invalid blob in credential')) - if not blob or not isinstance(blob, dict): - raise exception.ValidationError(attribute='blob', - target='credential') - if blob.get('access') is None: - raise exception.ValidationError(attribute='access', - target='blob') - ret_ref = ref.copy() - ret_ref['id'] = hashlib.sha256( - blob['access'].encode('utf8')).hexdigest() - # Update the blob with the trust_id, so credentials created - # with a trust scoped token will result in trust scoped - # tokens when authentication via ec2tokens happens - if trust_id is not None: - blob['trust_id'] = trust_id - ret_ref['blob'] = jsonutils.dumps(blob) - return ret_ref - else: - return super(CredentialV3, self)._assign_unique_id(ref) - - @controller.protected() - def create_credential(self, request, credential): - validation.lazy_validate(schema.credential_create, credential) - ref = self._assign_unique_id(self._normalize_dict(credential), - request.context.trust_id) - ref = PROVIDERS.credential_api.create_credential(ref['id'], ref) - return CredentialV3.wrap_member(request.context_dict, ref) - - @staticmethod - def _blob_to_json(ref): - # credentials stored via ec2tokens before the fix for #1259584 - # need json serializing, as that's the documented API format - blob = ref.get('blob') - if isinstance(blob, dict): - new_ref = ref.copy() - new_ref['blob'] = jsonutils.dumps(blob) - return new_ref - else: - return ref - - @controller.filterprotected('user_id', 'type') - def list_credentials(self, request, filters): - hints = CredentialV3.build_driver_hints(request, filters) - refs = PROVIDERS.credential_api.list_credentials(hints) - ret_refs = [self._blob_to_json(r) for r in refs] - return CredentialV3.wrap_collection(request.context_dict, ret_refs, - hints=hints) - - @controller.protected() - def get_credential(self, request, credential_id): - ref = PROVIDERS.credential_api.get_credential(credential_id) - ret_ref = self._blob_to_json(ref) - return CredentialV3.wrap_member(request.context_dict, ret_ref) - - @controller.protected() - def update_credential(self, request, credential_id, credential): - validation.lazy_validate(schema.credential_update, credential) - self._require_matching_id(credential_id, credential) - - ref = PROVIDERS.credential_api.update_credential( - credential_id, credential - ) - return CredentialV3.wrap_member(request.context_dict, ref) - - @controller.protected() - def delete_credential(self, request, credential_id): - return PROVIDERS.credential_api.delete_credential(credential_id) diff --git a/keystone/credential/routers.py b/keystone/credential/routers.py deleted file mode 100644 index 0b069653d4..0000000000 --- a/keystone/credential/routers.py +++ /dev/null @@ -1,30 +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. - -"""WSGI Routers for the Credentials service.""" - -from keystone.common import router -from keystone.common import wsgi -from keystone.credential import controllers - - -class Routers(wsgi.RoutersBase): - - _path_prefixes = ('credentials',) - - def append_v3_routers(self, mapper, routers): - routers.append( - router.Router(controllers.CredentialV3(), - 'credentials', 'credential', - resource_descriptions=self.v3_resources)) diff --git a/keystone/server/flask/__init__.py b/keystone/server/flask/__init__.py index 1305ef529a..e5f1648012 100644 --- a/keystone/server/flask/__init__.py +++ b/keystone/server/flask/__init__.py @@ -14,7 +14,6 @@ # isn't needed, keystone.server.flask exposes all the interesting bits # needed to develop restful APIs for keystone. -from keystone.server.flask import application from keystone.server.flask.common import APIBase # noqa from keystone.server.flask.common import base_url # noqa from keystone.server.flask.common import construct_json_home_data # noqa @@ -23,14 +22,10 @@ from keystone.server.flask.common import full_url # noqa from keystone.server.flask.common import JsonHomeData # noqa from keystone.server.flask.common import ResourceBase # noqa from keystone.server.flask.common import ResourceMap # noqa -from keystone.server.flask.core import * # noqa # NOTE(morgan): This allows for from keystone.flask import * and have all the # cool stuff needed to develop new APIs within a module/subsystem __all__ = ('APIBase', 'JsonHomeData', 'ResourceBase', 'ResourceMap', 'base_url', 'construct_json_home_data', 'construct_resource_map', - 'full_url', 'fail_gracefully') - -application_factory = application.application_factory -fail_gracefully = application.fail_gracefully + 'full_url') diff --git a/keystone/server/flask/application.py b/keystone/server/flask/application.py index e6c5d8b940..7216c33daf 100644 --- a/keystone/server/flask/application.py +++ b/keystone/server/flask/application.py @@ -31,7 +31,6 @@ from keystone.catalog import routers as catalog_routers from keystone.common import wsgi as keystone_wsgi from keystone.contrib.ec2 import routers as ec2_routers from keystone.contrib.s3 import routers as s3_routers -from keystone.credential import routers as credential_routers from keystone.endpoint_policy import routers as endpoint_policy_routers from keystone.federation import routers as federation_routers from keystone.identity import routers as identity_routers @@ -45,14 +44,13 @@ from keystone.trust import routers as trust_routers # TODO(morgan): _MOVED_API_PREFIXES to be removed when the legacy dispatch # support is removed. -_MOVED_API_PREFIXES = frozenset([]) +_MOVED_API_PREFIXES = frozenset(['credentials']) LOG = log.getLogger(__name__) ALL_API_ROUTERS = [auth_routers, assignment_routers, catalog_routers, - credential_routers, identity_routers, app_cred_routers, limit_routers, diff --git a/keystone/server/wsgi.py b/keystone/server/wsgi.py index af6909dbc9..17072fff6a 100644 --- a/keystone/server/wsgi.py +++ b/keystone/server/wsgi.py @@ -11,7 +11,6 @@ # under the License. -from keystone.server import flask as keystone_flask from keystone.server.flask import core as flask_core @@ -21,7 +20,7 @@ from keystone.server.flask import core as flask_core # export all the symbols from keystone.flask.core only specific ones that # are meant for public consumption def initialize_public_application(): - return keystone_flask.initialize_application( + return flask_core.initialize_application( name='public', config_files=flask_core._get_config_files()) diff --git a/keystone/tests/unit/common/test_utils.py b/keystone/tests/unit/common/test_utils.py index 431aa2af30..1de5c4a5d2 100644 --- a/keystone/tests/unit/common/test_utils.py +++ b/keystone/tests/unit/common/test_utils.py @@ -25,7 +25,7 @@ from keystone.common import utils as common_utils import keystone.conf from keystone.credential.providers import fernet as credential_fernet from keystone import exception -from keystone.server import flask as server_flask +from keystone.server.flask import application from keystone.tests import unit from keystone.tests.unit import ksfixtures from keystone.tests.unit import utils @@ -250,7 +250,7 @@ class UtilsTestCase(unit.BaseTestCase): class ServiceHelperTests(unit.BaseTestCase): - @server_flask.fail_gracefully + @application.fail_gracefully def _do_test(self): raise Exception("Test Exc") diff --git a/keystone/tests/unit/core.py b/keystone/tests/unit/core.py index e424a14d9d..1da72f045a 100644 --- a/keystone/tests/unit/core.py +++ b/keystone/tests/unit/core.py @@ -53,7 +53,8 @@ from keystone import exception from keystone.identity.backends.ldap import common as ks_ldap from keystone import notifications from keystone.resource.backends import base as resource_base -from keystone.server import flask as keystone_flask +from keystone.server.flask import application as flask_app +from keystone.server.flask import core as keystone_flask from keystone.tests.unit import ksfixtures @@ -865,7 +866,7 @@ class TestCase(BaseTestCase): self.addCleanup(self.cleanup_instance(*fixtures_to_cleanup)) def loadapp(self, name='public'): - app = keystone_flask.application.application_factory(name) + app = flask_app.application_factory(name) app.testing = True app.test_client_class = KeystoneFlaskTestClient