Consolidate data access control backoffice updates

This commit is contained in:
devmrko
2026-07-13 23:06:23 +09:00
parent 403298d474
commit e18b30feab
181 changed files with 11571 additions and 954 deletions

View File

@@ -0,0 +1,94 @@
-- ============================================================
-- 16_agent_ords_security_local_cleanup.sql
-- Agent ORDS security local-only cleanup.
--
-- This removes only CB_* objects used by the local ADB example.
-- It does not touch the existing RDS DB Link POC objects.
-- ============================================================
WHENEVER SQLERROR CONTINUE
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Cleaning VPD policy ===
BEGIN
DBMS_REDACT.DROP_POLICY(
object_schema => USER,
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_CONTENTS_REDACT'
);
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
BEGIN
DBMS_RLS.DROP_POLICY(
object_schema => USER,
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_AGENT_DOC_POLICY'
);
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
PROMPT === Cleaning DDS data grants ===
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.cb_dg_hr_docs'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.cb_dg_fin_docs'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.cb_dg_all_docs'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Cleaning DDS end users and roles ===
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_hr"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_fin"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_all"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP END USER "cb_dds_none"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_hr_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_fin_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_all_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE cb_dds_connect_only_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP ROLE cb_dds_connect_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Cleaning DB user and local objects ===
BEGIN EXECUTE IMMEDIATE 'DROP USER cb_ords CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP VIEW cb_dds_v_search_documents'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP VIEW cb_v_search_documents'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP FUNCTION cb_agent_doc_vpd_filter'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP CONTEXT cb_agent_ctx'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP PACKAGE cb_agent_ctx_pkg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_agent_bearer_key PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_permission_rule PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_permission PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_user_role PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_app_role PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_app_user PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_dds_documents PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE cb_search_documents PURGE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Agent ORDS security local cleanup complete ===
EXIT;

View File

@@ -0,0 +1,115 @@
-- ============================================================
-- 18_agent_ords_security_local_vpd_test.sql
-- Run as CB_ORDS.
--
-- Expected:
-- no context -> 0 rows
-- cb_hr_key -> HR rows only, 3 rows
-- cb_fin_key -> owner_emp_no = E2001 only, 1 row
-- cb_all_key -> all rows, 6 rows
-- contents column -> NULL unless CAN_READ_CONTENTS = Y
-- invalid key -> ORA-20002, then 0 rows
-- direct base table -> ORA-00942
-- ============================================================
WHENEVER SQLERROR CONTINUE
SET ECHO OFF
SET FEEDBACK ON
SET LINESIZE 220
SET PAGESIZE 100
COLUMN ctx_user_id FORMAT A12
COLUMN ctx_emp_no FORMAT A12
COLUMN ctx_dept_code FORMAT A12
COLUMN ctx_can_read_contents FORMAT A22
COLUMN title FORMAT A28
COLUMN owner_emp_no FORMAT A12
COLUMN dept_code FORMAT A10
COLUMN contents FORMAT A35
PROMPT
PROMPT === VPD 1. No Bearer key / context: fail closed ===
BEGIN
admin.cb_agent_ctx_pkg.clear_user;
END;
/
SELECT SYS_CONTEXT('CB_AGENT_CTX','USER_ID') AS ctx_user_id,
SYS_CONTEXT('CB_AGENT_CTX','EMP_NO') AS ctx_emp_no,
SYS_CONTEXT('CB_AGENT_CTX','DEPT_CODE') AS ctx_dept_code,
SYS_CONTEXT('CB_AGENT_CTX','CAN_READ_CONTENTS') AS ctx_can_read_contents
FROM dual;
SELECT COUNT(*) AS rows_visible
FROM admin.cb_v_search_documents;
PROMPT
PROMPT === VPD 2. Authorization: Bearer cb_hr_key -> HR department rows ===
BEGIN
admin.cb_agent_ctx_pkg.set_user_by_bearer('cb_hr_key');
END;
/
SELECT SYS_CONTEXT('CB_AGENT_CTX','USER_ID') AS ctx_user_id,
SYS_CONTEXT('CB_AGENT_CTX','EMP_NO') AS ctx_emp_no,
SYS_CONTEXT('CB_AGENT_CTX','DEPT_CODE') AS ctx_dept_code,
SYS_CONTEXT('CB_AGENT_CTX','CAN_READ_CONTENTS') AS ctx_can_read_contents
FROM dual;
SELECT doc_id, title, owner_emp_no, dept_code, contents
FROM admin.cb_v_search_documents
ORDER BY doc_id;
SELECT COUNT(*) AS rows_visible
FROM admin.cb_v_search_documents;
PROMPT
PROMPT === VPD 3. Authorization: Bearer cb_fin_key -> self row only ===
BEGIN
admin.cb_agent_ctx_pkg.set_user_by_bearer('cb_fin_key');
END;
/
SELECT SYS_CONTEXT('CB_AGENT_CTX','USER_ID') AS ctx_user_id,
SYS_CONTEXT('CB_AGENT_CTX','EMP_NO') AS ctx_emp_no,
SYS_CONTEXT('CB_AGENT_CTX','DEPT_CODE') AS ctx_dept_code,
SYS_CONTEXT('CB_AGENT_CTX','CAN_READ_CONTENTS') AS ctx_can_read_contents
FROM dual;
SELECT doc_id, title, owner_emp_no, dept_code, contents
FROM admin.cb_v_search_documents
ORDER BY doc_id;
SELECT COUNT(*) AS rows_visible
FROM admin.cb_v_search_documents;
SELECT doc_id, title, owner_emp_no, dept_code, contents
FROM admin.cb_v_search_documents
ORDER BY doc_id
FETCH FIRST 3 ROWS ONLY;
PROMPT
PROMPT === VPD 4. Authorization: Bearer cb_all_key -> all rows ===
BEGIN
admin.cb_agent_ctx_pkg.set_user_by_bearer('cb_all_key');
END;
/
SELECT COUNT(*) AS rows_visible
FROM admin.cb_v_search_documents;
PROMPT
PROMPT === VPD 5. Invalid Bearer key -> ORA-20002 and context cleared ===
BEGIN
admin.cb_agent_ctx_pkg.set_user_by_bearer('wrong_key');
END;
/
SELECT COUNT(*) AS rows_visible_after_invalid_key
FROM admin.cb_v_search_documents;
PROMPT
PROMPT === VPD 6. Bypass attempts: direct base/permission tables are not granted ===
SELECT COUNT(*) FROM admin.cb_search_documents;
SELECT COUNT(*) FROM admin.cb_app_user;
SELECT COUNT(*) FROM admin.cb_agent_bearer_key;
EXIT;

View File

@@ -0,0 +1,89 @@
-- ============================================================
-- 19_agent_ords_security_local_dds_setup.sql
-- Local-only executable DDS example for Agent ORDS security.
--
-- Scenario:
-- * DDS uses END USER -> DATA ROLE -> DATA GRANT.
-- * No permission table lookup is used.
-- * Each DATA GRANT declares the protected object, row filter,
-- and column restriction.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Creating DDS-only local table and view ===
CREATE TABLE cb_dds_documents (
doc_id NUMBER PRIMARY KEY,
title VARCHAR2(100) NOT NULL,
owner_emp_no VARCHAR2(20) NOT NULL,
dept_code VARCHAR2(20) NOT NULL,
contents VARCHAR2(4000),
created_at DATE DEFAULT SYSDATE NOT NULL
);
INSERT INTO cb_dds_documents VALUES (1, 'HR payroll guide', 'E1001', 'HR', 'Payroll policy and HR guide', SYSDATE);
INSERT INTO cb_dds_documents VALUES (2, 'HR recruiting plan', 'E1002', 'HR', 'Recruiting plan for HR team', SYSDATE);
INSERT INTO cb_dds_documents VALUES (3, 'Finance close checklist','E2001', 'FIN', 'Monthly close checklist', SYSDATE);
INSERT INTO cb_dds_documents VALUES (4, 'Finance audit memo', 'E2002', 'FIN', 'Audit memo for finance team', SYSDATE);
INSERT INTO cb_dds_documents VALUES (5, 'Sales forecast', 'E3001', 'SALES', 'Quarterly sales forecast', SYSDATE);
INSERT INTO cb_dds_documents VALUES (6, 'HR benefits notice', 'E1003', 'HR', 'Benefits notice for employees', SYSDATE);
CREATE OR REPLACE VIEW cb_dds_v_search_documents AS
SELECT doc_id,
title,
owner_emp_no,
dept_code,
contents,
created_at
FROM cb_dds_documents;
COMMIT;
PROMPT === 2. Creating DDS end users ===
CREATE END USER "cb_dds_hr" IDENTIFIED BY "CbDds#Hr2026Local1";
CREATE END USER "cb_dds_fin" IDENTIFIED BY "CbDds#Fin2026Local1";
CREATE END USER "cb_dds_all" IDENTIFIED BY "CbDds#All2026Local1";
CREATE END USER "cb_dds_none" IDENTIFIED BY "CbDds#None2026Local1";
PROMPT === 3. Creating DATA ROLEs and connection carrier role ===
CREATE ROLE cb_dds_connect_role;
GRANT CREATE SESSION TO cb_dds_connect_role;
CREATE DATA ROLE cb_dds_hr_role;
CREATE DATA ROLE cb_dds_fin_role;
CREATE DATA ROLE cb_dds_all_role;
CREATE DATA ROLE cb_dds_connect_only_role;
GRANT cb_dds_connect_role TO cb_dds_hr_role;
GRANT cb_dds_connect_role TO cb_dds_fin_role;
GRANT cb_dds_connect_role TO cb_dds_all_role;
GRANT cb_dds_connect_role TO cb_dds_connect_only_role;
PROMPT === 4. Mapping END USERs to DATA ROLEs ===
GRANT DATA ROLE cb_dds_hr_role TO "cb_dds_hr";
GRANT DATA ROLE cb_dds_fin_role TO "cb_dds_fin";
GRANT DATA ROLE cb_dds_all_role TO "cb_dds_all";
GRANT DATA ROLE cb_dds_connect_only_role TO "cb_dds_none";
PROMPT === 5. Creating DATA GRANTs ===
CREATE DATA GRANT admin.cb_dg_hr_docs
AS SELECT (ALL COLUMNS EXCEPT contents)
ON admin.cb_dds_v_search_documents
WHERE dept_code = 'HR'
TO cb_dds_hr_role;
CREATE DATA GRANT admin.cb_dg_fin_docs
AS SELECT (ALL COLUMNS EXCEPT contents)
ON admin.cb_dds_v_search_documents
WHERE dept_code = 'FIN'
TO cb_dds_fin_role;
CREATE DATA GRANT admin.cb_dg_all_docs
AS SELECT
ON admin.cb_dds_v_search_documents
TO cb_dds_all_role;
PROMPT === DDS local setup complete ===
EXIT;

