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():
|
||||
return api.start_api()
|
||||
return api.init_application()
|
||||
|
||||
|
||||
# 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):
|
||||
conf.register_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_session_conf_options(conf, group=barbican_group.name)
|
||||
|
||||
|
||||
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_auth_common_conf_options() +
|
||||
ks_loading.get_auth_plugin_conf_options(
|
||||
|
@ -43,3 +43,12 @@ class RequestContext(context.RequestContext):
|
||||
@classmethod
|
||||
def from_dict(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
|
||||
# limitations under the License.
|
||||
|
||||
import logging as py_logging
|
||||
import os
|
||||
|
||||
import falcon
|
||||
from oslo_config import cfg
|
||||
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
|
||||
|
||||
CONF = cfg.CONF
|
||||
logging.register_options(CONF)
|
||||
|
||||
# TODO(fmontei): Include deckhand-paste.ini later.
|
||||
CONFIG_FILES = ['deckhand.conf']
|
||||
logging.register_options(CONF)
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_FILES = ['deckhand.conf', 'deckhand-paste.ini']
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Create routes for the v1.0 API and sets up logging.
|
||||
"""
|
||||
config_files = _get_config_files()
|
||||
CONF([], project='deckhand', default_config_files=config_files)
|
||||
logging.setup(CONF, "deckhand")
|
||||
paste_file = config_files[-1]
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
LOG.info('Initiated Deckhand logging.')
|
||||
CONF([], project='deckhand', default_config_files=config_files)
|
||||
setup_logging(CONF)
|
||||
|
||||
policy.Enforcer(CONF)
|
||||
|
||||
LOG.debug('Starting WSGI application using %s configuration file.',
|
||||
paste_file)
|
||||
|
||||
db_api.drop_db()
|
||||
db_api.setup_db()
|
||||
|
||||
control_api = falcon.API(request_type=base.DeckhandRequest)
|
||||
|
||||
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
|
||||
app = deploy.loadapp('config:%s' % paste_file, name='deckhand_api')
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start_api()
|
||||
init_application()
|
||||
|
@ -12,8 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import yaml
|
||||
|
||||
import falcon
|
||||
|
||||
from deckhand import context
|
||||
@ -35,25 +33,9 @@ class BaseResource(object):
|
||||
resp.headers['Allow'] = ','.join(allowed_methods)
|
||||
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):
|
||||
|
||||
def __init__(self, env, options=None):
|
||||
super(DeckhandRequest, self).__init__(env, options)
|
||||
self.context = context.RequestContext.from_environ(self.env)
|
||||
context_type = context.RequestContext
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
|
@ -66,8 +66,7 @@ class BucketsResource(api_base.BaseResource):
|
||||
bucket_name, list(documents_to_create))
|
||||
|
||||
if created_documents:
|
||||
resp.body = self.to_yaml_body(
|
||||
self.view_builder.list(created_documents))
|
||||
resp.body = self.view_builder.list(created_documents)
|
||||
resp.status = falcon.HTTP_200
|
||||
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.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.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):
|
||||
@ -109,5 +109,4 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.append_header('Content-Type', 'application/x-yaml')
|
||||
resp.body = self.to_yaml_body(
|
||||
self.view_builder.list(rendered_documents))
|
||||
resp.body = 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.status = falcon.HTTP_201
|
||||
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):
|
||||
"""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.status = falcon.HTTP_200
|
||||
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')
|
||||
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.status = falcon.HTTP_200
|
||||
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):
|
||||
"""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)
|
||||
resp.status = falcon.HTTP_200
|
||||
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')
|
||||
@common.sanitize_params(['tag'])
|
||||
@ -64,7 +64,7 @@ class RevisionsResource(api_base.BaseResource):
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
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')
|
||||
def on_delete(self, req, resp):
|
||||
|
@ -48,4 +48,4 @@ class RollbackResource(api_base.BaseResource):
|
||||
revision_resp = self.view_builder.show(rollback_revision)
|
||||
resp.status = falcon.HTTP_201
|
||||
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
|
||||
status: 409
|
||||
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
|
||||
GET: /api/v1.0/revisions/$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']
|
||||
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
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
|
@ -14,9 +14,9 @@
|
||||
|
||||
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 policy_fixture
|
||||
from deckhand.tests.unit import fixtures
|
||||
|
||||
|
||||
class BaseControllerTest(test_base.DeckhandWithDBTestCase,
|
||||
@ -25,5 +25,9 @@ class BaseControllerTest(test_base.DeckhandWithDBTestCase,
|
||||
|
||||
def setUp(self):
|
||||
super(BaseControllerTest, self).setUp()
|
||||
self.app = falcon_testing.TestClient(api.start_api())
|
||||
self.policy = self.useFixture(policy_fixture.RealPolicyFixture())
|
||||
self.app = falcon_testing.TestClient(
|
||||
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
|
||||
|
||||
from deckhand.control import api
|
||||
from deckhand.control import base
|
||||
from deckhand.control import buckets
|
||||
from deckhand.control import revision_diffing
|
||||
from deckhand.control import revision_documents
|
||||
@ -45,19 +44,17 @@ class TestApi(test_base.DeckhandTestCase):
|
||||
if inspect.isclass(obj)]
|
||||
return class_names
|
||||
|
||||
@mock.patch.object(api, 'policy', autospec=True)
|
||||
@mock.patch.object(api, 'db_api', autospec=True)
|
||||
@mock.patch.object(api, 'logging', autospec=True)
|
||||
@mock.patch.object(api, 'CONF', autospec=True)
|
||||
@mock.patch.object(api, 'falcon', autospec=True)
|
||||
def test_start_api(self, mock_falcon, mock_config, mock_logging,
|
||||
mock_db_api):
|
||||
@mock.patch('deckhand.service.falcon', autospec=True)
|
||||
def test_init_application(self, mock_falcon, mock_config, mock_logging,
|
||||
mock_db_api, _):
|
||||
mock_falcon_api = mock_falcon.API.return_value
|
||||
|
||||
result = api.start_api()
|
||||
self.assertEqual(mock_falcon_api, result)
|
||||
api.init_application()
|
||||
|
||||
mock_falcon.API.assert_called_once_with(
|
||||
request_type=base.DeckhandRequest)
|
||||
mock_falcon_api.add_route.assert_has_calls([
|
||||
mock.call('/api/v1.0/bucket/{bucket_name}/documents',
|
||||
self.buckets_resource()),
|
||||
|
@ -12,6 +12,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Fixtures for Deckhand tests."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import yaml
|
||||
|
||||
@ -24,10 +27,37 @@ from deckhand import policies
|
||||
import deckhand.policy
|
||||
from deckhand.tests.unit import fake_policy
|
||||
|
||||
|
||||
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):
|
||||
"""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
|
||||
import deckhand.policy
|
||||
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):
|
||||
@ -32,7 +32,7 @@ class PolicyBaseTestCase(test_base.DeckhandTestCase):
|
||||
"deckhand:list_cleartext_documents": [['rule:admin_api']]
|
||||
}
|
||||
|
||||
self.policy = self.useFixture(policy_fixture.RealPolicyFixture())
|
||||
self.policy = self.useFixture(fixtures.RealPolicyFixture())
|
||||
self._set_rules()
|
||||
|
||||
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]
|
||||
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
@ -60,7 +60,8 @@
|
||||
# GET /api/v1.0/revisions
|
||||
#"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
|
||||
#"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
|
||||
Paste # MIT
|
||||
Routes>=2.3.1 # MIT
|
||||
keystoneauth1>=3.2.0 # Apache-2.0
|
||||
|
||||
six>=1.9.0 # MIT
|
||||
oslo.concurrency>=3.8.0 # Apache-2.0
|
||||
|
@ -3,8 +3,8 @@ name = deckhand
|
||||
summary = Secrets management persistence tool.
|
||||
description-file = README.rst
|
||||
|
||||
author = deckhand team
|
||||
home-page = http://deckhand-helm.readthedocs.io/en/latest/
|
||||
author = Deckhand team
|
||||
home-page = http://deckhand.readthedocs.io/en/latest/
|
||||
classifier =
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
|
@ -47,6 +47,9 @@ function gen_config {
|
||||
|
||||
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
|
||||
[DEFAULT]
|
||||
debug = true
|
||||
@ -54,6 +57,7 @@ log_config_append = $CONF_DIR/logging.conf
|
||||
log_file = deckhand.log
|
||||
log_dir = .
|
||||
use_stderr = true
|
||||
allow_anonymous_access = true
|
||||
|
||||
[oslo_policy]
|
||||
policy_file = policy.yaml
|
||||
@ -64,6 +68,15 @@ policy_file = policy.yaml
|
||||
connection = $DATABASE_URL
|
||||
|
||||
[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
|
||||
|
||||
echo $CONF_DIR/deckhand.conf 1>&2
|
||||
@ -73,6 +86,14 @@ EOCONF
|
||||
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 {
|
||||
log_section Creating policy file with liberal permissions
|
||||
|
||||
@ -92,6 +113,7 @@ function gen_policy {
|
||||
}
|
||||
|
||||
gen_config
|
||||
gen_paste
|
||||
gen_policy
|
||||
|
||||
uwsgi \
|
||||
|
Loading…
Reference in New Issue
Block a user