Files
vpd-permission-poc/database/adb/46_kb_stakeholder_token_permission_example.sql

472 lines
17 KiB
MySQL

-- ============================================================
-- 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,
p_rule_value 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, p_rule_value);
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_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', 'OWN_CONTRACT', NULL);
ensure_permission(v_manager_role_id, 'KB_CONTRACTS', 'FC_CHANNEL', 'CHANNEL_CONTRACT', NULL);
ensure_permission(v_designer_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'OWN_CUSTOMER', NULL);
ensure_permission(v_manager_role_id, 'KB_CUSTOMERS', 'CUST_ID', 'CHANNEL_CUSTOMER', NULL);
ensure_permission(v_designer_role_id, 'KB_COVERAGES', 'CUST_ID', 'OWN_CUSTOMER', NULL);
ensure_permission(v_manager_role_id, 'KB_COVERAGES', 'CUST_ID', 'CHANNEL_CUSTOMER', NULL);
ensure_permission(v_designer_role_id, 'KB_CLAIMS', 'CUST_ID', 'OWN_CUSTOMER', NULL);
ensure_permission(v_manager_role_id, 'KB_CLAIMS', 'CUST_ID', 'CHANNEL_CUSTOMER', NULL);
ensure_permission(v_designer_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'OWN_CUSTOMER', NULL);
ensure_permission(v_manager_role_id, 'KB_EXTERNAL_HOLDINGS', 'CUST_ID', 'CHANNEL_CUSTOMER', NULL);
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. Removing deprecated VPD column NULL policies ===
-- Architecture rule: VPD applies row predicates only. ASO/Data Redaction
-- owns all column masking and its user-specific original-value exceptions.
DECLARE
PROCEDURE drop_column_policy(p_object_name IN VARCHAR2, p_policy_name IN VARCHAR2) IS
BEGIN
DBMS_RLS.DROP_POLICY(
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -28102 THEN RAISE; END IF;
END;
BEGIN
drop_column_policy('KB_CUSTOMERS', 'KB_CUST_NM_CLS_POLICY');
drop_column_policy('KB_CUSTOMERS', 'KB_RRN_CLS_POLICY');
drop_column_policy('KB_CLAIMS', 'KB_CLAIM_AMT_CLS_POLICY');
drop_column_policy('KB_CLAIMS', 'KB_PAID_AMT_CLS_POLICY');
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_INSURER_CLS_POLICY');
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_PRODUCT_GRP_CLS_POLICY');
drop_column_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_PRODUCT_TYPE_CLS_POLICY');
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 ===