From b07d8e6fbd82c07f05921c9699881921f3cb1b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= Date: Thu, 20 Mar 2025 15:45:02 -0400 Subject: [PATCH] Add simple_crypto new_pkek subcommand This patch adds a new subcommand to `barbican-manage`: barbican-manage simple_crypto new_pkek --project $PROJECT_ID The subcommand adds a new Project-specific Key-encryption-key (pKEK) for the specified project. Previous pKEKs are marked as inactive, so all new secrets uploaded for that project will be encrypted using the new pKEK. Change-Id: Ica7ab4889ffb656d026095db8b43590000ceb546 --- barbican/cmd/barbican_manage.py | 10 ++ barbican/cmd/simple_crypto_pkek.py | 42 +++++++ barbican/model/repositories.py | 106 ++++++++++++------ ...mple-crypto-new-pkek-95b5d970cd85a6dd.yaml | 11 ++ 4 files changed, 134 insertions(+), 35 deletions(-) create mode 100644 barbican/cmd/simple_crypto_pkek.py create mode 100644 releasenotes/notes/add-simple-crypto-new-pkek-95b5d970cd85a6dd.yaml diff --git a/barbican/cmd/barbican_manage.py b/barbican/cmd/barbican_manage.py index 6a7db1089..471b39182 100644 --- a/barbican/cmd/barbican_manage.py +++ b/barbican/cmd/barbican_manage.py @@ -27,6 +27,7 @@ from oslo_log import log as logging from barbican.cmd import pkcs11_kek_rewrap as pkcs11_rewrap from barbican.cmd import simple_crypto_kek_rewrap +from barbican.cmd import simple_crypto_pkek from barbican.common import config from barbican.model import clean from barbican.model.migration import commands @@ -364,6 +365,15 @@ class SimpleCryptoCommands: ) rewrapper.execute(dryrun) + new_pkek_description = ("Create a new Project-specific Key-Encryption-Key " + "(pKEK) for the given project-id.") + + @args('--project', dest='project_id', metavar='', + help="External Project ID e.g. Keystone Project ID.") + def new_pkek(self, conf, project_id): + pkek_cmd = simple_crypto_pkek.SimpleCryptoPKEK(simple_crypto_pkek.CONF) + pkek_cmd.new_pkek(project_id) + CATEGORIES = { 'db': DbCommands, diff --git a/barbican/cmd/simple_crypto_pkek.py b/barbican/cmd/simple_crypto_pkek.py new file mode 100644 index 000000000..8c7114162 --- /dev/null +++ b/barbican/cmd/simple_crypto_pkek.py @@ -0,0 +1,42 @@ +# Copyright 2025 Red Hat, Inc. +# +# 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. +from barbican.common import resources +from barbican.common import utils +from barbican.model import repositories +from barbican.plugin.crypto import simple_crypto + + +CONF = simple_crypto.CONF + + +class SimpleCryptoPKEK: + + def __init__(self, conf): + self.crypto_plugin = simple_crypto.SimpleCryptoPlugin(conf) + self.crypto_plugin_name = utils.generate_fullname_for( + self.crypto_plugin + ) + repositories.setup_database_engine_and_factory() + + def new_pkek(self, external_id): + """Creates a new Project-specific KEK + + :param str external_id: Project ID as defined by the external identity + system. e.g. Keystone Project ID. + """ + print(f"Generating new pKEK for {external_id}") + project = resources.get_or_create_project(external_id) + kek_repo = repositories.get_kek_datum_repository() + _ = kek_repo.create_kek_datum(project, self.crypto_plugin_name) + repositories.commit() diff --git a/barbican/model/repositories.py b/barbican/model/repositories.py index 8fa95e34b..5fde3ebb6 100644 --- a/barbican/model/repositories.py +++ b/barbican/model/repositories.py @@ -29,7 +29,7 @@ from oslo_db import exception as db_exc from oslo_db.sqlalchemy import enginefacade from oslo_utils import timeutils from oslo_utils import uuidutils -import sqlalchemy +import sqlalchemy as sa from sqlalchemy import func as sa_func from sqlalchemy import or_ import sqlalchemy.orm as sa_orm @@ -111,7 +111,7 @@ def setup_database_engine_and_factory(initialize_secret_stores=False): # Utilize SQLAlchemy's scoped_session to ensure that we only have one # session instance per thread. session_maker = sa_orm.sessionmaker(bind=_ENGINE) - _SESSION_FACTORY = sqlalchemy.orm.scoped_session(session_maker) + _SESSION_FACTORY = sa.orm.scoped_session(session_maker) if initialize_secret_stores: _initialize_secret_stores_data() @@ -182,7 +182,7 @@ def _get_engine(engine): db_connection.close() if CONF.db_auto_create: - meta = sqlalchemy.MetaData() + meta = sa.MetaData() meta.reflect(bind=engine) tables = meta.tables @@ -532,7 +532,7 @@ class BaseRepo(object): for entity in query: # Its a soft delete so its more like entity update entity.delete(session=session) - except sqlalchemy.exc.SQLAlchemyError: + except sa.exc.SQLAlchemyError: LOG.exception('Problem finding project related entity to delete') if not suppress_exception: raise exception.BarbicanException(u._('Error deleting project ' @@ -929,7 +929,21 @@ class KEKDatumRepo(BaseRepo): plugin_name, suppress_exception=False, session=None): - """Find or create a KEK datum instance.""" + """Find or create a KEK datum instance + + Returns the active KEK datum for the given project. This method also + ensures there is only one active KEK. If more than one active KEK + is found, the newest KEK will be used and all others will be + deactivated. + + :param project: Project instance to be associated with the new KEK + :type project: :py:class:`barbican.model.models.Project` + :param str plugin_name: Fully qualified class name to identify + the encryption plugin. + :param bool supress_exception: ? - not used + :param session: + :type session: :py:class:`sqlalchemy.orm.Session` or None + """ if not plugin_name: raise exception.BarbicanException( u._('Tried to register crypto plugin with null or empty ' @@ -937,48 +951,70 @@ class KEKDatumRepo(BaseRepo): kek_datum = None - session = self.get_session(session) - - query = session.query(models.KEKDatum) - query = query.filter_by(project_id=project.id, - plugin_name=plugin_name, - active=True, - deleted=False) - - query = query.order_by(models.KEKDatum.created_at) - - kek_datums = query.all() + kek_datums = self._get_active_kek_datums(project.id, plugin_name) if not kek_datums: - kek_datum = models.KEKDatum() - - kek_datum.kek_label = "project-{0}-key-{1}".format( - project.external_id, uuidutils.generate_uuid()) - kek_datum.project_id = project.id - kek_datum.plugin_name = plugin_name - kek_datum.status = models.States.ACTIVE - - self.save(kek_datum) + kek_datum = self.create_kek_datum(project, plugin_name) else: - kek_datum = kek_datums.pop() - # (alee) There should be only one active KEKDatum. # Due to a race condition with many threads or # many barbican processes, its possible to have # multiple active KEKDatum. The code below makes # all the extra KEKDatum inactive # See LP#1726378 - for kd in kek_datums: - LOG.debug( - "Multiple active KEKDatum found for %s." - "Setting %s to be inactive.", - project.external_id, - kd.kek_label) - kd.active = False - self.save(kd) + kek_datum = kek_datums.pop() + self._deactivate_kek_datums(kek_datums) return kek_datum + def create_kek_datum(self, project, plugin_name): + """Create a new KEK instance + + Creates a new KEK instance associated with the given project and + plugin, and also deactivates any existing KEKs. + + :param project: Project instance to be associated with the new KEK + :type project: :py:class:`barbican.model.models.Project` + :param str plugin_name: Fully qualified class name to identify + the encryption plugin. + """ + if not plugin_name: + raise exception.BarbicanException( + u._('Tried to register crypto plugin with null or empty ' + 'name.')) + # Deactivate any existing KEKs + kek_datums = self._get_active_kek_datums(project.id, plugin_name) + self._deactivate_kek_datums(kek_datums) + + # Create new unbound KEK + new_kek = models.KEKDatum() + new_kek_id = uuidutils.generate_uuid() + new_kek.kek_label = f"project-{project.external_id}-key-{new_kek_id}" + new_kek.project_id = project.id + new_kek.plugin_name = plugin_name + new_kek.status = models.States.ACTIVE + LOG.info(f"Created new KEK {new_kek.kek_label}") + self.save(new_kek) + return new_kek + + def _get_active_kek_datums(self, project_id, plugin_name): + session = self.get_session() + stmt = sa.select(models.KEKDatum).where( + sa.and_( + models.KEKDatum.project_id == project_id, + models.KEKDatum.plugin_name == plugin_name, + models.KEKDatum.active.is_(True), + models.KEKDatum.deleted.is_(False) + ) + ).order_by(models.KEKDatum.created_at) + return session.execute(stmt).scalars().all() + + def _deactivate_kek_datums(self, kek_datums): + for kek in kek_datums: + LOG.info(f"Deactivating KEK {kek.kek_label}") + kek.active = False + self.save(kek) + def _do_entity_name(self): """Sub-class hook: return entity name, such as for debugging.""" return "KEKDatum" diff --git a/releasenotes/notes/add-simple-crypto-new-pkek-95b5d970cd85a6dd.yaml b/releasenotes/notes/add-simple-crypto-new-pkek-95b5d970cd85a6dd.yaml new file mode 100644 index 000000000..2186f40d4 --- /dev/null +++ b/releasenotes/notes/add-simple-crypto-new-pkek-95b5d970cd85a6dd.yaml @@ -0,0 +1,11 @@ +--- +security: + - | + Added a new subcommand to `barbican-manage`: + + barbican-manage simple_crypto new_pkek --project $PROJECT_ID + + This new command creates a new Project-specific Key-encryption-key (pKEK) + for the specified project. New secrets created in that project will be + encrypted with this new pKEK. Existing secrets are not modified by this + command.