[Developer] #619 map VPD rules to token context variables
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
- `CB_AGENT_BEARER_KEY.STAKEHOLDER_USER_ID`가 `KB_STAKEHOLDERS.USER_ID`를 가리킨다. 기존 숫자형 `USER_ID`는 기존 기능 호환용 application-user bridge로 유지한다.
|
||||
- 이해당사자마다 `CB_APP_USER.STAKEHOLDER_USER_ID` bridge를 하나 만들고, `KB_STAKEHOLDERS.ROLE`이 `지점장`이면 `KB_BRANCH_MANAGER_ROLE`, `설계사`이면 `KB_DESIGNER_ROLE`을 부여한다. 다른 역할은 토큰은 발급되지만 별도 권한을 추가하기 전까지 fail-closed다.
|
||||
- VPD package는 토큰에서 `STAKEHOLDER_USER_ID`, `STAKEHOLDER_ROLE`, 정규화된 `STAKEHOLDER_CHANNEL`을 application context에 설정한다. `설계사채널 → 설계사`, `GA채널 → GA`를 정규화한다.
|
||||
- `지점장` 여부는 package의 하드코딩 분기가 아니라 `KB_BRANCH_MANAGER_ROLE`에 부여된 접근 규칙으로 결정한다.
|
||||
- `지점장` 여부는 package의 하드코딩 분기가 아니라 `KB_BRANCH_MANAGER_ROLE`의 rule value(`지점장`)와 `KB_STAKEHOLDERS.ROLE` 일치로 결정한다. 설계사 분기는 같은 방식으로 `설계사`를 확인한다.
|
||||
- `RLS_FILTER`의 문자열은 실행하지 않는다. 권한 테이블에 저장된 allowlist rule type만 predicate로 변환한다.
|
||||
|
||||
## 3. 예제 권한
|
||||
@@ -26,7 +26,27 @@
|
||||
| KB_BRANCH_MANAGER_ROLE | 고객·담보·청구·외부보유 | `CUST_ID / STAKEHOLDER_CHANNEL` | 같은 채널 고객 기준 |
|
||||
| 두 역할 | KB_STAKEHOLDERS | `USER_ID / STAKEHOLDER_SELF` | 토큰 주체 자신의 매핑 행 |
|
||||
|
||||
`STAKEHOLDER_SELF`와 `STAKEHOLDER_CHANNEL`은 접근 규칙 화면에서 선택 가능한 행 규칙이다. `FC_ID`/`FC_CHANNEL`에서는 직접 비교하고, `CUST_ID`/`CONTRACT_NO`에서는 계약원장과 안전한 EXISTS 조인으로 변환한다.
|
||||
`STAKEHOLDER_SELF`와 `STAKEHOLDER_CHANNEL`은 접근 규칙 화면에서 선택 가능한 행 규칙이다. 두 규칙에는 대상 컬럼과 업무 역할값을 함께 저장한다. Bearer Token을 해석할 때 `KB_STAKEHOLDERS`를 한 번만 조회해 `STAKEHOLDER_USER_ID`, `STAKEHOLDER_ROLE`, `STAKEHOLDER_CHANNEL` secure context를 만들고, 이후 `FC_ID`/`FC_CHANNEL`과 `CUST_ID`/`CONTRACT_NO` 조건은 계약원장과 그 context만 사용한다. 허용된 역할별 분기는 **OR**로 합쳐진다.
|
||||
|
||||
```sql
|
||||
-- 고객·청구·외부보유의 CUST_ID 예시: 역할별 허용 분기
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM POC_2.KB_CONTRACTS c
|
||||
WHERE c.CUST_ID = <현재행>.CUST_ID
|
||||
AND SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_ROLE') = '설계사'
|
||||
AND c.FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM POC_2.KB_CONTRACTS c
|
||||
WHERE c.CUST_ID = <현재행>.CUST_ID
|
||||
AND SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_ROLE') = '지점장'
|
||||
AND c.FC_CHANNEL = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')
|
||||
)
|
||||
```
|
||||
|
||||
위의 `'설계사'`/`'지점장'`은 함수 하드코딩이 아니라 각 permission rule의 `rule_value`에서 온다. 토큰 원문은 predicate에 노출하지 않으며, `CB_AGENT_BEARER_KEY → KB_STAKEHOLDERS`에서 한 번 검증·해석한 subject context만 사용한다.
|
||||
|
||||
## 4. CLS / NULL 처리
|
||||
|
||||
@@ -75,7 +95,7 @@ Bearer Token → CB_AGENT_CTX_PKG → application context
|
||||
|
||||
### 2026-07-07 배포 검증 결과
|
||||
|
||||
- Maven 테스트 77건 통과.
|
||||
- Maven 테스트 78건 통과.
|
||||
- HTTPS 인증 요청으로 `/permissions`, `/tokens`가 각각 200을 반환했다. 기존 `/permissions`의 잘못된 Thymeleaf fallback 표현식도 함께 수정했다.
|
||||
- `CB_APP_USER` bridge 13명, 지점장 역할 2명, 설계사 역할 5명이 동기화됐다.
|
||||
- `POC_2`에 행 VPD 7개와 민감 컬럼 NULL VPD policy 7개, 관련 CLS 함수 7개가 모두 `VALID` 상태로 확인됐다.
|
||||
|
||||
@@ -170,56 +170,79 @@ BEGIN
|
||||
);
|
||||
END IF;
|
||||
ELSIF r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL') THEN
|
||||
-- 업무 사용자 원장(KB_STAKEHOLDERS)에서 해석한 토큰 주체를 쓴다.
|
||||
-- CUST_ID/CONTRACT_NO는 계약원장을 한 단계 감싼 EXISTS로만 연결해
|
||||
-- 임의 SQL이나 외부 rule text가 predicate에 섞이지 않게 한다.
|
||||
-- CB_AGENT_CTX_PKG가 Bearer token -> KB_STAKEHOLDERS를 한 번
|
||||
-- 해석해 USER_ID/ROLE/CHANNEL을 secure context로 설정한다.
|
||||
-- 따라서 보호 테이블 predicate에서 KB_STAKEHOLDERS를 다시 조인하지
|
||||
-- 않고, 해당 context와 KB_CONTRACTS 관계만 사용한다.
|
||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||
IF v_column IS NOT NULL
|
||||
AND UPPER(p_schema) = 'POC_2' THEN
|
||||
IF r.rule_type = 'STAKEHOLDER_SELF'
|
||||
AND ((v_target = 'KB_CONTRACTS' AND v_column = 'FC_ID')
|
||||
OR (v_target = 'KB_STAKEHOLDERS' AND v_column = 'USER_ID')) THEN
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')'
|
||||
);
|
||||
ELSIF r.rule_type = 'STAKEHOLDER_CHANNEL'
|
||||
AND v_target = 'KB_CONTRACTS'
|
||||
AND v_column = 'FC_CHANNEL' THEN
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')'
|
||||
);
|
||||
ELSIF v_target IN ('KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
|
||||
AND v_column IN ('CUST_ID', 'CONTRACT_NO') THEN
|
||||
DECLARE
|
||||
v_contract_key VARCHAR2(30);
|
||||
v_context_key VARCHAR2(30);
|
||||
v_context_name VARCHAR2(30);
|
||||
BEGIN
|
||||
DECLARE
|
||||
v_contract_key VARCHAR2(30);
|
||||
v_role_condition VARCHAR2(500);
|
||||
BEGIN
|
||||
-- 각 ALLOW rule은 append_or로 OR 분기가 된다. rule_value는
|
||||
-- 권한 설정에 저장된 해당 분기의 역할값이다. identity rule은
|
||||
-- NULL value를 허용해 모든 토큰 주체가 자기 mapping을 해석한다.
|
||||
v_role_condition := CASE
|
||||
WHEN TRIM(r.rule_value) IS NULL THEN '1 = 1'
|
||||
ELSE 'SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = '
|
||||
|| quote_literal(TRIM(r.rule_value))
|
||||
END;
|
||||
|
||||
IF r.rule_type = 'STAKEHOLDER_SELF'
|
||||
AND v_target = 'KB_CONTRACTS'
|
||||
AND v_column = 'FC_ID' THEN
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
'(' || v_role_condition ||
|
||||
' AND FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||
);
|
||||
ELSIF r.rule_type = 'STAKEHOLDER_CHANNEL'
|
||||
AND v_target = 'KB_CONTRACTS'
|
||||
AND v_column = 'FC_CHANNEL' THEN
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
'(' || v_role_condition ||
|
||||
' AND FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL''))'
|
||||
);
|
||||
ELSIF r.rule_type = 'STAKEHOLDER_SELF'
|
||||
AND v_target = 'KB_STAKEHOLDERS'
|
||||
AND v_column = 'USER_ID' THEN
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
'(' || v_role_condition ||
|
||||
' AND USER_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||
);
|
||||
ELSIF v_target IN ('KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
|
||||
AND v_column IN ('CUST_ID', 'CONTRACT_NO') THEN
|
||||
v_contract_key := CASE v_column
|
||||
WHEN 'CUST_ID' THEN 'CUST_ID'
|
||||
WHEN 'CONTRACT_NO' THEN 'CONTRACT_NO'
|
||||
END;
|
||||
v_context_key := CASE r.rule_type
|
||||
WHEN 'STAKEHOLDER_SELF' THEN 'FC_ID'
|
||||
ELSE 'FC_CHANNEL'
|
||||
END;
|
||||
v_context_name := CASE r.rule_type
|
||||
WHEN 'STAKEHOLDER_SELF' THEN 'STAKEHOLDER_USER_ID'
|
||||
ELSE 'STAKEHOLDER_CHANNEL'
|
||||
END;
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
'EXISTS (SELECT 1 FROM (' ||
|
||||
'SELECT c.' || v_contract_key || ' AS access_key ' ||
|
||||
'FROM POC_2.KB_CONTRACTS c ' ||
|
||||
'WHERE c.' || v_context_key || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''' ||
|
||||
v_context_name || ''')) allowed_contract ' ||
|
||||
'WHERE allowed_contract.access_key = ' || v_column || ')'
|
||||
);
|
||||
END;
|
||||
END IF;
|
||||
IF r.rule_type = 'STAKEHOLDER_SELF' THEN
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
'EXISTS (SELECT 1 FROM (' ||
|
||||
'SELECT c.' || v_contract_key || ' AS access_key ' ||
|
||||
'FROM POC_2.KB_CONTRACTS c ' ||
|
||||
'WHERE ' || v_role_condition ||
|
||||
' AND c.FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')) allowed_contract ' ||
|
||||
'WHERE allowed_contract.access_key = ' || v_column || ')'
|
||||
);
|
||||
ELSE
|
||||
append_or(
|
||||
r.permission_effect,
|
||||
'EXISTS (SELECT 1 FROM (' ||
|
||||
'SELECT c.' || v_contract_key || ' AS access_key ' ||
|
||||
'FROM POC_2.KB_CONTRACTS c ' ||
|
||||
'WHERE ' || v_role_condition ||
|
||||
' AND c.FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')) allowed_contract ' ||
|
||||
'WHERE allowed_contract.access_key = ' || v_column || ')'
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
END IF;
|
||||
ELSIF r.rule_type = '=' THEN
|
||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||
|
||||
@@ -313,7 +313,8 @@ DECLARE
|
||||
p_role_id IN NUMBER,
|
||||
p_target_name IN VARCHAR2,
|
||||
p_rule_column IN VARCHAR2,
|
||||
p_rule_type IN VARCHAR2
|
||||
p_rule_type IN VARCHAR2,
|
||||
p_rule_value IN VARCHAR2
|
||||
) IS
|
||||
v_permission_id NUMBER;
|
||||
BEGIN
|
||||
@@ -328,7 +329,7 @@ DECLARE
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
||||
VALUES (v_permission_id, p_role_id, p_target_name, 'SELECT', 'ALLOW');
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, p_rule_column, p_rule_type, NULL);
|
||||
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, p_rule_column, p_rule_type, p_rule_value);
|
||||
END;
|
||||
BEGIN
|
||||
ensure_role('KB_DESIGNER_ROLE', 'CONFIDENTIAL', v_designer_role_id);
|
||||
@@ -382,20 +383,20 @@ BEGIN
|
||||
|
||||
-- 토큰은 모든 이해당사자에게 발급되지만, 아래 역할이 없는 주체는
|
||||
-- 권한 레코드가 없으므로 VPD가 1=0으로 fail-closed 된다.
|
||||
ensure_permission(v_designer_role_id, 'KB_STAKEHOLDERS', 'USER_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_manager_role_id, 'KB_STAKEHOLDERS', 'USER_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_designer_role_id, 'KB_PRODUCTS', NULL, 'ALL');
|
||||
ensure_permission(v_manager_role_id, 'KB_PRODUCTS', NULL, 'ALL');
|
||||
ensure_permission(v_designer_role_id, 'KB_CONTRACTS', 'FC_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_manager_role_id, 'KB_CONTRACTS', 'FC_CHANNEL', 'STAKEHOLDER_CHANNEL');
|
||||
ensure_permission(v_designer_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_manager_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
||||
ensure_permission(v_designer_role_id, 'KB_COVERAGES', 'CUST_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_manager_role_id, 'KB_COVERAGES', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
||||
ensure_permission(v_designer_role_id, 'KB_CLAIMS', 'CUST_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_manager_role_id, 'KB_CLAIMS', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
||||
ensure_permission(v_designer_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'STAKEHOLDER_SELF');
|
||||
ensure_permission(v_manager_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
||||
ensure_permission(v_designer_role_id, 'KB_STAKEHOLDERS', 'USER_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||
ensure_permission(v_manager_role_id, 'KB_STAKEHOLDERS', 'USER_ID', 'STAKEHOLDER_SELF', '지점장');
|
||||
ensure_permission(v_designer_role_id, 'KB_PRODUCTS', NULL, 'ALL', NULL);
|
||||
ensure_permission(v_manager_role_id, 'KB_PRODUCTS', NULL, 'ALL', NULL);
|
||||
ensure_permission(v_designer_role_id, 'KB_CONTRACTS', 'FC_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||
ensure_permission(v_manager_role_id, 'KB_CONTRACTS', 'FC_CHANNEL', 'STAKEHOLDER_CHANNEL', '지점장');
|
||||
ensure_permission(v_designer_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||
ensure_permission(v_manager_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'STAKEHOLDER_CHANNEL', '지점장');
|
||||
ensure_permission(v_designer_role_id, 'KB_COVERAGES', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||
ensure_permission(v_manager_role_id, 'KB_COVERAGES', 'CUST_ID', 'STAKEHOLDER_CHANNEL', '지점장');
|
||||
ensure_permission(v_designer_role_id, 'KB_CLAIMS', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||
ensure_permission(v_manager_role_id, 'KB_CLAIMS', 'CUST_ID', 'STAKEHOLDER_CHANNEL', '지점장');
|
||||
ensure_permission(v_designer_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||
ensure_permission(v_manager_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'STAKEHOLDER_CHANNEL', '지점장');
|
||||
END;
|
||||
/
|
||||
COMMIT;
|
||||
|
||||
104
sql/adb/47_kb_stakeholder_permission_or_branches.sql
Normal file
104
sql/adb/47_kb_stakeholder_permission_or_branches.sql
Normal file
@@ -0,0 +1,104 @@
|
||||
-- ============================================================
|
||||
-- 47_kb_stakeholder_permission_or_branches.sql
|
||||
--
|
||||
-- Refines the KB example so the permission table carries the expected
|
||||
-- stakeholder role and each role branch becomes an OR candidate in the
|
||||
-- dynamic VPD filter. Execute after 26 and 46 as ADMIN.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET DEFINE OFF
|
||||
|
||||
PROMPT === 1. Storing expected stakeholder role in example rules ===
|
||||
UPDATE cb_permission_rule r
|
||||
SET rule_value = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
WHERE p.perm_id = r.perm_id
|
||||
AND ar.role_name = 'KB_DESIGNER_ROLE'
|
||||
) THEN '설계사'
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
WHERE p.perm_id = r.perm_id
|
||||
AND ar.role_name = 'KB_BRANCH_MANAGER_ROLE'
|
||||
) THEN '지점장'
|
||||
END
|
||||
WHERE r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL')
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
WHERE p.perm_id = r.perm_id
|
||||
AND ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
|
||||
);
|
||||
|
||||
PROMPT === 2. Adding an identity role so every stakeholder token can resolve itself ===
|
||||
DECLARE
|
||||
v_role_id NUMBER;
|
||||
v_permission_id NUMBER;
|
||||
v_count NUMBER;
|
||||
BEGIN
|
||||
BEGIN
|
||||
SELECT role_id INTO v_role_id
|
||||
FROM cb_app_role
|
||||
WHERE role_name = 'KB_STAKEHOLDER_IDENTITY_ROLE';
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
SELECT NVL(MAX(role_id), 0) + 1 INTO v_role_id FROM cb_app_role;
|
||||
INSERT INTO cb_app_role(role_id, role_name, max_sensitivity_level)
|
||||
VALUES (v_role_id, 'KB_STAKEHOLDER_IDENTITY_ROLE', 'INTERNAL');
|
||||
END;
|
||||
|
||||
FOR u IN (
|
||||
SELECT user_id
|
||||
FROM cb_app_user
|
||||
WHERE stakeholder_user_id IS NOT NULL
|
||||
AND active = 'Y'
|
||||
) LOOP
|
||||
SELECT COUNT(*) INTO v_count
|
||||
FROM cb_user_role
|
||||
WHERE user_id = u.user_id
|
||||
AND role_id = v_role_id;
|
||||
IF v_count = 0 THEN
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (u.user_id, v_role_id);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
BEGIN
|
||||
SELECT perm_id INTO v_permission_id
|
||||
FROM cb_permission
|
||||
WHERE role_id = v_role_id
|
||||
AND target_name = 'KB_STAKEHOLDERS'
|
||||
AND action_name = 'SELECT';
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
SELECT cb_permission_seq.NEXTVAL INTO v_permission_id FROM dual;
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
||||
VALUES (v_permission_id, v_role_id, 'KB_STAKEHOLDERS', 'SELECT', 'ALLOW');
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, 'USER_ID', 'STAKEHOLDER_SELF', NULL);
|
||||
END;
|
||||
END;
|
||||
/
|
||||
COMMIT;
|
||||
|
||||
PROMPT === 3. Result: role-specific permission branches ===
|
||||
SELECT ar.role_name,
|
||||
p.target_name,
|
||||
r.rule_column,
|
||||
r.rule_type,
|
||||
NVL(r.rule_value, '(token role)') AS expected_stakeholder_role
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
JOIN cb_permission_rule r ON r.perm_id = p.perm_id
|
||||
WHERE ar.role_name IN (
|
||||
'KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE', 'KB_STAKEHOLDER_IDENTITY_ROLE'
|
||||
)
|
||||
ORDER BY ar.role_name, p.target_name, r.rule_id;
|
||||
|
||||
PROMPT === KB role branches are combined as OR by CB_AGENT_DOC_VPD_FILTER ===
|
||||
@@ -24,7 +24,7 @@ public class PermissionService {
|
||||
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG");
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG", "STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
|
||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG");
|
||||
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
||||
|
||||
@@ -93,13 +93,27 @@
|
||||
WHEN pr2.rule_type = 'TAG' THEN 'REGEXP_LIKE(UPPER('
|
||||
|| NVL(pr2.rule_column, 'TECH_TAG')
|
||||
|| '), ''(^|,)' || REPLACE(UPPER(pr2.rule_value), '''', '''''') || '(,|$)'')'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_SELF' THEN pr2.rule_column
|
||||
|| ' = token stakeholder / 담당계약'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_CHANNEL' THEN pr2.rule_column
|
||||
|| ' = token stakeholder channel / 담당계약'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_SELF' AND pr2.rule_column = 'FC_ID' THEN
|
||||
'(SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = ''' || NVL(pr2.rule_value, '[역할값 필요]') || ''' '
|
||||
|| 'AND FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_SELF' AND pr2.rule_column = 'USER_ID' THEN
|
||||
'(USER_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_CHANNEL' AND pr2.rule_column = 'FC_CHANNEL' THEN
|
||||
'(SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = ''' || NVL(pr2.rule_value, '[역할값 필요]') || ''' '
|
||||
|| 'AND FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL''))'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_SELF' THEN
|
||||
'EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS c '
|
||||
|| 'WHERE c.' || pr2.rule_column || ' = [CURRENT_ROW].' || pr2.rule_column || ' '
|
||||
|| 'AND SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = ''' || NVL(pr2.rule_value, '[역할값 필요]') || ''' '
|
||||
|| 'AND c.FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||
WHEN pr2.rule_type = 'STAKEHOLDER_CHANNEL' THEN
|
||||
'EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS c '
|
||||
|| 'WHERE c.' || pr2.rule_column || ' = [CURRENT_ROW].' || pr2.rule_column || ' '
|
||||
|| 'AND SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = ''' || NVL(pr2.rule_value, '[역할값 필요]') || ''' '
|
||||
|| 'AND c.FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL''))'
|
||||
ELSE pr2.rule_column || ' ' || pr2.rule_type || ' ' || pr2.rule_value
|
||||
END,
|
||||
CHR(10) || 'AND '
|
||||
CHR(10) || 'OR '
|
||||
) WITHIN GROUP (ORDER BY pr2.rule_id)
|
||||
FROM cb_permission_rule pr2
|
||||
WHERE pr2.perm_id = p.perm_id
|
||||
|
||||
@@ -388,7 +388,7 @@ function syncRuleTypeHints(root = document) {
|
||||
}
|
||||
|
||||
const type = typeSelect.value;
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG'].includes(type);
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type);
|
||||
const columnOptional = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG'].includes(type);
|
||||
const placeholderByType = {
|
||||
ALL: '값 불필요',
|
||||
@@ -397,15 +397,15 @@ function syncRuleTypeHints(root = document) {
|
||||
DEPT: '예: HR',
|
||||
EMP_NO: '예: E2001',
|
||||
TAG: '예: SPRING_BOOT',
|
||||
STAKEHOLDER_SELF: '토큰 이해당사자에서 자동 계산',
|
||||
STAKEHOLDER_CHANNEL: '토큰 채널에서 자동 계산',
|
||||
STAKEHOLDER_SELF: '예: 설계사',
|
||||
STAKEHOLDER_CHANNEL: '예: 지점장',
|
||||
'=': '비교 값',
|
||||
'!=': '비교 값'
|
||||
};
|
||||
|
||||
columnSelect.required = !columnOptional;
|
||||
valueInput.required = valueRequired;
|
||||
valueInput.disabled = ['ALL', 'MY_DEPT', 'SELF', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type);
|
||||
valueInput.disabled = ['ALL', 'MY_DEPT', 'SELF'].includes(type);
|
||||
valueInput.placeholder = placeholderByType[type] || '비교 값';
|
||||
if (valueInput.disabled) {
|
||||
valueInput.value = '';
|
||||
@@ -705,9 +705,12 @@ function collectWizardRules(root) {
|
||||
if (type === 'ALL') {
|
||||
return 'ALL';
|
||||
}
|
||||
if (['MY_DEPT', 'SELF', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type)) {
|
||||
if (['MY_DEPT', 'SELF'].includes(type)) {
|
||||
return `${displayColumn} ${type}`;
|
||||
}
|
||||
if (['STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type)) {
|
||||
return `${displayColumn} ${type} 역할=${value}`;
|
||||
}
|
||||
if (type === 'TAG') {
|
||||
return `${displayColumn} TAG ${value.toUpperCase()}`;
|
||||
}
|
||||
@@ -740,10 +743,10 @@ function collectWizardPredicates(root) {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
||||
}
|
||||
if (type === 'STAKEHOLDER_SELF') {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID') 또는 담당계약 EXISTS`;
|
||||
return `EXISTS (KB_CONTRACTS c: c.${displayColumn} = <현재행>.${displayColumn} AND TOKEN_ROLE = '${value}' AND c.FC_ID = TOKEN_STAKEHOLDER_ID)`;
|
||||
}
|
||||
if (type === 'STAKEHOLDER_CHANNEL') {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL') 또는 담당계약 EXISTS`;
|
||||
return `EXISTS (KB_CONTRACTS c: c.${displayColumn} = <현재행>.${displayColumn} AND TOKEN_ROLE = '${value}' AND c.FC_CHANNEL = TOKEN_CHANNEL)`;
|
||||
}
|
||||
if (type === 'DEPT' || type === 'EMP_NO') {
|
||||
return `${displayColumn} = ${sqlLiteral(value)}`;
|
||||
@@ -869,7 +872,7 @@ function permissionWizardRuleValidationMessage(wizard) {
|
||||
if (values.some((rule) => rule.type === 'ALL') && values.length > 1) {
|
||||
return 'ALL 규칙은 다른 조건 규칙과 함께 저장할 수 없습니다. ALL만 남기거나 ALL을 삭제하세요.';
|
||||
}
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG'];
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'];
|
||||
const seen = new Set();
|
||||
for (const [index, rule] of values.entries()) {
|
||||
const position = index + 1;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>접근 규칙</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>VPD policy function이 참조할 사용자, 역할, 행 규칙, 컬럼 원문 허용 규칙을 저장합니다. KB 원장 예제에서 <code>STAKEHOLDER_SELF</code>는 토큰의 이해당사자 ID, <code>STAKEHOLDER_CHANNEL</code>은 역할에 연결된 채널로 행 범위를 계산합니다.</p>
|
||||
<p>VPD policy function이 참조할 사용자, 역할, 행 규칙, 컬럼 원문 허용 규칙을 저장합니다. KB 원장 예제는 토큰 주체에서 한 번 해석한 이해당사자 ID·역할·채널을 secure context로 사용하고, 대상 고객은 계약원장으로만 연결합니다. <code>STAKEHOLDER_SELF</code>와 <code>STAKEHOLDER_CHANNEL</code>은 각각의 역할 분기로 만들고 허용 분기끼리는 OR로 계산합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
</div>
|
||||
<details class="explanation-details">
|
||||
<summary>행 규칙과 기술 태그 OR 조건 보기</summary>
|
||||
<p class="wizard-hint">특정 기술 태그는 기본적으로 <code>TECH_TAG</code> 컬럼을 봅니다. 같은 ALLOW 권한에 태그를 여러 개 추가하면 “태그 A 또는 태그 B”로 조회됩니다. <code>이해당사자 본인 담당</code>은 <code>FC_ID</code>를 직접 비교하거나 <code>CUST_ID</code>/<code>CONTRACT_NO</code>를 계약원장에 안전하게 연결합니다. <code>이해당사자 채널 범위</code>도 같은 방식으로 <code>FC_CHANNEL</code>을 비교합니다. 두 이해당사자 규칙은 비교할 컬럼을 반드시 선택하세요.</p>
|
||||
<p class="wizard-hint">특정 기술 태그는 기본적으로 <code>TECH_TAG</code> 컬럼을 봅니다. 같은 ALLOW 권한에 태그를 여러 개 추가하면 “태그 A 또는 태그 B”로 조회됩니다. 이해당사자 규칙은 <strong>비교 컬럼</strong>과 <strong>업무 역할값</strong>(예: 설계사, 지점장)을 모두 입력하세요. 토큰을 해석한 뒤 <code>STAKEHOLDER_USER_ID</code>·<code>STAKEHOLDER_ROLE</code>·<code>STAKEHOLDER_CHANNEL</code> context를 사용합니다. <code>STAKEHOLDER_SELF</code>은 <code>c.FC_ID = context 사용자</code>, <code>STAKEHOLDER_CHANNEL</code>은 <code>c.FC_CHANNEL = context 채널</code>이며 서로 다른 허용 규칙은 OR로 결합됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,6 +241,7 @@
|
||||
|
||||
<section class="content-band">
|
||||
<h2>접근 규칙 목록</h2>
|
||||
<p class="section-subtitle">같은 보호 대상에 적용되는 ALLOW 역할 분기와 한 권한 안의 여러 행 규칙은 OR로 합칩니다. 예를 들어 설계사 본인계약 분기 또는 지점장 채널 분기 중 하나라도 성립하면 행이 허용됩니다. DENY 분기는 허용 결과에서 별도로 제외됩니다.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle permission-list-table">
|
||||
<thead>
|
||||
|
||||
@@ -175,8 +175,8 @@ class PermissionServiceTest {
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(
|
||||
new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", null),
|
||||
new RuleCommand("OWNER_EMP_NO", "STAKEHOLDER_CHANNEL", null)
|
||||
new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", "설계사"),
|
||||
new RuleCommand("OWNER_EMP_NO", "STAKEHOLDER_CHANNEL", "지점장")
|
||||
),
|
||||
List.of()
|
||||
);
|
||||
@@ -205,6 +205,22 @@ class PermissionServiceTest {
|
||||
.hasMessageContaining("컬럼이 필요");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStakeholderContextRuleWithoutExpectedRole() {
|
||||
var command = new PermissionSetCommand(
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", null)),
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertThatThrownBy(() -> permissionService.savePermissionSet(command))
|
||||
.isInstanceOf(AppException.class)
|
||||
.hasMessageContaining("값이 필요");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsMultipleVisibleColumns() {
|
||||
var command = new PermissionSetCommand(
|
||||
|
||||
Reference in New Issue
Block a user