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:
jfwood 2014-09-26 00:17:03 -05:00
parent da338282ca
commit 6b384e0830
6 changed files with 302 additions and 23 deletions

View File

@ -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)

View File

@ -123,6 +123,7 @@ class SecretController(object):
return plugin.get_secret(pecan.request.accept.header_value,
secret,
tenant,
self.repos,
twsk,
transport_key)

View File

@ -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),

View File

@ -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."""

View File

@ -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

208
bin/demo_requests.py Executable file
View File

@ -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)