[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_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다.
|
- 이해당사자마다 `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`를 정규화한다.
|
- 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로 변환한다.
|
- `RLS_FILTER`의 문자열은 실행하지 않는다. 권한 테이블에 저장된 allowlist rule type만 predicate로 변환한다.
|
||||||
|
|
||||||
## 3. 예제 권한
|
## 3. 예제 권한
|
||||||
@@ -26,7 +26,27 @@
|
|||||||
| KB_BRANCH_MANAGER_ROLE | 고객·담보·청구·외부보유 | `CUST_ID / STAKEHOLDER_CHANNEL` | 같은 채널 고객 기준 |
|
| KB_BRANCH_MANAGER_ROLE | 고객·담보·청구·외부보유 | `CUST_ID / STAKEHOLDER_CHANNEL` | 같은 채널 고객 기준 |
|
||||||
| 두 역할 | KB_STAKEHOLDERS | `USER_ID / STAKEHOLDER_SELF` | 토큰 주체 자신의 매핑 행 |
|
| 두 역할 | 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 처리
|
## 4. CLS / NULL 처리
|
||||||
|
|
||||||
@@ -75,7 +95,7 @@ Bearer Token → CB_AGENT_CTX_PKG → application context
|
|||||||
|
|
||||||
### 2026-07-07 배포 검증 결과
|
### 2026-07-07 배포 검증 결과
|
||||||
|
|
||||||
- Maven 테스트 77건 통과.
|
- Maven 테스트 78건 통과.
|
||||||
- HTTPS 인증 요청으로 `/permissions`, `/tokens`가 각각 200을 반환했다. 기존 `/permissions`의 잘못된 Thymeleaf fallback 표현식도 함께 수정했다.
|
- HTTPS 인증 요청으로 `/permissions`, `/tokens`가 각각 200을 반환했다. 기존 `/permissions`의 잘못된 Thymeleaf fallback 표현식도 함께 수정했다.
|
||||||
- `CB_APP_USER` bridge 13명, 지점장 역할 2명, 설계사 역할 5명이 동기화됐다.
|
- `CB_APP_USER` bridge 13명, 지점장 역할 2명, 설계사 역할 5명이 동기화됐다.
|
||||||
- `POC_2`에 행 VPD 7개와 민감 컬럼 NULL VPD policy 7개, 관련 CLS 함수 7개가 모두 `VALID` 상태로 확인됐다.
|
- `POC_2`에 행 VPD 7개와 민감 컬럼 NULL VPD policy 7개, 관련 CLS 함수 7개가 모두 `VALID` 상태로 확인됐다.
|
||||||
|
|||||||
@@ -170,56 +170,79 @@ BEGIN
|
|||||||
);
|
);
|
||||||
END IF;
|
END IF;
|
||||||
ELSIF r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL') THEN
|
ELSIF r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL') THEN
|
||||||
-- 업무 사용자 원장(KB_STAKEHOLDERS)에서 해석한 토큰 주체를 쓴다.
|
-- CB_AGENT_CTX_PKG가 Bearer token -> KB_STAKEHOLDERS를 한 번
|
||||||
-- CUST_ID/CONTRACT_NO는 계약원장을 한 단계 감싼 EXISTS로만 연결해
|
-- 해석해 USER_ID/ROLE/CHANNEL을 secure context로 설정한다.
|
||||||
-- 임의 SQL이나 외부 rule text가 predicate에 섞이지 않게 한다.
|
-- 따라서 보호 테이블 predicate에서 KB_STAKEHOLDERS를 다시 조인하지
|
||||||
|
-- 않고, 해당 context와 KB_CONTRACTS 관계만 사용한다.
|
||||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||||
IF v_column IS NOT NULL
|
IF v_column IS NOT NULL
|
||||||
AND UPPER(p_schema) = 'POC_2' THEN
|
AND UPPER(p_schema) = 'POC_2' THEN
|
||||||
IF r.rule_type = 'STAKEHOLDER_SELF'
|
DECLARE
|
||||||
AND ((v_target = 'KB_CONTRACTS' AND v_column = 'FC_ID')
|
v_contract_key VARCHAR2(30);
|
||||||
OR (v_target = 'KB_STAKEHOLDERS' AND v_column = 'USER_ID')) THEN
|
v_role_condition VARCHAR2(500);
|
||||||
append_or(
|
BEGIN
|
||||||
r.permission_effect,
|
-- 각 ALLOW rule은 append_or로 OR 분기가 된다. rule_value는
|
||||||
v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')'
|
-- 권한 설정에 저장된 해당 분기의 역할값이다. identity rule은
|
||||||
);
|
-- NULL value를 허용해 모든 토큰 주체가 자기 mapping을 해석한다.
|
||||||
ELSIF r.rule_type = 'STAKEHOLDER_CHANNEL'
|
v_role_condition := CASE
|
||||||
AND v_target = 'KB_CONTRACTS'
|
WHEN TRIM(r.rule_value) IS NULL THEN '1 = 1'
|
||||||
AND v_column = 'FC_CHANNEL' THEN
|
ELSE 'SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = '
|
||||||
append_or(
|
|| quote_literal(TRIM(r.rule_value))
|
||||||
r.permission_effect,
|
END;
|
||||||
v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')'
|
|
||||||
);
|
IF r.rule_type = 'STAKEHOLDER_SELF'
|
||||||
ELSIF v_target IN ('KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
|
AND v_target = 'KB_CONTRACTS'
|
||||||
AND v_column IN ('CUST_ID', 'CONTRACT_NO') THEN
|
AND v_column = 'FC_ID' THEN
|
||||||
DECLARE
|
append_or(
|
||||||
v_contract_key VARCHAR2(30);
|
r.permission_effect,
|
||||||
v_context_key VARCHAR2(30);
|
'(' || v_role_condition ||
|
||||||
v_context_name VARCHAR2(30);
|
' AND FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||||
BEGIN
|
);
|
||||||
|
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
|
v_contract_key := CASE v_column
|
||||||
WHEN 'CUST_ID' THEN 'CUST_ID'
|
WHEN 'CUST_ID' THEN 'CUST_ID'
|
||||||
WHEN 'CONTRACT_NO' THEN 'CONTRACT_NO'
|
WHEN 'CONTRACT_NO' THEN 'CONTRACT_NO'
|
||||||
END;
|
END;
|
||||||
v_context_key := CASE r.rule_type
|
IF r.rule_type = 'STAKEHOLDER_SELF' THEN
|
||||||
WHEN 'STAKEHOLDER_SELF' THEN 'FC_ID'
|
append_or(
|
||||||
ELSE 'FC_CHANNEL'
|
r.permission_effect,
|
||||||
END;
|
'EXISTS (SELECT 1 FROM (' ||
|
||||||
v_context_name := CASE r.rule_type
|
'SELECT c.' || v_contract_key || ' AS access_key ' ||
|
||||||
WHEN 'STAKEHOLDER_SELF' THEN 'STAKEHOLDER_USER_ID'
|
'FROM POC_2.KB_CONTRACTS c ' ||
|
||||||
ELSE 'STAKEHOLDER_CHANNEL'
|
'WHERE ' || v_role_condition ||
|
||||||
END;
|
' AND c.FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')) allowed_contract ' ||
|
||||||
append_or(
|
'WHERE allowed_contract.access_key = ' || v_column || ')'
|
||||||
r.permission_effect,
|
);
|
||||||
'EXISTS (SELECT 1 FROM (' ||
|
ELSE
|
||||||
'SELECT c.' || v_contract_key || ' AS access_key ' ||
|
append_or(
|
||||||
'FROM POC_2.KB_CONTRACTS c ' ||
|
r.permission_effect,
|
||||||
'WHERE c.' || v_context_key || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''' ||
|
'EXISTS (SELECT 1 FROM (' ||
|
||||||
v_context_name || ''')) allowed_contract ' ||
|
'SELECT c.' || v_contract_key || ' AS access_key ' ||
|
||||||
'WHERE allowed_contract.access_key = ' || v_column || ')'
|
'FROM POC_2.KB_CONTRACTS c ' ||
|
||||||
);
|
'WHERE ' || v_role_condition ||
|
||||||
END;
|
' AND c.FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')) allowed_contract ' ||
|
||||||
END IF;
|
'WHERE allowed_contract.access_key = ' || v_column || ')'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
END IF;
|
END IF;
|
||||||
ELSIF r.rule_type = '=' THEN
|
ELSIF r.rule_type = '=' THEN
|
||||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||||
|
|||||||
@@ -313,7 +313,8 @@ DECLARE
|
|||||||
p_role_id IN NUMBER,
|
p_role_id IN NUMBER,
|
||||||
p_target_name IN VARCHAR2,
|
p_target_name IN VARCHAR2,
|
||||||
p_rule_column IN VARCHAR2,
|
p_rule_column IN VARCHAR2,
|
||||||
p_rule_type IN VARCHAR2
|
p_rule_type IN VARCHAR2,
|
||||||
|
p_rule_value IN VARCHAR2
|
||||||
) IS
|
) IS
|
||||||
v_permission_id NUMBER;
|
v_permission_id NUMBER;
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -328,7 +329,7 @@ DECLARE
|
|||||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
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');
|
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)
|
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;
|
END;
|
||||||
BEGIN
|
BEGIN
|
||||||
ensure_role('KB_DESIGNER_ROLE', 'CONFIDENTIAL', v_designer_role_id);
|
ensure_role('KB_DESIGNER_ROLE', 'CONFIDENTIAL', v_designer_role_id);
|
||||||
@@ -382,20 +383,20 @@ BEGIN
|
|||||||
|
|
||||||
-- 토큰은 모든 이해당사자에게 발급되지만, 아래 역할이 없는 주체는
|
-- 토큰은 모든 이해당사자에게 발급되지만, 아래 역할이 없는 주체는
|
||||||
-- 권한 레코드가 없으므로 VPD가 1=0으로 fail-closed 된다.
|
-- 권한 레코드가 없으므로 VPD가 1=0으로 fail-closed 된다.
|
||||||
ensure_permission(v_designer_role_id, 'KB_STAKEHOLDERS', 'USER_ID', 'STAKEHOLDER_SELF');
|
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_manager_role_id, 'KB_STAKEHOLDERS', 'USER_ID', 'STAKEHOLDER_SELF', '지점장');
|
||||||
ensure_permission(v_designer_role_id, 'KB_PRODUCTS', NULL, 'ALL');
|
ensure_permission(v_designer_role_id, 'KB_PRODUCTS', NULL, 'ALL', NULL);
|
||||||
ensure_permission(v_manager_role_id, 'KB_PRODUCTS', NULL, 'ALL');
|
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_designer_role_id, 'KB_CONTRACTS', 'FC_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||||
ensure_permission(v_manager_role_id, 'KB_CONTRACTS', 'FC_CHANNEL', 'STAKEHOLDER_CHANNEL');
|
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_designer_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||||
ensure_permission(v_manager_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
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_designer_role_id, 'KB_COVERAGES', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||||
ensure_permission(v_manager_role_id, 'KB_COVERAGES', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
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_designer_role_id, 'KB_CLAIMS', 'CUST_ID', 'STAKEHOLDER_SELF', '설계사');
|
||||||
ensure_permission(v_manager_role_id, 'KB_CLAIMS', 'CUST_ID', 'STAKEHOLDER_CHANNEL');
|
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_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_manager_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'STAKEHOLDER_CHANNEL', '지점장');
|
||||||
END;
|
END;
|
||||||
/
|
/
|
||||||
COMMIT;
|
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",
|
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||||
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
|
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL");
|
||||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
|
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(
|
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
|
||||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG");
|
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG");
|
||||||
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
||||||
|
|||||||
@@ -93,13 +93,27 @@
|
|||||||
WHEN pr2.rule_type = 'TAG' THEN 'REGEXP_LIKE(UPPER('
|
WHEN pr2.rule_type = 'TAG' THEN 'REGEXP_LIKE(UPPER('
|
||||||
|| NVL(pr2.rule_column, 'TECH_TAG')
|
|| NVL(pr2.rule_column, 'TECH_TAG')
|
||||||
|| '), ''(^|,)' || REPLACE(UPPER(pr2.rule_value), '''', '''''') || '(,|$)'')'
|
|| '), ''(^|,)' || REPLACE(UPPER(pr2.rule_value), '''', '''''') || '(,|$)'')'
|
||||||
WHEN pr2.rule_type = 'STAKEHOLDER_SELF' THEN pr2.rule_column
|
WHEN pr2.rule_type = 'STAKEHOLDER_SELF' AND pr2.rule_column = 'FC_ID' THEN
|
||||||
|| ' = token stakeholder / 담당계약'
|
'(SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_ROLE'') = ''' || NVL(pr2.rule_value, '[역할값 필요]') || ''' '
|
||||||
WHEN pr2.rule_type = 'STAKEHOLDER_CHANNEL' THEN pr2.rule_column
|
|| 'AND FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID''))'
|
||||||
|| ' = token stakeholder channel / 담당계약'
|
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
|
ELSE pr2.rule_column || ' ' || pr2.rule_type || ' ' || pr2.rule_value
|
||||||
END,
|
END,
|
||||||
CHR(10) || 'AND '
|
CHR(10) || 'OR '
|
||||||
) WITHIN GROUP (ORDER BY pr2.rule_id)
|
) WITHIN GROUP (ORDER BY pr2.rule_id)
|
||||||
FROM cb_permission_rule pr2
|
FROM cb_permission_rule pr2
|
||||||
WHERE pr2.perm_id = p.perm_id
|
WHERE pr2.perm_id = p.perm_id
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ function syncRuleTypeHints(root = document) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = typeSelect.value;
|
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 columnOptional = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG'].includes(type);
|
||||||
const placeholderByType = {
|
const placeholderByType = {
|
||||||
ALL: '값 불필요',
|
ALL: '값 불필요',
|
||||||
@@ -397,15 +397,15 @@ function syncRuleTypeHints(root = document) {
|
|||||||
DEPT: '예: HR',
|
DEPT: '예: HR',
|
||||||
EMP_NO: '예: E2001',
|
EMP_NO: '예: E2001',
|
||||||
TAG: '예: SPRING_BOOT',
|
TAG: '예: SPRING_BOOT',
|
||||||
STAKEHOLDER_SELF: '토큰 이해당사자에서 자동 계산',
|
STAKEHOLDER_SELF: '예: 설계사',
|
||||||
STAKEHOLDER_CHANNEL: '토큰 채널에서 자동 계산',
|
STAKEHOLDER_CHANNEL: '예: 지점장',
|
||||||
'=': '비교 값',
|
'=': '비교 값',
|
||||||
'!=': '비교 값'
|
'!=': '비교 값'
|
||||||
};
|
};
|
||||||
|
|
||||||
columnSelect.required = !columnOptional;
|
columnSelect.required = !columnOptional;
|
||||||
valueInput.required = valueRequired;
|
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] || '비교 값';
|
valueInput.placeholder = placeholderByType[type] || '비교 값';
|
||||||
if (valueInput.disabled) {
|
if (valueInput.disabled) {
|
||||||
valueInput.value = '';
|
valueInput.value = '';
|
||||||
@@ -705,9 +705,12 @@ function collectWizardRules(root) {
|
|||||||
if (type === 'ALL') {
|
if (type === 'ALL') {
|
||||||
return 'ALL';
|
return 'ALL';
|
||||||
}
|
}
|
||||||
if (['MY_DEPT', 'SELF', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type)) {
|
if (['MY_DEPT', 'SELF'].includes(type)) {
|
||||||
return `${displayColumn} ${type}`;
|
return `${displayColumn} ${type}`;
|
||||||
}
|
}
|
||||||
|
if (['STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'].includes(type)) {
|
||||||
|
return `${displayColumn} ${type} 역할=${value}`;
|
||||||
|
}
|
||||||
if (type === 'TAG') {
|
if (type === 'TAG') {
|
||||||
return `${displayColumn} TAG ${value.toUpperCase()}`;
|
return `${displayColumn} TAG ${value.toUpperCase()}`;
|
||||||
}
|
}
|
||||||
@@ -740,10 +743,10 @@ function collectWizardPredicates(root) {
|
|||||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
||||||
}
|
}
|
||||||
if (type === 'STAKEHOLDER_SELF') {
|
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') {
|
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') {
|
if (type === 'DEPT' || type === 'EMP_NO') {
|
||||||
return `${displayColumn} = ${sqlLiteral(value)}`;
|
return `${displayColumn} = ${sqlLiteral(value)}`;
|
||||||
@@ -869,7 +872,7 @@ function permissionWizardRuleValidationMessage(wizard) {
|
|||||||
if (values.some((rule) => rule.type === 'ALL') && values.length > 1) {
|
if (values.some((rule) => rule.type === 'ALL') && values.length > 1) {
|
||||||
return 'ALL 규칙은 다른 조건 규칙과 함께 저장할 수 없습니다. ALL만 남기거나 ALL을 삭제하세요.';
|
return 'ALL 규칙은 다른 조건 규칙과 함께 저장할 수 없습니다. ALL만 남기거나 ALL을 삭제하세요.';
|
||||||
}
|
}
|
||||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG'];
|
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL'];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const [index, rule] of values.entries()) {
|
for (const [index, rule] of values.entries()) {
|
||||||
const position = index + 1;
|
const position = index + 1;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<h1>접근 규칙</h1>
|
<h1>접근 규칙</h1>
|
||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
<summary>도움말</summary>
|
<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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
<summary>행 규칙과 기술 태그 OR 조건 보기</summary>
|
<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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,6 +241,7 @@
|
|||||||
|
|
||||||
<section class="content-band">
|
<section class="content-band">
|
||||||
<h2>접근 규칙 목록</h2>
|
<h2>접근 규칙 목록</h2>
|
||||||
|
<p class="section-subtitle">같은 보호 대상에 적용되는 ALLOW 역할 분기와 한 권한 안의 여러 행 규칙은 OR로 합칩니다. 예를 들어 설계사 본인계약 분기 또는 지점장 채널 분기 중 하나라도 성립하면 행이 허용됩니다. DENY 분기는 허용 결과에서 별도로 제외됩니다.</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm align-middle permission-list-table">
|
<table class="table table-sm align-middle permission-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ class PermissionServiceTest {
|
|||||||
"SELECT",
|
"SELECT",
|
||||||
"ALLOW",
|
"ALLOW",
|
||||||
List.of(
|
List.of(
|
||||||
new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", null),
|
new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", "설계사"),
|
||||||
new RuleCommand("OWNER_EMP_NO", "STAKEHOLDER_CHANNEL", null)
|
new RuleCommand("OWNER_EMP_NO", "STAKEHOLDER_CHANNEL", "지점장")
|
||||||
),
|
),
|
||||||
List.of()
|
List.of()
|
||||||
);
|
);
|
||||||
@@ -205,6 +205,22 @@ class PermissionServiceTest {
|
|||||||
.hasMessageContaining("컬럼이 필요");
|
.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
|
@Test
|
||||||
void acceptsMultipleVisibleColumns() {
|
void acceptsMultipleVisibleColumns() {
|
||||||
var command = new PermissionSetCommand(
|
var command = new PermissionSetCommand(
|
||||||
|
|||||||
Reference in New Issue
Block a user