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