@@ -0,0 +1,33 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@ControllerAdvice
|
||||
public class AppExceptionHandler {
|
||||
|
||||
@ExceptionHandler(AppException.class)
|
||||
public String handleAppException(AppException exception, Model model) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
return "error";
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
public String handleDataAccessException(DataAccessException exception, Model model) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("errorTitle", error.title());
|
||||
model.addAttribute("errorMessage", error.message());
|
||||
return "error";
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public String handleUnexpectedException(Exception exception, Model model) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.unexpected(exception);
|
||||
model.addAttribute("errorTitle", error.title());
|
||||
model.addAttribute("errorMessage", error.message());
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.GroupService;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
private final UserService userService;
|
||||
private final GroupService groupService;
|
||||
private final PermissionService permissionService;
|
||||
|
||||
public DashboardController(
|
||||
UserService userService,
|
||||
GroupService groupService,
|
||||
PermissionService permissionService
|
||||
) {
|
||||
this.userService = userService;
|
||||
this.groupService = groupService;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public String dashboard(Model model) {
|
||||
// The backoffice home is an identity-administration landing page.
|
||||
// It intentionally does not query legacy CB_* VPD catalog objects.
|
||||
model.addAttribute("users", userService.findAll());
|
||||
model.addAttribute("groups", groupService.findAll());
|
||||
model.addAttribute("objects", List.of());
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
model.addAttribute("tokens", List.of());
|
||||
return "dashboard";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.effective.EffectiveMatrixView;
|
||||
import com.cloudhandson.vpdbackoffice.service.EffectiveMatrixService;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
public class EffectiveMatrixController {
|
||||
|
||||
private final EffectiveMatrixService matrixService;
|
||||
|
||||
public EffectiveMatrixController(EffectiveMatrixService matrixService) {
|
||||
this.matrixService = matrixService;
|
||||
}
|
||||
|
||||
@GetMapping("/effective-matrix")
|
||||
public String matrix(Model model) {
|
||||
try {
|
||||
model.addAttribute("matrix", matrixService.matrix());
|
||||
} catch (DataAccessException e) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(e);
|
||||
model.addAttribute("matrix", new EffectiveMatrixView(List.of(), List.of(), List.of(), 0));
|
||||
model.addAttribute("runtimeError", message);
|
||||
}
|
||||
return "effective-matrix";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.effective.GroupEffectiveAccessView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.effective.RoleEffectiveImpactView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.group.GroupCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.EffectiveMatrixService;
|
||||
import com.cloudhandson.vpdbackoffice.service.GroupService;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class GroupController {
|
||||
|
||||
private final GroupService groupService;
|
||||
private final UserService userService;
|
||||
private final PermissionService permissionService;
|
||||
private final EffectiveMatrixService effectiveMatrixService;
|
||||
|
||||
public GroupController(
|
||||
GroupService groupService,
|
||||
UserService userService,
|
||||
PermissionService permissionService,
|
||||
EffectiveMatrixService effectiveMatrixService
|
||||
) {
|
||||
this.groupService = groupService;
|
||||
this.userService = userService;
|
||||
this.permissionService = permissionService;
|
||||
this.effectiveMatrixService = effectiveMatrixService;
|
||||
}
|
||||
|
||||
@GetMapping("/groups")
|
||||
public String groups(Model model) {
|
||||
var matrix = effectiveMatrixService.matrix();
|
||||
model.addAttribute("groups", groupService.findAll());
|
||||
model.addAttribute("users", userService.findAll());
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
model.addAttribute("groupUsers", groupService.findGroupUsers());
|
||||
model.addAttribute("groupRoles", groupService.findGroupRoles());
|
||||
model.addAttribute("groupImpactByGroupId", matrix.groups().stream()
|
||||
.collect(Collectors.toMap(GroupEffectiveAccessView::groupId, impact -> impact)));
|
||||
model.addAttribute("roleImpactByRoleId", matrix.roles().stream()
|
||||
.collect(Collectors.toMap(RoleEffectiveImpactView::roleId, impact -> impact)));
|
||||
return "groups";
|
||||
}
|
||||
|
||||
@PostMapping("/groups")
|
||||
public String create(
|
||||
@RequestParam String groupCode,
|
||||
@RequestParam String groupName,
|
||||
@RequestParam(required = false) String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
groupService.createGroup(new GroupCreateCommand(groupCode, groupName, description));
|
||||
redirectAttributes.addFlashAttribute("message", "그룹을 추가했습니다.");
|
||||
return "redirect:/groups";
|
||||
}
|
||||
|
||||
@PostMapping("/groups/active")
|
||||
public String active(
|
||||
@RequestParam long groupId,
|
||||
@RequestParam boolean active,
|
||||
@RequestParam(defaultValue = "false") boolean confirmImpact,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
groupService.setActive(groupId, active, active || confirmImpact);
|
||||
redirectAttributes.addFlashAttribute("message", "그룹 상태를 변경했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("error", exception.getMessage());
|
||||
}
|
||||
return "redirect:/groups";
|
||||
}
|
||||
|
||||
@PostMapping("/groups/users")
|
||||
public String addUser(
|
||||
@RequestParam long groupId,
|
||||
@RequestParam long userId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
groupService.addUser(groupId, userId);
|
||||
redirectAttributes.addFlashAttribute("message", "그룹에 사용자를 추가했습니다.");
|
||||
return "redirect:/groups";
|
||||
}
|
||||
|
||||
@PostMapping("/groups/users/delete")
|
||||
public String removeUser(
|
||||
@RequestParam long groupId,
|
||||
@RequestParam long userId,
|
||||
@RequestParam(defaultValue = "false") boolean confirmImpact,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
groupService.removeUser(groupId, userId, confirmImpact);
|
||||
redirectAttributes.addFlashAttribute("message", "그룹 사용자를 해제했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("error", exception.getMessage());
|
||||
}
|
||||
return "redirect:/groups";
|
||||
}
|
||||
|
||||
@PostMapping("/groups/roles")
|
||||
public String addRole(
|
||||
@RequestParam long groupId,
|
||||
@RequestParam long roleId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
groupService.addRole(groupId, roleId);
|
||||
redirectAttributes.addFlashAttribute("message", "그룹에 역할을 부여했습니다.");
|
||||
return "redirect:/groups";
|
||||
}
|
||||
|
||||
@PostMapping("/groups/roles/delete")
|
||||
public String removeRole(
|
||||
@RequestParam long groupId,
|
||||
@RequestParam long roleId,
|
||||
@RequestParam(defaultValue = "false") boolean confirmImpact,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
groupService.removeRole(groupId, roleId, confirmImpact);
|
||||
redirectAttributes.addFlashAttribute("message", "그룹 역할을 해제했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("error", exception.getMessage());
|
||||
}
|
||||
return "redirect:/groups";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class LoginController {
|
||||
|
||||
private final BackofficeProperties properties;
|
||||
|
||||
public LoginController(BackofficeProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login(Model model) {
|
||||
model.addAttribute("rememberMeAvailable", properties.security().rememberMeConfigured());
|
||||
model.addAttribute("rememberMeDays", properties.security().rememberMeDays());
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRuleCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingTemplate;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.MaskingRuleService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class MaskingRuleController {
|
||||
|
||||
private final MaskingRuleService maskingRuleService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
|
||||
public MaskingRuleController(
|
||||
MaskingRuleService maskingRuleService,
|
||||
ProtectedObjectService protectedObjectService
|
||||
) {
|
||||
this.maskingRuleService = maskingRuleService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
}
|
||||
|
||||
@GetMapping("/masking-rules")
|
||||
public String maskingRules(Model model) {
|
||||
var objects = protectedObjectService.findEnabled();
|
||||
var managedObjectNames = maskingRuleService.managedObjectNames();
|
||||
var policyStatuses = maskingRuleService.findPolicyStatuses();
|
||||
var columnsByObject = protectedObjectService.findColumnsByObjectIds(
|
||||
objects.stream().map(object -> object.objectId()).toList()
|
||||
);
|
||||
var maskingTargetObjects = objects.stream()
|
||||
.filter(object -> managedObjectNames.contains(object.objectName()))
|
||||
.toList();
|
||||
model.addAttribute("rules", maskingRuleService.findAllRules());
|
||||
model.addAttribute("templates", Arrays.asList(MaskingTemplate.values()));
|
||||
model.addAttribute("columnRules", maskingRuleService.findColumnRules());
|
||||
model.addAttribute("policyStatuses", policyStatuses);
|
||||
model.addAttribute("policyStatusByObjectName", policyStatuses.stream().collect(Collectors.toMap(
|
||||
status -> status.objectName(),
|
||||
status -> status,
|
||||
(left, right) -> left
|
||||
)));
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("maskingTargetObjects", maskingTargetObjects);
|
||||
model.addAttribute("sensitiveColumnsByObject", objects.stream().collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> columnsByObject.getOrDefault(object.objectId(), List.of()).stream()
|
||||
.filter(column -> column.sensitive())
|
||||
.toList()
|
||||
)));
|
||||
model.addAttribute("availableMaskingColumnsByObject", maskingTargetObjects.stream().collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> availableMaskingColumns(object, columnsByObject.getOrDefault(object.objectId(), List.of()))
|
||||
)));
|
||||
return "masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/masking-rules")
|
||||
public String createRule(
|
||||
@RequestParam String ruleCode,
|
||||
@RequestParam String ruleName,
|
||||
@RequestParam String templateCode,
|
||||
@RequestParam(required = false) String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
maskingRuleService.createRule(new MaskingRuleCreateCommand(ruleCode, ruleName, templateCode, description));
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 마스킹 규칙을 등록했습니다.");
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return "redirect:/masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/masking-rules/active")
|
||||
public String setRuleActive(
|
||||
@RequestParam long ruleId,
|
||||
@RequestParam boolean active,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
var result = maskingRuleService.setRuleActive(ruleId, active);
|
||||
redirectAttributes.addFlashAttribute("message", (active ? "컬럼 마스킹 규칙을 활성화했습니다. " : "컬럼 마스킹 규칙을 비활성화했습니다. ")
|
||||
+ result.summary());
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/masking-rules/columns")
|
||||
public String assignColumnRule(
|
||||
@RequestParam long columnId,
|
||||
@RequestParam long ruleId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
var result = maskingRuleService.assignRuleToColumn(columnId, ruleId);
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼에 컬럼 마스킹 규칙을 연결했습니다. " + result.summary());
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/masking-rules/target-columns")
|
||||
public String addTargetColumn(
|
||||
@RequestParam String target,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
String[] parts = target == null ? new String[0] : target.split(":", 2);
|
||||
if (parts.length != 2) {
|
||||
throw new AppException("추가할 보호 객체와 컬럼을 선택하세요.");
|
||||
}
|
||||
var result = maskingRuleService.addTargetColumn(Long.parseLong(parts[0]), parts[1]);
|
||||
redirectAttributes.addFlashAttribute("message",
|
||||
"마스킹 대상 컬럼으로 등록했습니다. 실제 적용하려면 아래에서 기본 규칙을 연결하세요. " + result.summary());
|
||||
} catch (NumberFormatException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "추가할 보호 객체와 컬럼을 선택하세요.");
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return "redirect:/masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/masking-rules/columns/delete")
|
||||
public String removeColumnRule(@RequestParam long columnId, RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
var result = maskingRuleService.removeRuleFromColumn(columnId);
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 마스킹 규칙과 관련 사용자 예외를 해제했습니다. " + result.summary());
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/masking-rules/synchronize")
|
||||
public String synchronizeDatabasePolicies(RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
redirectAttributes.addFlashAttribute("message", maskingRuleService.synchronizeDatabasePolicies().summary());
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return "redirect:/masking-rules";
|
||||
}
|
||||
|
||||
private String safeMessage(Exception exception) {
|
||||
return exception instanceof AppException ? exception.getMessage() : "컬럼 마스킹 규칙을 저장하지 못했습니다. 입력값과 DB 상태를 확인하세요.";
|
||||
}
|
||||
|
||||
private List<String> availableMaskingColumns(
|
||||
ProtectedObject object,
|
||||
List<ProtectedColumn> protectedColumns
|
||||
) {
|
||||
var registeredSensitiveColumns = new HashSet<String>();
|
||||
protectedColumns.stream()
|
||||
.filter(ProtectedColumn::sensitive)
|
||||
.map(ProtectedColumn::columnName)
|
||||
.forEach(registeredSensitiveColumns::add);
|
||||
return protectedObjectService.findDatabaseColumns(object.owner(), object.objectName()).stream()
|
||||
.filter(columnName -> !registeredSensitiveColumns.contains(columnName))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
@Controller
|
||||
public class McpChatbotController {
|
||||
|
||||
private final McpChatbotService chatbotService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public McpChatbotController(
|
||||
McpChatbotService chatbotService,
|
||||
BearerTokenService tokenService,
|
||||
UserMapper userMapper
|
||||
) {
|
||||
this.chatbotService = chatbotService;
|
||||
this.tokenService = tokenService;
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-chatbot")
|
||||
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";
|
||||
}
|
||||
|
||||
@PostMapping("/mcp-chatbot")
|
||||
public String chat(
|
||||
@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
|
||||
) {
|
||||
try {
|
||||
String serverOrigin = ServletUriComponentsBuilder.fromRequestUri(request)
|
||||
.replacePath(null)
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
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());
|
||||
}
|
||||
return "fragments/mcp-chatbot-result :: result";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.McpClientDemoService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpToolRegistry;
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
@Controller
|
||||
public class McpClientDemoController {
|
||||
|
||||
private final McpToolRegistry toolRegistry;
|
||||
private final McpClientDemoService demoService;
|
||||
|
||||
public McpClientDemoController(McpToolRegistry toolRegistry, McpClientDemoService demoService) {
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.demoService = demoService;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-client-demo")
|
||||
public String page(Model model) {
|
||||
addTools(model);
|
||||
return "mcp-client-demo";
|
||||
}
|
||||
|
||||
@PostMapping("/mcp-client-demo")
|
||||
public String run(
|
||||
HttpServletRequest request,
|
||||
Model model
|
||||
) {
|
||||
addTools(model);
|
||||
try {
|
||||
String serverOrigin = ServletUriComponentsBuilder.fromRequestUri(request)
|
||||
.replacePath(null)
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
model.addAttribute("result", demoService.run(serverOrigin, "default", "", "", 50));
|
||||
} catch (Exception e) {
|
||||
model.addAttribute("errorMessage", e.getMessage());
|
||||
}
|
||||
return "fragments/mcp-client-demo-result :: result";
|
||||
}
|
||||
|
||||
private void addTools(Model model) {
|
||||
try {
|
||||
model.addAttribute("tools", toolRegistry.listTools());
|
||||
} catch (DataAccessException e) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(e);
|
||||
model.addAttribute("tools", List.of());
|
||||
model.addAttribute("runtimeError", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.McpProperties;
|
||||
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.McpSseService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpToolRegistry;
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
public class McpReasoningController {
|
||||
|
||||
private final McpToolRegistry toolRegistry;
|
||||
private final McpSseService mcpSseService;
|
||||
private final McpReasoningService reasoningService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final UserMapper userMapper;
|
||||
private final McpProperties mcpProperties;
|
||||
|
||||
public McpReasoningController(
|
||||
McpToolRegistry toolRegistry,
|
||||
McpSseService mcpSseService,
|
||||
McpReasoningService reasoningService,
|
||||
BearerTokenService tokenService,
|
||||
UserMapper userMapper,
|
||||
McpProperties mcpProperties
|
||||
) {
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.mcpSseService = mcpSseService;
|
||||
this.reasoningService = reasoningService;
|
||||
this.tokenService = tokenService;
|
||||
this.userMapper = userMapper;
|
||||
this.mcpProperties = mcpProperties;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-reasoning")
|
||||
public String page(Model model) {
|
||||
try {
|
||||
model.addAttribute("tools", toolRegistry.listTools());
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
} catch (DataAccessException e) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(e);
|
||||
model.addAttribute("tools", List.of());
|
||||
model.addAttribute("users", List.of());
|
||||
model.addAttribute("runtimeError", message);
|
||||
}
|
||||
return "mcp-reasoning";
|
||||
}
|
||||
|
||||
@GetMapping("/mcp/tools")
|
||||
@ResponseBody
|
||||
public Object tools() {
|
||||
return mcpSseService.registeredTools();
|
||||
}
|
||||
|
||||
@GetMapping("/mcp-sse")
|
||||
public String ssePage(Model model) {
|
||||
model.addAttribute("tools", mcpSseService.registeredTools());
|
||||
model.addAttribute("hmmMcpPublicUrl", mcpProperties.resolvedPublicUrl());
|
||||
return "mcp-sse";
|
||||
}
|
||||
|
||||
@PostMapping("/mcp-reasoning")
|
||||
public String reason(
|
||||
@RequestParam(defaultValue = "") String bearerToken,
|
||||
@RequestParam(required = false) Long tempUserId,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(defaultValue = "") String question,
|
||||
Model model
|
||||
) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.McpSseService;
|
||||
import com.cloudhandson.vpdbackoffice.service.McpUnauthorizedException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
@Controller
|
||||
public class McpSseController {
|
||||
|
||||
private static final long SSE_TIMEOUT_MILLIS = 30L * 60L * 1000L;
|
||||
|
||||
private final McpSseService mcpSseService;
|
||||
private final Map<String, McpSseSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
public McpSseController(McpSseService mcpSseService) {
|
||||
this.mcpSseService = mcpSseService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streamable HTTP transport for Codex and other current MCP clients.
|
||||
* The legacy SSE endpoints remain available for existing integrations.
|
||||
*/
|
||||
@PostMapping(path = "/mcp", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<ObjectNode> streamableMessage(
|
||||
@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorization,
|
||||
@RequestBody JsonNode request
|
||||
) {
|
||||
try {
|
||||
ObjectNode response = mcpSseService.handle(
|
||||
"default", request, bearerToken(authorization));
|
||||
// JSON-RPC notifications never receive a response body. Current MCP
|
||||
// clients send notifications/initialized immediately after initialize.
|
||||
if (request != null && !request.has("id")) {
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (McpUnauthorizedException exception) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(path = "/mcp/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter defaultSse() throws IOException {
|
||||
return openSse("default");
|
||||
}
|
||||
|
||||
@GetMapping(path = "/mcp/{contextPath}/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter contextSse(@PathVariable String contextPath) throws IOException {
|
||||
return openSse(contextPath);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/mcp/messages", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<?> defaultMessage(
|
||||
@RequestParam(required = false) String sessionId,
|
||||
@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorization,
|
||||
@RequestBody JsonNode request
|
||||
) throws IOException {
|
||||
return handleMessage("default", sessionId, authorization, request);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/mcp/{contextPath}/messages", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<?> contextMessage(
|
||||
@PathVariable String contextPath,
|
||||
@RequestParam(required = false) String sessionId,
|
||||
@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorization,
|
||||
@RequestBody JsonNode request
|
||||
) throws IOException {
|
||||
return handleMessage(contextPath, sessionId, authorization, request);
|
||||
}
|
||||
|
||||
private SseEmitter openSse(String contextPath) throws IOException {
|
||||
String normalizedContextPath = normalizeContextPath(contextPath);
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MILLIS);
|
||||
sessions.put(sessionId, new McpSseSession(normalizedContextPath, emitter));
|
||||
emitter.onCompletion(() -> sessions.remove(sessionId));
|
||||
emitter.onTimeout(() -> sessions.remove(sessionId));
|
||||
emitter.onError(error -> sessions.remove(sessionId));
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("endpoint")
|
||||
.data(messageEndpoint(normalizedContextPath, sessionId)));
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private ResponseEntity<?> handleMessage(
|
||||
String contextPath,
|
||||
String sessionId,
|
||||
String authorization,
|
||||
JsonNode request
|
||||
) throws IOException {
|
||||
String normalizedContextPath = normalizeContextPath(contextPath);
|
||||
ObjectNode response;
|
||||
try {
|
||||
response = mcpSseService.handle(
|
||||
normalizedContextPath, request, bearerToken(authorization));
|
||||
} catch (McpUnauthorizedException exception) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
McpSseSession session = sessions.get(sessionId);
|
||||
if (session == null || !session.contextPath().equals(normalizedContextPath)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
try {
|
||||
session.emitter().send(SseEmitter.event().name("message").data(response));
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (IOException e) {
|
||||
sessions.remove(sessionId);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private String messageEndpoint(String contextPath, String sessionId) {
|
||||
if ("default".equals(contextPath)) {
|
||||
return "/mcp/messages?sessionId=" + sessionId;
|
||||
}
|
||||
return "/mcp/" + contextPath + "/messages?sessionId=" + sessionId;
|
||||
}
|
||||
|
||||
private String normalizeContextPath(String contextPath) {
|
||||
String normalized = contextPath == null || contextPath.isBlank() ? "default" : contextPath.trim();
|
||||
if (!normalized.matches("[A-Za-z0-9][A-Za-z0-9_-]{0,63}")) {
|
||||
throw new IllegalArgumentException("MCP context path는 영문/숫자로 시작하고 영문/숫자/_/-만 사용할 수 있습니다: " + contextPath);
|
||||
}
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
private String bearerToken(String authorization) {
|
||||
if (authorization == null || !authorization.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||
return "";
|
||||
}
|
||||
return authorization.substring(7).trim();
|
||||
}
|
||||
|
||||
private record McpSseSession(String contextPath, SseEmitter emitter) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.operation.OperationHealthSummary;
|
||||
import com.cloudhandson.vpdbackoffice.service.MaskingRuleService;
|
||||
import com.cloudhandson.vpdbackoffice.service.OperationStatusService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class OperationStatusController {
|
||||
|
||||
private final OperationStatusService service;
|
||||
private final MaskingRuleService maskingRuleService;
|
||||
|
||||
public OperationStatusController(OperationStatusService service, MaskingRuleService maskingRuleService) {
|
||||
this.service = service;
|
||||
this.maskingRuleService = maskingRuleService;
|
||||
}
|
||||
|
||||
@GetMapping("/operation-status")
|
||||
public String status(Model model) {
|
||||
var rows = service.findRows();
|
||||
var maskingPolicyStatuses = maskingRuleService.findPolicyStatuses();
|
||||
model.addAttribute("rows", rows);
|
||||
model.addAttribute("maskingPolicyStatuses", maskingPolicyStatuses);
|
||||
model.addAttribute("summary", OperationHealthSummary.from(rows, maskingPolicyStatuses));
|
||||
return "operation-status";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerUpdateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class OrdsHandlerController {
|
||||
|
||||
private final OrdsMetadataService ordsMetadataService;
|
||||
|
||||
public OrdsHandlerController(OrdsMetadataService ordsMetadataService) {
|
||||
this.ordsMetadataService = ordsMetadataService;
|
||||
}
|
||||
|
||||
@GetMapping("/ords-handlers")
|
||||
public String handlers(Model model) {
|
||||
model.addAttribute("handlers", ordsMetadataService.findHandlers());
|
||||
return "ords-handlers";
|
||||
}
|
||||
|
||||
@PostMapping("/ords-handlers/update")
|
||||
public String update(
|
||||
@RequestParam long handlerId,
|
||||
@RequestParam String source,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
ordsMetadataService.updateHandlerSource(new OrdsHandlerUpdateCommand(handlerId, source));
|
||||
redirectAttributes.addFlashAttribute("message", "ORDS Handler Source를 저장했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", error.message());
|
||||
}
|
||||
return "redirect:/ords-handlers";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.GroupService;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
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 java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class PermissionController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PermissionController.class);
|
||||
private final PermissionService permissionService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final UserService userService;
|
||||
private final GroupService groupService;
|
||||
|
||||
public PermissionController(
|
||||
PermissionService permissionService,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
UserService userService,
|
||||
GroupService groupService
|
||||
) {
|
||||
this.permissionService = permissionService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.userService = userService;
|
||||
this.groupService = groupService;
|
||||
}
|
||||
|
||||
@GetMapping("/permissions/object-columns")
|
||||
@ResponseBody
|
||||
public List<String> objectColumns(@RequestParam String objectRef) {
|
||||
if (objectRef == null || objectRef.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
if (objectRef.startsWith("protected:")) {
|
||||
long objectId;
|
||||
try {
|
||||
objectId = Long.parseLong(objectRef.substring("protected:".length()));
|
||||
} catch (NumberFormatException exception) {
|
||||
return List.of();
|
||||
}
|
||||
return protectedObjectService.findColumns(objectId).stream()
|
||||
.map(column -> column.columnName())
|
||||
.toList();
|
||||
}
|
||||
if (objectRef.startsWith("db:")) {
|
||||
String value = objectRef.substring("db:".length());
|
||||
int dot = value.indexOf('.');
|
||||
if (dot < 1 || dot == value.length() - 1) {
|
||||
return List.of();
|
||||
}
|
||||
return protectedObjectService.findDatabaseColumns(value.substring(0, dot), value.substring(dot + 1));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@GetMapping("/permissions")
|
||||
public String permissions(Model model) {
|
||||
long started = System.nanoTime();
|
||||
var objects = protectedObjectService.findEnabledWithPermissions();
|
||||
long objectsAt = System.nanoTime();
|
||||
var roles = permissionService.findRoles();
|
||||
long rolesAt = System.nanoTime();
|
||||
var roleImpact = buildRoleImpact(roles);
|
||||
long roleImpactAt = System.nanoTime();
|
||||
var protectedColumnsByObject = protectedObjectService.findColumnsByObjectIds(
|
||||
objects.stream().map(object -> object.objectId()).toList()
|
||||
);
|
||||
var columnsByObject = objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedColumnsByObject.getOrDefault(object.objectId(), List.of()).stream()
|
||||
.map(column -> column.columnName())
|
||||
.toList()
|
||||
));
|
||||
var maskableColumnsByObject = objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedColumnsByObject.getOrDefault(object.objectId(), List.of()).stream()
|
||||
.filter(ProtectedColumn::sensitive)
|
||||
.map(column -> column.columnName())
|
||||
.toList()
|
||||
));
|
||||
var maskableColumnLabelsByObject = objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedColumnsByObject.getOrDefault(object.objectId(), List.of()).stream()
|
||||
.filter(ProtectedColumn::sensitive)
|
||||
.map(column -> column.columnName() + " [" + column.policyLabel() + "]")
|
||||
.toList()
|
||||
));
|
||||
long columnsAt = System.nanoTime();
|
||||
var dbObjects = protectedObjectService.findDatabaseObjects();
|
||||
long dbObjectsAt = System.nanoTime();
|
||||
var permissions = permissionService.findPermissionViews();
|
||||
var permissionCountByObject = permissions.stream()
|
||||
.filter(permission -> permission.objectId() > 0)
|
||||
.collect(Collectors.groupingBy(
|
||||
permission -> permission.objectId(),
|
||||
Collectors.counting()
|
||||
));
|
||||
var lastPermissionByPermissionId = permissions.stream()
|
||||
.collect(Collectors.toMap(
|
||||
permission -> permission.permissionId(),
|
||||
permission -> permission.objectId() > 0
|
||||
&& permissionCountByObject.getOrDefault(permission.objectId(), 0L) <= 1,
|
||||
(left, right) -> left,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
long permissionsAt = System.nanoTime();
|
||||
log.info("permissions page timings: objects={}ms roles={}ms roleImpact={}ms columns={}ms dbObjects={}ms permissions={}ms total={}ms",
|
||||
elapsedMillis(started, objectsAt),
|
||||
elapsedMillis(objectsAt, rolesAt),
|
||||
elapsedMillis(rolesAt, roleImpactAt),
|
||||
elapsedMillis(roleImpactAt, columnsAt),
|
||||
elapsedMillis(columnsAt, dbObjectsAt),
|
||||
elapsedMillis(dbObjectsAt, permissionsAt),
|
||||
elapsedMillis(started, permissionsAt));
|
||||
model.addAttribute("roles", roles);
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("columnsByObject", columnsByObject);
|
||||
model.addAttribute("maskableColumnsByObject", maskableColumnsByObject);
|
||||
model.addAttribute("maskableColumnLabelsByObject", maskableColumnLabelsByObject);
|
||||
model.addAttribute("directUsersByRole", roleImpact.directUsersByRole());
|
||||
model.addAttribute("groupsByRole", roleImpact.groupsByRole());
|
||||
model.addAttribute("groupUsersByRole", roleImpact.groupUsersByRole());
|
||||
model.addAttribute("dbObjects", dbObjects);
|
||||
model.addAttribute("permissions", permissions);
|
||||
model.addAttribute("lastPermissionByPermissionId", lastPermissionByPermissionId);
|
||||
return "permissions";
|
||||
}
|
||||
|
||||
private RoleImpact buildRoleImpact(List<AppRole> roles) {
|
||||
Map<Long, Set<String>> directUsersByRole = emptyRoleSetMap(roles);
|
||||
userService.findUserRoles().forEach(row ->
|
||||
directUsersByRole.computeIfAbsent(row.roleId(), ignored -> new LinkedHashSet<>()).add(row.username()));
|
||||
|
||||
Map<Long, Set<String>> groupsByRole = emptyRoleSetMap(roles);
|
||||
Map<Long, List<String>> usersByGroup = groupService.findGroupUsers().stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
row -> row.groupId(),
|
||||
LinkedHashMap::new,
|
||||
Collectors.mapping(row -> row.username(), Collectors.toList())
|
||||
));
|
||||
Map<Long, Set<String>> groupUsersByRole = emptyRoleSetMap(roles);
|
||||
groupService.findGroupRoles().forEach(row -> {
|
||||
groupsByRole.computeIfAbsent(row.roleId(), ignored -> new LinkedHashSet<>())
|
||||
.add(row.groupCode() + " / " + row.groupName());
|
||||
groupUsersByRole.computeIfAbsent(row.roleId(), ignored -> new LinkedHashSet<>())
|
||||
.addAll(usersByGroup.getOrDefault(row.groupId(), List.of()));
|
||||
});
|
||||
|
||||
return new RoleImpact(
|
||||
toListMap(directUsersByRole),
|
||||
toListMap(groupsByRole),
|
||||
toListMap(groupUsersByRole)
|
||||
);
|
||||
}
|
||||
|
||||
private Map<Long, Set<String>> emptyRoleSetMap(List<AppRole> roles) {
|
||||
Map<Long, Set<String>> map = new LinkedHashMap<>();
|
||||
roles.forEach(role -> map.put(role.roleId(), new LinkedHashSet<>()));
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> toListMap(Map<Long, Set<String>> source) {
|
||||
Map<Long, List<String>> map = new LinkedHashMap<>();
|
||||
source.forEach((key, values) -> map.put(key, new ArrayList<>(values)));
|
||||
return map;
|
||||
}
|
||||
|
||||
private record RoleImpact(
|
||||
Map<Long, List<String>> directUsersByRole,
|
||||
Map<Long, List<String>> groupsByRole,
|
||||
Map<Long, List<String>> groupUsersByRole
|
||||
) {
|
||||
}
|
||||
|
||||
private long elapsedMillis(long from, long to) {
|
||||
return (to - from) / 1_000_000;
|
||||
}
|
||||
|
||||
@PostMapping("/permissions")
|
||||
public String save(
|
||||
@RequestParam(required = false) Long roleId,
|
||||
@RequestParam(required = false) String objectRef,
|
||||
@RequestParam(defaultValue = "ALLOW") String permissionEffect,
|
||||
@RequestParam(required = false) List<String> ruleColumn,
|
||||
@RequestParam(required = false) List<String> ruleType,
|
||||
@RequestParam(required = false) List<String> ruleValue,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
if (roleId == null) {
|
||||
throw new AppException("권한을 적용할 역할을 선택하세요.");
|
||||
}
|
||||
long objectId = resolveObjectId(objectRef);
|
||||
permissionService.savePermissionSet(new PermissionSetCommand(
|
||||
roleId,
|
||||
objectId,
|
||||
"SELECT",
|
||||
permissionEffect,
|
||||
buildRules(ruleColumn, ruleType, ruleValue),
|
||||
List.of()
|
||||
));
|
||||
redirectAttributes.addFlashAttribute("message", "권한을 저장했습니다.");
|
||||
} catch (AppException | IllegalArgumentException exception) {
|
||||
redirectAttributes.addFlashAttribute("error", exception.getMessage());
|
||||
}
|
||||
return "redirect:/permissions";
|
||||
}
|
||||
|
||||
private List<RuleCommand> buildRules(
|
||||
List<String> ruleColumns,
|
||||
List<String> ruleTypes,
|
||||
List<String> ruleValues
|
||||
) {
|
||||
if (ruleTypes == null || ruleTypes.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return java.util.stream.IntStream.range(0, ruleTypes.size())
|
||||
.mapToObj(index -> new RuleCommand(
|
||||
valueAt(ruleColumns, index),
|
||||
valueAt(ruleTypes, index),
|
||||
valueAt(ruleValues, index)
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String valueAt(List<String> values, int index) {
|
||||
return values == null || index >= values.size() ? null : values.get(index);
|
||||
}
|
||||
|
||||
private long resolveObjectId(String objectRef) {
|
||||
if (objectRef == null || objectRef.isBlank()) {
|
||||
throw new IllegalArgumentException("보호 객체를 선택하세요.");
|
||||
}
|
||||
if (objectRef.startsWith("protected:")) {
|
||||
return Long.parseLong(objectRef.substring("protected:".length()));
|
||||
}
|
||||
if (objectRef.startsWith("db:")) {
|
||||
String value = objectRef.substring("db:".length());
|
||||
int dot = value.indexOf('.');
|
||||
if (dot < 1 || dot == value.length() - 1) {
|
||||
throw new IllegalArgumentException("DB 객체 형식이 올바르지 않습니다.");
|
||||
}
|
||||
return protectedObjectService.ensureProtectedObject(value.substring(0, dot), value.substring(dot + 1)).objectId();
|
||||
}
|
||||
throw new IllegalArgumentException("보호 객체 형식이 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
@PostMapping("/permissions/delete")
|
||||
public String delete(
|
||||
@RequestParam long permissionId,
|
||||
@RequestParam(defaultValue = "false") boolean confirmImpact,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
permissionService.deletePermission(permissionId, confirmImpact);
|
||||
redirectAttributes.addFlashAttribute("message", "권한을 삭제했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("error", exception.getMessage());
|
||||
}
|
||||
return "redirect:/permissions";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorQueryEmbedding;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsProbeService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import com.cloudhandson.vpdbackoffice.service.VectorKnowledgeService;
|
||||
import com.cloudhandson.vpdbackoffice.service.VpdPolicyService;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
public class ProbeController {
|
||||
|
||||
private final OrdsProbeService probeService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final VpdPolicyService vpdPolicyService;
|
||||
private final UserMapper userMapper;
|
||||
private final VectorKnowledgeService vectorKnowledgeService;
|
||||
|
||||
public ProbeController(
|
||||
OrdsProbeService probeService,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
BearerTokenService tokenService,
|
||||
VpdPolicyService vpdPolicyService,
|
||||
UserMapper userMapper,
|
||||
VectorKnowledgeService vectorKnowledgeService
|
||||
) {
|
||||
this.probeService = probeService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.tokenService = tokenService;
|
||||
this.vpdPolicyService = vpdPolicyService;
|
||||
this.userMapper = userMapper;
|
||||
this.vectorKnowledgeService = vectorKnowledgeService;
|
||||
}
|
||||
|
||||
@GetMapping("/probe")
|
||||
public String probe(Model model) {
|
||||
Set<String> defaultObjectKeys = defaultPermissionObjectKeys();
|
||||
List<ProtectedObject> objects =
|
||||
protectedObjectService.findEnabled().stream()
|
||||
.sorted(Comparator
|
||||
.comparing((ProtectedObject object) ->
|
||||
!defaultObjectKeys.contains(object.displayName()))
|
||||
.thenComparing(ProtectedObject::displayName))
|
||||
.toList();
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("defaultObjectKeys", defaultObjectKeys);
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
model.addAttribute("aiEmbeddingConfigured", vectorKnowledgeService.aiEmbeddingConfigured());
|
||||
return "probe";
|
||||
}
|
||||
|
||||
@PostMapping("/probe")
|
||||
public String run(
|
||||
@RequestParam long objectId,
|
||||
@RequestParam(defaultValue = "") String bearerToken,
|
||||
@RequestParam(required = false) Long tempUserId,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(required = false) String requestBody,
|
||||
@RequestParam(defaultValue = "DEMO") String embeddingMode,
|
||||
Model model
|
||||
) {
|
||||
String normalizedToken = bearerToken == null ? "" : bearerToken.trim();
|
||||
ProtectedObject selectedObject = protectedObjectService.findEnabled().stream()
|
||||
.filter(object -> object.objectId() == objectId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
boolean vectorSearch = selectedObject != null
|
||||
&& VectorKnowledgeService.VECTOR_OBJECT.equalsIgnoreCase(selectedObject.objectName());
|
||||
model.addAttribute("vectorSearch", vectorSearch);
|
||||
Long temporaryKeyId = null;
|
||||
if (tempUserId != null) {
|
||||
var issued = tokenService.issueTemporaryToken(tempUserId, "ORDS 검증 임시 실행");
|
||||
normalizedToken = issued.plainToken();
|
||||
temporaryKeyId = issued.keyId();
|
||||
}
|
||||
try {
|
||||
if (vectorSearch) {
|
||||
VectorQueryEmbedding vectorQuery = vectorKnowledgeService.vectorizeQuery(requestBody, embeddingMode);
|
||||
requestBody = vectorQuery.requestBody();
|
||||
model.addAttribute("vectorQuery", vectorQuery.query());
|
||||
model.addAttribute("vectorEmbeddingMode", vectorQuery.embeddingMode());
|
||||
model.addAttribute("vectorEmbeddingModel", vectorQuery.embeddingModel());
|
||||
}
|
||||
model.addAttribute("result", probeService.runProbe(
|
||||
new ProbeCommand(temporaryKeyId, objectId, normalizedToken, limit, requestBody)));
|
||||
model.addAttribute("tokenContext", tokenService.findTokenContextByPlainToken(normalizedToken));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
} finally {
|
||||
if (temporaryKeyId != null) {
|
||||
tokenService.revokeToken(temporaryKeyId, "temporary probe completed");
|
||||
}
|
||||
}
|
||||
model.addAttribute("selectedObject", selectedObject);
|
||||
return "fragments/probe-result :: result";
|
||||
}
|
||||
|
||||
private Set<String> defaultPermissionObjectKeys() {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
vpdPolicyService.findPolicies().stream()
|
||||
.filter(policy -> policy.permissionSystemDefault() && "YES".equalsIgnoreCase(policy.enabled()))
|
||||
.map(policy -> policy.objectDisplayName().toUpperCase())
|
||||
.forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.config.McpProperties;
|
||||
import com.cloudhandson.vpdbackoffice.config.ProductProperties;
|
||||
import com.cloudhandson.vpdbackoffice.service.DataCatalog;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
/** Supplies deployment labels to every server-rendered page. */
|
||||
@ControllerAdvice
|
||||
public class ProductModelAdvice {
|
||||
|
||||
private final ProductProperties product;
|
||||
private final DataCatalog catalog;
|
||||
private final McpProperties mcp;
|
||||
|
||||
public ProductModelAdvice(
|
||||
ProductProperties product,
|
||||
DataCatalog catalog,
|
||||
McpProperties mcp
|
||||
) {
|
||||
this.product = product;
|
||||
this.catalog = catalog;
|
||||
this.mcp = mcp;
|
||||
}
|
||||
|
||||
@ModelAttribute("product")
|
||||
ProductProperties product() {
|
||||
return product;
|
||||
}
|
||||
|
||||
@ModelAttribute("catalogOwner")
|
||||
String catalogOwner() {
|
||||
return catalog.owner();
|
||||
}
|
||||
|
||||
@ModelAttribute("mcp")
|
||||
McpProperties mcp() {
|
||||
return mcp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class ProtectedObjectController {
|
||||
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final OrdsMetadataService ordsMetadataService;
|
||||
|
||||
public ProtectedObjectController(
|
||||
ProtectedObjectService protectedObjectService,
|
||||
OrdsMetadataService ordsMetadataService
|
||||
) {
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.ordsMetadataService = ordsMetadataService;
|
||||
}
|
||||
|
||||
@GetMapping("/objects")
|
||||
public String objects(Model model) {
|
||||
var objects = protectedObjectService.findEnabled();
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects());
|
||||
model.addAttribute("columnsByObject", objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedObjectService.findColumns(object.objectId())
|
||||
)));
|
||||
return "objects";
|
||||
}
|
||||
|
||||
@PostMapping("/objects")
|
||||
public String create(
|
||||
@RequestParam String owner,
|
||||
@RequestParam String objectName,
|
||||
@RequestParam(required = false) String ordsPath,
|
||||
@RequestParam(required = false) String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
protectedObjectService.createObject(
|
||||
new ProtectedObjectCreateCommand(owner, objectName, ordsPath, null, null, description));
|
||||
redirectAttributes.addFlashAttribute("message", "ORDS 조회 Handler 대상을 추가했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
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,
|
||||
@RequestParam String ordsPath,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
protectedObjectService.updateOrdsPath(objectId, ordsPath);
|
||||
redirectAttributes.addFlashAttribute("message", "ORDS Path를 수정했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/objects";
|
||||
}
|
||||
|
||||
@PostMapping("/objects/column-policy")
|
||||
public String updateColumnPolicy(
|
||||
@RequestParam long columnId,
|
||||
@RequestParam String sensitivityLevel,
|
||||
@RequestParam String redactionMethod,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
protectedObjectService.updateColumnPolicy(columnId, sensitivityLevel, redactionMethod);
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 표시 보호 정책을 수정했습니다. 행 접근 권한은 변경되지 않습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/objects";
|
||||
}
|
||||
|
||||
@PostMapping("/objects/ords-handler")
|
||||
public String createOrdsHandler(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
var result = ordsMetadataService.createObjectQueryHandler(objectId);
|
||||
redirectAttributes.addFlashAttribute("message", "ORDS 조회 Handler를 생성했습니다: " + result.ordsPath());
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", error.message());
|
||||
}
|
||||
return "redirect:/objects";
|
||||
}
|
||||
|
||||
@GetMapping("/objects/ords-handler-source")
|
||||
public String ordsHandlerSource(@RequestParam long objectId, Model model) {
|
||||
try {
|
||||
model.addAttribute("object", protectedObjectService.assertEnabled(objectId));
|
||||
model.addAttribute("source", ordsMetadataService.objectQueryHandlerSource(objectId));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "fragments/ords-handler-source :: source";
|
||||
}
|
||||
|
||||
@PostMapping("/objects/disable")
|
||||
public String disable(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
||||
protectedObjectService.disableObject(objectId);
|
||||
redirectAttributes.addFlashAttribute("message", "ORDS 조회 Handler 대상을 비활성화했습니다.");
|
||||
return "redirect:/objects";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.effective.RoleEffectiveImpactView;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.EffectiveMatrixService;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class RoleController {
|
||||
|
||||
private final PermissionService permissionService;
|
||||
private final EffectiveMatrixService effectiveMatrixService;
|
||||
|
||||
public RoleController(PermissionService permissionService, EffectiveMatrixService effectiveMatrixService) {
|
||||
this.permissionService = permissionService;
|
||||
this.effectiveMatrixService = effectiveMatrixService;
|
||||
}
|
||||
|
||||
@GetMapping("/roles")
|
||||
public String roles(Model model) {
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
Map<Long, RoleEffectiveImpactView> roleImpactByRoleId = effectiveMatrixService.matrix().roles().stream()
|
||||
.collect(Collectors.toMap(RoleEffectiveImpactView::roleId, impact -> impact));
|
||||
model.addAttribute("roleImpactByRoleId", roleImpactByRoleId);
|
||||
return "roles";
|
||||
}
|
||||
|
||||
@PostMapping("/roles")
|
||||
public String create(
|
||||
@RequestParam String roleName,
|
||||
@RequestParam(required = false) String description,
|
||||
@RequestParam(defaultValue = "PUBLIC") String maxSensitivityLevel,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
permissionService.createRole(roleName, description, maxSensitivityLevel);
|
||||
redirectAttributes.addFlashAttribute("message", "역할을 추가했습니다.");
|
||||
return "redirect:/roles";
|
||||
}
|
||||
|
||||
@PostMapping("/roles/max-sensitivity")
|
||||
public String updateMaxSensitivity(
|
||||
@RequestParam long roleId,
|
||||
@RequestParam String maxSensitivityLevel,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
permissionService.updateRoleMaxSensitivity(roleId, maxSensitivityLevel);
|
||||
redirectAttributes.addFlashAttribute("message", "역할 민감도 허용 상한을 수정했습니다.");
|
||||
return "redirect:/roles";
|
||||
}
|
||||
|
||||
@PostMapping("/roles/delete")
|
||||
public String delete(
|
||||
@RequestParam long roleId,
|
||||
@RequestParam(defaultValue = "false") boolean confirmImpact,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
permissionService.deleteRole(roleId, confirmImpact);
|
||||
redirectAttributes.addFlashAttribute("message", "역할을 삭제했습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("error", exception.getMessage());
|
||||
}
|
||||
return "redirect:/roles";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
public record RuntimeErrorMessage(
|
||||
String title,
|
||||
String message,
|
||||
boolean showSupportCommand
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
final class RuntimeErrorMessages {
|
||||
|
||||
private RuntimeErrorMessages() {
|
||||
}
|
||||
|
||||
static RuntimeErrorMessage dataAccess(DataAccessException exception) {
|
||||
String detail = detail(exception);
|
||||
if (looksLikeConnectionProblem(exception, detail)) {
|
||||
return new RuntimeErrorMessage(
|
||||
"DB 연결 설정이 필요합니다.",
|
||||
"ADB에 연결할 수 없습니다. BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, "
|
||||
+ "BACKOFFICE_DB_PASSWORD를 확인하고 ./run.sh backoffice-support를 다시 실행하세요. 상세: "
|
||||
+ detail,
|
||||
true
|
||||
);
|
||||
}
|
||||
return new RuntimeErrorMessage(
|
||||
"데이터 처리 오류가 발생했습니다.",
|
||||
"요청을 처리하는 중 DB 데이터 타입, SQL, 또는 매핑 오류가 발생했습니다. "
|
||||
+ "입력값과 백오피스 테이블 스키마를 확인하세요. 상세: " + detail,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
static RuntimeErrorMessage unexpected(Exception exception) {
|
||||
return new RuntimeErrorMessage(
|
||||
"요청 처리 중 오류가 발생했습니다.",
|
||||
"예상하지 못한 오류가 발생했습니다. 입력값을 확인한 뒤 다시 시도하세요. 상세: "
|
||||
+ safeMessage(exception),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean looksLikeConnectionProblem(DataAccessException exception, String detail) {
|
||||
String text = (exception.getMessage() + " " + detail).toLowerCase(Locale.ROOT);
|
||||
return text.contains("connection refused")
|
||||
|| text.contains("the network adapter could not establish the connection")
|
||||
|| text.contains("io error")
|
||||
|| text.contains("ora-01017")
|
||||
|| text.contains("ora-12154")
|
||||
|| text.contains("ora-12514")
|
||||
|| text.contains("ora-12541");
|
||||
}
|
||||
|
||||
private static String detail(DataAccessException exception) {
|
||||
Throwable cause = exception.getMostSpecificCause();
|
||||
if (cause instanceof SQLException sqlException) {
|
||||
return trim(sqlException.getMessage());
|
||||
}
|
||||
return trim(safeMessage(cause));
|
||||
}
|
||||
|
||||
private static String safeMessage(Throwable throwable) {
|
||||
String message = throwable.getMessage();
|
||||
return trim(message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message);
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return "상세 메시지가 없습니다.";
|
||||
}
|
||||
String normalized = value.replaceAll("\\s+", " ").trim();
|
||||
return normalized.length() <= 300 ? normalized : normalized.substring(0, 300) + "...";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.SchemaMetadataService;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class SchemaMetadataController {
|
||||
|
||||
private final SchemaMetadataService schemaMetadataService;
|
||||
|
||||
public SchemaMetadataController(SchemaMetadataService schemaMetadataService) {
|
||||
this.schemaMetadataService = schemaMetadataService;
|
||||
}
|
||||
|
||||
@GetMapping("/schema-metadata")
|
||||
public String schemaMetadata(@RequestParam(required = false) String table, Model model) {
|
||||
String selectedKey = table == null || table.isBlank() ? schemaMetadataService.defaultKey() : table;
|
||||
model.addAttribute("catalog", schemaMetadataService.catalog());
|
||||
model.addAttribute("tables", schemaMetadataService.tables());
|
||||
model.addAttribute("selectedKey", selectedKey);
|
||||
try {
|
||||
model.addAttribute("metadata", schemaMetadataService.find(selectedKey));
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
model.addAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return "schema-metadata";
|
||||
}
|
||||
|
||||
@PostMapping("/schema-metadata/table-comment")
|
||||
public String updateTableComment(
|
||||
@RequestParam String table,
|
||||
@RequestParam(required = false) String comment,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
schemaMetadataService.updateTableComment(table, comment);
|
||||
redirectAttributes.addFlashAttribute("message", "테이블 comment를 저장했습니다.");
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return redirect(table);
|
||||
}
|
||||
|
||||
@PostMapping("/schema-metadata/column-comment")
|
||||
public String updateColumnComment(
|
||||
@RequestParam String table,
|
||||
@RequestParam String column,
|
||||
@RequestParam(required = false) String comment,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
schemaMetadataService.updateColumnComment(table, column, comment);
|
||||
redirectAttributes.addFlashAttribute("message", column + " 컬럼 comment를 저장했습니다.");
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return redirect(table);
|
||||
}
|
||||
|
||||
@PostMapping("/schema-metadata/table-annotation")
|
||||
public String updateTableAnnotation(
|
||||
@RequestParam String table,
|
||||
@RequestParam String annotationName,
|
||||
@RequestParam(required = false) String annotationValue,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
schemaMetadataService.updateTableAnnotation(table, annotationName, annotationValue);
|
||||
redirectAttributes.addFlashAttribute("message", "테이블 annotation을 저장했습니다.");
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return redirect(table);
|
||||
}
|
||||
|
||||
@PostMapping("/schema-metadata/column-annotation")
|
||||
public String updateColumnAnnotation(
|
||||
@RequestParam String table,
|
||||
@RequestParam String column,
|
||||
@RequestParam String annotationName,
|
||||
@RequestParam(required = false) String annotationValue,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
schemaMetadataService.updateColumnAnnotation(table, column, annotationName, annotationValue);
|
||||
redirectAttributes.addFlashAttribute("message", column + " 컬럼 annotation을 저장했습니다.");
|
||||
} catch (AppException | DataAccessException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", safeMessage(exception));
|
||||
}
|
||||
return redirect(table);
|
||||
}
|
||||
|
||||
private String redirect(String table) {
|
||||
return "redirect:/schema-metadata?table=" + (table == null ? "" : table);
|
||||
}
|
||||
|
||||
private String safeMessage(Exception exception) {
|
||||
return exception instanceof AppException
|
||||
? exception.getMessage()
|
||||
: "DB 메타데이터를 저장하지 못했습니다. 권한, 식별자, Oracle annotation 문법을 확인하세요.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
@ControllerAdvice
|
||||
public class SecurityModelAdvice {
|
||||
|
||||
@ModelAttribute("canMutate")
|
||||
public boolean canMutate() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
return authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.anyMatch("ROLE_ADMIN"::equals);
|
||||
}
|
||||
|
||||
@ModelAttribute("readOnlyMode")
|
||||
public boolean readOnlyMode() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication != null && authentication.isAuthenticated() && !canMutate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScriptSummary;
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.SecuritySqlScriptExplanationService;
|
||||
import com.cloudhandson.vpdbackoffice.service.SecuritySqlScriptService;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
/** Read-only page for the curated ASO, ORDS, and Select AI deployment scripts. */
|
||||
@Controller
|
||||
public class SecuritySqlScriptController {
|
||||
|
||||
private final SecuritySqlScriptService securitySqlScriptService;
|
||||
private final SecuritySqlScriptExplanationService explanationService;
|
||||
|
||||
public SecuritySqlScriptController(
|
||||
SecuritySqlScriptService securitySqlScriptService,
|
||||
SecuritySqlScriptExplanationService explanationService
|
||||
) {
|
||||
this.securitySqlScriptService = securitySqlScriptService;
|
||||
this.explanationService = explanationService;
|
||||
}
|
||||
|
||||
@GetMapping("/security-sql-scripts")
|
||||
public String scripts(@RequestParam(required = false) String script, Model model) {
|
||||
List<SecuritySqlScriptSummary> scripts = securitySqlScriptService.list();
|
||||
model.addAttribute("scripts", scripts);
|
||||
if (scripts.isEmpty()) {
|
||||
model.addAttribute("errorMessage", "표시할 보안 SQL 스크립트가 없습니다.");
|
||||
return "security-sql-scripts";
|
||||
}
|
||||
|
||||
String selectedId = script == null || script.isBlank() ? scripts.getFirst().scriptId() : script;
|
||||
try {
|
||||
model.addAttribute("selectedScript", securitySqlScriptService.find(selectedId));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
model.addAttribute("selectedScript", securitySqlScriptService.find(scripts.getFirst().scriptId()));
|
||||
}
|
||||
return "security-sql-scripts";
|
||||
}
|
||||
|
||||
@PostMapping("/security-sql-scripts/explanation")
|
||||
public String explain(@RequestParam String script, Model model) {
|
||||
try {
|
||||
model.addAttribute("explanation", explanationService.explain(script));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "fragments/security-sql-script-explanation :: explanation";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.BackofficeSchemaService;
|
||||
import com.cloudhandson.vpdbackoffice.service.SettingService;
|
||||
import com.cloudhandson.vpdbackoffice.config.McpProperties;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class SettingController {
|
||||
|
||||
private final SettingService settingService;
|
||||
private final BackofficeSchemaService backofficeSchemaService;
|
||||
private final McpProperties mcpProperties;
|
||||
|
||||
public SettingController(
|
||||
SettingService settingService,
|
||||
BackofficeSchemaService backofficeSchemaService,
|
||||
McpProperties mcpProperties
|
||||
) {
|
||||
this.settingService = settingService;
|
||||
this.backofficeSchemaService = backofficeSchemaService;
|
||||
this.mcpProperties = mcpProperties;
|
||||
}
|
||||
|
||||
@GetMapping("/settings")
|
||||
public String settings(Model model) {
|
||||
model.addAttribute("hmmMcpPublicUrl", mcpProperties.resolvedPublicUrl());
|
||||
try {
|
||||
model.addAttribute("ordsBaseUrl", settingService.ordsBaseUrl());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("ordsBaseUrl", "");
|
||||
model.addAttribute("runtimeErrorTitle", error.title());
|
||||
model.addAttribute("runtimeErrorMessage", error.message());
|
||||
}
|
||||
return "settings";
|
||||
}
|
||||
|
||||
@GetMapping("/settings/database")
|
||||
public String databaseSettings(Model model) {
|
||||
try {
|
||||
model.addAttribute("schemaPreflight", backofficeSchemaService.preflight());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("schemaPreflightErrorTitle", error.title());
|
||||
model.addAttribute("schemaPreflightErrorMessage", error.message());
|
||||
}
|
||||
return "settings-database";
|
||||
}
|
||||
|
||||
@PostMapping("/settings/ords")
|
||||
public String updateOrds(
|
||||
@RequestParam String ordsBaseUrl,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
settingService.updateOrdsBaseUrl(ordsBaseUrl);
|
||||
redirectAttributes.addFlashAttribute("message", ordsBaseUrl == null || ordsBaseUrl.isBlank()
|
||||
? "레거시 ORDS 설정을 해제했습니다. HMM MCP 및 HR 질의에는 영향이 없습니다."
|
||||
: "레거시 ORDS 설정을 저장했습니다.");
|
||||
return "redirect:/settings";
|
||||
}
|
||||
|
||||
@PostMapping("/settings/database/initialize")
|
||||
public String initializeSchema(
|
||||
@RequestParam String confirmation,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
if (!"DB 준비 실행".equals(confirmation)) {
|
||||
redirectAttributes.addFlashAttribute(
|
||||
"error", "DB 지원 DDL을 실행하려면 확인 문구 ‘DB 준비 실행’을 정확히 입력하세요.");
|
||||
return "redirect:/settings/database";
|
||||
}
|
||||
var results = backofficeSchemaService.initializeSchema();
|
||||
long failures = results.stream()
|
||||
.filter(result -> "FAILED".equalsIgnoreCase(result.status()))
|
||||
.count();
|
||||
redirectAttributes.addFlashAttribute("schemaResults", results);
|
||||
if (failures > 0) {
|
||||
redirectAttributes.addFlashAttribute("error", "백오피스 지원 DDL 실행 중 " + failures + "개 작업이 실패했습니다. 결과와 SQL을 확인하세요.");
|
||||
} else {
|
||||
redirectAttributes.addFlashAttribute("message", "백오피스 지원 테이블 DDL을 실행했습니다.");
|
||||
}
|
||||
return "redirect:/settings/database";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.StructuredDataService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class StructuredDataController {
|
||||
|
||||
private final StructuredDataService structuredDataService;
|
||||
|
||||
public StructuredDataController(StructuredDataService structuredDataService) {
|
||||
this.structuredDataService = structuredDataService;
|
||||
}
|
||||
|
||||
@GetMapping("/structured-data")
|
||||
public String structuredData(
|
||||
@RequestParam(required = false) String table,
|
||||
Model model
|
||||
) {
|
||||
String selectedKey = table == null || table.isBlank() ? structuredDataService.defaultKey() : table;
|
||||
model.addAttribute("catalog", structuredDataService.catalog());
|
||||
model.addAttribute("tables", structuredDataService.tables());
|
||||
model.addAttribute("selectedKey", selectedKey);
|
||||
try {
|
||||
model.addAttribute("preview", structuredDataService.preview(selectedKey));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "structured-data";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class TokenController {
|
||||
|
||||
private final BearerTokenService tokenService;
|
||||
private final UserService userService;
|
||||
private final Clock clock;
|
||||
|
||||
public TokenController(BearerTokenService tokenService, UserService userService, Clock clock) {
|
||||
this.tokenService = tokenService;
|
||||
this.userService = userService;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@GetMapping("/tokens")
|
||||
public String tokens(
|
||||
@RequestParam(defaultValue = "false") boolean includeInactive,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("tokens", tokenService.findAll(includeInactive));
|
||||
model.addAttribute("includeInactive", includeInactive);
|
||||
model.addAttribute("users", userService.findAll().stream().filter(user -> user.active()).toList());
|
||||
model.addAttribute("defaultExpiresAt", defaultExpiresAt());
|
||||
return "tokens";
|
||||
}
|
||||
|
||||
@PostMapping("/tokens")
|
||||
public String issue(
|
||||
@RequestParam long userId,
|
||||
@RequestParam String expiresAt,
|
||||
@RequestParam(required = false) String description,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
var issued = tokenService.issueToken(new com.cloudhandson.vpdbackoffice.domain.token.TokenIssueCommand(
|
||||
userId, parseBrowserDateTime(expiresAt), description));
|
||||
redirectAttributes.addFlashAttribute("issued", issued);
|
||||
return "redirect:/tokens";
|
||||
}
|
||||
|
||||
@PostMapping("/tokens/revoke")
|
||||
public String revoke(
|
||||
@RequestParam long keyId,
|
||||
@RequestParam(required = false) String reason,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
tokenService.revokeToken(keyId, reason);
|
||||
redirectAttributes.addFlashAttribute("message", "토큰을 회수했습니다.");
|
||||
return "redirect:/tokens";
|
||||
}
|
||||
|
||||
private String defaultExpiresAt() {
|
||||
return LocalDateTime.now(clock.withZone(ZoneId.systemDefault()))
|
||||
.plusMonths(1)
|
||||
.truncatedTo(ChronoUnit.MINUTES)
|
||||
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
|
||||
private OffsetDateTime parseBrowserDateTime(String expiresAt) {
|
||||
return LocalDateTime.parse(expiresAt)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toOffsetDateTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.user.UserCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final PermissionService permissionService;
|
||||
|
||||
public UserController(UserService userService, PermissionService permissionService) {
|
||||
this.userService = userService;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public String users(Model model) {
|
||||
model.addAttribute("users", userService.findAll());
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
model.addAttribute("userRoles", userService.findUserRoles());
|
||||
return "users";
|
||||
}
|
||||
|
||||
@PostMapping("/users")
|
||||
public String create(
|
||||
@RequestParam String username,
|
||||
@RequestParam String empNo,
|
||||
@RequestParam String deptCode,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
userService.createUser(new UserCreateCommand(username, empNo, deptCode));
|
||||
redirectAttributes.addFlashAttribute("message", "사용자를 추가했습니다.");
|
||||
return "redirect:/users";
|
||||
}
|
||||
|
||||
@PostMapping("/users/active")
|
||||
public String active(
|
||||
@RequestParam long userId,
|
||||
@RequestParam boolean active,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
userService.setActive(userId, active);
|
||||
redirectAttributes.addFlashAttribute("message", "사용자 상태를 변경했습니다.");
|
||||
return "redirect:/users";
|
||||
}
|
||||
|
||||
@PostMapping("/users/roles")
|
||||
public String grantRole(
|
||||
@RequestParam long userId,
|
||||
@RequestParam long roleId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
userService.grantRole(userId, roleId);
|
||||
redirectAttributes.addFlashAttribute("message", "역할을 부여했습니다.");
|
||||
return "redirect:/users";
|
||||
}
|
||||
|
||||
@PostMapping("/users/roles/delete")
|
||||
public String revokeRole(
|
||||
@RequestParam long userId,
|
||||
@RequestParam long roleId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
userService.revokeRole(userId, roleId);
|
||||
redirectAttributes.addFlashAttribute("message", "역할을 해제했습니다.");
|
||||
return "redirect:/users";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.MaskingRuleService;
|
||||
import com.cloudhandson.vpdbackoffice.service.UserService;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class UserMaskingRuleController {
|
||||
|
||||
private final MaskingRuleService maskingRuleService;
|
||||
private final UserService userService;
|
||||
|
||||
public UserMaskingRuleController(MaskingRuleService maskingRuleService, UserService userService) {
|
||||
this.maskingRuleService = maskingRuleService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/user-masking-rules")
|
||||
public String userMaskingRules(Model model) {
|
||||
model.addAttribute("users", userService.findAll());
|
||||
model.addAttribute("columnRules", maskingRuleService.findColumnRules().stream()
|
||||
.filter(columnRule -> columnRule.ruleEnabled())
|
||||
.toList());
|
||||
model.addAttribute("userRules", maskingRuleService.findUserRules());
|
||||
return "user-masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/user-masking-rules")
|
||||
public String assign(
|
||||
@RequestParam long userId,
|
||||
@RequestParam long columnId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
maskingRuleService.assignUserRule(userId, columnId, "UNMASK");
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 원문 표시 허용 사용자를 저장했습니다. 이 설정은 행 접근 권한을 추가하지 않습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/user-masking-rules";
|
||||
}
|
||||
|
||||
@PostMapping("/user-masking-rules/delete")
|
||||
public String remove(
|
||||
@RequestParam long userId,
|
||||
@RequestParam long columnId,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
maskingRuleService.removeUserRule(userId, columnId);
|
||||
redirectAttributes.addFlashAttribute("message", "컬럼 원문 표시 허용을 해제하고 컬럼의 기본 컬럼 마스킹 규칙으로 되돌렸습니다.");
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/user-masking-rules";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vector.VectorIngestResult;
|
||||
import com.cloudhandson.vpdbackoffice.service.VectorKnowledgeService;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class VectorKnowledgeController {
|
||||
|
||||
private final VectorKnowledgeService vectorKnowledgeService;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public VectorKnowledgeController(VectorKnowledgeService vectorKnowledgeService, UserMapper userMapper) {
|
||||
this.vectorKnowledgeService = vectorKnowledgeService;
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/vector-knowledge")
|
||||
public String page(Model model) {
|
||||
model.addAttribute("users", userMapper.findAll());
|
||||
model.addAttribute("aiEmbeddingConfigured", vectorKnowledgeService.aiEmbeddingConfigured());
|
||||
try {
|
||||
model.addAttribute("summary", vectorKnowledgeService.summary());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("summary", null);
|
||||
model.addAttribute("runtimeError", error);
|
||||
}
|
||||
return "vector-knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/vector-knowledge/ingest")
|
||||
public String ingest(
|
||||
@RequestParam String documentId,
|
||||
@RequestParam String title,
|
||||
@RequestParam(required = false, defaultValue = "") String sourceUri,
|
||||
@RequestParam String content,
|
||||
@RequestParam String techTags,
|
||||
@RequestParam(defaultValue = "600") int chunkSize,
|
||||
@RequestParam(defaultValue = "DEMO") String embeddingMode,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
VectorIngestResult result = vectorKnowledgeService.ingest(new VectorIngestCommand(
|
||||
documentId, title, sourceUri, content, techTags, chunkSize, embeddingMode));
|
||||
redirectAttributes.addFlashAttribute("ingestResult", result);
|
||||
redirectAttributes.addFlashAttribute("message",
|
||||
result.chunkCount() + "개 청크를 저장했습니다. 태그 " + result.tagCount() + "개가 각 청크에 연결되었습니다.");
|
||||
} catch (Exception exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "redirect:/vector-knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/vector-knowledge/search")
|
||||
public String search(
|
||||
@RequestParam long userId,
|
||||
@RequestParam String query,
|
||||
@RequestParam(defaultValue = "10") int limit,
|
||||
@RequestParam(defaultValue = "DEMO") String embeddingMode,
|
||||
Model model
|
||||
) {
|
||||
try {
|
||||
model.addAttribute("searchResult", vectorKnowledgeService.search(userId, query, limit, embeddingMode));
|
||||
} catch (Exception exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
}
|
||||
return "fragments/vector-search-result :: result";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.mvc.support.RedirectAttributes;
|
||||
|
||||
@Controller
|
||||
public class VpdPolicyController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(VpdPolicyController.class);
|
||||
private final VpdPolicyService vpdPolicyService;
|
||||
|
||||
public VpdPolicyController(VpdPolicyService vpdPolicyService) {
|
||||
this.vpdPolicyService = vpdPolicyService;
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-policies")
|
||||
public String policies(
|
||||
@RequestParam(required = false) String schemaOwner,
|
||||
Model model
|
||||
) {
|
||||
populatePolicyModel(schemaOwner, false, model);
|
||||
return "vpd-policies";
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-filter-policies")
|
||||
public String filterPolicies(
|
||||
@RequestParam(required = false) String schemaOwner,
|
||||
Model model
|
||||
) {
|
||||
populatePolicyModel(schemaOwner, true, model);
|
||||
return "vpd-filter-policies";
|
||||
}
|
||||
|
||||
/** Read-only operational view of the default dynamic permission filter. */
|
||||
@GetMapping("/vpd-filter-runtime")
|
||||
public String filterRuntime(Model model) {
|
||||
try {
|
||||
List<VpdPolicyView> policies = vpdPolicyService.findPolicies().stream()
|
||||
.filter(VpdPolicyView::permissionSystemDefault)
|
||||
.toList();
|
||||
model.addAttribute("policies", policies);
|
||||
if (!policies.isEmpty()) {
|
||||
VpdPolicyView filter = policies.get(0);
|
||||
model.addAttribute("source", vpdPolicyService.findFunctionSource(
|
||||
filter.functionOwner(), filter.packageName(), filter.functionName()));
|
||||
}
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("runtimeError", message);
|
||||
model.addAttribute("policies", List.of());
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
model.addAttribute("policies", List.of());
|
||||
}
|
||||
return "vpd-filter-runtime";
|
||||
}
|
||||
|
||||
private void populatePolicyModel(String schemaOwner, boolean includeFilterEditor, Model model) {
|
||||
try {
|
||||
long started = System.nanoTime();
|
||||
String selectedSchemaOwner = schemaOwner == null ? "" : schemaOwner.trim().toUpperCase();
|
||||
List<com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView> policies = vpdPolicyService.findPolicies();
|
||||
long policiesAt = System.nanoTime();
|
||||
Map<String, String> policyDescriptions = new LinkedHashMap<>();
|
||||
if (!includeFilterEditor) {
|
||||
policyDescriptions.putAll(vpdPolicyService.findPolicyDescriptionMap());
|
||||
policies.forEach(policy -> policyDescriptions.put(
|
||||
policy.objectDisplayName() + "|" + policy.policyName(),
|
||||
policyDescriptions.getOrDefault(
|
||||
policy.objectDisplayName() + "|" + policy.policyName(),
|
||||
policy.objectDisplayName() + "에 요청마다 현재 권한체계의 행 접근 조건을 적용하는 "
|
||||
+ policy.policyName() + " policy입니다."
|
||||
)
|
||||
));
|
||||
}
|
||||
long policyDescriptionsAt = System.nanoTime();
|
||||
model.addAttribute("policies", policies);
|
||||
model.addAttribute("policyDescriptions", policyDescriptions);
|
||||
model.addAttribute("vpdTargets", includeFilterEditor
|
||||
? List.of()
|
||||
: vpdPolicyService.findVpdTargets(selectedSchemaOwner));
|
||||
long targetsAt = System.nanoTime();
|
||||
model.addAttribute("selectedSchemaOwner", selectedSchemaOwner);
|
||||
var formOptions = vpdPolicyService.formOptions();
|
||||
long formOptionsAt = System.nanoTime();
|
||||
Map<String, String> filterDescriptions = new LinkedHashMap<>();
|
||||
filterDescriptions.putAll(vpdPolicyService.findFilterDescriptionMap());
|
||||
policies.forEach(policy -> filterDescriptions.putIfAbsent(
|
||||
policy.functionOwner() + "|" + policy.functionName(),
|
||||
defaultFilterDescription(policy.functionName())
|
||||
));
|
||||
if (includeFilterEditor) {
|
||||
formOptions.functions().forEach(function -> filterDescriptions.putIfAbsent(
|
||||
function.owner() + "|" + function.functionName(),
|
||||
defaultFilterDescription(function.functionName())
|
||||
));
|
||||
}
|
||||
long filterDescriptionsAt = System.nanoTime();
|
||||
Map<String, String> filterPredicates = new LinkedHashMap<>();
|
||||
if (includeFilterEditor) {
|
||||
formOptions.functions().forEach(function -> filterPredicates.put(
|
||||
function.owner() + "|" + function.functionName(),
|
||||
vpdPolicyService.findFilterPredicate(function.owner(), function.packageName(), function.functionName())
|
||||
));
|
||||
}
|
||||
long predicatesAt = System.nanoTime();
|
||||
model.addAttribute("formOptions", formOptions);
|
||||
model.addAttribute("filterDescriptions", filterDescriptions);
|
||||
model.addAttribute("filterPredicates", filterPredicates);
|
||||
log.info("vpd page timings: editor={} policies={}ms policyDescriptions={}ms targets={}ms formOptions={}ms filterDescriptions={}ms predicates={}ms total={}ms",
|
||||
includeFilterEditor,
|
||||
elapsedMillis(started, policiesAt),
|
||||
elapsedMillis(policiesAt, policyDescriptionsAt),
|
||||
elapsedMillis(policyDescriptionsAt, targetsAt),
|
||||
elapsedMillis(targetsAt, formOptionsAt),
|
||||
elapsedMillis(formOptionsAt, filterDescriptionsAt),
|
||||
elapsedMillis(filterDescriptionsAt, predicatesAt),
|
||||
elapsedMillis(started, predicatesAt));
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("runtimeError", message);
|
||||
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());
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
private static String defaultFilterDescription(String functionName) {
|
||||
if ("HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName)) {
|
||||
return "HMM 직원 본인·직접 보고 팀원·HR 관리자 역할을 행 조건으로 변환합니다.";
|
||||
}
|
||||
if ("CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)) {
|
||||
return "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다.";
|
||||
}
|
||||
return "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
|
||||
}
|
||||
|
||||
private static long elapsedMillis(long started, long finished) {
|
||||
return (finished - started) / 1_000_000;
|
||||
}
|
||||
|
||||
@PostMapping("/vpd-policies")
|
||||
public String createPolicy(
|
||||
@RequestParam String objectKey,
|
||||
@RequestParam String policyName,
|
||||
@RequestParam(required = false) String functionKey,
|
||||
@RequestParam(required = false) String functionOwner,
|
||||
@RequestParam(required = false) String functionName,
|
||||
@RequestParam(defaultValue = "SELECT") List<String> statementTypes,
|
||||
@RequestParam(defaultValue = "false") boolean enabled,
|
||||
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||
@RequestParam(required = false) String filterPredicate,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
createPolicyInternal(objectKey, policyName, functionKey, functionOwner, functionName, statementTypes, enabled,
|
||||
updateCheck, filterPredicate, redirectAttributes);
|
||||
return "redirect:/vpd-policies";
|
||||
}
|
||||
|
||||
@PostMapping("/vpd-policies/default")
|
||||
public String createDefaultPolicy(
|
||||
@RequestParam String objectKey,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
vpdPolicyService.createDefaultPermissionPolicy(objectKey);
|
||||
redirectAttributes.addFlashAttribute(
|
||||
"successMessage",
|
||||
"권한체계 자동 필터를 연결했습니다: " + objectKey
|
||||
);
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||
}
|
||||
return "redirect:/vpd-policies";
|
||||
}
|
||||
|
||||
@PostMapping("/vpd-filter-policies/filters")
|
||||
public String saveFilter(
|
||||
@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());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||
}
|
||||
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,
|
||||
@RequestParam String oldPolicyName,
|
||||
@RequestParam String objectKey,
|
||||
@RequestParam String policyName,
|
||||
@RequestParam(required = false) String functionKey,
|
||||
@RequestParam(required = false) String functionOwner,
|
||||
@RequestParam(required = false) String functionName,
|
||||
@RequestParam(defaultValue = "SELECT") List<String> statementTypes,
|
||||
@RequestParam(defaultValue = "false") boolean enabled,
|
||||
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||
@RequestParam(required = false) String filterPredicate,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
String[] objectParts = objectKey.split("\\.", 2);
|
||||
if (objectParts.length != 2) {
|
||||
throw new AppException("조회 대상 형식이 올바르지 않습니다: " + objectKey);
|
||||
}
|
||||
vpdPolicyService.replacePolicy(oldObjectKey, oldPolicyName, new VpdPolicyCreateCommand(
|
||||
objectParts[0],
|
||||
objectParts[1],
|
||||
policyName,
|
||||
functionKey,
|
||||
functionOwner,
|
||||
functionName,
|
||||
String.join(",", statementTypes),
|
||||
enabled,
|
||||
updateCheck,
|
||||
filterPredicate
|
||||
));
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Policy를 수정했습니다: " + policyName);
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||
}
|
||||
return "redirect:/vpd-filter-policies";
|
||||
}
|
||||
|
||||
private void createPolicyInternal(
|
||||
String objectKey,
|
||||
String policyName,
|
||||
String functionKey,
|
||||
String functionOwner,
|
||||
String functionName,
|
||||
List<String> statementTypes,
|
||||
boolean enabled,
|
||||
boolean updateCheck,
|
||||
String filterPredicate,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
String[] objectParts = objectKey.split("\\.", 2);
|
||||
if (objectParts.length != 2) {
|
||||
throw new AppException("조회 대상 형식이 올바르지 않습니다: " + objectKey);
|
||||
}
|
||||
vpdPolicyService.createPolicy(new VpdPolicyCreateCommand(
|
||||
objectParts[0],
|
||||
objectParts[1],
|
||||
policyName,
|
||||
functionKey,
|
||||
functionOwner,
|
||||
functionName,
|
||||
String.join(",", statementTypes),
|
||||
enabled,
|
||||
updateCheck,
|
||||
filterPredicate
|
||||
));
|
||||
redirectAttributes.addFlashAttribute("successMessage", "VPD policy를 등록했습니다: " + policyName);
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/vpd-policies/bulk")
|
||||
public String bulkApplyPolicy(
|
||||
@RequestParam String schemaOwner,
|
||||
@RequestParam(defaultValue = "false") boolean includeTables,
|
||||
@RequestParam(defaultValue = "false") boolean includeViews,
|
||||
@RequestParam(required = false) String policyName,
|
||||
@RequestParam(required = false) String functionKey,
|
||||
@RequestParam(required = false) String functionOwner,
|
||||
@RequestParam(required = false) String functionName,
|
||||
@RequestParam(defaultValue = "SELECT") List<String> statementTypes,
|
||||
@RequestParam(defaultValue = "false") boolean enabled,
|
||||
@RequestParam(defaultValue = "false") boolean updateCheck,
|
||||
@RequestParam(required = false) String filterPredicate,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
bulkApplyPolicyInternal(schemaOwner, includeTables, includeViews, policyName, functionKey, functionOwner, functionName,
|
||||
statementTypes, enabled, updateCheck, filterPredicate, redirectAttributes);
|
||||
return "redirect:/vpd-policies";
|
||||
}
|
||||
|
||||
private void bulkApplyPolicyInternal(
|
||||
String schemaOwner,
|
||||
boolean includeTables,
|
||||
boolean includeViews,
|
||||
String policyName,
|
||||
String functionKey,
|
||||
String functionOwner,
|
||||
String functionName,
|
||||
List<String> statementTypes,
|
||||
boolean enabled,
|
||||
boolean updateCheck,
|
||||
String filterPredicate,
|
||||
RedirectAttributes redirectAttributes
|
||||
) {
|
||||
try {
|
||||
var result = vpdPolicyService.bulkApplySchema(
|
||||
schemaOwner,
|
||||
includeTables,
|
||||
includeViews,
|
||||
policyName,
|
||||
functionKey,
|
||||
functionOwner,
|
||||
functionName,
|
||||
String.join(",", statementTypes),
|
||||
enabled,
|
||||
updateCheck,
|
||||
filterPredicate
|
||||
);
|
||||
redirectAttributes.addFlashAttribute("successMessage", result.summary());
|
||||
} catch (AppException exception) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", message.message());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-policies/function-source")
|
||||
public String functionSource(
|
||||
@RequestParam String owner,
|
||||
@RequestParam(required = false) String packageName,
|
||||
@RequestParam String functionName,
|
||||
Model model
|
||||
) {
|
||||
try {
|
||||
model.addAttribute("source", vpdPolicyService.findFunctionSource(owner, packageName, functionName));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("errorMessage", message.message());
|
||||
}
|
||||
return "fragments/vpd-function-source :: source";
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-policies/policy-detail")
|
||||
public String policyDetail(
|
||||
@RequestParam String objectOwner,
|
||||
@RequestParam String objectName,
|
||||
@RequestParam String policyName,
|
||||
Model model
|
||||
) {
|
||||
try {
|
||||
model.addAttribute("detail", vpdPolicyService.findPolicyDetail(objectOwner, objectName, policyName));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("errorMessage", message.message());
|
||||
}
|
||||
return "fragments/vpd-policy-detail :: detail";
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-policies/object-filter-detail")
|
||||
public String objectFilterDetail(
|
||||
@RequestParam String objectOwner,
|
||||
@RequestParam String objectName,
|
||||
Model model
|
||||
) {
|
||||
try {
|
||||
model.addAttribute("detail", vpdPolicyService.findObjectFilterDetail(objectOwner, objectName));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("errorMessage", message.message());
|
||||
}
|
||||
return "fragments/vpd-object-filter-detail :: detail";
|
||||
}
|
||||
|
||||
@GetMapping("/vpd-policies/policy-explanation")
|
||||
public String policyExplanation(
|
||||
@RequestParam String objectOwner,
|
||||
@RequestParam String objectName,
|
||||
@RequestParam String policyName,
|
||||
Model model
|
||||
) {
|
||||
try {
|
||||
model.addAttribute("explanation", vpdPolicyService.explainPolicy(objectOwner, objectName, policyName));
|
||||
} catch (AppException exception) {
|
||||
model.addAttribute("errorMessage", exception.getMessage());
|
||||
} catch (DataAccessException exception) {
|
||||
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("errorMessage", message.message());
|
||||
}
|
||||
return "fragments/vpd-policy-explanation :: explanation";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user