diff --git a/evoque/api/app.py b/evoque/api/app.py index aa9c620..87be677 100644 --- a/evoque/api/app.py +++ b/evoque/api/app.py @@ -12,8 +12,13 @@ import pecan +from oslo_config import cfg + +from evoque.api import auth from evoque.api import config as api_config +CONF = cfg.CONF + def get_pecan_config(): # Set up the pecan configuration @@ -33,7 +38,10 @@ def setup_app(config=None): **app_conf ) - # TODO(liuqing): Add oslo.middleware cors and keystone auth + # TODO(liuqing): Add oslo.middleware cors # http://docs.openstack.org/developer/oslo.middleware/cors.html + # Keystone auth middleware + app = auth.install(app, CONF, config.app.acl_public_routes) + return app diff --git a/evoque/api/auth.py b/evoque/api/auth.py new file mode 100644 index 0000000..6706ebd --- /dev/null +++ b/evoque/api/auth.py @@ -0,0 +1,35 @@ +# 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. + +"""Access Control Lists (ACL's) control access the API server.""" + +from oslo_config import cfg + +from evoque.api.middleware import auth_token + + +CONF = cfg.CONF + + +def install(app, conf, public_routes): + """Install ACL check on application. + :param app: A WSGI application. + :param conf: Settings. Dict'ified and passed to keystone middleware + :param public_routes: The list of the routes which will be allowed to + access without authentication. + :return: The same WSGI application with ACL installed. + """ + if not cfg.CONF.api.get('enable_authentication'): + return app + return auth_token.AuthTokenMiddleware(app, + conf=dict(conf), + public_api_routes=public_routes) diff --git a/evoque/api/config.py b/evoque/api/config.py index 7579c4d..ebf4e82 100644 --- a/evoque/api/config.py +++ b/evoque/api/config.py @@ -21,6 +21,9 @@ app = { hooks.ContextHook(), hooks.RPCHook(), ], + 'acl_public_routes': [ + '/' + ], } # Custom Configurations must be in Python dictionary format:: diff --git a/evoque/api/hooks.py b/evoque/api/hooks.py index 08a20a2..49d2af6 100644 --- a/evoque/api/hooks.py +++ b/evoque/api/hooks.py @@ -12,9 +12,15 @@ from pecan import hooks +from oslo_config import cfg + from evoque.common import context from evoque.engine.ticket import api as ticket_api +CONF = cfg.CONF +CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token', + group='keystone_authtoken') + class ContextHook(hooks.PecanHook): """Configures a request context and attaches it to the request. @@ -52,8 +58,11 @@ class ContextHook(hooks.PecanHook): roles = headers.get('X-Roles', '').split(',') auth_token_info = state.request.environ.get('keystone.token_info') + auth_url = CONF.keystone_authtoken.auth_uri + state.request.context = context.make_context( auth_token=auth_token, + auth_url=auth_url, auth_token_info=auth_token_info, user_name=user_name, user_id=user_id, diff --git a/evoque/api/middleware/__init__.py b/evoque/api/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/evoque/api/middleware/auth_token.py b/evoque/api/middleware/auth_token.py new file mode 100644 index 0000000..7cf22cc --- /dev/null +++ b/evoque/api/middleware/auth_token.py @@ -0,0 +1,60 @@ +# 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 re + +from keystonemiddleware import auth_token +from oslo_log import log + +from evoque.common import exceptions +from evoque.common.i18n import _ +from evoque.common import utils + +LOG = log.getLogger(__name__) + + +class AuthTokenMiddleware(auth_token.AuthProtocol): + """A wrapper on Keystone auth_token middleware. + + Does not perform verification of authentication tokens + for public routes in the API. + + """ + def __init__(self, app, conf, public_api_routes=None): + if public_api_routes is None: + public_api_routes = [] + route_pattern_tpl = '%s\.json?$' + + try: + self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl) + for route_tpl in public_api_routes] + except re.error as e: + msg = _('Cannot compile public API routes: %s') % e + + LOG.error(msg) + raise exceptions.ConfigInvalid(error_msg=msg) + + super(AuthTokenMiddleware, self).__init__(app, conf) + + def __call__(self, env, start_response): + path = utils.safe_rstrip(env.get('PATH_INFO'), '/') + + # The information whether the API call is being performed against the + # public API is required for some other components. Saving it to the + # WSGI environment is reasonable thereby. + env['is_public_api'] = any(map(lambda pattern: re.match(pattern, path), + self.public_api_routes)) + + if env['is_public_api']: + return self._app(env, start_response) + + return super(AuthTokenMiddleware, self).__call__(env, start_response) diff --git a/evoque/cmd/manage.py b/evoque/cmd/manage.py index 5d21b8e..21a3d5f 100644 --- a/evoque/cmd/manage.py +++ b/evoque/cmd/manage.py @@ -11,7 +11,7 @@ # under the License. """ -Glance Management Utility +Evoque Management Utility """ import os diff --git a/evoque/common/exceptions.py b/evoque/common/exceptions.py index a1e3b96..97c2c44 100644 --- a/evoque/common/exceptions.py +++ b/evoque/common/exceptions.py @@ -10,6 +10,47 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import log as logging + +from evoque.common.i18n import _ +from evoque.common.i18n import _LE + +LOG = logging.getLogger(__name__) + class NotImplementedError(NotImplementedError): pass + + +class EvoqueException(Exception): + """Base Evoque Exception + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + message = _("An unknown exception occurred.") + code = 500 + + def __init__(self, message=None, **kwargs): + self.kwargs = kwargs + + if 'code' not in self.kwargs and hasattr(self, 'code'): + self.kwargs['code'] = self.code + + if message: + self.message = message + + try: + self.message = self.message % kwargs + except Exception as e: + # kwargs doesn't match a variable in the message + # log the issue and the kwargs + LOG.exception(_LE('Exception in string format operation, ' + 'kwargs: %s') % kwargs) + raise e + + super(EvoqueException, self).__init__(self.message) + + +class ConfigInvalid(EvoqueException): + message = _("Invalid configuration file. %(error_msg)s") diff --git a/evoque/common/utils.py b/evoque/common/utils.py new file mode 100644 index 0000000..ef815af --- /dev/null +++ b/evoque/common/utils.py @@ -0,0 +1,36 @@ +# 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. + +"""Utilities and helper functions.""" + +import six + +from oslo_log import log as logging + +from evoque.common.i18n import _LW + +LOG = logging.getLogger(__name__) + + +def safe_rstrip(value, chars=None): + """Removes trailing characters from a string if that does not make it empty + :param value: A string value that will be stripped. + :param chars: Characters to remove. + :return: Stripped value. + """ + if not isinstance(value, six.string_types): + LOG.warn(_LW("Failed to remove trailing character. Returning original " + "object. Supplied object is not a string: %s,"), value) + return value + + return value.rstrip(chars) or value diff --git a/evoque/opts.py b/evoque/opts.py index 299a297..9f49293 100644 --- a/evoque/opts.py +++ b/evoque/opts.py @@ -42,6 +42,11 @@ def list_opts(): default=1000, help=_('The maximum number of items returned in a ' 'single response from a collection resource')), + cfg.BoolOpt('enable_authentication', + default=True, + help=_('This option enables or disables user ' + 'authentication via Keystone. ' + 'Default value is True.')), )), ("DEFAULT", ( cfg.StrOpt('host', diff --git a/requirements.txt b/requirements.txt index 321f972..5502f27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,12 +3,16 @@ # process, which may cause wedges in the gate later. pbr>=1.6 +keystonemiddleware!=2.4.0,>=2.0.0 oslo.config>=2.6.0 # Apache-2.0 +oslo.context>=0.2.0 # Apache-2.0 oslo.db>=3.0.0 # Apache-2.0 oslo.i18n>=1.5.0 # Apache-2.0 oslo.log>=1.8.0 # Apache-2.0 oslo.messaging!=1.17.0,!=1.17.1,!=2.6.0,!=2.6.1,>=1.16.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 +oslo.service>=0.10.0 # Apache-2.0 +oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 pecan>=1.0.0 SQLAlchemy<1.1.0,>=0.9.9 sqlalchemy-migrate>=0.9.6