Integrate Deckhand with keystone auth
This PS integrates Deckhand with keystone auth so that Deckhand can check whether a keystone token is authenticated (by way of keystonemiddleware) before proceeding with any requests. The architecture for this PS is borrowed from [0] which successfully integrates keystone authentication with the falcon web application framework. However, additional Deckhand-specific changes were made for tests to pass. The following changes have been made: - add paste deploy configuration file which adds keystonemiddleware integration to Deckhand; this makes it trivial for keystonemiddleware to determine whether a token in the X-Auth-Token header is authenticated - use paste.deploy to create a web app - update unit tests for testing controllers - update functional test script to ignore keystone authentication because functional tests don't currently support keystone integration [0] https://github.com/stannum-l/nautilus Change-Id: I6eeeb4a4d9ab1f1cc8fb338e5cc21136ab4d5684
This commit is contained in:
parent
d2d2312af9
commit
90226c2ae1
@ -16,7 +16,7 @@ from deckhand.control import api
|
|||||||
|
|
||||||
|
|
||||||
def start_deckhand():
|
def start_deckhand():
|
||||||
return api.start_api()
|
return api.init_application()
|
||||||
|
|
||||||
|
|
||||||
# Callable to be used by uwsgi.
|
# Callable to be used by uwsgi.
|
||||||
|
@ -33,15 +33,35 @@ barbican_opts = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
context_opts = [
|
||||||
|
cfg.BoolOpt('allow_anonymous_access', default=False,
|
||||||
|
help="""
|
||||||
|
Allow limited access to unauthenticated users.
|
||||||
|
|
||||||
|
Assign a boolean to determine API access for unathenticated
|
||||||
|
users. When set to False, the API cannot be accessed by
|
||||||
|
unauthenticated users. When set to True, unauthenticated users can
|
||||||
|
access the API with read-only privileges. This however only applies
|
||||||
|
when using ContextMiddleware.
|
||||||
|
|
||||||
|
Possible values:
|
||||||
|
* True
|
||||||
|
* False
|
||||||
|
"""),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def register_opts(conf):
|
def register_opts(conf):
|
||||||
conf.register_group(barbican_group)
|
conf.register_group(barbican_group)
|
||||||
conf.register_opts(barbican_opts, group=barbican_group)
|
conf.register_opts(barbican_opts, group=barbican_group)
|
||||||
|
conf.register_opts(context_opts)
|
||||||
ks_loading.register_auth_conf_options(conf, group=barbican_group.name)
|
ks_loading.register_auth_conf_options(conf, group=barbican_group.name)
|
||||||
ks_loading.register_session_conf_options(conf, group=barbican_group.name)
|
ks_loading.register_session_conf_options(conf, group=barbican_group.name)
|
||||||
|
|
||||||
|
|
||||||
def list_opts():
|
def list_opts():
|
||||||
opts = {barbican_group: barbican_opts +
|
opts = {None: context_opts,
|
||||||
|
barbican_group: barbican_opts +
|
||||||
ks_loading.get_session_conf_options() +
|
ks_loading.get_session_conf_options() +
|
||||||
ks_loading.get_auth_common_conf_options() +
|
ks_loading.get_auth_common_conf_options() +
|
||||||
ks_loading.get_auth_plugin_conf_options(
|
ks_loading.get_auth_plugin_conf_options(
|
||||||
|
@ -43,3 +43,12 @@ class RequestContext(context.RequestContext):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, values):
|
def from_dict(cls, values):
|
||||||
return cls(**values)
|
return cls(**values)
|
||||||
|
|
||||||
|
|
||||||
|
def get_context():
|
||||||
|
"""A helper method to get a blank context (useful for tests)."""
|
||||||
|
return RequestContext(user_id=None,
|
||||||
|
project_id=None,
|
||||||
|
roles=[],
|
||||||
|
is_admin=False,
|
||||||
|
overwrite=False)
|
||||||
|
@ -12,27 +12,22 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging as py_logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import falcon
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_policy import policy
|
||||||
|
from paste import deploy
|
||||||
|
|
||||||
from deckhand.control import base
|
|
||||||
from deckhand.control import buckets
|
|
||||||
from deckhand.control import revision_diffing
|
|
||||||
from deckhand.control import revision_documents
|
|
||||||
from deckhand.control import revision_tags
|
|
||||||
from deckhand.control import revisions
|
|
||||||
from deckhand.control import rollback
|
|
||||||
from deckhand.control import versions
|
|
||||||
from deckhand.db.sqlalchemy import api as db_api
|
from deckhand.db.sqlalchemy import api as db_api
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
logging.register_options(CONF)
|
|
||||||
|
|
||||||
# TODO(fmontei): Include deckhand-paste.ini later.
|
logging.register_options(CONF)
|
||||||
CONFIG_FILES = ['deckhand.conf']
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONFIG_FILES = ['deckhand.conf', 'deckhand-paste.ini']
|
||||||
|
|
||||||
|
|
||||||
def _get_config_files(env=None):
|
def _get_config_files(env=None):
|
||||||
@ -42,46 +37,38 @@ def _get_config_files(env=None):
|
|||||||
return [os.path.join(dirname, config_file) for config_file in CONFIG_FILES]
|
return [os.path.join(dirname, config_file) for config_file in CONFIG_FILES]
|
||||||
|
|
||||||
|
|
||||||
def start_api():
|
def setup_logging(conf):
|
||||||
|
# Add additional dependent libraries that have unhelp bug levels
|
||||||
|
extra_log_level_defaults = []
|
||||||
|
|
||||||
|
logging.set_defaults(default_log_levels=logging.get_default_log_levels() +
|
||||||
|
extra_log_level_defaults)
|
||||||
|
logging.setup(conf, 'deckhand')
|
||||||
|
py_logging.captureWarnings(True)
|
||||||
|
|
||||||
|
|
||||||
|
def init_application():
|
||||||
"""Main entry point for initializing the Deckhand API service.
|
"""Main entry point for initializing the Deckhand API service.
|
||||||
|
|
||||||
Create routes for the v1.0 API and sets up logging.
|
Create routes for the v1.0 API and sets up logging.
|
||||||
"""
|
"""
|
||||||
config_files = _get_config_files()
|
config_files = _get_config_files()
|
||||||
CONF([], project='deckhand', default_config_files=config_files)
|
paste_file = config_files[-1]
|
||||||
logging.setup(CONF, "deckhand")
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
CONF([], project='deckhand', default_config_files=config_files)
|
||||||
LOG.info('Initiated Deckhand logging.')
|
setup_logging(CONF)
|
||||||
|
|
||||||
|
policy.Enforcer(CONF)
|
||||||
|
|
||||||
|
LOG.debug('Starting WSGI application using %s configuration file.',
|
||||||
|
paste_file)
|
||||||
|
|
||||||
db_api.drop_db()
|
db_api.drop_db()
|
||||||
db_api.setup_db()
|
db_api.setup_db()
|
||||||
|
|
||||||
control_api = falcon.API(request_type=base.DeckhandRequest)
|
app = deploy.loadapp('config:%s' % paste_file, name='deckhand_api')
|
||||||
|
return app
|
||||||
v1_0_routes = [
|
|
||||||
('bucket/{bucket_name}/documents', buckets.BucketsResource()),
|
|
||||||
('revisions', revisions.RevisionsResource()),
|
|
||||||
('revisions/{revision_id}', revisions.RevisionsResource()),
|
|
||||||
('revisions/{revision_id}/diff/{comparison_revision_id}',
|
|
||||||
revision_diffing.RevisionDiffingResource()),
|
|
||||||
('revisions/{revision_id}/documents',
|
|
||||||
revision_documents.RevisionDocumentsResource()),
|
|
||||||
('revisions/{revision_id}/rendered-documents',
|
|
||||||
revision_documents.RenderedDocumentsResource()),
|
|
||||||
('revisions/{revision_id}/tags', revision_tags.RevisionTagsResource()),
|
|
||||||
('revisions/{revision_id}/tags/{tag}',
|
|
||||||
revision_tags.RevisionTagsResource()),
|
|
||||||
('rollback/{revision_id}', rollback.RollbackResource())
|
|
||||||
]
|
|
||||||
|
|
||||||
for path, res in v1_0_routes:
|
|
||||||
control_api.add_route(os.path.join('/api/v1.0', path), res)
|
|
||||||
|
|
||||||
control_api.add_route('/versions', versions.VersionsResource())
|
|
||||||
|
|
||||||
return control_api
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
start_api()
|
init_application()
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
|
|
||||||
from deckhand import context
|
from deckhand import context
|
||||||
@ -35,25 +33,9 @@ class BaseResource(object):
|
|||||||
resp.headers['Allow'] = ','.join(allowed_methods)
|
resp.headers['Allow'] = ','.join(allowed_methods)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
def to_yaml_body(self, dict_body):
|
|
||||||
"""Converts JSON body into YAML response body.
|
|
||||||
|
|
||||||
:param dict_body: response body to be converted to YAML.
|
|
||||||
:returns: YAML encoding of `dict_body`.
|
|
||||||
"""
|
|
||||||
if isinstance(dict_body, dict):
|
|
||||||
return yaml.safe_dump(dict_body)
|
|
||||||
elif isinstance(dict_body, list):
|
|
||||||
return yaml.safe_dump_all(dict_body)
|
|
||||||
raise TypeError('Unrecognized dict_body type when converting response '
|
|
||||||
'body to YAML format.')
|
|
||||||
|
|
||||||
|
|
||||||
class DeckhandRequest(falcon.Request):
|
class DeckhandRequest(falcon.Request):
|
||||||
|
context_type = context.RequestContext
|
||||||
def __init__(self, env, options=None):
|
|
||||||
super(DeckhandRequest, self).__init__(env, options)
|
|
||||||
self.context = context.RequestContext.from_environ(self.env)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project_id(self):
|
def project_id(self):
|
||||||
|
@ -66,8 +66,7 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
bucket_name, list(documents_to_create))
|
bucket_name, list(documents_to_create))
|
||||||
|
|
||||||
if created_documents:
|
if created_documents:
|
||||||
resp.body = self.to_yaml_body(
|
resp.body = self.view_builder.list(created_documents)
|
||||||
self.view_builder.list(created_documents))
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
|
|
||||||
|
134
deckhand/control/middleware.py
Normal file
134
deckhand/control/middleware.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# 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 yaml
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_serialization import jsonutils as json
|
||||||
|
|
||||||
|
import deckhand.context
|
||||||
|
from deckhand import errors
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMiddleware(object):
|
||||||
|
|
||||||
|
def process_request(self, req, resp):
|
||||||
|
"""Convert authentication information into a request context.
|
||||||
|
|
||||||
|
Generate a ``deckhand.context.RequestContext`` object from the
|
||||||
|
available authentication headers and store in the ``context`` attribute
|
||||||
|
of the ``req`` object.
|
||||||
|
|
||||||
|
:param req: ``falcon`` request object that will be given the context
|
||||||
|
object.
|
||||||
|
:raises: falcon.HTTPUnauthorized: when value of the
|
||||||
|
'X-Identity-Status' header is not 'Confirmed' and anonymous access
|
||||||
|
is disallowed.
|
||||||
|
"""
|
||||||
|
if req.headers.get('X-IDENTITY-STATUS') == 'Confirmed':
|
||||||
|
req.context = deckhand.context.RequestContext.from_environ(req.env)
|
||||||
|
elif CONF.allow_anonymous_access:
|
||||||
|
req.context = deckhand.context.get_context()
|
||||||
|
else:
|
||||||
|
raise falcon.HTTPUnauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
class HookableMiddlewareMixin(object):
|
||||||
|
"""Provides methods to extract before and after hooks from WSGI Middleware
|
||||||
|
Prior to falcon 0.2.0b1, it's necessary to provide falcon with middleware
|
||||||
|
as "hook" functions that are either invoked before (to process requests)
|
||||||
|
or after (to process responses) the API endpoint code runs.
|
||||||
|
This mixin allows the process_request and process_response methods from a
|
||||||
|
typical WSGI middleware object to be extracted for use as these hooks, with
|
||||||
|
the appropriate method signatures.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def as_before_hook(self):
|
||||||
|
"""Extract process_request method as "before" hook
|
||||||
|
:return: before hook function
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Need to wrap this up in a closure because the parameter counts
|
||||||
|
# differ
|
||||||
|
def before_hook(req, resp, params=None):
|
||||||
|
return self.process_request(req, resp)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return before_hook
|
||||||
|
except AttributeError as ex:
|
||||||
|
# No such method, we presume.
|
||||||
|
message_template = ("Failed to get before hook from middleware "
|
||||||
|
"{0} - {1}")
|
||||||
|
message = message_template.format(self.__name__, ex.message)
|
||||||
|
LOG.error(message)
|
||||||
|
raise errors.DeckhandException(message)
|
||||||
|
|
||||||
|
def as_after_hook(self):
|
||||||
|
"""Extract process_response method as "after" hook
|
||||||
|
:return: after hook function
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Need to wrap this up in a closure because the parameter counts
|
||||||
|
# differ
|
||||||
|
def after_hook(req, resp, resource=None):
|
||||||
|
return self.process_response(req, resp, resource)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return after_hook
|
||||||
|
except AttributeError as ex:
|
||||||
|
# No such method, we presume.
|
||||||
|
message_template = ("Failed to get after hook from middleware "
|
||||||
|
"{0} - {1}")
|
||||||
|
message = message_template.format(self.__name__, ex.message)
|
||||||
|
LOG.error(message)
|
||||||
|
raise errors.DeckhandException(message)
|
||||||
|
|
||||||
|
|
||||||
|
class YAMLTranslator(HookableMiddlewareMixin, object):
|
||||||
|
"""Middleware for converting all responses (error and success) to YAML.
|
||||||
|
|
||||||
|
``falcon`` error exceptions use JSON formatting and headers by default.
|
||||||
|
This middleware will intercept all responses and guarantee they are YAML
|
||||||
|
format.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This does not include the 401 Unauthorized that is raised by
|
||||||
|
``keystonemiddleware`` which is executed in the pipeline before
|
||||||
|
``falcon`` middleware.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_response(self, req, resp, resource):
|
||||||
|
resp.set_header('Content-Type', 'application/x-yaml')
|
||||||
|
|
||||||
|
for attr in ('body', 'data'):
|
||||||
|
if not hasattr(resp, attr):
|
||||||
|
continue
|
||||||
|
|
||||||
|
resp_attr = getattr(resp, attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp_attr = json.loads(resp_attr)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if isinstance(resp_attr, dict):
|
||||||
|
setattr(resp, attr, yaml.safe_dump(resp_attr))
|
||||||
|
elif isinstance(resp_attr, (list, tuple)):
|
||||||
|
setattr(resp, attr, yaml.safe_dump_all(resp_attr))
|
@ -38,4 +38,4 @@ class RevisionDiffingResource(api_base.BaseResource):
|
|||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(resp_body)
|
resp.body = resp_body
|
||||||
|
@ -62,7 +62,7 @@ class RevisionDocumentsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(self.view_builder.list(documents))
|
resp.body = self.view_builder.list(documents)
|
||||||
|
|
||||||
|
|
||||||
class RenderedDocumentsResource(api_base.BaseResource):
|
class RenderedDocumentsResource(api_base.BaseResource):
|
||||||
@ -109,5 +109,4 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(
|
resp.body = self.view_builder.list(rendered_documents)
|
||||||
self.view_builder.list(rendered_documents))
|
|
||||||
|
@ -52,7 +52,7 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(resp_body)
|
resp.body = resp_body
|
||||||
|
|
||||||
def on_get(self, req, resp, revision_id, tag=None):
|
def on_get(self, req, resp, revision_id, tag=None):
|
||||||
"""Show tag details or list all tags for a revision."""
|
"""Show tag details or list all tags for a revision."""
|
||||||
@ -73,7 +73,7 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(resp_body)
|
resp.body = resp_body
|
||||||
|
|
||||||
@policy.authorize('deckhand:list_tags')
|
@policy.authorize('deckhand:list_tags')
|
||||||
def _list_all_tags(self, req, resp, revision_id):
|
def _list_all_tags(self, req, resp, revision_id):
|
||||||
@ -86,7 +86,7 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
resp_body = revision_tag_view.ViewBuilder().list(resp_tags)
|
resp_body = revision_tag_view.ViewBuilder().list(resp_tags)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(resp_body)
|
resp.body = resp_body
|
||||||
|
|
||||||
def on_delete(self, req, resp, revision_id, tag=None):
|
def on_delete(self, req, resp, revision_id, tag=None):
|
||||||
"""Deletes a single tag or deletes all tags for a revision."""
|
"""Deletes a single tag or deletes all tags for a revision."""
|
||||||
|
@ -54,7 +54,7 @@ class RevisionsResource(api_base.BaseResource):
|
|||||||
revision_resp = self.view_builder.show(revision)
|
revision_resp = self.view_builder.show(revision)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(revision_resp)
|
resp.body = revision_resp
|
||||||
|
|
||||||
@policy.authorize('deckhand:list_revisions')
|
@policy.authorize('deckhand:list_revisions')
|
||||||
@common.sanitize_params(['tag'])
|
@common.sanitize_params(['tag'])
|
||||||
@ -64,7 +64,7 @@ class RevisionsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(revisions_resp)
|
resp.body = revisions_resp
|
||||||
|
|
||||||
@policy.authorize('deckhand:delete_revisions')
|
@policy.authorize('deckhand:delete_revisions')
|
||||||
def on_delete(self, req, resp):
|
def on_delete(self, req, resp):
|
||||||
|
@ -48,4 +48,4 @@ class RollbackResource(api_base.BaseResource):
|
|||||||
revision_resp = self.view_builder.show(rollback_revision)
|
revision_resp = self.view_builder.show(rollback_revision)
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
resp.body = self.to_yaml_body(revision_resp)
|
resp.body = revision_resp
|
||||||
|
68
deckhand/service.py
Normal file
68
deckhand/service.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# 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 os
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from deckhand.control import base
|
||||||
|
from deckhand.control import buckets
|
||||||
|
from deckhand.control import middleware
|
||||||
|
from deckhand.control import revision_diffing
|
||||||
|
from deckhand.control import revision_documents
|
||||||
|
from deckhand.control import revision_tags
|
||||||
|
from deckhand.control import revisions
|
||||||
|
from deckhand.control import rollback
|
||||||
|
from deckhand.control import versions
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_app(app, version=''):
|
||||||
|
|
||||||
|
v1_0_routes = [
|
||||||
|
('bucket/{bucket_name}/documents', buckets.BucketsResource()),
|
||||||
|
('revisions', revisions.RevisionsResource()),
|
||||||
|
('revisions/{revision_id}', revisions.RevisionsResource()),
|
||||||
|
('revisions/{revision_id}/diff/{comparison_revision_id}',
|
||||||
|
revision_diffing.RevisionDiffingResource()),
|
||||||
|
('revisions/{revision_id}/documents',
|
||||||
|
revision_documents.RevisionDocumentsResource()),
|
||||||
|
('revisions/{revision_id}/rendered-documents',
|
||||||
|
revision_documents.RenderedDocumentsResource()),
|
||||||
|
('revisions/{revision_id}/tags', revision_tags.RevisionTagsResource()),
|
||||||
|
('revisions/{revision_id}/tags/{tag}',
|
||||||
|
revision_tags.RevisionTagsResource()),
|
||||||
|
('rollback/{revision_id}', rollback.RollbackResource())
|
||||||
|
]
|
||||||
|
|
||||||
|
for path, res in v1_0_routes:
|
||||||
|
app.add_route(os.path.join('/api/%s' % version, path), res)
|
||||||
|
app.add_route('/versions', versions.VersionsResource())
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def deckhand_app_factory(global_config, **local_config):
|
||||||
|
# The order of the middleware is important because the `process_response`
|
||||||
|
# method for `YAMLTranslator` should execute after that of any other
|
||||||
|
# middleware to convert the response to YAML format.
|
||||||
|
middleware_list = [middleware.YAMLTranslator(),
|
||||||
|
middleware.ContextMiddleware()]
|
||||||
|
|
||||||
|
app = falcon.API(request_type=base.DeckhandRequest,
|
||||||
|
middleware=middleware_list)
|
||||||
|
|
||||||
|
return configure_app(app, version='v1.0')
|
@ -28,7 +28,3 @@ tests:
|
|||||||
PUT: /api/v1.0/bucket/b/documents
|
PUT: /api/v1.0/bucket/b/documents
|
||||||
status: 409
|
status: 409
|
||||||
data: <@resources/sample-doc.yaml
|
data: <@resources/sample-doc.yaml
|
||||||
# Deckhand exceptions return the following content-type header by
|
|
||||||
# default. TODO(fmontei): Override that later.
|
|
||||||
response_headers:
|
|
||||||
content-type: 'application/json; charset=UTF-8'
|
|
@ -69,7 +69,3 @@ tests:
|
|||||||
desc: Verify that the revision was deleted
|
desc: Verify that the revision was deleted
|
||||||
GET: /api/v1.0/revisions/$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']
|
GET: /api/v1.0/revisions/$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']
|
||||||
status: 404
|
status: 404
|
||||||
# Deckhand exceptions return the following content-type header by
|
|
||||||
# default. TODO(fmontei): Override that later.
|
|
||||||
response_headers:
|
|
||||||
content-type: 'application/json; charset=UTF-8'
|
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
|
@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
from falcon import testing as falcon_testing
|
from falcon import testing as falcon_testing
|
||||||
|
|
||||||
from deckhand.control import api
|
from deckhand import service
|
||||||
from deckhand.tests.unit import base as test_base
|
from deckhand.tests.unit import base as test_base
|
||||||
from deckhand.tests.unit import policy_fixture
|
from deckhand.tests.unit import fixtures
|
||||||
|
|
||||||
|
|
||||||
class BaseControllerTest(test_base.DeckhandWithDBTestCase,
|
class BaseControllerTest(test_base.DeckhandWithDBTestCase,
|
||||||
@ -25,5 +25,9 @@ class BaseControllerTest(test_base.DeckhandWithDBTestCase,
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseControllerTest, self).setUp()
|
super(BaseControllerTest, self).setUp()
|
||||||
self.app = falcon_testing.TestClient(api.start_api())
|
self.app = falcon_testing.TestClient(
|
||||||
self.policy = self.useFixture(policy_fixture.RealPolicyFixture())
|
service.deckhand_app_factory(None))
|
||||||
|
self.policy = self.useFixture(fixtures.RealPolicyFixture())
|
||||||
|
# NOTE: allow_anonymous_access allows these unit tests to get around
|
||||||
|
# Keystone authentication.
|
||||||
|
self.useFixture(fixtures.ConfPatcher(allow_anonymous_access=True))
|
||||||
|
@ -16,7 +16,6 @@ import inspect
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
from deckhand.control import api
|
from deckhand.control import api
|
||||||
from deckhand.control import base
|
|
||||||
from deckhand.control import buckets
|
from deckhand.control import buckets
|
||||||
from deckhand.control import revision_diffing
|
from deckhand.control import revision_diffing
|
||||||
from deckhand.control import revision_documents
|
from deckhand.control import revision_documents
|
||||||
@ -45,19 +44,17 @@ class TestApi(test_base.DeckhandTestCase):
|
|||||||
if inspect.isclass(obj)]
|
if inspect.isclass(obj)]
|
||||||
return class_names
|
return class_names
|
||||||
|
|
||||||
|
@mock.patch.object(api, 'policy', autospec=True)
|
||||||
@mock.patch.object(api, 'db_api', autospec=True)
|
@mock.patch.object(api, 'db_api', autospec=True)
|
||||||
@mock.patch.object(api, 'logging', autospec=True)
|
@mock.patch.object(api, 'logging', autospec=True)
|
||||||
@mock.patch.object(api, 'CONF', autospec=True)
|
@mock.patch.object(api, 'CONF', autospec=True)
|
||||||
@mock.patch.object(api, 'falcon', autospec=True)
|
@mock.patch('deckhand.service.falcon', autospec=True)
|
||||||
def test_start_api(self, mock_falcon, mock_config, mock_logging,
|
def test_init_application(self, mock_falcon, mock_config, mock_logging,
|
||||||
mock_db_api):
|
mock_db_api, _):
|
||||||
mock_falcon_api = mock_falcon.API.return_value
|
mock_falcon_api = mock_falcon.API.return_value
|
||||||
|
|
||||||
result = api.start_api()
|
api.init_application()
|
||||||
self.assertEqual(mock_falcon_api, result)
|
|
||||||
|
|
||||||
mock_falcon.API.assert_called_once_with(
|
|
||||||
request_type=base.DeckhandRequest)
|
|
||||||
mock_falcon_api.add_route.assert_has_calls([
|
mock_falcon_api.add_route.assert_has_calls([
|
||||||
mock.call('/api/v1.0/bucket/{bucket_name}/documents',
|
mock.call('/api/v1.0/bucket/{bucket_name}/documents',
|
||||||
self.buckets_resource()),
|
self.buckets_resource()),
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
"""Fixtures for Deckhand tests."""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -24,10 +27,37 @@ from deckhand import policies
|
|||||||
import deckhand.policy
|
import deckhand.policy
|
||||||
from deckhand.tests.unit import fake_policy
|
from deckhand.tests.unit import fake_policy
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ConfPatcher(fixtures.Fixture):
|
||||||
|
"""Fixture to patch and restore global CONF.
|
||||||
|
|
||||||
|
This also resets overrides for everything that is patched during
|
||||||
|
it's teardown.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Constructor
|
||||||
|
|
||||||
|
:params group: if specified all config options apply to that group.
|
||||||
|
|
||||||
|
:params **kwargs: the rest of the kwargs are processed as a
|
||||||
|
set of key/value pairs to be set as configuration override.
|
||||||
|
|
||||||
|
"""
|
||||||
|
super(ConfPatcher, self).__init__()
|
||||||
|
self.group = kwargs.pop('group', None)
|
||||||
|
self.args = kwargs
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ConfPatcher, self).setUp()
|
||||||
|
for k, v in self.args.items():
|
||||||
|
self.addCleanup(CONF.clear_override, k, self.group)
|
||||||
|
CONF.set_override(k, v, self.group)
|
||||||
|
|
||||||
|
|
||||||
class RealPolicyFixture(fixtures.Fixture):
|
class RealPolicyFixture(fixtures.Fixture):
|
||||||
"""Load the live policy for tests.
|
"""Load the live policy for tests.
|
||||||
|
|
@ -17,7 +17,7 @@ from oslo_policy import policy as common_policy
|
|||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
import deckhand.policy
|
import deckhand.policy
|
||||||
from deckhand.tests.unit import base as test_base
|
from deckhand.tests.unit import base as test_base
|
||||||
from deckhand.tests.unit import policy_fixture
|
from deckhand.tests.unit import fixtures
|
||||||
|
|
||||||
|
|
||||||
class PolicyBaseTestCase(test_base.DeckhandTestCase):
|
class PolicyBaseTestCase(test_base.DeckhandTestCase):
|
||||||
@ -32,7 +32,7 @@ class PolicyBaseTestCase(test_base.DeckhandTestCase):
|
|||||||
"deckhand:list_cleartext_documents": [['rule:admin_api']]
|
"deckhand:list_cleartext_documents": [['rule:admin_api']]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.policy = self.useFixture(policy_fixture.RealPolicyFixture())
|
self.policy = self.useFixture(fixtures.RealPolicyFixture())
|
||||||
self._set_rules()
|
self._set_rules()
|
||||||
|
|
||||||
def _set_rules(self):
|
def _set_rules(self):
|
||||||
|
35
etc/deckhand/deckhand-paste.ini
Normal file
35
etc/deckhand/deckhand-paste.ini
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
[filter:debug]
|
||||||
|
use = egg:oslo.middleware#debug
|
||||||
|
|
||||||
|
[filter:cors]
|
||||||
|
paste.filter_factory = oslo_middleware.cors:filter_factory
|
||||||
|
oslo_config_project = deckhand
|
||||||
|
|
||||||
|
[filter:request_id]
|
||||||
|
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||||
|
|
||||||
|
[app:api]
|
||||||
|
paste.app_factory = deckhand.service:deckhand_app_factory
|
||||||
|
|
||||||
|
[pipeline:deckhand_api]
|
||||||
|
pipeline = authtoken api
|
@ -1,5 +1,24 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
|
||||||
|
#
|
||||||
|
# From deckhand.conf
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Allow limited access to unauthenticated users.
|
||||||
|
#
|
||||||
|
# Assign a boolean to determine API access for unathenticated
|
||||||
|
# users. When set to False, the API cannot be accessed by
|
||||||
|
# unauthenticated users. When set to True, unauthenticated users can
|
||||||
|
# access the API with read-only privileges. This however only applies
|
||||||
|
# when using ContextMiddleware.
|
||||||
|
#
|
||||||
|
# Possible values:
|
||||||
|
# * True
|
||||||
|
# * False
|
||||||
|
# (boolean value)
|
||||||
|
#allow_anonymous_access = false
|
||||||
|
|
||||||
#
|
#
|
||||||
# From oslo.log
|
# From oslo.log
|
||||||
#
|
#
|
||||||
|
@ -60,7 +60,8 @@
|
|||||||
# GET /api/v1.0/revisions
|
# GET /api/v1.0/revisions
|
||||||
#"deckhand:list_revisions": "rule:admin_api"
|
#"deckhand:list_revisions": "rule:admin_api"
|
||||||
|
|
||||||
# Delete all revisions.
|
# Delete all revisions. Warning: this is equivalent to purging the
|
||||||
|
# database.
|
||||||
# DELETE /api/v1.0/revisions
|
# DELETE /api/v1.0/revisions
|
||||||
#"deckhand:delete_revisions": "rule:admin_api"
|
#"deckhand:delete_revisions": "rule:admin_api"
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
|||||||
PasteDeploy>=1.5.0 # MIT
|
PasteDeploy>=1.5.0 # MIT
|
||||||
Paste # MIT
|
Paste # MIT
|
||||||
Routes>=2.3.1 # MIT
|
Routes>=2.3.1 # MIT
|
||||||
|
keystoneauth1>=3.2.0 # Apache-2.0
|
||||||
|
|
||||||
six>=1.9.0 # MIT
|
six>=1.9.0 # MIT
|
||||||
oslo.concurrency>=3.8.0 # Apache-2.0
|
oslo.concurrency>=3.8.0 # Apache-2.0
|
||||||
|
@ -3,8 +3,8 @@ name = deckhand
|
|||||||
summary = Secrets management persistence tool.
|
summary = Secrets management persistence tool.
|
||||||
description-file = README.rst
|
description-file = README.rst
|
||||||
|
|
||||||
author = deckhand team
|
author = Deckhand team
|
||||||
home-page = http://deckhand-helm.readthedocs.io/en/latest/
|
home-page = http://deckhand.readthedocs.io/en/latest/
|
||||||
classifier =
|
classifier =
|
||||||
Intended Audience :: Information Technology
|
Intended Audience :: Information Technology
|
||||||
Intended Audience :: System Administrators
|
Intended Audience :: System Administrators
|
||||||
|
@ -47,6 +47,9 @@ function gen_config {
|
|||||||
|
|
||||||
cp etc/deckhand/logging.conf.sample $CONF_DIR/logging.conf
|
cp etc/deckhand/logging.conf.sample $CONF_DIR/logging.conf
|
||||||
|
|
||||||
|
# NOTE: allow_anonymous_access allows these functional tests to get around
|
||||||
|
# Keystone authentication, but the context that is provided has zero privileges
|
||||||
|
# so we must also override the policy file for authorization to pass.
|
||||||
cat <<EOCONF > $CONF_DIR/deckhand.conf
|
cat <<EOCONF > $CONF_DIR/deckhand.conf
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
debug = true
|
debug = true
|
||||||
@ -54,6 +57,7 @@ log_config_append = $CONF_DIR/logging.conf
|
|||||||
log_file = deckhand.log
|
log_file = deckhand.log
|
||||||
log_dir = .
|
log_dir = .
|
||||||
use_stderr = true
|
use_stderr = true
|
||||||
|
allow_anonymous_access = true
|
||||||
|
|
||||||
[oslo_policy]
|
[oslo_policy]
|
||||||
policy_file = policy.yaml
|
policy_file = policy.yaml
|
||||||
@ -64,6 +68,15 @@ policy_file = policy.yaml
|
|||||||
connection = $DATABASE_URL
|
connection = $DATABASE_URL
|
||||||
|
|
||||||
[keystone_authtoken]
|
[keystone_authtoken]
|
||||||
|
# Populate keystone_authtoken with values like the following should Keystone
|
||||||
|
# integration be needed here.
|
||||||
|
# project_domain_name = Default
|
||||||
|
# project_name = admin
|
||||||
|
# user_domain_name = Default
|
||||||
|
# password = devstack
|
||||||
|
# username = admin
|
||||||
|
# auth_url = http://127.0.0.1/identity
|
||||||
|
# auth_type = password
|
||||||
EOCONF
|
EOCONF
|
||||||
|
|
||||||
echo $CONF_DIR/deckhand.conf 1>&2
|
echo $CONF_DIR/deckhand.conf 1>&2
|
||||||
@ -73,6 +86,14 @@ EOCONF
|
|||||||
rm -f deckhand.log
|
rm -f deckhand.log
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gen_paste {
|
||||||
|
log_section Creating paste config without [filter:authtoken]
|
||||||
|
# NOTE(fmontei): Since this script does not currently support Keystone
|
||||||
|
# integration, we remove ``filter:authtoken`` from the ``deckhand_api``
|
||||||
|
# pipeline to avoid any kind of auth issues.
|
||||||
|
sed 's/authtoken api/api/' etc/deckhand/deckhand-paste.ini &> $CONF_DIR/deckhand-paste.ini
|
||||||
|
}
|
||||||
|
|
||||||
function gen_policy {
|
function gen_policy {
|
||||||
log_section Creating policy file with liberal permissions
|
log_section Creating policy file with liberal permissions
|
||||||
|
|
||||||
@ -92,6 +113,7 @@ function gen_policy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gen_config
|
gen_config
|
||||||
|
gen_paste
|
||||||
gen_policy
|
gen_policy
|
||||||
|
|
||||||
uwsgi \
|
uwsgi \
|
||||||
|
Loading…
Reference in New Issue
Block a user