migrate_datasource_credentials.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """Explicitly encrypt one legacy DataSource credential and remove plaintext."""
  2. import argparse
  3. import json
  4. from dataclasses import dataclass
  5. from app.core.data_source.models import DataSourceCredential
  6. @dataclass(frozen=True)
  7. class MigrationResult:
  8. data_source_uid: str
  9. credential_version: int
  10. status: str
  11. def migrate_one(
  12. data_source_uid,
  13. graph_session,
  14. credential_repository,
  15. platform_session,
  16. *,
  17. actor_uid,
  18. ):
  19. record = graph_session.run(
  20. """
  21. MATCH (n:DataSource {uid: $uid})
  22. RETURN properties(n) AS properties
  23. """,
  24. {"uid": str(data_source_uid)},
  25. ).single()
  26. if record is None:
  27. raise ValueError("data source was not found")
  28. properties = dict(record["properties"])
  29. username = properties.get("username")
  30. password = properties.get("password")
  31. if not username or not password:
  32. raise ValueError("legacy plaintext credential is incomplete")
  33. credential = DataSourceCredential(str(username), str(password))
  34. try:
  35. sealed = credential_repository.create_version(
  36. platform_session,
  37. data_source_uid=str(data_source_uid),
  38. credential=credential,
  39. actor_uid=actor_uid,
  40. )
  41. verified = credential_repository.codec.decrypt(sealed)
  42. if verified != credential:
  43. raise ValueError("credential round-trip verification failed")
  44. platform_session.commit()
  45. except Exception:
  46. platform_session.rollback()
  47. raise
  48. try:
  49. graph_session.run(
  50. """
  51. MATCH (n:DataSource {uid: $uid})
  52. SET n.credential_ref = $credential_ref,
  53. n.credential_version = $credential_version
  54. """,
  55. {
  56. "uid": str(data_source_uid),
  57. "credential_ref": str(data_source_uid),
  58. "credential_version": sealed.credential_version,
  59. },
  60. )
  61. graph_session.run(
  62. """
  63. MATCH (n:DataSource {uid: $uid})
  64. REMOVE n.username, n.password, n.conn_str,
  65. n.connection_string, n.connection_url
  66. """,
  67. {"uid": str(data_source_uid)},
  68. )
  69. except Exception as graph_error:
  70. try:
  71. credential_repository.compensate_failed_activation(
  72. platform_session,
  73. data_source_uid=str(data_source_uid),
  74. failed_version=sealed.credential_version,
  75. restore_version=None,
  76. actor_uid=actor_uid,
  77. )
  78. platform_session.commit()
  79. except Exception:
  80. platform_session.rollback()
  81. raise ValueError(
  82. "credential migration requires reconciliation"
  83. ) from graph_error
  84. return MigrationResult(
  85. data_source_uid=str(data_source_uid),
  86. credential_version=sealed.credential_version,
  87. status="migrated",
  88. )
  89. def main():
  90. parser = argparse.ArgumentParser(
  91. description="Encrypt one legacy DataSource credential"
  92. )
  93. parser.add_argument("--uid", required=True)
  94. parser.add_argument(
  95. "--confirm-encrypt-and-remove-plaintext",
  96. action="store_true",
  97. )
  98. args = parser.parse_args()
  99. if not args.confirm_encrypt_and_remove_plaintext:
  100. parser.error(
  101. "--confirm-encrypt-and-remove-plaintext is required"
  102. )
  103. from app import create_app, db
  104. from app.core.data_source.credentials import (
  105. CredentialCodec,
  106. DataSourceCredentialRepository,
  107. )
  108. from app.services.neo4j_driver import neo4j_driver
  109. app = create_app()
  110. with app.app_context():
  111. codec = CredentialCodec.from_base64(
  112. app.config.get("DATASOURCE_CREDENTIAL_MASTER_KEY", ""),
  113. app.config.get("DATASOURCE_CREDENTIAL_KEY_VERSION", "v1"),
  114. )
  115. repository = DataSourceCredentialRepository(codec)
  116. with neo4j_driver.get_session() as graph_session:
  117. result = migrate_one(
  118. args.uid,
  119. graph_session,
  120. repository,
  121. db.session,
  122. actor_uid=None,
  123. )
  124. print(
  125. json.dumps(
  126. {
  127. "data_source_uid": result.data_source_uid,
  128. "credential_version": result.credential_version,
  129. "status": result.status,
  130. },
  131. ensure_ascii=False,
  132. )
  133. )
  134. if __name__ == "__main__":
  135. main()