package cn.iocoder.yudao.module.system.framework.agent;
|
|
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
|
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.http.HttpStatus;
|
import org.springframework.stereotype.Service;
|
|
import java.nio.charset.StandardCharsets;
|
import java.security.MessageDigest;
|
import java.security.NoSuchAlgorithmException;
|
import java.security.SecureRandom;
|
import java.time.LocalDateTime;
|
import java.util.Base64;
|
import java.util.Set;
|
import java.util.UUID;
|
|
/** 签发、校验和撤销智能体短期委托。Redis 只保存密钥摘要。 */
|
@Service
|
@RequiredArgsConstructor
|
public class SystemAgentDelegationService {
|
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
|
@Value("${yudao.agent.bridge.delegation-ttl-seconds:1800}")
|
private long delegationTtlSeconds;
|
|
private final AgentDelegationStore delegationStore;
|
private final PermissionCommonApi permissionApi;
|
|
public AgentDelegationToken issue(String taskId, String moduleId, Long attachmentId,
|
Set<String> operations, String permission) {
|
return issue(taskId, moduleId, attachmentId == null ? Set.of() : Set.of(attachmentId),
|
operations, permission);
|
}
|
|
public AgentDelegationToken issue(String taskId, String moduleId, Set<Long> attachmentIds,
|
Set<String> operations, String permission) {
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
if (userId == null) {
|
throw new AgentBridgeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "未登录或登录已过期");
|
}
|
if (!permissionApi.hasAnyPermissions(userId, permission)) {
|
throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN", "当前用户缺少委托所需权限");
|
}
|
if (taskId == null || taskId.isBlank() || taskId.length() > 128 || operations == null || operations.isEmpty()
|
|| attachmentIds == null || attachmentIds.size() > 20 || attachmentIds.stream().anyMatch(id -> id == null || id <= 0)) {
|
throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "DELEGATION_SCOPE_INVALID", "委托任务或操作范围无效");
|
}
|
|
String id = UUID.randomUUID().toString();
|
byte[] secretBytes = new byte[32];
|
SECURE_RANDOM.nextBytes(secretBytes);
|
String secret = Base64.getUrlEncoder().withoutPadding().encodeToString(secretBytes);
|
String token = id + "." + secret;
|
long ttlSeconds = Math.max(60, Math.min(delegationTtlSeconds, 3600));
|
LocalDateTime now = LocalDateTime.now();
|
|
AgentDelegation delegation = new AgentDelegation();
|
delegation.setId(id);
|
delegation.setSecretHash(sha256(secret));
|
delegation.setTaskId(taskId);
|
delegation.setUserId(userId);
|
delegation.setModuleId(moduleId);
|
delegation.setAttachmentId(attachmentIds.size() == 1 ? attachmentIds.iterator().next() : null);
|
delegation.setAttachmentIds(Set.copyOf(attachmentIds));
|
delegation.setOperations(Set.copyOf(operations));
|
delegation.setPermission(permission);
|
delegation.setIssuedAt(now);
|
delegation.setExpiresAt(now.plusSeconds(ttlSeconds));
|
delegationStore.save(delegation, ttlSeconds);
|
return new AgentDelegationToken(id, token, delegation.getExpiresAt());
|
}
|
|
public AgentPrincipal require(String token, String taskId, String operation, String permission,
|
String moduleId, Long attachmentId) {
|
String[] tokenParts = token == null ? new String[0] : token.split("\\.", 2);
|
if (tokenParts.length != 2) {
|
throw invalidToken();
|
}
|
AgentDelegation delegation = delegationStore.get(tokenParts[0]);
|
if (delegation == null || delegation.getExpiresAt() == null
|
|| !delegation.getExpiresAt().isAfter(LocalDateTime.now())
|
|| !constantTimeEquals(delegation.getSecretHash(), sha256(tokenParts[1]))) {
|
throw invalidToken();
|
}
|
if (!delegation.getTaskId().equals(taskId) || !delegation.getOperations().contains(operation)
|
|| !delegation.getPermission().equals(permission)
|
|| (delegation.getModuleId() != null && !delegation.getModuleId().equals(moduleId))
|
|| !allowsAttachment(delegation, attachmentId)) {
|
throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_SCOPE_MISMATCH", "委托票据不允许当前操作");
|
}
|
if (!permissionApi.hasAnyPermissions(delegation.getUserId(), permission)) {
|
throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_PERMISSION_REVOKED", "委托用户权限已变更");
|
}
|
LoginUser loginUser = new LoginUser().setId(delegation.getUserId())
|
.setUserType(UserTypeEnum.ADMIN.getValue())
|
.setExpiresTime(delegation.getExpiresAt());
|
SecurityFrameworkUtils.setLoginUser(loginUser, ServletUtils.getRequest());
|
return new AgentPrincipal(delegation.getUserId(), "delegated:" + delegation.getId());
|
}
|
|
private static boolean allowsAttachment(AgentDelegation delegation, Long attachmentId) {
|
if (delegation.getAttachmentIds() != null && !delegation.getAttachmentIds().isEmpty()) {
|
return attachmentId != null && delegation.getAttachmentIds().contains(attachmentId);
|
}
|
return delegation.getAttachmentId() == null || delegation.getAttachmentId().equals(attachmentId);
|
}
|
|
public void revoke(String delegationId, Long currentUserId) {
|
AgentDelegation delegation = delegationStore.get(delegationId);
|
if (delegation == null) {
|
return;
|
}
|
if (!delegation.getUserId().equals(currentUserId)) {
|
throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_FORBIDDEN", "无权撤销该委托票据");
|
}
|
delegationStore.delete(delegationId);
|
}
|
|
private static AgentBridgeException invalidToken() {
|
return new AgentBridgeException(HttpStatus.UNAUTHORIZED, "DELEGATION_INVALID", "委托票据无效、已撤销或已过期");
|
}
|
|
private static boolean constantTimeEquals(String left, String right) {
|
return left != null && right != null && MessageDigest.isEqual(left.getBytes(StandardCharsets.US_ASCII),
|
right.getBytes(StandardCharsets.US_ASCII));
|
}
|
|
private static String sha256(String value) {
|
try {
|
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
} catch (NoSuchAlgorithmException ex) {
|
throw new IllegalStateException("SHA-256 unavailable", ex);
|
}
|
}
|
}
|