fix #492: add token context selection
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
public record McpReasoningCommand(
|
||||
Long tokenKeyId,
|
||||
long objectId,
|
||||
String bearerToken,
|
||||
int limit,
|
||||
|
||||
@@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
public record ProbeCommand(
|
||||
Long tokenKeyId,
|
||||
@Positive long objectId,
|
||||
@NotBlank String bearerToken,
|
||||
@Min(1) @Max(500) int limit
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.token;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record TokenContextView(
|
||||
long keyId,
|
||||
long userId,
|
||||
String username,
|
||||
String keyPrefix,
|
||||
LocalDateTime expiresAt,
|
||||
LocalDateTime revokedAt,
|
||||
String description,
|
||||
boolean active,
|
||||
List<String> directRoles,
|
||||
List<String> groups,
|
||||
List<String> inheritedRoles
|
||||
) {
|
||||
|
||||
public String statusLabel() {
|
||||
if (active) {
|
||||
return "ACTIVE";
|
||||
}
|
||||
return revokedAt == null ? "EXPIRED" : "REVOKED";
|
||||
}
|
||||
|
||||
public String maskedToken() {
|
||||
if (keyPrefix == null || keyPrefix.isBlank()) {
|
||||
return "****";
|
||||
}
|
||||
return keyPrefix + "****";
|
||||
}
|
||||
|
||||
public String displayLabel() {
|
||||
return "#" + keyId + " / " + username + " / " + statusLabel();
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,27 @@ package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.GroupRoleView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.GroupUserView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.IssuedToken;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.TokenContextView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.TokenIssueCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
|
||||
import com.cloudhandson.vpdbackoffice.domain.user.UserRoleView;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.BearerTokenMapper;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.GroupMapper;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -21,6 +31,7 @@ public class BearerTokenService {
|
||||
|
||||
private final BearerTokenMapper tokenMapper;
|
||||
private final UserMapper userMapper;
|
||||
private final GroupMapper groupMapper;
|
||||
private final AuditService auditService;
|
||||
private final TokenGenerator tokenGenerator;
|
||||
private final TokenHasher tokenHasher;
|
||||
@@ -30,6 +41,7 @@ public class BearerTokenService {
|
||||
public BearerTokenService(
|
||||
BearerTokenMapper tokenMapper,
|
||||
UserMapper userMapper,
|
||||
GroupMapper groupMapper,
|
||||
AuditService auditService,
|
||||
TokenGenerator tokenGenerator,
|
||||
TokenHasher tokenHasher,
|
||||
@@ -38,6 +50,7 @@ public class BearerTokenService {
|
||||
) {
|
||||
this.tokenMapper = tokenMapper;
|
||||
this.userMapper = userMapper;
|
||||
this.groupMapper = groupMapper;
|
||||
this.auditService = auditService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
this.tokenHasher = tokenHasher;
|
||||
@@ -49,6 +62,31 @@ public class BearerTokenService {
|
||||
return tokenMapper.findAll();
|
||||
}
|
||||
|
||||
public List<TokenContextView> findTokenContextOptions() {
|
||||
List<BearerTokenRecord> tokens = tokenMapper.findAll();
|
||||
List<GroupUserView> groupUsers = groupMapper.findGroupUsers();
|
||||
Map<Long, List<String>> directRolesByUser = directRolesByUser();
|
||||
Map<Long, List<String>> groupsByUser = groupsByUser(groupUsers);
|
||||
Map<Long, List<String>> inheritedRolesByUser = inheritedRolesByUser(groupUsers);
|
||||
LocalDateTime now = LocalDateTime.now(clock.withZone(ZoneId.systemDefault()));
|
||||
|
||||
return tokens.stream()
|
||||
.map(token -> new TokenContextView(
|
||||
token.keyId(),
|
||||
token.userId(),
|
||||
token.username(),
|
||||
token.keyPrefix(),
|
||||
token.expiresAt(),
|
||||
token.revokedAt(),
|
||||
token.description(),
|
||||
token.active(now),
|
||||
directRolesByUser.getOrDefault(token.userId(), List.of()),
|
||||
groupsByUser.getOrDefault(token.userId(), List.of()),
|
||||
inheritedRolesByUser.getOrDefault(token.userId(), List.of())
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public BearerTokenRecord findById(long keyId) {
|
||||
return tokenMapper.findById(keyId);
|
||||
}
|
||||
@@ -68,6 +106,45 @@ public class BearerTokenService {
|
||||
return hash.equalsIgnoreCase(record.keyHash());
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> directRolesByUser() {
|
||||
Map<Long, Set<String>> grouped = new LinkedHashMap<>();
|
||||
for (UserRoleView view : userMapper.findUserRoles()) {
|
||||
grouped.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.roleName());
|
||||
}
|
||||
return toListMap(grouped);
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> groupsByUser(List<GroupUserView> groupUsers) {
|
||||
Map<Long, Set<String>> grouped = new LinkedHashMap<>();
|
||||
for (GroupUserView view : groupUsers) {
|
||||
grouped.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>())
|
||||
.add(view.groupCode() + " / " + view.groupName());
|
||||
}
|
||||
return toListMap(grouped);
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> inheritedRolesByUser(List<GroupUserView> groupUsers) {
|
||||
Map<Long, Set<String>> rolesByGroup = new LinkedHashMap<>();
|
||||
for (GroupRoleView view : groupMapper.findGroupRoles()) {
|
||||
rolesByGroup.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.roleName());
|
||||
}
|
||||
|
||||
Map<Long, Set<String>> rolesByUser = new LinkedHashMap<>();
|
||||
for (GroupUserView view : groupUsers) {
|
||||
Set<String> roles = rolesByGroup.getOrDefault(view.groupId(), Set.of());
|
||||
if (!roles.isEmpty()) {
|
||||
rolesByUser.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).addAll(roles);
|
||||
}
|
||||
}
|
||||
return toListMap(rolesByUser);
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> toListMap(Map<Long, Set<String>> source) {
|
||||
Map<Long, List<String>> result = new LinkedHashMap<>();
|
||||
source.forEach((key, value) -> result.put(key, new ArrayList<>(value)));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public IssuedToken issueToken(TokenIssueCommand command) {
|
||||
AppUser user = userMapper.findById(command.userId());
|
||||
|
||||
@@ -41,6 +41,7 @@ public class McpReasoningService {
|
||||
ProtectedObject object = protectedObjectService.assertEnabled(command.objectId());
|
||||
McpToolView tool = toolRegistry.toolFor(object);
|
||||
ProbeResult probeResult = ordsProbeService.runProbe(new ProbeCommand(
|
||||
command.tokenKeyId(),
|
||||
command.objectId(),
|
||||
command.bearerToken(),
|
||||
normalizeLimit(command.limit())
|
||||
|
||||
@@ -111,7 +111,7 @@ public class McpSseService {
|
||||
McpToolView tool = findTool(toolName);
|
||||
String bearerToken = arguments.path("bearerToken").asText("");
|
||||
int limit = normalizeLimit(arguments.path("limit").asInt(50));
|
||||
ProbeResult probeResult = ordsProbeService.runProbe(new ProbeCommand(tool.objectId(), bearerToken, limit));
|
||||
ProbeResult probeResult = ordsProbeService.runProbe(new ProbeCommand(null, tool.objectId(), bearerToken, limit));
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("toolName", tool.name());
|
||||
|
||||
@@ -75,10 +75,23 @@ public class OrdsProbeService {
|
||||
));
|
||||
}
|
||||
|
||||
BearerTokenRecord token = tokenService.findByPlainToken(command.bearerToken());
|
||||
if (token == null) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다."));
|
||||
BearerTokenRecord token;
|
||||
if (command.tokenKeyId() == null) {
|
||||
token = tokenService.findByPlainToken(command.bearerToken());
|
||||
if (token == null) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다."));
|
||||
}
|
||||
} else {
|
||||
token = tokenService.findById(command.tokenKeyId());
|
||||
if (token == null) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.INVALID_TOKEN, "INVALID_TOKEN", "선택한 등록 토큰을 찾을 수 없습니다."));
|
||||
}
|
||||
if (!tokenService.matches(token, command.bearerToken())) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.INVALID_TOKEN, "INVALID_TOKEN", "입력한 Bearer Token 원문이 선택한 등록 토큰과 일치하지 않습니다."));
|
||||
}
|
||||
}
|
||||
if (!token.active(LocalDateTime.now(clock.withZone(ZoneId.systemDefault())))) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
@@ -232,6 +245,9 @@ public class OrdsProbeService {
|
||||
}
|
||||
|
||||
private Long tokenKeyId(ProbeCommand command) {
|
||||
if (command.tokenKeyId() != null) {
|
||||
return command.tokenKeyId();
|
||||
}
|
||||
BearerTokenRecord token = tokenService.findByPlainToken(command.bearerToken());
|
||||
return token == null ? null : token.keyId();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.mcp.McpReasoningCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpReasoningService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpToolRegistry;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
@@ -22,15 +22,18 @@ public class McpReasoningController {
|
||||
private final McpToolRegistry toolRegistry;
|
||||
private final McpReasoningService reasoningService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final BearerTokenService tokenService;
|
||||
|
||||
public McpReasoningController(
|
||||
McpToolRegistry toolRegistry,
|
||||
McpReasoningService reasoningService,
|
||||
ProtectedObjectService protectedObjectService
|
||||
ProtectedObjectService protectedObjectService,
|
||||
BearerTokenService tokenService
|
||||
) {
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.reasoningService = reasoningService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-reasoning")
|
||||
@@ -38,10 +41,12 @@ public class McpReasoningController {
|
||||
try {
|
||||
model.addAttribute("objects", protectedObjectService.findEnabled());
|
||||
model.addAttribute("tools", toolRegistry.listTools());
|
||||
model.addAttribute("tokens", tokenService.findTokenContextOptions());
|
||||
} catch (DataAccessException e) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(e);
|
||||
model.addAttribute("objects", List.of());
|
||||
model.addAttribute("tools", List.of());
|
||||
model.addAttribute("tokens", List.of());
|
||||
model.addAttribute("runtimeError", message);
|
||||
}
|
||||
return "mcp-reasoning";
|
||||
@@ -77,6 +82,7 @@ public class McpReasoningController {
|
||||
|
||||
@PostMapping("/mcp-reasoning")
|
||||
public String reason(
|
||||
@RequestParam(required = false) Long tokenKeyId,
|
||||
@RequestParam long objectId,
|
||||
@RequestParam String bearerToken,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@@ -84,7 +90,7 @@ public class McpReasoningController {
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("result", reasoningService.reason(
|
||||
new McpReasoningCommand(objectId, bearerToken, limit, question)));
|
||||
new McpReasoningCommand(tokenKeyId, objectId, bearerToken, limit, question)));
|
||||
return "fragments/mcp-reasoning-result :: result";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsProbeService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -14,29 +15,34 @@ public class ProbeController {
|
||||
|
||||
private final OrdsProbeService probeService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final BearerTokenService tokenService;
|
||||
|
||||
public ProbeController(
|
||||
OrdsProbeService probeService,
|
||||
ProtectedObjectService protectedObjectService
|
||||
ProtectedObjectService protectedObjectService,
|
||||
BearerTokenService tokenService
|
||||
) {
|
||||
this.probeService = probeService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@GetMapping("/probe")
|
||||
public String probe(Model model) {
|
||||
model.addAttribute("objects", protectedObjectService.findEnabled());
|
||||
model.addAttribute("tokens", tokenService.findTokenContextOptions());
|
||||
return "probe";
|
||||
}
|
||||
|
||||
@PostMapping("/probe")
|
||||
public String run(
|
||||
@RequestParam(required = false) Long tokenKeyId,
|
||||
@RequestParam long objectId,
|
||||
@RequestParam String bearerToken,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("result", probeService.runProbe(new ProbeCommand(objectId, bearerToken, limit)));
|
||||
model.addAttribute("result", probeService.runProbe(new ProbeCommand(tokenKeyId, objectId, bearerToken, limit)));
|
||||
return "fragments/probe-result :: result";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user