diff --git a/reana_db/cli.py b/reana_db/cli.py index 442d1d0..dc0983d 100644 --- a/reana_db/cli.py +++ b/reana_db/cli.py @@ -21,6 +21,7 @@ from reana_db.database import init_db from reana_db.models import Resource, ResourceType from reana_db.utils import ( + change_key_encrypted_columns, update_users_cpu_quota, update_users_disk_quota, update_workflows_cpu_quota, @@ -43,6 +44,25 @@ def init(): click.secho("Database initialised.", fg="green") +@cli.command() +@click.option( + "--old-key", + required=True, + help="Previous key used to encrypt database columns.", +) +def migrate_secret_key(old_key): + """Change the secret key used to encrypt database columns.""" + click.echo("Migrating secret key...") + + try: + change_key_encrypted_columns(old_key) + except Exception: + logging.exception("Failed to migrate secret key") + sys.exit(1) + + click.echo("Successfully migrated secret key") + + @cli.group("alembic") @click.pass_context def alembic_group(ctx): diff --git a/reana_db/models.py b/reana_db/models.py index f6f8d5e..fc0cf2e 100644 --- a/reana_db/models.py +++ b/reana_db/models.py @@ -51,8 +51,8 @@ from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine from sqlalchemy.dialects.postgresql import ARRAY +import reana_db.config from reana_db.config import ( - DB_SECRET_KEY, DEFAULT_QUOTA_LIMITS, DEFAULT_QUOTA_RESOURCES, WORKFLOW_TERMINATION_QUOTA_UPDATE_POLICY, @@ -87,6 +87,15 @@ def generate_uuid(): return str(uuid.uuid4()) +def _secret_key(): + """Secret key used to encrypt databse columns. + + Do not use `DB_SECRET_KEY` directly, as that does not let us change the key + at runtime, which is needed when migrating between different keys. + """ + return reana_db.config.DB_SECRET_KEY + + class QuotaBase: """Quota base functionality.""" @@ -326,7 +335,7 @@ class UserToken(Base, Timestamp): id_ = Column(UUIDType, primary_key=True, default=generate_uuid) token = Column( - EncryptedType(String(length=255), DB_SECRET_KEY, AesEngine, "pkcs5"), + EncryptedType(String(length=255), _secret_key, AesEngine, "pkcs5"), unique=True, ) status = Column(Enum(UserTokenStatus)) diff --git a/reana_db/utils.py b/reana_db/utils.py index 8c943be..f002da1 100644 --- a/reana_db/utils.py +++ b/reana_db/utils.py @@ -612,3 +612,33 @@ def update_workflows_disk_quota() -> None: for workflow in workflows: store_workflow_disk_quota(workflow) timer.count_event() + + +def change_key_encrypted_columns(old_key): + """Re-encrypt database columns with new secret key. + + REANA should be already deployed with the new secret key in `REANA_SECRET_KEY`. + The old key is needed to decrypt the database and is passed as parameter. + """ + from reana_db.database import Session + from reana_db.models import UserToken + from reana_db import config + + new_key = config.DB_SECRET_KEY + + # set old key to be able to decrypt columns in database + config.DB_SECRET_KEY = old_key + + # read the columns from the database + user_tokens = Session.query(UserToken.id_, UserToken.token).all() + Session.expunge_all() + + # revert to new key + config.DB_SECRET_KEY = new_key + + # write columns to the database to encrypt them with new key + for user_token in user_tokens: + UserToken.query.filter_by(id_=user_token.id_).update( + {"token": user_token.token} + ) + Session.commit()