diff --git a/cloudpulse/api/__init__.py b/cloudpulse/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudpulse/api/app.py b/cloudpulse/api/app.py new file mode 100644 index 0000000..924ca32 --- /dev/null +++ b/cloudpulse/api/app.py @@ -0,0 +1,62 @@ +# 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 oslo_config import cfg +import pecan + +from cloudpulse.api import auth +from cloudpulse.api import config as api_config +from cloudpulse.api import middleware + +# Register options for the service +API_SERVICE_OPTS = [ + cfg.IntOpt('port', + default=9511, + help='The port for the cloudpulse API server'), + cfg.StrOpt('host', + default='127.0.0.1', + help='The listen IP for the cloudpulse API server'), + cfg.IntOpt('max_limit', + default=1000, + help='The maximum number of items returned in a single ' + 'response from a collection resource.') +] + +CONF = cfg.CONF +opt_group = cfg.OptGroup(name='api', + title='Options for the cloudpulse-api service') +CONF.register_group(opt_group) +CONF.register_opts(API_SERVICE_OPTS, opt_group) + + +def get_pecan_config(): + # Set up the pecan configuration + filename = api_config.__file__.replace('.pyc', '.py') + return pecan.configuration.conf_from_file(filename) + + +def setup_app(config=None): + if not config: + config = get_pecan_config() + + app_conf = dict(config.app) + + app = pecan.make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + wrap_app=middleware.ParsableErrorMiddleware, + **app_conf + ) + +# TBD Add test hook later +# cpulseTimer(10, timerfunc, "Cpulse") + return auth.install(app, CONF, config.app.acl_public_routes) diff --git a/cloudpulse/api/auth.py b/cloudpulse/api/auth.py new file mode 100644 index 0000000..bc4e487 --- /dev/null +++ b/cloudpulse/api/auth.py @@ -0,0 +1,48 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# 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 cloudpulse.api.middleware import auth_token + + +AUTH_OPTS = [ + cfg.BoolOpt('enable_authentication', + default=True, + help='This option enables or disables user authentication ' + 'via keystone. Default value is True.'), +] + +CONF = cfg.CONF +CONF.register_opts(AUTH_OPTS) + + +def install(app, conf, public_routes): + """Install ACL check on application. + + :param app: A WSGI applicatin. + :param conf: Settings. Dict'ified and passed to keystonemiddleware + :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.get('enable_authentication'): + return app + return auth_token.AuthTokenMiddleware(app, + conf=dict(conf), + public_api_routes=public_routes) diff --git a/cloudpulse/api/config.py b/cloudpulse/api/config.py new file mode 100644 index 0000000..b33cf57 --- /dev/null +++ b/cloudpulse/api/config.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2013 - Noorul Islam K M +# +# 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 cloudpulse.api import hooks + +# Pecan Application Configurations +app = { + 'root': 'cloudpulse.api.controllers.root.RootController', + 'modules': ['cloudpulse.api'], + 'debug': False, + 'hooks': [ + hooks.ContextHook(), + hooks.RPCHook(), + hooks.NoExceptionTracebackHook(), + ], + 'acl_public_routes': [ + '/' + ], +} diff --git a/cloudpulse/api/hooks.py b/cloudpulse/api/hooks.py new file mode 100644 index 0000000..3f8869c --- /dev/null +++ b/cloudpulse/api/hooks.py @@ -0,0 +1,119 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# 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 oslo_config import cfg +from oslo_utils import importutils +from pecan import hooks + +from cloudpulse.common import context +from cloudpulse.conductor import api as conductor_api + + +class ContextHook(hooks.PecanHook): + """Configures a request context and attaches it to the request. + + The following HTTP request headers are used: + + X-User: + Used for context.user. + + X-User-Id: + Used for context.user_id. + + X-Project-Name: + Used for context.project. + + X-Project-Id: + Used for context.project_id. + + X-Auth-Token: + Used for context.auth_token. + + """ + + def before(self, state): + headers = state.request.headers + user = headers.get('X-User') + user_id = headers.get('X-User-Id') + project = headers.get('X-Project-Name') + project_id = headers.get('X-Project-Id') + domain_id = headers.get('X-User-Domain-Id') + domain_name = headers.get('X-User-Domain-Name') + auth_token = headers.get('X-Storage-Token') + auth_token = headers.get('X-Auth-Token', auth_token) + auth_token_info = state.request.environ.get('keystone.token_info') + + auth_url = headers.get('X-Auth-Url') + if auth_url is None: + importutils.import_module('keystonemiddleware.auth_token') + auth_url = cfg.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=user, + user_id=user_id, + project=project, + project_id=project_id, + domain_id=domain_id, + domain_name=domain_name) + + +class RPCHook(hooks.PecanHook): + """Attach the rpcapi object to the request so controllers can get to it.""" + + def before(self, state): + state.request.rpcapi = conductor_api.API(context=state.request.context) + + +class NoExceptionTracebackHook(hooks.PecanHook): + """Workaround rpc.common: deserialize_remote_exception. + + deserialize_remote_exception builds rpc exception traceback into error + message which is then sent to the client. Such behavior is a security + concern so this hook is aimed to cut-off traceback from the error message. + """ + # NOTE(max_lobur): 'after' hook used instead of 'on_error' because + # 'on_error' never fired for wsme+pecan pair. wsme @wsexpose decorator + # catches and handles all the errors, so 'on_error' dedicated for unhandled + # exceptions never fired. + def after(self, state): + # Omit empty body. Some errors may not have body at this level yet. + if not state.response.body: + return + + # Do nothing if there is no error. + if 200 <= state.response.status_int < 400: + return + + json_body = state.response.json + # Do not remove traceback when server in debug mode (except 'Server' + # errors when 'debuginfo' will be used for traces). + if cfg.CONF.debug and json_body.get('faultcode') != 'Server': + return + + faultsting = json_body.get('faultstring') + traceback_marker = 'Traceback (most recent call last):' + if faultsting and (traceback_marker in faultsting): + # Cut-off traceback. + faultsting = faultsting.split(traceback_marker, 1)[0] + # Remove trailing newlines and spaces if any. + json_body['faultstring'] = faultsting.rstrip() + # Replace the whole json. Cannot change original one beacause it's + # generated on the fly. + state.response.json = json_body diff --git a/requirements.txt b/requirements.txt index aff346e..04a75f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ pbr>=0.6,!=0.7,<1.0 Babel>=1.3 jsonpatch>=1.1 +keystonemiddleware>=1.5.0 oslo.concurrency>=1.8.0,<1.9.0 # Apache-2.0 oslo.config>=1.9.3,<1.10.0 # Apache-2.0 oslo.context>=0.2.0,<0.3.0 # Apache-2.0