按《FL最终代码审计与上线阻断清单》194 项已审计工作树形成候选,基座 = origin/main 11e0778。
- 源码/测试/资源:沿用已审计工作树内容(96 项相对远端为新增或更新),含未跟踪实现
CallbackFactStore、CallbackSourcePolicy、VariableContract、FormalApplyCommandService、
FlowActorContextGate 等;C01 续验夹具修正一并入内。
- CI:保留远端 11e0778 的门禁强化(CI_SOURCE_SHA 核对、容器回收验证、空 schema 反向门禁、
main ref 才推镜像);ci/flow-test-schema.sql 按当前实体重新生成(14→20 表,补 6 新表与
6 新列,回调日志复合唯一键 uk_flw_log_event_reg),workflow 表数断言同步 14→20。
- 依据:docs/tasks/evidence/FL-final-audit-20261027(194 项哈希/版本核对)。
134 lines
6.2 KiB
Java
134 lines
6.2 KiB
Java
package com.yunhai.flowcenter.service;
|
||
|
||
import com.fasterxml.jackson.databind.JsonNode;
|
||
import com.yunhai.flowcenter.entity.FlwUsageGrant;
|
||
import com.yunhai.flowcenter.repository.FlwUsageGrantRepository;
|
||
import com.yunhai.platform.apistd.BizException;
|
||
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
|
||
import java.util.List;
|
||
|
||
/**
|
||
* 应用使用授权管理面(2026-10-25 T01):学校×流程线×稳定主体的授予/撤销/查询。
|
||
*
|
||
* <p>授予时把 appKey 解析成 UserBase 稳定主键 appId 落库(授权对象是主体不是凭据),
|
||
* 并按学校开通事实做发放闸(enforce 开启时未开通拒发)。撤销只置 {@code enabled=0}
|
||
* (不删行,审计与幂等判据保留);撤权只拦新发起,不回写在途实例与已受理命令。</p>
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class FlowUsageGrantAdminService {
|
||
|
||
private final FlwUsageGrantRepository grantRepository;
|
||
private final UserBaseActivationClient activationClient;
|
||
|
||
@Autowired(required = false)
|
||
private OpenApiAppClient appClient;
|
||
|
||
@org.springframework.beans.factory.annotation.Value("${flow-center.formal-command.enabled:false}")
|
||
private boolean formalEnabled;
|
||
|
||
public List<FlwUsageGrant> listByFlow(Long schId, String flowGuid) {
|
||
return grantRepository.findBySchIdAndFlowGuidOrderByUpdateTimeDescIdDesc(schId, flowGuid);
|
||
}
|
||
|
||
public List<FlwUsageGrant> listByApp(Long schId, Integer appId) {
|
||
return grantRepository.findBySchIdAndAppIdOrderByUpdateTimeDescIdDesc(schId, appId);
|
||
}
|
||
|
||
/**
|
||
* 授予/重申(三元键 upsert)。
|
||
*
|
||
* @throws BizException appKey 不可解析为稳定主体(fail-closed,管理面必须授到真实主体上);
|
||
* enforce 开启且应用未在该校开通或核验不可用
|
||
*/
|
||
@Transactional
|
||
public FlwUsageGrant grant(Long schId, String flowGuid, String appKey, String remark, String operator) {
|
||
if (schId == null || schId <= 0 || flowGuid == null || flowGuid.trim().isEmpty()
|
||
|| appKey == null || appKey.trim().isEmpty()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "schId/flowGuid/appKey 必填");
|
||
}
|
||
String cleanKey = appKey.trim();
|
||
JsonNode registered = fetchRegistered(cleanKey);
|
||
Integer appId = registered == null ? null : readId(registered);
|
||
if (appId == null) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||
"appKey 未能在 UserBase 解析出稳定应用主体,拒绝授权:" + cleanKey);
|
||
}
|
||
if (activationClient.isEnforce()
|
||
&& !activationClient.isAppActivatedAtSchool(appId, schId)) {
|
||
throw new BizException(ErrorCode.FORBIDDEN,
|
||
"该应用未在学校 " + schId + " 开通,不能授予流程使用许可");
|
||
}
|
||
FlwUsageGrant grant = grantRepository
|
||
.findBySchIdAndFlowGuidAndAppId(schId, flowGuid.trim(), appId)
|
||
.orElseGet(FlwUsageGrant::new);
|
||
boolean isNew = grant.getId() == null;
|
||
if (isNew) {
|
||
grant.setSchId(schId);
|
||
grant.setFlowGuid(flowGuid.trim());
|
||
grant.setAppId(appId);
|
||
}
|
||
if (formalEnabled) {
|
||
String source = registered.path("sourcePrincipalId").asText("");
|
||
try {
|
||
if (!java.util.UUID.fromString(source).toString().equals(source)) throw new IllegalArgumentException();
|
||
} catch (IllegalArgumentException invalid) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "UserBase 稳定来源未迁移,不能发放正式许可");
|
||
}
|
||
if (!activationClient.isAppActivatedStrict(appId, schId)) throw new BizException(ErrorCode.FORBIDDEN, "学校未开通");
|
||
grant.setSourcePrincipalId(source);
|
||
}
|
||
grant.setAppKeySnapshot(cleanKey);
|
||
grant.setAppNameSnapshot(registered.path("appName").asText(null));
|
||
grant.setEnabled(1);
|
||
grant.setRemark(remark);
|
||
FlwUsageGrant saved = grantRepository.save(grant);
|
||
log.info("[usage-grant] 授予 schId={} flowGuid={} appId={} appKey={} operator={} 新建={}",
|
||
schId, flowGuid, appId, cleanKey, operator, isNew);
|
||
return saved;
|
||
}
|
||
|
||
/** 撤销(enabled=0,行保留);幂等:对已撤销行重复撤销不报错 */
|
||
@Transactional
|
||
public FlwUsageGrant revoke(Long schId, String flowGuid, Integer appId, String remark, String operator) {
|
||
FlwUsageGrant grant = grantRepository
|
||
.findBySchIdAndFlowGuidAndAppId(schId, flowGuid, appId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
|
||
"使用许可不存在:school=" + schId + " flow=" + flowGuid + " app=" + appId));
|
||
grant.setEnabled(0);
|
||
if (remark != null && !remark.trim().isEmpty()) {
|
||
grant.setRemark(remark.trim());
|
||
}
|
||
FlwUsageGrant saved = grantRepository.save(grant);
|
||
log.info("[usage-grant] 撤销 schId={} flowGuid={} appId={} operator={}(只拦新发起,在途不受影响)",
|
||
schId, flowGuid, appId, operator);
|
||
return saved;
|
||
}
|
||
|
||
private JsonNode fetchRegistered(String appKey) {
|
||
if (appClient == null) {
|
||
throw new BizException(ErrorCode.UNAUTHORIZED, "UserBase 应用注册通道未装配,拒绝授权");
|
||
}
|
||
try {
|
||
JsonNode node = appClient.getAppByKey(appKey);
|
||
return node == null || node.isNull() ? null : node;
|
||
} catch (RuntimeException e) {
|
||
log.warn("[usage-grant] UserBase 注册解析失败 appKey={}:{}", appKey, e.toString());
|
||
throw new BizException(ErrorCode.UNAUTHORIZED, "UserBase 应用注册解析不可用,请稍后重试");
|
||
}
|
||
}
|
||
|
||
private Integer readId(JsonNode registered) {
|
||
JsonNode idNode = registered.path("id");
|
||
return idNode.isNumber() ? idNode.intValue() : null;
|
||
}
|
||
}
|