Inital API Commit
Creates necessary API files and implements health API route. Change-Id: Id545d65949fcc48a05565f39b08180d4aa86006f
This commit is contained in:
parent
4dd7e64f0b
commit
9eb6f9c686
|
@ -0,0 +1,25 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
#PasteDeploy Configuration File
|
||||
#Used to configure uWSGI middleware pipeline
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||
|
||||
[app:promenade-api]
|
||||
paste.app_factory = promenade.promenade:paste_start_promenade
|
||||
|
||||
[pipeline:main]
|
||||
pipeline = authtoken promenade-api
|
|
@ -147,7 +147,8 @@ def _matches_filter(document, *, schema, labels):
|
|||
def _get(documents, kind=None, schema=None, name=None):
|
||||
if kind is not None:
|
||||
if schema is not None:
|
||||
raise AssertionError('Logic error: specified both kind and schema')
|
||||
msg = "Only kind or schema may be specified, not both"
|
||||
raise exceptions.ValidationException(msg)
|
||||
schema = 'promenade/%s/v1' % kind
|
||||
|
||||
for document in documents:
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 falcon
|
||||
|
||||
from promenade.control.base import BaseResource, PromenadeRequest
|
||||
from promenade.control.health_api import HealthResource
|
||||
from promenade.control.middleware import (AuthMiddleware, ContextMiddleware,
|
||||
LoggingMiddleware)
|
||||
from promenade import exceptions as exc
|
||||
from promenade import logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_api():
|
||||
middlewares = [
|
||||
AuthMiddleware(),
|
||||
ContextMiddleware(),
|
||||
LoggingMiddleware(),
|
||||
]
|
||||
control_api = falcon.API(
|
||||
request_type=PromenadeRequest, middleware=middlewares)
|
||||
|
||||
# v1.0 of Promenade API
|
||||
v1_0_routes = [
|
||||
# API for managing region data
|
||||
('/health', HealthResource()),
|
||||
]
|
||||
|
||||
# Set up the 1.0 routes
|
||||
route_v1_0_prefix = '/api/v1.0'
|
||||
for path, res in v1_0_routes:
|
||||
route = '{}{}'.format(route_v1_0_prefix, path)
|
||||
LOG.info('Adding route: %s Handled by %s', route,
|
||||
res.__class__.__name__)
|
||||
control_api.add_route(route, res)
|
||||
|
||||
control_api.add_route('/versions', VersionsResource())
|
||||
|
||||
# Error handlers (FILO handling)
|
||||
control_api.add_error_handler(Exception, exc.default_exception_handler)
|
||||
control_api.add_error_handler(exc.PromenadeException,
|
||||
exc.PromenadeException.handle)
|
||||
|
||||
# built-in error serializer
|
||||
control_api.set_error_serializer(exc.default_error_serializer)
|
||||
|
||||
return control_api
|
||||
|
||||
|
||||
class VersionsResource(BaseResource):
|
||||
"""
|
||||
Lists the versions supported by this API
|
||||
"""
|
||||
|
||||
def on_get(self, req, resp):
|
||||
resp.body = self.to_json({
|
||||
'v1.0': {
|
||||
'path': '/api/v1.0',
|
||||
'status': 'stable'
|
||||
}
|
||||
})
|
||||
resp.status = falcon.HTTP_200
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 json
|
||||
import uuid
|
||||
|
||||
from oslo_context import context
|
||||
from jsonschema import validate
|
||||
|
||||
import falcon
|
||||
import falcon.request as request
|
||||
import falcon.routing as routing
|
||||
|
||||
from promenade import exceptions as exc
|
||||
from promenade import logging
|
||||
|
||||
|
||||
class BaseResource(object):
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('promenade.control')
|
||||
|
||||
def on_options(self, req, resp, **kwargs):
|
||||
"""
|
||||
Handle options requests
|
||||
"""
|
||||
method_map = routing.create_http_method_map(self)
|
||||
for method in method_map:
|
||||
if method_map.get(method).__name__ != 'method_not_allowed':
|
||||
resp.append_header('Allow', method)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def req_json(self, req, validate_json_schema=None):
|
||||
"""
|
||||
Reads and returns the input json message, optionally validates against
|
||||
a provided jsonschema
|
||||
:param req: the falcon request object
|
||||
:param validate_json_schema: the optional jsonschema to use for
|
||||
validation
|
||||
"""
|
||||
has_input = False
|
||||
if ((req.content_length is not None or req.content_length != 0)
|
||||
and (req.content_type is not None
|
||||
and req.content_type.lower() == 'application/json')):
|
||||
raw_body = req.stream.read(req.content_length or 0)
|
||||
if raw_body is not None:
|
||||
has_input = True
|
||||
self.info(req.context, 'Input message body: %s' % raw_body)
|
||||
else:
|
||||
self.info(req.context, 'No message body specified')
|
||||
if has_input:
|
||||
# read the json and validate if necessary
|
||||
try:
|
||||
raw_body = raw_body.decode('utf-8')
|
||||
json_body = json.loads(raw_body)
|
||||
if validate_json_schema:
|
||||
# raises an exception if it doesn't validate
|
||||
validate(json_body, json.loads(validate_json_schema))
|
||||
return json_body
|
||||
except json.JSONDecodeError as jex:
|
||||
self.error(req.context,
|
||||
"Invalid JSON in request: \n%s" % raw_body)
|
||||
raise exc.InvalidFormatError(
|
||||
title='JSON could not be decoded',
|
||||
description='%s: Invalid JSON in body: %s' % (req.path,
|
||||
jex))
|
||||
else:
|
||||
# No body passed as input. Fail validation if it was asekd for
|
||||
if validate_json_schema is not None:
|
||||
raise exc.InvalidFormatError(
|
||||
title='Json body is required',
|
||||
description='%s: Bad input, no body provided' % (req.path))
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_json(self, body_dict):
|
||||
"""
|
||||
Thin wrapper around json.dumps, providing the default=str config
|
||||
"""
|
||||
return json.dumps(body_dict, default=str)
|
||||
|
||||
def log_message(self, ctx, level, msg):
|
||||
"""
|
||||
Logs a message with context, and extra populated.
|
||||
"""
|
||||
extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'}
|
||||
|
||||
if ctx is not None:
|
||||
extra = {
|
||||
'user': ctx.user,
|
||||
'req_id': ctx.request_id,
|
||||
'external_ctx': ctx.external_marker,
|
||||
}
|
||||
|
||||
self.logger.log(level, msg, extra=extra)
|
||||
|
||||
def debug(self, ctx, msg):
|
||||
"""
|
||||
Debug logger for resources, incorporating context.
|
||||
"""
|
||||
self.log_message(ctx, logging.DEBUG, msg)
|
||||
|
||||
def info(self, ctx, msg):
|
||||
"""
|
||||
Info logger for resources, incorporating context.
|
||||
"""
|
||||
self.log_message(ctx, logging.INFO, msg)
|
||||
|
||||
def warn(self, ctx, msg):
|
||||
"""
|
||||
Warn logger for resources, incorporating context.
|
||||
"""
|
||||
self.log_message(ctx, logging.WARN, msg)
|
||||
|
||||
def error(self, ctx, msg):
|
||||
"""
|
||||
Error logger for resources, incorporating context.
|
||||
"""
|
||||
self.log_message(ctx, logging.ERROR, msg)
|
||||
|
||||
|
||||
class PromenadeRequestContext(context.RequestContext):
|
||||
"""
|
||||
Context object for promenade resource requests
|
||||
"""
|
||||
|
||||
def __init__(self, external_marker=None, policy_engine=None, **kwargs):
|
||||
self.log_level = 'error'
|
||||
self.request_id = str(uuid.uuid4())
|
||||
self.external_marker = external_marker
|
||||
self.policy_engine = policy_engine
|
||||
self.is_admin_project = False
|
||||
self.authenticated = False
|
||||
super(PromenadeRequestContext, self).__init__(**kwargs)
|
||||
|
||||
def set_log_level(self, level):
|
||||
if level in ['error', 'info', 'debug']:
|
||||
self.log_level = level
|
||||
|
||||
def set_user(self, user):
|
||||
self.user = user
|
||||
|
||||
def set_project(self, project):
|
||||
self.project = project
|
||||
|
||||
def add_role(self, role):
|
||||
self.roles.append(role)
|
||||
|
||||
def add_roles(self, roles):
|
||||
self.roles.extend(roles)
|
||||
|
||||
def remove_role(self, role):
|
||||
self.roles = [x for x in self.roles if x != role]
|
||||
|
||||
def set_external_marker(self, marker):
|
||||
self.external_marker = marker
|
||||
|
||||
def set_policy_engine(self, engine):
|
||||
self.policy_engine = engine
|
||||
|
||||
def to_policy_view(self):
|
||||
policy_dict = {}
|
||||
|
||||
policy_dict['user_id'] = self.user_id
|
||||
policy_dict['user_domain_id'] = self.user_domain_id
|
||||
policy_dict['project_id'] = self.project_id
|
||||
policy_dict['project_domain_id'] = self.project_domain_id
|
||||
policy_dict['roles'] = self.roles
|
||||
policy_dict['is_admin_project'] = self.is_admin_project
|
||||
|
||||
return policy_dict
|
||||
|
||||
|
||||
class PromenadeRequest(request.Request):
|
||||
context_type = PromenadeRequestContext
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 falcon
|
||||
|
||||
from promenade.control import base
|
||||
|
||||
|
||||
class HealthResource(base.BaseResource):
|
||||
"""
|
||||
Return empty response/body to show
|
||||
that promenade is healthy
|
||||
"""
|
||||
|
||||
def on_get(self, req, resp):
|
||||
"""
|
||||
It really does nothing right now. It may do more later
|
||||
"""
|
||||
resp.status = falcon.HTTP_204
|
|
@ -0,0 +1,127 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 uuid
|
||||
|
||||
from promenade import logging
|
||||
from promenade import policy
|
||||
|
||||
|
||||
class AuthMiddleware(object):
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('promenade')
|
||||
|
||||
# Authentication
|
||||
def process_request(self, req, resp):
|
||||
ctx = req.context
|
||||
ctx.set_policy_engine(policy.policy_engine)
|
||||
|
||||
for k, v in req.headers.items():
|
||||
self.logger.debug("Request with header %s: %s" % (k, v))
|
||||
|
||||
auth_status = req.get_header(
|
||||
'X-SERVICE-IDENTITY-STATUS') # will be set to Confirmed or Invalid
|
||||
service = True
|
||||
|
||||
if auth_status is None:
|
||||
auth_status = req.get_header('X-IDENTITY-STATUS')
|
||||
service = False
|
||||
|
||||
if auth_status == 'Confirmed':
|
||||
# Process account and roles
|
||||
ctx.authenticated = True
|
||||
# User Identity, unique within owning domain
|
||||
ctx.user = req.get_header(
|
||||
'X-SERVICE-USER-NAME') if service else req.get_header(
|
||||
'X-USER-NAME')
|
||||
# Identity-service managed unique identifier
|
||||
ctx.user_id = req.get_header(
|
||||
'X-SERVICE-USER-ID') if service else req.get_header(
|
||||
'X-USER-ID')
|
||||
# Identity service managed unique identifier of owning domain of
|
||||
# user name
|
||||
ctx.user_domain_id = req.get_header(
|
||||
'X-SERVICE-USER-DOMAIN-ID') if service else req.get_header(
|
||||
'X-USER-DOMAIN-ID')
|
||||
# Identity service managed unique identifier
|
||||
ctx.project_id = req.get_header(
|
||||
'X-SERVICE-PROJECT-ID') if service else req.get_header(
|
||||
'X-PROJECT-ID')
|
||||
# Name of owning domain of project
|
||||
ctx.project_domain_id = req.get_header(
|
||||
'X-SERVICE-PROJECT-DOMAIN-ID') if service else req.get_header(
|
||||
'X-PROJECT-DOMAIN-NAME')
|
||||
if service:
|
||||
# comma delimieted list of case-sensitive role names
|
||||
ctx.add_roles(req.get_header('X-SERVICE-ROLES').split(','))
|
||||
else:
|
||||
ctx.add_roles(req.get_header('X-ROLES').split(','))
|
||||
|
||||
if req.get_header('X-IS-ADMIN-PROJECT') == 'True':
|
||||
ctx.is_admin_project = True
|
||||
else:
|
||||
ctx.is_admin_project = False
|
||||
|
||||
self.logger.debug(
|
||||
'Request from authenticated user %s with roles %s', ctx.user,
|
||||
','.join(ctx.roles))
|
||||
else:
|
||||
ctx.authenticated = False
|
||||
|
||||
|
||||
class ContextMiddleware(object):
|
||||
"""
|
||||
Handle looking at the X-Context_Marker to see if it has value and that
|
||||
value is a UUID (or close enough). If not, generate one.
|
||||
"""
|
||||
|
||||
def _format_uuid_string(self, string):
|
||||
return (string.replace('urn:', '').replace('uuid:', '').strip('{}')
|
||||
.replace('-', '').lower())
|
||||
|
||||
def _is_uuid_like(self, val):
|
||||
try:
|
||||
return str(uuid.UUID(val)).replace(
|
||||
'-', '') == self._format_uuid_string(val)
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
def process_request(self, req, resp):
|
||||
ctx = req.context
|
||||
ext_marker = req.get_header('X-Context-Marker')
|
||||
if ext_marker is not None and self.is_uuid_like(ext_marker):
|
||||
# external passed in an ok context marker
|
||||
ctx.set_external_marker(ext_marker)
|
||||
else:
|
||||
# use the request id
|
||||
ctx.set_external_marker(ctx.request_id)
|
||||
|
||||
|
||||
class LoggingMiddleware(object):
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('promenade.control')
|
||||
|
||||
def process_response(self, req, resp, resource, req_succeeded):
|
||||
ctx = req.context
|
||||
|
||||
extra = {
|
||||
'user': ctx.user,
|
||||
'req_id': ctx.request_id,
|
||||
'external_ctx': ctx.external_marker,
|
||||
}
|
||||
|
||||
resp.append_header('X-Promenade-Req', ctx.request_id)
|
||||
self.logger.info(
|
||||
'%s %s - %s', req.method, req.uri, resp.status, extra=extra)
|
||||
self.logger.debug('Response body:\n%s', resp.body, extra=extra)
|
|
@ -1,21 +1,258 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 json
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import falcon
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Standard error handler
|
||||
def format_error_resp(req,
|
||||
resp,
|
||||
status_code,
|
||||
message="",
|
||||
reason="",
|
||||
error_type=None,
|
||||
retry=False,
|
||||
error_list=None,
|
||||
info_list=None):
|
||||
"""
|
||||
Write a error message body and throw a Falcon exception to trigger
|
||||
an HTTP status
|
||||
:param req: Falcon request object
|
||||
:param resp: Falcon response object to update
|
||||
:param status_code: Falcon status_code constant
|
||||
:param message: Optional error message to include in the body.
|
||||
This should be the summary level of the error
|
||||
message, encompassing an overall result. If
|
||||
no other messages are passed in the error_list,
|
||||
this message will be repeated in a generated
|
||||
message for the output message_list.
|
||||
:param reason: Optional reason code to include in the body
|
||||
:param error_type: If specified, the error type will be used,
|
||||
otherwise, this will be set to
|
||||
'Unspecified Exception'
|
||||
:param retry: Optional flag whether client should retry the operation.
|
||||
:param error_list: optional list of error dictionaries. Minimally,
|
||||
the dictionary will contain the 'message' field,
|
||||
but should also contain 'error': True
|
||||
:param info_list: optional list of info message dictionaries.
|
||||
Minimally, the dictionary needs to contain a
|
||||
'message' field, but should also have a
|
||||
'error': False field.
|
||||
"""
|
||||
|
||||
if error_type is None:
|
||||
error_type = 'Unspecified Exception'
|
||||
|
||||
# since we're handling errors here, if error list is none, set
|
||||
# up a default error item. If we have info items, add them to the
|
||||
# message list as well. In both cases, if the error flag is not
|
||||
# set, set it appropriately.
|
||||
if error_list is None:
|
||||
error_list = [{
|
||||
'message': 'An error ocurred, but was not specified',
|
||||
'error': True
|
||||
}]
|
||||
else:
|
||||
for error_item in error_list:
|
||||
if 'error' not in error_item:
|
||||
error_item['error'] = True
|
||||
|
||||
if info_list is None:
|
||||
info_list = []
|
||||
else:
|
||||
for info_item in info_list:
|
||||
if 'error' not in info_item:
|
||||
info_item['error'] = False
|
||||
|
||||
message_list = error_list + info_list
|
||||
|
||||
version = 'N/A'
|
||||
|
||||
for part in req.path.split('/'):
|
||||
if '.' in part and part.startswith('v'):
|
||||
version = part
|
||||
break
|
||||
|
||||
error_response = {
|
||||
'kind': 'status',
|
||||
'apiVersion': version,
|
||||
'metadata': {},
|
||||
'status': 'Failure',
|
||||
'message': message,
|
||||
'reason': reason,
|
||||
'details': {
|
||||
'errorType': error_type,
|
||||
'errorCount': len(error_list),
|
||||
'messageList': message_list
|
||||
},
|
||||
'code': status_code,
|
||||
'retry': retry
|
||||
}
|
||||
|
||||
resp.body = json.dumps(error_response, default=str)
|
||||
resp.content_type = 'application/json'
|
||||
resp.status = status_code
|
||||
|
||||
|
||||
def default_error_serializer(req, resp, exception):
|
||||
"""
|
||||
Writes the default error message body, when we don't handle it otherwise
|
||||
"""
|
||||
format_error_resp(
|
||||
req,
|
||||
resp,
|
||||
status_code=exception.status,
|
||||
message=exception.description,
|
||||
reason=exception.title,
|
||||
error_type=exception.__class__.__name__,
|
||||
error_list=[{
|
||||
'message': exception.description,
|
||||
'error': True
|
||||
}],
|
||||
info_list=None)
|
||||
|
||||
|
||||
def default_exception_handler(ex, req, resp, params):
|
||||
"""
|
||||
Catch-all exception handler for standardized output.
|
||||
If this is a standard falcon HTTPError, rethrow it for handling
|
||||
"""
|
||||
if isinstance(ex, falcon.HTTPError):
|
||||
# allow the falcon http errors to bubble up and get handled
|
||||
raise ex
|
||||
else:
|
||||
# take care of the uncaught stuff
|
||||
exc_string = traceback.format_exc()
|
||||
LOG.error('Unhanded Exception being handled: \n%s', exc_string)
|
||||
format_error_resp(
|
||||
req,
|
||||
resp,
|
||||
falcon.HTTP_500,
|
||||
error_type=ex.__class__.__name__,
|
||||
message="Unhandled Exception raised: %s" % str(ex),
|
||||
retry=True)
|
||||
|
||||
|
||||
class PromenadeException(Exception):
|
||||
"""
|
||||
Base error containing enough information to make a promenade-formatted
|
||||
error
|
||||
"""
|
||||
EXIT_CODE = 1
|
||||
|
||||
def __init__(self, message, *, trace=True):
|
||||
self.message = message
|
||||
def __init__(self,
|
||||
title=None,
|
||||
description=None,
|
||||
error_list=None,
|
||||
info_list=None,
|
||||
status=None,
|
||||
retry=False,
|
||||
trace=False):
|
||||
"""
|
||||
:param description: The internal error description
|
||||
:param error_list: The list of errors
|
||||
:param status: The desired falcon HTTP response code
|
||||
:param title: The title of the error message
|
||||
:param error_list: A list of errors to be included in output
|
||||
messages list
|
||||
:param info_list: A list of informational messages to be
|
||||
included in the output messages list
|
||||
:param retry: Optional retry directive for the consumer
|
||||
:param trace: Return traceback
|
||||
"""
|
||||
|
||||
self.title = title or self.__class__.title
|
||||
|
||||
self.status = status or self.__class__.status
|
||||
|
||||
self.description = description
|
||||
self.error_list = massage_error_list(error_list, description)
|
||||
self.info_list = info_list
|
||||
self.retry = retry
|
||||
self.trace = trace
|
||||
super().__init__(
|
||||
PromenadeException._gen_ex_message(title, description))
|
||||
|
||||
@staticmethod
|
||||
def _gen_ex_message(title, description):
|
||||
ttl = title or 'Exception'
|
||||
dsc = description or 'No additional decsription'
|
||||
return '{} : {}'.format(ttl, dsc)
|
||||
|
||||
@staticmethod
|
||||
def handle(ex, req, resp, params):
|
||||
"""
|
||||
The handler used for app errors and child classes
|
||||
"""
|
||||
format_error_resp(
|
||||
req,
|
||||
resp,
|
||||
ex.status,
|
||||
message=ex.title,
|
||||
reason=ex.description,
|
||||
error_list=ex.error_list,
|
||||
info_list=ex.info_list,
|
||||
error_type=ex.__class__.__name__,
|
||||
retry=ex.retry)
|
||||
|
||||
def display(self, debug=False):
|
||||
if self.trace or debug:
|
||||
LOG.exception(self.message)
|
||||
LOG.exception(self.description)
|
||||
else:
|
||||
LOG.error(self.message)
|
||||
LOG.error(self.description)
|
||||
|
||||
|
||||
class ApiError(PromenadeException):
|
||||
"""
|
||||
An error to handle general api errors.
|
||||
"""
|
||||
|
||||
title = 'Api Error'
|
||||
status = falcon.HTTP_400
|
||||
|
||||
|
||||
class InvalidFormatError(PromenadeException):
|
||||
"""
|
||||
An exception to cover invalid input formatting
|
||||
"""
|
||||
|
||||
title = 'Invalid Input Error'
|
||||
status = falcon.HTTP_400
|
||||
|
||||
|
||||
class ValidationException(PromenadeException):
|
||||
pass
|
||||
title = 'Validation Error'
|
||||
|
||||
|
||||
def massage_error_list(error_list, placeholder_description):
|
||||
"""
|
||||
Returns a best-effort attempt to make a nice error list
|
||||
"""
|
||||
output_error_list = []
|
||||
if error_list:
|
||||
for error in error_list:
|
||||
if not error.get('message'):
|
||||
output_error_list.append({'message': error, 'error': True})
|
||||
else:
|
||||
if 'error' not in error:
|
||||
error['error'] = True
|
||||
output_error_list.append(error)
|
||||
if not output_error_list:
|
||||
output_error_list.append({'message': placeholder_description})
|
||||
return output_error_list
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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
|
||||
|
||||
import falcon
|
||||
|
||||
from promenade import exceptions as ex
|
||||
|
||||
# TODO: Add policy_engine
|
||||
policy_engine = None
|
||||
|
||||
|
||||
class ApiEnforcer(object):
|
||||
"""
|
||||
A decorator class for enforcing RBAC policies
|
||||
"""
|
||||
|
||||
def __init__(self, action):
|
||||
self.action = action
|
||||
|
||||
def __call__(self, f):
|
||||
@functools.wraps(f)
|
||||
def secure_handler(slf, req, resp, *args, **kwargs):
|
||||
ctx = req.context
|
||||
policy_eng = ctx.policy_engine
|
||||
slf.info(ctx, "Policy Engine: %s" % policy_eng.__class__.__name__)
|
||||
# perform auth
|
||||
slf.info(ctx, "Enforcing policy %s on request %s" %
|
||||
(self.action, ctx.request_id))
|
||||
# policy engine must be configured
|
||||
if policy_eng is None:
|
||||
slf.error(
|
||||
ctx,
|
||||
"Error-Policy engine required-action: %s" % self.action)
|
||||
raise ex.PromenadeException(
|
||||
title="Auth is not being handled by any policy engine",
|
||||
status=falcon.HTTP_500,
|
||||
retry=False)
|
||||
authorized = False
|
||||
try:
|
||||
if policy_eng.authorize(self.action, ctx):
|
||||
# authorized
|
||||
slf.info(ctx, "Request is authorized")
|
||||
authorized = True
|
||||
except Exception:
|
||||
# couldn't service the auth request
|
||||
slf.error(
|
||||
ctx,
|
||||
"Error - Expectation Failed - action: %s" % self.action)
|
||||
raise ex.ApiError(
|
||||
title="Expectation Failed",
|
||||
status=falcon.HTTP_417,
|
||||
retry=False)
|
||||
if authorized:
|
||||
return f(slf, req, resp, *args, **kwargs)
|
||||
else:
|
||||
slf.error(
|
||||
ctx,
|
||||
"Auth check failed. Authenticated:%s" % ctx.authenticated)
|
||||
# raise the appropriate response exeception
|
||||
if ctx.authenticated:
|
||||
slf.error(
|
||||
ctx,
|
||||
"Error: Forbidden access - action: %s" % self.action)
|
||||
raise ex.ApiError(
|
||||
title="Forbidden",
|
||||
status=falcon.HTTP_403,
|
||||
description="Credentials do not permit access",
|
||||
retry=False)
|
||||
else:
|
||||
slf.error(ctx, "Error - Unauthenticated access")
|
||||
raise ex.ApiError(
|
||||
title="Unauthenticated",
|
||||
status=falcon.HTTP_401,
|
||||
description="Credentials are not established",
|
||||
retry=False)
|
||||
|
||||
return secure_handler
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 promenade.control import api
|
||||
from promenade import logging
|
||||
|
||||
|
||||
def start_promenade():
|
||||
# Setup root logger
|
||||
logging.setup(verbose=False)
|
||||
# TODO: Add policy engine to start
|
||||
# Start the API
|
||||
return api.start_api()
|
||||
|
||||
|
||||
# Initialization compatible with PasteDeploy
|
||||
def paste_start_promenade(global_conf, **kwargs):
|
||||
return promenade
|
||||
|
||||
|
||||
promenade = start_promenade()
|
|
@ -1,8 +1,9 @@
|
|||
from . import logging
|
||||
import hashlib
|
||||
import io
|
||||
import tarfile
|
||||
|
||||
from promenade import logging
|
||||
|
||||
__all__ = ['TarBundler']
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
from . import exceptions
|
||||
from . import logging
|
||||
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# 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 promenade import exceptions
|
||||
from promenade import logging
|
||||
import jsonschema
|
||||
import os
|
||||
import pkg_resources
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
click==6.7
|
||||
falcon==1.2.0
|
||||
jinja2==2.9.6
|
||||
jsonpath-ng==1.4.3
|
||||
jsonschema==2.6.0
|
||||
keystonemiddleware==4.17.0
|
||||
oslo.context>=2.14.0
|
||||
PasteDeploy==1.5.2
|
||||
pbr==3.0.1
|
||||
pyyaml==3.12
|
||||
requests==2.18.4
|
||||
|
|
|
@ -2,14 +2,18 @@ certifi==2017.7.27.1
|
|||
chardet==3.0.4
|
||||
click==6.7
|
||||
decorator==4.1.2
|
||||
falcon==1.2.0
|
||||
idna==2.6
|
||||
Jinja2==2.9.6
|
||||
jsonpath-ng==1.4.3
|
||||
jsonschema==2.6.0
|
||||
keystonemiddleware==4.17.0
|
||||
MarkupSafe==1.0
|
||||
oslo.context==2.19.1
|
||||
PasteDeploy==1.5.2
|
||||
pbr==3.0.1
|
||||
ply==3.10
|
||||
PyYAML==3.12
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
urllib3==1.22
|
||||
urllib3==1.22
|
Loading…
Reference in New Issue