View File

@@ -0,0 +1,46 @@
-- ============================================================
-- 20_agent_ords_security_local_dds_test.sql
-- Run as cb_dds_hr / cb_dds_fin / cb_dds_all / cb_dds_none.
--
-- Expected:
-- cb_dds_hr -> 3 rows from admin.cb_dds_v_search_documents
-- cb_dds_fin -> 2 rows from admin.cb_dds_v_search_documents
-- cb_dds_all -> 6 rows from admin.cb_dds_v_search_documents
-- contents -> NULL for hr/fin grants, visible for all grant
-- cb_dds_none -> ORA-00942 on admin.cb_dds_v_search_documents
-- base table -> ORA-00942 for every DDS end user
-- ============================================================
WHENEVER SQLERROR CONTINUE
SET ECHO OFF
SET FEEDBACK ON
SET LINESIZE 220
SET PAGESIZE 100
COLUMN end_user_name FORMAT A16
COLUMN title FORMAT A28
COLUMN owner_emp_no FORMAT A12
COLUMN dept_code FORMAT A10
COLUMN contents FORMAT A35
PROMPT
PROMPT === DDS 1. End user context ===
SELECT ORA_END_USER_CONTEXT.username AS end_user_name
FROM dual;
PROMPT
PROMPT === DDS 2. Protected view row count ===
SELECT COUNT(*) AS rows_visible
FROM admin.cb_dds_v_search_documents;
PROMPT
PROMPT === DDS 3. Protected view sample ===
SELECT doc_id, title, owner_emp_no, dept_code, contents
FROM admin.cb_dds_v_search_documents
ORDER BY doc_id;
PROMPT
PROMPT === DDS 4. Bypass attempts: no DATA GRANT on base/VPD objects ===
SELECT COUNT(*) FROM admin.cb_dds_documents;
SELECT COUNT(*) FROM admin.cb_search_documents;
SELECT COUNT(*) FROM admin.cb_v_search_documents;
EXIT;

View File

@@ -0,0 +1,25 @@
-- ============================================================
-- 21_agent_ords_security_ords_enable_schema.sql
-- Enable the CB_ORDS schema for ORDS module/handler definitions.
--
-- Run as ADMIN after CB_ORDS has been created.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Enabling CB_ORDS as an ORDS schema ===
BEGIN
ORDS.ENABLE_SCHEMA(
p_enabled => TRUE,
p_schema => 'CB_ORDS',
p_url_mapping_type => 'BASE_PATH',
p_url_mapping_pattern => 'cb-ords',
p_auto_rest_auth => FALSE
);
COMMIT;
END;
/
PROMPT === CB_ORDS ORDS schema enabled ===
EXIT;

View File

@@ -0,0 +1,44 @@
-- ============================================================
-- 23_agent_ords_security_ords_handler_test.sql
-- Run as CB_ORDS.
--
-- This directly executes the same package that the ORDS handlers
-- call. It proves the database-side behavior without requiring an
-- external ORDS URL.
-- ============================================================
WHENEVER SQLERROR CONTINUE
SET ECHO OFF
SET FEEDBACK ON
SET LONG 100000
SET LONGCHUNKSIZE 100000
SET LINESIZE 220
SET PAGESIZE 100
SET SERVEROUTPUT ON
COLUMN response FORMAT A180 WORD_WRAPPED
PROMPT
PROMPT === ORDS handler probe 1. VPD path with mandatory Bearer header ===
SELECT cb_ords_handler_pkg.vpd_search_json('Bearer cb_hr_key') AS response
FROM dual;
PROMPT
PROMPT === ORDS handler probe 2. VPD path without Bearer header ===
BEGIN
DBMS_OUTPUT.PUT_LINE(cb_ords_handler_pkg.vpd_search_json(NULL));
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
/
PROMPT
PROMPT === ORDS handler probe 3. DDS path with Bearer header only ===
SELECT cb_ords_handler_pkg.dds_bearer_probe_json('Bearer cb_hr_key') AS response
FROM dual;
PROMPT
PROMPT === ORDS handler probe 4. DDS path with all-access Bearer header only ===
SELECT cb_ords_handler_pkg.dds_bearer_probe_json('Bearer cb_all_key') AS response
FROM dual;
EXIT;

View File

@@ -0,0 +1,99 @@
-- ============================================================
-- 24_agent_ords_security_inventory.sql
-- Central security inventory for Agent ORDS security.
--
-- Run as ADMIN after VPD, DDS, and ORDS handler setup.
-- ============================================================
WHENEVER SQLERROR CONTINUE
SET ECHO OFF
SET FEEDBACK ON
SET LINESIZE 260
SET PAGESIZE 200
COLUMN object_owner FORMAT A12
COLUMN object_name FORMAT A32
COLUMN policy_name FORMAT A26
COLUMN pf_owner FORMAT A12
COLUMN function FORMAT A30
COLUMN sel FORMAT A5
COLUMN ins FORMAT A5
COLUMN upd FORMAT A5
COLUMN del FORMAT A5
COLUMN enable FORMAT A8
COLUMN expression FORMAT A70 WORD_WRAPPED
COLUMN grant_name FORMAT A24
COLUMN grantee FORMAT A28
COLUMN privilege FORMAT A10
COLUMN allowed_columns FORMAT A76 WORD_WRAPPED
COLUMN excluded_columns FORMAT A22
COLUMN predicate FORMAT A28
COLUMN end_user_name FORMAT A26
COLUMN data_role FORMAT A28
PROMPT
PROMPT === Inventory 1. VPD policies attached to protected objects ===
SELECT object_owner,
object_name,
policy_name,
pf_owner,
function,
sel,
ins,
upd,
del,
enable
FROM dba_policies
WHERE object_owner = 'ADMIN'
AND object_name LIKE 'CB%'
ORDER BY object_name, policy_name;
PROMPT
PROMPT === Inventory 2. Redaction policies attached to protected objects ===
SELECT object_owner,
object_name,
policy_name,
enable,
expression
FROM redaction_policies
WHERE object_owner = 'ADMIN'
AND object_name LIKE 'CB%'
ORDER BY object_name, policy_name;
PROMPT
PROMPT === Inventory 3. DDS grant summary, one row per DATA GRANT ===
SELECT grant_name,
object_name,
grantee,
privilege,
COALESCE(
LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name),
'ALL COLUMNS'
) AS allowed_columns,
COALESCE(MAX(granted_with_all_columns_except), '-') AS excluded_columns,
predicate
FROM dba_data_grants
WHERE owner = 'ADMIN'
AND object_name LIKE 'CB%'
GROUP BY grant_name, object_name, grantee, privilege, predicate
ORDER BY object_name, grant_name;
PROMPT
PROMPT === Inventory 4. DDS end user -> data role -> data grant matrix ===
SELECT rg.grantee AS end_user_name,
rg.data_role,
dg.grant_name,
dg.object_name,
dg.predicate,
COALESCE(MAX(dg.granted_with_all_columns_except), '-') AS excluded_columns
FROM dba_data_role_grants rg
LEFT JOIN dba_data_grants dg
ON dg.grantee = rg.data_role
WHERE rg.grantee LIKE 'cb\_dds\_%' ESCAPE '\'
GROUP BY rg.grantee,
rg.data_role,
dg.grant_name,
dg.object_name,
dg.predicate
ORDER BY rg.grantee, dg.object_name, dg.grant_name;
EXIT;

