refs #707: enforce HMM leave team VPD
This commit is contained in:
@@ -67,11 +67,14 @@ public record PermissionView(
|
||||
if (upper.startsWith("STATIC_SQL ")) {
|
||||
return "정적 SQL 조건: " + rawRule.substring("STATIC_SQL ".length());
|
||||
}
|
||||
if (upper.contains(" MY_DEPT")) {
|
||||
if ("MY_DEPT".equals(upper) || upper.contains(" MY_DEPT")) {
|
||||
return "내 부서 행";
|
||||
}
|
||||
if (upper.contains(" SELF")) {
|
||||
return "내 사번/소유자 행";
|
||||
if ("MANAGED_TEAM".equals(upper) || upper.contains(" MANAGED_TEAM")) {
|
||||
return "본인 및 직접 보고 팀원 행";
|
||||
}
|
||||
if ("SELF".equals(upper) || upper.contains(" SELF")) {
|
||||
return "본인 직원 행";
|
||||
}
|
||||
if (upper.contains(" TAG ")) {
|
||||
return "태그 조건: " + rawRule;
|
||||
|
||||
@@ -17,6 +17,7 @@ public record VpdFunctionOption(
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,21 @@ public record VpdPolicyFormOptions(
|
||||
|
||||
public String defaultPermissionFunctionKey() {
|
||||
return functions.stream()
|
||||
.filter(function -> "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(function.functionName()))
|
||||
.filter(VpdFunctionOption::permissionSystemDefault)
|
||||
.findFirst()
|
||||
.map(VpdFunctionOption::value)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
public String defaultPermissionFunctionKey(String objectName) {
|
||||
String preferredFunction = objectName != null
|
||||
&& objectName.toUpperCase(java.util.Locale.ROOT).startsWith("HMM_LEAVE_")
|
||||
? "HMM_LEAVE_VPD_FILTER"
|
||||
: "CB_AGENT_DOC_VPD_FILTER";
|
||||
return functions.stream()
|
||||
.filter(function -> preferredFunction.equalsIgnoreCase(function.functionName()))
|
||||
.findFirst()
|
||||
.map(VpdFunctionOption::value)
|
||||
.orElseGet(this::defaultPermissionFunctionKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public record VpdPolicyView(
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,18 +333,16 @@ public class BackofficeSchemaService {
|
||||
);
|
||||
}
|
||||
|
||||
addCountCheck(checks, "VPD Runtime", "CB_AGENT_CTX", "CONTEXT",
|
||||
"SELECT COUNT(*) FROM all_context WHERE namespace = 'CB_AGENT_CTX'",
|
||||
"sqlcl에서 17_agent_ords_security_local_vpd_setup.sql 또는 CREATE CONTEXT를 실행하세요.",
|
||||
addCountCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX", "CONTEXT",
|
||||
"SELECT COUNT(*) FROM all_context WHERE namespace = 'HMM_ACCESS_CTX'",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.",
|
||||
true);
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG", "PACKAGE",
|
||||
"sqlcl에서 cb_agent_ctx_pkg package를 생성/컴파일하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG BODY", "PACKAGE BODY",
|
||||
"sqlcl에서 cb_agent_ctx_pkg package body를 생성/컴파일하세요.", "CB_AGENT_CTX_PKG");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_DOC_VPD_FILTER", "FUNCTION",
|
||||
"sqlcl에서 26_agent_ords_security_dynamic_vpd_filter.sql을 실행하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CAN_READ_COLUMN", "FUNCTION",
|
||||
"sqlcl에서 backoffice support redaction function을 생성/컴파일하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX_PKG", "PACKAGE",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX_PKG BODY", "PACKAGE BODY",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.", "HMM_ACCESS_CTX_PKG");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "HMM_LEAVE_VPD_FILTER", "FUNCTION",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.");
|
||||
addPolicyCheck(checks);
|
||||
|
||||
addCountCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "USER",
|
||||
@@ -360,8 +358,6 @@ public class BackofficeSchemaService {
|
||||
addAllObjectCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "CB_ORDS_HANDLER_PKG", "PACKAGE",
|
||||
"CB_ORDS로 접속해 22_agent_ords_security_ords_handler_setup.sql을 실행하세요.");
|
||||
|
||||
addGrantCheck(checks, currentUser, "CB_AGENT_CTX_PKG", "EXECUTE");
|
||||
addGrantCheck(checks, currentUser, "CB_AGENT_CAN_READ_COLUMN", "EXECUTE");
|
||||
addProtectedObjectGrantCheck(checks);
|
||||
|
||||
return new SchemaPreflightView(currentUser, ORDS_SCHEMA, checks, appDdlPreview(), sqlclScript(currentUser));
|
||||
@@ -597,14 +593,14 @@ public class BackofficeSchemaService {
|
||||
SELECT COUNT(*)
|
||||
FROM all_policies
|
||||
WHERE pf_owner = USER
|
||||
AND function = 'CB_AGENT_DOC_VPD_FILTER'
|
||||
AND function = 'HMM_LEAVE_VPD_FILTER'
|
||||
""");
|
||||
checks.add(new SchemaPreflightCheck(
|
||||
"VPD Runtime",
|
||||
"CB_AGENT_DOC_VPD_FILTER 적용 policy",
|
||||
"HMM_LEAVE_VPD_FILTER 적용 policy",
|
||||
"DBMS_RLS POLICY",
|
||||
count > 0 ? "OK" : "WARN",
|
||||
count > 0 ? count + "개 policy가 기본 filter를 사용합니다." : "기본 filter가 적용된 TABLE/VIEW가 없습니다.",
|
||||
count > 0 ? count + "개 HMM 휴가 policy가 기본 filter를 사용합니다." : "HMM 휴가 filter가 적용된 TABLE/VIEW가 없습니다.",
|
||||
count > 0 ? "조치 없음" : "운영 > VPD 설정에서 개별 또는 벌크 적용을 실행하세요.",
|
||||
false
|
||||
));
|
||||
@@ -640,9 +636,14 @@ public class BackofficeSchemaService {
|
||||
}
|
||||
|
||||
private void addProtectedObjectGrantCheck(List<SchemaPreflightCheck> checks) {
|
||||
if (!exists("SELECT COUNT(*) FROM user_tables WHERE table_name = 'CB_PROTECTED_OBJECT'")) {
|
||||
if (!exists("""
|
||||
SELECT COUNT(*)
|
||||
FROM user_objects
|
||||
WHERE object_name = 'CB_PROTECTED_OBJECT'
|
||||
AND object_type IN ('TABLE', 'VIEW')
|
||||
""")) {
|
||||
checks.add(new SchemaPreflightCheck("Grants", "보호 객체 SELECT grants", "GRANT", "WARN",
|
||||
"CB_PROTECTED_OBJECT 테이블이 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
|
||||
"CB_PROTECTED_OBJECT 객체가 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
|
||||
"지원 테이블 생성/확인 후 다시 점검하세요.", false));
|
||||
return;
|
||||
}
|
||||
@@ -706,26 +707,16 @@ public class BackofficeSchemaService {
|
||||
private String sqlclScript(String currentUser) {
|
||||
String owner = currentUser == null || currentUser.isBlank() ? "ADMIN" : currentUser;
|
||||
return """
|
||||
-- sqlcl에서 실행할 VPD/ORDS runtime 준비 순서
|
||||
-- 1. ADMIN 또는 보호 객체 owner로 접속
|
||||
@sql/adb/17_agent_ords_security_local_vpd_setup.sql
|
||||
-- sqlcl에서 실행할 HMM VPD runtime 준비 순서
|
||||
-- 1. ADMIN으로 접속
|
||||
@sql/adb/25_agent_ords_security_backoffice_support.sql
|
||||
@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
|
||||
@sql/adb/62_kb_aso_masking_backoffice_metadata.sql
|
||||
@sql/adb/21_agent_ords_security_ords_enable_schema.sql
|
||||
@sql/adb/72_hmm_leave_team_vpd.sql
|
||||
|
||||
-- 2. ORDS parsing schema로 접속
|
||||
CONNECT cb_ords/<password>@<tns_alias>
|
||||
@sql/adb/22_agent_ords_security_ords_handler_setup.sql
|
||||
|
||||
-- 3. 대표 권한 부여 SQL
|
||||
-- 2. 비면제 업무 runtime 사용자에 필요한 최소 권한
|
||||
CONNECT %s/<password>@<tns_alias>
|
||||
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
|
||||
GRANT SELECT ON <owner>.<table_or_view> TO cb_ords;
|
||||
|
||||
-- 4. 마스킹 규칙을 UI에서 컬럼에 연결한 뒤 실행
|
||||
@sql/adb/64_kb_aso_masking_default_column_rules.sql
|
||||
@sql/adb/63_kb_aso_masking_rule_runtime.sql
|
||||
GRANT EXECUTE ON hmm_access_ctx_pkg TO <hmm_runtime_user>;
|
||||
GRANT SELECT ON hmm_leave_balances TO <hmm_runtime_user>;
|
||||
GRANT SELECT ON hmm_leave_requests TO <hmm_runtime_user>;
|
||||
""".formatted(owner.toLowerCase());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class PermissionService {
|
||||
|
||||
private static final Set<String> RULE_TYPES = Set.of(
|
||||
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||
"ALL", "=", "!=", "MANAGED_TEAM", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL",
|
||||
"TOKEN_SUBJECT", "OWN_CONTRACT", "CHANNEL_CONTRACT", "OWN_CUSTOMER", "CHANNEL_CUSTOMER",
|
||||
"STATIC_SQL");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG", "STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL", "STATIC_SQL");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
|
||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
|
||||
"MANAGED_TEAM", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
|
||||
private static final Set<String> STATIC_SQL_KEYWORDS = Set.of(
|
||||
"AND", "OR", "NOT", "NULL", "IS", "IN", "LIKE", "BETWEEN", "ESCAPE", "TRUE", "FALSE",
|
||||
"UPPER", "LOWER", "TRIM", "NVL", "COALESCE", "TO_CHAR", "TO_DATE", "REGEXP_LIKE", "INSTR",
|
||||
|
||||
@@ -39,6 +39,8 @@ public class VpdPolicyService {
|
||||
private static final long CATALOG_CACHE_MILLIS = 15 * 60_000L;
|
||||
private static final String COMMON_POLICY_NAME = "CB_PERMISSION_SELECT_POLICY";
|
||||
private static final String DEFAULT_PERMISSION_FILTER_FUNCTION = "CB_AGENT_DOC_VPD_FILTER";
|
||||
private static final String HMM_LEAVE_POLICY_NAME = "HMM_LEAVE_SCOPE_POLICY";
|
||||
private static final String HMM_LEAVE_FILTER_FUNCTION = "HMM_LEAVE_VPD_FILTER";
|
||||
/*
|
||||
* DBMS_RLS.ADD_POLICY accepts PL/SQL BOOLEAN arguments. Keep all four
|
||||
* permitted flag combinations as fixed statements: database object names
|
||||
@@ -154,8 +156,8 @@ 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
|
||||
if (isSystemManagedPermissionFilter(functionName)) {
|
||||
throw new AppException("기본 동적 권한 필터 " + functionName
|
||||
+ "는 이 화면에서 수정할 수 없습니다. 권한체계는 사용자·그룹·역할·행 접근 규칙 화면에서 변경하세요.");
|
||||
}
|
||||
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
|
||||
@@ -180,15 +182,18 @@ public class VpdPolicyService {
|
||||
if (objectParts.length != 2) {
|
||||
throw new AppException("보호할 객체 형식이 올바르지 않습니다: " + objectKey);
|
||||
}
|
||||
String functionKey = formOptions().defaultPermissionFunctionKey();
|
||||
String functionKey = formOptions().defaultPermissionFunctionKey(objectParts[1]);
|
||||
if (functionKey.isBlank()) {
|
||||
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
|
||||
throw new AppException("대상 객체에 사용할 기본 동적 권한 필터"
|
||||
+ "가 설치되어 있지 않습니다. 운영 상태에서 동적 권한 필터 설치 여부를 확인한 뒤 다시 적용하세요.");
|
||||
}
|
||||
String policyName = isHmmLeaveObject(objectParts[1])
|
||||
? HMM_LEAVE_POLICY_NAME
|
||||
: COMMON_POLICY_NAME;
|
||||
createPolicy(new VpdPolicyCreateCommand(
|
||||
objectParts[0],
|
||||
objectParts[1],
|
||||
COMMON_POLICY_NAME,
|
||||
policyName,
|
||||
functionKey,
|
||||
null,
|
||||
null,
|
||||
@@ -352,8 +357,8 @@ public class VpdPolicyService {
|
||||
description = null;
|
||||
}
|
||||
return description == null || description.isBlank()
|
||||
? (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
|
||||
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
|
||||
? (isSystemManagedPermissionFilter(functionName)
|
||||
? defaultPermissionFilterDescription(functionName)
|
||||
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.")
|
||||
: description;
|
||||
}
|
||||
@@ -377,7 +382,7 @@ public class VpdPolicyService {
|
||||
*/
|
||||
public String findFilterPredicate(String owner, String packageName, String functionName) {
|
||||
if (packageName != null && !packageName.isBlank()
|
||||
|| DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
|| isSystemManagedPermissionFilter(functionName)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
@@ -493,10 +498,11 @@ public class VpdPolicyService {
|
||||
) {
|
||||
List<VpdPolicyTemplateOption> templates = new ArrayList<>();
|
||||
functions.stream()
|
||||
.filter(function -> DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(function.functionName()))
|
||||
.findFirst()
|
||||
.ifPresent(function -> templates.add(new VpdPolicyTemplateOption(
|
||||
COMMON_POLICY_NAME,
|
||||
.filter(VpdFunctionOption::permissionSystemDefault)
|
||||
.forEach(function -> templates.add(new VpdPolicyTemplateOption(
|
||||
HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(function.functionName())
|
||||
? HMM_LEAVE_POLICY_NAME
|
||||
: COMMON_POLICY_NAME,
|
||||
function.owner(),
|
||||
function.packageName(),
|
||||
function.functionName(),
|
||||
@@ -746,6 +752,23 @@ public class VpdPolicyService {
|
||||
return formOptions().defaultPermissionFunctionKey();
|
||||
}
|
||||
|
||||
private boolean isSystemManagedPermissionFilter(String functionName) {
|
||||
return DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
|
||||
|| HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(functionName);
|
||||
}
|
||||
|
||||
private boolean isHmmLeaveObject(String objectName) {
|
||||
return objectName != null
|
||||
&& objectName.toUpperCase(Locale.ROOT).startsWith("HMM_LEAVE_");
|
||||
}
|
||||
|
||||
private String defaultPermissionFilterDescription(String functionName) {
|
||||
if (HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
return "HMM 직원 본인·직접 보고 팀원·HR 관리자 역할을 행 조건으로 변환합니다.";
|
||||
}
|
||||
return "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다.";
|
||||
}
|
||||
|
||||
private String normalizeStatementTypes(String value) {
|
||||
String raw = value == null || value.isBlank() ? "SELECT" : value;
|
||||
List<String> statements = List.of(raw.split(",")).stream()
|
||||
|
||||
@@ -153,9 +153,13 @@ public class VpdPolicyController {
|
||||
}
|
||||
|
||||
private static String defaultFilterDescription(String functionName) {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
|
||||
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user