"""Explicitly encrypt one legacy DataSource credential and remove plaintext.""" import argparse import json from dataclasses import dataclass from app.core.data_source.models import DataSourceCredential @dataclass(frozen=True) class MigrationResult: data_source_uid: str credential_version: int status: str def migrate_one( data_source_uid, graph_session, credential_repository, platform_session, *, actor_uid, ): record = graph_session.run( """ MATCH (n:DataSource {uid: $uid}) RETURN properties(n) AS properties """, {"uid": str(data_source_uid)}, ).single() if record is None: raise ValueError("data source was not found") properties = dict(record["properties"]) username = properties.get("username") password = properties.get("password") if not username or not password: raise ValueError("legacy plaintext credential is incomplete") credential = DataSourceCredential(str(username), str(password)) try: sealed = credential_repository.create_version( platform_session, data_source_uid=str(data_source_uid), credential=credential, actor_uid=actor_uid, ) verified = credential_repository.codec.decrypt(sealed) if verified != credential: raise ValueError("credential round-trip verification failed") platform_session.commit() except Exception: platform_session.rollback() raise try: graph_session.run( """ MATCH (n:DataSource {uid: $uid}) SET n.credential_ref = $credential_ref, n.credential_version = $credential_version """, { "uid": str(data_source_uid), "credential_ref": str(data_source_uid), "credential_version": sealed.credential_version, }, ) graph_session.run( """ MATCH (n:DataSource {uid: $uid}) REMOVE n.username, n.password, n.conn_str, n.connection_string, n.connection_url """, {"uid": str(data_source_uid)}, ) except Exception as graph_error: try: credential_repository.compensate_failed_activation( platform_session, data_source_uid=str(data_source_uid), failed_version=sealed.credential_version, restore_version=None, actor_uid=actor_uid, ) platform_session.commit() except Exception: platform_session.rollback() raise ValueError( "credential migration requires reconciliation" ) from graph_error return MigrationResult( data_source_uid=str(data_source_uid), credential_version=sealed.credential_version, status="migrated", ) def main(): parser = argparse.ArgumentParser( description="Encrypt one legacy DataSource credential" ) parser.add_argument("--uid", required=True) parser.add_argument( "--confirm-encrypt-and-remove-plaintext", action="store_true", ) args = parser.parse_args() if not args.confirm_encrypt_and_remove_plaintext: parser.error( "--confirm-encrypt-and-remove-plaintext is required" ) from app import create_app, db from app.core.data_source.credentials import ( CredentialCodec, DataSourceCredentialRepository, ) from app.services.neo4j_driver import neo4j_driver app = create_app() with app.app_context(): codec = CredentialCodec.from_base64( app.config.get("DATASOURCE_CREDENTIAL_MASTER_KEY", ""), app.config.get("DATASOURCE_CREDENTIAL_KEY_VERSION", "v1"), ) repository = DataSourceCredentialRepository(codec) with neo4j_driver.get_session() as graph_session: result = migrate_one( args.uid, graph_session, repository, db.session, actor_uid=None, ) print( json.dumps( { "data_source_uid": result.data_source_uid, "credential_version": result.credential_version, "status": result.status, }, ensure_ascii=False, ) ) if __name__ == "__main__": main()