View File

@@ -0,0 +1,24 @@
-- DDS row/column verification fixtures. Run as ADMIN after 31 and 34.
CREATE TABLE admin.cb_dds_security_fixture (
fixture_id NUMBER PRIMARY KEY,
dept_code VARCHAR2(30) NOT NULL,
owner_emp_no VARCHAR2(30) NOT NULL,
tech_tag VARCHAR2(100) NOT NULL,
title VARCHAR2(200) NOT NULL,
contents VARCHAR2(2000) NOT NULL,
public_summary VARCHAR2(500) NOT NULL
);
INSERT INTO admin.cb_dds_security_fixture VALUES (1,'SALES','E2001','SALES','영업 파이프라인','영업 원문','영업 요약');
INSERT INTO admin.cb_dds_security_fixture VALUES (2,'HR','E10234','HR','인사 운영','인사 원문','인사 요약');
INSERT INTO admin.cb_dds_security_fixture VALUES (3,'FIN','E99999','FINANCE','재무 마감','재무 원문','재무 요약');
COMMIT;
CREATE OR REPLACE DATA GRANT admin.dds_demo_token_fixture_grant
AS SELECT (fixture_id, dept_code, owner_emp_no, tech_tag, title, public_summary)
ON admin.cb_dds_security_fixture
WHERE admin.cb_dds_vector_tag_allowed(tech_tag) = 1
TO cb_dds_token_role;
-- Expected: token mapped to SALES sees row 1; HR/FIN rows are denied.
-- CONTENTS is deliberately excluded: this is the column-level deny case.

View File

@@ -0,0 +1,32 @@
-- DDS execution evidence: durable FGA audit for the protected vector view.
-- Run as ADMIN/AUDIT_ADMIN. In unified auditing the records are queried from
-- UNIFIED_AUDIT_TRAIL with AUDIT_TYPE = 'FineGrainedAudit'.
BEGIN
DBMS_FGA.DROP_POLICY(
object_schema => 'ADMIN', object_name => 'CB_DDS_VECTOR_SEARCH_DOCUMENTS',
policy_name => 'DDS_VECTOR_EXECUTION_EVIDENCE');
EXCEPTION WHEN OTHERS THEN
IF SQLCODE != -28102 THEN RAISE; END IF;
END;
/
BEGIN
DBMS_FGA.ADD_POLICY(
object_schema => 'ADMIN',
object_name => 'CB_DDS_VECTOR_SEARCH_DOCUMENTS',
policy_name => 'DDS_VECTOR_EXECUTION_EVIDENCE',
audit_condition => NULL,
audit_column => NULL,
statement_types => 'SELECT',
audit_trail => DBMS_FGA.DB_EXTENDED,
enable => TRUE);
END;
/
-- Evidence lookup (grant AUDIT_VIEWER or expose a definer-rights reader view
-- to the backoffice; never grant this to DDS END USER accounts):
-- SELECT event_timestamp, client_identifier, dbusername, object_name,
-- return_code, sql_text, application_contexts
-- FROM unified_audit_trail
-- WHERE audit_type = 'FineGrainedAudit'
-- AND object_schema = 'ADMIN'
-- AND object_name = 'CB_DDS_VECTOR_SEARCH_DOCUMENTS'
-- ORDER BY event_timestamp DESC;

View File

@@ -432,88 +432,27 @@ BEGIN
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;
/
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 replace_column_policy(
p_object_name IN VARCHAR2,
p_column_name IN VARCHAR2,
p_policy_name IN VARCHAR2,
p_function_name IN VARCHAR2
) IS
PROCEDURE drop_column_policy(p_object_name IN VARCHAR2, p_policy_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
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
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');
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;
/

View File

@@ -65,13 +65,6 @@ BEGIN
require_runtime_object('CB_AGENT_CTX_PKG', 'PACKAGE BODY');
require_runtime_object('CB_AGENT_DOC_VPD_FILTER', 'FUNCTION');
require_runtime_object('CB_AGENT_CAN_READ_COLUMN', 'FUNCTION');
require_runtime_object('CB_KB_CUST_NM_CLS', 'FUNCTION');
require_runtime_object('CB_KB_RRN_CLS', 'FUNCTION');
require_runtime_object('CB_KB_CLAIM_AMT_CLS', 'FUNCTION');
require_runtime_object('CB_KB_PAID_AMT_CLS', 'FUNCTION');
require_runtime_object('CB_KB_EXT_INSURER_CLS', 'FUNCTION');
require_runtime_object('CB_KB_EXT_PRODUCT_GRP_CLS', 'FUNCTION');
require_runtime_object('CB_KB_EXT_PRODUCT_TYPE_CLS', 'FUNCTION');
END;
/
@@ -128,50 +121,28 @@ BEGIN
END;
/
PROMPT === 4. Reapplying column NULL policies ===
PROMPT === 4. Removing deprecated VPD column NULL policies ===
-- Architecture rule: VPD filters rows only. Column masking is handled solely
-- by ASO / DBMS_REDACT through 63_kb_aso_masking_rule_runtime.sql.
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
v_count NUMBER;
PROCEDURE drop_column_policy(p_object_name IN VARCHAR2, p_policy_name IN VARCHAR2) IS
BEGIN
SELECT COUNT(*)
INTO v_count
FROM all_policies
WHERE object_owner = 'POC_2'
AND object_name = p_object_name
AND policy_name = p_policy_name;
IF v_count > 0 THEN
DBMS_RLS.DROP_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => p_policy_name
);
END IF;
DBMS_RLS.ADD_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => p_policy_name,
function_schema => 'ADMIN',
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
DBMS_RLS.DROP_POLICY(
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name
);
EXCEPTION
WHEN OTHERS THEN
-- A missing policy is already the desired state. Do not hide other errors.
IF SQLCODE != -28102 THEN RAISE; END IF;
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');
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;
/

View File

@@ -0,0 +1,100 @@
-- ============================================================
-- 62_kb_aso_masking_backoffice_metadata.sql
--
-- Creates the metadata used by the masking-rule backoffice pages.
-- Run as ADMIN with SQLcl. This script does not create a DBMS_REDACT
-- policy; run 63_kb_aso_masking_rule_runtime.sql after rules are
-- connected to protected columns.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Creating masking-rule metadata tables ===
DECLARE
PROCEDURE ensure_table(p_name IN VARCHAR2, p_ddl IN CLOB) IS
BEGIN
EXECUTE IMMEDIATE p_ddl;
DBMS_OUTPUT.PUT_LINE('created ' || p_name);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -955 THEN
DBMS_OUTPUT.PUT_LINE('exists ' || p_name);
ELSE
RAISE;
END IF;
END;
BEGIN
ensure_table('CB_MASKING_RULE', q'[
CREATE TABLE cb_masking_rule (
rule_id NUMBER PRIMARY KEY,
rule_code VARCHAR2(64) NOT NULL UNIQUE,
rule_name VARCHAR2(100) NOT NULL,
template_code VARCHAR2(30) NOT NULL,
description VARCHAR2(400),
enabled_yn CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL
)]');
ensure_table('CB_COLUMN_MASKING_RULE', q'[
CREATE TABLE cb_column_masking_rule (
column_id NUMBER PRIMARY KEY,
rule_id NUMBER NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
)]');
ensure_table('CB_USER_MASKING_RULE', q'[
CREATE TABLE cb_user_masking_rule (
user_id NUMBER NOT NULL,
column_id NUMBER NOT NULL,
decision VARCHAR2(10) NOT NULL CHECK (decision IN ('MASK','UNMASK')),
active_yn CHAR(1) DEFAULT 'Y' CHECK (active_yn IN ('Y','N')) NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT cb_user_masking_rule_pk PRIMARY KEY (user_id, column_id)
)]');
END;
/
PROMPT === Seeding curated masking templates ===
DECLARE
v_next_rule_id NUMBER;
PROCEDURE ensure_rule(
p_code IN VARCHAR2,
p_name IN VARCHAR2,
p_template IN VARCHAR2,
p_description IN VARCHAR2
) IS
BEGIN
MERGE INTO cb_masking_rule dst
USING (
SELECT p_code rule_code,
p_name rule_name,
p_template template_code,
p_description description
FROM dual
) src
ON (dst.rule_code = src.rule_code)
WHEN NOT MATCHED THEN
INSERT (rule_id, rule_code, rule_name, template_code, description, enabled_yn)
VALUES (v_next_rule_id, src.rule_code, src.rule_name, src.template_code, src.description, 'Y');
v_next_rule_id := v_next_rule_id + 1;
END;
BEGIN
SELECT NVL(MAX(rule_id), 0) + 1 INTO v_next_rule_id FROM cb_masking_rule;
ensure_rule('MASK_NULLIFY', '값 숨김 (NULL)', 'NULLIFY',
'값을 NULL로 반환하는 기본 마스킹 방식');
ensure_rule('MASK_FULL', '전체 마스킹', 'FULL',
'문자형은 공백, 숫자형은 0으로 반환하는 전체 마스킹 방식');
ensure_rule('MASK_TEXT_PARTIAL', '문자열 일부 마스킹', 'TEXT_PARTIAL',
'첫 글자만 보이고 나머지는 가리는 문자열 마스킹 방식');
ensure_rule('MASK_RRN_PARTIAL', '주민등록번호 부분 마스킹', 'RRN_PARTIAL',
'앞 6자리만 보이고 나머지는 가리는 식별번호 마스킹 방식');
END;
/
COMMIT;
PROMPT === Metadata ready ===
SELECT rule_id, rule_code, rule_name, template_code, enabled_yn
FROM cb_masking_rule
ORDER BY rule_id;
EXIT

