S8DataSourceService.cs 4.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  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. /// <summary>
  6. /// S8 数据源配置服务。
  7. ///
  8. /// <b>S8-LEGACY-RUNTIME-RETIREMENT-1:本服务已退化为只读。</b>
  9. ///
  10. /// 产品裁决:S8 = 数据中台**消费层**,不是数据接入层。
  11. /// <code>
  12. /// Data Platform → Governed Dataset → S8 Provider → Canonical Contract → Watch Rule
  13. /// </code>
  14. /// 物理数据源(连接串 / 主机 / 端口 / 凭据)的治理权归数据平台,不由租户 S8 管理员维护。
  15. ///
  16. /// <b>为什么不能只隐藏 UI</b>:<c>ado_s8_data_source.endpoint</c> 存的是**含凭据的真实连接串**,
  17. /// 而 API 仍可被直接调用。写能力留着就等于「入口看不见但仍可写库、仍可让 S8 发起对外连接」。
  18. /// 故在服务层(写入的唯一收敛点)统一拒绝。
  19. ///
  20. /// <b>为什么连接测试也退役</b>:<c>TestAsync</c> 虽只写 <c>last_check_*</c>,但它会用库里的 endpoint
  21. /// **真实建立外部数据库连接** —— 那正是本批要终止的「S8 维护物理连接」能力,
  22. /// 比单纯的行更新更需要关掉。随之移除 <c>S8SqlSugarScopeFactory</c> 依赖:
  23. /// 本服务从此**在结构上**不具备发起外连的能力,而不只是"约定不用"。
  24. ///
  25. /// <b>保留的部分</b>:
  26. /// · <see cref="ListAsync"/> —— 存量 Legacy 规则仍绑着这些行,排障与历史核对需要能看到(endpoint 仍脱敏);
  27. /// · 写方法签名 —— 硬约束,<c>S8TenantIsolationContractTests</c> 用反射断言带
  28. /// <c>S8TrustedScope</c> 的写入口存在、且无 scope 的旧重载不存在;
  29. /// · <c>ado_s8_data_source</c> 表 / <c>S8DataSourceRowLoader</c> / <c>S8LegacySqlDataProvider</c> ——
  30. /// 本批只做 Runtime Retirement,物理清理留待后续独立批次。
  31. /// </summary>
  32. public class S8DataSourceService : ITransient
  33. {
  34. private readonly SqlSugarRepository<AdoS8DataSource> _rep;
  35. public S8DataSourceService(SqlSugarRepository<AdoS8DataSource> rep)
  36. {
  37. _rep = rep;
  38. }
  39. /// <summary>数据源写入口退役后的统一文案。与 Rule 侧退役文案分开:二者是两条不同的产品裁决。</summary>
  40. public const string RetiredMessage =
  41. "S8 已切换为数据中台 Dataset 消费模式,物理数据源由数据平台治理,"
  42. + "S8 侧不再提供数据源的新增 / 修改 / 删除 / 连接测试。";
  43. /// <summary>
  44. /// 只读列出本作用域的数据源。endpoint 中的 Pwd / Password 一律脱敏后返回。
  45. /// </summary>
  46. public async Task<List<AdoS8DataSource>> ListAsync(long tenantId, long factoryId)
  47. {
  48. var rows = await _rep.AsQueryable()
  49. .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId)
  50. .ToListAsync();
  51. foreach (var r in rows) r.Endpoint = MaskSecret(r.Endpoint);
  52. return rows;
  53. }
  54. // ================================================================================
  55. // 写入口:全部退役。
  56. //
  57. // ⚠️ 抛出发生在**任何 DB 访问之前**:不做 LoadScopedAsync、不做重复性查询。
  58. // 这既保证零 DB 触碰,也保证任意 id(含越权 id)一律 410 而非 404
  59. // ——「这个能力没了」优先于「这条记录不属于你」,
  60. // 避免用越权探测反推他租户资源是否存在。
  61. // ================================================================================
  62. public Task<AdoS8DataSource> CreateAsync(AdoS8DataSource body, S8TrustedScope scope) =>
  63. throw new S8WriteRetiredException(RetiredMessage);
  64. public Task<AdoS8DataSource> UpdateAsync(long id, AdoS8DataSource body, S8TrustedScope scope) =>
  65. throw new S8WriteRetiredException(RetiredMessage);
  66. public Task DeleteAsync(long id, S8TrustedScope scope) =>
  67. throw new S8WriteRetiredException(RetiredMessage);
  68. public Task<object> TestAsync(long id, S8TrustedScope scope) =>
  69. throw new S8WriteRetiredException(RetiredMessage);
  70. // BUG-13:endpoint 中的 Pwd=xxx / Password=xxx(大小写不敏感)替换为 ******,保留其它字段。
  71. private static readonly Regex SecretPattern = new(
  72. @"(?i)(Pwd|Password)\s*=\s*([^;]*)",
  73. RegexOptions.Compiled);
  74. private static string? MaskSecret(string? endpoint)
  75. {
  76. if (string.IsNullOrWhiteSpace(endpoint)) return endpoint;
  77. return SecretPattern.Replace(endpoint, m => $"{m.Groups[1].Value}=******");
  78. }
  79. }