fix #557: guide permission-driven VPD flow
This commit is contained in:
@@ -44,4 +44,65 @@ public record ProbeResult(
|
||||
responseBody
|
||||
);
|
||||
}
|
||||
|
||||
public boolean successLike() {
|
||||
return status == ProbeStatus.SUCCESS || status == ProbeStatus.VPD_DENY_EMPTY_RESULT;
|
||||
}
|
||||
|
||||
public String title() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "권한에 따라 데이터를 볼 수 있습니다.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "현재 권한으로 볼 수 있는 행이 없습니다.";
|
||||
case TOKEN_NOT_FOUND -> "DB에 등록되지 않은 토큰입니다.";
|
||||
case TOKEN_INACTIVE -> "만료되었거나 회수된 토큰입니다.";
|
||||
case INVALID_TOKEN -> "입력한 토큰 정보가 일치하지 않습니다.";
|
||||
case OBJECT_DISABLED -> "검증 대상이 비활성 상태입니다.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "현재 권한으로 이 대상에 접근할 수 없습니다.";
|
||||
case VPD_FILTER_ERROR -> "이 객체에 연결된 별도 VPD Filter가 실행되지 않습니다.";
|
||||
case ORDS_PATH_NOT_FOUND -> "검증 대상의 ORDS 경로를 찾지 못했습니다.";
|
||||
case ORDS_NOT_CONFIGURED -> "ORDS 연결 주소가 아직 설정되지 않았습니다.";
|
||||
case ORDS_UNAVAILABLE -> "ORDS 서버에 연결할 수 없습니다.";
|
||||
case ORDS_TIMEOUT -> "ORDS 응답을 기다리다 시간이 초과되었습니다.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS 응답 형식을 해석할 수 없습니다.";
|
||||
case UNKNOWN_ERROR -> "검증 중 예상하지 못한 문제가 발생했습니다.";
|
||||
};
|
||||
}
|
||||
|
||||
public String plainSummary() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "토큰의 사용자와 역할을 기준으로 VPD가 적용되었고, 허용된 데이터 " + rowCount
|
||||
+ "개가 반환되었습니다.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "호출은 정상 처리됐지만 VPD가 현재 사용자에게 허용한 행은 0개입니다. 권한 규칙과 실제 데이터가 맞지 않으면 정상 결과이며 오류가 아닐 수 있습니다.";
|
||||
case TOKEN_NOT_FOUND -> "입력한 원문과 일치하는 등록 기록이 현재 DB에 없습니다. 예전에 발급한 값이거나 다른 환경의 토큰일 수 있습니다.";
|
||||
case TOKEN_INACTIVE -> "토큰은 DB에 있지만 만료되었거나 관리자가 회수해 더 이상 사용자 권한을 증명할 수 없습니다.";
|
||||
case INVALID_TOKEN -> "화면에서 선택한 정보와 입력한 토큰 원문이 서로 다릅니다.";
|
||||
case OBJECT_DISABLED -> "선택한 객체가 검증 대상으로 활성화되어 있지 않아 ORDS 호출을 시작하지 않았습니다.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "토큰은 확인됐지만 ORDS 또는 DB가 이 객체에 대한 접근을 거부했습니다.";
|
||||
case VPD_FILTER_ERROR -> "토큰과 사용자 권한은 확인됐지만, 대상 객체의 VPD 함수가 DB 오류를 내어 행 필터를 계산하지 못했습니다.";
|
||||
case ORDS_PATH_NOT_FOUND -> "권한 판단 전 단계에서 실제 ORDS 주소와 등록된 객체 경로가 일치하지 않았습니다.";
|
||||
case ORDS_NOT_CONFIGURED -> "백오피스가 호출할 ORDS 기준 주소가 없어 권한 검증을 시작하지 못했습니다.";
|
||||
case ORDS_UNAVAILABLE -> "권한 판단 전 단계에서 ORDS 서버 또는 네트워크에 연결하지 못했습니다.";
|
||||
case ORDS_TIMEOUT -> "ORDS가 제한 시간 안에 응답하지 않아 권한 결과를 확인하지 못했습니다.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS 호출은 끝났지만 rows/items 배열이 없는 응답이라 권한 결과로 표시하지 못했습니다.";
|
||||
case UNKNOWN_ERROR -> "토큰, 권한, ORDS 중 어느 단계의 문제인지 기술 상세를 확인해야 합니다.";
|
||||
};
|
||||
}
|
||||
|
||||
public String nextAction() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "반환된 행과 마스킹 컬럼이 예상한 범위인지 확인하세요. 다르면 권한 화면의 행·열 규칙을 조정한 뒤 다시 검증하세요.";
|
||||
case VPD_DENY_EMPTY_RESULT -> "유효 권한 화면에서 사용자에게 직접 또는 그룹으로 상속된 역할과 행 규칙을 확인하세요.";
|
||||
case TOKEN_NOT_FOUND -> "토큰 화면에서 현재 환경의 사용자에게 새 토큰을 발급하고, 한 번만 표시되는 원문을 복사해 다시 검증하세요.";
|
||||
case TOKEN_INACTIVE -> "토큰 화면에서 활성 토큰을 새로 발급한 뒤 다시 검증하세요.";
|
||||
case INVALID_TOKEN -> "복사한 원문이 맞는지 확인하고, 원문을 잃었다면 새 토큰을 발급하세요.";
|
||||
case OBJECT_DISABLED -> "보호 객체를 활성화하고 권한을 등록한 뒤 다시 검증하세요.";
|
||||
case OBJECT_NOT_ACCESSIBLE -> "유효 권한과 ORDS handler의 대상 객체가 같은지 확인하세요.";
|
||||
case VPD_FILTER_ERROR -> "토큰이나 권한을 바꾸지 말고 DB 보호 연결에서 이 객체의 Filter를 확인하세요. 일반 권한 객체라면 권한체계 자동 Filter로 복구하세요.";
|
||||
case ORDS_PATH_NOT_FOUND -> "보호 객체의 ORDS 경로와 실제 module/template 경로를 맞춘 뒤 다시 실행하세요.";
|
||||
case ORDS_NOT_CONFIGURED -> "설정에서 ORDS 기준 주소를 등록한 뒤 백오피스를 재시작하세요.";
|
||||
case ORDS_UNAVAILABLE, ORDS_TIMEOUT -> "권한 설정을 바꾸지 말고 먼저 ORDS 실행 상태, 주소와 네트워크를 확인하세요.";
|
||||
case INVALID_ORDS_RESPONSE -> "ORDS handler가 rows 또는 items 배열을 반환하는지 확인하세요.";
|
||||
case UNKNOWN_ERROR -> "아래 기술 상세의 오류 코드와 응답을 확인한 뒤 해당 단계부터 점검하세요.";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public enum ProbeStatus {
|
||||
OBJECT_DISABLED,
|
||||
INVALID_TOKEN,
|
||||
OBJECT_NOT_ACCESSIBLE,
|
||||
VPD_FILTER_ERROR,
|
||||
ORDS_PATH_NOT_FOUND,
|
||||
ORDS_NOT_CONFIGURED,
|
||||
ORDS_UNAVAILABLE,
|
||||
|
||||
@@ -15,4 +15,8 @@ public record VpdFunctionOption(
|
||||
public String label() {
|
||||
return value() + " / " + objectType;
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,8 @@ public record VpdPolicyView(
|
||||
String packagePrefix = packageName == null || packageName.isBlank() ? "" : packageName + ".";
|
||||
return functionOwner + "." + packagePrefix + functionName;
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,17 @@ public class BearerTokenService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public TokenContextView findTokenContextByPlainToken(String plainToken) {
|
||||
BearerTokenRecord record = findByPlainToken(plainToken);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return findTokenContextOptions().stream()
|
||||
.filter(context -> context.keyId() == record.keyId())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public BearerTokenRecord findById(long keyId) {
|
||||
return tokenMapper.findById(keyId);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,10 @@ public class OrdsProbeService {
|
||||
token = tokenService.findByPlainToken(command.bearerToken());
|
||||
if (token == null) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다."));
|
||||
ProbeStatus.TOKEN_NOT_FOUND,
|
||||
"TOKEN_NOT_FOUND",
|
||||
"현재 DB에 등록된 토큰이 아닙니다. 토큰 화면에서 이 환경의 새 토큰을 발급하세요."
|
||||
));
|
||||
}
|
||||
} else {
|
||||
token = tokenService.findById(command.tokenKeyId());
|
||||
|
||||
@@ -20,6 +20,9 @@ public class ProbeErrorClassifier {
|
||||
if (text.contains("ORA-00942") || text.contains("ORA-01031")) {
|
||||
return ProbeStatus.OBJECT_NOT_ACCESSIBLE;
|
||||
}
|
||||
if (text.contains("ORA-28110") || text.contains("SQL Error Code 28110")) {
|
||||
return ProbeStatus.VPD_FILTER_ERROR;
|
||||
}
|
||||
if (status != null && (status.value() == 401 || status.value() == 403)) {
|
||||
return ProbeStatus.INVALID_TOKEN;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@ public class VpdPolicyService {
|
||||
|
||||
@Transactional
|
||||
public void saveFilterFunction(String functionOwnerValue, String functionNameValue, String filterPredicateValue) {
|
||||
String functionName = requiredIdentifier(functionNameValue, "Function name");
|
||||
if (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
|
||||
+ "는 이 화면에서 수정할 수 없습니다. 권한체계는 사용자·그룹·역할·권한 규칙 화면에서 변경하세요.");
|
||||
}
|
||||
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
|
||||
String functionOwner = functionOwnerValue == null || functionOwnerValue.isBlank()
|
||||
? currentUser
|
||||
@@ -104,7 +109,6 @@ public class VpdPolicyService {
|
||||
throw new AppException("Filter function 등록/수정은 현재 연결 사용자 스키마에만 가능합니다. 현재 사용자: "
|
||||
+ currentUser + ", Function owner: " + functionOwner);
|
||||
}
|
||||
String functionName = requiredIdentifier(functionNameValue, "Function name");
|
||||
String filterPredicate = filterPredicateValue == null ? "" : filterPredicateValue.trim();
|
||||
if (filterPredicate.isBlank()) {
|
||||
throw new AppException("Filter predicate는 필수입니다.");
|
||||
@@ -113,6 +117,31 @@ public class VpdPolicyService {
|
||||
clearCatalogCache();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createDefaultPermissionPolicy(String objectKey) {
|
||||
String[] objectParts = objectKey == null ? new String[0] : objectKey.split("\\.", 2);
|
||||
if (objectParts.length != 2) {
|
||||
throw new AppException("보호할 객체 형식이 올바르지 않습니다: " + objectKey);
|
||||
}
|
||||
String functionKey = formOptions().defaultPermissionFunctionKey();
|
||||
if (functionKey.isBlank()) {
|
||||
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
|
||||
+ "가 설치되어 있지 않습니다. 운영 상태에서 동적 권한 필터 설치 여부를 확인한 뒤 다시 적용하세요.");
|
||||
}
|
||||
createPolicy(new VpdPolicyCreateCommand(
|
||||
objectParts[0],
|
||||
objectParts[1],
|
||||
COMMON_POLICY_NAME,
|
||||
functionKey,
|
||||
null,
|
||||
null,
|
||||
"SELECT",
|
||||
true,
|
||||
false,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void replacePolicy(String oldObjectKey, String oldPolicyName, VpdPolicyCreateCommand command) {
|
||||
String[] objectParts = oldObjectKey == null ? new String[0] : oldObjectKey.split("\\.", 2);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.cloudhandson.vpdbackoffice.service.BearerTokenService;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsProbeService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import com.cloudhandson.vpdbackoffice.service.VpdPolicyService;
|
||||
import 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;
|
||||
@@ -16,33 +22,58 @@ public class ProbeController {
|
||||
private final OrdsProbeService probeService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final BearerTokenService tokenService;
|
||||
private final VpdPolicyService vpdPolicyService;
|
||||
|
||||
public ProbeController(
|
||||
OrdsProbeService probeService,
|
||||
ProtectedObjectService protectedObjectService,
|
||||
BearerTokenService tokenService
|
||||
BearerTokenService tokenService,
|
||||
VpdPolicyService vpdPolicyService
|
||||
) {
|
||||
this.probeService = probeService;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.tokenService = tokenService;
|
||||
this.vpdPolicyService = vpdPolicyService;
|
||||
}
|
||||
|
||||
@GetMapping("/probe")
|
||||
public String probe(Model model) {
|
||||
model.addAttribute("objects", protectedObjectService.findEnabled());
|
||||
model.addAttribute("tokens", tokenService.findTokenContextOptions());
|
||||
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);
|
||||
return "probe";
|
||||
}
|
||||
|
||||
@PostMapping("/probe")
|
||||
public String run(
|
||||
@RequestParam(required = false) Long tokenKeyId,
|
||||
@RequestParam long objectId,
|
||||
@RequestParam String bearerToken,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("result", probeService.runProbe(new ProbeCommand(tokenKeyId, objectId, bearerToken, limit)));
|
||||
String normalizedToken = bearerToken == null ? "" : bearerToken.trim();
|
||||
model.addAttribute("result", probeService.runProbe(new ProbeCommand(null, objectId, normalizedToken, limit)));
|
||||
model.addAttribute("tokenContext", tokenService.findTokenContextByPlainToken(normalizedToken));
|
||||
model.addAttribute("selectedObject", protectedObjectService.findEnabled().stream()
|
||||
.filter(object -> object.objectId() == objectId)
|
||||
.findFirst()
|
||||
.orElse(null));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,26 @@ public class VpdPolicyController {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user