reconcile_datasource_credentials.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """Report and narrowly repair Neo4j/PostgreSQL credential references."""
  2. import argparse
  3. import json
  4. from sqlalchemy import text
  5. def inspect_reconciliation(graph_session, platform_session):
  6. graph_records = graph_session.run(
  7. """
  8. MATCH (n:DataSource)
  9. RETURN n.uid AS uid, n.credential_ref AS credential_ref,
  10. n.credential_version AS credential_version,
  11. n.username IS NOT NULL OR n.password IS NOT NULL
  12. AS plaintext_remaining
  13. """
  14. )
  15. graph = {
  16. str(record["uid"]): dict(record)
  17. for record in graph_records
  18. if record.get("uid")
  19. }
  20. rows = platform_session.execute(
  21. text(
  22. """
  23. SELECT data_source_uid::text AS uid, credential_version, status
  24. FROM public.datasource_credentials
  25. ORDER BY data_source_uid, credential_version
  26. """
  27. )
  28. ).mappings()
  29. credentials = {}
  30. for row in rows:
  31. credentials.setdefault(str(row["uid"]), []).append(dict(row))
  32. report = []
  33. for uid in sorted(set(graph) | set(credentials)):
  34. issues = []
  35. node = graph.get(uid)
  36. versions = credentials.get(uid, [])
  37. active = [
  38. row for row in versions if row["status"] == "active"
  39. ]
  40. if node is None:
  41. issues.append("orphaned_credential")
  42. else:
  43. if node.get("plaintext_remaining"):
  44. issues.append("plaintext_remaining")
  45. referenced = node.get("credential_version")
  46. matching = [
  47. row
  48. for row in versions
  49. if row["credential_version"] == referenced
  50. ]
  51. if not matching:
  52. issues.append("missing_credential")
  53. elif matching[0]["status"] == "revoked":
  54. issues.append("revoked_credential_referenced")
  55. if active and referenced != active[-1]["credential_version"]:
  56. issues.append("stale_credential_version")
  57. if issues:
  58. report.append({"data_source_uid": uid, "issues": issues})
  59. return report
  60. def repair_one(data_source_uid, graph_session, platform_session):
  61. row = (
  62. platform_session.execute(
  63. text(
  64. """
  65. SELECT credential_version
  66. FROM public.datasource_credentials
  67. WHERE data_source_uid = CAST(:uid AS uuid)
  68. AND status = 'active'
  69. ORDER BY credential_version DESC
  70. LIMIT 1
  71. """
  72. ),
  73. {"uid": str(data_source_uid)},
  74. )
  75. .mappings()
  76. .one_or_none()
  77. )
  78. if row is None:
  79. raise ValueError("no active credential is available for repair")
  80. graph_session.run(
  81. """
  82. MATCH (n:DataSource {uid: $uid})
  83. SET n.credential_ref = $uid,
  84. n.credential_version = $credential_version
  85. """,
  86. {
  87. "uid": str(data_source_uid),
  88. "credential_version": int(row["credential_version"]),
  89. },
  90. )
  91. return {
  92. "data_source_uid": str(data_source_uid),
  93. "status": "reference_repaired",
  94. }
  95. def main():
  96. parser = argparse.ArgumentParser(
  97. description="Reconcile DataSource credential references"
  98. )
  99. parser.add_argument("--repair", action="store_true")
  100. parser.add_argument("--uid")
  101. args = parser.parse_args()
  102. if args.repair and not args.uid:
  103. parser.error("--repair requires --uid")
  104. from app import create_app, db
  105. from app.services.neo4j_driver import neo4j_driver
  106. app = create_app()
  107. with app.app_context():
  108. with neo4j_driver.get_session() as graph_session:
  109. report = inspect_reconciliation(graph_session, db.session)
  110. for item in report:
  111. print(json.dumps(item, ensure_ascii=False))
  112. if args.repair:
  113. result = repair_one(args.uid, graph_session, db.session)
  114. print(json.dumps(result, ensure_ascii=False))
  115. if __name__ == "__main__":
  116. main()