S8DataSourceService.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using System.Text.RegularExpressions;
  4. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  5. public class S8DataSourceService : ITransient
  6. {
  7. private readonly SqlSugarRepository<AdoS8DataSource> _rep;
  8. public S8DataSourceService(SqlSugarRepository<AdoS8DataSource> rep) => _rep = rep;
  9. public async Task<List<AdoS8DataSource>> ListAsync(long tenantId, long factoryId)
  10. {
  11. var rows = await _rep.AsQueryable()
  12. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId)
  13. .ToListAsync();
  14. foreach (var r in rows) r.Endpoint = MaskSecret(r.Endpoint);
  15. return rows;
  16. }
  17. // S8-TENANT-FACTORY-P0-CLOSURE-1:归属一律由服务端可信作用域盖章,忽略 body.TenantId / body.FactoryId。
  18. public async Task<AdoS8DataSource> CreateAsync(AdoS8DataSource body, S8TrustedScope scope)
  19. {
  20. if (string.IsNullOrWhiteSpace(body.DataSourceCode) || string.IsNullOrWhiteSpace(body.Type))
  21. throw new S8BizException("数据源编码和类型必填");
  22. body.TenantId = scope.TenantId;
  23. body.FactoryId = scope.FactoryId;
  24. var exists = await _rep.AsQueryable()
  25. .AnyAsync(x => x.TenantId == body.TenantId && x.FactoryId == body.FactoryId && x.DataSourceCode == body.DataSourceCode);
  26. if (exists) throw new S8BizException("数据源编码已存在");
  27. body.Id = 0;
  28. body.CreatedAt = DateTime.Now;
  29. await _rep.InsertAsync(body);
  30. body.Endpoint = MaskSecret(body.Endpoint);
  31. return body;
  32. }
  33. // S8-TENANT-FACTORY-P0-CLOSURE-1:按 Id + 可信作用域绑行;越权 Id 视为不存在,归属不可被 body 改写。
  34. public async Task<AdoS8DataSource> UpdateAsync(long id, AdoS8DataSource body, S8TrustedScope scope)
  35. {
  36. var e = await LoadScopedAsync(id, scope);
  37. if (string.IsNullOrWhiteSpace(body.DataSourceCode) || string.IsNullOrWhiteSpace(body.Type))
  38. throw new S8BizException("数据源编码和类型必填");
  39. var exists = await _rep.AsQueryable()
  40. .AnyAsync(x => x.Id != id && x.TenantId == e.TenantId && x.FactoryId == e.FactoryId && x.DataSourceCode == body.DataSourceCode);
  41. if (exists) throw new S8BizException("数据源编码已存在");
  42. // 入参 endpoint 含掩码占位符(Pwd=****** / Password=******)时保留旧值的真实密码段,避免前端
  43. // 回填脱敏值后误覆盖。Endpoint 全空时也不覆盖原密码。
  44. body.Endpoint = MergeEndpointPreservingSecret(body.Endpoint, e.Endpoint);
  45. body.Id = id;
  46. body.TenantId = e.TenantId;
  47. body.FactoryId = e.FactoryId;
  48. body.CreatedAt = e.CreatedAt;
  49. body.UpdatedAt = DateTime.Now;
  50. await _rep.UpdateAsync(body);
  51. body.Endpoint = MaskSecret(body.Endpoint);
  52. return body;
  53. }
  54. // S8-TENANT-FACTORY-P0-CLOSURE-1:删除必须先按可信作用域绑行,禁止裸 DeleteByIdAsync(id)。
  55. public async Task DeleteAsync(long id, S8TrustedScope scope)
  56. {
  57. var e = await LoadScopedAsync(id, scope);
  58. await _rep.DeleteByIdAsync(e.Id);
  59. }
  60. /// <summary>按 Id + 可信作用域取行;不在作用域内一律按「不存在」处理,不泄露他租户资源是否存在。</summary>
  61. private async Task<AdoS8DataSource> LoadScopedAsync(long id, S8TrustedScope scope) =>
  62. await _rep.AsQueryable()
  63. .Where(x => x.Id == id && x.TenantId == scope.TenantId && x.FactoryId == scope.FactoryId)
  64. .FirstAsync() ?? throw new S8NotFoundException();
  65. public async Task<object> TestAsync(long id, S8TrustedScope scope)
  66. {
  67. var entity = await LoadScopedAsync(id, scope);
  68. var success = !string.IsNullOrWhiteSpace(entity.Endpoint);
  69. entity.LastCheckAt = DateTime.Now;
  70. entity.LastCheckStatus = success ? "SUCCESS" : "FAILED: endpoint is empty";
  71. entity.UpdatedAt = DateTime.Now;
  72. await _rep.UpdateAsync(entity);
  73. return new
  74. {
  75. id,
  76. success,
  77. message = success ? "连接信息校验通过" : "连接地址为空,未通过校验",
  78. entity.LastCheckAt,
  79. entity.LastCheckStatus
  80. };
  81. }
  82. // BUG-13:endpoint 中的 Pwd=xxx / Password=xxx(大小写不敏感)替换为 ******,保留其它字段。
  83. private static readonly Regex SecretPattern = new(
  84. @"(?i)(Pwd|Password)\s*=\s*([^;]*)",
  85. RegexOptions.Compiled);
  86. private static string? MaskSecret(string? endpoint)
  87. {
  88. if (string.IsNullOrWhiteSpace(endpoint)) return endpoint;
  89. return SecretPattern.Replace(endpoint, m => $"{m.Groups[1].Value}=******");
  90. }
  91. private static string? MergeEndpointPreservingSecret(string? incoming, string? existing)
  92. {
  93. if (string.IsNullOrWhiteSpace(incoming)) return existing;
  94. if (string.IsNullOrWhiteSpace(existing)) return incoming;
  95. // 提取旧 endpoint 中的真实密码值(首个匹配为准)
  96. var oldMatch = SecretPattern.Match(existing);
  97. if (!oldMatch.Success) return incoming;
  98. var realSecret = oldMatch.Groups[2].Value;
  99. // 把入参里 Pwd=****** 之类的占位还原为真实密码
  100. return SecretPattern.Replace(incoming, m =>
  101. {
  102. var v = m.Groups[2].Value;
  103. return IsMaskedPlaceholder(v) ? $"{m.Groups[1].Value}={realSecret}" : m.Value;
  104. });
  105. }
  106. private static bool IsMaskedPlaceholder(string? v) =>
  107. !string.IsNullOrEmpty(v) && v.All(c => c == '*');
  108. }