barbican/barbican/tests/cmd/test_db_cleanup.py

437 lines
18 KiB
Python

# Copyright (c) 2016 IBM
#
# 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 datetime
from unittest import mock
from oslo_db import exception as db_exc
from sqlalchemy.exc import IntegrityError
from barbican.model import clean
from barbican.model import models
from barbican.model import repositories as repos
from barbican.tests import database_utils as utils
def _create_project(project_name):
"""Wrapper to create a project and clean"""
def project_decorator(test_func):
def project_wrapper(self, *args, **kwargs):
project = utils.create_project(external_id=project_name)
kwargs['project'] = project
test_result = test_func(self, *args, **kwargs)
project.delete()
return test_result
return project_wrapper
return project_decorator
def _entry_exists(entry):
"""Check to see if entry should exist in the database"""
model = entry.__class__
entry_id = entry.id
session = repos.get_session()
query = session.query(model).filter(model.id == entry_id)
count = query.count()
return count >= 1
def _entry_is_soft_deleted(entry):
model = entry.__class__
entry_id = entry.id
session = repos.get_session()
query = session.query(model)
result = query.filter(model.id == entry_id).first().deleted
return result
def _setup_entry(name, *args, **kwargs):
func_name = "create_" + name
if not hasattr(utils, func_name):
raise Exception("Cannot create an entry called %s", name)
func = getattr(utils, func_name)
kwargs['session'] = repos.get_session()
entry = func(*args, **kwargs)
return entry
class WhenTestingDBCleanUpCommand(utils.RepositoryTestCase):
def tearDown(self):
super(WhenTestingDBCleanUpCommand, self).tearDown()
repos.rollback()
@_create_project("my keystone id")
def test_soft_deleted_secret_orders(self, project):
"""Test that secrets without child order get deleted"""
# Create a secret tied to an order and one secret that is not
secret1 = _setup_entry('secret', project=project)
secret2 = _setup_entry('secret', project=project)
order = _setup_entry('order', project=project, secret=secret1)
# Delete secrets
secret1.delete()
secret2.delete()
clean.cleanup_parent_with_no_child(models.Secret, models.Order)
# Assert that only secret2 is removed
self.assertTrue(_entry_exists(secret1))
self.assertFalse(_entry_exists(secret2))
# delete order and secret
order.delete()
clean.cleanup_all()
self.assertFalse(_entry_exists(order))
self.assertFalse(_entry_exists(secret2))
def test_cleanup_soft_deletes_transport_keys(self):
"""Test Cleaning up soft deleted transport keys"""
# create transport key
transport_key = _setup_entry('transport_key')
# delete transport key
transport_key.delete()
clean.cleanup_all()
self.assertFalse(_entry_exists(transport_key))
@_create_project("my keystone id")
def test_cleanup_soft_deletes_secrets(self, project):
"""Test cleaning up secrets and secret_meta"""
# create secret and secret_meta
secret = _setup_entry('secret', project=project)
secret_metadatum = _setup_entry('secret_metadatum', secret=secret)
secret_user_metadatum = _setup_entry('secret_user_metadatum',
secret=secret)
kek_datum = _setup_entry('kek_datum', project=project)
enc_datum = _setup_entry('encrypted_datum', secret=secret,
kek_datum=kek_datum)
# delete secret, it should automatically delete
# secret_metadatum, enc_datum, and secret_user_metadatum
# kek_datum should still exist
secret.delete()
clean.cleanup_all()
self.assertFalse(_entry_exists(secret))
self.assertFalse(_entry_exists(secret_metadatum))
self.assertFalse(_entry_exists(secret_user_metadatum))
self.assertFalse(_entry_exists(enc_datum))
self.assertTrue(_entry_exists(kek_datum))
@_create_project("my keystone id")
def test_cleanup_soft_deletes_containers(self, project):
"""Test cleaning up containers and secrets"""
# create container, secret, and container_secret
container = _setup_entry('container', project=project)
secret = _setup_entry('secret', project=project)
container_secret = _setup_entry('container_secret',
container=container, secret=secret)
# delete container secret and container
container.delete()
clean.cleanup_all()
# check that container secret and container are deleted
# but secret still exists
self.assertFalse(_entry_exists(container_secret))
self.assertFalse(_entry_exists(container))
self.assertTrue(_entry_exists(secret))
# cleanup secrets
secret.delete()
clean.cleanup_all()
self.assertFalse(_entry_exists(secret))
@_create_project("my keystone id")
def test_cleanup_container_with_order_child(self, project):
container = _setup_entry('container', project=project)
secret = _setup_entry('secret', project=project)
secret_container = _setup_entry('container_secret',
container=container, secret=secret)
order = _setup_entry('order', project=project, secret=secret,
container=container)
container.delete()
clean.cleanup_all()
# only the secret_container should be removed from the database
# since it is a child of the container
self.assertFalse(_entry_exists(secret_container))
self.assertTrue(_entry_exists(secret))
self.assertTrue(_entry_exists(order))
# container should still exist since child order still exists
self.assertTrue(_entry_exists(container))
order.delete()
clean.cleanup_all()
# assert that only the secret exists
self.assertFalse(_entry_exists(order))
self.assertFalse(_entry_exists(container))
self.assertTrue(_entry_exists(secret))
secret.delete()
clean.cleanup_all()
# the secret should now be able to be removed
self.assertFalse(_entry_exists(secret))
@_create_project("my clean order keystone id")
def test_cleanup_orders(self, project):
"""Test cleaning up an order and it's children"""
# create order, order meta, and plugin meta, and retry task
order = _setup_entry('order', project=project)
order_barbican_meta_data = _setup_entry('order_meta_datum',
order=order)
order_plugin_metadata = _setup_entry('order_plugin_metadatum',
order=order)
order_retry_task = _setup_entry('order_retry', order=order)
# soft delete order and retry task,
# it should automatically delete the children
order.delete()
order_retry_task.delete()
clean.cleanup_all()
# assert everything has been cleaned up
self.assertFalse(_entry_exists(order))
self.assertFalse(_entry_exists(order_plugin_metadata))
self.assertFalse(_entry_exists(order_retry_task))
self.assertFalse(_entry_exists(order_barbican_meta_data))
@_create_project("my clean order with child keystone id")
def test_cleanup_order_with_child(self, project):
"""Test cleaning up an order with a child"""
# create order and retry task
order = _setup_entry('order', project=project)
order_retry_task = _setup_entry('order_retry', order=order)
# soft delete order and retry task,
# it should automatically delete the children
order.delete()
clean.cleanup_all()
# assert that the order was not cleaned due to child
self.assertTrue(_entry_exists(order))
self.assertTrue(_entry_exists(order_retry_task))
order_retry_task.delete()
clean.cleanup_all()
# assert everything has been cleaned up
self.assertFalse(_entry_exists(order))
self.assertFalse(_entry_exists(order_retry_task))
@_create_project("my keystone id")
def test_cleanup_soft_deletion_date(self, project):
"""Test cleaning up entries within date"""
secret = _setup_entry('secret', project=project)
order = order = _setup_entry('order', project=project, secret=secret)
current_time = datetime.datetime.utcnow()
tomorrow = current_time + datetime.timedelta(days=1)
yesterday = current_time - datetime.timedelta(days=1)
secret.delete()
order.delete()
# Assert that nothing is deleted due to date
clean.cleanup_softdeletes(models.Order, threshold_date=yesterday)
clean.cleanup_parent_with_no_child(models.Secret, models.Order,
threshold_date=yesterday)
self.assertTrue(_entry_exists(secret))
self.assertTrue(_entry_exists(order))
# Assert that everything is deleted due to date
clean.cleanup_softdeletes(models.Order, threshold_date=tomorrow)
clean.cleanup_parent_with_no_child(models.Secret, models.Order,
threshold_date=tomorrow)
self.assertFalse(_entry_exists(secret))
self.assertFalse(_entry_exists(order))
@_create_project("my keystone id")
def test_soft_deleting_expired_secrets(self, project):
"""Test soft deleting secrets that are expired"""
current_time = datetime.datetime.utcnow()
tomorrow = current_time + datetime.timedelta(days=1)
yesterday = current_time - datetime.timedelta(days=1)
not_expired_secret = _setup_entry('secret', project=project)
expired_secret = _setup_entry('secret', project=project)
not_expired_secret.expiration = tomorrow
expired_secret.expiration = yesterday
# Create children for expired secret
expired_secret_store_metadatum = _setup_entry('secret_metadatum',
secret=expired_secret)
expired_secret_user_metadatum = _setup_entry('secret_user_metadatum',
secret=expired_secret)
kek_datum = _setup_entry('kek_datum', project=project)
expired_enc_datum = _setup_entry('encrypted_datum',
secret=expired_secret,
kek_datum=kek_datum)
container = _setup_entry('container', project=project)
expired_container_secret = _setup_entry('container_secret',
container=container,
secret=expired_secret)
expired_acl_secret = _setup_entry('acl_secret',
secret=expired_secret,
user_ids=["fern", "chris"])
clean.soft_delete_expired_secrets(current_time)
self.assertTrue(_entry_is_soft_deleted(expired_secret))
self.assertFalse(_entry_is_soft_deleted(not_expired_secret))
# Make sure the children of the expired secret are soft deleted as well
self.assertTrue(_entry_is_soft_deleted(expired_enc_datum))
self.assertTrue(_entry_is_soft_deleted(expired_container_secret))
self.assertTrue(_entry_is_soft_deleted(expired_secret_store_metadatum))
self.assertTrue(_entry_is_soft_deleted(expired_secret_user_metadatum))
self.assertFalse(_entry_exists(expired_acl_secret))
def test_cleaning_unassociated_projects(self):
"""Test cleaning projects that have no child entries"""
childless_project = _setup_entry('project',
external_id="childless project")
project_with_children = _setup_entry(
'project',
external_id="project with children")
project_children_list = list()
project_children_list.append(
_setup_entry('kek_datum', project=project_with_children))
project_children_list.append(
_setup_entry('secret', project=project_with_children))
container = _setup_entry('container', project=project_with_children)
project_children_list.append(container)
project_children_list.append(
_setup_entry('container_consumer_meta', container=container))
cert_authority = _setup_entry('certificate_authority',
project=project_with_children)
project_children_list.append(cert_authority)
project_children_list.append(
_setup_entry('preferred_cert_authority',
cert_authority=cert_authority))
project_children_list.append(
_setup_entry('project_cert_authority',
certificate_authority=cert_authority))
project_children_list.append(_setup_entry('project_quotas',
project=project_with_children))
clean.cleanup_unassociated_projects()
self.assertTrue(_entry_exists(project_with_children))
self.assertFalse(_entry_exists(childless_project))
container.delete()
for child in project_children_list:
child.delete()
clean.cleanup_all()
clean.cleanup_unassociated_projects()
self.assertFalse(_entry_exists(project_with_children))
@mock.patch('barbican.model.clean.cleanup_all')
@mock.patch('barbican.model.clean.soft_delete_expired_secrets')
@mock.patch('barbican.model.clean.cleanup_unassociated_projects')
@mock.patch('barbican.model.clean.repo')
@mock.patch('barbican.model.clean.log')
@mock.patch('barbican.model.clean.CONF')
def test_clean_up_command(self, mock_conf, mock_log, mock_repo,
mock_clean_unc_projects,
mock_soft_del_expire_secrets, mock_clean_all):
"""Tests the clean command"""
test_sql_url = "mysql+pymysql://notrealuser:datab@127.0.0.1/barbican't"
min_num_days = 91
do_clean_unassociated_projects = True
do_soft_delete_expired_secrets = True
verbose = True
test_log_file = "/tmp/sometempfile"
clean.clean_command(test_sql_url, min_num_days,
do_clean_unassociated_projects,
do_soft_delete_expired_secrets, verbose,
test_log_file)
set_calls = [mock.call('debug', True),
mock.call('log_file', test_log_file),
mock.call('sql_connection', test_sql_url)]
mock_conf.set_override.assert_has_calls(set_calls)
clear_calls = [mock.call('debug'), mock.call('log_file'),
mock.call('sql_connection')]
mock_conf.clear_override.assert_has_calls(clear_calls)
self.assertTrue(mock_repo.setup_database_engine_and_factory.called)
self.assertTrue(mock_repo.commit.called)
self.assertTrue(mock_repo.clear.called)
self.assertTrue(mock_clean_unc_projects.called)
self.assertTrue(mock_soft_del_expire_secrets)
self.assertTrue(mock_clean_all)
@mock.patch('barbican.model.clean.cleanup_all')
@mock.patch('barbican.model.clean.soft_delete_expired_secrets')
@mock.patch('barbican.model.clean.cleanup_unassociated_projects')
@mock.patch('barbican.model.clean.repo')
@mock.patch('barbican.model.clean.log')
@mock.patch('barbican.model.clean.CONF')
def test_clean_up_command_with_false_args(
self, mock_conf, mock_log, mock_repo, mock_clean_unc_projects,
mock_soft_del_expire_secrets, mock_clean_all):
"""Tests the clean command with false args"""
test_sql_url = None
min_num_days = -1
do_clean_unassociated_projects = False
do_soft_delete_expired_secrets = False
verbose = None
test_log_file = None
clean.clean_command(test_sql_url, min_num_days,
do_clean_unassociated_projects,
do_soft_delete_expired_secrets, verbose,
test_log_file)
mock_conf.set_override.assert_not_called()
mock_conf.clear_override.assert_not_called()
self.assertTrue(mock_repo.setup_database_engine_and_factory.called)
self.assertTrue(mock_repo.commit.called)
self.assertTrue(mock_repo.clear.called)
self.assertTrue(mock_clean_all)
self.assertFalse(mock_clean_unc_projects.called)
self.assertFalse(mock_soft_del_expire_secrets.called)
@mock.patch('barbican.model.clean.cleanup_all',
side_effect=IntegrityError("", "", "", ""))
@mock.patch('barbican.model.clean.repo')
@mock.patch('barbican.model.clean.log')
@mock.patch('barbican.model.clean.CONF')
def test_clean_up_command_with_exception(
self, mock_conf, mock_log, mock_repo, mock_clean_all):
"""Tests that the clean command throws exceptions"""
args = ("sql", 2, False, False, False, "/tmp/nope")
self.assertRaises(IntegrityError, clean.clean_command, *args)
self.assertTrue(mock_repo.rollback.called)
@_create_project("my integrity error keystone id")
def test_db_cleanup_raise_integrity_error(self, project):
"""Test that an integrity error is thrown
This test tests the invalid scenario where
the secret meta was not marked for deletion during the secret deletion.
We want to make sure an integrity error is thrown during clean up.
"""
# create secret
secret = _setup_entry('secret', project=project)
secret_metadatum = _setup_entry('secret_metadatum', secret=secret)
# delete parent but not child and assert integrity error
secret.deleted = True
secret_metadatum.deleted = False
self.assertRaises(db_exc.DBReferenceError, clean.cleanup_all)