-- ============================================================ -- 46_kb_stakeholder_token_permission_example.sql -- -- KB_STAKEHOLDERS를 토큰의 업무 주체로 연결하고, 접근 규칙 화면에서 -- 관리하는 설계사/지점장 예제 권한을 등록한다. -- -- Prerequisite: 17, 25, 26 scripts. Execute as ADMIN. -- 26 must include STAKEHOLDER_SELF / STAKEHOLDER_CHANNEL rule support. -- ============================================================ WHENEVER SQLERROR EXIT SQL.SQLCODE SET ECHO OFF SET FEEDBACK ON SET DEFINE OFF PROMPT === 1. Adding stakeholder bridge columns === DECLARE PROCEDURE add_column_if_missing( p_table_name IN VARCHAR2, p_column_name IN VARCHAR2, p_ddl IN VARCHAR2 ) IS v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM user_tab_columns WHERE table_name = p_table_name AND column_name = p_column_name; IF v_count = 0 THEN EXECUTE IMMEDIATE p_ddl; END IF; END; BEGIN add_column_if_missing( 'CB_APP_USER', 'STAKEHOLDER_USER_ID', 'ALTER TABLE cb_app_user ADD (stakeholder_user_id VARCHAR2(30))' ); add_column_if_missing( 'CB_AGENT_BEARER_KEY', 'STAKEHOLDER_USER_ID', 'ALTER TABLE cb_agent_bearer_key ADD (stakeholder_user_id VARCHAR2(30))' ); END; / PROMPT === 2. Resolving Bearer Token to stakeholder context === CREATE OR REPLACE PACKAGE cb_agent_ctx_pkg AUTHID DEFINER AS PROCEDURE clear_user; PROCEDURE set_user(p_user_id IN NUMBER); PROCEDURE set_user_values( p_user_id IN NUMBER, p_emp_no IN VARCHAR2, p_dept_code IN VARCHAR2, p_can_read_contents IN VARCHAR2 ); PROCEDURE set_user_by_bearer(p_bearer_key IN VARCHAR2); END; / CREATE OR REPLACE PACKAGE BODY cb_agent_ctx_pkg AS PROCEDURE clear_stakeholder_context AS BEGIN DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID', NULL); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_ROLE', NULL); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL', NULL); END; PROCEDURE clear_user AS BEGIN DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'USER_ID', NULL); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'EMP_NO', NULL); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE', NULL); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'CAN_READ_CONTENTS', NULL); clear_stakeholder_context; END; PROCEDURE set_user_values( p_user_id IN NUMBER, p_emp_no IN VARCHAR2, p_dept_code IN VARCHAR2, p_can_read_contents IN VARCHAR2 ) AS BEGIN DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'USER_ID', TO_CHAR(p_user_id)); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'EMP_NO', p_emp_no); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE', p_dept_code); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'CAN_READ_CONTENTS', p_can_read_contents); clear_stakeholder_context; END; PROCEDURE set_stakeholder_context( p_stakeholder_user_id IN VARCHAR2, p_role IN VARCHAR2, p_channel IN VARCHAR2 ) AS v_channel VARCHAR2(50); BEGIN v_channel := CASE TRIM(p_channel) WHEN '설계사채널' THEN '설계사' WHEN 'GA채널' THEN 'GA' ELSE TRIM(p_channel) END; DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID', p_stakeholder_user_id); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_ROLE', p_role); DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL', v_channel); END; PROCEDURE set_user(p_user_id IN NUMBER) AS v_emp_no cb_app_user.employee_no%TYPE; v_dept_code cb_app_user.dept_code%TYPE; v_can_read_contents cb_app_user.can_read_contents%TYPE; BEGIN SELECT employee_no, dept_code, CASE WHEN EXISTS ( SELECT 1 FROM cb_user_role ur JOIN cb_permission p ON p.role_id = ur.role_id JOIN cb_permission_column pc ON pc.permission_id = p.perm_id WHERE ur.user_id = p_user_id AND p.target_name = 'CB_V_SEARCH_DOCUMENTS' AND p.action_name = 'SELECT' AND pc.column_name = 'CONTENTS' ) THEN 'Y' ELSE 'N' END INTO v_emp_no, v_dept_code, v_can_read_contents FROM cb_app_user WHERE user_id = p_user_id AND active = 'Y'; set_user_values(p_user_id, v_emp_no, v_dept_code, v_can_read_contents); EXCEPTION WHEN NO_DATA_FOUND THEN clear_user; RAISE_APPLICATION_ERROR(-20003, 'Mapped application user not found'); END; PROCEDURE set_user_by_bearer(p_bearer_key IN VARCHAR2) AS v_user_id cb_app_user.user_id%TYPE; v_emp_no cb_app_user.employee_no%TYPE; v_dept_code cb_app_user.dept_code%TYPE; v_can_read_contents cb_app_user.can_read_contents%TYPE; v_stakeholder_user_id VARCHAR2(30); v_stakeholder_role VARCHAR2(50); v_stakeholder_channel VARCHAR2(50); BEGIN IF p_bearer_key IS NULL THEN clear_user; RAISE_APPLICATION_ERROR(-20001, 'Authorization Bearer key is required'); END IF; SELECT u.user_id, u.employee_no, u.dept_code, CASE WHEN EXISTS ( SELECT 1 FROM cb_user_role ur JOIN cb_permission p ON p.role_id = ur.role_id JOIN cb_permission_column pc ON pc.permission_id = p.perm_id WHERE ur.user_id = u.user_id AND p.target_name = 'CB_V_SEARCH_DOCUMENTS' AND p.action_name = 'SELECT' AND pc.column_name = 'CONTENTS' ) THEN 'Y' ELSE 'N' END, COALESCE(k.stakeholder_user_id, u.stakeholder_user_id) INTO v_user_id, v_emp_no, v_dept_code, v_can_read_contents, v_stakeholder_user_id FROM cb_agent_bearer_key k JOIN cb_app_user u ON u.user_id = k.user_id WHERE k.key_hash = STANDARD_HASH(p_bearer_key, 'SHA256') AND k.active = 'Y' AND k.revoked_at IS NULL AND (k.expires_at IS NULL OR k.expires_at > SYSDATE) AND u.active = 'Y'; -- USER_ID를 먼저 넣어야 KB_STAKEHOLDERS 자체가 VPD 보호된 뒤에도 -- 해당 주체의 자기 행만 안전하게 해석할 수 있다. set_user_values(v_user_id, v_emp_no, v_dept_code, v_can_read_contents); IF v_stakeholder_user_id IS NOT NULL THEN DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID', v_stakeholder_user_id); SELECT role, channel INTO v_stakeholder_role, v_stakeholder_channel FROM poc_2.kb_stakeholders WHERE user_id = v_stakeholder_user_id; set_stakeholder_context(v_stakeholder_user_id, v_stakeholder_role, v_stakeholder_channel); END IF; EXCEPTION WHEN NO_DATA_FOUND THEN clear_user; RAISE_APPLICATION_ERROR(-20002, 'Invalid or expired Bearer key'); END; END; / SHOW ERRORS PACKAGE BODY cb_agent_ctx_pkg PROMPT === 3. Syncing stakeholder bridge and example permissions === DECLARE v_designer_role_id NUMBER; v_manager_role_id NUMBER; v_object_id NUMBER; v_user_id NUMBER; PROCEDURE ensure_role( p_role_name IN VARCHAR2, p_max_level IN VARCHAR2, p_role_id OUT NUMBER ) IS BEGIN SELECT role_id INTO p_role_id FROM cb_app_role WHERE role_name = p_role_name; EXCEPTION WHEN NO_DATA_FOUND THEN SELECT NVL(MAX(role_id), 0) + 1 INTO p_role_id FROM cb_app_role; INSERT INTO cb_app_role(role_id, role_name, max_sensitivity_level) VALUES (p_role_id, p_role_name, p_max_level); END; PROCEDURE ensure_object( p_object_name IN VARCHAR2, p_description IN VARCHAR2, p_object_id OUT NUMBER ) IS BEGIN SELECT object_id INTO p_object_id FROM cb_protected_object WHERE owner = 'POC_2' AND object_name = p_object_name; UPDATE cb_protected_object SET enabled_yn = 'Y', ords_path = 'cb-ords/cb-object-query/poc_2/' || LOWER(p_object_name), description = p_description WHERE object_id = p_object_id; EXCEPTION WHEN NO_DATA_FOUND THEN SELECT NVL(MAX(object_id), 0) + 1 INTO p_object_id FROM cb_protected_object; INSERT INTO cb_protected_object( object_id, owner, object_name, ords_path, enabled_yn, description ) VALUES ( p_object_id, 'POC_2', p_object_name, 'cb-ords/cb-object-query/poc_2/' || LOWER(p_object_name), 'Y', p_description ); END; FUNCTION is_sensitive(p_object_name IN VARCHAR2, p_column_name IN VARCHAR2) RETURN CHAR IS BEGIN IF (p_object_name = 'KB_CUSTOMERS' AND p_column_name IN ('CUST_NM', 'RRN_MASKED')) OR (p_object_name = 'KB_CLAIMS' AND p_column_name IN ('CLAIM_AMT', 'PAID_AMT')) OR (p_object_name = 'KB_EXTERNAL_HOLDINGS' AND p_column_name IN ('EXT_INSURER', 'EXT_PRODUCT_GRP', 'EXT_PRODUCT_TYPE')) THEN RETURN 'Y'; END IF; RETURN 'N'; END; PROCEDURE ensure_columns(p_object_name IN VARCHAR2, p_object_id IN NUMBER) IS v_column_id NUMBER; v_count NUMBER; v_sensitive CHAR(1); BEGIN FOR c IN ( SELECT column_name FROM all_tab_columns WHERE owner = 'POC_2' AND table_name = p_object_name ORDER BY column_id ) LOOP v_sensitive := is_sensitive(p_object_name, c.column_name); SELECT COUNT(*) INTO v_count FROM cb_protected_column WHERE object_id = p_object_id AND column_name = c.column_name; IF v_count = 0 THEN SELECT NVL(MAX(column_id), 0) + 1 INTO v_column_id FROM cb_protected_column; INSERT INTO cb_protected_column( column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method ) VALUES ( v_column_id, p_object_id, c.column_name, v_sensitive, CASE v_sensitive WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END, CASE v_sensitive WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END ); ELSIF v_sensitive = 'Y' THEN UPDATE cb_protected_column SET sensitive_yn = 'Y', sensitivity_level = 'CONFIDENTIAL', redaction_method = 'NULLIFY' WHERE object_id = p_object_id AND column_name = c.column_name; END IF; END LOOP; END; PROCEDURE ensure_user_role(p_user_id IN NUMBER, p_role_id IN NUMBER) IS v_count NUMBER; BEGIN SELECT COUNT(*) INTO v_count FROM cb_user_role WHERE user_id = p_user_id AND role_id = p_role_id; IF v_count = 0 THEN INSERT INTO cb_user_role(user_id, role_id) VALUES (p_user_id, p_role_id); END IF; END; PROCEDURE ensure_permission( p_role_id IN NUMBER, p_target_name IN VARCHAR2, p_rule_column IN VARCHAR2, p_rule_type IN VARCHAR2 ) IS v_permission_id NUMBER; BEGIN SELECT perm_id INTO v_permission_id FROM cb_permission WHERE role_id = p_role_id AND target_name = p_target_name 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, 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); END; BEGIN ensure_role('KB_DESIGNER_ROLE', 'CONFIDENTIAL', v_designer_role_id); ensure_role('KB_BRANCH_MANAGER_ROLE', 'CONFIDENTIAL', v_manager_role_id); FOR s IN ( SELECT user_id, user_nm, role, channel FROM poc_2.kb_stakeholders ORDER BY user_id ) LOOP BEGIN SELECT user_id INTO v_user_id FROM cb_app_user WHERE stakeholder_user_id = s.user_id; UPDATE cb_app_user SET user_name = s.user_nm, employee_no = s.user_id, dept_code = s.channel, active = 'Y' WHERE user_id = v_user_id; EXCEPTION WHEN NO_DATA_FOUND THEN SELECT NVL(MAX(user_id), 0) + 1 INTO v_user_id FROM cb_app_user; INSERT INTO cb_app_user( user_id, user_name, employee_no, dept_code, stakeholder_user_id, can_read_contents, active ) VALUES ( v_user_id, s.user_nm, s.user_id, s.channel, s.user_id, 'N', 'Y' ); END; IF s.role = '지점장' THEN ensure_user_role(v_user_id, v_manager_role_id); ELSIF s.role = '설계사' THEN ensure_user_role(v_user_id, v_designer_role_id); END IF; END LOOP; FOR o IN ( SELECT 'KB_STAKEHOLDERS' object_name, '토큰 업무 주체·역할·채널 원장' description FROM dual UNION ALL SELECT 'KB_CUSTOMERS', '고객 기본정보 원장' FROM dual UNION ALL SELECT 'KB_PRODUCTS', '보험상품 마스터' FROM dual UNION ALL SELECT 'KB_CONTRACTS', '보험계약 원장' FROM dual UNION ALL SELECT 'KB_COVERAGES', '담보·특약 원장' FROM dual UNION ALL SELECT 'KB_CLAIMS', '보험금 청구·지급 원장' FROM dual UNION ALL SELECT 'KB_EXTERNAL_HOLDINGS', '외부보유정보 원장' FROM dual ) LOOP ensure_object(o.object_name, o.description, v_object_id); ensure_columns(o.object_name, v_object_id); END LOOP; -- 토큰은 모든 이해당사자에게 발급되지만, 아래 역할이 없는 주체는 -- 권한 레코드가 없으므로 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'); END; / COMMIT; PROMPT === 4. Applying row VPD policies to KB business tables === DECLARE PROCEDURE replace_row_policy(p_object_name IN VARCHAR2) IS BEGIN BEGIN DBMS_RLS.DROP_POLICY( object_schema => 'POC_2', object_name => p_object_name, policy_name => 'KB_' || p_object_name || '_ROW_POLICY' ); EXCEPTION WHEN OTHERS THEN NULL; END; DBMS_RLS.ADD_POLICY( object_schema => 'POC_2', object_name => p_object_name, policy_name => 'KB_' || p_object_name || '_ROW_POLICY', function_schema => USER, policy_function => 'CB_AGENT_DOC_VPD_FILTER', statement_types => 'SELECT', policy_type => DBMS_RLS.DYNAMIC, enable => TRUE ); END; BEGIN replace_row_policy('KB_STAKEHOLDERS'); replace_row_policy('KB_CUSTOMERS'); replace_row_policy('KB_PRODUCTS'); replace_row_policy('KB_CONTRACTS'); replace_row_policy('KB_COVERAGES'); replace_row_policy('KB_CLAIMS'); replace_row_policy('KB_EXTERNAL_HOLDINGS'); END; / PROMPT === 5. Applying column NULL policies from permission display exceptions === -- ADB rejects application PL/SQL functions inside DBMS_REDACT expressions. -- SEC_RELEVANT_COL_OPT=ALL_ROWS is the VPD equivalent: it retains the row -- but returns NULL for the protected column while the column predicate is false. CREATE OR REPLACE FUNCTION cb_kb_cust_nm_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_CUSTOMERS', 'CUST_NM') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / CREATE OR REPLACE FUNCTION cb_kb_rrn_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_CUSTOMERS', 'RRN_MASKED') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / CREATE OR REPLACE FUNCTION cb_kb_claim_amt_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_CLAIMS', 'CLAIM_AMT') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / CREATE OR REPLACE FUNCTION cb_kb_paid_amt_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_CLAIMS', 'PAID_AMT') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / CREATE OR REPLACE FUNCTION cb_kb_ext_insurer_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / CREATE OR REPLACE FUNCTION cb_kb_ext_product_grp_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / CREATE OR REPLACE FUNCTION cb_kb_ext_product_type_cls(p_schema IN VARCHAR2, p_object IN VARCHAR2) RETURN VARCHAR2 AUTHID DEFINER AS BEGIN RETURN CASE WHEN cb_agent_can_read_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE') = 'Y' THEN '1 = 1' ELSE '1 = 0' END; END; / DECLARE PROCEDURE replace_column_policy( p_object_name IN VARCHAR2, p_column_name IN VARCHAR2, p_policy_name IN VARCHAR2, p_function_name IN VARCHAR2 ) IS BEGIN BEGIN DBMS_RLS.DROP_POLICY( object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name ); EXCEPTION WHEN OTHERS THEN NULL; END; DBMS_RLS.ADD_POLICY( object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name, function_schema => USER, policy_function => p_function_name, statement_types => 'SELECT', policy_type => DBMS_RLS.DYNAMIC, sec_relevant_cols => p_column_name, sec_relevant_cols_opt => DBMS_RLS.ALL_ROWS, enable => TRUE ); END; BEGIN replace_column_policy('KB_CUSTOMERS', 'CUST_NM', 'KB_CUST_NM_CLS_POLICY', 'CB_KB_CUST_NM_CLS'); replace_column_policy('KB_CUSTOMERS', 'RRN_MASKED', 'KB_RRN_CLS_POLICY', 'CB_KB_RRN_CLS'); replace_column_policy('KB_CLAIMS', 'CLAIM_AMT', 'KB_CLAIM_AMT_CLS_POLICY', 'CB_KB_CLAIM_AMT_CLS'); replace_column_policy('KB_CLAIMS', 'PAID_AMT', 'KB_PAID_AMT_CLS_POLICY', 'CB_KB_PAID_AMT_CLS'); replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER', 'KB_EXT_INSURER_CLS_POLICY', 'CB_KB_EXT_INSURER_CLS'); replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP', 'KB_EXT_PRODUCT_GRP_CLS_POLICY', 'CB_KB_EXT_PRODUCT_GRP_CLS'); replace_column_policy('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE', 'KB_EXT_PRODUCT_TYPE_CLS_POLICY', 'CB_KB_EXT_PRODUCT_TYPE_CLS'); END; / PROMPT === 6. Example configuration summary === SELECT r.role_name, p.target_name, pr.rule_column, pr.rule_type, NVL(pr.rule_value, '-') AS rule_value FROM cb_permission p JOIN cb_app_role r ON r.role_id = p.role_id JOIN cb_permission_rule pr ON pr.perm_id = p.perm_id WHERE r.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE') ORDER BY r.role_name, p.target_name, pr.rule_id; PROMPT === KB stakeholder token permission example complete ===