Fix error in two-step secret PUT with base64
An attempt to PUT base64-encoded binary data as the 2nd step in storing a secret resulted in a SQLAlchemy 'already attached in another session' error with the secret model. This CR moves to the SQLAlchemy scoped session which ensures one session per thread. This CR also adds a simple 'smoke test' script that was used to exercise the API during the development of this CR, and could be helpful for others looking for a simple way to test and evaluate their local Barbican API instances. Closes-Bug: #1374270 Change-Id: Ie50e5f03be7b9b12d82a285a71e2f1b5c2483ea0
This commit is contained in:
parent
da338282ca
commit
6b384e0830
|
@ -124,14 +124,14 @@ class ContainerConsumersController(object):
|
|||
LOG.debug('Start on_post...%s', data)
|
||||
|
||||
try:
|
||||
self.container_repo.get(self.container_id, keystone_id)
|
||||
container = self.container_repo.get(self.container_id, keystone_id)
|
||||
except exception.NotFound:
|
||||
controllers.containers.container_not_found()
|
||||
|
||||
new_consumer = models.ContainerConsumerMetadatum(self.container_id,
|
||||
data)
|
||||
new_consumer.tenant_id = tenant.id
|
||||
self.consumer_repo.create_from(new_consumer)
|
||||
self.consumer_repo.create_from(new_consumer, container)
|
||||
|
||||
pecan.response.headers['Location'] = (
|
||||
'/containers/{0}/consumers'.format(new_consumer.container_id)
|
||||
|
|
|
@ -123,6 +123,7 @@ class SecretController(object):
|
|||
return plugin.get_secret(pecan.request.accept.header_value,
|
||||
secret,
|
||||
tenant,
|
||||
self.repos,
|
||||
twsk,
|
||||
transport_key)
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ TODO: The top part of this file was 'borrowed' from Glance, but seems
|
|||
quite intense for sqlalchemy, and maybe could be simplified.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
|
@ -174,9 +173,12 @@ def get_maker(autocommit=True, expire_on_commit=False):
|
|||
global _MAKER, _ENGINE
|
||||
assert _ENGINE
|
||||
if not _MAKER:
|
||||
_MAKER = sa_orm.sessionmaker(bind=_ENGINE,
|
||||
autocommit=autocommit,
|
||||
expire_on_commit=expire_on_commit)
|
||||
# Utilize SQLAlchemy's scoped_session to ensure that we only have one
|
||||
# session instance per thread.
|
||||
_MAKER = sqlalchemy.orm.scoped_session(
|
||||
sa_orm.sessionmaker(bind=_ENGINE,
|
||||
autocommit=autocommit,
|
||||
expire_on_commit=expire_on_commit))
|
||||
return _MAKER
|
||||
|
||||
|
||||
|
@ -307,7 +309,6 @@ class BaseRepo(object):
|
|||
suppress_exception=False, session=None):
|
||||
"""Get an entity or raise if it does not exist."""
|
||||
session = self.get_session(session)
|
||||
|
||||
try:
|
||||
query = self._do_build_get_query(entity_id,
|
||||
keystone_id, session)
|
||||
|
@ -322,8 +323,9 @@ class BaseRepo(object):
|
|||
LOG.exception("Not found for %s", entity_id)
|
||||
entity = None
|
||||
if not suppress_exception:
|
||||
raise exception.NotFound("No %s found with ID %s"
|
||||
% (self._do_entity_name(), entity_id))
|
||||
raise exception.NotFound(
|
||||
"No {0} found with ID {1}".format(
|
||||
self._do_entity_name(), entity_id))
|
||||
|
||||
return entity
|
||||
|
||||
|
@ -560,7 +562,8 @@ class SecretRepo(BaseRepo):
|
|||
query = query.order_by(models.Secret.created_at)
|
||||
query = query.filter_by(deleted=False)
|
||||
|
||||
# Note: Must use '== None' below, not 'is None'.
|
||||
# Note(john-wood-w): SQLAlchemy requires '== None' below,
|
||||
# not 'is None'.
|
||||
query = query.filter(or_(models.Secret.expiration == None,
|
||||
models.Secret.expiration > utcnow))
|
||||
|
||||
|
@ -607,7 +610,8 @@ class SecretRepo(BaseRepo):
|
|||
"""Sub-class hook: build a retrieve query."""
|
||||
utcnow = timeutils.utcnow()
|
||||
|
||||
# Note: Must use '== None' below, not 'is None'.
|
||||
# Note(john-wood-w): SQLAlchemy requires '== None' below,
|
||||
# not 'is None'.
|
||||
# TODO(jfwood): Performance? Is the many-to-many join needed?
|
||||
expiration_filter = or_(models.Secret.expiration == None,
|
||||
models.Secret.expiration > utcnow)
|
||||
|
@ -668,6 +672,25 @@ class SecretStoreMetadatumRepo(BaseRepo):
|
|||
meta_model.secret = secret_model
|
||||
meta_model.save(session=session)
|
||||
|
||||
def get_metadata_for_secret(self, secret_id):
|
||||
"""Returns a dict of SecretStoreMetadatum instances."""
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
try:
|
||||
query = session.query(models.SecretStoreMetadatum)
|
||||
query = query.filter_by(deleted=False)
|
||||
|
||||
query = query.filter(
|
||||
models.SecretStoreMetadatum.secret_id == secret_id)
|
||||
|
||||
metadata = query.all()
|
||||
|
||||
except sa_orm.exc.NoResultFound:
|
||||
metadata = dict()
|
||||
|
||||
return dict((m.key, m.value) for m in metadata)
|
||||
|
||||
def _do_entity_name(self):
|
||||
"""Sub-class hook: return entity name, such as for debugging."""
|
||||
return "SecretStoreMetadatum"
|
||||
|
@ -864,7 +887,6 @@ class OrderPluginMetadatumRepo(BaseRepo):
|
|||
query = session.query(models.OrderPluginMetadatum)
|
||||
query = query.filter_by(deleted=False)
|
||||
|
||||
# Note: Must use '== None' below, not 'is None'.
|
||||
query = query.filter(
|
||||
models.OrderPluginMetadatum.order_id == order_id)
|
||||
|
||||
|
@ -1039,10 +1061,14 @@ class ContainerConsumerRepo(BaseRepo):
|
|||
% (self._do_entity_name()))
|
||||
return consumer
|
||||
|
||||
def create_from(self, new_consumer):
|
||||
def create_from(self, new_consumer, container):
|
||||
session = get_session()
|
||||
try:
|
||||
super(ContainerConsumerRepo, self).create_from(new_consumer)
|
||||
except exception.Duplicate:
|
||||
with session.begin():
|
||||
container.updated_at = timeutils.utcnow()
|
||||
container.consumers.append(new_consumer)
|
||||
container.save(session=session)
|
||||
except sqlalchemy.exc.IntegrityError:
|
||||
# This operation is idempotent, so log this and move on
|
||||
LOG.debug("Consumer %s already exists for container %s,"
|
||||
" continuing...", (new_consumer.name, new_consumer.URL),
|
||||
|
|
|
@ -129,14 +129,13 @@ def store_secret(unencrypted_raw, content_type_raw, content_encoding,
|
|||
return secret_model, None
|
||||
|
||||
|
||||
def get_secret(requesting_content_type, secret_model, tenant_model,
|
||||
def get_secret(requesting_content_type, secret_model, tenant_model, repos,
|
||||
twsk=None, transport_key=None):
|
||||
tr.analyze_before_decryption(requesting_content_type)
|
||||
|
||||
# Construct metadata dict from data model.
|
||||
# Note: Must use the dict/tuple format for py2.6 usage.
|
||||
secret_metadata = dict((k, v.value) for (k, v) in
|
||||
secret_model.secret_store_metadata.items())
|
||||
secret_metadata = _get_secret_meta(secret_model, repos)
|
||||
|
||||
if twsk is not None:
|
||||
secret_metadata['trans_wrapped_session_key'] = twsk
|
||||
|
@ -160,11 +159,10 @@ def get_secret(requesting_content_type, secret_model, tenant_model,
|
|||
requesting_content_type)
|
||||
|
||||
|
||||
def get_transport_key_id_for_retrieval(secret_model):
|
||||
def get_transport_key_id_for_retrieval(secret_model, repos):
|
||||
"""Return a transport key ID for retrieval if the plugin supports it."""
|
||||
|
||||
secret_metadata = dict((k, v.value) for (k, v) in
|
||||
secret_model.secret_store_metadata.items())
|
||||
secret_metadata = _get_secret_meta(secret_model, repos)
|
||||
|
||||
plugin_manager = secret_store.SecretStorePluginManager()
|
||||
retrieve_plugin = plugin_manager.get_plugin_retrieve_delete(
|
||||
|
@ -262,8 +260,7 @@ def delete_secret(secret_model, project_id, repos):
|
|||
|
||||
# Construct metadata dict from data model.
|
||||
# Note: Must use the dict/tuple format for py2.6 usage.
|
||||
secret_metadata = dict((k, v.value) for (k, v) in
|
||||
secret_model.secret_store_metadata.items())
|
||||
secret_metadata = _get_secret_meta(secret_model, repos)
|
||||
|
||||
# Locate a suitable plugin to delete the secret from.
|
||||
plugin_manager = secret_store.SecretStorePluginManager()
|
||||
|
@ -335,6 +332,14 @@ def _get_secret(
|
|||
return secret_dto
|
||||
|
||||
|
||||
def _get_secret_meta(secret_model, repos):
|
||||
if secret_model:
|
||||
return repos.secret_meta_repo.get_metadata_for_secret(
|
||||
secret_model.id)
|
||||
else:
|
||||
return dict()
|
||||
|
||||
|
||||
def _save_secret_metadata(secret_model, secret_metadata,
|
||||
store_plugin, content_type, repos):
|
||||
"""Add secret metadata to a secret."""
|
||||
|
|
|
@ -875,6 +875,27 @@ class WhenGettingSecretsListUsingSecretsResource(FunctionalTest):
|
|||
self.assertFalse('previous' in resp.namespace)
|
||||
self.assertFalse('next' in resp.namespace)
|
||||
|
||||
def test_should_list_secrets_normalize_bits_to_zero(self):
|
||||
# Set bits to a bogus value, that the secrets controller should clean.
|
||||
self.params['bits'] = 'bogus-bits'
|
||||
|
||||
self.app.get(
|
||||
'/secrets/',
|
||||
dict((k, v) for k, v in self.params.items() if v is not None)
|
||||
)
|
||||
|
||||
# Verify that controller call above normalizes bits to zero!
|
||||
self.secret_repo.get_by_create_date.assert_called_once_with(
|
||||
self.keystone_id,
|
||||
offset_arg=u'{0}'.format(self.offset),
|
||||
limit_arg=u'{0}'.format(self.limit),
|
||||
suppress_exception=True,
|
||||
name='',
|
||||
alg=None,
|
||||
mode=None,
|
||||
bits=0
|
||||
)
|
||||
|
||||
def _create_url(self, keystone_id, offset_arg=None, limit_arg=None):
|
||||
if limit_arg:
|
||||
offset = int(offset_arg)
|
||||
|
@ -959,6 +980,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
|||
self.kek_repo = mock.MagicMock()
|
||||
|
||||
self.secret_meta_repo = mock.MagicMock()
|
||||
self.secret_meta_repo.get_metadata_for_secret.return_value = None
|
||||
|
||||
self.transport_key_model = models.TransportKey(
|
||||
"default_plugin", "my transport key")
|
||||
|
@ -1006,6 +1028,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
|||
'text/plain',
|
||||
self.secret,
|
||||
self.tenant,
|
||||
mock.ANY,
|
||||
None,
|
||||
None
|
||||
)
|
||||
|
@ -1033,6 +1056,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
|||
'text/plain',
|
||||
self.secret,
|
||||
self.tenant,
|
||||
mock.ANY,
|
||||
twsk,
|
||||
self.transport_key_model.transport_key
|
||||
)
|
||||
|
@ -1133,6 +1157,7 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
|||
'application/octet-stream',
|
||||
self.secret,
|
||||
self.tenant,
|
||||
mock.ANY,
|
||||
None,
|
||||
None
|
||||
)
|
||||
|
@ -1308,6 +1333,20 @@ class WhenGettingPuttingOrDeletingSecretUsingSecretResource(FunctionalTest):
|
|||
|
||||
self.assertEqual(resp.status_int, 415)
|
||||
|
||||
def test_should_raise_put_secret_no_content_type(self):
|
||||
self.secret.encrypted_data = []
|
||||
resp = self.app.put(
|
||||
'/secrets/{0}/'.format(self.secret.id),
|
||||
'plain text',
|
||||
headers={
|
||||
'Accept': 'text/plain',
|
||||
'Content-Type': ''
|
||||
},
|
||||
expect_errors=True
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 415)
|
||||
|
||||
def test_should_raise_put_secret_not_found(self):
|
||||
# Force error, due to secret not found.
|
||||
self.secret_repo.get.return_value = None
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Demonstrates the various Barbican API calls, against an unauthenticated local
|
||||
Barbican server. This script is intended to be a lightweight way to demonstrate
|
||||
and 'smoke test' the Barbican API via it's REST API, with no other dependencies
|
||||
required including the Barbican Python client. Not that this script is not
|
||||
intended to replace DevStack or Tempest style testing.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import sys
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
LOG.setLevel(logging.DEBUG)
|
||||
LOG.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
|
||||
# Project ID:
|
||||
proj = '12345'
|
||||
|
||||
# Endpoint:
|
||||
end_point = 'http://localhost:9311'
|
||||
version = 'v1'
|
||||
|
||||
# Basic header info:
|
||||
hdrs = {'X-Project-Id': proj, 'content-type': 'application/json'}
|
||||
|
||||
# Consumer data.
|
||||
payload_consumer = {
|
||||
'name': 'foo-service',
|
||||
'URL': 'https://www.fooservice.com/widgets/1234'
|
||||
}
|
||||
|
||||
|
||||
def demo_version():
|
||||
"""Get version"""
|
||||
v = requests.get(end_point, headers=hdrs)
|
||||
LOG.info('Version: {0}\n'.format(v.text))
|
||||
|
||||
|
||||
def demo_store_secret_one_step_text(suppress=False):
|
||||
"""Store secret (1-step):"""
|
||||
ep_1step = '/'.join([end_point, version, 'secrets'])
|
||||
|
||||
# POST metadata:
|
||||
payload = {
|
||||
'payload': 'my-secret-here',
|
||||
'payload_content_type': 'text/plain'
|
||||
}
|
||||
pr = requests.post(ep_1step, data=json.dumps(payload), headers=hdrs)
|
||||
pr_j = pr.json()
|
||||
secret_ref = pr.json().get('secret_ref')
|
||||
|
||||
# GET secret:
|
||||
hdrs_get = dict(hdrs)
|
||||
hdrs_get.update({
|
||||
'accept': 'text/plain'})
|
||||
gr = requests.get(secret_ref, headers=hdrs_get)
|
||||
if not suppress:
|
||||
LOG.info('Get secret 1-step (text): {0}\n'.format(gr.content))
|
||||
|
||||
return secret_ref
|
||||
|
||||
|
||||
def demo_store_secret_two_step_binary():
|
||||
"""Store secret (2-step):"""
|
||||
secret = 'bXktc2VjcmV0LWhlcmU=' # base64 of 'my secret'
|
||||
ep_2step = '/'.join([end_point, version, 'secrets'])
|
||||
|
||||
# POST metadata:
|
||||
payload = {}
|
||||
pr = requests.post(ep_2step, data=json.dumps(payload), headers=hdrs)
|
||||
pr_j = pr.json()
|
||||
secret_ref = pr.json().get('secret_ref')
|
||||
assert(secret_ref)
|
||||
|
||||
# PUT data to store:
|
||||
hdrs_put = dict(hdrs)
|
||||
hdrs_put.update({
|
||||
'content-type': 'application/octet-stream',
|
||||
'content-encoding': 'base64'}
|
||||
)
|
||||
requests.put(secret_ref, data=secret, headers=hdrs_put)
|
||||
|
||||
# GET secret:
|
||||
hdrs_get = dict(hdrs)
|
||||
hdrs_get.update({
|
||||
'accept': 'application/octet-stream'})
|
||||
gr = requests.get(secret_ref, headers=hdrs_get)
|
||||
LOG.info('Get secret 2-step (binary): {0}\n'.format(gr.content))
|
||||
|
||||
return secret_ref
|
||||
|
||||
|
||||
def demo_store_container_rsa():
|
||||
"""Store secret (2-step):"""
|
||||
ep_cont = '/'.join([end_point, version, 'containers'])
|
||||
secret_prk = demo_store_secret_one_step_text(suppress=True)
|
||||
secret_puk = demo_store_secret_one_step_text(suppress=True)
|
||||
secret_pp = demo_store_secret_one_step_text(suppress=True)
|
||||
|
||||
# POST metadata:
|
||||
payload = {
|
||||
"name": "container name",
|
||||
"type": "rsa",
|
||||
"secret_refs": [{
|
||||
"name": "private_key",
|
||||
"secret_ref": secret_prk
|
||||
},
|
||||
{
|
||||
"name": "public_key",
|
||||
"secret_ref": secret_puk
|
||||
},
|
||||
{
|
||||
"name": "private_key_passphrase",
|
||||
"secret_ref": secret_pp
|
||||
}]
|
||||
}
|
||||
pr = requests.post(ep_cont, data=json.dumps(payload), headers=hdrs)
|
||||
pr_j = pr.json()
|
||||
container_ref = pr.json().get('container_ref')
|
||||
|
||||
# GET container:
|
||||
hdrs_get = dict(hdrs)
|
||||
gr = requests.get(container_ref, headers=hdrs_get)
|
||||
LOG.info('Get RSA container: {0}\n'.format(gr.content))
|
||||
|
||||
return container_ref
|
||||
|
||||
|
||||
def demo_delete_secret(secret_ref):
|
||||
"""Delete secret by its HATEOS reference"""
|
||||
ep_delete = secret_ref
|
||||
|
||||
# DELETE secret:
|
||||
dr = requests.delete(ep_delete, headers=hdrs)
|
||||
gr = requests.get(secret_ref, headers=hdrs)
|
||||
assert(404 == gr.status_code)
|
||||
LOG.info('...Deleted Secret: {0}\n'.format(secret_ref))
|
||||
|
||||
|
||||
def demo_delete_container(container_ref):
|
||||
"""Delete container by its HATEOS reference"""
|
||||
ep_delete = container_ref
|
||||
|
||||
# DELETE container:
|
||||
dr = requests.delete(ep_delete, headers=hdrs)
|
||||
gr = requests.get(container_ref, headers=hdrs)
|
||||
assert(404 == gr.status_code)
|
||||
LOG.info('...Deleted Container: {0}\n'.format(container_ref))
|
||||
|
||||
|
||||
def demo_consumers_add(container_ref):
|
||||
"""Add consumer to a container:"""
|
||||
ep_add = '/'.join([container_ref, 'consumers'])
|
||||
|
||||
# POST metadata:
|
||||
pr = requests.post(ep_add, data=json.dumps(payload_consumer), headers=hdrs)
|
||||
pr_consumers = pr.json().get('consumers')
|
||||
assert(pr_consumers)
|
||||
assert(len(pr_consumers) == 1)
|
||||
LOG.info('...Consumer response: {0}'.format(pr_consumers))
|
||||
|
||||
|
||||
def demo_consumers_delete(container_ref):
|
||||
"""Delete consumer from a container:"""
|
||||
ep_delete = '/'.join([container_ref, 'consumers'])
|
||||
|
||||
# POST metadata:
|
||||
pr = requests.delete(
|
||||
ep_delete, data=json.dumps(payload_consumer), headers=hdrs)
|
||||
pr_consumers = pr.json().get('consumers')
|
||||
assert(not pr_consumers)
|
||||
LOG.info('...Deleted Consumer from: {0}'.format(container_ref))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
demo_version()
|
||||
secret_ref = demo_store_secret_one_step_text()
|
||||
demo_delete_secret(secret_ref)
|
||||
|
||||
secret_ref = demo_store_secret_two_step_binary()
|
||||
demo_delete_secret(secret_ref)
|
||||
|
||||
container_ref = demo_store_container_rsa()
|
||||
demo_consumers_add(container_ref)
|
||||
demo_consumers_add(container_ref) # Should be idempotent
|
||||
demo_consumers_delete(container_ref)
|
||||
demo_consumers_add(container_ref)
|
||||
demo_delete_container(container_ref)
|
Loading…
Reference in New Issue