Introduce User-Meta table, model, and repo

This patch introduces the user-meta database table, model, and
repository. It is the first of several patches which will complete
the "User Defined Metadata for Barbican Secrets" Blueprint.

Other Patches will include:
1. ) API and Tests(Unit and Functional)
2. ) Documentation
3. ) Client Upgrades

Implements: blueprint add-user-metadata
Change-Id: I4b6ae9e7090eb66fe8c89e62116d9a8483642a29
This commit is contained in:
Fernando Diaz 2016-01-21 12:19:45 -06:00
parent 83e7caa02c
commit 52b0479fcc
5 changed files with 195 additions and 2 deletions

View File

@ -0,0 +1,52 @@
# Copyright (c) 2015 IBM
# All 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.
"""add-secret-user-metadata
Revision ID: dce488646127
Revises: 39a96e67e990
Create Date: 2016-02-09 04:52:03.975486
"""
# revision identifiers, used by Alembic.
revision = 'dce488646127'
down_revision = '39a96e67e990'
from alembic import op
import sqlalchemy as sa
def upgrade():
ctx = op.get_context()
con = op.get_bind()
table_exists = ctx.dialect.has_table(con.engine, 'secret_user_metadata')
if not table_exists:
op.create_table(
'secret_user_metadata',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('key', sa.String(length=255), nullable=False),
sa.Column('value', sa.String(length=255), nullable=False),
sa.Column('secret_id', sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['secret_id'], ['secrets.id'],),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('secret_id', 'key',
name='_secret_key_uc')
)

View File

@ -297,6 +297,12 @@ class Secret(BASE, SoftDeleteMixIn, ModelBase):
backref="secret",
cascade="all, delete-orphan")
secret_user_metadata = orm.relationship(
"SecretUserMetadatum",
collection_class=col.attribute_mapped_collection('key'),
backref="secret",
cascade="all, delete-orphan")
def __init__(self, parsed_request=None):
"""Creates secret from a dict."""
super(Secret, self).__init__()
@ -322,6 +328,9 @@ class Secret(BASE, SoftDeleteMixIn, ModelBase):
for k, v in self.secret_store_metadata.items():
v.delete(session)
for k, v in self.secret_user_metadata.items():
v.delete(session)
for datum in self.encrypted_data:
datum.delete(session)
@ -346,7 +355,7 @@ class Secret(BASE, SoftDeleteMixIn, ModelBase):
'algorithm': self.algorithm,
'bit_length': self.bit_length,
'mode': self.mode,
'creator_id': self.creator_id
'creator_id': self.creator_id,
}
@ -382,6 +391,41 @@ class SecretStoreMetadatum(BASE, SoftDeleteMixIn, ModelBase):
}
class SecretUserMetadatum(BASE, SoftDeleteMixIn, ModelBase):
"""Represents Secret user metadatum for a single key-value pair."""
__tablename__ = "secret_user_metadata"
key = sa.Column(sa.String(255), nullable=False)
value = sa.Column(sa.String(255), nullable=False)
secret_id = sa.Column(
sa.String(36), sa.ForeignKey('secrets.id'), index=True, nullable=False)
__table_args__ = (sa.UniqueConstraint('secret_id', 'key',
name='_secret_key_uc'),)
def __init__(self, key, value):
super(SecretUserMetadatum, self).__init__()
msg = u._("Must supply non-None {0} argument "
"for SecretUserMetadatum entry.")
if key is None:
raise exception.MissingArgumentError(msg.format("key"))
self.key = key
if value is None:
raise exception.MissingArgumentError(msg.format("value"))
self.value = value
def _do_extra_dict_fields(self):
"""Sub-class hook method: return dict of fields."""
return {
'key': self.key,
'value': self.value
}
class EncryptedDatum(BASE, SoftDeleteMixIn, ModelBase):
"""Represents the encrypted data for a Secret."""

View File