View File

@@ -0,0 +1,679 @@
-- ============================================================
-- 63_kb_aso_masking_rule_runtime.sql
--
-- Synchronizes CB_MASKING_RULE / CB_COLUMN_MASKING_RULE and
-- CB_USER_MASKING_RULE into Oracle Data Redaction policies.
--
-- Important behaviour
-- * One selected masking rule is applied to each protected column.
-- * A direct user rule controls whether that user is masked or receives
-- an original-value exception (UNMASK).
-- * VPD still determines row visibility. Existing VPD NULL policies
-- remain in force for users without column permission.
--
-- Run as ADMIN with SQLcl after 62 to initialize the runtime package, or as
-- a manual recovery/reconciliation action. The backoffice synchronizes
-- DBMS_REDACT on normal configuration changes; this script follows the same
-- source-of-truth rule for existing installations.
-- Requires EXECUTE on DBMS_REDACT and ADMINISTER REDACTION POLICY for POC_2
-- objects.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Validating masking-rule metadata ===
DECLARE
PROCEDURE require_table(p_table_name IN VARCHAR2) IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM user_tables
WHERE table_name = p_table_name;
IF v_count = 0 THEN
RAISE_APPLICATION_ERROR(-20001,
'Missing ' || p_table_name || '. Run 62_kb_aso_masking_backoffice_metadata.sql first.');
END IF;
END;
BEGIN
require_table('CB_MASKING_RULE');
require_table('CB_COLUMN_MASKING_RULE');
require_table('CB_USER_MASKING_RULE');
END;
/
PROMPT === Updating trusted bearer context with column exception flags ===
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_masking_rule_context AS
BEGIN
FOR r IN (SELECT column_id FROM cb_column_masking_rule) LOOP
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'MR_' || TO_CHAR(r.column_id), NULL);
END LOOP;
END;
PROCEDURE set_masking_rule_context(p_user_id IN NUMBER) AS
BEGIN
clear_masking_rule_context;
FOR r IN (
SELECT link.column_id,
CASE WHEN EXISTS (
SELECT 1
FROM cb_user_masking_rule user_rule
WHERE user_rule.user_id = p_user_id
AND user_rule.column_id = link.column_id
AND user_rule.active_yn = 'Y'
AND user_rule.decision = 'UNMASK'
) THEN 'Y' ELSE 'N' END AS can_unmask
FROM cb_column_masking_rule link
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
WHERE rule.enabled_yn = 'Y'
) LOOP
DBMS_SESSION.SET_CONTEXT('CB_AGENT_CTX', 'MR_' || TO_CHAR(r.column_id), r.can_unmask);
END LOOP;
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;
clear_masking_rule_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;
clear_masking_rule_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);
set_masking_rule_context(p_user_id);
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';
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;
set_masking_rule_context(v_user_id);
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
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
PROMPT === Preserving the all-access admin raw-display contract ===
MERGE INTO cb_user_masking_rule dst
USING (
SELECT user_row.user_id, column_rule.column_id
FROM cb_app_user user_row
CROSS JOIN cb_column_masking_rule column_rule
JOIN cb_masking_rule rule ON rule.rule_id = column_rule.rule_id
WHERE user_row.stakeholder_user_id = 'KB_VPD_ADMIN'
AND user_row.active = 'Y'
AND rule.enabled_yn = 'Y'
) src
ON (dst.user_id = src.user_id AND dst.column_id = src.column_id)
WHEN NOT MATCHED THEN
INSERT (user_id, column_id, decision, active_yn, updated_at)
VALUES (src.user_id, src.column_id, 'UNMASK', 'Y', SYSTIMESTAMP);
COMMIT;
PROMPT === Removing stale columns and disabling unconfigured KB policies ===
-- A policy definition is deliberately retained when no column is configured.
-- DISABLE_POLICY makes the effective state match the empty backoffice setting
-- and lets a later column assignment re-enable the same named policy safely.
DECLARE
PROCEDURE reconcile_existing_policy(
p_object_name IN VARCHAR2,
p_policy_name IN VARCHAR2
) IS
v_policy_count NUMBER;
v_configured_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_policy_count
FROM redaction_policies
WHERE object_owner = 'POC_2' AND object_name = p_object_name AND policy_name = p_policy_name;
IF v_policy_count = 0 THEN
RETURN;
END IF;
SELECT COUNT(*) INTO v_configured_count
FROM cb_column_masking_rule link
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
JOIN cb_protected_column column_row ON column_row.column_id = link.column_id
JOIN cb_protected_object object_row ON object_row.object_id = column_row.object_id
WHERE object_row.owner = 'POC_2'
AND object_row.object_name = p_object_name
AND rule.enabled_yn = 'Y';
IF v_configured_count = 0 THEN
DBMS_REDACT.DISABLE_POLICY('POC_2', p_object_name, p_policy_name);
RETURN;
END IF;
FOR stale_column IN (
SELECT policy_column.column_name
FROM redaction_columns policy_column
WHERE policy_column.object_owner = 'POC_2'
AND policy_column.object_name = p_object_name
AND NOT EXISTS (
SELECT 1
FROM cb_column_masking_rule link
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
JOIN cb_protected_column column_row ON column_row.column_id = link.column_id
JOIN cb_protected_object object_row ON object_row.object_id = column_row.object_id
WHERE object_row.owner = 'POC_2'
AND object_row.object_name = p_object_name
AND column_row.column_name = policy_column.column_name
AND rule.enabled_yn = 'Y'
)
) LOOP
DBMS_REDACT.ALTER_POLICY(
object_schema => 'POC_2', object_name => p_object_name, policy_name => p_policy_name,
action => DBMS_REDACT.DROP_COLUMN, column_name => stale_column.column_name
);
END LOOP;
END;
BEGIN
reconcile_existing_policy('KB_CUSTOMERS', 'KB_CUSTOMER_PII_REDACT');
reconcile_existing_policy('KB_CLAIMS', 'KB_CLAIM_AMOUNT_REDACT');
reconcile_existing_policy('KB_CONTRACTS', 'KB_CONTRACT_PREMIUM_REDACT');
reconcile_existing_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_HOLDING_REDACT');
END;
/
PROMPT === Integrating configured columns into the existing KB Data Redaction policies ===
DECLARE
v_policy_name VARCHAR2(128);
v_expression_name VARCHAR2(128);
v_expression VARCHAR2(4000);
PROCEDURE require_existing_policy(
p_object_name IN VARCHAR2,
p_policy_name IN VARCHAR2
) IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM redaction_policies
WHERE object_owner = 'POC_2'
AND object_name = p_object_name
AND policy_name = p_policy_name;
IF v_count = 0 THEN
RAISE_APPLICATION_ERROR(-20012,
'Existing Data Redaction policy not found: POC_2.' || p_object_name || '.' || p_policy_name);
END IF;
END;
PROCEDURE upsert_expression(
p_expression_name IN VARCHAR2,
p_expression IN VARCHAR2
) IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM redaction_expressions
WHERE policy_expression_name = p_expression_name;
IF v_count = 0 THEN
DBMS_REDACT.CREATE_POLICY_EXPRESSION(
policy_expression_name => p_expression_name,
expression => p_expression,
policy_expression_description => 'Mask unless trusted bearer context grants an original-value exception'
);
ELSE
DBMS_REDACT.UPDATE_POLICY_EXPRESSION(
policy_expression_name => p_expression_name,
expression => p_expression,
policy_expression_description => 'Mask unless trusted bearer context grants an original-value exception'
);
END IF;
END;
PROCEDURE modify_column(
p_object_name IN VARCHAR2,
p_policy_name IN VARCHAR2,
p_column_name IN VARCHAR2,
p_template_code IN VARCHAR2
) IS
PROCEDURE apply(
p_function_type IN BINARY_INTEGER,
p_pattern IN VARCHAR2 DEFAULT NULL,
p_replace IN VARCHAR2 DEFAULT NULL
) IS
BEGIN
DBMS_REDACT.ALTER_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => p_policy_name,
action => DBMS_REDACT.MODIFY_COLUMN,
column_name => p_column_name,
function_type => p_function_type,
regexp_pattern => p_pattern,
regexp_replace_string => p_replace
);
END;
BEGIN
CASE p_template_code
WHEN 'NULLIFY' THEN
apply(DBMS_REDACT.NULLIFY);
WHEN 'FULL' THEN
apply(DBMS_REDACT.FULL);
WHEN 'TEXT_PARTIAL' THEN
apply(DBMS_REDACT.REGEXP, '(^.).*$', '\1***');
WHEN 'RRN_PARTIAL' THEN
apply(DBMS_REDACT.REGEXP, '(^[0-9]{6})-?[0-9]{7}$', '\1-*******');
ELSE
RAISE_APPLICATION_ERROR(-20013, 'Unsupported masking template: ' || p_template_code);
END CASE;
END;
BEGIN
FOR configured_column IN (
SELECT protected_object.object_name,
protected_column.column_id,
protected_column.column_name,
masking_rule.template_code
FROM cb_column_masking_rule link
JOIN cb_masking_rule masking_rule ON masking_rule.rule_id = link.rule_id
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
WHERE protected_object.owner = 'POC_2'
AND masking_rule.enabled_yn = 'Y'
ORDER BY protected_object.object_name, protected_column.column_id
) LOOP
v_policy_name := CASE configured_column.object_name
WHEN 'KB_CUSTOMERS' THEN 'KB_CUSTOMER_PII_REDACT'
WHEN 'KB_CLAIMS' THEN 'KB_CLAIM_AMOUNT_REDACT'
WHEN 'KB_CONTRACTS' THEN 'KB_CONTRACT_PREMIUM_REDACT'
WHEN 'KB_EXTERNAL_HOLDINGS' THEN 'KB_EXT_HOLDING_REDACT'
ELSE NULL
END;
IF v_policy_name IS NULL THEN
RAISE_APPLICATION_ERROR(-20014,
'No existing KB Data Redaction policy mapping for ' || configured_column.object_name);
END IF;
require_existing_policy(configured_column.object_name, v_policy_name);
DBMS_REDACT.ENABLE_POLICY('POC_2', configured_column.object_name, v_policy_name);
modify_column(
configured_column.object_name,
v_policy_name,
configured_column.column_name,
configured_column.template_code
);
v_expression_name := 'CBMR_' || TO_CHAR(configured_column.column_id);
v_expression := 'SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(configured_column.column_id)
|| ''') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(configured_column.column_id) || ''') <> ''Y''';
upsert_expression(v_expression_name, v_expression);
DBMS_REDACT.APPLY_POLICY_EXPR_TO_COL(
object_schema => 'POC_2',
object_name => configured_column.object_name,
column_name => configured_column.column_name,
policy_expression_name => v_expression_name
);
END LOOP;
END;
/
COMMIT;
PROMPT === Verification ===
SELECT object_name, policy_name, expression
FROM redaction_policies
WHERE object_owner = 'POC_2'
AND object_name IN ('KB_CUSTOMERS', 'KB_CLAIMS', 'KB_CONTRACTS', 'KB_EXTERNAL_HOLDINGS')
ORDER BY object_name, policy_name;
SELECT object_name, column_name, function_type
FROM redaction_columns
WHERE object_owner = 'POC_2'
AND object_name IN ('KB_CUSTOMERS', 'KB_CLAIMS', 'KB_CONTRACTS', 'KB_EXTERNAL_HOLDINGS')
ORDER BY object_name, column_name;
SELECT policy_expression_name, object_name, column_name, expression
FROM redaction_expressions
WHERE policy_expression_name LIKE 'CBMR_%'
ORDER BY policy_expression_name;
SELECT user_row.user_name,
protected_object.object_name,
protected_column.column_name,
user_rule.decision
FROM cb_user_masking_rule user_rule
JOIN cb_app_user user_row ON user_row.user_id = user_rule.user_id
JOIN cb_protected_column protected_column ON protected_column.column_id = user_rule.column_id
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
ORDER BY user_row.user_name, protected_object.object_name, protected_column.column_name;
EXIT
-- Legacy replacement implementation retained below for migration reference.
-- SQLcl exits above, so it never executes.
PROMPT === Rebuilding managed Oracle Data Redaction policies ===
DECLARE
v_policy_name VARCHAR2(128);
v_expression_name VARCHAR2(128);
v_first BOOLEAN;
PROCEDURE apply_column(
p_first IN BOOLEAN,
p_object_name IN VARCHAR2,
p_policy_name IN VARCHAR2,
p_column_name IN VARCHAR2,
p_template_code IN VARCHAR2
) IS
PROCEDURE first_policy(
p_function_type IN BINARY_INTEGER,
p_pattern IN VARCHAR2 DEFAULT NULL,
p_replace IN VARCHAR2 DEFAULT NULL
) IS
BEGIN
DBMS_REDACT.ADD_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => p_policy_name,
policy_description => 'Managed by CB masking rule backoffice',
column_name => p_column_name,
function_type => p_function_type,
expression => '1=1',
regexp_pattern => p_pattern,
regexp_replace_string => p_replace,
enable => TRUE
);
END;
PROCEDURE additional_column(
p_function_type IN BINARY_INTEGER,
p_pattern IN VARCHAR2 DEFAULT NULL,
p_replace IN VARCHAR2 DEFAULT NULL
) IS
BEGIN
DBMS_REDACT.ALTER_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => p_policy_name,
action => DBMS_REDACT.ADD_COLUMN,
column_name => p_column_name,
function_type => p_function_type,
regexp_pattern => p_pattern,
regexp_replace_string => p_replace
);
END;
PROCEDURE invoke(
p_function_type IN BINARY_INTEGER,
p_pattern IN VARCHAR2 DEFAULT NULL,
p_replace IN VARCHAR2 DEFAULT NULL
) IS
BEGIN
IF p_first THEN
first_policy(p_function_type, p_pattern, p_replace);
ELSE
additional_column(p_function_type, p_pattern, p_replace);
END IF;
END;
BEGIN
CASE p_template_code
WHEN 'NULLIFY' THEN
invoke(DBMS_REDACT.NULLIFY);
WHEN 'FULL' THEN
invoke(DBMS_REDACT.FULL);
WHEN 'TEXT_PARTIAL' THEN
invoke(DBMS_REDACT.REGEXP, '(^.).*$', '\1***');
WHEN 'RRN_PARTIAL' THEN
invoke(DBMS_REDACT.REGEXP, '(^[0-9]{6})-?[0-9]{7}$', '\1-*******');
ELSE
RAISE_APPLICATION_ERROR(-20011, 'Unsupported template: ' || p_template_code);
END CASE;
END;
PROCEDURE drop_managed_policy(p_object_name IN VARCHAR2, p_policy_name IN VARCHAR2) IS
BEGIN
DBMS_REDACT.DROP_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => p_policy_name
);
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
PROCEDURE drop_expression(p_expression_name IN VARCHAR2) IS
BEGIN
DBMS_REDACT.DROP_POLICY_EXPRESSION(p_expression_name);
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
BEGIN
-- These three policies are legacy test policies found on the KB objects.
-- Data Redaction permits only one policy per object, so replace them with
-- the managed policy rather than leaving their client-identifier tests live.
drop_managed_policy('KB_CUSTOMERS', 'KB_CUSTOMER_PII_REDACT');
drop_managed_policy('KB_CLAIMS', 'KB_CLAIM_AMOUNT_REDACT');
drop_managed_policy('KB_EXTERNAL_HOLDINGS', 'KB_EXT_HOLDING_REDACT');
FOR object_row IN (
SELECT DISTINCT po.object_name
FROM cb_column_masking_rule link
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
JOIN cb_protected_column pc ON pc.column_id = link.column_id
JOIN cb_protected_object po ON po.object_id = pc.object_id
WHERE rule.enabled_yn = 'Y'
AND po.owner = 'POC_2'
) LOOP
v_policy_name := 'CB_ASO_' || object_row.object_name || '_MASK';
drop_managed_policy(object_row.object_name, v_policy_name);
END LOOP;
FOR column_row IN (
SELECT link.column_id
FROM cb_column_masking_rule link
) LOOP
drop_expression('CBMR_' || TO_CHAR(column_row.column_id));
END LOOP;
FOR object_row IN (
SELECT DISTINCT po.object_name
FROM cb_column_masking_rule link
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
JOIN cb_protected_column pc ON pc.column_id = link.column_id
JOIN cb_protected_object po ON po.object_id = pc.object_id
WHERE rule.enabled_yn = 'Y'
AND po.owner = 'POC_2'
ORDER BY po.object_name
) LOOP
v_policy_name := 'CB_ASO_' || object_row.object_name || '_MASK';
v_first := TRUE;
FOR column_row IN (
SELECT pc.column_id,
pc.column_name,
rule.template_code,
rule.rule_name
FROM cb_column_masking_rule link
JOIN cb_masking_rule rule ON rule.rule_id = link.rule_id
JOIN cb_protected_column pc ON pc.column_id = link.column_id
JOIN cb_protected_object po ON po.object_id = pc.object_id
WHERE po.owner = 'POC_2'
AND po.object_name = object_row.object_name
AND rule.enabled_yn = 'Y'
ORDER BY pc.column_id
) LOOP
apply_column(v_first, object_row.object_name, v_policy_name,
column_row.column_name, column_row.template_code);
v_expression_name := 'CBMR_' || TO_CHAR(column_row.column_id);
DBMS_REDACT.CREATE_POLICY_EXPRESSION(
policy_expression_name => v_expression_name,
expression => 'SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(column_row.column_id)
|| ''') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''MR_' || TO_CHAR(column_row.column_id) || ''') <> ''Y''',
policy_expression_description => 'Mask unless trusted bearer context grants an original-value exception'
);
DBMS_REDACT.APPLY_POLICY_EXPR_TO_COL(
object_schema => 'POC_2',
object_name => object_row.object_name,
column_name => column_row.column_name,
policy_expression_name => v_expression_name
);
v_first := FALSE;
END LOOP;
END LOOP;
END;
/
COMMIT;
PROMPT === Verification ===
SELECT object_name, policy_name, expression
FROM redaction_policies
WHERE object_owner = 'POC_2'
AND policy_name LIKE 'CB_ASO_%'
ORDER BY object_name, policy_name;
SELECT object_name, column_name, function_type
FROM redaction_columns
WHERE object_owner = 'POC_2'
AND object_name IN ('KB_CUSTOMERS', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
ORDER BY object_name, column_name;
SELECT user_row.user_name,
protected_object.object_name,
protected_column.column_name,
user_rule.decision
FROM cb_user_masking_rule user_rule
JOIN cb_app_user user_row ON user_row.user_id = user_rule.user_id
JOIN cb_protected_column protected_column ON protected_column.column_id = user_rule.column_id
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
ORDER BY user_row.user_name, protected_object.object_name, protected_column.column_name;
EXIT

View File

@@ -0,0 +1,54 @@
-- ============================================================
-- 64_kb_aso_masking_default_column_rules.sql
--
-- Initial blacklist of KB columns subject to ASO masking.
-- Run as ADMIN after 62. Execute 63 afterwards to apply the
-- configured rules as DBMS_REDACT policies.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Assigning the KB masking target blacklist ===
MERGE INTO cb_column_masking_rule dst
USING (
SELECT protected_column.column_id,
masking_rule.rule_id
FROM cb_protected_column protected_column
JOIN cb_protected_object protected_object
ON protected_object.object_id = protected_column.object_id
JOIN cb_masking_rule masking_rule
ON masking_rule.rule_code = CASE
WHEN protected_object.object_name = 'KB_CUSTOMERS'
AND protected_column.column_name = 'RRN_MASKED'
THEN 'MASK_RRN_PARTIAL'
ELSE 'MASK_NULLIFY'
END
WHERE (protected_object.object_name = 'KB_CUSTOMERS'
AND protected_column.column_name IN ('RRN_MASKED'))
OR (protected_object.object_name = 'KB_CLAIMS'
AND protected_column.column_name IN ('CLAIM_AMT', 'PAID_AMT'))
OR (protected_object.object_name = 'KB_EXTERNAL_HOLDINGS'
AND protected_column.column_name IN ('EXT_INSURER', 'EXT_PRODUCT_GRP', 'EXT_PRODUCT_TYPE'))
) src
ON (dst.column_id = src.column_id)
WHEN MATCHED THEN
UPDATE SET dst.rule_id = src.rule_id, dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT (column_id, rule_id, updated_at)
VALUES (src.column_id, src.rule_id, SYSTIMESTAMP);
COMMIT;
PROMPT === Configured masking target blacklist ===
SELECT protected_object.owner,
protected_object.object_name,
protected_column.column_name,
masking_rule.rule_code,
masking_rule.rule_name,
masking_rule.template_code
FROM cb_column_masking_rule link
JOIN cb_protected_column protected_column ON protected_column.column_id = link.column_id
JOIN cb_protected_object protected_object ON protected_object.object_id = protected_column.object_id
JOIN cb_masking_rule masking_rule ON masking_rule.rule_id = link.rule_id
ORDER BY protected_object.object_name, protected_column.column_name;
EXIT

View File

@@ -0,0 +1,221 @@
-- ============================================================
-- 65_kb_select_ai_vpd_query_api.sql
--
-- Natural-language KB query API backing package.
-- Run as POC_2. The caller (CB_ORDS) establishes CB_AGENT_CTX before
-- invoking this package, so generated SQL is still subject to POC_2 VPD.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Annotating KB schema for Select AI ===
COMMENT ON TABLE poc_2.kb_customers IS '고객원장. CUST_ID는 C로 시작하는 고객 식별자 예: C1001006.';
COMMENT ON COLUMN poc_2.kb_customers.cust_id IS '고객 식별자. C로 시작하는 값 예: C1001006.';
COMMENT ON TABLE poc_2.kb_products IS '상품원장 및 약관상품 마스터. PRODUCT_CD는 숫자형 상품/약관 코드 예: 41048.';
COMMENT ON COLUMN poc_2.kb_products.product_cd IS '상품 또는 약관상품 코드. 숫자형 코드 예: 41048. KB_CONTRACTS.PRODUCT_CD와 조인한다.';
COMMENT ON COLUMN poc_2.kb_products.clause_product_nm IS '약관상품명 또는 상품명. 고객 설명용 상품 비교에 사용한다.';
COMMENT ON COLUMN poc_2.kb_products.insurance_type IS '보험 대분류. 예: 자동차, 장기, 일반.';
COMMENT ON COLUMN poc_2.kb_products.product_type IS '상품 유형 또는 세부 상품군.';
COMMENT ON COLUMN poc_2.kb_products.clause_version IS '약관 버전.';
COMMENT ON COLUMN poc_2.kb_products.sale_status IS '판매 상태.';
COMMENT ON COLUMN poc_2.kb_products.active_yn IS '상품 활성 여부.';
COMMENT ON TABLE poc_2.kb_contracts IS '계약원장. CONTRACT_NO는 CT로 시작하는 계약번호이고, PRODUCT_CD는 숫자형 상품/약관 코드다.';
COMMENT ON COLUMN poc_2.kb_contracts.contract_no IS '계약번호. CT로 시작하는 값 예: CT2699001. 41048 같은 순수 숫자 코드는 계약번호가 아니라 PRODUCT_CD일 가능성이 높다.';
COMMENT ON COLUMN poc_2.kb_contracts.cust_id IS '계약 고객 식별자. KB_CUSTOMERS.CUST_ID와 조인한다.';
COMMENT ON COLUMN poc_2.kb_contracts.product_cd IS '계약의 상품/약관 코드. 숫자형 코드 예: 41048. KB_PRODUCTS.PRODUCT_CD와 조인한다.';
COMMENT ON COLUMN poc_2.kb_contracts.fc_id IS '담당 설계사 또는 FC 사용자 ID. 예: FC00789.';
COMMENT ON COLUMN poc_2.kb_contracts.fc_channel IS '담당 채널. 예: 설계사, GA, 다이렉트.';
COMMENT ON COLUMN poc_2.kb_contracts.contract_status IS '계약 상태. 예: 정상, 실효.';
COMMENT ON COLUMN poc_2.kb_contracts.premium IS '계약 보험료.';
COMMENT ON COLUMN poc_2.kb_contracts.pay_cycle IS '보험료 납입 주기.';
COMMENT ON TABLE poc_2.kb_coverages IS '담보원장. 계약번호 CONTRACT_NO 기준으로 KB_CONTRACTS와 조인해 보장/특약을 설명한다.';
COMMENT ON COLUMN poc_2.kb_coverages.contract_no IS '담보가 속한 계약번호. KB_CONTRACTS.CONTRACT_NO와 조인한다.';
COMMENT ON COLUMN poc_2.kb_coverages.cust_id IS '담보 고객 식별자.';
COMMENT ON COLUMN poc_2.kb_coverages.coverage_nm IS '담보명 또는 특약명.';
COMMENT ON COLUMN poc_2.kb_coverages.coverage_type IS '담보 유형.';
COMMENT ON COLUMN poc_2.kb_coverages.coverage_div IS '담보 구분.';
COMMENT ON COLUMN poc_2.kb_coverages.insured_amt IS '가입금액 또는 보장금액.';
COMMENT ON COLUMN poc_2.kb_coverages.renew_due_dt IS '갱신 예정일.';
COMMENT ON TABLE poc_2.kb_external_holdings IS '외부보유정보 원장. 타 보험사 보유계약과 약관상품을 고객 CUST_ID 기준으로 비교한다.';
COMMENT ON COLUMN poc_2.kb_external_holdings.cust_id IS '외부보유정보 고객 식별자. KB_CUSTOMERS.CUST_ID와 조인한다.';
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_insurer IS '외부 보험사명. 예: 삼성화재.';
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_product_grp IS '외부 상품군. 예: 자동차.';
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_product_type IS '외부 상품 유형. 예: 개인용.';
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_clause_nm IS '외부 약관상품명.';
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_sale_status IS '외부 상품 판매 상태.';
COMMENT ON COLUMN poc_2.kb_external_holdings.ext_renew_month IS '외부 보유계약 갱신월.';
PROMPT === Creating VPD-aware Select AI query API ===
CREATE OR REPLACE PACKAGE poc_2.kb_select_ai_vpd_query_api AUTHID DEFINER AS
PROCEDURE open_query(
p_prompt IN CLOB,
p_limit IN PLS_INTEGER,
p_rows OUT SYS_REFCURSOR,
p_generated_sql OUT CLOB
);
END kb_select_ai_vpd_query_api;
/
CREATE OR REPLACE PACKAGE BODY poc_2.kb_select_ai_vpd_query_api AS
c_profile_name CONSTANT VARCHAR2(128) := 'KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1';
c_max_prompt_length CONSTANT PLS_INTEGER := 4000;
c_max_rows CONSTANT PLS_INTEGER := 100;
FUNCTION normalized_sql(p_generated_sql IN CLOB) RETURN VARCHAR2 IS
v_sql VARCHAR2(32767);
v_comment_end PLS_INTEGER;
BEGIN
IF p_generated_sql IS NULL OR DBMS_LOB.GETLENGTH(p_generated_sql) = 0 THEN
RAISE_APPLICATION_ERROR(-20811, 'Select AI did not generate SQL.');
END IF;
IF DBMS_LOB.GETLENGTH(p_generated_sql) > 32767 THEN
RAISE_APPLICATION_ERROR(-20812, 'Generated SQL is too long.');
END IF;
v_sql := TRIM(DBMS_LOB.SUBSTR(p_generated_sql, 32767, 1));
-- Select AI sometimes wraps an otherwise valid SQL statement in markdown
-- fences or adds a short leading explanation comment. Strip only those
-- wrapper artifacts before the strict safety validator runs. Comments that
-- remain inside the executable statement are still rejected below.
LOOP
v_sql := TRIM(v_sql);
v_sql := REGEXP_REPLACE(v_sql, '^[[:space:]]*```[[:alpha:]]*[[:space:]]*', '');
v_sql := REGEXP_REPLACE(v_sql, '[[:space:]]*```[[:space:]]*$', '');
v_sql := TRIM(v_sql);
IF SUBSTR(v_sql, 1, 2) = '--' THEN
v_comment_end := INSTR(v_sql, CHR(10));
IF v_comment_end = 0 THEN
RAISE_APPLICATION_ERROR(-20813, 'Select AI returned only a comment, not SQL.');
END IF;
v_sql := SUBSTR(v_sql, v_comment_end + 1);
ELSIF SUBSTR(v_sql, 1, 2) = '/*' THEN
v_comment_end := INSTR(v_sql, '*/');
IF v_comment_end = 0 THEN
RAISE_APPLICATION_ERROR(-20814, 'Unclosed generated SQL comment is not allowed.');
END IF;
v_sql := SUBSTR(v_sql, v_comment_end + 2);
ELSE
EXIT;
END IF;
END LOOP;
v_sql := RTRIM(TRIM(v_sql), ';');
RETURN TRIM(v_sql);
END normalized_sql;
PROCEDURE assert_safe_kb_select(p_sql IN VARCHAR2) IS
-- Select AI normally double-quotes Oracle identifiers. Validate a quote-free
-- copy so POC_2.KB_CLAIMS and "POC_2"."KB_CLAIMS" follow the same policy.
v_upper VARCHAR2(32767) := REPLACE(UPPER(p_sql), '"', '');
-- Mask string literals before checking semicolons, keywords, packages, and
-- data dictionary names. A harmless CASE output string should not be treated
-- as executable SQL syntax.
v_lexical VARCHAR2(32767) :=
REGEXP_REPLACE(REPLACE(UPPER(p_sql), '"', ''), '''(''''|[^''])*''', '''X''');
BEGIN
IF NOT REGEXP_LIKE(v_lexical, '^(SELECT|WITH)[[:space:]]') THEN
RAISE_APPLICATION_ERROR(
-20813,
'Only a single SELECT or WITH query is allowed. Generated prefix: '
|| SUBSTR(v_upper, 1, 160)
);
END IF;
IF INSTR(v_lexical, ';') > 0
OR INSTR(v_lexical, '--') > 0
OR INSTR(v_lexical, '/*') > 0
OR INSTR(v_lexical, '*/') > 0 THEN
RAISE_APPLICATION_ERROR(
-20814,
'Comments or multiple SQL statements remain after normalization. Generated prefix: '
|| SUBSTR(v_upper, 1, 160)
);
END IF;
IF REGEXP_LIKE(
v_lexical,
'(^|[^A-Z_])(ALTER|BEGIN|COMMIT|CREATE|DECLARE|DELETE|DROP|EXECUTE|GRANT|INSERT|MERGE|REVOKE|ROLLBACK|TRUNCATE|UPDATE)([^A-Z_]|$)'
) THEN
RAISE_APPLICATION_ERROR(-20815, 'Only read-only SQL is allowed.');
END IF;
IF INSTR(v_lexical, 'DBMS_') > 0
OR INSTR(v_lexical, 'UTL_') > 0
OR INSTR(v_lexical, 'SYS.') > 0
OR INSTR(v_lexical, 'ADMIN.') > 0
OR INSTR(v_lexical, 'CB_') > 0
OR INSTR(v_lexical, 'ALL_') > 0
OR INSTR(v_lexical, 'DBA_') > 0
OR INSTR(v_lexical, 'USER_') > 0
OR INSTR(v_lexical, 'FOR UPDATE') > 0 THEN
RAISE_APPLICATION_ERROR(-20816, 'System, backoffice, and locking objects are not allowed.');
END IF;
IF NOT REGEXP_LIKE(
v_lexical,
'(FROM|JOIN)[[:space:]]+(POC_2[.])?KB_(CUSTOMERS|PRODUCTS|CONTRACTS|COVERAGES|CLAIMS|EXTERNAL_HOLDINGS|STAKEHOLDERS)([[:space:],)]|$)'
) THEN
RAISE_APPLICATION_ERROR(-20817, 'The generated SQL must query a permitted KB business table.');
END IF;
END assert_safe_kb_select;
PROCEDURE open_query(
p_prompt IN CLOB,
p_limit IN PLS_INTEGER,
p_rows OUT SYS_REFCURSOR,
p_generated_sql OUT CLOB
) IS
v_prompt VARCHAR2(4000);
v_generation_prompt CLOB;
v_generated_sql CLOB;
v_sql VARCHAR2(32767);
v_limit PLS_INTEGER;
BEGIN
IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(TRIM(p_prompt)) = 0 THEN
RAISE_APPLICATION_ERROR(-20801, 'prompt is required.');
END IF;
IF DBMS_LOB.GETLENGTH(p_prompt) > c_max_prompt_length THEN
RAISE_APPLICATION_ERROR(-20802, 'prompt must be 4,000 characters or fewer.');
END IF;
v_prompt := TRIM(DBMS_LOB.SUBSTR(p_prompt, c_max_prompt_length, 1));
IF REGEXP_LIKE(UPPER(v_prompt), 'SELECT[[:space:]]+AI') THEN
RAISE_APPLICATION_ERROR(-20803, 'SELECT AI action prefixes are not allowed in prompt.');
END IF;
v_limit := LEAST(GREATEST(NVL(p_limit, 50), 1), c_max_rows);
v_generation_prompt :=
'Return exactly one Oracle SQL SELECT or WITH statement and no markdown. '
|| 'Use only these POC_2 business tables: KB_CUSTOMERS, KB_PRODUCTS, KB_CONTRACTS, '
|| 'KB_COVERAGES, KB_CLAIMS, KB_EXTERNAL_HOLDINGS, KB_STAKEHOLDERS. '
|| 'Never use DDL, DML, PL/SQL, system views, backoffice tables, comments, or FOR UPDATE. '
|| 'The caller request is: ' || v_prompt;
v_generated_sql := DBMS_CLOUD_AI.GENERATE(
prompt => v_generation_prompt,
profile_name => c_profile_name,
action => 'showsql'
);
v_sql := normalized_sql(v_generated_sql);
assert_safe_kb_select(v_sql);
-- CB_AGENT_CTX was set by CB_ORDS before this call. Parsing this statement
-- in the same database session makes Oracle evaluate POC_2 VPD policies.
OPEN p_rows FOR
'SELECT * FROM (' || v_sql || ') WHERE ROWNUM <= :row_limit'
USING v_limit;
p_generated_sql := v_sql;
END open_query;
END kb_select_ai_vpd_query_api;
/
SHOW ERRORS
PROMPT === Granting fixed query API to CB_ORDS ===
GRANT EXECUTE ON poc_2.kb_select_ai_vpd_query_api TO cb_ords;
PROMPT === POC_2 Select AI VPD query API ready ===
EXIT

