fix #557: guide permission-driven VPD flow
This commit is contained in:
@@ -2,7 +2,6 @@ package com.cloudhandson.vpdbackoffice.domain.mcp;
|
||||
|
||||
public record McpReasoningCommand(
|
||||
Long tokenKeyId,
|
||||
long objectId,
|
||||
String bearerToken,
|
||||
int limit,
|
||||
String question
|
||||
|
||||
@@ -9,6 +9,11 @@ public record ProbeCommand(
|
||||
Long tokenKeyId,
|
||||
@Positive long objectId,
|
||||
@NotBlank String bearerToken,
|
||||
@Min(1) @Max(500) int limit
|
||||
@Min(1) @Max(500) int limit,
|
||||
String requestBody
|
||||
) {
|
||||
|
||||
public ProbeCommand(Long tokenKeyId, long objectId, String bearerToken, int limit) {
|
||||
this(tokenKeyId, objectId, bearerToken, limit, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.protectedobject;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public record ProtectedColumn(
|
||||
long columnId,
|
||||
long objectId,
|
||||
@@ -20,4 +22,28 @@ public record ProtectedColumn(
|
||||
String method = redactionMethod == null || redactionMethod.isBlank() ? "NONE" : redactionMethod;
|
||||
return level + "/" + method;
|
||||
}
|
||||
|
||||
public String sensitivityLabel() {
|
||||
String level = sensitivityLevel == null || sensitivityLevel.isBlank() ? "PUBLIC" : sensitivityLevel;
|
||||
return switch (level.toUpperCase(Locale.ROOT)) {
|
||||
case "INTERNAL" -> "내부용";
|
||||
case "CONFIDENTIAL" -> "기밀";
|
||||
case "RESTRICTED" -> "제한";
|
||||
default -> "기본 표시";
|
||||
};
|
||||
}
|
||||
|
||||
public String redactionLabel() {
|
||||
String method = redactionMethod == null || redactionMethod.isBlank() ? "NONE" : redactionMethod;
|
||||
return switch (method.toUpperCase(Locale.ROOT)) {
|
||||
case "NULLIFY" -> "값 숨김(NULL)";
|
||||
case "PARTIAL" -> "일부 숨김";
|
||||
case "FULL" -> "전체 숨김";
|
||||
default -> "마스킹 없음";
|
||||
};
|
||||
}
|
||||
|
||||
public String displayPolicyLabel() {
|
||||
return sensitive() ? sensitivityLabel() + " · " + redactionLabel() : "기본 표시 · 마스킹 없음";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,14 @@ public record ProtectedObject(
|
||||
String owner,
|
||||
String objectName,
|
||||
String ordsPath,
|
||||
String enabledYn
|
||||
String enabledYn,
|
||||
String description
|
||||
) {
|
||||
|
||||
public ProtectedObject(long objectId, String owner, String objectName, String ordsPath, String enabledYn) {
|
||||
this(objectId, owner, objectName, ordsPath, enabledYn, null);
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return "Y".equalsIgnoreCase(enabledYn);
|
||||
}
|
||||
@@ -15,4 +20,10 @@ public record ProtectedObject(
|
||||
public String displayName() {
|
||||
return owner + "." + objectName;
|
||||
}
|
||||
|
||||
public String descriptionOrDefault() {
|
||||
return description == null || description.isBlank()
|
||||
? "등록된 DB 객체를 권한체계와 ORDS로 조회하는 대상"
|
||||
: description;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ public record ProtectedObjectCreateCommand(
|
||||
@NotBlank String objectName,
|
||||
@NotBlank String ordsPath,
|
||||
String columns,
|
||||
String sensitiveColumns
|
||||
String sensitiveColumns,
|
||||
String description
|
||||
) {
|
||||
|
||||
public ProtectedObjectCreateCommand(
|
||||
String owner,
|
||||
String objectName,
|
||||
String ordsPath,
|
||||
String columns,
|
||||
String sensitiveColumns
|
||||
) {
|
||||
this(owner, objectName, ordsPath, columns, sensitiveColumns, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public record VpdTargetView(
|
||||
String objectType,
|
||||
String protectedYn,
|
||||
String ordsPath,
|
||||
String description,
|
||||
int policyCount,
|
||||
String policyNames,
|
||||
String filterNames
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
@Mapper
|
||||
public interface BearerTokenMapper {
|
||||
|
||||
List<BearerTokenRecord> findAll();
|
||||
List<BearerTokenRecord> findAll(@Param("includeInactive") int includeInactive);
|
||||
|
||||
BearerTokenRecord findById(@Param("keyId") long keyId);
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ public interface ProtectedObjectMapper {
|
||||
|
||||
int updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath);
|
||||
|
||||
int updateDescription(@Param("objectId") long objectId, @Param("description") String description);
|
||||
|
||||
int enableObject(@Param("objectId") long objectId);
|
||||
|
||||
int disableObject(@Param("objectId") long objectId);
|
||||
|
||||
@@ -54,4 +54,28 @@ public interface VpdPolicyMapper {
|
||||
@Param("objectName") String objectName,
|
||||
@Param("objectType") String objectType
|
||||
);
|
||||
|
||||
String findPolicyDescription(
|
||||
@Param("objectOwner") String objectOwner,
|
||||
@Param("objectName") String objectName,
|
||||
@Param("policyName") String policyName
|
||||
);
|
||||
|
||||
String findFilterDescription(
|
||||
@Param("functionOwner") String functionOwner,
|
||||
@Param("functionName") String functionName
|
||||
);
|
||||
|
||||
int upsertPolicyDescription(
|
||||
@Param("objectOwner") String objectOwner,
|
||||
@Param("objectName") String objectName,
|
||||
@Param("policyName") String policyName,
|
||||
@Param("description") String description
|
||||
);
|
||||
|
||||
int upsertFilterDescription(
|
||||
@Param("functionOwner") String functionOwner,
|
||||
@Param("functionName") String functionName,
|
||||
@Param("description") String description
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,8 @@ public class BackofficeSchemaService {
|
||||
owner VARCHAR2(128) NOT NULL,
|
||||
object_name VARCHAR2(128) NOT NULL UNIQUE,
|
||||
ords_path VARCHAR2(300) NOT NULL,
|
||||
enabled_yn CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL
|
||||
enabled_yn CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL,
|
||||
description VARCHAR2(200)
|
||||
)
|
||||
"""),
|
||||
new TableDefinition("CB_PROTECTED_COLUMN", """
|
||||
@@ -148,6 +149,25 @@ public class BackofficeSchemaService {
|
||||
setting_value VARCHAR2(1000),
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
)
|
||||
"""),
|
||||
new TableDefinition("CB_VPD_POLICY_NOTE", """
|
||||
CREATE TABLE cb_vpd_policy_note (
|
||||
object_owner VARCHAR2(128) NOT NULL,
|
||||
object_name VARCHAR2(128) NOT NULL,
|
||||
policy_name VARCHAR2(128) NOT NULL,
|
||||
description VARCHAR2(500) NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT cb_vpd_policy_note_pk PRIMARY KEY (object_owner, object_name, policy_name)
|
||||
)
|
||||
"""),
|
||||
new TableDefinition("CB_VPD_FILTER_NOTE", """
|
||||
CREATE TABLE cb_vpd_filter_note (
|
||||
function_owner VARCHAR2(128) NOT NULL,
|
||||
function_name VARCHAR2(128) NOT NULL,
|
||||
description VARCHAR2(500) NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT cb_vpd_filter_note_pk PRIMARY KEY (function_owner, function_name)
|
||||
)
|
||||
""")
|
||||
);
|
||||
|
||||
@@ -160,6 +180,8 @@ public class BackofficeSchemaService {
|
||||
"ALTER TABLE cb_permission_rule ADD (rule_column VARCHAR2(128))"),
|
||||
new ColumnDefinition("CB_AGENT_BEARER_KEY", "DESCRIPTION",
|
||||
"ALTER TABLE cb_agent_bearer_key ADD (description VARCHAR2(200))"),
|
||||
new ColumnDefinition("CB_PROTECTED_OBJECT", "DESCRIPTION",
|
||||
"ALTER TABLE cb_protected_object ADD (description VARCHAR2(200))"),
|
||||
new ColumnDefinition("CB_PROTECTED_COLUMN", "SENSITIVITY_LEVEL",
|
||||
"ALTER TABLE cb_protected_column ADD (sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL)"),
|
||||
new ColumnDefinition("CB_PROTECTED_COLUMN", "REDACTION_METHOD",
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
@@ -59,11 +60,15 @@ public class BearerTokenService {
|
||||
}
|
||||
|
||||
public List<BearerTokenRecord> findAll() {
|
||||
return tokenMapper.findAll();
|
||||
return findAll(false);
|
||||
}
|
||||
|
||||
public List<BearerTokenRecord> findAll(boolean includeInactive) {
|
||||
return tokenMapper.findAll(includeInactive ? 1 : 0);
|
||||
}
|
||||
|
||||
public List<TokenContextView> findTokenContextOptions() {
|
||||
List<BearerTokenRecord> tokens = tokenMapper.findAll();
|
||||
List<BearerTokenRecord> tokens = findAll(true);
|
||||
List<GroupUserView> groupUsers = groupMapper.findGroupUsers();
|
||||
Map<Long, List<String>> directRolesByUser = directRolesByUser();
|
||||
Map<Long, List<String>> groupsByUser = groupsByUser(groupUsers);
|
||||
@@ -196,6 +201,13 @@ public class BearerTokenService {
|
||||
return new IssuedToken(keyId, prefix, plainToken, command.expiresAt());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public IssuedToken issueTemporaryToken(long userId, String purpose) {
|
||||
OffsetDateTime expiresAt = OffsetDateTime.now(clock).plus(Duration.ofMinutes(10));
|
||||
return issueToken(new TokenIssueCommand(userId, expiresAt,
|
||||
purpose == null || purpose.isBlank() ? "임시 검증 토큰" : purpose));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void revokeToken(long keyId, String reason) {
|
||||
int updated = tokenMapper.revokeToken(
|
||||
|
||||
@@ -6,10 +6,11 @@ import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -17,20 +18,17 @@ public class McpReasoningService {
|
||||
|
||||
private static final int MAX_EVIDENCE_ROWS = 20;
|
||||
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final OrdsProbeService ordsProbeService;
|
||||
private final McpToolRegistry toolRegistry;
|
||||
private final OpenAiCompatibleClient aiClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public McpReasoningService(
|
||||
ProtectedObjectService protectedObjectService,
|
||||
OrdsProbeService ordsProbeService,
|
||||
McpToolRegistry toolRegistry,
|
||||
OpenAiCompatibleClient aiClient,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.ordsProbeService = ordsProbeService;
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.aiClient = aiClient;
|
||||
@@ -38,11 +36,10 @@ public class McpReasoningService {
|
||||
}
|
||||
|
||||
public McpReasoningResult reason(McpReasoningCommand command) {
|
||||
ProtectedObject object = protectedObjectService.assertEnabled(command.objectId());
|
||||
McpToolView tool = toolRegistry.toolFor(object);
|
||||
McpToolView tool = selectTool(command.question());
|
||||
ProbeResult probeResult = ordsProbeService.runProbe(new ProbeCommand(
|
||||
command.tokenKeyId(),
|
||||
command.objectId(),
|
||||
tool.objectId(),
|
||||
command.bearerToken(),
|
||||
normalizeLimit(command.limit())
|
||||
));
|
||||
@@ -75,6 +72,28 @@ public class McpReasoningService {
|
||||
}
|
||||
}
|
||||
|
||||
private McpToolView selectTool(String question) {
|
||||
List<McpToolView> tools = toolRegistry.listTools();
|
||||
if (tools.isEmpty()) {
|
||||
throw new AppException("사용 가능한 MCP tool이 없습니다. ORDS 조회 대상을 먼저 등록하세요.");
|
||||
}
|
||||
String normalized = question == null ? "" : question.toLowerCase(Locale.ROOT);
|
||||
return tools.stream()
|
||||
.max(Comparator.comparingInt(tool -> score(tool, normalized)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private int score(McpToolView tool, String question) {
|
||||
int score = 0;
|
||||
for (String token : (tool.name() + " " + tool.displayName() + " " + tool.description())
|
||||
.toLowerCase(Locale.ROOT).split("[^a-z0-9가-힣]+")) {
|
||||
if (token.length() >= 2 && question.contains(token)) {
|
||||
score += 1;
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private int normalizeLimit(int limit) {
|
||||
if (limit < 1) {
|
||||
return 50;
|
||||
|
||||
@@ -73,14 +73,14 @@ public class McpSseService {
|
||||
ObjectNode item = objectMapper.createObjectNode();
|
||||
item.put("name", tool.name());
|
||||
item.put("description", tool.description());
|
||||
item.set("inputSchema", inputSchema());
|
||||
item.set("inputSchema", inputSchema(tool));
|
||||
tools.add(item);
|
||||
}
|
||||
result.set("tools", tools);
|
||||
return result;
|
||||
}
|
||||
|
||||
private ObjectNode inputSchema() {
|
||||
private ObjectNode inputSchema(McpToolView tool) {
|
||||
ObjectNode schema = objectMapper.createObjectNode();
|
||||
schema.put("type", "object");
|
||||
ObjectNode properties = objectMapper.createObjectNode();
|
||||
@@ -100,6 +100,17 @@ public class McpSseService {
|
||||
schema.set("properties", properties);
|
||||
ArrayNode required = objectMapper.createArrayNode();
|
||||
required.add("bearerToken");
|
||||
if (isVectorTool(tool)) {
|
||||
ObjectNode embedding = objectMapper.createObjectNode();
|
||||
embedding.put("type", "array");
|
||||
embedding.put("description", "외부 임베딩 모델이 만든 검색 벡터. 이 예제 fixture는 4차원입니다.");
|
||||
ObjectNode items = objectMapper.createObjectNode();
|
||||
items.put("type", "number");
|
||||
embedding.set("items", items);
|
||||
embedding.put("minItems", 1);
|
||||
properties.set("embedding", embedding);
|
||||
required.add("embedding");
|
||||
}
|
||||
schema.set("required", required);
|
||||
schema.put("additionalProperties", false);
|
||||
return schema;
|
||||
@@ -111,7 +122,18 @@ 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(null, tool.objectId(), bearerToken, limit));
|
||||
String requestBody = null;
|
||||
if (isVectorTool(tool)) {
|
||||
JsonNode embedding = arguments.get("embedding");
|
||||
if (embedding == null || !embedding.isArray() || embedding.isEmpty()) {
|
||||
throw new AppException("벡터 검색 tool에는 embedding 배열이 필요합니다.");
|
||||
}
|
||||
ObjectNode body = objectMapper.createObjectNode();
|
||||
body.set("embedding", embedding);
|
||||
requestBody = body.toString();
|
||||
}
|
||||
ProbeResult probeResult = ordsProbeService.runProbe(
|
||||
new ProbeCommand(null, tool.objectId(), bearerToken, limit, requestBody));
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("toolName", tool.name());
|
||||
@@ -149,6 +171,10 @@ public class McpSseService {
|
||||
.orElseThrow(() -> new AppException("MCP tool을 찾을 수 없습니다: " + toolName));
|
||||
}
|
||||
|
||||
private boolean isVectorTool(McpToolView tool) {
|
||||
return tool != null && tool.displayName().toUpperCase().endsWith("CB_VECTOR_SEARCH_DOCUMENTS");
|
||||
}
|
||||
|
||||
private int normalizeLimit(int limit) {
|
||||
if (limit < 1) {
|
||||
return 50;
|
||||
|
||||
@@ -27,9 +27,13 @@ public class McpToolRegistry {
|
||||
|
||||
private McpToolView toTool(ProtectedObject object) {
|
||||
String name = "ords.query." + safeName(object.owner()) + "." + safeName(object.objectName());
|
||||
String vectorHint = "CB_VECTOR_SEARCH_DOCUMENTS".equalsIgnoreCase(object.objectName())
|
||||
? " embedding[]을 입력으로 받는 전용 벡터 검색 Handler입니다."
|
||||
: "";
|
||||
return new McpToolView(
|
||||
name,
|
||||
object.displayName() + " 보호 객체를 Bearer Token으로 ORDS 호출해 VPD/Redaction 적용 결과를 조회합니다.",
|
||||
object.descriptionOrDefault() + " · "
|
||||
+ object.displayName() + "을 Bearer Token으로 ORDS 호출해 VPD/표시 보호 결과를 조회합니다." + vectorHint,
|
||||
object.objectId(),
|
||||
object.displayName(),
|
||||
object.ordsPath()
|
||||
|
||||
@@ -20,6 +20,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class OrdsMetadataService {
|
||||
|
||||
private static final String VECTOR_SEARCH_OBJECT = "CB_VECTOR_SEARCH_DOCUMENTS";
|
||||
|
||||
private final OrdsMetadataMapper mapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final JdbcTemplate ordsMetadataJdbcTemplate;
|
||||
@@ -77,6 +79,7 @@ public class OrdsMetadataService {
|
||||
|
||||
public String objectQueryHandlerSource(long objectId) {
|
||||
ProtectedObject object = protectedObjectService.assertEnabled(objectId);
|
||||
rejectGenericVectorHandler(object);
|
||||
List<String> columns = protectedObjectService.findColumns(objectId).stream()
|
||||
.map(ProtectedColumn::columnName)
|
||||
.toList();
|
||||
@@ -138,6 +141,7 @@ public class OrdsMetadataService {
|
||||
@Transactional
|
||||
public OrdsObjectHandlerResult createObjectQueryHandler(long objectId) {
|
||||
ProtectedObject object = protectedObjectService.assertEnabled(objectId);
|
||||
rejectGenericVectorHandler(object);
|
||||
List<String> columns = protectedObjectService.findColumns(objectId).stream()
|
||||
.map(ProtectedColumn::columnName)
|
||||
.toList();
|
||||
@@ -263,6 +267,14 @@ public class OrdsMetadataService {
|
||||
""".formatted(selectColumns, object.owner(), object.objectName());
|
||||
}
|
||||
|
||||
private void rejectGenericVectorHandler(ProtectedObject object) {
|
||||
if (VECTOR_SEARCH_OBJECT.equalsIgnoreCase(object.objectName())) {
|
||||
throw new AppException(
|
||||
"벡터 검색 객체는 일반 Handler로 등록할 수 없습니다. "
|
||||
+ "29_agent_ords_vector_search_ords.sql의 전용 Handler를 사용하세요(EMBEDDING 미노출).");
|
||||
}
|
||||
}
|
||||
|
||||
private String handlerSourceTypeCode(String sourceType) {
|
||||
String normalized = sourceType == null ? "" : sourceType.toLowerCase(Locale.ROOT);
|
||||
if (normalized.contains("query")) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Set;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
@@ -114,10 +115,14 @@ public class OrdsProbeService {
|
||||
URI uri = buildUri(ordsBaseUrl, object.ordsPath(), command.limit());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(command.bearerToken());
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
String requestBody = command.requestBody() == null || command.requestBody().isBlank()
|
||||
? "{}"
|
||||
: command.requestBody().trim();
|
||||
requestHeaders = prettyHeaders(maskedRequestHeaders(headers));
|
||||
requestPayload = prettyJson("{}");
|
||||
requestPayload = prettyJson(requestBody);
|
||||
ResponseEntity<String> response = ordsRestTemplate.exchange(
|
||||
uri, HttpMethod.POST, new HttpEntity<>(headers), String.class);
|
||||
uri, HttpMethod.POST, new HttpEntity<>(requestBody, headers), String.class);
|
||||
ProbeResult result = parseSuccess(
|
||||
response.getBody(),
|
||||
object.objectId(),
|
||||
|
||||
@@ -19,9 +19,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class PermissionService {
|
||||
|
||||
private static final Set<String> RULE_TYPES = Set.of("ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of("=", "!=", "DEPT", "EMP_NO");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of("MY_DEPT", "SELF", "DEPT", "EMP_NO");
|
||||
private static final Set<String> RULE_TYPES = Set.of(
|
||||
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
|
||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG");
|
||||
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
||||
private static final Set<String> SENSITIVITY_LEVELS = Set.of(
|
||||
"PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED");
|
||||
@@ -124,12 +127,13 @@ public class PermissionService {
|
||||
|
||||
permissionMapper.deleteRules(permissionId);
|
||||
for (RuleCommand rule : command.rules()) {
|
||||
String type = normalize(rule.ruleType());
|
||||
permissionMapper.insertRule(new PermissionRule(
|
||||
permissionMapper.nextRuleId(),
|
||||
permissionId,
|
||||
normalizeNullable(rule.ruleColumn()),
|
||||
normalize(rule.ruleType()),
|
||||
clean(rule.ruleValue())
|
||||
type,
|
||||
normalizeRuleValue(type, rule.ruleValue())
|
||||
));
|
||||
}
|
||||
|
||||
@@ -191,16 +195,23 @@ public class PermissionService {
|
||||
if (column != null && !allowedColumns.contains(column)) {
|
||||
throw new AppException("행 규칙 컬럼은 보호 객체 컬럼이어야 합니다: " + column);
|
||||
}
|
||||
if (!seen.add(column + ":" + type + ":" + clean(rule.ruleValue()))) {
|
||||
String ruleValue = normalizeRuleValue(type, rule.ruleValue());
|
||||
if (!seen.add(column + ":" + type + ":" + ruleValue)) {
|
||||
throw new AppException("중복된 행 규칙이 있습니다.");
|
||||
}
|
||||
hasAll = hasAll || "ALL".equals(type);
|
||||
if (!"ALL".equals(type) && column == null && !DEFAULT_COLUMN_RULE_TYPES.contains(type)) {
|
||||
throw new AppException(type + " 규칙에는 컬럼이 필요합니다.");
|
||||
}
|
||||
if (VALUE_REQUIRED_RULE_TYPES.contains(type) && clean(rule.ruleValue()).isBlank()) {
|
||||
if ("TAG".equals(type) && column == null && !allowedColumns.contains("TECH_TAG")) {
|
||||
throw new AppException("TAG 규칙의 기본 컬럼 TECH_TAG가 보호 객체에 없습니다. 컬럼을 지정하세요.");
|
||||
}
|
||||
if (VALUE_REQUIRED_RULE_TYPES.contains(type) && ruleValue.isBlank()) {
|
||||
throw new AppException(type + " 규칙에는 값이 필요합니다.");
|
||||
}
|
||||
if ("TAG".equals(type) && !ruleValue.matches("[A-Z0-9_-]+")) {
|
||||
throw new AppException("TAG 값은 영문 대문자, 숫자, '_' 또는 '-'만 사용할 수 있습니다.");
|
||||
}
|
||||
}
|
||||
if (hasAll && rules.size() > 1) {
|
||||
throw new AppException("ALL 규칙은 다른 규칙과 함께 저장할 수 없습니다.");
|
||||
@@ -255,6 +266,11 @@ public class PermissionService {
|
||||
return cleaned.isBlank() ? null : cleaned.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeRuleValue(String type, String value) {
|
||||
String cleaned = clean(value);
|
||||
return "TAG".equals(type) ? cleaned.toUpperCase(Locale.ROOT) : cleaned;
|
||||
}
|
||||
|
||||
private String clean(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
@@ -137,7 +137,8 @@ public class ProtectedObjectService {
|
||||
normalizedObjectName,
|
||||
defaultOrdsPath(normalizedOwner, normalizedObjectName),
|
||||
String.join(",", columns),
|
||||
""
|
||||
"",
|
||||
"DB 객체를 한 개의 ORDS 조회 Handler로 노출하는 예제 대상"
|
||||
);
|
||||
long objectId = mapper.nextObjectId();
|
||||
mapper.insertObject(objectId, command);
|
||||
@@ -170,13 +171,20 @@ public class ProtectedObjectService {
|
||||
String objectName = command.objectName().trim().toUpperCase(Locale.ROOT);
|
||||
String ordsPath = command.ordsPath();
|
||||
if (ordsPath == null || ordsPath.isBlank()) {
|
||||
throw new AppException("ORDS Path는 실제 ORDS module/template 경로를 입력해야 합니다.");
|
||||
ordsPath = defaultOrdsPath(owner, objectName);
|
||||
}
|
||||
String columns = command.columns();
|
||||
if (columns == null || columns.isBlank()) {
|
||||
columns = String.join(",", mapper.findDatabaseColumns(owner, objectName));
|
||||
}
|
||||
return new ProtectedObjectCreateCommand(owner, objectName, ordsPath.trim(), columns, command.sensitiveColumns());
|
||||
return new ProtectedObjectCreateCommand(
|
||||
owner,
|
||||
objectName,
|
||||
ordsPath.trim(),
|
||||
columns,
|
||||
command.sensitiveColumns(),
|
||||
command.description()
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -192,6 +200,21 @@ public class ProtectedObjectService {
|
||||
ordsPath.trim()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateDescription(long objectId, String description) {
|
||||
String normalized = description == null ? "" : description.trim();
|
||||
if (normalized.length() > 200) {
|
||||
throw new AppException("조회 대상 설명은 200자 이내여야 합니다.");
|
||||
}
|
||||
int updated = mapper.updateDescription(objectId, normalized.isBlank() ? null : normalized);
|
||||
if (updated == 0) {
|
||||
throw new AppException("설명을 수정할 조회 대상을 찾을 수 없습니다.");
|
||||
}
|
||||
databaseObjectsCache = null;
|
||||
auditService.record(new AuditEvent("PROTECTED_OBJECT_DESCRIPTION_UPDATED", null, objectId, "SUCCESS", null, null,
|
||||
normalized));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateColumnPolicy(long columnId, String sensitivityLevel, String redactionMethod) {
|
||||
String normalizedLevel = normalizeOption(sensitivityLevel, "PUBLIC", SENSITIVITY_LEVELS, "민감도 등급");
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -32,6 +34,9 @@ public class VpdPolicyService {
|
||||
private static final long CATALOG_CACHE_MILLIS = 60_000L;
|
||||
private static final String COMMON_POLICY_NAME = "CB_PERMISSION_SELECT_POLICY";
|
||||
private static final String DEFAULT_PERMISSION_FILTER_FUNCTION = "CB_AGENT_DOC_VPD_FILTER";
|
||||
private static final Pattern RETURN_LITERAL = Pattern.compile(
|
||||
"(?is)\\bRETURN\\s+'((?:''|[^'])*)'\\s*;"
|
||||
);
|
||||
|
||||
private final VpdPolicyMapper mapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
@@ -94,6 +99,10 @@ public class VpdPolicyService {
|
||||
);
|
||||
}
|
||||
|
||||
public String currentUser() {
|
||||
return jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveFilterFunction(String functionOwnerValue, String functionNameValue, String filterPredicateValue) {
|
||||
String functionName = requiredIdentifier(functionNameValue, "Function name");
|
||||
@@ -275,6 +284,75 @@ public class VpdPolicyService {
|
||||
return new VpdPolicyDetail(policy, buildAddPolicyBlock(policy));
|
||||
}
|
||||
|
||||
public String findPolicyDescription(String objectOwner, String objectName, String policyName) {
|
||||
String description;
|
||||
try {
|
||||
description = mapper.findPolicyDescription(objectOwner, objectName, policyName);
|
||||
} catch (DataAccessException ignored) {
|
||||
description = null;
|
||||
}
|
||||
return description == null || description.isBlank()
|
||||
? objectOwner + "." + objectName + "에 요청마다 현재 권한체계의 행 접근 조건을 적용하는 " + policyName + " policy입니다."
|
||||
: description;
|
||||
}
|
||||
|
||||
public String findFilterDescription(String functionOwner, String functionName) {
|
||||
String description;
|
||||
try {
|
||||
description = mapper.findFilterDescription(functionOwner, functionName);
|
||||
} catch (DataAccessException ignored) {
|
||||
description = null;
|
||||
}
|
||||
return description == null || description.isBlank()
|
||||
? (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
|
||||
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
|
||||
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.")
|
||||
: description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the literal predicate used by a simple standalone Filter function created by this UI.
|
||||
* Packaged or system-managed functions intentionally return an empty string because their
|
||||
* source is not a single editable literal.
|
||||
*/
|
||||
public String findFilterPredicate(String owner, String packageName, String functionName) {
|
||||
if (packageName != null && !packageName.isBlank()
|
||||
|| DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
VpdFunctionSource source = findFunctionSource(owner, null, functionName);
|
||||
if (source.source() == null) {
|
||||
return "";
|
||||
}
|
||||
Matcher matcher = RETURN_LITERAL.matcher(source.source());
|
||||
return matcher.find() ? matcher.group(1).replace("''", "'") : "";
|
||||
} catch (DataAccessException | AppException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void savePolicyDescription(String objectOwner, String objectName, String policyName, String description) {
|
||||
String normalized = normalizeDescription(description);
|
||||
mapper.upsertPolicyDescription(
|
||||
requiredIdentifier(objectOwner, "Object owner"),
|
||||
requiredIdentifier(objectName, "Object name"),
|
||||
requiredIdentifier(policyName, "Policy name"),
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveFilterDescription(String functionOwner, String functionName, String description) {
|
||||
String normalized = normalizeDescription(description);
|
||||
mapper.upsertFilterDescription(
|
||||
requiredIdentifier(functionOwner, "Function owner"),
|
||||
requiredIdentifier(functionName, "Function name"),
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
public VpdObjectFilterDetail findObjectFilterDetail(String objectOwner, String objectName) {
|
||||
String normalizedOwner = requiredIdentifier(objectOwner, "Object owner");
|
||||
String normalizedObject = requiredIdentifier(objectName, "Object name");
|
||||
@@ -611,6 +689,17 @@ public class VpdPolicyService {
|
||||
return String.join(",", statements);
|
||||
}
|
||||
|
||||
private String normalizeDescription(String value) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
if (normalized.isBlank()) {
|
||||
throw new AppException("정책/필터 설명은 필수입니다.");
|
||||
}
|
||||
if (normalized.length() > 500) {
|
||||
throw new AppException("정책/필터 설명은 500자 이내여야 합니다.");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String escapeSqlLiteral(String value) {
|
||||
return value.replace("'", "''");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.McpChatbotService;
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -13,13 +17,27 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
public class McpChatbotController {
|
||||
|
||||
private final McpChatbotService chatbotService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public McpChatbotController(McpChatbotService chatbotService) {
|
||||
public McpChatbotController(
|
||||
McpChatbotService chatbotService,
|
||||
BearerTokenService tokenService,
|
||||
UserMapper userMapper
|
||||
) {
|
||||
this.chatbotService = chatbotService;
|
||||
this.tokenService = tokenService;
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-chatbot")
|
||||
public String page() {
|
||||
public String page(Model model) {
|
||||
try {
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
} catch (DataAccessException exception) {
|
||||
model.addAttribute("users", List.of());
|
||||
model.addAttribute("runtimeError", RuntimeErrorMessages.dataAccess(exception));
|
||||
}
|
||||
return "mcp-chatbot";
|
||||
}
|
||||
|
||||
@@ -28,6 +46,7 @@ public class McpChatbotController {
|
||||
@RequestParam(defaultValue = "vpd-live") String contextPath,
|
||||
@RequestParam(defaultValue = "") String question,
|
||||
@RequestParam(defaultValue = "") String bearerToken,
|
||||
@RequestParam(required = false) Long tempUserId,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
HttpServletRequest request,
|
||||
Model model
|
||||
@@ -38,7 +57,20 @@ public class McpChatbotController {
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
model.addAttribute("result", chatbotService.chat(serverOrigin, contextPath, question, bearerToken, limit));
|
||||
String effectiveToken = bearerToken;
|
||||
Long temporaryKeyId = null;
|
||||
if (tempUserId != null) {
|
||||
var issued = tokenService.issueTemporaryToken(tempUserId, "MCP Chatbot 임시 실행");
|
||||
effectiveToken = issued.plainToken();
|
||||
temporaryKeyId = issued.keyId();
|
||||
}
|
||||
try {
|
||||
model.addAttribute("result", chatbotService.chat(serverOrigin, contextPath, question, effectiveToken, limit));
|
||||
} finally {
|
||||
if (temporaryKeyId != null) {
|
||||
tokenService.revokeToken(temporaryKeyId, "temporary chatbot completed");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
model.addAttribute("errorMessage", e.getMessage());
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
@Controller
|
||||
@@ -31,10 +30,6 @@ public class McpClientDemoController {
|
||||
|
||||
@PostMapping("/mcp-client-demo")
|
||||
public String run(
|
||||
@RequestParam(defaultValue = "vpd-live") String contextPath,
|
||||
@RequestParam(defaultValue = "") String toolName,
|
||||
@RequestParam(defaultValue = "") String bearerToken,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
HttpServletRequest request,
|
||||
Model model
|
||||
) {
|
||||
@@ -45,7 +40,7 @@ public class McpClientDemoController {
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
model.addAttribute("result", demoService.run(serverOrigin, contextPath, toolName, bearerToken, limit));
|
||||
model.addAttribute("result", demoService.run(serverOrigin, "default", "", "", 50));
|
||||
} catch (Exception e) {
|
||||
model.addAttribute("errorMessage", e.getMessage());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.mcp.McpReasoningCommand;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpReasoningService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpToolRegistry;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -21,32 +21,30 @@ public class McpReasoningController {
|
||||
|
||||
private final McpToolRegistry toolRegistry;
|
||||
private final McpReasoningService reasoningService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public McpReasoningController(
|
||||
McpToolRegistry toolRegistry,
|
||||
McpReasoningService reasoningService,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
BearerTokenService tokenService
|
||||
BearerTokenService tokenService,
|
||||
UserMapper userMapper
|
||||
) {
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.reasoningService = reasoningService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.tokenService = tokenService;
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-reasoning")
|
||||
public String page(Model model) {
|
||||
try {
|
||||
model.addAttribute("objects", protectedObjectService.findEnabled());
|
||||
model.addAttribute("tools", toolRegistry.listTools());
|
||||
model.addAttribute("tokens", tokenService.findTokenContextOptions());
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
} 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("users", List.of());
|
||||
model.addAttribute("runtimeError", message);
|
||||
}
|
||||
return "mcp-reasoning";
|
||||
@@ -82,15 +80,29 @@ public class McpReasoningController {
|
||||
|
||||
@PostMapping("/mcp-reasoning")
|
||||
public String reason(
|
||||
@RequestParam(required = false) Long tokenKeyId,
|
||||
@RequestParam long objectId,
|
||||
@RequestParam String bearerToken,
|
||||
@RequestParam(defaultValue = "") String bearerToken,
|
||||
@RequestParam(required = false) Long tempUserId,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(defaultValue = "") String question,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("result", reasoningService.reason(
|
||||
new McpReasoningCommand(tokenKeyId, objectId, bearerToken, limit, question)));
|
||||
String effectiveToken = bearerToken;
|
||||
Long temporaryKeyId = null;
|
||||
try {
|
||||
if (tempUserId != null) {
|
||||
var issued = tokenService.issueTemporaryToken(tempUserId, "MCP Reasoning 임시 실행");
|
||||
effectiveToken = issued.plainToken();
|
||||
temporaryKeyId = issued.keyId();
|
||||
}
|
||||
model.addAttribute("result", reasoningService.reason(
|
||||
new McpReasoningCommand(temporaryKeyId, effectiveToken, limit, question)));
|
||||
} catch (Exception exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
} finally {
|
||||
if (temporaryKeyId != null) {
|
||||
tokenService.revokeToken(temporaryKeyId, "temporary reasoning completed");
|
||||
}
|
||||
}
|
||||
return "fragments/mcp-reasoning-result :: result";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsProbeService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import com.cloudhandson.vpdbackoffice.service.VpdPolicyService;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -23,17 +24,20 @@ public class ProbeController {
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final VpdPolicyService vpdPolicyService;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public ProbeController(
|
||||
OrdsProbeService probeService,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
BearerTokenService tokenService,
|
||||
VpdPolicyService vpdPolicyService
|
||||
VpdPolicyService vpdPolicyService,
|
||||
UserMapper userMapper
|
||||
) {
|
||||
this.probeService = probeService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.tokenService = tokenService;
|
||||
this.vpdPolicyService = vpdPolicyService;
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/probe")
|
||||
@@ -48,19 +52,35 @@ public class ProbeController {
|
||||
.toList();
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("defaultObjectKeys", defaultObjectKeys);
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
return "probe";
|
||||
}
|
||||
|
||||
@PostMapping("/probe")
|
||||
public String run(
|
||||
@RequestParam long objectId,
|
||||
@RequestParam String bearerToken,
|
||||
@RequestParam(defaultValue = "") String bearerToken,
|
||||
@RequestParam(required = false) Long tempUserId,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(required = false) String requestBody,
|
||||
Model model
|
||||
) {
|
||||
String normalizedToken = bearerToken == null ? "" : bearerToken.trim();
|
||||
model.addAttribute("result", probeService.runProbe(new ProbeCommand(null, objectId, normalizedToken, limit)));
|
||||
model.addAttribute("tokenContext", tokenService.findTokenContextByPlainToken(normalizedToken));
|
||||
Long temporaryKeyId = null;
|
||||
if (tempUserId != null) {
|
||||
var issued = tokenService.issueTemporaryToken(tempUserId, "ORDS 검증 임시 실행");
|
||||
normalizedToken = issued.plainToken();
|
||||
temporaryKeyId = issued.keyId();
|
||||
}
|
||||
try {
|
||||
model.addAttribute("result", probeService.runProbe(
|
||||
new ProbeCommand(temporaryKeyId, objectId, normalizedToken, limit, requestBody)));
|
||||
model.addAttribute("tokenContext", tokenService.findTokenContextByPlainToken(normalizedToken));
|
||||
} finally {
|
||||
if (temporaryKeyId != null) {
|
||||
tokenService.revokeToken(temporaryKeyId, "temporary probe completed");
|
||||
}
|
||||
}
|
||||
model.addAttribute("selectedObject", protectedObjectService.findEnabled().stream()
|
||||
.filter(object -> object.objectId() == objectId)
|
||||
.findFirst()
|
||||
|
||||
@@ -44,12 +44,13 @@ public class ProtectedObjectController {
|
||||
public String create(
|
||||
@RequestParam String owner,
|
||||
@RequestParam String objectName,
|
||||
@RequestParam String ordsPath,
|
||||
@RequestParam(required = false) String ordsPath,
|
||||
@RequestParam(required = false) String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
protectedObjectService.createObject(
|
||||
new ProtectedObjectCreateCommand(owner, objectName, ordsPath, null, null));
|
||||
new ProtectedObjectCreateCommand(owner, objectName, ordsPath, null, null, description));
|
||||
redirectAttributes.addFlashAttribute("message", "ORDS 조회 Handler 대상을 추가했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
@@ -57,6 +58,21 @@ public class ProtectedObjectController {
|
||||
return "redirect:/objects";
|
||||
}
|
||||
|
||||
@PostMapping("/objects/description")
|
||||
public String updateDescription(
|
||||
@RequestParam long objectId,
|
||||
@RequestParam(required = false) String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
protectedObjectService.updateDescription(objectId, description);
|
||||
redirectAttributes.addFlashAttribute("message", "조회 대상 설명을 저장했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/objects";
|
||||
}
|
||||
|
||||
@PostMapping("/objects/ords-path")
|
||||
public String updateOrdsPath(
|
||||
@RequestParam long objectId,
|
||||
@@ -81,7 +97,7 @@ public class ProtectedObjectController {
|
||||
) {
|
||||
try {
|
||||
protectedObjectService.updateColumnPolicy(columnId, sensitivityLevel, redactionMethod);
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 민감도/마스킹 정책을 수정했습니다.");
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 표시 보호 정책을 수정했습니다. 행 접근 권한은 변경되지 않습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
|
||||
@@ -30,8 +30,12 @@ public class TokenController {
|
||||
}
|
||||
|
||||
@GetMapping("/tokens")
|
||||
public String tokens(Model model) {
|
||||
model.addAttribute("tokens", tokenService.findAll());
|
||||
public String tokens(
|
||||
@RequestParam(defaultValue = "false") boolean includeInactive,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("tokens", tokenService.findAll(includeInactive));
|
||||
model.addAttribute("includeInactive", includeInactive);
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
model.addAttribute("defaultExpiresAt", defaultExpiresAt());
|
||||
return "tokens";
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.VpdPolicyService;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -42,10 +44,34 @@ public class VpdPolicyController {
|
||||
private void populatePolicyModel(String schemaOwner, Model model) {
|
||||
try {
|
||||
String selectedSchemaOwner = schemaOwner == null ? "" : schemaOwner.trim().toUpperCase();
|
||||
model.addAttribute("policies", vpdPolicyService.findPolicies());
|
||||
List<com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView> policies = vpdPolicyService.findPolicies();
|
||||
Map<String, String> policyDescriptions = new LinkedHashMap<>();
|
||||
policies.forEach(policy -> policyDescriptions.put(
|
||||
policy.objectDisplayName() + "|" + policy.policyName(),
|
||||
vpdPolicyService.findPolicyDescription(policy.objectOwner(), policy.objectName(), policy.policyName())
|
||||
));
|
||||
model.addAttribute("policies", policies);
|
||||
model.addAttribute("policyDescriptions", policyDescriptions);
|
||||
model.addAttribute("vpdTargets", vpdPolicyService.findVpdTargets(selectedSchemaOwner));
|
||||
model.addAttribute("selectedSchemaOwner", selectedSchemaOwner);
|
||||
model.addAttribute("formOptions", vpdPolicyService.formOptions());
|
||||
var formOptions = vpdPolicyService.formOptions();
|
||||
Map<String, String> filterDescriptions = new LinkedHashMap<>();
|
||||
formOptions.functions().forEach(function -> filterDescriptions.put(
|
||||
function.owner() + "|" + function.functionName(),
|
||||
vpdPolicyService.findFilterDescription(function.owner(), function.functionName())
|
||||
));
|
||||
policies.forEach(policy -> filterDescriptions.putIfAbsent(
|
||||
policy.functionOwner() + "|" + policy.functionName(),
|
||||
vpdPolicyService.findFilterDescription(policy.functionOwner(), policy.functionName())
|
||||
));
|
||||
Map<String, String> filterPredicates = new LinkedHashMap<>();
|
||||
formOptions.functions().forEach(function -> filterPredicates.put(
|
||||
function.owner() + "|" + function.functionName(),
|
||||
vpdPolicyService.findFilterPredicate(function.owner(), function.packageName(), function.functionName())
|
||||
));
|
||||
model.addAttribute("formOptions", formOptions);
|
||||
model.addAttribute("filterDescriptions", filterDescriptions);
|
||||
model.addAttribute("filterPredicates", filterPredicates);
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("runtimeError", message);
|
||||
@@ -53,12 +79,18 @@ public class VpdPolicyController {
|
||||
model.addAttribute("vpdTargets", List.of());
|
||||
model.addAttribute("selectedSchemaOwner", "");
|
||||
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
|
||||
model.addAttribute("policyDescriptions", Map.of());
|
||||
model.addAttribute("filterDescriptions", Map.of());
|
||||
model.addAttribute("filterPredicates", Map.of());
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
model.addAttribute("policies", List.of());
|
||||
model.addAttribute("vpdTargets", List.of());
|
||||
model.addAttribute("selectedSchemaOwner", "");
|
||||
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
|
||||
model.addAttribute("policyDescriptions", Map.of());
|
||||
model.addAttribute("filterDescriptions", Map.of());
|
||||
model.addAttribute("filterPredicates", Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +137,18 @@ public class VpdPolicyController {
|
||||
@RequestParam(required = false) String functionOwner,
|
||||
@RequestParam String functionName,
|
||||
@RequestParam String filterPredicate,
|
||||
@RequestParam(defaultValue = "Filter function이 반환하는 predicate로 조회 행을 제한합니다.") String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
vpdPolicyService.saveFilterFunction(functionOwner, functionName, filterPredicate);
|
||||
vpdPolicyService.saveFilterDescription(
|
||||
functionOwner == null || functionOwner.isBlank()
|
||||
? vpdPolicyService.currentUser()
|
||||
: functionOwner,
|
||||
functionName,
|
||||
description
|
||||
);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Filter function을 저장했습니다: " + functionName);
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
@@ -119,6 +159,23 @@ public class VpdPolicyController {
|
||||
return "redirect:/vpd-filter-policies";
|
||||
}
|
||||
|
||||
@PostMapping("/vpd-policies/description")
|
||||
public String savePolicyDescription(
|
||||
@RequestParam String objectOwner,
|
||||
@RequestParam String objectName,
|
||||
@RequestParam String policyName,
|
||||
@RequestParam String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
vpdPolicyService.savePolicyDescription(objectOwner, objectName, policyName, description);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Policy 설명을 저장했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/vpd-policies";
|
||||
}
|
||||
|
||||
@PostMapping("/vpd-filter-policies/replace")
|
||||
public String replaceFilterPolicy(
|
||||
@RequestParam String oldObjectKey,
|
||||
|
||||
Reference in New Issue
Block a user