| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- """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()
|