MdpSourcePasswordResolver.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. using System.Text.RegularExpressions;
  2. using Admin.NET.Core;
  3. using Microsoft.Extensions.Logging;
  4. namespace Admin.NET.Plugin.AiDOP.DataPlatform;
  5. public interface IMdpSourcePasswordResolver
  6. {
  7. /// <summary>解析数据源口令。返回值不得写入日志、实体或数据库。</summary>
  8. string Resolve(string sourceCode, string? storedSecret);
  9. }
  10. public sealed class MdpSourceSecretException : Exception
  11. {
  12. public string Code { get; }
  13. public MdpSourceSecretException(string code, string message) : base(message)
  14. {
  15. Code = code;
  16. }
  17. }
  18. /// <summary>
  19. /// 口令只在建连时解析:环境变量优先,其次系统密文,最后才是迁移期明文。
  20. /// </summary>
  21. public sealed class MdpSourcePasswordResolver : IMdpSourcePasswordResolver, ITransient
  22. {
  23. private readonly ILogger<MdpSourcePasswordResolver>? _logger;
  24. public MdpSourcePasswordResolver(ILogger<MdpSourcePasswordResolver>? logger = null)
  25. {
  26. _logger = logger;
  27. }
  28. public static string EnvironmentVariableName(string sourceCode)
  29. {
  30. if (string.IsNullOrWhiteSpace(sourceCode))
  31. throw new ArgumentException("sourceCode 不能为空", nameof(sourceCode));
  32. var token = Regex.Replace(sourceCode.Trim().ToUpperInvariant(), "[^A-Z0-9]", "_");
  33. return "AIDOP_MDP_SOURCE_" + token + "_PASSWORD";
  34. }
  35. public string Resolve(string sourceCode, string? storedSecret)
  36. => ResolveCore(sourceCode, storedSecret, DecryptStored, _logger);
  37. public string Classify(string sourceCode, string? storedSecret)
  38. {
  39. var envName = EnvironmentVariableName(sourceCode);
  40. if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName)))
  41. return "ENV";
  42. if (string.IsNullOrEmpty(storedSecret))
  43. return "MISSING";
  44. return TryDecrypt(storedSecret, DecryptStored, out _) ? "ENCRYPTED" : "LEGACY_PLAINTEXT";
  45. }
  46. internal static string ResolveCore(
  47. string sourceCode,
  48. string? storedSecret,
  49. Func<string, string?> decrypt,
  50. ILogger? logger)
  51. {
  52. var envName = EnvironmentVariableName(sourceCode);
  53. var fromEnv = Environment.GetEnvironmentVariable(envName);
  54. if (!string.IsNullOrEmpty(fromEnv))
  55. return fromEnv;
  56. if (!string.IsNullOrEmpty(storedSecret))
  57. {
  58. if (TryDecrypt(storedSecret, decrypt, out var plain))
  59. return plain;
  60. logger?.LogWarning(
  61. "mdp source {SourceCode} still uses a legacy plaintext secret. Set {EnvName} and clear the stored value.",
  62. sourceCode, envName);
  63. return storedSecret;
  64. }
  65. throw new MdpSourceSecretException(
  66. "SECRET_MISSING",
  67. $"源 {sourceCode} 未配置口令。请设置环境变量 {envName}。");
  68. }
  69. private static string? DecryptStored(string stored)
  70. {
  71. var decrypted = CryptogramUtil.Decrypt(stored);
  72. return decrypted;
  73. }
  74. private static bool TryDecrypt(string stored, Func<string, string?> decrypt, out string plain)
  75. {
  76. plain = "";
  77. try
  78. {
  79. var decrypted = decrypt(stored);
  80. // 原样返回入参不是解密成功,按迁移期明文处理。
  81. if (string.IsNullOrEmpty(decrypted) || string.Equals(decrypted, stored, StringComparison.Ordinal))
  82. return false;
  83. plain = decrypted;
  84. return true;
  85. }
  86. catch
  87. {
  88. return false;
  89. }
  90. }
  91. }