@ -64,6 +64,7 @@ _PROJECT_CA_REPOSITORY = None
_PROJECT_QUOTAS_REPOSITORY = None
_SECRET_ACL_REPOSITORY = None
_SECRET_META_REPOSITORY = None
_SECRET_USER_META_REPOSITORY = None
_SECRET_REPOSITORY = None
_TRANSPORT_KEY_REPOSITORY = None
@ -782,6 +783,58 @@ class SecretStoreMetadatumRepo(BaseRepo):
pass
class SecretUserMetadatumRepo(BaseRepo):
"""Repository for the SecretUserMetadatum entity
Stores key/value information on behalf of a Secret.
"""
def save(self, metadata, secret_model):
"""Saves the the specified metadata for the secret.
:raises NotFound if entity does not exist.
"""
now = timeutils.utcnow()
for k, v in metadata.items():
meta_model = models.SecretUserMetadatum(k, v)
meta_model.updated_at = now
meta_model.secret = secret_model
meta_model.save()
def get_metadata_for_secret(self, secret_id):
"""Returns a dict of SecretUserMetadatum instances."""
session = get_session()
try:
query = session.query(models.SecretUserMetadatum)
query = query.filter_by(deleted=False)
query = query.filter(
models.SecretUserMetadatum.secret_id == secret_id)
metadata = query.all()
except sa_orm.exc.NoResultFound:
metadata = {}
return {m.key: m.value for m in metadata}
def _do_entity_name(self):
"""Sub-class hook: return entity name, such as for debugging."""
return "SecretUserMetadatum"
def _do_build_get_query(self, entity_id, external_project_id, session):
"""Sub-class hook: build a retrieve query."""
query = session.query(models.SecretUserMetadatum)
return query.filter_by(id=entity_id)
def _do_validate(self, values):
"""Sub-class hook: validate values."""
pass
class KEKDatumRepo(BaseRepo):
"""Repository for the KEKDatum entity
@ -2149,6 +2202,13 @@ def get_secret_meta_repository():
return _get_repository(_SECRET_META_REPOSITORY, SecretStoreMetadatumRepo)
def get_secret_user_meta_repository():
"""Returns a singleton Secret user meta repository instance."""
global _SECRET_USER_META_REPOSITORY
return _get_repository(_SECRET_USER_META_REPOSITORY,
SecretUserMetadatumRepo)
def get_secret_repository():
"""Returns a singleton Secret repository instance."""
global _SECRET_REPOSITORY

View File

@ -64,6 +64,42 @@ class WhenCreatingNewSecret(utils.BaseTestCase):
self.assertEqual(self.parsed_secret['secret_type'], secret.secret_type)
class WhenCreatingNewSecretMetadata(utils.BaseTestCase):
def setUp(self):
super(WhenCreatingNewSecretMetadata, self).setUp()
self.key = 'dog'
self.value = 'poodle'
self.metadata = {
'key': self.key,
'value': self.value
}
def test_new_secret_metadata_is_created_from_dict(self):
secret_meta = models.SecretUserMetadatum(self.key, self.value)
self.assertEqual(self.key, secret_meta.key)
self.assertEqual(self.value, secret_meta.value)
fields = secret_meta.to_dict_fields()
self.assertEqual(self.metadata['key'],
fields['key'])
self.assertEqual(self.metadata['value'],
fields['value'])
def test_should_raise_exception_metadata_with_no_key(self):
self.assertRaises(exception.MissingArgumentError,
models.SecretUserMetadatum,
None,
self.value)
def test_should_raise_exception_metadata_with_no_value(self):
self.assertRaises(exception.MissingArgumentError,
models.SecretUserMetadatum,
self.key,
None)
class WhenCreatingNewOrder(utils.BaseTestCase):
def setUp(self):
super(WhenCreatingNewOrder, self).setUp()

View File

@ -23,7 +23,7 @@ class SecretModel(BaseModel):
secret_ref=None, bit_length=None, mode=None, secret_type=None,
payload_content_type=None, payload=None, content_types=None,
payload_content_encoding=None, status=None, updated=None,
created=None, creator_id=None):
created=None, creator_id=None, metadata=None):
super(SecretModel, self).__init__()
self.name = name
@ -41,3 +41,4 @@ class SecretModel(BaseModel):
self.updated = updated
self.created = created
self.creator_id = creator_id
self.metadata = metadata