View File

@@ -0,0 +1,149 @@
-- ============================================================
-- 66_kb_select_ai_vpd_query_ords.sql
--
-- POST /ords/cb-ords/kb-select-ai-vpd/query
-- Authorization: Bearer <VPD token>
-- {"prompt":"채널별 청구 건수를 보여줘", "limit":50}
--
-- Run as CB_ORDS after 65. The Bearer token is resolved before Select AI
-- generates and executes its single validated read-only SQL statement.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Granting context setup to the ORDS gateway ===
GRANT EXECUTE ON cb_ords_handler_pkg TO ords_public_user;
PROMPT === Resetting only the VPD Select AI query module ===
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'kb.select.ai.vpd');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
PROMPT === Creating VPD-aware Select AI query endpoint ===
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'kb.select.ai.vpd',
p_base_path => 'kb-select-ai-vpd/',
p_items_per_page => 0,
p_status => 'PUBLISHED'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'kb.select.ai.vpd',
p_pattern => 'query'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'kb.select.ai.vpd',
p_pattern => 'query',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_source => q'~
DECLARE
v_body_text CLOB;
v_prompt VARCHAR2(4000);
v_limit PLS_INTEGER;
v_rows SYS_REFCURSOR;
v_generated_sql CLOB;
v_response CLOB;
v_error_code NUMBER;
v_error_message VARCHAR2(4000);
BEGIN
-- Resolves the Bearer token and sets the VPD/ASO application context in
-- this very ORDS database session before the generated SQL is parsed.
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
v_body_text := :body_text;
v_prompt := JSON_VALUE(v_body_text, '$.prompt' RETURNING VARCHAR2(4000) NULL ON ERROR);
v_limit := JSON_VALUE(v_body_text, '$.limit' RETURNING NUMBER DEFAULT 50 ON ERROR);
IF v_prompt IS NULL OR TRIM(v_prompt) IS NULL THEN
RAISE_APPLICATION_ERROR(-20801, 'prompt is required');
END IF;
IF v_limit IS NULL OR v_limit < 1 OR v_limit > 100 THEN
RAISE_APPLICATION_ERROR(-20802, 'limit must be between 1 and 100');
END IF;
-- Dynamic PL/SQL keeps ORDS gateway validation limited to the local handler
-- package while CB_ORDS invokes only the fixed POC_2 API granted to it.
EXECUTE IMMEDIATE
'BEGIN poc_2.kb_select_ai_vpd_query_api.open_query(:prompt, :row_limit, :rows, :sql); END;'
USING IN v_prompt, IN v_limit, OUT v_rows, OUT v_generated_sql;
:status_code := 200;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
APEX_JSON.OPEN_OBJECT;
APEX_JSON.WRITE('profile', 'KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1');
APEX_JSON.WRITE('generatedSql', v_generated_sql);
APEX_JSON.WRITE('items', v_rows);
APEX_JSON.CLOSE_OBJECT;
cb_ords_handler_pkg.clear_vpd_context;
EXCEPTION
WHEN OTHERS THEN
v_error_code := SQLCODE;
v_error_message := SQLERRM;
cb_ords_handler_pkg.clear_vpd_context;
:status_code := CASE
WHEN v_error_code IN (-20001, -20002) THEN 403
WHEN v_error_code BETWEEN -20899 AND -20800 THEN 400
WHEN v_error_code BETWEEN -20199 AND -20100 THEN 403
ELSE 500
END;
SELECT JSON_OBJECT(
'errorCode' VALUE v_error_code,
'error' VALUE CASE
WHEN v_error_code IN (-20001, -20002) THEN
'VPD token is missing, invalid, expired, or has no access permission.'
WHEN v_error_code BETWEEN -20899 AND -20800 THEN v_error_message
ELSE 'Select AI query execution failed.'
END
RETURNING CLOB
)
INTO v_response
FROM dual;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
HTP.P(v_response);
END;
~',
p_items_per_page => 0
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'kb.select.ai.vpd',
p_pattern => 'query',
p_method => 'POST',
p_name => 'Authorization',
p_bind_variable_name => 'auth_header',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN'
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'kb.select.ai.vpd',
p_pattern => 'query',
p_method => 'POST',
p_name => 'X-VPD-Probe-Id',
p_bind_variable_name => 'probe_id',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN'
);
COMMIT;
END;
/
PROMPT === VPD-aware Select AI query endpoint ready ===
PROMPT Path: /ords/cb-ords/kb-select-ai-vpd/query
EXIT

