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) {
|
||||
|
||||
@@ -61,24 +61,67 @@
|
||||
|
||||
<select id="countPermissionsByRoleId" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM dual
|
||||
WHERE 1 = 0
|
||||
FROM cb_permission
|
||||
WHERE role_id = #{roleId,jdbcType=NUMERIC}
|
||||
</select>
|
||||
|
||||
<select id="findPermissionViews" resultType="com.cloudhandson.vpdbackoffice.domain.permission.PermissionView">
|
||||
SELECT CAST(NULL AS NUMBER) AS permission_id,
|
||||
CAST(NULL AS NUMBER) AS role_id,
|
||||
CAST(NULL AS VARCHAR2(80)) AS role_name,
|
||||
CAST(NULL AS NUMBER) AS object_id,
|
||||
CAST(NULL AS VARCHAR2(200)) AS object_name,
|
||||
CAST(NULL AS VARCHAR2(30)) AS action,
|
||||
CAST(NULL AS VARCHAR2(20)) AS permission_effect,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS rules,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS visible_columns,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS filter_preview,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS null_policy_preview
|
||||
FROM dual
|
||||
WHERE 1 = 0
|
||||
SELECT p.perm_id AS permission_id,
|
||||
r.role_id,
|
||||
r.role_name,
|
||||
NVL(o.object_id, -1) AS object_id,
|
||||
p.target_name AS object_name,
|
||||
p.action_name AS action,
|
||||
NVL(p.permission_effect, 'ALLOW') AS permission_effect,
|
||||
LISTAGG(
|
||||
NVL2(pr.rule_column, pr.rule_column || ' ', '') ||
|
||||
pr.rule_type ||
|
||||
NVL2(pr.rule_value, ' ' || pr.rule_value, ''),
|
||||
', '
|
||||
) WITHIN GROUP (ORDER BY pr.rule_id) AS rules,
|
||||
(
|
||||
SELECT LISTAGG(pc.column_name, ', ')
|
||||
WITHIN GROUP (ORDER BY pc.column_name)
|
||||
FROM cb_permission_column pc
|
||||
WHERE pc.permission_id = p.perm_id
|
||||
) AS visible_columns,
|
||||
(
|
||||
SELECT LISTAGG(
|
||||
CASE
|
||||
WHEN pr2.rule_type = 'ALL' THEN '1 = 1'
|
||||
WHEN pr2.rule_type = 'MANAGED_TEAM' THEN
|
||||
NVL(pr2.rule_column, 'EMPLOYEE_ID')
|
||||
|| ' IN (current employee and direct reports)'
|
||||
WHEN pr2.rule_type = 'SELF' THEN
|
||||
NVL(pr2.rule_column, 'EMPLOYEE_ID')
|
||||
|| ' = SYS_CONTEXT(HMM_ACCESS_CTX.EMPLOYEE_ID)'
|
||||
WHEN pr2.rule_type = 'STATIC_SQL' THEN pr2.rule_value
|
||||
ELSE NVL2(pr2.rule_column, pr2.rule_column || ' ', '')
|
||||
|| pr2.rule_type
|
||||
|| NVL2(pr2.rule_value, ' ' || pr2.rule_value, '')
|
||||
END,
|
||||
CHR(10) || 'AND '
|
||||
) WITHIN GROUP (ORDER BY pr2.rule_id)
|
||||
FROM cb_permission_rule pr2
|
||||
WHERE pr2.perm_id = p.perm_id
|
||||
) AS filter_preview,
|
||||
'컬럼 원문/마스킹은 컬럼 마스킹에서 관리' AS null_policy_preview
|
||||
FROM cb_permission p
|
||||
JOIN hmm_access_roles r
|
||||
ON r.role_id = p.role_id
|
||||
LEFT JOIN cb_protected_object o
|
||||
ON o.object_name = p.target_name
|
||||
LEFT JOIN cb_permission_rule pr
|
||||
ON pr.perm_id = p.perm_id
|
||||
GROUP BY
|
||||
p.perm_id,
|
||||
r.role_id,
|
||||
r.role_name,
|
||||
o.object_id,
|
||||
p.target_name,
|
||||
p.action_name,
|
||||
p.permission_effect
|
||||
ORDER BY r.role_name, p.target_name
|
||||
</select>
|
||||
|
||||
<select id="findPermissionId" resultType="long">
|
||||
|
||||
@@ -396,9 +396,10 @@ function syncRuleTypeHints(root = document) {
|
||||
|
||||
const type = typeSelect.value;
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL', 'STATIC_SQL'].includes(type);
|
||||
const columnOptional = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
const columnOptional = ['ALL', 'MANAGED_TEAM', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
const placeholderByType = {
|
||||
ALL: '값 불필요',
|
||||
MANAGED_TEAM: '값 불필요',
|
||||
MY_DEPT: '값 불필요',
|
||||
SELF: '값 불필요',
|
||||
DEPT: '예: HR',
|
||||
@@ -417,10 +418,10 @@ function syncRuleTypeHints(root = document) {
|
||||
};
|
||||
|
||||
columnSelect.required = !columnOptional;
|
||||
columnSelect.disabled = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
columnSelect.disabled = ['ALL', 'MANAGED_TEAM', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
valueInput.required = valueRequired;
|
||||
valueInput.disabled = [
|
||||
'ALL', 'MY_DEPT', 'SELF', 'TOKEN_SUBJECT', 'OWN_CONTRACT',
|
||||
'ALL', 'MANAGED_TEAM', 'MY_DEPT', 'SELF', 'TOKEN_SUBJECT', 'OWN_CONTRACT',
|
||||
'CHANNEL_CONTRACT', 'OWN_CUSTOMER', 'CHANNEL_CUSTOMER'
|
||||
].includes(type);
|
||||
valueInput.placeholder = placeholderByType[type] || '비교 값';
|
||||
@@ -708,8 +709,9 @@ function sqlLiteral(value) {
|
||||
|
||||
function permissionRuleBusinessLabel(type, column, value) {
|
||||
const displayColumn = column || {
|
||||
MANAGED_TEAM: 'EMPLOYEE_ID',
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
SELF: 'EMPLOYEE_ID',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
@@ -724,6 +726,9 @@ function permissionRuleBusinessLabel(type, column, value) {
|
||||
if (type === 'ALL') {
|
||||
return '전체 행';
|
||||
}
|
||||
if (type === 'MANAGED_TEAM') {
|
||||
return `본인 및 직접 보고 팀원 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'TOKEN_SUBJECT') {
|
||||
return `토큰 이해관계자 본인 행 (${displayColumn})`;
|
||||
}
|
||||
@@ -746,7 +751,7 @@ function permissionRuleBusinessLabel(type, column, value) {
|
||||
return `내 부서 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'SELF') {
|
||||
return `내 사번/소유자 행 (${displayColumn})`;
|
||||
return `본인 직원 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'TAG') {
|
||||
return `태그 조건 ${displayColumn} contains ${value || '-'}`;
|
||||
@@ -760,8 +765,9 @@ function collectWizardRules(root) {
|
||||
const type = row.querySelector('[name="ruleType"]')?.value || '';
|
||||
const value = row.querySelector('[name="ruleValue"]')?.value || '';
|
||||
const displayColumn = column || {
|
||||
MANAGED_TEAM: 'EMPLOYEE_ID',
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
SELF: 'EMPLOYEE_ID',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
@@ -785,8 +791,9 @@ function collectWizardPredicates(root) {
|
||||
const type = row.querySelector('[name="ruleType"]')?.value || '';
|
||||
const value = row.querySelector('[name="ruleValue"]')?.value || '';
|
||||
const displayColumn = column || {
|
||||
MANAGED_TEAM: 'EMPLOYEE_ID',
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
SELF: 'EMPLOYEE_ID',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
@@ -803,6 +810,9 @@ function collectWizardPredicates(root) {
|
||||
if (type === 'ALL') {
|
||||
return '1 = 1';
|
||||
}
|
||||
if (type === 'MANAGED_TEAM') {
|
||||
return `${displayColumn} IN (현재 직원 및 직접 보고 팀원)`;
|
||||
}
|
||||
if (type === 'STATIC_SQL') {
|
||||
return value;
|
||||
}
|
||||
@@ -810,7 +820,7 @@ function collectWizardPredicates(root) {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE')`;
|
||||
}
|
||||
if (type === 'SELF') {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
||||
return `${displayColumn} = SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')`;
|
||||
}
|
||||
if (type === 'STAKEHOLDER_SELF') {
|
||||
return `EXISTS (KB_CONTRACTS c: c.${displayColumn} = <현재행>.${displayColumn} AND TOKEN_ROLE = '${value}' AND c.FC_ID = TOKEN_STAKEHOLDER_ID)`;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<h1>행 접근 규칙</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. 저장값은 최종 SQL이 아니라 <code>CB_AGENT_DOC_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꾸는 매핑 데이터입니다.</p>
|
||||
<p>컬럼 원문/마스킹은 이 화면에서 처리하지 않습니다. 주민번호, 보험료, 청구금액 같은 컬럼 표시는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 관리합니다.</p>
|
||||
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. HMM 휴가 원장의 저장값은 <code>HMM_LEAVE_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꿉니다.</p>
|
||||
<p>팀장은 본인과 직속 팀원, 팀원은 본인 행만 조회하도록 설정합니다. 컬럼 원문/마스킹은 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 별도로 관리합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -157,19 +157,14 @@
|
||||
<option value="">객체 컬럼 선택</option>
|
||||
</select>
|
||||
<select class="form-select rule-type-select" name="ruleType">
|
||||
<option value="ALL">ALL</option>
|
||||
<optgroup label="KB · 본인 범위">
|
||||
<option value="OWN_CONTRACT">본인 담당 계약</option>
|
||||
<option value="OWN_CUSTOMER">본인 담당 고객</option>
|
||||
<optgroup label="HMM HR · 표준 범위">
|
||||
<option value="SELF">본인 직원 행</option>
|
||||
<option value="MANAGED_TEAM">본인 및 직접 보고 팀원</option>
|
||||
<option value="ALL">전체 행</option>
|
||||
</optgroup>
|
||||
<optgroup label="KB · 채널 범위">
|
||||
<option value="CHANNEL_CONTRACT">토큰 채널 계약</option>
|
||||
<option value="CHANNEL_CUSTOMER">토큰 채널 고객</option>
|
||||
</optgroup>
|
||||
<optgroup label="KB · 토큰 식별">
|
||||
<option value="TOKEN_SUBJECT">토큰 사용자 본인</option>
|
||||
</optgroup>
|
||||
<optgroup label="SQL · 정적 조건">
|
||||
<optgroup label="고급 조건">
|
||||
<option value="=">선택 컬럼 값 일치</option>
|
||||
<option value="!=">선택 컬럼 값 불일치</option>
|
||||
<option value="STATIC_SQL">정적 SQL 조건식</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
@@ -179,12 +174,12 @@
|
||||
</div>
|
||||
<details class="explanation-details">
|
||||
<summary>행 규칙의 두 가지 적용 방식 보기</summary>
|
||||
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong>은 <code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 최종 WHERE 절에 직접 들어가지 않습니다.</p>
|
||||
<p class="wizard-hint"><strong>표준 범위</strong>는 유효한 HMM 토큰에서 만든 직원 context로 치환됩니다. <code>SELF</code>는 현재 직원의 행만, <code>MANAGED_TEAM</code>은 현재 직원과 <code>MANAGER_EMPLOYEE_ID</code>로 연결된 직접 보고자의 행만 남깁니다. <strong>정적 SQL 조건식</strong>은 현재 객체 컬럼을 사용한 고정 WHERE 조건을 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다.</p>
|
||||
<p class="wizard-hint"><strong>컬럼 원문/마스킹은 제외했습니다.</strong> 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.</p>
|
||||
</details>
|
||||
<details class="explanation-details">
|
||||
<summary>조건 코드가 실제 VPD WHERE 조각으로 바뀌는 방식</summary>
|
||||
<p class="wizard-hint">이 표는 저장 규칙이 <code>CB_AGENT_DOC_VPD_FILTER</code>에서 어떤 predicate로 변환되는지 설명하기 위한 예시입니다. 실제 컬럼 존재 여부와 대상 객체는 DB에서 다시 검증됩니다.</p>
|
||||
<p class="wizard-hint">이 표는 저장 규칙이 <code>HMM_LEAVE_VPD_FILTER</code>에서 어떤 predicate로 변환되는지 설명하기 위한 예시입니다. 실제 컬럼 존재 여부와 대상 객체는 DB에서 다시 검증됩니다.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>저장 조건 코드</th><th>업무 의미</th><th>VPD filter가 만드는 조건</th></tr></thead>
|
||||
@@ -195,29 +190,19 @@
|
||||
<td><code>1 = 1</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TOKEN_SUBJECT</code></td>
|
||||
<td>토큰으로 식별된 이해관계자 본인 행</td>
|
||||
<td><code>USER_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')</code></td>
|
||||
<td><code>SELF</code></td>
|
||||
<td>토큰으로 식별된 HMM 직원 본인 행</td>
|
||||
<td><code>EMPLOYEE_ID = SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>OWN_CONTRACT</code></td>
|
||||
<td>토큰 사용자가 담당 설계사인 계약</td>
|
||||
<td><code>FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>CHANNEL_CONTRACT</code></td>
|
||||
<td>토큰 사용자의 채널에 속한 계약</td>
|
||||
<td><code>FC_CHANNEL = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>OWN_CUSTOMER</code> / <code>CHANNEL_CUSTOMER</code></td>
|
||||
<td>계약원장에서 담당자 또는 채널 기준으로 연결되는 고객·청구·담보·외부보유 행</td>
|
||||
<td><code>EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS ...)</code></td>
|
||||
<td><code>MANAGED_TEAM</code></td>
|
||||
<td>팀장 본인과 직접 보고 팀원의 행</td>
|
||||
<td><code>EMPLOYEE_ID IN (현재 직원 및 MANAGER_EMPLOYEE_ID가 현재 직원인 직원)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>STATIC_SQL</code></td>
|
||||
<td>현재 객체 컬럼으로 표현한 고정 조건</td>
|
||||
<td><code>CONTRACT_STATUS = '정상'</code>처럼 검증된 현재 객체 컬럼 조건</td>
|
||||
<td><code>REQUEST_STATUS = 'PENDING'</code>처럼 검증된 현재 객체 컬럼 조건</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<h1 class="mt-2">행 접근 필터 구조</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>기본 동적 필터 <code>CB_AGENT_DOC_VPD_FILTER</code>의 연결 상태와 현재 DB에 배포된 함수를 읽기 전용으로 보여줍니다. 행 접근 변경은 <a href="/permissions">행 접근 규칙</a>에서 수행합니다.</p>
|
||||
<p>HMM 기본 동적 필터 <code>HMM_LEAVE_VPD_FILTER</code>의 연결 상태와 현재 DB에 배포된 함수를 읽기 전용으로 보여줍니다. 행 접근 변경은 <a href="/permissions">행 접근 규칙</a>에서 수행합니다.</p>
|
||||
<section class="content-band mt-3 mb-0">
|
||||
<h2>행 필터(VPD) 적용 흐름</h2>
|
||||
<p><strong>Oracle VPD 정책 함수는 Bearer Token을 직접 해석하지 않습니다.</strong> 앞 단계의 <code>cb_agent_ctx_pkg.set_user_by_bearer</code>가 만든 <code>CB_AGENT_CTX</code>를 읽어, 현재 SELECT에 붙일 WHERE predicate를 생성합니다.</p>
|
||||
<p><strong>Oracle VPD 정책 함수는 Bearer Token을 직접 해석하지 않습니다.</strong> 앞 단계의 <code>hmm_access_ctx_pkg.set_user_by_bearer</code>가 만든 <code>HMM_ACCESS_CTX</code>를 읽어, 현재 SELECT에 붙일 WHERE predicate를 생성합니다.</p>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="vpd-help-flow-title vpd-help-flow-desc">
|
||||
<title id="vpd-help-flow-title">Bearer 토큰 기반 행 필터 적용 흐름</title>
|
||||
@@ -29,15 +29,15 @@
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M900 136 V280 H755 V315"/>
|
||||
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">ORDS 요청</text></g>
|
||||
<g class="diagram-node"><rect x="225" y="48" width="180" height="88" rx="12"/><text x="315" y="76" class="diagram-node-title">토큰 검증·해석</text><text x="315" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="315" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
|
||||
<g class="diagram-node"><rect x="445" y="48" width="180" height="88" rx="12"/><text x="535" y="76" class="diagram-node-title">CB_AGENT_CTX</text><text x="535" y="103" class="diagram-node-detail">USER_ID · DEPT_CODE · EMP_NO</text><text x="535" y="120" class="diagram-node-note">STAKEHOLDER_USER_ID · CHANNEL</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="665" y="48" width="180" height="88" rx="12"/><text x="755" y="76" class="diagram-node-title">행 접근 함수 호출</text><text x="755" y="103" class="diagram-node-detail">CB_AGENT_DOC_VPD_FILTER</text><text x="755" y="120" class="diagram-node-note">p_object = 조회 대상</text></g>
|
||||
<g class="diagram-node"><rect x="445" y="48" width="180" height="88" rx="12"/><text x="535" y="76" class="diagram-node-title">HMM_ACCESS_CTX</text><text x="535" y="103" class="diagram-node-detail">EMPLOYEE_ID · CODE</text><text x="535" y="120" class="diagram-node-note">TEAM_ID</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="665" y="48" width="180" height="88" rx="12"/><text x="755" y="76" class="diagram-node-title">행 접근 함수 호출</text><text x="755" y="103" class="diagram-node-detail">HMM_LEAVE_VPD_FILTER</text><text x="755" y="120" class="diagram-node-note">p_object = 휴가 원장</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="885" y="48" width="185" height="88" rx="12"/><text x="977" y="76" class="diagram-node-title">WHERE predicate</text><text x="977" y="103" class="diagram-node-detail">ALLOW / DENY 결합</text><text x="977" y="120" class="diagram-node-note">원래 SELECT에 자동 추가</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="590" y="212" width="310" height="72" rx="12"/><text x="745" y="239" class="diagram-node-title">역할·권한·규칙 메타데이터</text><text x="745" y="263" class="diagram-node-detail">cb_user_role · cb_group_role · cb_permission_rule</text></g>
|
||||
<g class="diagram-node"><rect x="665" y="315" width="180" height="64" rx="12"/><text x="755" y="341" class="diagram-node-title">ALLOW 없음 / USER_ID 없음</text><text x="755" y="363" class="diagram-node-detail">RETURN 1 = 0 · 행 없음</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="590" y="212" width="310" height="72" rx="12"/><text x="745" y="239" class="diagram-node-title">HMM 역할·권한·규칙</text><text x="745" y="263" class="diagram-node-detail">employee/group roles · permission rules</text></g>
|
||||
<g class="diagram-node"><rect x="665" y="315" width="180" height="64" rx="12"/><text x="755" y="341" class="diagram-node-title">ALLOW/직원 context 없음</text><text x="755" y="363" class="diagram-node-detail">RETURN 1 = 0 · 행 없음</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="885" y="315" width="185" height="64" rx="12"/><text x="977" y="341" class="diagram-node-title">ALLOW 조건 충족</text><text x="977" y="363" class="diagram-node-detail">조건에 맞는 행만 반환</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="form-hint mb-0"><code>rule_type</code>별 WHERE 조각은 권한 내부에서 AND, ALLOW 권한끼리는 OR, DENY 권한은 최종적으로 <code>AND NOT (...)</code>으로 결합됩니다. ASO는 같은 context의 <code>MR_<column_id></code>로 <em>컬럼 표시</em>를 정하고, 행 접근 필터는 <code>USER_ID</code>·<code>DEPT_CODE</code>·<code>EMP_NO</code>·<code>STAKEHOLDER_*</code>로 <em>보이는 행</em>을 정합니다.</p>
|
||||
<p class="form-hint mb-0"><code>rule_type</code>별 WHERE 조각은 권한 내부에서 AND, ALLOW 권한끼리는 OR, DENY 권한은 최종적으로 <code>AND NOT (...)</code>으로 결합됩니다. 행 접근 필터는 <code>EMPLOYEE_ID</code>와 직원의 직접 보고 관계로 <em>보이는 행</em>을 정합니다.</p>
|
||||
</section>
|
||||
</details>
|
||||
</section>
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="macro-micro-grid">
|
||||
<div>
|
||||
<h3>1. 신뢰 컨텍스트 설정</h3>
|
||||
<p>ORDS가 Bearer Token을 검증하고 <code>CB_AGENT_CTX</code>에 사용자·이해관계자·채널 정보를 설정합니다.</p>
|
||||
<p>애플리케이션이 Bearer Token을 검증하고 <code>HMM_ACCESS_CTX</code>에 직원·팀 정보를 설정합니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>2. 권한과 규칙 계산</h3>
|
||||
@@ -102,10 +102,10 @@
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>점검 항목</th><th>확인 기준</th><th>조치 위치</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 ORDS에서 검증하고 요청 종료 시 컨텍스트를 정리합니다.</td><td>ORDS Handler / <code>CB_AGENT_CTX_PKG</code></td></tr>
|
||||
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 DB 패키지에서 해시·만료·회수·재직 상태를 검증합니다.</td><td><code>HMM_ACCESS_CTX_PKG</code></td></tr>
|
||||
<tr><td>권한 결합</td><td>권한 내부 규칙은 AND, ALLOW 권한은 OR, DENY 조건은 최종적으로 제외합니다.</td><td><a href="/permissions">행 접근 규칙</a></td></tr>
|
||||
<tr><td>오류·미권한</td><td>유효한 컨텍스트나 ALLOW 권한이 없으면 행 접근은 차단돼야 합니다.</td><td><a href="/probe">접근 검증</a></td></tr>
|
||||
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 보험료·주민번호 등의 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
||||
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 개인정보 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PermissionViewTest {
|
||||
|
||||
@Test
|
||||
void translatesHmmLeaveScopeRulesForBackofficeUsers() {
|
||||
PermissionView view = new PermissionView(
|
||||
1L,
|
||||
2L,
|
||||
"HMM_HR_MANAGER",
|
||||
10L,
|
||||
"HMM_LEAVE_REQUESTS",
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
"EMPLOYEE_ID MANAGED_TEAM,EMPLOYEE_ID SELF",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
assertThat(view.businessRuleSummary())
|
||||
.contains("본인 및 직접 보고 팀원 행")
|
||||
.contains("본인 직원 행");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class VpdPolicyFormOptionsTest {
|
||||
|
||||
@Test
|
||||
void choosesHmmFilterForHmmLeaveObjects() {
|
||||
VpdPolicyFormOptions options = new VpdPolicyFormOptions(
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(
|
||||
new VpdFunctionOption("ADMIN", null, "CB_AGENT_DOC_VPD_FILTER", "FUNCTION"),
|
||||
new VpdFunctionOption("ADMIN", null, "HMM_LEAVE_VPD_FILTER", "FUNCTION")
|
||||
),
|
||||
List.of(),
|
||||
List.of("SELECT")
|
||||
);
|
||||
|
||||
assertThat(options.defaultPermissionFunctionKey("HMM_LEAVE_BALANCES"))
|
||||
.isEqualTo("ADMIN.HMM_LEAVE_VPD_FILTER");
|
||||
assertThat(options.defaultPermissionFunctionKey("HMM_LEAVE_REQUESTS"))
|
||||
.isEqualTo("ADMIN.HMM_LEAVE_VPD_FILTER");
|
||||
assertThat(options.defaultPermissionFunctionKey("CB_AGENT_DOCUMENTS"))
|
||||
.isEqualTo("ADMIN.CB_AGENT_DOC_VPD_FILTER");
|
||||
}
|
||||
}
|
||||
@@ -184,8 +184,10 @@ class GuidedFlowTemplateTest {
|
||||
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
|
||||
|
||||
assertThat(html)
|
||||
.contains("value=\"OWN_CUSTOMER\">본인 담당 고객")
|
||||
.contains("value=\"MANAGED_TEAM\">본인 및 직접 보고 팀원")
|
||||
.contains("value=\"SELF\">본인 직원 행")
|
||||
.contains("value=\"STATIC_SQL\">정적 SQL 조건식")
|
||||
.contains("HMM_LEAVE_VPD_FILTER")
|
||||
.contains("행 규칙의 두 가지 적용 방식 보기")
|
||||
.contains("한 권한 안의 규칙은 모두 AND")
|
||||
.contains("서로 다른 역할의 ALLOW 권한은 OR")
|
||||
|
||||
Reference in New Issue
Block a user