View File

@@ -0,0 +1,125 @@
-- ============================================================
-- 67_drop_legacy_kb_contracts_premium_vpd_cls_policy.sql
--
-- 목적:
-- 과거 VPD Column-Level Security(CLS) PoC에서 생성된
-- POC_2.KB_CONTRACTS.PREMIUM 대상 VPD 컬럼 정책을 제거한다.
--
-- 배경:
-- 현재 운영 설계는 다음처럼 역할을 분리한다.
--
-- * VPD/DBMS_RLS : 행 수준 필터링만 담당
-- * ASO/Data Redaction : 컬럼 마스킹만 담당
--
-- 따라서 DBMS_RLS.ADD_POLICY의 sec_relevant_cols 기반 컬럼 제어 정책은
-- 운영 정책에서 제거한다. 이 스크립트는 named policy 하나만 삭제하며,
-- ASO/Data Redaction 정책은 건드리지 않는다.
--
-- 삭제 대상:
-- object_schema : POC_2
-- object_name : KB_CONTRACTS
-- policy_name : KB_PREMIUM_CLS_POLICY
-- column : PREMIUM
--
-- 실행 권한:
-- ADMIN 또는 DBMS_RLS.DROP_POLICY 수행 권한이 있는 계정.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Before: legacy VPD CLS policy ===
COLUMN object_owner FORMAT A18
COLUMN object_name FORMAT A32
COLUMN policy_name FORMAT A36
COLUMN function FORMAT A40
COLUMN enable FORMAT A10
COLUMN policy_type FORMAT A20
SELECT object_owner,
object_name,
policy_name,
pf_owner,
package,
function,
enable,
policy_type,
sel
FROM dba_policies
WHERE object_owner = 'POC_2'
AND object_name = 'KB_CONTRACTS'
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
PROMPT === Before: security relevant columns ===
COLUMN sec_rel_column FORMAT A32
SELECT object_owner,
object_name,
policy_name,
sec_rel_column
FROM dba_sec_relevant_cols
WHERE object_owner = 'POC_2'
AND object_name = 'KB_CONTRACTS'
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
PROMPT === Dropping only the legacy VPD CLS policy ===
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM dba_policies
WHERE object_owner = 'POC_2'
AND object_name = 'KB_CONTRACTS'
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
IF v_count > 0 THEN
DBMS_RLS.DROP_POLICY(
object_schema => 'POC_2',
object_name => 'KB_CONTRACTS',
policy_name => 'KB_PREMIUM_CLS_POLICY'
);
END IF;
END;
/
PROMPT === After: legacy VPD CLS policy ===
SELECT object_owner,
object_name,
policy_name,
pf_owner,
package,
function,
enable,
policy_type,
sel
FROM dba_policies
WHERE object_owner = 'POC_2'
AND object_name = 'KB_CONTRACTS'
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
PROMPT === After: security relevant columns ===
SELECT object_owner,
object_name,
policy_name,
sec_rel_column
FROM dba_sec_relevant_cols
WHERE object_owner = 'POC_2'
AND object_name = 'KB_CONTRACTS'
AND UPPER(policy_name) = 'KB_PREMIUM_CLS_POLICY';
PROMPT === ASO/Data Redaction on PREMIUM is intentionally not changed ===
COLUMN column_name FORMAT A24
COLUMN function_type FORMAT A28
SELECT object_owner,
object_name,
column_name,
function_type,
function_parameters
FROM redaction_columns
WHERE object_owner = 'POC_2'
AND object_name = 'KB_CONTRACTS'
AND UPPER(column_name) = 'PREMIUM';
PROMPT === Legacy KB_CONTRACTS premium VPD CLS cleanup complete ===