refs #741 #742: reorganize repository by application

This commit is contained in:
devmrko
2026-08-03 10:20:36 +09:00
parent 2e44ed0b97
commit e9d50e6a32
445 changed files with 1523 additions and 1885 deletions

View File

@@ -0,0 +1,93 @@
-- ============================================================
-- 00_cleanup.sql
-- Idempotent teardown so we can re-run the POC from scratch.
-- Errors are ignored (objects may not exist on first run).
-- Run as ADMIN.
--
-- DEFINE: &DBLINK_PG_NAME, &DBLINK_MY_NAME
-- ============================================================
WHENEVER SQLERROR CONTINUE NONE;
SET ECHO OFF
SET FEEDBACK OFF
SET DEFINE ON
PROMPT === Dropping VPD policies (if present) ===
BEGIN DBMS_RLS.DROP_POLICY(USER, 'V_CUSTOMERS_PG', 'CUSTOMERS_PG_POLICY'); EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN DBMS_RLS.DROP_POLICY(USER, 'V_CUSTOMERS_MY', 'CUSTOMERS_MY_POLICY'); EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping Data Redaction policies (if present) ===
BEGIN DBMS_REDACT.DROP_POLICY(USER, 'V_CUSTOMERS_PG', 'PII_REDACT_PG'); EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN DBMS_REDACT.DROP_POLICY(USER, 'V_CUSTOMERS_MY', 'PII_REDACT_MY'); EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping logon trigger ===
BEGIN EXECUTE IMMEDIATE 'DROP TRIGGER vpd_logon_trg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping views ===
BEGIN EXECUTE IMMEDIATE 'DROP VIEW v_customers_pg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP VIEW v_customers_my'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping secure context and package ===
BEGIN EXECUTE IMMEDIATE 'DROP CONTEXT vpd_ctx'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP PACKAGE ctx_pkg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP FUNCTION vpd_region_filter'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping end-user accounts (cascade) ===
BEGIN EXECUTE IMMEDIATE 'DROP USER vpduser_my CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP USER vpduser_pg CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP USER vpduser_both CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP USER vpduser_none CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
-- Legacy names from the original 2-user scenario — kept for idempotent
-- re-runs over an already-installed POC.
BEGIN EXECUTE IMMEDIATE 'DROP USER vpduser_a CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP USER vpduser_b CASCADE'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping permission tables ===
BEGIN EXECUTE IMMEDIATE 'DROP TABLE permission CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE user_group CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE app_group CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE app_user CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE app_customer CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP TABLE db_source CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping DB Links + credentials (cascade heterogeneous gateway) ===
BEGIN
DBMS_CLOUD_ADMIN.DROP_DATABASE_LINK(db_link_name => UPPER('&DBLINK_PG_NAME'));
EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN
DBMS_CLOUD_ADMIN.DROP_DATABASE_LINK(db_link_name => UPPER('&DBLINK_MY_NAME'));
EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN
DBMS_CLOUD.DROP_CREDENTIAL(credential_name => UPPER('&DBLINK_PG_NAME._CRED'));
EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN
DBMS_CLOUD.DROP_CREDENTIAL(credential_name => UPPER('&DBLINK_MY_NAME._CRED'));
EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Cleanup complete ===
EXIT;

119
database/adb/01_dblinks.sql Normal file
View File

@@ -0,0 +1,119 @@
-- ============================================================
-- 01_dblinks.sql
-- Run as ADMIN.
--
-- ADB 에서 외부 Postgres / MySQL 로 가는 DB Link 를 만든다.
-- DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK 를 쓰면 ADB 가 내장 게이트웨이로
-- heterogeneous (PG/MySQL) 연결을 처리해 준다.
--
-- 멱등성: 같은 이름의 link/credential 이 있으면 drop 후 재생성.
--
-- DEFINE: &DBLINK_PG_NAME, &DBLINK_MY_NAME,
-- &PG_HOST, &PG_PORT, &PG_DB, &PG_USER, &PG_PASSWORD,
-- &MY_HOST, &MY_PORT, &MY_DB, &MY_USER, &MY_PASSWORD
-- ============================================================
-- ECHO OFF: heredoc 의 DEFINE 으로 주입되는 패스워드가 stdout 으로 흐르지 않게.
-- 디버그 필요할 땐 sql 파일 단독으로만 실행하고, run.sh 경유 시는 절대 ON 하지 말 것.
SET ECHO OFF
SET VERIFY OFF
SET FEEDBACK ON
SET DEFINE ON
SET TERMOUT ON
WHENEVER SQLERROR EXIT SQL.SQLCODE
PROMPT === 1) DB link / credential ( drop) ===
DECLARE
PROCEDURE drop_link_if_exists(p_name IN VARCHAR2) IS
l_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO l_cnt FROM user_db_links
WHERE db_link = UPPER(p_name);
IF l_cnt > 0 THEN
DBMS_CLOUD_ADMIN.DROP_DATABASE_LINK(db_link_name => UPPER(p_name));
DBMS_OUTPUT.PUT_LINE('dropped db_link ' || UPPER(p_name));
END IF;
EXCEPTION WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('drop_link skip ' || p_name || ': ' || SQLERRM);
END;
PROCEDURE drop_cred_if_exists(p_name IN VARCHAR2) IS
l_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO l_cnt FROM user_credentials
WHERE credential_name = UPPER(p_name);
IF l_cnt > 0 THEN
DBMS_CLOUD.DROP_CREDENTIAL(credential_name => UPPER(p_name));
DBMS_OUTPUT.PUT_LINE('dropped credential ' || UPPER(p_name));
END IF;
EXCEPTION WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('drop_cred skip ' || p_name || ': ' || SQLERRM);
END;
BEGIN
drop_link_if_exists('&DBLINK_PG_NAME');
drop_link_if_exists('&DBLINK_MY_NAME');
drop_cred_if_exists('&DBLINK_PG_NAME._CRED');
drop_cred_if_exists('&DBLINK_MY_NAME._CRED');
END;
/
PROMPT === 2) credential ( DB ) ===
BEGIN
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => '&DBLINK_PG_NAME._CRED',
username => '&PG_USER',
password => '&PG_PASSWORD'
);
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => '&DBLINK_MY_NAME._CRED',
username => '&MY_USER',
password => '&MY_PASSWORD'
);
END;
/
PROMPT === 3) DB Link Postgres ===
BEGIN
DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK(
db_link_name => '&DBLINK_PG_NAME',
hostname => '&PG_HOST',
port => &PG_PORT,
service_name => '&PG_DB',
credential_name => '&DBLINK_PG_NAME._CRED',
gateway_params => JSON_OBJECT('db_type' VALUE 'postgres'),
ssl_server_cert_dn => NULL,
directory_name => NULL
);
END;
/
PROMPT === 4) DB Link MySQL ===
BEGIN
DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK(
db_link_name => '&DBLINK_MY_NAME',
hostname => '&MY_HOST',
port => &MY_PORT,
service_name => '&MY_DB',
credential_name => '&DBLINK_MY_NAME._CRED',
-- mysql_community: AWS RDS for MySQL, self-managed MySQL CE 등에 필요.
-- 디폴트 'mysql' 은 MySQL Enterprise/Commercial 전용.
gateway_params => JSON_OBJECT('db_type' VALUE 'mysql_community'),
ssl_server_cert_dn => NULL,
directory_name => NULL
);
END;
/
PROMPT === 5) DB link ===
COL db_link FORMAT a25
COL host FORMAT a60
SELECT db_link, host FROM user_db_links ORDER BY db_link;
PROMPT === 6) link SELECT ===
WHENEVER SQLERROR CONTINUE
PROMPT -- Postgres
SELECT COUNT(*) AS pg_customers FROM "public"."customers"@&DBLINK_PG_NAME;
PROMPT -- MySQL
SELECT COUNT(*) AS my_customers FROM "&MY_DB"."customers"@&DBLINK_MY_NAME;
PROMPT === 01_dblinks done ===
EXIT

View File

@@ -0,0 +1,56 @@
-- ============================================================
-- 01_perm_tables.sql
-- Permission model (the "policy plane"). All tables live in ADMIN.
-- Run as ADMIN.
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Creating permission-model tables ===
CREATE TABLE app_customer (
customer_id NUMBER PRIMARY KEY,
customer_name VARCHAR2(80) NOT NULL UNIQUE
);
CREATE TABLE app_user (
user_id NUMBER PRIMARY KEY,
db_username VARCHAR2(30) NOT NULL UNIQUE, -- matches Oracle SESSION_USER (UPPER)
customer_id NUMBER NOT NULL REFERENCES app_customer(customer_id),
active CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N'))
);
CREATE TABLE app_group (
group_id NUMBER PRIMARY KEY,
customer_id NUMBER NOT NULL REFERENCES app_customer(customer_id),
group_name VARCHAR2(60) NOT NULL,
UNIQUE (customer_id, group_name)
);
CREATE TABLE user_group (
user_id NUMBER NOT NULL REFERENCES app_user(user_id),
group_id NUMBER NOT NULL REFERENCES app_group(group_id),
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE db_source (
source_id NUMBER PRIMARY KEY,
source_name VARCHAR2(60) NOT NULL UNIQUE, -- e.g. RDS_POSTGRES, RDS_MYSQL
source_type VARCHAR2(30) NOT NULL, -- 'DBLINK_PG','DBLINK_MY','EXTERNAL_TABLE',...
dblink_name VARCHAR2(128)
);
-- Each permission row says: this group, on this object of this source,
-- is allowed to see rows where region is in `allowed_regions`.
-- Convention: '*' means no restriction (full access).
CREATE TABLE permission (
perm_id NUMBER PRIMARY KEY,
group_id NUMBER NOT NULL REFERENCES app_group(group_id),
source_id NUMBER NOT NULL REFERENCES db_source(source_id),
object_name VARCHAR2(60) NOT NULL, -- the local view name, e.g. 'V_CUSTOMERS_PG'
allowed_regions VARCHAR2(200) NOT NULL, -- CSV: 'KR,APAC' or '*'
UNIQUE (group_id, source_id, object_name)
);
PROMPT === Permission tables created ===
EXIT;

87
database/adb/03_seed.sql Normal file
View File

@@ -0,0 +1,87 @@
-- ============================================================
-- 03_seed.sql
-- Seed FOUR end-users to demonstrate a 2x2 source-access matrix:
--
-- user PG view MySQL view VPD predicate effect
-- ------------- ----------- ------------ ---------------------------
-- VPDUSER_MY blocked ALL rows PG='1=0' / MY=NULL(='*')
-- VPDUSER_PG ALL rows blocked PG=NULL / MY='1=0'
-- VPDUSER_BOTH ALL rows ALL rows PG=NULL / MY=NULL
-- VPDUSER_NONE blocked blocked PG='1=0' / MY='1=0'
--
-- The `permission` table grants per (group, source/view).
-- For this simplified demo every grant uses allowed_regions='*'
-- (full visibility within the source). Row-level region filtering
-- is still supported by the policy function — see commented examples
-- at the bottom of this file for how to layer it in.
--
-- Run as ADMIN.
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Seeding permission data ===
-- Tenant (kept generic — multi-tenant hook for future use).
INSERT INTO app_customer (customer_id, customer_name) VALUES (1, 'Acme Corp');
-- Four end-users, each Oracle SESSION_USER value (uppercased).
INSERT INTO app_user (user_id, db_username, customer_id) VALUES (1, 'VPDUSER_MY', 1);
INSERT INTO app_user (user_id, db_username, customer_id) VALUES (2, 'VPDUSER_PG', 1);
INSERT INTO app_user (user_id, db_username, customer_id) VALUES (3, 'VPDUSER_BOTH', 1);
INSERT INTO app_user (user_id, db_username, customer_id) VALUES (4, 'VPDUSER_NONE', 1);
-- One group per access pattern (1:1 in this demo; in production one
-- group typically aggregates many users).
INSERT INTO app_group (group_id, customer_id, group_name) VALUES (10, 1, 'MY_ONLY');
INSERT INTO app_group (group_id, customer_id, group_name) VALUES (20, 1, 'PG_ONLY');
INSERT INTO app_group (group_id, customer_id, group_name) VALUES (30, 1, 'BOTH_SOURCES');
-- (No group is needed for VPDUSER_NONE — absence of grants == fail-closed.)
-- (VPDUSER_NONE deliberately has no user_group row -> fail-closed.)
INSERT INTO user_group (user_id, group_id) VALUES (1, 10);
INSERT INTO user_group (user_id, group_id) VALUES (2, 20);
INSERT INTO user_group (user_id, group_id) VALUES (3, 30);
-- Source registry.
INSERT INTO db_source (source_id, source_name, source_type, dblink_name)
VALUES (100, 'RDS_POSTGRES', 'DBLINK_PG', 'RDS_POSTGRES_LINK');
INSERT INTO db_source (source_id, source_name, source_type, dblink_name)
VALUES (200, 'RDS_MYSQL', 'DBLINK_MY', 'RDS_LINK');
-- Permissions: '*' means no row filter (full visibility on that view).
-- Mapping (group -> view):
-- MY_ONLY(10) -> V_CUSTOMERS_MY
-- PG_ONLY(20) -> V_CUSTOMERS_PG
-- BOTH_SOURCES(30) -> V_CUSTOMERS_PG + V_CUSTOMERS_MY
-- (VPDUSER_NONE has no group, hence no permission row.)
INSERT INTO permission (perm_id, group_id, source_id, object_name, allowed_regions)
VALUES (1, 10, 200, 'V_CUSTOMERS_MY', '*');
INSERT INTO permission (perm_id, group_id, source_id, object_name, allowed_regions)
VALUES (2, 20, 100, 'V_CUSTOMERS_PG', '*');
INSERT INTO permission (perm_id, group_id, source_id, object_name, allowed_regions)
VALUES (3, 30, 100, 'V_CUSTOMERS_PG', '*');
INSERT INTO permission (perm_id, group_id, source_id, object_name, allowed_regions)
VALUES (4, 30, 200, 'V_CUSTOMERS_MY', '*');
COMMIT;
-- ------------------------------------------------------------
-- HOW TO LAYER IN ROW-LEVEL REGION FILTERS (uncomment to try)
-- ------------------------------------------------------------
-- 'BOTH_SOURCES' showing only APAC from PG instead of '*':
-- UPDATE permission SET allowed_regions = 'APAC'
-- WHERE group_id = 30 AND object_name = 'V_CUSTOMERS_PG';
-- COMMIT;
--
-- Multi-region (CSV) example for the same group on MySQL:
-- UPDATE permission SET allowed_regions = 'APAC,EMEA'
-- WHERE group_id = 30 AND object_name = 'V_CUSTOMERS_MY';
-- COMMIT;
--
-- The policy function vpd_region_filter handles CSV → IN-list
-- conversion automatically. See database/adb/06_policy.sql.
-- ------------------------------------------------------------
PROMPT === Seed complete ===
EXIT;

View File

@@ -0,0 +1,70 @@
-- ============================================================
-- 03_secure_ctx.sql
-- Secure application context. ONLY ctx_pkg can SET vpd_ctx.*
-- End-users cannot spoof their identity / region list.
--
-- The package is AUTHID DEFINER so it can read permission tables
-- without granting end-users direct SELECT on those tables.
-- It uses SYS_CONTEXT('USERENV','SESSION_USER') to identify the
-- *invoker*, so even if an end-user calls it directly they only
-- ever load their OWN permissions.
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Creating context package spec ===
CREATE OR REPLACE PACKAGE ctx_pkg AUTHID DEFINER AS
-- Called from the LOGON trigger (or manually).
-- Loads the connecting user's allowed regions per object into vpd_ctx.
PROCEDURE init;
END ctx_pkg;
/
PROMPT === Creating context package body ===
CREATE OR REPLACE PACKAGE BODY ctx_pkg AS
PROCEDURE init IS
v_user app_user.db_username%TYPE := SYS_CONTEXT('USERENV','SESSION_USER');
v_uid app_user.user_id%TYPE;
BEGIN
-- Look up app_user by Oracle session username (always uppercased).
BEGIN
SELECT user_id INTO v_uid
FROM app_user
WHERE db_username = v_user
AND active = 'Y';
EXCEPTION WHEN NO_DATA_FOUND THEN
-- Not an app user; leave context empty -> policy will return impossible predicate.
RETURN;
END;
-- For each object the user has permission on, aggregate allowed_regions
-- across all groups the user belongs to.
-- '*' wins over any specific list.
FOR r IN (
SELECT p.object_name,
CASE WHEN MAX(CASE WHEN p.allowed_regions='*' THEN 1 ELSE 0 END) = 1
THEN '*'
ELSE LISTAGG(p.allowed_regions, ',') WITHIN GROUP (ORDER BY p.allowed_regions)
END AS regions
FROM permission p
JOIN user_group ug ON ug.group_id = p.group_id
WHERE ug.user_id = v_uid
GROUP BY p.object_name
) LOOP
-- Attribute name = object name (e.g. V_CUSTOMERS_PG), value = CSV of regions.
DBMS_SESSION.SET_CONTEXT('VPD_CTX', r.object_name, r.regions);
END LOOP;
-- Also store the user_id for auditing / future use.
DBMS_SESSION.SET_CONTEXT('VPD_CTX', 'USER_ID', TO_CHAR(v_uid));
END init;
END ctx_pkg;
/
PROMPT === Binding secure context to ctx_pkg ===
-- This is the critical line: only code running INSIDE admin.ctx_pkg
-- can call DBMS_SESSION.SET_CONTEXT on the VPD_CTX namespace.
CREATE OR REPLACE CONTEXT vpd_ctx USING ctx_pkg;
PROMPT === Secure context ready ===
EXIT;

31
database/adb/05_views.sql Normal file
View File

@@ -0,0 +1,31 @@
-- ============================================================
-- 04_views.sql
-- Local views over the remote (heterogeneous) DB-linked tables.
-- End-users will be granted SELECT on these views only.
-- The raw @dblink references stay inside ADMIN, invisible to users.
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Creating v_customers_pg (Postgres) ===
-- Postgres is case-sensitive: schema, table, and column names must be quoted.
CREATE OR REPLACE VIEW v_customers_pg AS
SELECT "customer_id" AS customer_id,
"full_name" AS full_name,
"email" AS email,
"signup_date" AS signup_date,
"region" AS region
FROM "public"."customers"@RDS_POSTGRES_LINK;
PROMPT === Creating v_customers_my (MySQL) ===
-- MySQL via Oracle gateway: schema/table need quoting (lowercase preserved).
CREATE OR REPLACE VIEW v_customers_my AS
SELECT "customer_id" AS customer_id,
"full_name" AS full_name,
"email" AS email,
"signup_date" AS signup_date,
"region" AS region
FROM "ecommerce_poc"."customers"@RDS_LINK;
PROMPT === Views created ===
EXIT;

View File

@@ -0,0 +1,84 @@
-- ============================================================
-- 05_policy.sql
-- VPD policy function + DBMS_RLS.ADD_POLICY attachments.
--
-- The function returns a row-filter predicate based on the
-- secure context loaded at logon. If no permission is loaded for
-- this object, it returns an impossible predicate so the user
-- sees ZERO rows (fail closed).
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Creating policy function vpd_region_filter ===
CREATE OR REPLACE FUNCTION vpd_region_filter(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2 AS
v_regions VARCHAR2(4000);
v_pred VARCHAR2(4000);
v_list VARCHAR2(4000);
BEGIN
-- Read the CSV of allowed regions for this object from secure context.
v_regions := SYS_CONTEXT('VPD_CTX', UPPER(p_object));
IF v_regions IS NULL THEN
-- Fail closed: no entry => no rows visible.
RETURN '1=0';
END IF;
IF INSTR(v_regions, '*') > 0 THEN
-- Wildcard => no row filter (full visibility on this object).
RETURN NULL;
END IF;
-- Convert CSV 'KR,APAC' -> "'KR','APAC'" for an IN-list.
-- (Region values come only from our own permission table, so quoting
-- by escaping single quotes is sufficient; no untrusted user input.)
SELECT LISTAGG('''' || REPLACE(TRIM(column_value),'''','''''') || '''', ',')
WITHIN GROUP (ORDER BY column_value)
INTO v_list
FROM TABLE(APEX_STRING.SPLIT(v_regions, ','));
IF v_list IS NULL THEN
RETURN '1=0';
END IF;
v_pred := 'region IN (' || v_list || ')';
RETURN v_pred;
END;
/
SHOW ERRORS
PROMPT === Attaching policies to views ===
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'V_CUSTOMERS_PG',
policy_name => 'CUSTOMERS_PG_POLICY',
function_schema => USER,
policy_function => 'VPD_REGION_FILTER',
statement_types => 'SELECT',
update_check => FALSE,
enable => TRUE
);
END;
/
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'V_CUSTOMERS_MY',
policy_name => 'CUSTOMERS_MY_POLICY',
function_schema => USER,
policy_function => 'VPD_REGION_FILTER',
statement_types => 'SELECT',
update_check => FALSE,
enable => TRUE
);
END;
/
PROMPT === Policies attached ===
EXIT;

View File

@@ -0,0 +1,87 @@
-- ============================================================
-- 05a_redaction.sql
-- Column-level masking with Oracle Data Redaction (DBMS_REDACT).
--
-- Policy:
-- PII columns (email, full_name) are MASKED unless the session
-- has full-region access ('*') on the corresponding view.
--
-- - VPDUSER_MY -> PG view masked, MY view unmasked (allowed '*')
-- - VPDUSER_PG -> PG view unmasked, MY view masked
-- - VPDUSER_BOTH -> both views unmasked
-- - VPDUSER_NONE -> both masked (but rows filtered to 0 anyway)
--
-- Reuses the secure VPD_CTX populated at logon — no new context.
-- Data Redaction and VPD compose: VPD filters rows first, Redaction
-- then transforms columns on the surviving rows.
--
-- NOTE: ADMIN has the EXEMPT REDACTION POLICY system privilege
-- implicitly via DBA, so ADMIN sessions still see real values.
-- End-users do not have it, so they see the masked output.
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Creating PII redaction policy on v_customers_pg ===
BEGIN
DBMS_REDACT.ADD_POLICY(
object_schema => USER,
object_name => 'V_CUSTOMERS_PG',
column_name => 'EMAIL',
policy_name => 'PII_REDACT_PG',
function_type => DBMS_REDACT.REGEXP,
regexp_pattern => '^(.)(.*)(@.*)$',
regexp_replace_string => '\1****\3',
regexp_position => 1,
regexp_occurrence => 1,
expression => 'SYS_CONTEXT(''VPD_CTX'',''V_CUSTOMERS_PG'') IS NULL OR SYS_CONTEXT(''VPD_CTX'',''V_CUSTOMERS_PG'') != ''*'''
);
DBMS_REDACT.ALTER_POLICY(
object_schema => USER,
object_name => 'V_CUSTOMERS_PG',
policy_name => 'PII_REDACT_PG',
action => DBMS_REDACT.ADD_COLUMN,
column_name => 'FULL_NAME',
function_type => DBMS_REDACT.REGEXP,
regexp_pattern => '^(.)(.*)$',
regexp_replace_string => '\1****',
regexp_position => 1,
regexp_occurrence => 1
);
END;
/
PROMPT === Creating PII redaction policy on v_customers_my ===
BEGIN
DBMS_REDACT.ADD_POLICY(
object_schema => USER,
object_name => 'V_CUSTOMERS_MY',
column_name => 'EMAIL',
policy_name => 'PII_REDACT_MY',
function_type => DBMS_REDACT.REGEXP,
regexp_pattern => '^(.)(.*)(@.*)$',
regexp_replace_string => '\1****\3',
regexp_position => 1,
regexp_occurrence => 1,
expression => 'SYS_CONTEXT(''VPD_CTX'',''V_CUSTOMERS_MY'') IS NULL OR SYS_CONTEXT(''VPD_CTX'',''V_CUSTOMERS_MY'') != ''*'''
);
DBMS_REDACT.ALTER_POLICY(
object_schema => USER,
object_name => 'V_CUSTOMERS_MY',
policy_name => 'PII_REDACT_MY',
action => DBMS_REDACT.ADD_COLUMN,
column_name => 'FULL_NAME',
function_type => DBMS_REDACT.REGEXP,
regexp_pattern => '^(.)(.*)$',
regexp_replace_string => '\1****',
regexp_position => 1,
regexp_occurrence => 1
);
END;
/
PROMPT === Redaction policies attached ===
EXIT;

View File

@@ -0,0 +1,77 @@
-- ============================================================
-- 07_end_users.sql
-- Create the FOUR end-user accounts with MINIMAL privileges.
-- Add a LOGON trigger that loads each user's context automatically.
-- Run as ADMIN.
--
-- DEFINE: &VPDUSER_MY_PASSWORD, &VPDUSER_PG_PASSWORD,
-- &VPDUSER_BOTH_PASSWORD, &VPDUSER_NONE_PASSWORD
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE ON
SET VERIFY OFF -- 비번이 'new 1:' 라인으로 echo 되는 것을 막음 (leak 방지)
PROMPT === Creating end-user accounts ===
-- Passwords come from .env (DEFINE) so they aren't hardcoded in source.
-- Production should use IAM / proxy auth / mTLS instead of static passwords.
CREATE USER vpduser_my IDENTIFIED BY "&VPDUSER_MY_PASSWORD";
CREATE USER vpduser_pg IDENTIFIED BY "&VPDUSER_PG_PASSWORD";
CREATE USER vpduser_both IDENTIFIED BY "&VPDUSER_BOTH_PASSWORD";
CREATE USER vpduser_none IDENTIFIED BY "&VPDUSER_NONE_PASSWORD";
-- Login privilege only. No QUOTA — these users never create objects.
GRANT CREATE SESSION TO vpduser_my;
GRANT CREATE SESSION TO vpduser_pg;
GRANT CREATE SESSION TO vpduser_both;
GRANT CREATE SESSION TO vpduser_none;
PROMPT === Granting SELECT on the policy-protected views ONLY ===
-- We deliberately grant SELECT on BOTH views to all four users.
-- The VPD policy decides what they actually see — including 0 rows
-- when there is no permission row. This is what makes the demo a
-- clean security boundary: revoking access doesn't mean revoking
-- the GRANT; it means removing the row in `permission`.
GRANT SELECT ON v_customers_pg TO vpduser_my;
GRANT SELECT ON v_customers_my TO vpduser_my;
GRANT SELECT ON v_customers_pg TO vpduser_pg;
GRANT SELECT ON v_customers_my TO vpduser_pg;
GRANT SELECT ON v_customers_pg TO vpduser_both;
GRANT SELECT ON v_customers_my TO vpduser_both;
GRANT SELECT ON v_customers_pg TO vpduser_none;
GRANT SELECT ON v_customers_my TO vpduser_none;
-- ctx_pkg is bound to the secure context; calling it is harmless
-- (the package always loads ONLY the caller's own permissions).
GRANT EXECUTE ON ctx_pkg TO vpduser_my;
GRANT EXECUTE ON ctx_pkg TO vpduser_pg;
GRANT EXECUTE ON ctx_pkg TO vpduser_both;
GRANT EXECUTE ON ctx_pkg TO vpduser_none;
-- NOTE on what we are deliberately NOT granting:
-- * NO grant on app_user / permission / etc. -> users can't read who-can-see-what
-- * NO grant on the underlying @dblink tables -> can't bypass the view
-- * NO grant on DBMS_RLS -> can't alter/drop the policy
-- * NO EXEMPT ACCESS POLICY -> can't escape VPD
-- * NO CREATE TABLE / CREATE MATERIALIZED VIEW -> can't snapshot filtered data
-- * NO DBA / PDB_DBA / RESOURCE roles -> least privilege
PROMPT === Creating LOGON trigger that auto-loads context ===
CREATE OR REPLACE TRIGGER vpd_logon_trg
AFTER LOGON ON DATABASE
BEGIN
-- Only fire for our application end-users. ADMIN logons keep normal behavior.
IF SYS_CONTEXT('USERENV','SESSION_USER') IN
('VPDUSER_MY','VPDUSER_PG','VPDUSER_BOTH','VPDUSER_NONE') THEN
admin.ctx_pkg.init;
END IF;
EXCEPTION
WHEN OTHERS THEN
-- Never block login on a context-loading error; fail closed (no perms loaded
-- means policy returns 1=0, i.e. no rows). Log if you have an audit table.
NULL;
END;
/
PROMPT === End-users ready ===
EXIT;

View File

@@ -0,0 +1,75 @@
-- ============================================================
-- 08_tests_user_my.sql
-- Run as VPDUSER_MY (group MY_ONLY).
--
-- Expected:
-- - regions_pg = NULL -> policy returns '1=0' -> 0 rows from PG view
-- - regions_my = '*' -> policy returns NULL -> ALL rows from MySQL view
-- - email/full_name on MySQL view are unmasked ('*' bypasses redaction)
-- - all five bypass attempts fail (this is the full bypass-attempt suite;
-- the other three user tests rerun the most relevant subset only)
-- ============================================================
SET FEEDBACK ON
SET LINESIZE 200
SET PAGESIZE 100
PROMPT
PROMPT === Who am I, and what context did the LOGON trigger load? ===
SELECT USER AS session_user,
SYS_CONTEXT('VPD_CTX','USER_ID') AS app_user_id,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_PG') AS regions_pg,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_MY') AS regions_my
FROM dual;
PROMPT
PROMPT === Row counts (expect: PG=0, MY=17) ===
SELECT 'V_CUSTOMERS_PG' AS view_name, COUNT(*) AS rows_visible FROM admin.v_customers_pg
UNION ALL
SELECT 'V_CUSTOMERS_MY', COUNT(*) FROM admin.v_customers_my;
PROMPT
PROMPT === MySQL view sample (expect: ALL regions, UNMASKED email/full_name) ===
COLUMN customer_id FORMAT 9999
COLUMN full_name FORMAT A20
COLUMN email FORMAT A30
COLUMN region FORMAT A8
SELECT customer_id, full_name, email, region
FROM admin.v_customers_my
ORDER BY customer_id
FETCH FIRST 5 ROWS ONLY;
PROMPT
PROMPT === PG view sample (expect: NO ROWS fail closed) ===
SELECT customer_id, full_name, email, region
FROM admin.v_customers_pg
ORDER BY customer_id;
PROMPT
PROMPT === BYPASS 1: query remote tables directly (expect ORA-00942 / privilege error) ===
WHENEVER SQLERROR CONTINUE;
SELECT COUNT(*) FROM "public"."customers"@RDS_POSTGRES_LINK;
SELECT COUNT(*) FROM "ecommerce_poc"."customers"@RDS_LINK;
PROMPT
PROMPT === BYPASS 2: try to spoof the context (expect ORA-01031) ===
BEGIN
DBMS_SESSION.SET_CONTEXT('VPD_CTX','V_CUSTOMERS_PG','*');
END;
/
PROMPT
PROMPT === BYPASS 3: try to drop the policy (expect ORA-00942 / privilege error) ===
BEGIN
DBMS_RLS.DROP_POLICY('ADMIN','V_CUSTOMERS_PG','CUSTOMERS_PG_POLICY');
END;
/
PROMPT
PROMPT === BYPASS 4: try to read the permission table (expect ORA-00942) ===
SELECT COUNT(*) FROM admin.permission;
PROMPT
PROMPT === BYPASS 5: try to read app_user (expect ORA-00942) ===
SELECT COUNT(*) FROM admin.app_user;
EXIT;

View File

@@ -0,0 +1,44 @@
-- ============================================================
-- 09_tests_user_pg.sql
-- Run as VPDUSER_PG (group PG_ONLY).
--
-- Expected (mirror of 08):
-- - regions_pg = '*' -> ALL rows from PG view, unmasked
-- - regions_my = NULL -> 0 rows from MySQL view
-- ============================================================
SET FEEDBACK ON
SET LINESIZE 200
SET PAGESIZE 100
PROMPT
PROMPT === Who am I, and what context did the LOGON trigger load? ===
SELECT USER AS session_user,
SYS_CONTEXT('VPD_CTX','USER_ID') AS app_user_id,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_PG') AS regions_pg,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_MY') AS regions_my
FROM dual;
PROMPT
PROMPT === Row counts (expect: PG=12, MY=0) ===
SELECT 'V_CUSTOMERS_PG' AS view_name, COUNT(*) AS rows_visible FROM admin.v_customers_pg
UNION ALL
SELECT 'V_CUSTOMERS_MY', COUNT(*) FROM admin.v_customers_my;
PROMPT
PROMPT === PG view sample (expect: ALL regions, UNMASKED email/full_name) ===
COLUMN customer_id FORMAT 9999
COLUMN full_name FORMAT A20
COLUMN email FORMAT A30
COLUMN region FORMAT A8
SELECT customer_id, full_name, email, region
FROM admin.v_customers_pg
ORDER BY customer_id
FETCH FIRST 5 ROWS ONLY;
PROMPT
PROMPT === MySQL view sample (expect: NO ROWS fail closed) ===
SELECT customer_id, full_name, email, region
FROM admin.v_customers_my
ORDER BY customer_id;
EXIT;

View File

@@ -0,0 +1,45 @@
-- ============================================================
-- 10_tests_user_both.sql
-- Run as VPDUSER_BOTH (group BOTH_SOURCES).
--
-- Expected:
-- - regions_pg = '*' AND regions_my = '*'
-- - Both views fully visible, PII unmasked everywhere
-- ============================================================
SET FEEDBACK ON
SET LINESIZE 200
SET PAGESIZE 100
PROMPT
PROMPT === Who am I, and what context did the LOGON trigger load? ===
SELECT USER AS session_user,
SYS_CONTEXT('VPD_CTX','USER_ID') AS app_user_id,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_PG') AS regions_pg,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_MY') AS regions_my
FROM dual;
PROMPT
PROMPT === Row counts (expect: PG=12, MY=17) ===
SELECT 'V_CUSTOMERS_PG' AS view_name, COUNT(*) AS rows_visible FROM admin.v_customers_pg
UNION ALL
SELECT 'V_CUSTOMERS_MY', COUNT(*) FROM admin.v_customers_my;
PROMPT
PROMPT === PG view sample (expect: UNMASKED) ===
COLUMN customer_id FORMAT 9999
COLUMN full_name FORMAT A20
COLUMN email FORMAT A30
COLUMN region FORMAT A8
SELECT customer_id, full_name, email, region
FROM admin.v_customers_pg
ORDER BY customer_id
FETCH FIRST 5 ROWS ONLY;
PROMPT
PROMPT === MySQL view sample (expect: UNMASKED) ===
SELECT customer_id, full_name, email, region
FROM admin.v_customers_my
ORDER BY customer_id
FETCH FIRST 5 ROWS ONLY;
EXIT;

View File

@@ -0,0 +1,53 @@
-- ============================================================
-- 11_tests_user_none.sql
-- Run as VPDUSER_NONE — the "default deny" case.
--
-- This user has CREATE SESSION and SELECT on both views, but ZERO
-- rows in the permission table. The LOGON trigger still fires and
-- ctx_pkg.init still runs — it just finds nothing to load.
--
-- Expected:
-- - regions_pg = NULL AND regions_my = NULL
-- - Both views return 0 rows (policy returns '1=0' / fail-closed)
-- - This proves the model is "deny by default" — adding a user
-- without a permission row is automatically safe.
-- ============================================================
SET FEEDBACK ON
SET LINESIZE 200
SET PAGESIZE 100
PROMPT
PROMPT === Who am I, and what context did the LOGON trigger load? ===
PROMPT === (expect: regions_pg and regions_my both NULL) ===
SELECT USER AS session_user,
SYS_CONTEXT('VPD_CTX','USER_ID') AS app_user_id,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_PG') AS regions_pg,
SYS_CONTEXT('VPD_CTX','V_CUSTOMERS_MY') AS regions_my
FROM dual;
PROMPT
PROMPT === Row counts (expect: PG=0, MY=0 fail-closed) ===
SELECT 'V_CUSTOMERS_PG' AS view_name, COUNT(*) AS rows_visible FROM admin.v_customers_pg
UNION ALL
SELECT 'V_CUSTOMERS_MY', COUNT(*) FROM admin.v_customers_my;
PROMPT
PROMPT === Confirm no rows leak through despite the GRANT on the view ===
SELECT * FROM admin.v_customers_pg WHERE ROWNUM <= 1;
SELECT * FROM admin.v_customers_my WHERE ROWNUM <= 1;
PROMPT
PROMPT === BYPASS: even with no permission row, all 4 bypass surfaces blocked ===
WHENEVER SQLERROR CONTINUE;
SELECT COUNT(*) FROM "public"."customers"@RDS_POSTGRES_LINK;
SELECT COUNT(*) FROM admin.permission;
BEGIN
DBMS_SESSION.SET_CONTEXT('VPD_CTX','V_CUSTOMERS_PG','*');
END;
/
BEGIN
DBMS_RLS.DROP_POLICY('ADMIN','V_CUSTOMERS_PG','CUSTOMERS_PG_POLICY');
END;
/
EXIT;

View File

@@ -0,0 +1,83 @@
-- ============================================================
-- 12_tests_admin_audit.sql
-- Run as ADMIN to audit / verify policy attachment and the
-- 4-user access matrix.
-- ============================================================
SET FEEDBACK ON
SET LINESIZE 220
SET PAGESIZE 100
COL object_name FORMAT a20
COL policy FORMAT a25
COL pf_owner FORMAT a15
COL package FORMAT a15
COL function FORMAT a25
COL sel FORMAT a3
COL enable FORMAT a6
PROMPT
PROMPT === Attached VPD policies (expect rows for V_CUSTOMERS_PG and V_CUSTOMERS_MY) ===
SELECT object_name,
policy_name AS policy,
pf_owner,
package,
function,
sel,
enable
FROM dba_policies
WHERE object_owner = USER
AND object_name IN ('V_CUSTOMERS_PG','V_CUSTOMERS_MY')
ORDER BY object_name, policy_name;
PROMPT
PROMPT === Attached Data Redaction policies ===
COL object_name FORMAT a20
COL policy_name FORMAT a20
COL expression FORMAT a80 WORD_WRAPPED
SELECT object_name,
policy_name,
expression
FROM redaction_policies
WHERE object_owner = USER
ORDER BY object_name, policy_name;
PROMPT
PROMPT === Redacted columns ===
COL object_name FORMAT a20
COL column_name FORMAT a15
COL function_type FORMAT a15
COL regexp_pattern FORMAT a25
COL regexp_replace_string FORMAT a15
SELECT object_name,
column_name,
function_type,
regexp_pattern,
regexp_replace_string
FROM redaction_columns
WHERE object_owner = USER
ORDER BY object_name, column_name;
PROMPT
PROMPT === 4-user access matrix (from permission table) ===
PROMPT === Expected: ===
PROMPT === VPDUSER_MY -> V_CUSTOMERS_MY '*' ===
PROMPT === VPDUSER_PG -> V_CUSTOMERS_PG '*' ===
PROMPT === VPDUSER_BOTH -> V_CUSTOMERS_PG '*' + V_CUSTOMERS_MY '*' ===
PROMPT === VPDUSER_NONE -> (no rows fail-closed by absence) ===
COL db_username FORMAT a14
COL group_name FORMAT a14
COL source_name FORMAT a14
COL object_name FORMAT a16
COL allowed_regions FORMAT a16
SELECT u.db_username,
g.group_name,
s.source_name,
p.object_name,
p.allowed_regions
FROM app_user u
LEFT JOIN user_group ug ON ug.user_id = u.user_id
LEFT JOIN app_group g ON g.group_id = ug.group_id
LEFT JOIN permission p ON p.group_id = g.group_id
LEFT JOIN db_source s ON s.source_id = p.source_id
ORDER BY u.db_username, p.object_name NULLS LAST;
EXIT;

View File

@@ -0,0 +1,192 @@
-- ============================================================
-- 13_dds_variant.sql (OPTIONAL — Oracle 26ai Deep Data Security)
-- ============================================================
-- Reimplements the same 4-user source-access matrix as the VPD
-- path (06_policy.sql + 07_end_users.sql), but using Oracle Deep
-- Data Security (DDS) — the declarative SQL successor to VPD/RAS
-- introduced in Oracle AI Database 26ai (announced 2026-04-09).
--
-- Matrix (identical to VPD demo):
-- user PG view MySQL view
-- -------------- ----------- -----------
-- DDSUSER_MY blocked ALL rows
-- DDSUSER_PG ALL rows blocked
-- DDSUSER_BOTH ALL rows ALL rows
-- DDSUSER_NONE blocked blocked (no data role = default deny)
--
-- ============================================================
-- PREREQUISITES (verify before running)
-- ------------------------------------------------------------
-- * Oracle AI Database 23.26.2 or later
-- * COMPATIBLE init parameter >= 20.0
-- * ADMIN holds: CREATE END USER, CREATE DATA ROLE, GRANT DATA
-- ROLE, CREATE DATA GRANT system privileges (ADMIN gets them
-- by default on ADB; on on-prem you may need to grant).
--
-- Check version + COMPATIBLE before running:
-- SELECT banner_full FROM v$version;
-- SELECT value FROM v$parameter WHERE name = 'compatible';
-- ============================================================
-- COEXISTENCE NOTE
-- ------------------------------------------------------------
-- This script creates *new* end users with the `ddsuser_` prefix
-- so it does NOT collide with the VPD demo users (`vpduser_*`).
--
-- It also creates **dedicated DDS-only views** (`v_dds_customers_pg`,
-- `v_dds_customers_my`) — separate from the VPD demo's
-- `v_customers_pg`/`v_customers_my`. Reason: the VPD views have a
-- live `DBMS_RLS` policy that returns `1=0` for any session whose
-- LOGON trigger didn't load the VPD application context — i.e. for
-- our DDS end users. Putting DDS Data Grants on top of the VPD
-- views would silently return 0 rows (DDS allows but VPD blocks).
-- Dedicated views with no VPD policy let DDS be the sole gatekeeper.
--
-- We deliberately do NOT enable `SET USE DATA GRANTS ONLY` on the
-- DDS views in this demo — but doing so is the recommended
-- production posture (single declarative policy plane).
--
-- DEFINE: &DDSUSER_MY_PASSWORD, &DDSUSER_PG_PASSWORD,
-- &DDSUSER_BOTH_PASSWORD, &DDSUSER_NONE_PASSWORD
-- ============================================================
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE ON
SET VERIFY OFF -- 비번이 'new 1:' 라인으로 echo 되는 것을 막음 (leak 방지)
PROMPT === 0. Creating DDS-only views (no VPD policy attached) ===
-- Functionally identical to v_customers_pg / v_customers_my but
-- kept separate so that DDS Data Grants are the sole authority.
CREATE OR REPLACE VIEW v_dds_customers_pg AS
SELECT "customer_id" AS customer_id,
"full_name" AS full_name,
"email" AS email,
"signup_date" AS signup_date,
"region" AS region
FROM "public"."customers"@RDS_POSTGRES_LINK;
CREATE OR REPLACE VIEW v_dds_customers_my AS
SELECT "customer_id" AS customer_id,
"full_name" AS full_name,
"email" AS email,
"signup_date" AS signup_date,
"region" AS region
FROM "ecommerce_poc"."customers"@RDS_LINK;
PROMPT === 1. Creating local END USERs (schemaless, no objects) ===
-- End users in DDS do not own a schema and cannot create objects.
-- They authenticate, then receive access purely via DATA GRANTs.
CREATE END USER "ddsuser_my" IDENTIFIED BY "&DDSUSER_MY_PASSWORD";
CREATE END USER "ddsuser_pg" IDENTIFIED BY "&DDSUSER_PG_PASSWORD";
CREATE END USER "ddsuser_both" IDENTIFIED BY "&DDSUSER_BOTH_PASSWORD";
CREATE END USER "ddsuser_none" IDENTIFIED BY "&DDSUSER_NONE_PASSWORD";
PROMPT === 2. Creating DATA ROLEs + connection role for direct logon ===
-- Per the DDS direct-logon pattern: a standard ROLE carries
-- CREATE SESSION, then that standard role is granted to each
-- data role so end users can actually connect.
CREATE ROLE dds_db_role;
GRANT CREATE SESSION TO dds_db_role;
CREATE DATA ROLE my_only_role;
CREATE DATA ROLE pg_only_role;
CREATE DATA ROLE both_sources_role;
-- A "connect only" data role for ddsuser_none — they need to be able
-- to log in (so we can prove "authenticated but no data visible"),
-- but they must hold NO data grants. END USERs can't be grantees of
-- a regular ROLE directly (ORA-01917) — connection privilege must
-- flow through a DATA ROLE.
CREATE DATA ROLE connect_only_role;
GRANT dds_db_role TO my_only_role;
GRANT dds_db_role TO pg_only_role;
GRANT dds_db_role TO both_sources_role;
GRANT dds_db_role TO connect_only_role;
PROMPT === 3. Mapping end users to data roles ===
GRANT DATA ROLE my_only_role TO "ddsuser_my";
GRANT DATA ROLE pg_only_role TO "ddsuser_pg";
GRANT DATA ROLE both_sources_role TO "ddsuser_both";
GRANT DATA ROLE connect_only_role TO "ddsuser_none";
PROMPT === 4. Creating DATA GRANTs (the declarative equivalent of the VPD policy) ===
-- These five lines replace the entire vpd_region_filter PL/SQL
-- function + the per-row lookups against the `permission` table.
-- Each grant is a single declarative SQL statement.
-- DDSUSER_MY -> ALL rows from v_customers_my, no PG access.
CREATE DATA GRANT admin.dds_my_only_grant_mysql
AS SELECT
ON admin.v_dds_customers_my
TO my_only_role;
-- DDSUSER_PG -> ALL rows from v_customers_pg, no MY access.
CREATE DATA GRANT admin.dds_pg_only_grant_pg
AS SELECT
ON admin.v_dds_customers_pg
TO pg_only_role;
-- DDSUSER_BOTH -> both views.
CREATE DATA GRANT admin.dds_both_grant_pg
AS SELECT
ON admin.v_dds_customers_pg
TO both_sources_role;
CREATE DATA GRANT admin.dds_both_grant_mysql
AS SELECT
ON admin.v_dds_customers_my
TO both_sources_role;
-- DDSUSER_NONE: NO data grant -> default deny.
PROMPT === 5. (Optional) Region row-level filter DDS-style ===
-- The VPD path stores allowed_regions in a `permission` table and
-- builds the predicate at query time. DDS makes this a declarative
-- WHERE clause directly on the grant. Example: restrict
-- both_sources_role to APAC only on the PG view. Uncomment to try.
--
-- CREATE OR REPLACE DATA GRANT admin.dds_both_grant_pg
-- AS SELECT
-- ON admin.v_dds_customers_pg
-- WHERE region = 'APAC'
-- TO both_sources_role;
--
-- For multi-region, use IN-list (no CSV-splitting helper needed,
-- unlike the VPD policy):
--
-- CREATE OR REPLACE DATA GRANT admin.dds_both_grant_mysql
-- AS SELECT
-- ON admin.v_dds_customers_my
-- WHERE region IN ('APAC','EMEA')
-- TO both_sources_role;
PROMPT === 6. (Optional) Column masking DDS-style ===
-- The VPD path uses a separate Data Redaction policy
-- (06a_redaction.sql) to NULL out the email column. DDS folds this
-- into the grant itself with ALL COLUMNS EXCEPT — one statement
-- handles both row filter AND column mask.
--
-- CREATE OR REPLACE DATA GRANT admin.dds_both_grant_pg
-- AS SELECT (ALL COLUMNS EXCEPT email)
-- ON admin.v_dds_customers_pg
-- TO both_sources_role;
--
-- Excluded columns return NULL (same UX as redaction), but it's
-- one declarative grant instead of (policy function + redaction
-- policy + permission table row).
PROMPT === DDS variant ready ===
-- ------------------------------------------------------------
-- Verify with:
-- sqlplus '"ddsuser_pg"'/<pw>@<service>
-- SQL> SELECT ORA_END_USER_CONTEXT.username FROM dual;
-- SQL> SELECT COUNT(*) FROM admin.v_dds_customers_pg; -- expect 12
-- SQL> SELECT COUNT(*) FROM admin.v_dds_customers_my; -- expect ORA-00942
-- (DDS hides the object entirely when no grant exists — note this is
-- stronger than VPD which would return "0 rows" for the same case.)
--
-- Audit the grants:
-- SELECT * FROM dba_data_grants;
-- SELECT * FROM dba_data_roles;
-- SELECT * FROM dba_end_users;
-- ------------------------------------------------------------
EXIT;

View File

@@ -0,0 +1,61 @@
-- ============================================================
-- 14_tests_dds_user.sql
-- Shared verification script for all 4 DDS end-users.
-- Run as ddsuser_my / ddsuser_pg / ddsuser_both / ddsuser_none.
--
-- Expected matrix (DDS):
-- user v_dds_customers_pg v_dds_customers_my
-- --------------- --------------------- ---------------------
-- ddsuser_my ORA-00942 (hidden) 17 rows
-- ddsuser_pg 12 rows ORA-00942 (hidden)
-- ddsuser_both 12 rows 17 rows
-- ddsuser_none ORA-00942 (hidden) ORA-00942 (hidden)
--
-- Note: where VPD silently returns "0 rows", DDS returns
-- `ORA-00942 (table or view does not exist)` — the object is
-- not visible at all. `WHENEVER SQLERROR CONTINUE` is set so the
-- script keeps going past expected ORA-00942 errors.
-- ============================================================
WHENEVER SQLERROR CONTINUE
SET FEEDBACK ON
SET LINESIZE 200
SET PAGESIZE 100
PROMPT
PROMPT === Who am I? (DDS end-user context) ===
SELECT ORA_END_USER_CONTEXT.username AS end_user_name FROM dual;
PROMPT
PROMPT === Row counts on the DDS-only views ===
PROMPT (ORA-00942 here means "no DATA GRANT" that's the expected deny path)
SELECT 'v_dds_customers_pg' AS view_name, COUNT(*) AS rows_visible
FROM admin.v_dds_customers_pg;
SELECT 'v_dds_customers_my' AS view_name, COUNT(*) AS rows_visible
FROM admin.v_dds_customers_my;
PROMPT
PROMPT === Sample (first 3 rows from each view, if visible) ===
COLUMN customer_id FORMAT 9999
COLUMN full_name FORMAT A20
COLUMN email FORMAT A30
COLUMN region FORMAT A8
SELECT customer_id, full_name, email, region
FROM admin.v_dds_customers_pg
ORDER BY customer_id FETCH FIRST 3 ROWS ONLY;
SELECT customer_id, full_name, email, region
FROM admin.v_dds_customers_my
ORDER BY customer_id FETCH FIRST 3 ROWS ONLY;
PROMPT
PROMPT === Bypass attempts ===
PROMPT -- 1) underlying VPD view (no GRANT given) -> must fail
SELECT COUNT(*) FROM admin.v_customers_pg;
SELECT COUNT(*) FROM admin.v_customers_my;
PROMPT -- 2) raw remote table via DB Link (no GRANT on the link) -> must fail
SELECT COUNT(*) FROM "public"."customers"@RDS_POSTGRES_LINK;
SELECT COUNT(*) FROM "ecommerce_poc"."customers"@RDS_LINK;
PROMPT -- 3) permission / app_user tables (VPD plane) -> must fail
SELECT COUNT(*) FROM admin.permission;
SELECT COUNT(*) FROM admin.app_user;
EXIT;

View File

@@ -0,0 +1,53 @@
-- ============================================================
-- 15_dds_cleanup.sql
-- Idempotent teardown for the DDS variant (13_dds_variant.sql).
-- Safe to run before 13_dds_variant.sql to wipe partial state.
-- Errors are ignored (objects may not exist on first run).
-- Run as ADMIN.
-- ============================================================
WHENEVER SQLERROR CONTINUE NONE;
SET ECHO OFF
SET FEEDBACK OFF
PROMPT === Dropping DDS data grants (if present) ===
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_my_only_grant_mysql'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_pg_only_grant_pg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_both_grant_pg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_both_grant_mysql'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping DDS end users (no CASCADE END USERs own no objects) ===
BEGIN EXECUTE IMMEDIATE 'DROP END USER "ddsuser_my"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP END USER "ddsuser_pg"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP END USER "ddsuser_both"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP END USER "ddsuser_none"'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping DDS data roles (if present) ===
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE my_only_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE pg_only_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE both_sources_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP DATA ROLE connect_only_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping DDS regular role (session-carrier) ===
BEGIN EXECUTE IMMEDIATE 'DROP ROLE dds_db_role'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === Dropping DDS-only views ===
BEGIN EXECUTE IMMEDIATE 'DROP VIEW v_dds_customers_pg'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
BEGIN EXECUTE IMMEDIATE 'DROP VIEW v_dds_customers_my'; EXCEPTION WHEN OTHERS THEN NULL; END;
/
PROMPT === DDS cleanup complete ===
EXIT;

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,489 @@
-- ============================================================
-- 17_agent_ords_security_local_vpd_setup.sql
-- Local-only executable VPD example for Agent ORDS security.
--
-- Scenario:
-- * ORDS connects as CB_ORDS.
-- * ORDS receives Authorization: Bearer <key>.
-- * ORDS handler calls ADMIN.CB_AGENT_CTX_PKG.SET_USER_BY_BEARER.
-- * VPD reads SYS_CONTEXT and filters ADMIN.CB_V_SEARCH_DOCUMENTS.
-- * DBMS_REDACT masks the CONTENTS column unless the key user is allowed.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Creating local base table and sample documents ===
CREATE TABLE cb_search_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_search_documents VALUES (1, 'HR payroll guide', 'E1001', 'HR', 'Payroll policy and HR guide', SYSDATE);
INSERT INTO cb_search_documents VALUES (2, 'HR recruiting plan', 'E1002', 'HR', 'Recruiting plan for HR team', SYSDATE);
INSERT INTO cb_search_documents VALUES (3, 'Finance close checklist','E2001', 'FIN', 'Monthly close checklist', SYSDATE);
INSERT INTO cb_search_documents VALUES (4, 'Finance audit memo', 'E2002', 'FIN', 'Audit memo for finance team', SYSDATE);
INSERT INTO cb_search_documents VALUES (5, 'Sales forecast', 'E3001', 'SALES', 'Quarterly sales forecast', SYSDATE);
INSERT INTO cb_search_documents VALUES (6, 'HR benefits notice', 'E1003', 'HR', 'Benefits notice for employees', SYSDATE);
CREATE OR REPLACE VIEW cb_v_search_documents AS
SELECT doc_id,
title,
owner_emp_no,
dept_code,
contents,
created_at
FROM cb_search_documents;
PROMPT === 2. Creating user/role/permission tables ===
CREATE TABLE cb_app_user (
user_id NUMBER PRIMARY KEY,
user_name VARCHAR2(50) NOT NULL,
employee_no VARCHAR2(20) NOT NULL,
dept_code VARCHAR2(20) NOT NULL,
stakeholder_user_id VARCHAR2(30),
can_read_contents CHAR(1) DEFAULT 'N' CHECK (can_read_contents IN ('Y','N')) NOT NULL,
active CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL
);
CREATE TABLE cb_app_role (
role_id NUMBER PRIMARY KEY,
role_name VARCHAR2(50) NOT NULL,
max_sensitivity_level VARCHAR2(20) DEFAULT 'PUBLIC' NOT NULL
);
CREATE TABLE cb_user_role (
user_id NUMBER NOT NULL REFERENCES cb_app_user(user_id),
role_id NUMBER NOT NULL REFERENCES cb_app_role(role_id),
CONSTRAINT cb_user_role_pk PRIMARY KEY (user_id, role_id)
);
CREATE TABLE cb_permission (
perm_id NUMBER PRIMARY KEY,
role_id NUMBER NOT NULL REFERENCES cb_app_role(role_id),
target_name VARCHAR2(128) NOT NULL,
action_name VARCHAR2(30) NOT NULL,
permission_effect VARCHAR2(10) DEFAULT 'ALLOW' NOT NULL
);
CREATE TABLE cb_permission_rule (
rule_id NUMBER PRIMARY KEY,
perm_id NUMBER NOT NULL REFERENCES cb_permission(perm_id),
rule_column VARCHAR2(128),
rule_type VARCHAR2(30) NOT NULL,
rule_value VARCHAR2(1000 CHAR)
);
CREATE TABLE cb_permission_column (
permission_id NUMBER NOT NULL REFERENCES cb_permission(perm_id),
column_name VARCHAR2(128) NOT NULL,
CONSTRAINT cb_permission_column_pk PRIMARY KEY (permission_id, column_name)
);
CREATE TABLE cb_agent_bearer_key (
key_id NUMBER PRIMARY KEY,
user_id NUMBER NOT NULL REFERENCES cb_app_user(user_id),
stakeholder_user_id VARCHAR2(30),
key_hash VARCHAR2(128) NOT NULL UNIQUE,
key_prefix VARCHAR2(16),
issued_at DATE DEFAULT SYSDATE NOT NULL,
expires_at DATE,
revoked_at DATE,
active CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL
);
PROMPT === 3. Seeding permissions ===
INSERT INTO cb_app_user VALUES (101, '이에이치알', 'E10234', 'HR', NULL, 'N', 'Y');
INSERT INTO cb_app_user VALUES (102, '박파이넨스', 'E2001', 'FIN', NULL, 'N', 'Y');
INSERT INTO cb_app_user VALUES (103, '김어드민', 'E99999', 'HQ', NULL, 'Y', 'Y');
INSERT INTO cb_app_role VALUES (10, 'HR_DEPT_ROLE', 'INTERNAL');
INSERT INTO cb_app_role VALUES (20, 'FIN_SELF_ROLE', 'INTERNAL');
INSERT INTO cb_app_role VALUES (30, 'ALL_DOC_ROLE', 'CONFIDENTIAL');
INSERT INTO cb_user_role VALUES (101, 10);
INSERT INTO cb_user_role VALUES (102, 20);
INSERT INTO cb_user_role VALUES (103, 30);
INSERT INTO cb_permission VALUES (100, 10, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission VALUES (200, 20, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission VALUES (300, 30, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission_rule (rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (1000, 100, NULL, 'MY_DEPT', NULL);
INSERT INTO cb_permission_rule (rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (2000, 200, NULL, 'SELF', NULL);
INSERT INTO cb_permission_rule (rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (3000, 300, NULL, 'ALL', NULL);
INSERT INTO cb_permission_column VALUES (300, 'CONTENTS');
INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix)
VALUES (1, 101, STANDARD_HASH('cb_hr_key', 'SHA256'), 'cb_hr');
INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix)
VALUES (2, 102, STANDARD_HASH('cb_fin_key', 'SHA256'), 'cb_fin');
INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix)
VALUES (3, 103, STANDARD_HASH('cb_all_key', 'SHA256'), 'cb_all');
COMMIT;
PROMPT === 4. Creating secure application context package ===
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 CONTEXT cb_agent_ctx USING cb_agent_ctx_pkg;
CREATE OR REPLACE PACKAGE BODY cb_agent_ctx_pkg AS
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);
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);
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;
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
INTO v_user_id, v_emp_no, v_dept_code, v_can_read_contents
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);
EXCEPTION
WHEN NO_DATA_FOUND THEN
clear_user;
RAISE_APPLICATION_ERROR(-20002, 'Invalid or expired Bearer key');
END;
END;
/
SHOW ERRORS
PROMPT === 5. Creating VPD policy function ===
CREATE OR REPLACE FUNCTION cb_agent_doc_vpd_filter(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
v_user_id NUMBER;
v_target VARCHAR2(128);
v_allow_predicate VARCHAR2(32767);
v_deny_predicate VARCHAR2(32767);
FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
RETURN '''' || REPLACE(NVL(p_value, ''), '''', '''''') || '''';
END;
FUNCTION safe_column(
p_owner IN VARCHAR2,
p_table_name IN VARCHAR2,
p_column_name IN VARCHAR2
) RETURN VARCHAR2 IS
v_column VARCHAR2(128);
v_count NUMBER;
BEGIN
IF p_column_name IS NULL THEN
RETURN NULL;
END IF;
v_column := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(TRIM(p_column_name)));
SELECT COUNT(*)
INTO v_count
FROM all_tab_columns
WHERE owner = UPPER(p_owner)
AND table_name = UPPER(p_table_name)
AND column_name = v_column;
IF v_count = 0 THEN
RETURN NULL;
END IF;
RETURN v_column;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
PROCEDURE append_or(p_effect IN VARCHAR2, p_clause IN VARCHAR2) IS
BEGIN
IF p_clause IS NULL THEN
RETURN;
END IF;
IF p_effect = 'DENY' THEN
IF v_deny_predicate IS NULL THEN
v_deny_predicate := '(' || p_clause || ')';
ELSE
v_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')';
END IF;
ELSIF v_allow_predicate IS NULL THEN
v_allow_predicate := '(' || p_clause || ')';
ELSE
v_allow_predicate := v_allow_predicate || ' OR (' || p_clause || ')';
END IF;
END;
BEGIN
BEGIN
v_user_id := TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'));
EXCEPTION
WHEN OTHERS THEN
RETURN '1 = 0';
END;
IF v_user_id IS NULL OR p_schema IS NULL OR p_object IS NULL THEN
RETURN '1 = 0';
END IF;
v_target := UPPER(TRIM(p_object));
FOR r IN (
SELECT UPPER(TRIM(r.rule_type)) AS rule_type,
UPPER(TRIM(r.rule_column)) AS rule_column,
NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') AS permission_effect,
r.rule_value
FROM cb_user_role ur
JOIN cb_permission p
ON p.role_id = ur.role_id
JOIN cb_permission_rule r
ON r.perm_id = p.perm_id
WHERE ur.user_id = v_user_id
AND p.target_name = v_target
AND p.action_name = 'SELECT'
ORDER BY r.rule_id
) LOOP
DECLARE
v_column VARCHAR2(128);
BEGIN
IF r.rule_type = 'ALL' THEN
IF r.permission_effect = 'DENY' THEN
RETURN '1 = 0';
END IF;
v_allow_predicate := '(1 = 1)';
ELSIF r.rule_type = 'MY_DEPT' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
IF v_column IS NOT NULL THEN
append_or(r.permission_effect, v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
END IF;
ELSIF r.rule_type = 'SELF' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
IF v_column IS NOT NULL THEN
append_or(r.permission_effect, v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
END IF;
ELSIF r.rule_type = 'DEPT' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
IF v_column IS NOT NULL THEN
append_or(r.permission_effect, v_column || ' = ' || quote_literal(r.rule_value));
END IF;
ELSIF r.rule_type = 'EMP_NO' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
IF v_column IS NOT NULL THEN
append_or(r.permission_effect, v_column || ' = ' || quote_literal(r.rule_value));
END IF;
ELSIF r.rule_type = 'TAG' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'TECH_TAG'));
IF v_column IS NOT NULL
AND r.rule_value IS NOT NULL
AND REGEXP_LIKE(TRIM(r.rule_value), '^[A-Za-z0-9_-]+$') THEN
append_or(
r.permission_effect,
'REGEXP_LIKE(UPPER(' || v_column || '), ' ||
quote_literal('(^|,)' || UPPER(TRIM(r.rule_value)) || '(,|$)') || ')'
);
END IF;
ELSIF r.rule_type = '=' THEN
v_column := safe_column(p_schema, p_object, r.rule_column);
IF v_column IS NOT NULL THEN
append_or(r.permission_effect, 'TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value));
END IF;
ELSIF r.rule_type IN ('!=', '<>') THEN
v_column := safe_column(p_schema, p_object, r.rule_column);
IF v_column IS NOT NULL THEN
append_or(r.permission_effect, 'TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value));
END IF;
END IF;
END;
END LOOP;
IF v_allow_predicate IS NULL THEN
RETURN '1 = 0';
END IF;
IF v_deny_predicate IS NULL THEN
IF v_allow_predicate = '(1 = 1)' THEN
RETURN '1 = 1';
END IF;
RETURN '(' || v_allow_predicate || ')';
END IF;
RETURN '((' || v_allow_predicate || ') AND NOT (' || v_deny_predicate || '))';
END;
/
SHOW ERRORS
PROMPT === 6. Attaching VPD policy to the protected view ===
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_AGENT_DOC_POLICY',
function_schema => USER,
policy_function => 'CB_AGENT_DOC_VPD_FILTER',
statement_types => 'SELECT',
enable => TRUE
);
END;
/
PROMPT === 7. Adding column redaction on CONTENTS ===
CREATE OR REPLACE FUNCTION cb_agent_can_read_column(
p_target_name IN VARCHAR2,
p_column_name IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
v_allowed NUMBER;
BEGIN
IF SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID') IS NULL THEN
RETURN 'N';
END IF;
SELECT COUNT(*)
INTO v_allowed
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 = TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'))
AND p.target_name = UPPER(p_target_name)
AND p.action_name = 'SELECT'
AND pc.column_name = UPPER(p_column_name);
RETURN CASE WHEN v_allowed > 0 THEN 'Y' ELSE 'N' END;
END;
/
SHOW ERRORS
BEGIN
DBMS_REDACT.ADD_POLICY(
object_schema => USER,
object_name => 'CB_V_SEARCH_DOCUMENTS',
column_name => 'CONTENTS',
policy_name => 'CB_CONTENTS_REDACT',
function_type => DBMS_REDACT.NULLIFY,
expression => 'SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') != ''Y'''
);
END;
/
PROMPT === 8. Creating ORDS runtime schema ===
CREATE USER cb_ords IDENTIFIED BY "CbOrdS#2026Local1";
GRANT CREATE SESSION TO cb_ords;
GRANT CREATE PROCEDURE TO cb_ords;
GRANT SELECT ON cb_v_search_documents TO cb_ords;
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords;
GRANT EXECUTE ON cb_agent_can_read_column TO cb_ords;
PROMPT === Local VPD setup 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,196 @@
-- ============================================================
-- 22_agent_ords_security_ords_handler_setup.sql
-- ORDS module/handler package for Agent ORDS security.
--
-- Run as CB_ORDS after:
-- * ADMIN VPD setup
-- * ADMIN DDS setup
-- * ADMIN ORDS schema enablement
--
-- Purpose:
-- * Common package: Authorization header -> Bearer key -> SYS_CONTEXT.
-- * Object query handlers: call the common package, then query their
-- own protected table/view.
-- * VPD demo path: a fixed demo handler for ADMIN.CB_V_SEARCH_DOCUMENTS.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
PROMPT === Creating CB_ORDS handler package ===
CREATE OR REPLACE PACKAGE cb_ords_handler_pkg AUTHID DEFINER AS
PROCEDURE set_vpd_context(
p_authorization IN VARCHAR2,
p_probe_id IN VARCHAR2 DEFAULT NULL
);
FUNCTION set_vpd_context_sql(p_authorization IN VARCHAR2) RETURN NUMBER;
PROCEDURE clear_vpd_context;
END;
/
CREATE OR REPLACE PACKAGE BODY cb_ords_handler_pkg AS
FUNCTION extract_bearer_key(p_authorization IN VARCHAR2) RETURN VARCHAR2 AS
v_auth VARCHAR2(4000);
BEGIN
v_auth := TRIM(p_authorization);
IF v_auth IS NULL OR LOWER(SUBSTR(v_auth, 1, 7)) != 'bearer ' THEN
RAISE_APPLICATION_ERROR(-20101, 'Authorization header must be Bearer <key>');
END IF;
RETURN TRIM(SUBSTR(v_auth, 8));
END;
PROCEDURE set_vpd_context(
p_authorization IN VARCHAR2,
p_probe_id IN VARCHAR2 DEFAULT NULL
) AS
v_bearer_key VARCHAR2(4000);
BEGIN
DBMS_SESSION.CLEAR_IDENTIFIER;
admin.cb_agent_ctx_pkg.clear_user;
v_bearer_key := extract_bearer_key(p_authorization);
admin.cb_agent_ctx_pkg.set_user_by_bearer(v_bearer_key);
IF TRIM(p_probe_id) IS NOT NULL THEN
DBMS_SESSION.SET_IDENTIFIER(SUBSTR(TRIM(p_probe_id), 1, 64));
END IF;
END;
FUNCTION set_vpd_context_sql(p_authorization IN VARCHAR2) RETURN NUMBER AS
BEGIN
set_vpd_context(p_authorization);
RETURN 1;
END;
PROCEDURE clear_vpd_context AS
BEGIN
admin.cb_agent_ctx_pkg.clear_user;
DBMS_SESSION.CLEAR_IDENTIFIER;
END;
END;
/
SHOW ERRORS
PROMPT === Defining ORDS module and handlers ===
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'cb.agent.security');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'cb.agent.security',
p_base_path => 'cb-agent-security/',
p_items_per_page => 25,
p_status => 'PUBLISHED'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'cb.agent.security',
p_pattern => 'vpd/documents'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'cb.agent.security',
p_pattern => 'vpd/documents',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_source => q'!
DECLARE
v_rows SYS_REFCURSOR;
v_vpd_predicate VARCHAR2(32767);
v_effective_sql VARCHAR2(32767);
BEGIN
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
-- The predicate below is the value returned by the same DBMS_RLS policy
-- function that protects ADMIN.CB_V_SEARCH_DOCUMENTS.
BEGIN
v_vpd_predicate := admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS');
v_effective_sql := 'SELECT d.doc_id, d.title, d.owner_emp_no, d.dept_code, d.contents '
|| 'FROM admin.cb_v_search_documents d WHERE (' || v_vpd_predicate || ') '
|| 'AND ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500)';
EXCEPTION
WHEN OTHERS THEN
v_vpd_predicate := NULL;
v_effective_sql := NULL;
END;
OPEN v_rows FOR
SELECT d.doc_id,
d.title,
d.owner_emp_no,
d.dept_code,
d.contents
FROM admin.cb_v_search_documents d
WHERE ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500);
: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;
IF v_effective_sql IS NOT NULL THEN
APEX_JSON.WRITE('vpd_predicate', v_vpd_predicate);
APEX_JSON.WRITE('effective_sql', v_effective_sql);
END IF;
APEX_JSON.WRITE('items', v_rows);
APEX_JSON.CLOSE_OBJECT;
cb_ords_handler_pkg.clear_vpd_context;
EXCEPTION
WHEN OTHERS THEN
cb_ords_handler_pkg.clear_vpd_context;
:status_code := 403;
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('error', SQLERRM);
APEX_JSON.CLOSE_OBJECT;
END;
!',
p_items_per_page => 0
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'cb.agent.security',
p_pattern => 'vpd/documents',
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 => 'cb.agent.security',
p_pattern => 'vpd/documents',
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'
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'cb.agent.security',
p_pattern => 'vpd/documents',
p_method => 'POST',
p_name => 'limit',
p_bind_variable_name => 'row_limit',
p_source_type => 'URI',
p_param_type => 'INT',
p_access_method => 'IN'
);
COMMIT;
END;
/
PROMPT === ORDS handler setup complete ===
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,418 @@
-- ============================================================
-- 25_agent_ords_security_backoffice_support.sql
-- Support objects for the Spring Boot VPD/ORDS backoffice.
--
-- Run as ADMIN after 17_agent_ords_security_local_vpd_setup.sql.
-- This script keeps the existing CB_* VPD model and adds only the
-- metadata/audit objects needed by the backoffice UI.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
ALTER SESSION DISABLE PARALLEL DML;
PROMPT === Adding optional description column to bearer keys ===
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_agent_bearer_key ADD (description VARCHAR2(200))';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN -- ORA-01430: column being added already exists
RAISE;
END IF;
END;
/
PROMPT === Creating backoffice sequences ===
DECLARE
PROCEDURE create_seq(p_name IN VARCHAR2, p_start IN NUMBER) AS
BEGIN
EXECUTE IMMEDIATE
'CREATE SEQUENCE ' || p_name || ' START WITH ' || p_start || ' INCREMENT BY 1 NOCACHE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
BEGIN
create_seq('cb_agent_bearer_key_seq', 1000);
create_seq('cb_permission_seq', 1000);
create_seq('cb_permission_rule_seq', 10000);
create_seq('cb_ords_probe_audit_seq', 1);
END;
/
PROMPT === Creating protected object whitelist ===
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_permission_rule ADD (rule_column VARCHAR2(128))';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_permission ADD (permission_effect VARCHAR2(10) DEFAULT ''ALLOW'' NOT NULL)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_app_role ADD (max_sensitivity_level VARCHAR2(20) DEFAULT ''PUBLIC'' NOT NULL)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
PROMPT === Creating group role support tables ===
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_app_group (
group_id NUMBER PRIMARY KEY,
group_code VARCHAR2(100) NOT NULL UNIQUE,
group_name VARCHAR2(100) NOT NULL,
description VARCHAR2(200),
active_yn CHAR(1) DEFAULT ''Y'' CHECK (active_yn IN (''Y'',''N'')) NOT NULL
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_user_group (
group_id NUMBER NOT NULL,
user_id NUMBER NOT NULL,
CONSTRAINT cb_user_group_pk PRIMARY KEY (group_id, user_id)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_group_role (
group_id NUMBER NOT NULL,
role_id NUMBER NOT NULL,
CONSTRAINT cb_group_role_pk PRIMARY KEY (group_id, role_id)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_protected_object (
object_id NUMBER PRIMARY KEY,
owner VARCHAR2(128) NOT NULL,
object_name VARCHAR2(128) NOT NULL UNIQUE,
ords_path VARCHAR2(300) NOT NULL,
enabled_yn CHAR(1) DEFAULT ''Y'' CHECK (enabled_yn IN (''Y'',''N'')) NOT NULL,
description VARCHAR2(200)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_protected_object ADD (description VARCHAR2(200))';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_protected_column (
column_id NUMBER PRIMARY KEY,
object_id NUMBER NOT NULL REFERENCES cb_protected_object(object_id),
column_name VARCHAR2(128) NOT NULL,
sensitive_yn CHAR(1) DEFAULT ''N'' CHECK (sensitive_yn IN (''Y'',''N'')) NOT NULL,
visible_role_id NUMBER,
sensitivity_level VARCHAR2(20) DEFAULT ''PUBLIC'' NOT NULL,
redaction_method VARCHAR2(20) DEFAULT ''NONE'' NOT NULL,
CONSTRAINT cb_protected_column_uk UNIQUE (object_id, column_name)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_protected_column ADD (sensitivity_level VARCHAR2(20) DEFAULT ''PUBLIC'' NOT NULL)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_protected_column ADD (redaction_method VARCHAR2(20) DEFAULT ''NONE'' NOT NULL)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
UPDATE cb_protected_column
SET sensitivity_level = CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END,
redaction_method = CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END
WHERE sensitivity_level = 'PUBLIC'
AND redaction_method = 'NONE'
AND sensitive_yn = 'Y';
-- Autonomous Database can use parallel DML. Finish the compatibility update
-- before merging the seed rows into the same table.
COMMIT;
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_permission_column (
permission_id NUMBER NOT NULL,
column_name VARCHAR2(128) NOT NULL,
CONSTRAINT cb_permission_column_pk PRIMARY KEY (permission_id, column_name)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_ords_probe_audit (
audit_id NUMBER PRIMARY KEY,
event_type VARCHAR2(50) NOT NULL,
key_id NUMBER,
object_id NUMBER,
status VARCHAR2(50),
row_count NUMBER,
error_code VARCHAR2(100),
message VARCHAR2(500),
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_vpd_policy_note (
object_owner VARCHAR2(128) NOT NULL,
object_name VARCHAR2(128) NOT NULL,
policy_name VARCHAR2(128) NOT NULL,
description VARCHAR2(500) NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT cb_vpd_policy_note_pk PRIMARY KEY (object_owner, object_name, policy_name)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_vpd_filter_note (
function_owner VARCHAR2(128) NOT NULL,
function_name VARCHAR2(128) NOT NULL,
description VARCHAR2(500) NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT cb_vpd_filter_note_pk PRIMARY KEY (function_owner, function_name)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
PROMPT === Creating backoffice settings ===
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE cb_backoffice_setting (
setting_key VARCHAR2(100) PRIMARY KEY,
setting_value VARCHAR2(1000),
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
MERGE INTO cb_backoffice_setting dst
USING (
SELECT 'ORDS_BASE_URL' setting_key,
'https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords' setting_value
FROM dual
) src
ON (dst.setting_key = src.setting_key)
WHEN MATCHED THEN UPDATE SET dst.setting_value = src.setting_value
WHERE dst.setting_value IS NULL OR TRIM(dst.setting_value) IS NULL
WHEN NOT MATCHED THEN INSERT (setting_key, setting_value, updated_at)
VALUES (src.setting_key, src.setting_value, SYSTIMESTAMP);
PROMPT === Seeding default VPD ORDS protected object ===
MERGE INTO cb_protected_object dst
USING (
SELECT 1 AS object_id,
'ADMIN' AS owner,
'CB_V_SEARCH_DOCUMENTS' AS object_name,
'cb-ords/cb-agent-security/vpd/documents' AS ords_path,
'Y' AS enabled_yn
FROM dual
) src
ON (dst.object_id = src.object_id)
WHEN MATCHED THEN UPDATE SET
dst.owner = src.owner,
dst.object_name = src.object_name,
dst.ords_path = src.ords_path,
dst.enabled_yn = src.enabled_yn
WHEN NOT MATCHED THEN INSERT (object_id, owner, object_name, ords_path, enabled_yn)
VALUES (src.object_id, src.owner, src.object_name, src.ords_path, src.enabled_yn);
MERGE INTO cb_protected_column dst
USING (
SELECT 1 column_id, 1 object_id, 'DOC_ID' column_name, 'N' sensitive_yn, 'PUBLIC' sensitivity_level, 'NONE' redaction_method FROM dual UNION ALL
SELECT 2, 1, 'TITLE', 'N', 'PUBLIC', 'NONE' FROM dual UNION ALL
SELECT 3, 1, 'OWNER_EMP_NO', 'N', 'INTERNAL', 'NONE' FROM dual UNION ALL
SELECT 4, 1, 'DEPT_CODE', 'N', 'INTERNAL', 'NONE' FROM dual UNION ALL
SELECT 5, 1, 'CONTENTS', 'Y', 'CONFIDENTIAL', 'NULLIFY' FROM dual
) src
ON (dst.object_id = src.object_id AND dst.column_name = src.column_name)
WHEN MATCHED THEN UPDATE SET
dst.sensitive_yn = src.sensitive_yn,
dst.sensitivity_level = src.sensitivity_level,
dst.redaction_method = src.redaction_method
WHEN NOT MATCHED THEN INSERT (
column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method
)
VALUES (
src.column_id, src.object_id, src.column_name, src.sensitive_yn, src.sensitivity_level, src.redaction_method
);
MERGE INTO cb_permission_column dst
USING (
SELECT 300 permission_id, 'CONTENTS' column_name FROM dual
) src
ON (dst.permission_id = src.permission_id AND dst.column_name = src.column_name)
WHEN NOT MATCHED THEN INSERT (permission_id, column_name)
VALUES (src.permission_id, src.column_name);
PROMPT === Replacing CONTENTS redaction with role column permission check ===
CREATE OR REPLACE FUNCTION cb_agent_can_read_column(
p_target_name IN VARCHAR2,
p_column_name IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
v_allowed NUMBER;
BEGIN
IF SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID') IS NULL THEN
RETURN 'N';
END IF;
SELECT COUNT(*)
INTO v_allowed
FROM (
SELECT ur.role_id
FROM cb_user_role ur
WHERE ur.user_id = TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'))
UNION
SELECT gr.role_id
FROM cb_user_group ug
JOIN cb_app_group g
ON g.group_id = ug.group_id
AND g.active_yn = 'Y'
JOIN cb_group_role gr
ON gr.group_id = ug.group_id
WHERE ug.user_id = TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'))
) er
JOIN cb_permission p
ON p.role_id = er.role_id
JOIN cb_permission_column pc
ON pc.permission_id = p.perm_id
WHERE p.target_name = UPPER(p_target_name)
AND p.action_name = 'SELECT'
AND pc.column_name = UPPER(p_column_name);
RETURN CASE WHEN v_allowed > 0 THEN 'Y' ELSE 'N' END;
END;
/
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_REDACT.ADD_POLICY(
object_schema => USER,
object_name => 'CB_V_SEARCH_DOCUMENTS',
column_name => 'CONTENTS',
policy_name => 'CB_CONTENTS_REDACT',
function_type => DBMS_REDACT.NULLIFY,
expression => 'SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') != ''Y'''
);
END;
/
GRANT EXECUTE ON cb_agent_can_read_column TO cb_ords;
COMMIT;
PROMPT === Backoffice support setup complete ===
EXIT;

View File

@@ -0,0 +1,379 @@
-- ============================================================
-- 26_agent_ords_security_dynamic_vpd_filter.sql
-- Replaces the demo VPD filter with a whitelist predicate builder.
--
-- The function does not hardcode object names or rule values. It reads:
-- CB_AGENT_CTX.USER_ID -> direct CB_USER_ROLE + group CB_USER_GROUP/CB_GROUP_ROLE
-- -> CB_PERMISSION -> CB_PERMISSION_RULE
-- and builds a row predicate for the object passed by DBMS_RLS.
-- A permission's rules are combined with AND. Separate ALLOW permissions are
-- combined with OR and DENY permissions are subtracted at the end. This lets
-- a token-context condition be narrowed by a literal static SQL condition.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Creating dynamic whitelist VPD filter ===
CREATE OR REPLACE FUNCTION cb_agent_doc_vpd_filter(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
-- This function is intentionally whitelist-first:
-- no matched stored row rule means no rows are visible.
v_user_id NUMBER;
v_target VARCHAR2(128);
v_allow_predicate VARCHAR2(32767);
v_deny_predicate VARCHAR2(32767);
v_active_permission_id NUMBER;
v_active_permission_effect VARCHAR2(10);
v_permission_predicate VARCHAR2(32767);
v_permission_invalid BOOLEAN := FALSE;
FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
RETURN '''' || REPLACE(NVL(p_value, ''), '''', '''''') || '''';
END;
FUNCTION safe_column(
p_owner IN VARCHAR2,
p_table_name IN VARCHAR2,
p_column_name IN VARCHAR2
) RETURN VARCHAR2 IS
v_column VARCHAR2(128);
v_count NUMBER;
BEGIN
IF p_column_name IS NULL THEN
RETURN NULL;
END IF;
v_column := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(TRIM(p_column_name)));
SELECT COUNT(*)
INTO v_count
FROM all_tab_columns
WHERE owner = UPPER(p_owner)
AND table_name = UPPER(p_table_name)
AND column_name = v_column;
IF v_count = 0 THEN
RETURN NULL;
END IF;
RETURN v_column;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
FUNCTION safe_static_predicate(
p_owner IN VARCHAR2,
p_table_name IN VARCHAR2,
p_predicate IN VARCHAR2
) RETURN VARCHAR2 IS
v_predicate VARCHAR2(32767) := TRIM(p_predicate);
v_lexical VARCHAR2(32767);
v_token VARCHAR2(128);
v_index PLS_INTEGER := 1;
v_count NUMBER;
v_has_column BOOLEAN := FALSE;
BEGIN
-- STATIC_SQL is intentionally an expression only: no statement
-- separator/comment/bind/database-link/qualified object is accepted.
-- Every identifier outside a string literal must be a column of the
-- protected object or an allowlisted SQL operator/function.
IF v_predicate IS NULL
OR LENGTH(v_predicate) > 1000
OR INSTR(v_predicate, ';') > 0
OR INSTR(v_predicate, '--') > 0
OR INSTR(v_predicate, '/*') > 0
OR INSTR(v_predicate, '*/') > 0
OR INSTR(v_predicate, ':') > 0
OR INSTR(v_predicate, '@') > 0
OR INSTR(v_predicate, '||') > 0
OR REGEXP_LIKE(v_predicate, '[[:cntrl:]]')
OR MOD(LENGTH(v_predicate) - LENGTH(REPLACE(v_predicate, '''', '')), 2) <> 0 THEN
RETURN NULL;
END IF;
-- The unconditional predicate is a deliberately narrow exception to the
-- column-reference rule below. It is used by the explicit KB VPD admin
-- permission and is rendered by the backoffice as the physical condition
-- "1 = 1", not as an implicit bypass.
IF REGEXP_LIKE(v_predicate, '^1[[:space:]]*=[[:space:]]*1$') THEN
RETURN '1 = 1';
END IF;
-- Replace SQL string literals before extracting identifiers. Doubled
-- quotes in a literal are supported by the expression below.
v_lexical := REGEXP_REPLACE(v_predicate, '''(''''|[^''])*''', ' ');
IF REGEXP_LIKE(v_lexical, '[[:alpha:]_$#][[:alnum:]_$#]*[[:space:]]*\.') THEN
RETURN NULL;
END IF;
LOOP
v_token := UPPER(REGEXP_SUBSTR(
v_lexical, '[[:alpha:]_$#][[:alnum:]_$#]*', 1, v_index
));
EXIT WHEN v_token IS NULL;
v_index := v_index + 1;
IF v_token IN (
'AND', 'OR', 'NOT', 'NULL', 'IS', 'IN', 'LIKE', 'BETWEEN', 'ESCAPE',
'TRUE', 'FALSE', 'UPPER', 'LOWER', 'TRIM', 'NVL', 'COALESCE',
'TO_CHAR', 'TO_DATE', 'REGEXP_LIKE', 'INSTR', 'LENGTH', 'SUBSTR',
'REPLACE', 'CAST', 'AS', 'DATE', 'TIMESTAMP', 'NUMBER', 'VARCHAR2', 'CHAR'
) THEN
NULL;
ELSE
SELECT COUNT(*)
INTO v_count
FROM all_tab_columns
WHERE owner = UPPER(p_owner)
AND table_name = UPPER(p_table_name)
AND column_name = v_token;
IF v_count = 0 THEN
RETURN NULL;
END IF;
v_has_column := TRUE;
END IF;
END LOOP;
RETURN CASE WHEN v_has_column THEN v_predicate ELSE NULL END;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
PROCEDURE append_permission(p_effect IN VARCHAR2, p_clause IN VARCHAR2) IS
BEGIN
IF p_clause IS NULL THEN
RETURN;
END IF;
IF p_effect = 'DENY' THEN
IF v_deny_predicate IS NULL THEN
v_deny_predicate := p_clause;
ELSE
v_deny_predicate := '(' || v_deny_predicate || ') OR (' || p_clause || ')';
END IF;
ELSIF v_allow_predicate IS NULL THEN
v_allow_predicate := p_clause;
ELSE
v_allow_predicate := '(' || v_allow_predicate || ') OR (' || p_clause || ')';
END IF;
END;
PROCEDURE append_rule(p_clause IN VARCHAR2) IS
BEGIN
IF p_clause IS NULL THEN
v_permission_invalid := TRUE;
RETURN;
END IF;
IF v_permission_predicate IS NULL THEN
v_permission_predicate := p_clause;
ELSE
v_permission_predicate := '(' || v_permission_predicate || ') AND (' || p_clause || ')';
END IF;
END;
PROCEDURE flush_permission IS
BEGIN
IF NOT v_permission_invalid THEN
append_permission(v_active_permission_effect, v_permission_predicate);
END IF;
v_permission_predicate := NULL;
v_permission_invalid := FALSE;
END;
BEGIN
BEGIN
v_user_id := TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'));
EXCEPTION
WHEN OTHERS THEN
RETURN '1 = 0';
END;
IF v_user_id IS NULL OR p_schema IS NULL OR p_object IS NULL THEN
RETURN '1 = 0';
END IF;
v_target := UPPER(TRIM(p_object));
FOR r IN (
WITH effective_role AS (
SELECT ur.role_id
FROM cb_user_role ur
WHERE ur.user_id = v_user_id
UNION
SELECT gr.role_id
FROM cb_user_group ug
JOIN cb_app_group g
ON g.group_id = ug.group_id
AND g.active_yn = 'Y'
JOIN cb_group_role gr
ON gr.group_id = ug.group_id
WHERE ug.user_id = v_user_id
)
SELECT p.perm_id,
UPPER(TRIM(r.rule_type)) AS rule_type,
UPPER(TRIM(r.rule_column)) AS rule_column,
NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') AS permission_effect,
r.rule_value
FROM effective_role er
JOIN cb_permission p
ON p.role_id = er.role_id
JOIN cb_permission_rule r
ON r.perm_id = p.perm_id
WHERE p.target_name = v_target
AND p.action_name = 'SELECT'
ORDER BY p.perm_id, r.rule_id
) LOOP
IF v_active_permission_id IS NULL THEN
v_active_permission_id := r.perm_id;
v_active_permission_effect := r.permission_effect;
ELSIF v_active_permission_id <> r.perm_id THEN
flush_permission;
v_active_permission_id := r.perm_id;
v_active_permission_effect := r.permission_effect;
END IF;
DECLARE
v_column VARCHAR2(128);
v_static_predicate VARCHAR2(32767);
BEGIN
IF r.rule_type = 'ALL' THEN
append_rule('1 = 1');
ELSIF r.rule_type = 'MY_DEPT' THEN
-- NULL rule_column keeps backward compatibility with the demo seed.
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
IF v_column IS NOT NULL THEN
append_rule(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
END IF;
ELSIF r.rule_type = 'SELF' THEN
-- NULL rule_column keeps backward compatibility with the demo seed.
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
IF v_column IS NOT NULL THEN
append_rule(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
END IF;
ELSIF r.rule_type = 'DEPT' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
IF v_column IS NOT NULL THEN
append_rule(v_column || ' = ' || quote_literal(r.rule_value));
END IF;
ELSIF r.rule_type = 'EMP_NO' THEN
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
IF v_column IS NOT NULL THEN
append_rule(v_column || ' = ' || quote_literal(r.rule_value));
END IF;
ELSIF r.rule_type = 'TAG' THEN
-- A TAG rule defaults to the ingestion metadata column TECH_TAG.
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'TECH_TAG'));
IF v_column IS NOT NULL
AND r.rule_value IS NOT NULL
AND REGEXP_LIKE(TRIM(r.rule_value), '^[A-Za-z0-9_-]+$') THEN
append_rule(
'REGEXP_LIKE(UPPER(' || v_column || '), ' ||
quote_literal('(^|,)' || UPPER(TRIM(r.rule_value)) || '(,|$)') || ')'
);
END IF;
ELSIF r.rule_type IN (
'TOKEN_SUBJECT', 'OWN_CONTRACT', 'CHANNEL_CONTRACT', 'OWN_CUSTOMER', 'CHANNEL_CUSTOMER'
) THEN
-- 백오피스 권한 설정의 조건 코드를 안전한 WHERE 조각으로 확장한다.
-- token -> stakeholder 해석은 context package가 한 번만 수행하며,
-- VPD filter는 context와 대상/계약원장만 쓴다.
v_column := safe_column(p_schema, p_object, r.rule_column);
IF v_column IS NOT NULL
AND UPPER(p_schema) = 'POC_2' THEN
IF r.rule_type = 'TOKEN_SUBJECT'
AND v_target = 'KB_STAKEHOLDERS'
AND v_column = 'USER_ID' THEN
append_rule(
'USER_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')'
);
ELSIF r.rule_type = 'OWN_CONTRACT'
AND v_target = 'KB_CONTRACTS'
AND v_column = 'FC_ID' THEN
append_rule(
'FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')'
);
ELSIF r.rule_type = 'CHANNEL_CONTRACT'
AND v_target = 'KB_CONTRACTS'
AND v_column = 'FC_CHANNEL' THEN
append_rule(
'FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')'
);
ELSIF r.rule_type IN ('OWN_CUSTOMER', 'CHANNEL_CUSTOMER')
AND v_target IN ('KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
AND v_column IN ('CUST_ID', 'CONTRACT_NO') THEN
DECLARE
v_contract_key VARCHAR2(30);
v_subject_filter VARCHAR2(500);
BEGIN
v_contract_key := CASE v_column
WHEN 'CUST_ID' THEN 'CUST_ID'
WHEN 'CONTRACT_NO' THEN 'CONTRACT_NO'
END;
v_subject_filter := CASE r.rule_type
WHEN 'OWN_CUSTOMER' THEN
'c.FC_ID = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_USER_ID'')'
ELSE
'c.FC_CHANNEL = SYS_CONTEXT(''CB_AGENT_CTX'', ''STAKEHOLDER_CHANNEL'')'
END;
append_rule(
'EXISTS (SELECT 1 FROM (SELECT c.' || v_contract_key || ' AS access_key ' ||
'FROM POC_2.KB_CONTRACTS c WHERE ' || v_subject_filter ||
') allowed_contract WHERE allowed_contract.access_key = ' || v_column || ')'
);
END;
END IF;
END IF;
ELSIF r.rule_type = 'STATIC_SQL' THEN
v_static_predicate := safe_static_predicate(p_schema, p_object, r.rule_value);
append_rule(v_static_predicate);
ELSIF r.rule_type = '=' THEN
v_column := safe_column(p_schema, p_object, r.rule_column);
IF v_column IS NOT NULL THEN
append_rule('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value));
END IF;
ELSIF r.rule_type IN ('!=', '<>') THEN
v_column := safe_column(p_schema, p_object, r.rule_column);
IF v_column IS NOT NULL THEN
append_rule('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value));
END IF;
ELSE
append_rule(NULL);
END IF;
END;
END LOOP;
flush_permission;
IF v_allow_predicate IS NULL THEN
RETURN '1 = 0';
END IF;
IF v_deny_predicate IS NULL THEN
IF v_allow_predicate = '1 = 1' THEN
RETURN '1 = 1';
END IF;
RETURN '(' || v_allow_predicate || ')';
END IF;
RETURN '((' || v_allow_predicate || ') AND NOT (' || v_deny_predicate || '))';
END;
/
SHOW ERRORS FUNCTION cb_agent_doc_vpd_filter
-- ORDS SQL tracing calls the same policy function after set_vpd_context so
-- the response can expose the token-specific VPD predicate. This is
-- diagnostic metadata only; the SELECT remains protected by DBMS_RLS.
GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords;
PROMPT === Dynamic whitelist VPD filter ready ===

View File

@@ -0,0 +1,318 @@
-- ============================================================
-- 27_agent_ords_security_dynamic_vpd_filter_test.sql
-- Unit-style checks for cb_agent_doc_vpd_filter.
--
-- Run after:
-- @database/adb/26_agent_ords_security_dynamic_vpd_filter.sql
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
SET SERVEROUTPUT ON
PROMPT === Preparing dynamic VPD filter unit test data ===
BEGIN
cb_agent_ctx_pkg.clear_user;
DELETE FROM cb_permission_rule
WHERE perm_id BETWEEN 456000 AND 456999;
DELETE FROM cb_permission
WHERE perm_id BETWEEN 456000 AND 456999;
DELETE FROM cb_user_role
WHERE role_id BETWEEN 456000 AND 456999;
DELETE FROM cb_app_role
WHERE role_id BETWEEN 456000 AND 456999;
DELETE FROM cb_app_user
WHERE user_id BETWEEN 456000 AND 456999;
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456101, 'rule_test_hr', 'E456101', 'HR', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456102, 'rule_test_self', 'E2001', 'FIN', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456103, 'rule_test_all', 'E456103', 'HQ', 'Y', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456104, 'rule_test_eq', 'E456104', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456105, 'rule_test_ne', 'E456105', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456106, 'rule_test_bad_column', 'E456106', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456107, 'rule_test_quote_value', 'E456107', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456108, 'rule_test_or_injection', 'E456108', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456109, 'rule_test_column_injection', 'E456109', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456110, 'rule_test_target_mismatch', 'E456110', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456111, 'rule_test_allow_deny', 'E456111', 'HR', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456112, 'rule_test_deny_only', 'E456112', 'HR', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456113, 'rule_test_static_and', 'E456113', 'HR', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (456114, 'rule_test_static_invalid', 'E456114', 'HR', 'N', 'Y');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456101, 'RULE_TEST_MY_DEPT');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456102, 'RULE_TEST_SELF');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456103, 'RULE_TEST_ALL');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456104, 'RULE_TEST_EQ');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456105, 'RULE_TEST_NE');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456106, 'RULE_TEST_BAD_COLUMN');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456107, 'RULE_TEST_QUOTE_VALUE');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456108, 'RULE_TEST_OR_INJECTION');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456109, 'RULE_TEST_COLUMN_INJECTION');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456110, 'RULE_TEST_TARGET_MISMATCH');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456111, 'RULE_TEST_ALLOW_DENY');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456112, 'RULE_TEST_DENY_ONLY');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456113, 'RULE_TEST_STATIC_AND');
INSERT INTO cb_app_role(role_id, role_name) VALUES (456114, 'RULE_TEST_STATIC_INVALID');
INSERT INTO cb_user_role(user_id, role_id) VALUES (456101, 456101);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456102, 456102);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456103, 456103);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456104, 456104);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456105, 456105);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456106, 456106);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456107, 456107);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456108, 456108);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456109, 456109);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456110, 456110);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456111, 456111);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456112, 456112);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456113, 456113);
INSERT INTO cb_user_role(user_id, role_id) VALUES (456114, 456114);
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456101, 456101, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456102, 456102, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456103, 456103, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456104, 456104, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456105, 456105, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456106, 456106, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456107, 456107, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456108, 456108, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456109, 456109, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
VALUES (456110, 456110, 'BOARD_POSTS', 'SELECT');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (456111, 456111, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (456112, 456111, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'DENY');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (456113, 456112, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'DENY');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (456114, 456113, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (456115, 456114, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456101, 456101, NULL, 'MY_DEPT', NULL);
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456102, 456102, NULL, 'SELF', NULL);
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456103, 456103, NULL, 'ALL', NULL);
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456104, 456104, 'DOC_ID', '=', '1');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456105, 456105, 'DEPT_CODE', '!=', 'HR');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456106, 456106, 'NO_SUCH_COLUMN', '=', 'x');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456107, 456107, 'TITLE', '=', 'Bob''s plan');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456108, 456108, 'TITLE', '=', 'HR'' OR ''1''=''1'' --');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456109, 456109, 'DOC_ID) OR 1=1 --', '=', '1');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456110, 456110, NULL, 'ALL', NULL);
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456111, 456111, NULL, 'ALL', NULL);
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456112, 456112, 'DEPT_CODE', '=', 'HR');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456113, 456113, 'DEPT_CODE', '=', 'HR');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456114, 456114, 'DEPT_CODE', '=', 'HR');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456115, 456114, NULL, 'STATIC_SQL', 'OWNER_EMP_NO = ''E456113''');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (456116, 456115, NULL, 'STATIC_SQL', 'DEPT_CODE = ''HR''; 1 = 1');
COMMIT;
END;
/
PROMPT === Running dynamic VPD filter unit checks ===
DECLARE
PROCEDURE fail(p_label IN VARCHAR2, p_expected IN VARCHAR2, p_actual IN VARCHAR2) IS
BEGIN
RAISE_APPLICATION_ERROR(
-20456,
p_label || ' expected [' || p_expected || '] actual [' || p_actual || ']'
);
END;
PROCEDURE assert_equals_for_object(
p_label IN VARCHAR2,
p_user_id IN NUMBER,
p_object IN VARCHAR2,
p_expected IN VARCHAR2
) IS
v_actual VARCHAR2(32767);
BEGIN
IF p_user_id IS NULL THEN
cb_agent_ctx_pkg.clear_user;
ELSE
cb_agent_ctx_pkg.set_user(p_user_id);
END IF;
v_actual := cb_agent_doc_vpd_filter('ADMIN', p_object);
IF v_actual != p_expected THEN
fail(p_label, p_expected, v_actual);
END IF;
DBMS_OUTPUT.PUT_LINE('PASS ' || p_label || ': ' || v_actual);
END;
PROCEDURE assert_equals(
p_label IN VARCHAR2,
p_user_id IN NUMBER,
p_expected IN VARCHAR2
) IS
BEGIN
assert_equals_for_object(p_label, p_user_id, 'CB_V_SEARCH_DOCUMENTS', p_expected);
END;
PROCEDURE assert_contains_for_object(
p_label IN VARCHAR2,
p_user_id IN NUMBER,
p_object IN VARCHAR2,
p_expected IN VARCHAR2
) IS
v_actual VARCHAR2(32767);
BEGIN
cb_agent_ctx_pkg.set_user(p_user_id);
v_actual := cb_agent_doc_vpd_filter('ADMIN', p_object);
IF INSTR(v_actual, p_expected) = 0 THEN
fail(p_label, p_expected, v_actual);
END IF;
DBMS_OUTPUT.PUT_LINE('PASS ' || p_label || ': ' || v_actual);
END;
PROCEDURE assert_contains(
p_label IN VARCHAR2,
p_user_id IN NUMBER,
p_expected IN VARCHAR2
) IS
BEGIN
assert_contains_for_object(p_label, p_user_id, 'CB_V_SEARCH_DOCUMENTS', p_expected);
END;
BEGIN
assert_equals('NO_CONTEXT_DENIES', NULL, '1 = 0');
assert_contains('MY_DEPT_DEFAULT_COLUMN', 456101, 'DEPT_CODE = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
assert_contains('SELF_DEFAULT_COLUMN', 456102, 'OWNER_EMP_NO = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
assert_equals('ALL_ALLOWS', 456103, '1 = 1');
assert_contains('EQUALS_RULE', 456104, 'TO_CHAR(DOC_ID) = ''1''');
assert_contains('NOT_EQUALS_RULE', 456105, 'TO_CHAR(DEPT_CODE) <> ''HR''');
assert_equals('BAD_COLUMN_DENIES', 456106, '1 = 0');
assert_contains('QUOTE_VALUE_ESCAPED', 456107, 'TO_CHAR(TITLE) = ''Bob''''s plan''');
assert_contains('OR_INJECTION_STAYS_LITERAL', 456108, 'TO_CHAR(TITLE) = ''HR'''' OR ''''1''''=''''1'''' --''');
assert_equals('COLUMN_INJECTION_DENIES', 456109, '1 = 0');
assert_equals_for_object('TARGET_MISMATCH_DENIES', 456110, 'CB_V_SEARCH_DOCUMENTS', '1 = 0');
assert_equals_for_object('TARGET_MATCH_ALL_ALLOWS', 456110, 'BOARD_POSTS', '1 = 1');
assert_contains('ALLOW_ALL_MINUS_DENY_DEPT', 456111, 'AND NOT');
assert_contains('ALLOW_ALL_MINUS_DENY_DEPT_DETAIL', 456111, 'TO_CHAR(DEPT_CODE) = ''HR''');
assert_equals('DENY_ONLY_DENIES', 456112, '1 = 0');
assert_contains('CONTEXT_AND_STATIC_SQL', 456113, 'TO_CHAR(DEPT_CODE) = ''HR'') AND (OWNER_EMP_NO = ''E456113'')');
assert_equals('INVALID_STATIC_SQL_DENIES', 456114, '1 = 0');
cb_agent_ctx_pkg.clear_user;
END;
/
PROMPT === Cleaning dynamic VPD filter unit test data ===
BEGIN
cb_agent_ctx_pkg.clear_user;
DELETE FROM cb_permission_rule
WHERE perm_id BETWEEN 456000 AND 456999;
DELETE FROM cb_permission
WHERE perm_id BETWEEN 456000 AND 456999;
DELETE FROM cb_user_role
WHERE role_id BETWEEN 456000 AND 456999;
DELETE FROM cb_app_role
WHERE role_id BETWEEN 456000 AND 456999;
DELETE FROM cb_app_user
WHERE user_id BETWEEN 456000 AND 456999;
COMMIT;
END;
/
PROMPT === Dynamic VPD filter unit checks passed ===

View File

@@ -0,0 +1,268 @@
-- ============================================================
-- 28_agent_ords_vector_tag_vpd_setup.sql
-- Vector/chunk knowledge search data plane with tag-based VPD.
--
-- Run as ADMIN after:
-- * 25_agent_ords_security_backoffice_support.sql
-- * 26_agent_ords_security_dynamic_vpd_filter.sql
--
-- Target: Oracle AI Database 23ai/26ai (VECTOR data type).
-- The embedding is produced by the ingestion pipeline. The database
-- stores the chunk, its vector, and one or more normalized TECH_TAG rows.
-- The VPD policy protects a one-row-per-chunk view whose TECH_TAG value is a
-- comma-delimited tag list, so ALLOW and DENY apply to the whole chunk.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Creating vector chunk and tag tables ===
BEGIN
EXECUTE IMMEDIATE 'CREATE SEQUENCE cb_vector_chunk_seq START WITH 30000 INCREMENT BY 1 NOCACHE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE q'!
CREATE TABLE cb_vector_document_chunk (
chunk_id NUMBER PRIMARY KEY,
document_id VARCHAR2(200) NOT NULL,
chunk_no NUMBER NOT NULL,
title VARCHAR2(500),
chunk_text CLOB NOT NULL,
source_uri VARCHAR2(1000),
embedding VECTOR,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT cb_vector_chunk_uk UNIQUE (document_id, chunk_no)
)!';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE q'!
CREATE TABLE cb_vector_document_tag (
chunk_id NUMBER NOT NULL REFERENCES cb_vector_document_chunk(chunk_id) ON DELETE CASCADE,
tech_tag VARCHAR2(100) NOT NULL,
CONSTRAINT cb_vector_tag_pk PRIMARY KEY (chunk_id, tech_tag),
CONSTRAINT cb_vector_tag_format_ck CHECK (REGEXP_LIKE(tech_tag, '^[A-Z0-9_-]+$'))
)!';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX cb_vector_tag_ix ON cb_vector_document_tag (tech_tag, chunk_id)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
PROMPT === 2. Seeding a small, repeatable vector search example ===
-- These four-dimensional vectors are only a smoke-test fixture. Replace
-- them with the embedding model's dimension in the real ingestion pipeline.
INSERT INTO cb_vector_document_chunk (
chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding
)
SELECT 28001, 'knowledge-001', 1, 'Spring Boot VPD 시작하기',
TO_CLOB('Spring Boot 애플리케이션은 토큰으로 사용자를 식별하고 ORDS를 호출한다.'),
'kb://security/spring-boot-vpd', TO_VECTOR('[0.10,0.20,0.30,0.40]')
FROM dual
WHERE NOT EXISTS (SELECT 1 FROM cb_vector_document_chunk WHERE chunk_id = 28001);
INSERT INTO cb_vector_document_chunk (
chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding
)
SELECT 28002, 'knowledge-001', 2, 'Oracle VPD 정책 연결',
TO_CLOB('Oracle VPD는 현재 사용자 권한에 맞는 행만 반환하도록 WHERE 조건을 동적으로 붙인다.'),
'kb://security/oracle-vpd', TO_VECTOR('[0.20,0.30,0.40,0.50]')
FROM dual
WHERE NOT EXISTS (SELECT 1 FROM cb_vector_document_chunk WHERE chunk_id = 28002);
INSERT INTO cb_vector_document_chunk (
chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding
)
SELECT 28003, 'knowledge-002', 1, 'MCP 도구 권한 설계',
TO_CLOB('MCP 도구도 같은 역할과 기술 태그 규칙을 사용해 지식자료 검색 범위를 제한할 수 있다.'),
'kb://platform/mcp-permission', TO_VECTOR('[0.80,0.70,0.60,0.50]')
FROM dual
WHERE NOT EXISTS (SELECT 1 FROM cb_vector_document_chunk WHERE chunk_id = 28003);
INSERT INTO cb_vector_document_tag (chunk_id, tech_tag)
SELECT 28001, 'SPRING_BOOT' FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM cb_vector_document_tag WHERE chunk_id = 28001 AND tech_tag = 'SPRING_BOOT'
);
INSERT INTO cb_vector_document_tag (chunk_id, tech_tag)
SELECT 28001, 'ORDS' FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM cb_vector_document_tag WHERE chunk_id = 28001 AND tech_tag = 'ORDS'
);
INSERT INTO cb_vector_document_tag (chunk_id, tech_tag)
SELECT 28002, 'ORACLE_VPD' FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM cb_vector_document_tag WHERE chunk_id = 28002 AND tech_tag = 'ORACLE_VPD'
);
INSERT INTO cb_vector_document_tag (chunk_id, tech_tag)
SELECT 28002, 'ORDS' FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM cb_vector_document_tag WHERE chunk_id = 28002 AND tech_tag = 'ORDS'
);
INSERT INTO cb_vector_document_tag (chunk_id, tech_tag)
SELECT 28003, 'MCP' FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM cb_vector_document_tag WHERE chunk_id = 28003 AND tech_tag = 'MCP'
);
PROMPT === 3. Creating the tag-aware one-row-per-chunk protected view ===
CREATE OR REPLACE VIEW cb_vector_search_documents AS
SELECT c.chunk_id,
c.document_id,
c.chunk_no,
c.title,
c.chunk_text,
c.source_uri,
c.embedding,
(
SELECT LISTAGG(t.tech_tag, ',') WITHIN GROUP (ORDER BY t.tech_tag)
FROM cb_vector_document_tag t
WHERE t.chunk_id = c.chunk_id
) AS tech_tag
FROM cb_vector_document_chunk c
WHERE EXISTS (
SELECT 1
FROM cb_vector_document_tag t
WHERE t.chunk_id = c.chunk_id
);
PROMPT === 4. Attaching dynamic VPD to the view ===
BEGIN
DBMS_RLS.DROP_POLICY(
object_schema => USER,
object_name => 'CB_VECTOR_SEARCH_DOCUMENTS',
policy_name => 'CB_VECTOR_TAG_SELECT_POLICY'
);
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => USER,
object_name => 'CB_VECTOR_SEARCH_DOCUMENTS',
policy_name => 'CB_VECTOR_TAG_SELECT_POLICY',
function_schema => USER,
policy_function => 'CB_AGENT_DOC_VPD_FILTER',
statement_types => 'SELECT',
policy_type => DBMS_RLS.DYNAMIC,
enable => TRUE
);
END;
/
PROMPT === 5. Registering the protected object and its columns ===
DECLARE
v_object_id NUMBER;
PROCEDURE ensure_column(
p_column_name IN VARCHAR2,
p_sensitive_yn IN CHAR,
p_sensitivity_level IN VARCHAR2,
p_redaction_method IN VARCHAR2
) IS
v_count NUMBER;
v_id NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM cb_protected_column
WHERE object_id = v_object_id
AND column_name = p_column_name;
IF v_count = 0 THEN
SELECT NVL(MAX(column_id), 0) + 1
INTO v_id
FROM cb_protected_column;
INSERT INTO cb_protected_column (
column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method
) VALUES (
v_id, v_object_id, p_column_name, p_sensitive_yn, p_sensitivity_level, p_redaction_method
);
ELSE
UPDATE cb_protected_column
SET sensitive_yn = p_sensitive_yn,
sensitivity_level = p_sensitivity_level,
redaction_method = p_redaction_method
WHERE object_id = v_object_id
AND column_name = p_column_name;
END IF;
END;
BEGIN
BEGIN
SELECT object_id
INTO v_object_id
FROM cb_protected_object
WHERE object_name = 'CB_VECTOR_SEARCH_DOCUMENTS';
UPDATE cb_protected_object
SET owner = USER,
ords_path = 'cb-ords/cb-agent-vector/search',
enabled_yn = 'Y',
description = '기술 태그 권한이 적용된 벡터 청크 검색 전용 ORDS Handler'
WHERE object_id = v_object_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(object_id), 0) + 1
INTO v_object_id
FROM cb_protected_object;
INSERT INTO cb_protected_object (
object_id, owner, object_name, ords_path, enabled_yn, description
) VALUES (
v_object_id, USER, 'CB_VECTOR_SEARCH_DOCUMENTS',
'cb-ords/cb-agent-vector/search', 'Y',
'기술 태그 권한이 적용된 벡터 청크 검색 전용 ORDS Handler'
);
END;
-- Column policy here is only a display-policy hint. Row access is the TAG
-- VPD rule above; the vector handler never returns EMBEDDING to the caller.
ensure_column('CHUNK_ID', 'N', 'PUBLIC', 'NONE');
ensure_column('DOCUMENT_ID', 'N', 'PUBLIC', 'NONE');
ensure_column('CHUNK_NO', 'N', 'PUBLIC', 'NONE');
ensure_column('TITLE', 'N', 'PUBLIC', 'NONE');
ensure_column('CHUNK_TEXT', 'N', 'PUBLIC', 'NONE');
ensure_column('SOURCE_URI', 'N', 'PUBLIC', 'NONE');
ensure_column('TECH_TAG', 'N', 'PUBLIC', 'NONE');
ensure_column('EMBEDDING', 'Y', 'RESTRICTED', 'NULLIFY');
END;
/
PROMPT === 6. Granting the ORDS runtime read access to the protected view ===
GRANT SELECT ON cb_vector_search_documents TO cb_ords;
COMMIT;
PROMPT === Vector/tag VPD setup complete ===
PROMPT Next: run 29_agent_ords_vector_search_ords.sql as CB_ORDS.
PROMPT Backoffice ingestion: /vector-knowledge (DEMO-4D or configured AI embeddings).
EXIT;

View File

@@ -0,0 +1,194 @@
-- ============================================================
-- 29_agent_ords_vector_search_ords.sql
-- Dedicated ORDS vector search endpoint for the tag-protected view.
--
-- Run as CB_ORDS after 22_agent_ords_security_ords_handler_setup.sql and
-- 28_agent_ords_vector_tag_vpd_setup.sql.
--
-- Request:
-- POST /ords/cb-ords/cb-agent-vector/search?limit=10
-- Authorization: Bearer <key>
-- {"embedding":[0.10,0.20,0.30,0.40]}
--
-- The handler sets the application context, lets VPD filter one row per
-- chunk by the aggregated TECH_TAG list, then calculates vector distance.
-- EMBEDDING is never returned.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Creating the vector search ORDS module ===
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'cb.agent.vector');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'cb.agent.vector',
p_base_path => 'cb-agent-vector/',
p_items_per_page => 25,
p_status => 'PUBLISHED'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'cb.agent.vector',
p_pattern => 'search'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'cb.agent.vector',
p_pattern => 'search',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_source => q'!
DECLARE
v_rows SYS_REFCURSOR;
v_body_text CLOB;
v_embedding_text VARCHAR2(32767);
v_vpd_predicate VARCHAR2(32767);
v_effective_sql VARCHAR2(32767);
BEGIN
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
BEGIN
v_vpd_predicate := admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_VECTOR_SEARCH_DOCUMENTS');
v_effective_sql := 'SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, tech_tag, score '
|| 'FROM (SELECT d.chunk_id, d.document_id, d.chunk_no, d.title, d.chunk_text, '
|| 'd.source_uri, d.tech_tag, VECTOR_DISTANCE(d.embedding, TO_VECTOR(:embedding), COSINE) AS score '
|| 'FROM admin.cb_vector_search_documents d '
|| 'WHERE d.embedding IS NOT NULL';
IF TRIM(v_vpd_predicate) <> '1 = 1' THEN
v_effective_sql := v_effective_sql || ' AND (' || v_vpd_predicate || ')';
END IF;
v_effective_sql := v_effective_sql
|| ' ORDER BY score) ranked_chunks '
|| 'WHERE ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 10), 1), 100)';
EXCEPTION
WHEN OTHERS THEN
v_vpd_predicate := NULL;
v_effective_sql := NULL;
END;
-- ORDS exposes :body_text as a stream bind. Read it exactly once; referring
-- to the stream bind twice causes ORA-17270 (Duplicate stream parameter).
v_body_text := :body_text;
-- JSON_QUERY accepts a numeric JSON array. The JSON_VALUE fallback also
-- permits clients that send the vector as the text "[0.1,0.2,...]".
v_embedding_text := JSON_QUERY(
v_body_text,
'$.embedding' RETURNING VARCHAR2(32767)
);
IF v_embedding_text IS NULL THEN
v_embedding_text := JSON_VALUE(
v_body_text,
'$.embedding' RETURNING VARCHAR2(32767)
);
END IF;
IF v_embedding_text IS NULL OR TRIM(v_embedding_text) IS NULL THEN
RAISE_APPLICATION_ERROR(-20801, 'embedding is required');
END IF;
OPEN v_rows FOR
SELECT chunk_id,
document_id,
chunk_no,
title,
chunk_text,
source_uri,
tech_tag,
score
FROM (
SELECT d.chunk_id,
d.document_id,
d.chunk_no,
d.title,
d.chunk_text,
d.source_uri,
d.tech_tag,
VECTOR_DISTANCE(
d.embedding,
TO_VECTOR(v_embedding_text),
COSINE
) AS score
FROM admin.cb_vector_search_documents d
WHERE d.embedding IS NOT NULL
ORDER BY score
) ranked_chunks
WHERE ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 10), 1), 100);
: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;
IF v_effective_sql IS NOT NULL THEN
APEX_JSON.WRITE('vpd_predicate', v_vpd_predicate);
APEX_JSON.WRITE('effective_sql', v_effective_sql);
END IF;
APEX_JSON.WRITE('items', v_rows);
APEX_JSON.CLOSE_OBJECT;
cb_ords_handler_pkg.clear_vpd_context;
EXCEPTION
WHEN OTHERS THEN
cb_ords_handler_pkg.clear_vpd_context;
:status_code := CASE WHEN SQLCODE = -20801 THEN 400 ELSE 403 END;
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('error', SQLERRM);
APEX_JSON.CLOSE_OBJECT;
END;
!',
p_items_per_page => 0
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'cb.agent.vector',
p_pattern => 'search',
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 => 'cb.agent.vector',
p_pattern => 'search',
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'
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'cb.agent.vector',
p_pattern => 'search',
p_method => 'POST',
p_name => 'limit',
p_bind_variable_name => 'row_limit',
p_source_type => 'URI',
p_param_type => 'INT',
p_access_method => 'IN'
);
COMMIT;
END;
/
PROMPT === Vector search ORDS endpoint ready ===
PROMPT Path: /ords/cb-ords/cb-agent-vector/search
EXIT;

View File

@@ -0,0 +1,130 @@
-- ============================================================
-- 30_agent_ords_vector_tag_vpd_test.sql
-- Unit-style checks for TAG rules on CB_VECTOR_SEARCH_DOCUMENTS.
--
-- Run as ADMIN after 26_agent_ords_security_dynamic_vpd_filter.sql and
-- 28_agent_ords_vector_tag_vpd_setup.sql.
-- This script only creates temporary permission-plane rows in the 458xxx
-- range, checks the generated predicates, and removes those rows.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
SET SERVEROUTPUT ON
PROMPT === Preparing TAG rule test data ===
BEGIN
cb_agent_ctx_pkg.clear_user;
DELETE FROM cb_permission_rule WHERE perm_id BETWEEN 458000 AND 458999;
DELETE FROM cb_permission WHERE perm_id BETWEEN 458000 AND 458999;
DELETE FROM cb_user_role WHERE role_id BETWEEN 458000 AND 458999;
DELETE FROM cb_app_role WHERE role_id BETWEEN 458000 AND 458999;
DELETE FROM cb_app_user WHERE user_id BETWEEN 458000 AND 458999;
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (458101, 'tag_test_or', 'E458101', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (458102, 'tag_test_deny', 'E458102', 'QA', 'N', 'Y');
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (458103, 'tag_test_none', 'E458103', 'QA', 'N', 'Y');
INSERT INTO cb_app_role(role_id, role_name) VALUES (458101, 'TAG_TEST_OR');
INSERT INTO cb_app_role(role_id, role_name) VALUES (458102, 'TAG_TEST_ALLOW_DENY');
INSERT INTO cb_app_role(role_id, role_name) VALUES (458103, 'TAG_TEST_NONE');
INSERT INTO cb_user_role(user_id, role_id) VALUES (458101, 458101);
INSERT INTO cb_user_role(user_id, role_id) VALUES (458102, 458102);
INSERT INTO cb_user_role(user_id, role_id) VALUES (458103, 458103);
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (458101, 458101, 'CB_VECTOR_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (458102, 458102, 'CB_VECTOR_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (458103, 458102, 'CB_VECTOR_SEARCH_DOCUMENTS', 'SELECT', 'DENY');
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (458104, 458103, 'CB_VECTOR_SEARCH_DOCUMENTS', 'SELECT', 'DENY');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (458101, 458101, NULL, 'TAG', 'SPRING_BOOT');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (458102, 458101, NULL, 'TAG', 'ORACLE_VPD');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (458103, 458102, NULL, 'TAG', 'SPRING_BOOT');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (458104, 458102, NULL, 'TAG', 'ORACLE_VPD');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (458105, 458103, NULL, 'TAG', 'ORDS');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (458106, 458104, NULL, 'TAG', 'SPRING_BOOT');
COMMIT;
END;
/
PROMPT === Checking ALLOW TAG OR semantics and DENY precedence ===
DECLARE
PROCEDURE assert_contains(
p_label IN VARCHAR2,
p_user_id IN NUMBER,
p_expected IN VARCHAR2
) IS
v_actual VARCHAR2(32767);
BEGIN
cb_agent_ctx_pkg.set_user(p_user_id);
v_actual := cb_agent_doc_vpd_filter('ADMIN', 'CB_VECTOR_SEARCH_DOCUMENTS');
IF INSTR(v_actual, p_expected) = 0 THEN
RAISE_APPLICATION_ERROR(-20458,
p_label || ' expected [' || p_expected || '] actual [' || v_actual || ']');
END IF;
DBMS_OUTPUT.PUT_LINE('PASS ' || p_label || ': ' || v_actual);
END;
PROCEDURE assert_equals(
p_label IN VARCHAR2,
p_user_id IN NUMBER,
p_expected IN VARCHAR2
) IS
v_actual VARCHAR2(32767);
BEGIN
cb_agent_ctx_pkg.set_user(p_user_id);
v_actual := cb_agent_doc_vpd_filter('ADMIN', 'CB_VECTOR_SEARCH_DOCUMENTS');
IF v_actual != p_expected THEN
RAISE_APPLICATION_ERROR(-20458,
p_label || ' expected [' || p_expected || '] actual [' || v_actual || ']');
END IF;
DBMS_OUTPUT.PUT_LINE('PASS ' || p_label || ': ' || v_actual);
END;
BEGIN
assert_contains('TWO_ALLOW_TAGS_ARE_OR', 458101, 'OR');
assert_contains('TWO_ALLOW_TAGS_INCLUDE_SPRING', 458101, 'REGEXP_LIKE(UPPER(TECH_TAG), ''(^|,)SPRING_BOOT(,|$)'')');
assert_contains('TWO_ALLOW_TAGS_INCLUDE_VPD', 458101, 'REGEXP_LIKE(UPPER(TECH_TAG), ''(^|,)ORACLE_VPD(,|$)'')');
assert_contains('DENY_TAG_IS_SUBTRACTED', 458102, 'AND NOT');
assert_contains('DENY_TAG_DETAIL', 458102, 'REGEXP_LIKE(UPPER(TECH_TAG), ''(^|,)ORDS(,|$)'')');
assert_equals('DENY_ONLY_FAILS_CLOSED', 458103, '1 = 0');
cb_agent_ctx_pkg.clear_user;
END;
/
PROMPT === Cleaning TAG rule test data ===
BEGIN
cb_agent_ctx_pkg.clear_user;
DELETE FROM cb_permission_rule WHERE perm_id BETWEEN 458000 AND 458999;
DELETE FROM cb_permission WHERE perm_id BETWEEN 458000 AND 458999;
DELETE FROM cb_user_role WHERE role_id BETWEEN 458000 AND 458999;
DELETE FROM cb_app_role WHERE role_id BETWEEN 458000 AND 458999;
DELETE FROM cb_app_user WHERE user_id BETWEEN 458000 AND 458999;
COMMIT;
END;
/
PROMPT === TAG rule checks passed ===
EXIT;

View File

@@ -0,0 +1,58 @@
-- ============================================================
-- 31_agent_ords_backoffice_ux_metadata.sql
-- Upgrade existing backoffice metadata for the screen explanations.
-- Run as ADMIN before deploying this application version.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE cb_protected_object ADD (description VARCHAR2(200))';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1430 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE q'!
CREATE TABLE cb_vpd_policy_note (
object_owner VARCHAR2(128) NOT NULL,
object_name VARCHAR2(128) NOT NULL,
policy_name VARCHAR2(128) NOT NULL,
description VARCHAR2(500) NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT cb_vpd_policy_note_pk PRIMARY KEY (object_owner, object_name, policy_name)
)!';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE q'!
CREATE TABLE cb_vpd_filter_note (
function_owner VARCHAR2(128) NOT NULL,
function_name VARCHAR2(128) NOT NULL,
description VARCHAR2(500) NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT cb_vpd_filter_note_pk PRIMARY KEY (function_owner, function_name)
)!';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
COMMIT;
PROMPT === Backoffice UX metadata upgrade complete ===
EXIT;

View File

@@ -0,0 +1,119 @@
-- ============================================================
-- 31_dds_standalone_demo_setup.sql
-- Independent local Deep Data Security demo.
--
-- This script deliberately does not use VPD views, application
-- permission tables, or external database links. It creates a
-- local PG/MY-shaped matrix so the DDS demo can run independently
-- when the VPD demo's RDS links are unavailable.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE ON
SET VERIFY OFF
PROMPT === 1. Creating DDS-only local source tables ===
BEGIN
EXECUTE IMMEDIATE 'DROP VIEW v_dds_customers_pg';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP VIEW v_dds_customers_my';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE cb_dds_demo_customers_pg PURGE';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE cb_dds_demo_customers_my PURGE';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
CREATE TABLE cb_dds_demo_customers_pg (
customer_id NUMBER PRIMARY KEY,
full_name VARCHAR2(100) NOT NULL,
email VARCHAR2(200),
signup_date DATE DEFAULT SYSDATE NOT NULL,
region VARCHAR2(30) NOT NULL
);
CREATE TABLE cb_dds_demo_customers_my (
customer_id NUMBER PRIMARY KEY,
full_name VARCHAR2(100) NOT NULL,
email VARCHAR2(200),
signup_date DATE DEFAULT SYSDATE NOT NULL,
region VARCHAR2(30) NOT NULL
);
INSERT INTO cb_dds_demo_customers_pg VALUES (101, 'PG Alice', 'pg.alice@example.test', DATE '2026-01-10', 'APAC');
INSERT INTO cb_dds_demo_customers_pg VALUES (102, 'PG Bob', 'pg.bob@example.test', DATE '2026-01-11', 'EMEA');
INSERT INTO cb_dds_demo_customers_pg VALUES (103, 'PG Carol', 'pg.carol@example.test', DATE '2026-01-12', 'AMER');
INSERT INTO cb_dds_demo_customers_my VALUES (201, 'MY Alice', 'my.alice@example.test', DATE '2026-02-10', 'APAC');
INSERT INTO cb_dds_demo_customers_my VALUES (202, 'MY Bob', 'my.bob@example.test', DATE '2026-02-11', 'EMEA');
INSERT INTO cb_dds_demo_customers_my VALUES (203, 'MY Carol', 'my.carol@example.test', DATE '2026-02-12', 'AMER');
CREATE OR REPLACE VIEW v_dds_customers_pg AS
SELECT customer_id, full_name, email, signup_date, region
FROM cb_dds_demo_customers_pg;
CREATE OR REPLACE VIEW v_dds_customers_my AS
SELECT customer_id, full_name, email, signup_date, region
FROM cb_dds_demo_customers_my;
COMMIT;
PROMPT === 2. Creating independent DDS END USERs ===
CREATE END USER "dds_demo_my" IDENTIFIED BY "&DDSUSER_MY_PASSWORD";
CREATE END USER "dds_demo_pg" IDENTIFIED BY "&DDSUSER_PG_PASSWORD";
CREATE END USER "dds_demo_both" IDENTIFIED BY "&DDSUSER_BOTH_PASSWORD";
CREATE END USER "dds_demo_none" IDENTIFIED BY "&DDSUSER_NONE_PASSWORD";
PROMPT === 3. Creating independent DATA ROLEs ===
CREATE ROLE dds_demo_connect_role;
GRANT CREATE SESSION TO dds_demo_connect_role;
CREATE DATA ROLE dds_demo_my_role;
CREATE DATA ROLE dds_demo_pg_role;
CREATE DATA ROLE dds_demo_both_role;
CREATE DATA ROLE dds_demo_none_role;
GRANT dds_demo_connect_role TO dds_demo_my_role;
GRANT dds_demo_connect_role TO dds_demo_pg_role;
GRANT dds_demo_connect_role TO dds_demo_both_role;
GRANT dds_demo_connect_role TO dds_demo_none_role;
GRANT DATA ROLE dds_demo_my_role TO "dds_demo_my";
GRANT DATA ROLE dds_demo_pg_role TO "dds_demo_pg";
GRANT DATA ROLE dds_demo_both_role TO "dds_demo_both";
GRANT DATA ROLE dds_demo_none_role TO "dds_demo_none";
PROMPT === 4. Creating independent DATA GRANTs ===
CREATE DATA GRANT admin.dds_demo_my_grant
AS SELECT
ON admin.v_dds_customers_my
TO dds_demo_my_role;
CREATE DATA GRANT admin.dds_demo_pg_grant
AS SELECT
ON admin.v_dds_customers_pg
TO dds_demo_pg_role;
CREATE DATA GRANT admin.dds_demo_both_pg_grant
AS SELECT
ON admin.v_dds_customers_pg
TO dds_demo_both_role;
CREATE DATA GRANT admin.dds_demo_both_my_grant
AS SELECT
ON admin.v_dds_customers_my
TO dds_demo_both_role;
PROMPT === Independent DDS demo setup complete ===
EXIT;

View File

@@ -0,0 +1,156 @@
-- ============================================================
-- 32_dds_vector_tag_setup.sql
-- DDS counterpart of the VPD vector/tag search object.
--
-- The common backoffice stores chunks and TECH_TAG rows in
-- CB_VECTOR_DOCUMENT_CHUNK / CB_VECTOR_DOCUMENT_TAG. DDS must not query
-- CB_VECTOR_SEARCH_DOCUMENTS because that view carries the VPD policy.
-- This script creates a DDS-only view and declares tag access with DATA
-- GRANTs. The application uses the same user/role/TAG management journey;
-- the final enforcement is END USER -> DATA ROLE -> DATA GRANT.
--
-- Prerequisite: 28_agent_ords_vector_tag_vpd_setup.sql and 31_dds_standalone_demo_setup.sql
-- Run as ADMIN on an Oracle AI Database release with Deep Data Security.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Creating a DDS-only vector view (no VPD policy) ===
CREATE OR REPLACE VIEW cb_dds_vector_search_documents AS
SELECT c.chunk_id,
c.document_id,
c.chunk_no,
c.title,
c.chunk_text,
c.source_uri,
c.embedding,
(
SELECT LISTAGG(t.tech_tag, ',') WITHIN GROUP (ORDER BY t.tech_tag)
FROM cb_vector_document_tag t
WHERE t.chunk_id = c.chunk_id
) AS tech_tag
FROM cb_vector_document_chunk c
WHERE EXISTS (
SELECT 1
FROM cb_vector_document_tag t
WHERE t.chunk_id = c.chunk_id
);
PROMPT === 2. Replacing DATA GRANTs for the four demo identities ===
-- The tag expression is deliberately an OR expression. Adding another
-- allowed tag means replacing the corresponding grant expression, not
-- writing a second conflicting policy.
BEGIN
EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_demo_my_vector_grant';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_demo_pg_vector_grant';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP DATA GRANT admin.dds_demo_both_vector_grant';
EXCEPTION WHEN OTHERS THEN NULL;
END;
/
CREATE OR REPLACE DATA GRANT admin.dds_demo_my_vector_grant
AS SELECT
ON admin.cb_dds_vector_search_documents
WHERE REGEXP_LIKE(UPPER(tech_tag), '(^|,)(SPRING_BOOT|ORDS)(,|$)')
TO dds_demo_my_role;
CREATE OR REPLACE DATA GRANT admin.dds_demo_pg_vector_grant
AS SELECT
ON admin.cb_dds_vector_search_documents
WHERE REGEXP_LIKE(UPPER(tech_tag), '(^|,)(ORACLE_VPD|ORACLE_DDS)(,|$)')
TO dds_demo_pg_role;
CREATE OR REPLACE DATA GRANT admin.dds_demo_both_vector_grant
AS SELECT
ON admin.cb_dds_vector_search_documents
WHERE REGEXP_LIKE(UPPER(tech_tag), '(^|,)(SPRING_BOOT|ORDS|ORACLE_VPD|ORACLE_DDS|MCP)(,|$)')
TO dds_demo_both_role;
-- dds_demo_none_role intentionally receives no grant (default deny).
PROMPT === 3. Registering the vector object in the common application permission model ===
-- The application permission tables remain the source of truth. Basic vector
-- access is a role whitelist: the seeded application roles have one ALLOW
-- permission plus an ALL rule and can search every vector chunk. TAG rules
-- are reserved for a role deliberately configured with a row condition.
DECLARE
PROCEDURE ensure_vector_all_permission(p_role_id IN NUMBER) IS
v_perm_id NUMBER;
v_rule_id NUMBER;
BEGIN
BEGIN
SELECT perm_id
INTO v_perm_id
FROM cb_permission
WHERE role_id = p_role_id
AND target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND action_name = 'SELECT'
AND permission_effect = 'ALLOW'
AND ROWNUM = 1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(perm_id), 0) + 1 INTO v_perm_id FROM cb_permission;
INSERT INTO cb_permission (
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
v_perm_id, p_role_id, 'CB_VECTOR_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW'
);
END;
-- The old demo seed put arbitrary TECH_TAG filters on these base roles.
-- Full access must not depend on the current list of document tags.
DELETE FROM cb_permission_rule
WHERE perm_id = v_perm_id
AND UPPER(TRIM(rule_type)) = 'TAG';
SELECT MIN(rule_id)
INTO v_rule_id
FROM cb_permission_rule
WHERE perm_id = v_perm_id
AND UPPER(TRIM(rule_type)) = 'ALL';
IF v_rule_id IS NULL THEN
SELECT NVL(MAX(rule_id), 0) + 1 INTO v_rule_id FROM cb_permission_rule;
INSERT INTO cb_permission_rule (
rule_id, perm_id, rule_column, rule_type, rule_value
) VALUES (
v_rule_id, v_perm_id, NULL, 'ALL', NULL
);
END IF;
DELETE FROM cb_permission_rule
WHERE perm_id = v_perm_id
AND UPPER(TRIM(rule_type)) = 'ALL'
AND rule_id <> v_rule_id;
END;
BEGIN
ensure_vector_all_permission(10);
ensure_vector_all_permission(20);
ensure_vector_all_permission(30);
END;
/
PROMPT === 4. Vector object inventory ===
SELECT 'DDS_VECTOR_OBJECT' AS item,
'ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS' AS value
FROM dual;
SELECT grant_name, object_name, grantee
FROM dba_data_grants
WHERE grant_name IN (
'DDS_DEMO_MY_VECTOR_GRANT',
'DDS_DEMO_PG_VECTOR_GRANT',
'DDS_DEMO_BOTH_VECTOR_GRANT'
)
ORDER BY grant_name;
PROMPT === DDS vector/tag DATA GRANT setup complete ===
EXIT;

View File

@@ -0,0 +1,21 @@
-- ============================================================
-- 33_agent_ords_sql_trace_grant.sql
-- Enable the optional VPD predicate trace for an existing ORDS install.
--
-- Run as ADMIN after 26_agent_ords_security_dynamic_vpd_filter.sql.
-- Then run 22_agent_ords_security_ords_handler_setup.sql as CB_ORDS, or
-- save the trace-enabled Handler source from the backoffice UI.
--
-- The grant exposes only the existing predicate function to the ORDS
-- runtime. It does not weaken DBMS_RLS enforcement or expose bearer values.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
PROMPT === Enabling ORDS VPD predicate trace ===
GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords;
PROMPT === ORDS VPD predicate trace grant ready ===
PROMPT Next: run 22_agent_ords_security_ords_handler_setup.sql as CB_ORDS
EXIT;

View File

@@ -0,0 +1,28 @@
-- ============================================================
-- 34_agent_ords_execution_evidence_grant.sql
-- Enable SQL_ID and DBMS_XPLAN evidence for VPD probe requests.
--
-- Run as SYS or a DBA account that can grant SYS.V_$ dynamic-performance
-- views. Typical Autonomous ADMIN accounts cannot delegate these views.
-- The grants are read-only and scoped to the BACKOFFICE_DB_USERNAME account
-- that displays the evidence (for example, ADMIN). They do not grant table
-- DML or alter DBMS_RLS enforcement.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
-- Change ADMIN when BACKOFFICE_DB_USERNAME uses a dedicated observer account.
DEFINE diagnostic_user = ADMIN
PROMPT === Granting backoffice cursor-evidence read access ===
GRANT SELECT ON V_$SQL TO &diagnostic_user;
GRANT SELECT ON V_$SQL_PLAN TO &diagnostic_user;
GRANT SELECT ON V_$SQL_PLAN_STATISTICS_ALL TO &diagnostic_user;
GRANT SELECT ON V_$SESSION TO &diagnostic_user;
GRANT EXECUTE ON DBMS_XPLAN TO &diagnostic_user;
PROMPT === Backoffice cursor-evidence grants ready ===
PROMPT SQL_ID evidence will appear after the next /probe call.
UNDEFINE diagnostic_user
EXIT;

View File

@@ -0,0 +1,163 @@
-- ============================================================
-- 34_dds_token_data_grant_common_auth.sql
-- Token-driven DDS enforcement using the common CB_* permission model.
--
-- This is the product-shaped DDS path:
-- 1. One local DDS END USER is used as the technical query identity.
-- 2. The application calls CB_AGENT_CTX_PKG.SET_USER_BY_BEARER(token).
-- 3. A DATA GRANT is attached to one DDS DATA ROLE on the protected VIEW.
-- 4. Its predicate function reads CB_PERMISSION / CB_PERMISSION_RULE,
-- including direct roles and active group-inherited roles.
--
-- The token is not a bind parameter in CREATE DATA GRANT syntax. It is
-- resolved before SELECT and stored in the trusted CB_AGENT_CTX namespace;
-- the DATA GRANT predicate then evaluates the common permission tables.
--
-- Prerequisite: 17_agent_ords_security_local_vpd_setup.sql,
-- 25_agent_ords_security_backoffice_support.sql,
-- 31_dds_standalone_demo_setup.sql,
-- 32_dds_vector_tag_setup.sql
-- Run as ADMIN on an Oracle AI Database release with Deep Data Security.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE ON
SET VERIFY OFF
PROMPT === 1. Creating one technical DDS END USER and its DATA ROLE ===
CREATE END USER IF NOT EXISTS "dds_demo_token" IDENTIFIED BY "&DDSUSER_TOKEN_PASSWORD";
BEGIN
EXECUTE IMMEDIATE 'CREATE ROLE cb_dds_token_connect_role';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1921 THEN
RAISE;
END IF;
END;
/
GRANT CREATE SESSION TO cb_dds_token_connect_role;
CREATE DATA ROLE IF NOT EXISTS cb_dds_token_role;
GRANT cb_dds_token_connect_role TO cb_dds_token_role;
GRANT DATA ROLE cb_dds_token_role TO "dds_demo_token";
-- A DDS END USER is not a conventional database user. Standard object
-- privileges therefore go to a standard role inherited by the DATA ROLE.
GRANT EXECUTE ON admin.cb_agent_ctx_pkg TO cb_dds_token_connect_role;
PROMPT === 2. Creating the common permission predicate function ===
CREATE OR REPLACE FUNCTION admin.cb_dds_vector_tag_allowed(
p_tech_tag IN VARCHAR2
) RETURN NUMBER
AUTHID DEFINER
AS
v_user_id NUMBER;
v_allow NUMBER := 0;
v_deny NUMBER := 0;
BEGIN
BEGIN
v_user_id := TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'));
EXCEPTION
WHEN OTHERS THEN
RETURN 0;
END;
IF v_user_id IS NULL OR p_tech_tag IS NULL THEN
RETURN 0;
END IF;
-- Direct roles and roles inherited from active application groups are
-- deliberately expanded at query time. CB_* remains the source of truth.
SELECT COUNT(*)
INTO v_allow
FROM (
SELECT ur.role_id
FROM cb_user_role ur
WHERE ur.user_id = v_user_id
UNION
SELECT gr.role_id
FROM cb_user_group ug
JOIN cb_app_group g
ON g.group_id = ug.group_id
AND g.active_yn = 'Y'
JOIN cb_group_role gr
ON gr.group_id = ug.group_id
WHERE ug.user_id = v_user_id
) effective_role
JOIN cb_app_user u
ON u.user_id = v_user_id
AND u.active = 'Y'
JOIN cb_permission p
ON p.role_id = effective_role.role_id
JOIN cb_permission_rule r
ON r.perm_id = p.perm_id
WHERE p.target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND p.action_name = 'SELECT'
AND p.permission_effect = 'ALLOW'
AND (
r.rule_type = 'ALL'
OR (
r.rule_type = 'TAG'
AND REGEXP_LIKE(
UPPER(p_tech_tag),
'(^|,)' || UPPER(TRIM(r.rule_value)) || '(,|$)'
)
)
);
SELECT COUNT(*)
INTO v_deny
FROM (
SELECT ur.role_id
FROM cb_user_role ur
WHERE ur.user_id = v_user_id
UNION
SELECT gr.role_id
FROM cb_user_group ug
JOIN cb_app_group g
ON g.group_id = ug.group_id
AND g.active_yn = 'Y'
JOIN cb_group_role gr
ON gr.group_id = ug.group_id
WHERE ug.user_id = v_user_id
) effective_role
JOIN cb_permission p
ON p.role_id = effective_role.role_id
JOIN cb_permission_rule r
ON r.perm_id = p.perm_id
WHERE p.target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND p.action_name = 'SELECT'
AND p.permission_effect = 'DENY'
AND r.rule_type = 'TAG'
AND REGEXP_LIKE(
UPPER(p_tech_tag),
'(^|,)' || UPPER(TRIM(r.rule_value)) || '(,|$)'
);
IF v_allow > 0 AND v_deny = 0 THEN
RETURN 1;
END IF;
RETURN 0;
END;
/
SHOW ERRORS
PROMPT === 3. Creating one object-level DATA GRANT ===
CREATE OR REPLACE DATA GRANT admin.dds_demo_token_vector_grant
AS SELECT
ON admin.cb_dds_vector_search_documents
WHERE admin.cb_dds_vector_tag_allowed(tech_tag) = 1
TO cb_dds_token_role;
PROMPT === 4. Verifying the object-level grant ===
SELECT grant_name, object_name, grantee
FROM dba_data_grants
WHERE grant_name = 'DDS_DEMO_TOKEN_VECTOR_GRANT';
PROMPT === Token-driven DDS DATA GRANT setup complete ===
PROMPT Call ADMIN.CB_AGENT_CTX_PKG.SET_USER_BY_BEARER(:token) on the
PROMPT technical connection, then SELECT from ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS.
EXIT;

View File

@@ -0,0 +1,24 @@
-- Remove only the token-driven DDS demonstration objects.
-- Run as ADMIN after confirming no application still uses dds_demo_token.
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
DROP DATA GRANT IF EXISTS admin.dds_demo_token_vector_grant;
DROP FUNCTION admin.cb_dds_vector_tag_allowed;
DROP DATA ROLE IF EXISTS cb_dds_token_role;
BEGIN
EXECUTE IMMEDIATE 'DROP ROLE cb_dds_token_connect_role';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -1919 THEN
RAISE;
END IF;
END;
/
DROP END USER IF EXISTS "dds_demo_token";
PROMPT DDS token DATA GRANT objects removed.
EXIT;

View File

@@ -0,0 +1,173 @@
-- ============================================================
-- 36_dds_sales_knowledge_scenario.sql
-- Business scenario fixture: a sales employee searches only SALES-tagged
-- vector knowledge with a bearer token.
--
-- The access value is stored in CB_VECTOR_DOCUMENT_TAG.TECH_TAG. The
-- existing DDS token Data Grant (34_...) reads the common CB_* permission
-- tables at query time, so this script only adds the business user, role,
-- permission rule, sample chunk, and a short-lived demo token.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Adding the sales business user and role ===
MERGE INTO cb_app_user target
USING (
SELECT 104 user_id, 'agent_sales' user_name, 'E3001' employee_no,
'SALES' dept_code, 'N' can_read_contents, 'Y' active
FROM dual
) source
ON (target.user_id = source.user_id)
WHEN MATCHED THEN UPDATE SET
target.user_name = source.user_name,
target.employee_no = source.employee_no,
target.dept_code = source.dept_code,
target.can_read_contents = source.can_read_contents,
target.active = source.active
WHEN NOT MATCHED THEN INSERT (
user_id, user_name, employee_no, dept_code, can_read_contents, active
) VALUES (
source.user_id, source.user_name, source.employee_no,
source.dept_code, source.can_read_contents, source.active
);
MERGE INTO cb_app_role target
USING (
SELECT 40 role_id, 'SALES_KNOWLEDGE_ROLE' role_name,
'INTERNAL' max_sensitivity_level
FROM dual
) source
ON (target.role_id = source.role_id)
WHEN MATCHED THEN UPDATE SET
target.role_name = source.role_name,
target.max_sensitivity_level = source.max_sensitivity_level
WHEN NOT MATCHED THEN INSERT (
role_id, role_name, max_sensitivity_level
) VALUES (
source.role_id, source.role_name, source.max_sensitivity_level
);
MERGE INTO cb_user_role target
USING (SELECT 104 user_id, 40 role_id FROM dual) source
ON (target.user_id = source.user_id AND target.role_id = source.role_id)
WHEN NOT MATCHED THEN INSERT (user_id, role_id)
VALUES (source.user_id, source.role_id);
PROMPT === 2. Registering SALES as the allowed knowledge value ===
MERGE INTO cb_permission target
USING (
SELECT 400 perm_id, 40 role_id, 'CB_VECTOR_SEARCH_DOCUMENTS' target_name,
'SELECT' action_name, 'ALLOW' permission_effect
FROM dual
) source
ON (target.perm_id = source.perm_id)
WHEN MATCHED THEN UPDATE SET
target.role_id = source.role_id,
target.target_name = source.target_name,
target.action_name = source.action_name,
target.permission_effect = source.permission_effect
WHEN NOT MATCHED THEN INSERT (
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
source.perm_id, source.role_id, source.target_name,
source.action_name, source.permission_effect
);
MERGE INTO cb_permission_rule target
USING (
SELECT 4000 rule_id, 400 perm_id, 'TECH_TAG' rule_column,
'TAG' rule_type, 'SALES' rule_value
FROM dual
) source
ON (target.rule_id = source.rule_id)
WHEN MATCHED THEN UPDATE SET
target.perm_id = source.perm_id,
target.rule_column = source.rule_column,
target.rule_type = source.rule_type,
target.rule_value = source.rule_value
WHEN NOT MATCHED THEN INSERT (
rule_id, perm_id, rule_column, rule_type, rule_value
) VALUES (
source.rule_id, source.perm_id, source.rule_column,
source.rule_type, source.rule_value
);
PROMPT === 3. Adding a SALES-tagged vector knowledge chunk ===
MERGE INTO cb_vector_document_chunk target
USING (
SELECT 28004 chunk_id, 'knowledge-sales-001' document_id, 1 chunk_no,
'세일즈 파이프라인 운영 가이드' title,
TO_CLOB('세일즈 파이프라인 단계별 관리 기준과 영업 담당자용 고객 후속 조치 가이드입니다.') chunk_text,
'kb://business/sales/pipeline' source_uri,
TO_VECTOR('[0.65,0.25,0.15,0.10]') embedding
FROM dual
) source
ON (target.chunk_id = source.chunk_id)
WHEN MATCHED THEN UPDATE SET
target.document_id = source.document_id,
target.chunk_no = source.chunk_no,
target.title = source.title,
target.chunk_text = source.chunk_text,
target.source_uri = source.source_uri,
target.embedding = source.embedding
WHEN NOT MATCHED THEN INSERT (
chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding
) VALUES (
source.chunk_id, source.document_id, source.chunk_no, source.title,
source.chunk_text, source.source_uri, source.embedding
);
MERGE INTO cb_vector_document_tag target
USING (SELECT 28004 chunk_id, 'SALES' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag)
VALUES (source.chunk_id, source.tech_tag);
MERGE INTO cb_vector_document_tag target
USING (SELECT 28004 chunk_id, 'INTERNAL' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag)
VALUES (source.chunk_id, source.tech_tag);
PROMPT === 4. Issuing a short-lived demo token ===
DECLARE
v_key_id NUMBER;
BEGIN
UPDATE cb_agent_bearer_key
SET user_id = 104,
key_hash = STANDARD_HASH('dds_sales_demo_token', 'SHA256'),
active = 'Y',
revoked_at = NULL,
expires_at = SYSDATE + (1 / 24)
WHERE key_prefix = 'sales_demo';
IF SQL%ROWCOUNT = 0 THEN
SELECT NVL(MAX(key_id), 0) + 1 INTO v_key_id FROM cb_agent_bearer_key;
INSERT INTO cb_agent_bearer_key (
key_id, user_id, key_hash, key_prefix, issued_at,
expires_at, revoked_at, active
) VALUES (
v_key_id, 104, STANDARD_HASH('dds_sales_demo_token', 'SHA256'),
'sales_demo', SYSDATE, SYSDATE + (1 / 24), NULL, 'Y'
);
END IF;
END;
/
COMMIT;
SELECT u.user_name, u.dept_code, r.role_name,
p.target_name, pr.rule_column, pr.rule_value,
'dds_sales_demo_token' AS demo_token
FROM cb_app_user u
JOIN cb_user_role ur ON ur.user_id = u.user_id
JOIN cb_app_role r ON r.role_id = ur.role_id
JOIN cb_permission p ON p.role_id = r.role_id
JOIN cb_permission_rule pr ON pr.perm_id = p.perm_id
WHERE u.user_id = 104;
PROMPT Sales scenario ready: use dds_sales_demo_token in the DDS token search.
EXIT;

View File

@@ -0,0 +1,18 @@
-- Remove only the SALES knowledge scenario fixture.
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
DELETE FROM cb_agent_bearer_key WHERE key_prefix = 'sales_demo';
DELETE FROM cb_vector_document_tag WHERE chunk_id = 28004;
DELETE FROM cb_vector_document_chunk WHERE chunk_id = 28004;
DELETE FROM cb_permission_rule WHERE rule_id = 4000;
DELETE FROM cb_permission WHERE perm_id = 400;
DELETE FROM cb_user_role WHERE user_id = 104 AND role_id = 40;
DELETE FROM cb_app_role WHERE role_id = 40;
DELETE FROM cb_app_user WHERE user_id = 104;
COMMIT;
PROMPT Sales knowledge scenario removed.
EXIT;

View File

@@ -0,0 +1,91 @@
-- ============================================================
-- 38_agent_ords_vector_all_permission_normalization.sql
-- Normalize the seeded vector access roles to whitelist / ALL access.
--
-- The default roles 10, 20 and 30 may search every document chunk. A role
-- with no matching ALLOW permission remains fail-closed through the VPD
-- function (1 = 0). Keep row conditions such as TAG on dedicated restricted
-- roles that do not also have an ALL rule.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Normalizing default vector roles to ALL ===
DECLARE
PROCEDURE normalize_vector_role(p_role_id IN NUMBER) IS
v_perm_id NUMBER;
v_rule_id NUMBER;
BEGIN
SELECT MIN(perm_id)
INTO v_perm_id
FROM cb_permission
WHERE role_id = p_role_id
AND target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND action_name = 'SELECT'
AND permission_effect = 'ALLOW';
IF v_perm_id IS NULL THEN
SELECT NVL(MAX(perm_id), 0) + 1 INTO v_perm_id FROM cb_permission;
INSERT INTO cb_permission (
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
v_perm_id, p_role_id, 'CB_VECTOR_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW'
);
END IF;
-- Remove only the legacy demo TAG filters from the default ALL roles.
DELETE FROM cb_permission_rule
WHERE perm_id = v_perm_id
AND UPPER(TRIM(rule_type)) = 'TAG';
SELECT MIN(rule_id)
INTO v_rule_id
FROM cb_permission_rule
WHERE perm_id = v_perm_id
AND UPPER(TRIM(rule_type)) = 'ALL';
IF v_rule_id IS NULL THEN
SELECT NVL(MAX(rule_id), 0) + 1 INTO v_rule_id FROM cb_permission_rule;
INSERT INTO cb_permission_rule (
rule_id, perm_id, rule_column, rule_type, rule_value
) VALUES (
v_rule_id, v_perm_id, NULL, 'ALL', NULL
);
END IF;
DELETE FROM cb_permission_rule
WHERE perm_id = v_perm_id
AND UPPER(TRIM(rule_type)) = 'ALL'
AND rule_id <> v_rule_id;
END;
BEGIN
normalize_vector_role(10); -- HR_DEPT_ROLE
normalize_vector_role(20); -- FIN_SELF_ROLE
normalize_vector_role(30); -- ALL_DOC_ROLE
COMMIT;
END;
/
PROMPT === Verifying default vector whitelist rules ===
SELECT r.role_id,
r.role_name,
p.target_name,
p.action_name,
p.permission_effect,
pr.rule_type,
pr.rule_column,
pr.rule_value
FROM cb_app_role r
JOIN cb_permission p
ON p.role_id = r.role_id
LEFT JOIN cb_permission_rule pr
ON pr.perm_id = p.perm_id
WHERE r.role_id IN (10, 20, 30)
AND p.target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND p.action_name = 'SELECT'
ORDER BY r.role_id, p.perm_id, pr.rule_id;
PROMPT === Default vector whitelist normalization complete ===
EXIT;

View File

@@ -0,0 +1,47 @@
-- ============================================================
-- 39_agent_ords_default_user_display_name.sql
-- Give the seeded verification users human-readable display names.
-- User IDs, roles, bearer keys and VPD authorization remain unchanged.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Renaming default verification users ===
DECLARE
PROCEDURE rename_user(
p_user_id IN NUMBER,
p_user_name IN VARCHAR2
) IS
BEGIN
UPDATE cb_app_user
SET user_name = p_user_name
WHERE user_id = p_user_id;
IF SQL%ROWCOUNT <> 1 THEN
RAISE_APPLICATION_ERROR(-20839, 'Default verification user not found: ' || p_user_id);
END IF;
END;
BEGIN
rename_user(101, '이에이치알'); -- HR_DEPT_ROLE
rename_user(102, '박파이넨스'); -- FIN_SELF_ROLE
rename_user(103, '김어드민'); -- ALL_DOC_ROLE
COMMIT;
END;
/
PROMPT === Verifying renamed users and their roles ===
SELECT u.user_id,
u.user_name,
u.employee_no,
u.dept_code,
r.role_name
FROM cb_app_user u
LEFT JOIN cb_user_role ur ON ur.user_id = u.user_id
LEFT JOIN cb_app_role r ON r.role_id = ur.role_id
WHERE u.user_id IN (101, 102, 103)
ORDER BY u.user_id, r.role_id;
PROMPT === Default verification user display names updated ===
EXIT;

View File

@@ -0,0 +1,211 @@
-- ============================================================
-- 40_dds_protection_evidence.sql
-- Immutable DDS publication fingerprints and deterministic validation fixtures.
--
-- Prerequisite: 25, 32, 34. Run as ADMIN.
-- The script is idempotent and never prints a bearer secret.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Evidence metadata ===
BEGIN
EXECUTE IMMEDIATE 'CREATE SEQUENCE cb_dds_protection_snapshot_seq START WITH 1 INCREMENT BY 1 NOCACHE';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE SEQUENCE cb_dds_validation_evidence_seq START WITH 1 INCREMENT BY 1 NOCACHE';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
BEGIN
EXECUTE IMMEDIATE q'[
CREATE TABLE cb_dds_protection_snapshot (
snapshot_id NUMBER PRIMARY KEY,
object_name VARCHAR2(261) NOT NULL,
enforcement_path VARCHAR2(20) NOT NULL CHECK (enforcement_path IN ('SERVICE','DIRECT')),
subject_key VARCHAR2(100) NOT NULL,
fingerprint VARCHAR2(64) NOT NULL,
actor VARCHAR2(100) NOT NULL,
note VARCHAR2(500),
published_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
)]';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
BEGIN
EXECUTE IMMEDIATE q'[
CREATE TABLE cb_dds_validation_fixture (
fixture_id VARCHAR2(100) PRIMARY KEY,
enforcement_path VARCHAR2(20) NOT NULL CHECK (enforcement_path IN ('SERVICE','DIRECT')),
subject_key VARCHAR2(100) NOT NULL,
application_user_id NUMBER DEFAULT 0 NOT NULL,
document_id VARCHAR2(200) NOT NULL,
expected_visible CHAR(1) NOT NULL CHECK (expected_visible IN ('Y','N')),
description VARCHAR2(500) NOT NULL,
active_yn CHAR(1) DEFAULT 'Y' NOT NULL CHECK (active_yn IN ('Y','N')),
fixture_version VARCHAR2(40) DEFAULT 'v1' NOT NULL
)]';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
BEGIN
EXECUTE IMMEDIATE q'[
CREATE TABLE cb_dds_validation_evidence (
evidence_id NUMBER PRIMARY KEY,
run_id VARCHAR2(36) NOT NULL,
fixture_id VARCHAR2(100) NOT NULL,
enforcement_path VARCHAR2(20) NOT NULL CHECK (enforcement_path IN ('SERVICE','DIRECT')),
subject_key VARCHAR2(100) NOT NULL,
expected_visible CHAR(1) NOT NULL CHECK (expected_visible IN ('Y','N')),
actual_visible CHAR(1) NOT NULL CHECK (actual_visible IN ('Y','N')),
outcome VARCHAR2(20) NOT NULL CHECK (outcome IN ('PASSED','FAILED')),
fingerprint VARCHAR2(64) NOT NULL,
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
)]';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX cb_dds_snapshot_ix ON cb_dds_protection_snapshot (enforcement_path, subject_key, published_at)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX cb_dds_validation_ix ON cb_dds_validation_evidence (enforcement_path, subject_key, created_at)';
EXCEPTION WHEN OTHERS THEN IF SQLCODE <> -955 THEN RAISE; END IF; END;
/
PROMPT === 2. Deterministic SERVICE fixtures ===
MERGE INTO cb_app_user target
USING (SELECT 9904 user_id, 'dds_fixture_agent' user_name, 'E9904' employee_no,
'DDS' dept_code, 'N' can_read_contents, 'Y' active FROM dual) source
ON (target.user_id = source.user_id)
WHEN MATCHED THEN UPDATE SET target.user_name = source.user_name, target.employee_no = source.employee_no,
target.dept_code = source.dept_code, target.can_read_contents = source.can_read_contents, target.active = source.active
WHEN NOT MATCHED THEN INSERT (user_id, user_name, employee_no, dept_code, can_read_contents, active)
VALUES (source.user_id, source.user_name, source.employee_no, source.dept_code, source.can_read_contents, source.active);
MERGE INTO cb_app_role target
USING (SELECT 9904 role_id, 'DDS_FIXTURE_ALLOW_ROLE' role_name, 'INTERNAL' max_sensitivity_level FROM dual) source
ON (target.role_id = source.role_id)
WHEN MATCHED THEN UPDATE SET target.role_name = source.role_name, target.max_sensitivity_level = source.max_sensitivity_level
WHEN NOT MATCHED THEN INSERT (role_id, role_name, max_sensitivity_level)
VALUES (source.role_id, source.role_name, source.max_sensitivity_level);
MERGE INTO cb_user_role target
USING (SELECT 9904 user_id, 9904 role_id FROM dual) source
ON (target.user_id = source.user_id AND target.role_id = source.role_id)
WHEN NOT MATCHED THEN INSERT (user_id, role_id) VALUES (source.user_id, source.role_id);
MERGE INTO cb_permission target
USING (SELECT 9904 perm_id, 9904 role_id, 'CB_VECTOR_SEARCH_DOCUMENTS' target_name,
'SELECT' action_name, 'ALLOW' permission_effect FROM dual) source
ON (target.perm_id = source.perm_id)
WHEN MATCHED THEN UPDATE SET target.role_id = source.role_id, target.target_name = source.target_name,
target.action_name = source.action_name, target.permission_effect = source.permission_effect
WHEN NOT MATCHED THEN INSERT (perm_id, role_id, target_name, action_name, permission_effect)
VALUES (source.perm_id, source.role_id, source.target_name, source.action_name, source.permission_effect);
MERGE INTO cb_permission_rule target
USING (SELECT 9904 rule_id, 9904 perm_id, 'TECH_TAG' rule_column, 'TAG' rule_type,
'DDS_VERIFY_ALLOW' rule_value FROM dual) source
ON (target.rule_id = source.rule_id)
WHEN MATCHED THEN UPDATE SET target.perm_id = source.perm_id, target.rule_column = source.rule_column,
target.rule_type = source.rule_type, target.rule_value = source.rule_value
WHEN NOT MATCHED THEN INSERT (rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (source.rule_id, source.perm_id, source.rule_column, source.rule_type, source.rule_value);
MERGE INTO cb_vector_document_chunk target
USING (SELECT 29040 chunk_id, 'dds-fixture-service-allow' document_id, 1 chunk_no,
'DDS verification allow fixture' title, TO_CLOB('Deterministic allow fixture.') chunk_text,
'fixture://dds/service/allow' source_uri, TO_VECTOR('[0.11,0.22,0.33,0.44]') embedding FROM dual) source
ON (target.chunk_id = source.chunk_id)
WHEN MATCHED THEN UPDATE SET target.document_id = source.document_id, target.chunk_no = source.chunk_no,
target.title = source.title, target.chunk_text = source.chunk_text, target.source_uri = source.source_uri, target.embedding = source.embedding
WHEN NOT MATCHED THEN INSERT (chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding)
VALUES (source.chunk_id, source.document_id, source.chunk_no, source.title, source.chunk_text, source.source_uri, source.embedding);
MERGE INTO cb_vector_document_chunk target
USING (SELECT 29041 chunk_id, 'dds-fixture-service-deny' document_id, 1 chunk_no,
'DDS verification deny fixture' title, TO_CLOB('Deterministic deny fixture.') chunk_text,
'fixture://dds/service/deny' source_uri, TO_VECTOR('[0.12,0.23,0.34,0.45]') embedding FROM dual) source
ON (target.chunk_id = source.chunk_id)
WHEN MATCHED THEN UPDATE SET target.document_id = source.document_id, target.chunk_no = source.chunk_no,
target.title = source.title, target.chunk_text = source.chunk_text, target.source_uri = source.source_uri, target.embedding = source.embedding
WHEN NOT MATCHED THEN INSERT (chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding)
VALUES (source.chunk_id, source.document_id, source.chunk_no, source.title, source.chunk_text, source.source_uri, source.embedding);
MERGE INTO cb_vector_document_chunk target
USING (SELECT 29042 chunk_id, 'dds-fixture-direct' document_id, 1 chunk_no,
'DDS direct comparison fixture' title, TO_CLOB('Deterministic direct fixture.') chunk_text,
'fixture://dds/direct' source_uri, TO_VECTOR('[0.13,0.24,0.35,0.46]') embedding FROM dual) source
ON (target.chunk_id = source.chunk_id)
WHEN MATCHED THEN UPDATE SET target.document_id = source.document_id, target.chunk_no = source.chunk_no,
target.title = source.title, target.chunk_text = source.chunk_text, target.source_uri = source.source_uri, target.embedding = source.embedding
WHEN NOT MATCHED THEN INSERT (chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding)
VALUES (source.chunk_id, source.document_id, source.chunk_no, source.title, source.chunk_text, source.source_uri, source.embedding);
MERGE INTO cb_vector_document_tag target USING (SELECT 29040 chunk_id, 'DDS_VERIFY_ALLOW' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag) VALUES (source.chunk_id, source.tech_tag);
MERGE INTO cb_vector_document_tag target USING (SELECT 29041 chunk_id, 'DDS_VERIFY_DENY' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag) VALUES (source.chunk_id, source.tech_tag);
MERGE INTO cb_vector_document_tag target USING (SELECT 29042 chunk_id, 'ORACLE_DDS' tech_tag FROM dual) source
ON (target.chunk_id = source.chunk_id AND target.tech_tag = source.tech_tag)
WHEN NOT MATCHED THEN INSERT (chunk_id, tech_tag) VALUES (source.chunk_id, source.tech_tag);
PROMPT === 3. Registering fixture manifest ===
MERGE INTO cb_dds_validation_fixture target
USING (SELECT 'SERVICE_ALLOW' fixture_id, 'SERVICE' enforcement_path, 'service' subject_key, 9904 application_user_id,
'dds-fixture-service-allow' document_id, 'Y' expected_visible, 'Temporary token sees allowed tag' description FROM dual) source
ON (target.fixture_id = source.fixture_id)
WHEN MATCHED THEN UPDATE SET target.enforcement_path = source.enforcement_path, target.subject_key = source.subject_key,
target.application_user_id = source.application_user_id, target.document_id = source.document_id,
target.expected_visible = source.expected_visible, target.description = source.description, target.active_yn = 'Y'
WHEN NOT MATCHED THEN INSERT (fixture_id, enforcement_path, subject_key, application_user_id, document_id, expected_visible, description)
VALUES (source.fixture_id, source.enforcement_path, source.subject_key, source.application_user_id, source.document_id, source.expected_visible, source.description);
MERGE INTO cb_dds_validation_fixture target
USING (SELECT 'SERVICE_DENY' fixture_id, 'SERVICE' enforcement_path, 'service' subject_key, 9904 application_user_id,
'dds-fixture-service-deny' document_id, 'N' expected_visible, 'Temporary token cannot see denied tag' description FROM dual) source
ON (target.fixture_id = source.fixture_id)
WHEN MATCHED THEN UPDATE SET target.enforcement_path = source.enforcement_path, target.subject_key = source.subject_key,
target.application_user_id = source.application_user_id, target.document_id = source.document_id,
target.expected_visible = source.expected_visible, target.description = source.description, target.active_yn = 'Y'
WHEN NOT MATCHED THEN INSERT (fixture_id, enforcement_path, subject_key, application_user_id, document_id, expected_visible, description)
VALUES (source.fixture_id, source.enforcement_path, source.subject_key, source.application_user_id, source.document_id, source.expected_visible, source.description);
MERGE INTO cb_dds_validation_fixture target
USING (SELECT 'DIRECT_ALLOW' fixture_id, 'DIRECT' enforcement_path, 'both' subject_key, 0 application_user_id,
'dds-fixture-direct' document_id, 'Y' expected_visible, 'Combined DDS profile sees direct fixture' description FROM dual) source
ON (target.fixture_id = source.fixture_id)
WHEN MATCHED THEN UPDATE SET target.subject_key = source.subject_key, target.document_id = source.document_id,
target.expected_visible = source.expected_visible, target.description = source.description, target.active_yn = 'Y'
WHEN NOT MATCHED THEN INSERT (fixture_id, enforcement_path, subject_key, application_user_id, document_id, expected_visible, description)
VALUES (source.fixture_id, source.enforcement_path, source.subject_key, source.application_user_id, source.document_id, source.expected_visible, source.description);
MERGE INTO cb_dds_validation_fixture target
USING (SELECT 'DIRECT_DENY' fixture_id, 'DIRECT' enforcement_path, 'none' subject_key, 0 application_user_id,
'dds-fixture-direct' document_id, 'N' expected_visible, 'Default deny DDS profile cannot see fixture' description FROM dual) source
ON (target.fixture_id = source.fixture_id)
WHEN MATCHED THEN UPDATE SET target.subject_key = source.subject_key, target.document_id = source.document_id,
target.expected_visible = source.expected_visible, target.description = source.description, target.active_yn = 'Y'
WHEN NOT MATCHED THEN INSERT (fixture_id, enforcement_path, subject_key, application_user_id, document_id, expected_visible, description)
VALUES (source.fixture_id, source.enforcement_path, source.subject_key, source.application_user_id, source.document_id, source.expected_visible, source.description);
PROMPT === 4. Recording the service baseline ===
MERGE INTO cb_dds_protection_snapshot target
USING (
SELECT 'SERVICE' enforcement_path, 'service' subject_key,
STANDARD_HASH('SERVICE|ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS|CB_DDS_TOKEN_ROLE|DDS_DEMO_TOKEN_VECTOR_GRANT|ADMIN.CB_DDS_VECTOR_TAG_ALLOWED', 'SHA256') fingerprint
FROM dual
) source
ON (target.enforcement_path = source.enforcement_path AND target.subject_key = source.subject_key)
WHEN NOT MATCHED THEN INSERT (snapshot_id, object_name, enforcement_path, subject_key, fingerprint, actor, note, published_at)
VALUES (cb_dds_protection_snapshot_seq.NEXTVAL, 'ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS', source.enforcement_path,
source.subject_key, source.fingerprint, 'DDS migration', 'Initial token-service baseline', SYSTIMESTAMP);
COMMIT;
SELECT fixture_id, enforcement_path, subject_key, expected_visible
FROM cb_dds_validation_fixture
ORDER BY fixture_id;
PROMPT === DDS protection evidence setup complete ===
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,70 @@
-- ============================================================
-- 42_agent_ords_fga_execution_audit.sql
-- Durable, request-correlated execution evidence for VPD probes.
--
-- Run as the protected-object owner (ADMIN in this PoC), after the
-- VPD policies and CB_PROTECTED_OBJECT entries have been applied.
--
-- Each enabled protected object gets an FGA SELECT policy. Autonomous Oracle
-- writes SQL_TEXT and RLS_INFO to UNIFIED_AUDIT_TRAIL (traditional databases
-- can expose the same information through DBA_FGA_AUDIT_TRAIL). The ORDS handler puts the
-- backoffice-generated X-VPD-Probe-Id into CLIENT_ID, so the UI can read
-- exactly this request's audit row rather than guess from V$SQL.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
PROMPT === Applying FGA execution-audit policies ===
DECLARE
v_policy_name VARCHAR2(128);
BEGIN
FOR r IN (
SELECT object_id, owner, object_name
FROM cb_protected_object
WHERE enabled_yn = 'Y'
ORDER BY object_id
) LOOP
IF UPPER(r.owner) <> USER THEN
RAISE_APPLICATION_ERROR(
-20842,
'Run this script as protected object owner ' || r.owner
|| ' (current user: ' || USER || ')'
);
END IF;
v_policy_name := 'CB_VPD_EXEC_AUDIT_' || r.object_id;
BEGIN
DBMS_FGA.DROP_POLICY(
object_schema => r.owner,
object_name => r.object_name,
policy_name => v_policy_name
);
EXCEPTION
WHEN OTHERS THEN
-- ORA-28102: policy does not exist. Other errors must stop setup.
IF SQLCODE <> -28102 THEN
RAISE;
END IF;
END;
DBMS_FGA.ADD_POLICY(
object_schema => r.owner,
object_name => r.object_name,
policy_name => v_policy_name,
audit_condition => NULL,
audit_column => NULL,
statement_types => 'SELECT',
audit_trail => DBMS_FGA.DB + DBMS_FGA.EXTENDED,
enable => TRUE
);
END LOOP;
END;
/
COMMIT;
PROMPT === FGA policies are ready ===
PROMPT Run 22, 29 and 43 as CB_ORDS, then run a new permission-result probe.
PROMPT The resulting row is read from UNIFIED_AUDIT_TRAIL by CLIENT_IDENTIFIER.
EXIT;

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

@@ -0,0 +1,59 @@
-- ============================================================
-- 43_agent_ords_probe_id_handler_patch.sql
-- Adds X-VPD-Probe-Id to existing generic cb.object.query handlers.
--
-- Run as CB_ORDS after 22_agent_ords_security_ords_handler_setup.sql.
-- The dedicated document/vector handlers are updated by 22/29 themselves.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
PROMPT === Patching generic object-query handlers for FGA correlation ===
DECLARE
v_source CLOB;
BEGIN
FOR r IN (
SELECT m.name AS module_name,
t.uri_template,
h.method,
h.source
FROM user_ords_handlers h
JOIN user_ords_templates t ON t.id = h.template_id
JOIN user_ords_modules m ON m.id = t.module_id
WHERE m.name = 'cb.object.query'
AND h.method = 'POST'
AND INSTR(h.source, 'cb_ords_handler_pkg.set_vpd_context(:auth_header)') > 0
) LOOP
v_source := REPLACE(
r.source,
'cb_ords_handler_pkg.set_vpd_context(:auth_header)',
'cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id)'
);
ORDS.DEFINE_HANDLER(
p_module_name => r.module_name,
p_pattern => r.uri_template,
p_method => r.method,
p_source_type => ORDS.source_type_plsql,
p_source => v_source,
p_items_per_page => 0
);
ORDS.DEFINE_PARAMETER(
p_module_name => r.module_name,
p_pattern => r.uri_template,
p_method => r.method,
p_name => 'X-VPD-Probe-Id',
p_bind_variable_name => 'probe_id',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN'
);
END LOOP;
END;
/
COMMIT;
PROMPT === Generic handler patch complete ===
EXIT;

View File

@@ -0,0 +1,244 @@
-- ============================================================
-- 44_dds_mcp_local_end_user_setup.sql
--
-- Publishes every active CB_APP_USER as a passwordless local DDS END USER
-- for the DDS SSE MCP path. The application executes one protected query
-- per request with an EndUserSecurityContext; direct DB logon is not enabled.
--
-- Permission source: CB_APP_USER + direct/group role assignments +
-- CB_PERMISSION / CB_PERMISSION_RULE / CB_PERMISSION_COLUMN.
-- Current protected target: ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS.
--
-- Prerequisite: 25_agent_ords_security_backoffice_support.sql,
-- 28_agent_ords_vector_tag_vpd_setup.sql,
-- 32_dds_vector_tag_setup.sql.
-- Run as ADMIN on Oracle AI Database 26ai.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Persisting the published local END USER mapping ===
BEGIN
EXECUTE IMMEDIATE q'[
CREATE TABLE cb_dds_end_user_map (
application_user_id NUMBER PRIMARY KEY REFERENCES cb_app_user(user_id),
end_user_name VARCHAR2(128) NOT NULL UNIQUE,
data_role_name VARCHAR2(128) NOT NULL UNIQUE,
lookup_key_ref VARCHAR2(128) NOT NULL,
grant_name VARCHAR2(128) NOT NULL,
publish_status VARCHAR2(20) NOT NULL
CHECK (publish_status IN ('PENDING', 'PUBLISHED', 'REVOKED', 'FAILED')),
published_at TIMESTAMP,
last_error VARCHAR2(1000)
)]';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE <> -955 THEN RAISE; END IF;
END;
/
MERGE INTO cb_dds_end_user_map target
USING (
SELECT user_id AS application_user_id,
'DDS_U_' || TO_CHAR(user_id) AS end_user_name,
'DDS_U_' || TO_CHAR(user_id) || '_ROLE' AS data_role_name,
'DDS_OCI_IAM_CLIENT_SECRET_DERIVED_V1' AS lookup_key_ref,
'DDS_MCP_U_' || TO_CHAR(user_id) || '_VECTOR_GRANT' AS grant_name
FROM cb_app_user
) source
ON (target.application_user_id = source.application_user_id)
WHEN MATCHED THEN UPDATE SET
target.end_user_name = source.end_user_name,
target.data_role_name = source.data_role_name,
target.lookup_key_ref = source.lookup_key_ref,
target.grant_name = source.grant_name
WHEN NOT MATCHED THEN INSERT (
application_user_id, end_user_name, data_role_name, lookup_key_ref,
grant_name, publish_status
) VALUES (
source.application_user_id, source.end_user_name, source.data_role_name,
source.lookup_key_ref, source.grant_name, 'PENDING'
);
PROMPT === 2. Publishing END USER, DATA ROLE, and configured vector grants ===
DECLARE
v_predicate VARCHAR2(32767);
v_excluded_columns VARCHAR2(32767);
v_select_clause VARCHAR2(32767);
PROCEDURE drop_data_grant(p_grant_name IN VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE 'DROP DATA GRANT ADMIN.' || p_grant_name;
EXCEPTION
WHEN OTHERS THEN NULL; -- First publish has no generated grant.
END;
PROCEDURE revoke_data_role(p_role_name IN VARCHAR2, p_end_user_name IN VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE 'REVOKE DATA ROLE ' || p_role_name || ' FROM "' || p_end_user_name || '"';
EXCEPTION
WHEN OTHERS THEN NULL; -- Revoke is intentionally idempotent in this publisher.
END;
FUNCTION vector_predicate(p_user_id IN NUMBER) RETURN VARCHAR2 IS
v_allow VARCHAR2(32767);
v_deny VARCHAR2(32767);
v_clause VARCHAR2(2000);
v_effect VARCHAR2(10);
v_type VARCHAR2(30);
v_value VARCHAR2(100);
PROCEDURE append_clause(p_target IN OUT NOCOPY VARCHAR2, p_clause IN VARCHAR2) IS
BEGIN
IF p_clause IS NULL THEN RETURN; END IF;
IF p_target IS NULL THEN p_target := '(' || p_clause || ')';
ELSE p_target := p_target || ' OR (' || p_clause || ')'; END IF;
END;
BEGIN
FOR rule IN (
WITH effective_role AS (
SELECT role_id FROM cb_user_role WHERE user_id = p_user_id
UNION
SELECT gr.role_id
FROM cb_user_group ug
JOIN cb_app_group g ON g.group_id = ug.group_id AND g.active_yn = 'Y'
JOIN cb_group_role gr ON gr.group_id = ug.group_id
WHERE ug.user_id = p_user_id
)
SELECT NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') AS permission_effect,
UPPER(TRIM(r.rule_type)) AS rule_type,
TRIM(r.rule_value) AS rule_value
FROM effective_role er
JOIN cb_permission p ON p.role_id = er.role_id
JOIN cb_permission_rule r ON r.perm_id = p.perm_id
WHERE p.target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND p.action_name = 'SELECT'
ORDER BY p.perm_id, r.rule_id
) LOOP
v_effect := rule.permission_effect;
v_type := rule.rule_type;
v_value := rule.rule_value;
v_clause := NULL;
IF v_type = 'ALL' THEN
v_clause := '1 = 1';
ELSIF v_type = 'TAG'
AND v_value IS NOT NULL
AND REGEXP_LIKE(v_value, '^[A-Za-z0-9_-]+$') THEN
v_clause := 'REGEXP_LIKE(UPPER(tech_tag), ''(^|,)' || UPPER(v_value) || '(,|$)'')';
END IF;
IF v_effect = 'DENY' THEN append_clause(v_deny, v_clause);
ELSE append_clause(v_allow, v_clause); END IF;
END LOOP;
IF v_allow IS NULL THEN RETURN NULL; END IF;
IF v_deny IS NULL THEN RETURN v_allow; END IF;
RETURN '(' || v_allow || ') AND NOT (' || v_deny || ')';
END;
FUNCTION excluded_vector_columns(p_user_id IN NUMBER) RETURN VARCHAR2 IS
v_columns VARCHAR2(32767);
BEGIN
SELECT LISTAGG(column_name, ', ') WITHIN GROUP (ORDER BY column_name)
INTO v_columns
FROM (
SELECT pc.column_name
FROM cb_protected_column pc
JOIN cb_protected_object po ON po.object_id = pc.object_id
WHERE po.object_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND pc.sensitive_yn = 'Y'
-- VECTOR_DISTANCE needs EMBEDDING internally. The MCP response never
-- returns it, but it must stay readable by the database engine.
AND pc.column_name <> 'EMBEDDING'
AND NOT EXISTS (
WITH effective_role AS (
SELECT role_id FROM cb_user_role WHERE user_id = p_user_id
UNION
SELECT gr.role_id
FROM cb_user_group ug
JOIN cb_app_group g ON g.group_id = ug.group_id AND g.active_yn = 'Y'
JOIN cb_group_role gr ON gr.group_id = ug.group_id
WHERE ug.user_id = p_user_id
)
SELECT 1
FROM effective_role er
JOIN cb_permission p ON p.role_id = er.role_id
JOIN cb_permission_column allowed ON allowed.permission_id = p.perm_id
WHERE p.target_name = 'CB_VECTOR_SEARCH_DOCUMENTS'
AND p.action_name = 'SELECT'
AND p.permission_effect = 'ALLOW'
AND allowed.column_name = pc.column_name
)
);
RETURN v_columns;
EXCEPTION
WHEN NO_DATA_FOUND THEN RETURN NULL;
END;
BEGIN
FOR mapped_user IN (
SELECT m.application_user_id, m.end_user_name, m.data_role_name, m.grant_name
FROM cb_dds_end_user_map m
JOIN cb_app_user u ON u.user_id = m.application_user_id
WHERE u.active = 'Y'
ORDER BY m.application_user_id
) LOOP
EXECUTE IMMEDIATE 'CREATE END USER IF NOT EXISTS "' || mapped_user.end_user_name || '"';
EXECUTE IMMEDIATE 'CREATE DATA ROLE IF NOT EXISTS ' || mapped_user.data_role_name;
EXECUTE IMMEDIATE 'GRANT DATA ROLE ' || mapped_user.data_role_name
|| ' TO "' || mapped_user.end_user_name || '"';
v_predicate := vector_predicate(mapped_user.application_user_id);
drop_data_grant(mapped_user.grant_name);
IF v_predicate IS NOT NULL THEN
v_excluded_columns := excluded_vector_columns(mapped_user.application_user_id);
v_select_clause := CASE WHEN v_excluded_columns IS NULL THEN 'AS SELECT'
ELSE 'AS SELECT (ALL COLUMNS EXCEPT ' || v_excluded_columns || ')' END;
EXECUTE IMMEDIATE 'CREATE DATA GRANT ADMIN.' || mapped_user.grant_name || ' '
|| v_select_clause || ' ON ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS WHERE '
|| v_predicate || ' TO ' || mapped_user.data_role_name;
END IF;
UPDATE cb_dds_end_user_map
SET publish_status = 'PUBLISHED', published_at = SYSTIMESTAMP, last_error = NULL
WHERE application_user_id = mapped_user.application_user_id;
END LOOP;
-- A disabled application user must not retain a usable DDS role/grant.
FOR inactive_user IN (
SELECT m.application_user_id, m.end_user_name, m.data_role_name, m.grant_name
FROM cb_dds_end_user_map m
JOIN cb_app_user u ON u.user_id = m.application_user_id
WHERE u.active <> 'Y'
) LOOP
drop_data_grant(inactive_user.grant_name);
revoke_data_role(inactive_user.data_role_name, inactive_user.end_user_name);
UPDATE cb_dds_end_user_map
SET publish_status = 'REVOKED', published_at = SYSTIMESTAMP, last_error = NULL
WHERE application_user_id = inactive_user.application_user_id;
END LOOP;
COMMIT;
END;
/
PROMPT === 3. Published DDS MCP identity inventory ===
SELECT m.application_user_id,
u.user_name,
m.end_user_name,
m.data_role_name,
m.grant_name,
m.publish_status
FROM cb_dds_end_user_map m
JOIN cb_app_user u ON u.user_id = m.application_user_id
ORDER BY m.application_user_id;
SELECT rg.grantee AS end_user_name,
rg.data_role,
dg.grant_name,
dg.object_name
FROM dba_data_role_grants rg
LEFT JOIN dba_data_grants dg ON dg.grantee = rg.data_role
WHERE rg.grantee LIKE 'DDS\_U\_%' ESCAPE '\'
ORDER BY rg.grantee, dg.grant_name;
PROMPT === DDS MCP local END USER publish complete ===
EXIT;

View File

@@ -0,0 +1,59 @@
-- ============================================================
-- 45_dds_mcp_local_end_user_test.sql
-- Catalog-level acceptance test for 44. Run as ADMIN.
-- The SSE/JDBC context assertion is performed by dds_vector_search after
-- the OCI IAM database-access token configuration is supplied.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET LINESIZE 220
SET PAGESIZE 200
DECLARE
v_missing_map NUMBER;
v_missing_end_user NUMBER;
v_missing_role NUMBER;
BEGIN
SELECT COUNT(*) INTO v_missing_map
FROM cb_app_user u
LEFT JOIN cb_dds_end_user_map m ON m.application_user_id = u.user_id
WHERE u.active = 'Y'
AND (m.application_user_id IS NULL OR m.publish_status <> 'PUBLISHED');
SELECT COUNT(*) INTO v_missing_end_user
FROM cb_dds_end_user_map m
JOIN cb_app_user u ON u.user_id = m.application_user_id AND u.active = 'Y'
LEFT JOIN dba_end_users eu ON eu.username = m.end_user_name
WHERE eu.username IS NULL;
SELECT COUNT(*) INTO v_missing_role
FROM cb_dds_end_user_map m
JOIN cb_app_user u ON u.user_id = m.application_user_id AND u.active = 'Y'
LEFT JOIN dba_data_role_grants rg
ON rg.grantee = m.end_user_name
AND rg.data_role = m.data_role_name
WHERE rg.data_role IS NULL;
IF v_missing_map <> 0 OR v_missing_end_user <> 0 OR v_missing_role <> 0 THEN
RAISE_APPLICATION_ERROR(-20944,
'DDS MCP publish validation failed: map=' || v_missing_map
|| ', end_user=' || v_missing_end_user || ', data_role=' || v_missing_role);
END IF;
END;
/
COLUMN user_name FORMAT A22
COLUMN end_user_name FORMAT A18
COLUMN data_role_name FORMAT A24
COLUMN grant_name FORMAT A34
SELECT u.user_name, m.end_user_name, m.data_role_name, m.grant_name,
CASE WHEN dg.grant_name IS NULL THEN 'DEFAULT_DENY' ELSE 'PUBLISHED' END AS vector_access
FROM cb_dds_end_user_map m
JOIN cb_app_user u ON u.user_id = m.application_user_id
LEFT JOIN dba_data_grants dg ON dg.grant_name = m.grant_name
WHERE u.active = 'Y'
ORDER BY m.application_user_id;
PROMPT Catalog validation passed. Next: call /dds/mcp/sse and dds_vector_search with a valid MCP Bearer.
EXIT;

View File

@@ -0,0 +1,471 @@
-- ============================================================
-- 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 ===

View File

@@ -0,0 +1,104 @@
-- ============================================================
-- 47_kb_stakeholder_permission_or_branches.sql
--
-- Refines the KB example so the permission table carries the expected
-- stakeholder role and each role branch becomes an OR candidate in the
-- dynamic VPD filter. Execute after 26 and 46 as ADMIN.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Storing expected stakeholder role in example rules ===
UPDATE cb_permission_rule r
SET rule_value = CASE
WHEN EXISTS (
SELECT 1
FROM cb_permission p
JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id
AND ar.role_name = 'KB_DESIGNER_ROLE'
) THEN '설계사'
WHEN EXISTS (
SELECT 1
FROM cb_permission p
JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id
AND ar.role_name = 'KB_BRANCH_MANAGER_ROLE'
) THEN '지점장'
END
WHERE r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL')
AND EXISTS (
SELECT 1
FROM cb_permission p
JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id
AND ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
);
PROMPT === 2. Adding an identity role so every stakeholder token can resolve itself ===
DECLARE
v_role_id NUMBER;
v_permission_id NUMBER;
v_count NUMBER;
BEGIN
BEGIN
SELECT role_id INTO v_role_id
FROM cb_app_role
WHERE role_name = 'KB_STAKEHOLDER_IDENTITY_ROLE';
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(role_id), 0) + 1 INTO v_role_id FROM cb_app_role;
INSERT INTO cb_app_role(role_id, role_name, max_sensitivity_level)
VALUES (v_role_id, 'KB_STAKEHOLDER_IDENTITY_ROLE', 'INTERNAL');
END;
FOR u IN (
SELECT user_id
FROM cb_app_user
WHERE stakeholder_user_id IS NOT NULL
AND active = 'Y'
) LOOP
SELECT COUNT(*) INTO v_count
FROM cb_user_role
WHERE user_id = u.user_id
AND role_id = v_role_id;
IF v_count = 0 THEN
INSERT INTO cb_user_role(user_id, role_id) VALUES (u.user_id, v_role_id);
END IF;
END LOOP;
BEGIN
SELECT perm_id INTO v_permission_id
FROM cb_permission
WHERE role_id = v_role_id
AND target_name = 'KB_STAKEHOLDERS'
AND action_name = 'SELECT';
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT cb_permission_seq.NEXTVAL INTO v_permission_id FROM dual;
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (v_permission_id, v_role_id, 'KB_STAKEHOLDERS', 'SELECT', 'ALLOW');
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, 'USER_ID', 'TOKEN_SUBJECT', NULL);
END;
END;
/
COMMIT;
PROMPT === 3. Result: role-specific permission branches ===
SELECT ar.role_name,
p.target_name,
r.rule_column,
r.rule_type,
NVL(r.rule_value, '(token role)') AS expected_stakeholder_role
FROM cb_permission p
JOIN cb_app_role ar ON ar.role_id = p.role_id
JOIN cb_permission_rule r ON r.perm_id = p.perm_id
WHERE ar.role_name IN (
'KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE', 'KB_STAKEHOLDER_IDENTITY_ROLE'
)
ORDER BY ar.role_name, p.target_name, r.rule_id;
PROMPT === KB role branches are combined as OR by CB_AGENT_DOC_VPD_FILTER ===

View File

@@ -0,0 +1,137 @@
-- ============================================================
-- 48_kb_permission_condition_codes.sql
--
-- Replaces the first KB example's stakeholder-specific predicate types with
-- policy condition codes. CB_AGENT_DOC_VPD_FILTER expands these codes into
-- the target object's WHERE predicate.
-- Execute as ADMIN after 25 and 46.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
ALTER SESSION DISABLE PARALLEL DML;
PROMPT === 1. Replacing KB example rules with filter condition codes ===
UPDATE cb_permission_rule r
SET rule_type = CASE
WHEN EXISTS (
SELECT 1 FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id AND ar.role_name = 'KB_DESIGNER_ROLE'
AND p.target_name = 'KB_CONTRACTS'
) THEN 'OWN_CONTRACT'
WHEN EXISTS (
SELECT 1 FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id AND ar.role_name = 'KB_BRANCH_MANAGER_ROLE'
AND p.target_name = 'KB_CONTRACTS'
) THEN 'CHANNEL_CONTRACT'
WHEN EXISTS (
SELECT 1 FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id AND ar.role_name = 'KB_DESIGNER_ROLE'
AND p.target_name IN ('KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
) THEN 'OWN_CUSTOMER'
WHEN EXISTS (
SELECT 1 FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id AND ar.role_name = 'KB_BRANCH_MANAGER_ROLE'
AND p.target_name IN ('KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
) THEN 'CHANNEL_CUSTOMER'
ELSE r.rule_type
END,
rule_value = NULL
WHERE r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL')
AND EXISTS (
SELECT 1
FROM cb_permission p
JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE p.perm_id = r.perm_id
AND ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
AND p.target_name IN ('KB_CONTRACTS', 'KB_CUSTOMERS', 'KB_COVERAGES', 'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS')
);
COMMIT;
PROMPT === 2. Keeping token subject access as an identity-only permission ===
DECLARE
v_identity_role_id NUMBER;
v_permission_id NUMBER;
v_count NUMBER;
BEGIN
BEGIN
SELECT role_id INTO v_identity_role_id
FROM cb_app_role
WHERE role_name = 'KB_STAKEHOLDER_IDENTITY_ROLE';
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(role_id), 0) + 1 INTO v_identity_role_id FROM cb_app_role;
INSERT INTO cb_app_role(role_id, role_name, max_sensitivity_level)
VALUES (v_identity_role_id, 'KB_STAKEHOLDER_IDENTITY_ROLE', 'INTERNAL');
END;
FOR u IN (
SELECT user_id FROM cb_app_user
WHERE stakeholder_user_id IS NOT NULL AND active = 'Y'
) LOOP
SELECT COUNT(*) INTO v_count
FROM cb_user_role
WHERE user_id = u.user_id AND role_id = v_identity_role_id;
IF v_count = 0 THEN
INSERT INTO cb_user_role(user_id, role_id) VALUES (u.user_id, v_identity_role_id);
END IF;
END LOOP;
-- Business roles do not need to grant the stakeholder source row. The
-- identity role alone lets a valid token resolve its own stakeholder row.
DELETE FROM cb_permission_column
WHERE permission_id IN (
SELECT p.perm_id
FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
AND p.target_name = 'KB_STAKEHOLDERS'
);
DELETE FROM cb_permission_rule
WHERE perm_id IN (
SELECT p.perm_id
FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
AND p.target_name = 'KB_STAKEHOLDERS'
);
DELETE FROM cb_permission
WHERE perm_id IN (
SELECT p.perm_id
FROM cb_permission p JOIN cb_app_role ar ON ar.role_id = p.role_id
WHERE ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
AND p.target_name = 'KB_STAKEHOLDERS'
);
BEGIN
SELECT perm_id INTO v_permission_id
FROM cb_permission
WHERE role_id = v_identity_role_id
AND target_name = 'KB_STAKEHOLDERS'
AND action_name = 'SELECT';
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT cb_permission_seq.NEXTVAL INTO v_permission_id FROM dual;
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
VALUES (v_permission_id, v_identity_role_id, 'KB_STAKEHOLDERS', 'SELECT', 'ALLOW');
END;
DELETE FROM cb_permission_rule WHERE perm_id = v_permission_id;
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, 'USER_ID', 'TOKEN_SUBJECT', NULL);
END;
/
COMMIT;
PROMPT === 3. Condition codes consumed by the VPD filter ===
SELECT ar.role_name, p.target_name, r.rule_column, r.rule_type
FROM cb_permission p
JOIN cb_app_role ar ON ar.role_id = p.role_id
JOIN cb_permission_rule r ON r.perm_id = p.perm_id
WHERE ar.role_name IN (
'KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE', 'KB_STAKEHOLDER_IDENTITY_ROLE'
)
ORDER BY ar.role_name, p.target_name, r.rule_id;
PROMPT === KB permission condition codes ready ===

View File

@@ -0,0 +1,35 @@
-- ============================================================
-- 49_permission_static_sql_condition.sql
--
-- Makes room for a literal, static WHERE condition attached to an
-- allowlisted permission rule. The VPD function validates the expression
-- again before returning it as part of a predicate.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Enlarging rule value for static SQL conditions ===
DECLARE
v_data_length NUMBER;
BEGIN
SELECT data_length
INTO v_data_length
FROM user_tab_columns
WHERE table_name = 'CB_PERMISSION_RULE'
AND column_name = 'RULE_VALUE';
IF v_data_length < 1000 THEN
EXECUTE IMMEDIATE
'ALTER TABLE cb_permission_rule MODIFY (rule_value VARCHAR2(1000 CHAR))';
END IF;
END;
/
SELECT column_name, data_type, char_length
FROM user_tab_columns
WHERE table_name = 'CB_PERMISSION_RULE'
AND column_name = 'RULE_VALUE';
PROMPT === Static SQL condition storage ready ===

View File

@@ -0,0 +1,228 @@
-- ============================================================
-- 51_kb_vpd_admin_token.sql
--
-- Creates an application-level KB VPD administrator. This is not an
-- EXEMPT ACCESS POLICY database account: the token is resolved to an
-- application context and the existing VPD policy function returns ALL
-- only because KB_VPD_ADMIN_ROLE has explicit ALL permissions.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
SET SERVEROUTPUT ON
PROMPT === Creating KB VPD administrator, permissions, and one-time token ===
GRANT EXECUTE ON cb_agent_ctx_pkg TO POC_2;
DECLARE
c_stakeholder_user_id CONSTANT VARCHAR2(30) := 'KB_VPD_ADMIN';
c_role_name CONSTANT VARCHAR2(80) := 'KB_VPD_ADMIN_ROLE';
c_token_description CONSTANT VARCHAR2(200) := 'KB VPD admin full-access token';
v_admin_role_id NUMBER;
v_identity_role_id NUMBER;
v_app_user_id NUMBER;
v_permission_id NUMBER;
v_bearer_token VARCHAR2(128);
v_count NUMBER;
PROCEDURE ensure_role(
p_role_name IN VARCHAR2,
p_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_level);
END;
PROCEDURE ensure_user_role(p_user_id IN NUMBER, p_role_id IN NUMBER) IS
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_all_permission(p_target_name IN VARCHAR2) IS
BEGIN
SELECT MIN(perm_id)
INTO v_permission_id
FROM cb_permission
WHERE role_id = v_admin_role_id
AND target_name = p_target_name
AND action_name = 'SELECT';
IF v_permission_id IS NULL THEN
SELECT cb_permission_seq.NEXTVAL INTO v_permission_id FROM dual;
INSERT INTO cb_permission(
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
v_permission_id, v_admin_role_id, p_target_name, 'SELECT', 'ALLOW'
);
ELSE
UPDATE cb_permission
SET permission_effect = 'ALLOW'
WHERE perm_id = v_permission_id;
END IF;
DELETE FROM cb_permission_rule WHERE perm_id = v_permission_id;
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, NULL, 'STATIC_SQL', '1 = 1');
-- CB_ORDS is the non-exempt data runtime. VPD still applies after
-- this object privilege is granted.
EXECUTE IMMEDIATE
'GRANT SELECT ON POC_2.' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_target_name) || ' TO CB_ORDS';
END;
PROCEDURE allow_original_column(
p_target_name IN VARCHAR2,
p_column_name IN VARCHAR2
) IS
BEGIN
SELECT MIN(perm_id)
INTO v_permission_id
FROM cb_permission
WHERE role_id = v_admin_role_id
AND target_name = p_target_name
AND action_name = 'SELECT';
SELECT COUNT(*)
INTO v_count
FROM cb_permission_column
WHERE permission_id = v_permission_id
AND column_name = p_column_name;
IF v_count = 0 THEN
INSERT INTO cb_permission_column(permission_id, column_name)
VALUES (v_permission_id, p_column_name);
END IF;
END;
BEGIN
MERGE INTO poc_2.kb_stakeholders target
USING (
SELECT c_stakeholder_user_id AS user_id,
'VPD 관리자' AS user_nm,
'관리자' AS role,
'관리자채널' AS channel,
'전체 KB 원장 및 민감 컬럼 접근' AS access_scope,
'권한 테이블의 KB_VPD_ADMIN_ROLE / ALL 규칙으로 적용' AS rls_filter
FROM dual
) source
ON (target.user_id = source.user_id)
WHEN MATCHED THEN UPDATE SET
target.user_nm = source.user_nm,
target.role = source.role,
target.channel = source.channel,
target.access_scope = source.access_scope,
target.rls_filter = source.rls_filter
WHEN NOT MATCHED THEN INSERT (
user_id, user_nm, role, channel, access_scope, rls_filter
) VALUES (
source.user_id, source.user_nm, source.role, source.channel,
source.access_scope, source.rls_filter
);
BEGIN
SELECT user_id
INTO v_app_user_id
FROM cb_app_user
WHERE stakeholder_user_id = c_stakeholder_user_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(user_id), 0) + 1 INTO v_app_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_app_user_id, 'VPD 관리자', c_stakeholder_user_id, '관리자채널',
c_stakeholder_user_id, 'Y', 'Y'
);
END;
UPDATE cb_app_user
SET user_name = 'VPD 관리자',
employee_no = c_stakeholder_user_id,
dept_code = '관리자채널',
can_read_contents = 'Y',
active = 'Y'
WHERE user_id = v_app_user_id;
ensure_role(c_role_name, 'RESTRICTED', v_admin_role_id);
ensure_role('KB_STAKEHOLDER_IDENTITY_ROLE', 'INTERNAL', v_identity_role_id);
ensure_user_role(v_app_user_id, v_admin_role_id);
ensure_user_role(v_app_user_id, v_identity_role_id);
FOR object_name IN (
SELECT column_value
FROM TABLE(sys.odcivarchar2list(
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
))
) LOOP
ensure_all_permission(object_name.column_value);
END LOOP;
allow_original_column('KB_CUSTOMERS', 'CUST_NM');
allow_original_column('KB_CUSTOMERS', 'RRN_MASKED');
allow_original_column('KB_CLAIMS', 'CLAIM_AMT');
allow_original_column('KB_CLAIMS', 'PAID_AMT');
allow_original_column('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER');
allow_original_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP');
allow_original_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE');
-- Re-running this script rotates the only full-access administrator token.
UPDATE cb_agent_bearer_key
SET active = 'N',
revoked_at = SYSDATE
WHERE stakeholder_user_id = c_stakeholder_user_id
AND active = 'Y';
v_bearer_token := 'kbvpdadm_' || DBMS_RANDOM.STRING('x', 48);
INSERT INTO cb_agent_bearer_key(
key_id, user_id, stakeholder_user_id, key_prefix, key_hash,
issued_at, expires_at, revoked_at, active, description
) VALUES (
cb_agent_bearer_key_seq.NEXTVAL, v_app_user_id, c_stakeholder_user_id,
'kbvpdadm', STANDARD_HASH(v_bearer_token, 'SHA256'),
SYSDATE, ADD_MONTHS(SYSDATE, 12), NULL, 'Y', c_token_description
);
COMMIT;
-- This is the application step that binds the Bearer Token to the trusted
-- context. DBMS_RLS then invokes CB_AGENT_DOC_VPD_FILTER automatically.
cb_agent_ctx_pkg.set_user_by_bearer(v_bearer_token);
DBMS_OUTPUT.PUT_LINE('STAKEHOLDER_USER_ID='
|| SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID'));
DBMS_OUTPUT.PUT_LINE('EFFECTIVE_ROLE=' || c_role_name);
DBMS_OUTPUT.PUT_LINE('KB_CUSTOMERS_PREDICATE='
|| cb_agent_doc_vpd_filter('POC_2', 'KB_CUSTOMERS'));
DBMS_OUTPUT.PUT_LINE('KB_CLAIMS_PREDICATE='
|| cb_agent_doc_vpd_filter('POC_2', 'KB_CLAIMS'));
DBMS_OUTPUT.PUT_LINE('KB_EXTERNAL_HOLDINGS_PREDICATE='
|| cb_agent_doc_vpd_filter('POC_2', 'KB_EXTERNAL_HOLDINGS'));
cb_agent_ctx_pkg.clear_user;
-- Show once. The raw token is never stored; only its SHA-256 hash remains.
DBMS_OUTPUT.PUT_LINE('BEARER_TOKEN=' || v_bearer_token);
END;
/
PROMPT === KB VPD administrator token ready ===

View File

@@ -0,0 +1,223 @@
-- ============================================================
-- 52_cleanup_to_kb_vpd_poc.sql
--
-- Removes legacy document/Vector/DDS/test examples and keeps only
-- the KB structured-data VPD POC:
-- POC_2.KB_* seven ledgers, stakeholder identity, KB roles,
-- bearer-token permission metadata, and their VPD policies.
--
-- Oracle-maintained ANNOTATIONS_* / VECTOR$* auxiliary objects are
-- not dropped directly. Their owning RAG/vector tables are dropped.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
SET SERVEROUTPUT ON
ALTER SESSION DISABLE PARALLEL DML;
PROMPT === 1. Removing document, legacy role, and test permissions ===
DECLARE
c_role_keep_1 CONSTANT VARCHAR2(80) := 'KB_DESIGNER_ROLE';
c_role_keep_2 CONSTANT VARCHAR2(80) := 'KB_BRANCH_MANAGER_ROLE';
c_role_keep_3 CONSTANT VARCHAR2(80) := 'KB_STAKEHOLDER_IDENTITY_ROLE';
c_role_keep_4 CONSTANT VARCHAR2(80) := 'KB_VPD_ADMIN_ROLE';
BEGIN
DELETE FROM cb_permission_column
WHERE permission_id IN (
SELECT p.perm_id
FROM cb_permission p
JOIN cb_app_role r ON r.role_id = p.role_id
WHERE r.role_name NOT IN (c_role_keep_1, c_role_keep_2, c_role_keep_3, c_role_keep_4)
OR p.target_name NOT IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
);
DELETE FROM cb_permission_rule
WHERE perm_id IN (
SELECT p.perm_id
FROM cb_permission p
JOIN cb_app_role r ON r.role_id = p.role_id
WHERE r.role_name NOT IN (c_role_keep_1, c_role_keep_2, c_role_keep_3, c_role_keep_4)
OR p.target_name NOT IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
);
DELETE FROM cb_permission
WHERE perm_id IN (
SELECT p.perm_id
FROM cb_permission p
JOIN cb_app_role r ON r.role_id = p.role_id
WHERE r.role_name NOT IN (c_role_keep_1, c_role_keep_2, c_role_keep_3, c_role_keep_4)
OR p.target_name NOT IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
);
DELETE FROM cb_user_role
WHERE role_id IN (
SELECT role_id
FROM cb_app_role
WHERE role_name NOT IN (c_role_keep_1, c_role_keep_2, c_role_keep_3, c_role_keep_4)
);
DELETE FROM cb_group_role;
DELETE FROM cb_user_group;
DELETE FROM cb_app_group;
DELETE FROM cb_app_role
WHERE role_name NOT IN (c_role_keep_1, c_role_keep_2, c_role_keep_3, c_role_keep_4);
-- Keep only tokens attached to an actual KB stakeholder.
DELETE FROM cb_agent_bearer_key
WHERE stakeholder_user_id IS NULL;
-- Non-stakeholder users were document/test sample principals.
DELETE FROM cb_app_user
WHERE stakeholder_user_id IS NULL;
DELETE FROM cb_vpd_filter_note;
DELETE FROM cb_vpd_policy_note;
DELETE FROM cb_ords_probe_audit;
DELETE FROM cb_protected_column
WHERE object_id IN (
SELECT object_id
FROM cb_protected_object
WHERE NOT (
owner = 'POC_2'
AND object_name IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
)
);
DELETE FROM cb_protected_object
WHERE NOT (
owner = 'POC_2'
AND object_name IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
);
COMMIT;
END;
/
PROMPT === 2. Removing legacy document VPD/redaction and source objects ===
BEGIN
DBMS_REDACT.DROP_POLICY(
object_schema => 'ADMIN',
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_CONTENTS_REDACT'
);
EXCEPTION
WHEN OTHERS THEN
-- The policy may already be absent in a reduced POC schema.
DBMS_OUTPUT.PUT_LINE('redaction policy not removed: ' || SQLERRM);
END;
/
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM all_policies
WHERE object_owner = 'ADMIN'
AND object_name = 'CB_V_SEARCH_DOCUMENTS'
AND policy_name = 'CB_AGENT_DOC_POLICY';
IF v_count > 0 THEN
DBMS_RLS.DROP_POLICY(
object_schema => 'ADMIN',
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_AGENT_DOC_POLICY'
);
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP VIEW cb_v_search_documents';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE cb_search_documents PURGE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
/
PROMPT === 3. Dropping unrelated POC_2 backup, RAG, and security-example tables ===
DECLARE
PROCEDURE drop_table_if_exists(p_table_name IN VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE
'DROP TABLE POC_2.' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_table_name)
|| ' CASCADE CONSTRAINTS PURGE';
DBMS_OUTPUT.PUT_LINE('dropped POC_2.' || p_table_name);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -942 THEN
NULL;
ELSE
RAISE;
END IF;
END;
BEGIN
drop_table_if_exists('BKP_CLM_20260701144052');
drop_table_if_exists('BKP_CONT_20260701144052');
drop_table_if_exists('BKP_COV_20260701144052');
drop_table_if_exists('BKP_CUST_20260701144052');
drop_table_if_exists('BKP_EXT_20260701144052');
drop_table_if_exists('BKP_PROD_20260701144052');
drop_table_if_exists('KB_CUST_BAK_202607011343');
drop_table_if_exists('KB_CUST_BAK_20260701134439');
drop_table_if_exists('KB_AIDP_RAG_CORPUS_CONFIG');
drop_table_if_exists('KB_AIDP_RAG_QUERY_CACHE');
drop_table_if_exists('KB_COMPETITOR_TERMS_CHUNKS');
drop_table_if_exists('KB_COMPETITOR_TERMS_DOCUMENTS');
drop_table_if_exists('KB_OWN_TERMS_CHUNKS');
drop_table_if_exists('KB_OWN_TERMS_DOCUMENTS');
drop_table_if_exists('KB_SECURITY_AUDIT_LOG');
drop_table_if_exists('KB_SECURITY_POLICY_CATALOG');
END;
/
PROMPT === 4. Remaining KB VPD POC inventory ===
SELECT object_name
FROM cb_protected_object
WHERE owner = 'POC_2'
ORDER BY object_name;
SELECT role_name
FROM cb_app_role
ORDER BY role_name;
SELECT key_id, stakeholder_user_id, key_prefix, active, expires_at
FROM cb_agent_bearer_key
ORDER BY key_id;
SELECT object_owner, object_name, policy_name, function, enable
FROM all_policies
WHERE object_owner = 'POC_2'
ORDER BY object_name, policy_name;
PROMPT === KB VPD POC cleanup complete ===

View File

@@ -0,0 +1,101 @@
-- ============================================================
-- 53_drop_legacy_poc_objects.sql
-- Completes the physical-object portion of the KB VPD POC cleanup
-- after 52 has removed legacy permissions, roles, tokens and users.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
SET SERVEROUTPUT ON
PROMPT === Removing legacy document VPD/redaction and source objects ===
BEGIN
DBMS_REDACT.DROP_POLICY(
object_schema => 'ADMIN',
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_CONTENTS_REDACT'
);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('redaction policy not removed: ' || SQLERRM);
END;
/
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM all_policies
WHERE object_owner = 'ADMIN'
AND object_name = 'CB_V_SEARCH_DOCUMENTS'
AND policy_name = 'CB_AGENT_DOC_POLICY';
IF v_count > 0 THEN
DBMS_RLS.DROP_POLICY(
object_schema => 'ADMIN',
object_name => 'CB_V_SEARCH_DOCUMENTS',
policy_name => 'CB_AGENT_DOC_POLICY'
);
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP VIEW cb_v_search_documents';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE cb_search_documents PURGE';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
/
PROMPT === Dropping unrelated POC_2 backup, RAG, and security-example tables ===
DECLARE
PROCEDURE drop_table_if_exists(p_table_name IN VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE
'DROP TABLE POC_2.' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_table_name)
|| ' CASCADE CONSTRAINTS PURGE';
DBMS_OUTPUT.PUT_LINE('dropped POC_2.' || p_table_name);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -942 THEN
NULL;
ELSE
RAISE;
END IF;
END;
BEGIN
drop_table_if_exists('BKP_CLM_20260701144052');
drop_table_if_exists('BKP_CONT_20260701144052');
drop_table_if_exists('BKP_COV_20260701144052');
drop_table_if_exists('BKP_CUST_20260701144052');
drop_table_if_exists('BKP_EXT_20260701144052');
drop_table_if_exists('BKP_PROD_20260701144052');
drop_table_if_exists('KB_CUST_BAK_202607011343');
drop_table_if_exists('KB_CUST_BAK_20260701134439');
drop_table_if_exists('KB_AIDP_RAG_CORPUS_CONFIG');
drop_table_if_exists('KB_AIDP_RAG_QUERY_CACHE');
drop_table_if_exists('KB_COMPETITOR_TERMS_CHUNKS');
drop_table_if_exists('KB_COMPETITOR_TERMS_DOCUMENTS');
drop_table_if_exists('KB_OWN_TERMS_CHUNKS');
drop_table_if_exists('KB_OWN_TERMS_DOCUMENTS');
drop_table_if_exists('KB_SECURITY_AUDIT_LOG');
drop_table_if_exists('KB_SECURITY_POLICY_CATALOG');
END;
/
PROMPT === Legacy physical objects removed ===

View File

@@ -0,0 +1,47 @@
-- ============================================================
-- 54_drop_legacy_dds_ords_examples.sql
-- Removes only the former document-search ORDS/DDS example objects.
-- It deliberately keeps CB_ORDS because it is the non-exempt KB VPD
-- runtime principal.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
SET SERVEROUTPUT ON
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'cb.agent.security');
DBMS_OUTPUT.PUT_LINE('dropped ORDS module cb.agent.security');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('ORDS module not removed: ' || SQLERRM);
END;
/
DECLARE
PROCEDURE drop_ddl_if_present(p_ddl IN VARCHAR2) IS
BEGIN
EXECUTE IMMEDIATE p_ddl;
DBMS_OUTPUT.PUT_LINE('dropped: ' || p_ddl);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('not present: ' || p_ddl);
END;
BEGIN
drop_ddl_if_present('DROP DATA GRANT admin.cb_dg_hr_docs');
drop_ddl_if_present('DROP DATA GRANT admin.cb_dg_fin_docs');
drop_ddl_if_present('DROP DATA GRANT admin.cb_dg_all_docs');
drop_ddl_if_present('DROP END USER "cb_dds_hr"');
drop_ddl_if_present('DROP END USER "cb_dds_fin"');
drop_ddl_if_present('DROP END USER "cb_dds_all"');
drop_ddl_if_present('DROP END USER "cb_dds_none"');
drop_ddl_if_present('DROP DATA ROLE cb_dds_hr_role');
drop_ddl_if_present('DROP DATA ROLE cb_dds_fin_role');
drop_ddl_if_present('DROP DATA ROLE cb_dds_all_role');
drop_ddl_if_present('DROP DATA ROLE cb_dds_connect_only_role');
drop_ddl_if_present('DROP ROLE cb_dds_connect_role');
END;
/
PROMPT === Legacy DDS/ORDS examples removed ===

View File

@@ -0,0 +1,174 @@
-- ============================================================
-- 55_reapply_kb_vpd_after_table_reload.sql
--
-- KB 원장을 DROP/CREATE 또는 재적재한 뒤 실행한다.
-- 테이블 재생성으로 사라진 POC_2 KB VPD/CLS 정책과 CB_ORDS SELECT
-- 권한만 복구한다. 사용자·역할·권한 조건과 Bearer 토큰은 변경하지 않는다.
-- Execute as ADMIN using SQLcl.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === 1. Validating reloaded KB table contract ===
DECLARE
PROCEDURE require_column(p_table_name IN VARCHAR2, p_column_name IN VARCHAR2) IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM all_tab_columns
WHERE owner = 'POC_2'
AND table_name = p_table_name
AND column_name = p_column_name;
IF v_count = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Required column is missing: POC_2.' || p_table_name || '.' || p_column_name);
END IF;
END;
PROCEDURE require_runtime_object(p_object_name IN VARCHAR2, p_object_type IN VARCHAR2) IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM all_objects
WHERE owner = 'ADMIN'
AND object_name = p_object_name
AND object_type = p_object_type
AND status = 'VALID';
IF v_count = 0 THEN
RAISE_APPLICATION_ERROR(-20002, 'Required runtime object is invalid: ADMIN.' || p_object_name);
END IF;
END;
BEGIN
require_column('KB_STAKEHOLDERS', 'USER_ID');
require_column('KB_STAKEHOLDERS', 'ROLE');
require_column('KB_STAKEHOLDERS', 'CHANNEL');
require_column('KB_CONTRACTS', 'CUST_ID');
require_column('KB_CONTRACTS', 'CONTRACT_NO');
require_column('KB_CONTRACTS', 'FC_ID');
require_column('KB_CONTRACTS', 'FC_CHANNEL');
require_column('KB_CUSTOMERS', 'CUST_ID');
require_column('KB_CUSTOMERS', 'CUST_NM');
require_column('KB_CUSTOMERS', 'RRN_MASKED');
require_column('KB_COVERAGES', 'CUST_ID');
require_column('KB_CLAIMS', 'CUST_ID');
require_column('KB_CLAIMS', 'CLAIM_AMT');
require_column('KB_CLAIMS', 'PAID_AMT');
require_column('KB_EXTERNAL_HOLDINGS', 'CUST_ID');
require_column('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER');
require_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP');
require_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE');
require_runtime_object('CB_AGENT_CTX_PKG', 'PACKAGE');
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');
END;
/
PROMPT === 2. Restoring CB_ORDS runtime grants ===
GRANT SELECT ON POC_2.KB_STAKEHOLDERS TO CB_ORDS;
GRANT SELECT ON POC_2.KB_CUSTOMERS TO CB_ORDS;
GRANT SELECT ON POC_2.KB_PRODUCTS TO CB_ORDS;
GRANT SELECT ON POC_2.KB_CONTRACTS TO CB_ORDS;
GRANT SELECT ON POC_2.KB_COVERAGES TO CB_ORDS;
GRANT SELECT ON POC_2.KB_CLAIMS TO CB_ORDS;
GRANT SELECT ON POC_2.KB_EXTERNAL_HOLDINGS TO CB_ORDS;
GRANT EXECUTE ON CB_AGENT_CTX_PKG TO CB_ORDS;
GRANT EXECUTE ON CB_AGENT_DOC_VPD_FILTER TO CB_ORDS;
GRANT EXECUTE ON CB_AGENT_CAN_READ_COLUMN TO CB_ORDS;
PROMPT === 3. Reapplying row VPD policies ===
DECLARE
PROCEDURE replace_row_policy(p_object_name IN VARCHAR2) IS
v_count NUMBER;
v_policy_name VARCHAR2(128) := 'KB_' || p_object_name || '_ROW_POLICY';
BEGIN
SELECT COUNT(*)
INTO v_count
FROM all_policies
WHERE object_owner = 'POC_2'
AND object_name = p_object_name
AND policy_name = v_policy_name;
IF v_count > 0 THEN
DBMS_RLS.DROP_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => v_policy_name
);
END IF;
DBMS_RLS.ADD_POLICY(
object_schema => 'POC_2',
object_name => p_object_name,
policy_name => v_policy_name,
function_schema => 'ADMIN',
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 === 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 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
-- A missing policy is already the desired state. Do not hide other errors.
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 === 5. Verification ===
SELECT object_name,
COUNT(*) AS policy_count,
LISTAGG(policy_name, ', ') WITHIN GROUP (ORDER BY policy_name) AS policy_names
FROM all_policies
WHERE object_owner = 'POC_2'
AND object_name IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
GROUP BY object_name
ORDER BY object_name;
SELECT table_name, grantee, privilege
FROM all_tab_privs
WHERE table_schema = 'POC_2'
AND grantee = 'CB_ORDS'
AND privilege = 'SELECT'
AND table_name IN (
'KB_CUSTOMERS', 'KB_PRODUCTS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CLAIMS', 'KB_EXTERNAL_HOLDINGS', 'KB_STAKEHOLDERS'
)
ORDER BY table_name;
PROMPT === KB VPD/CLS restoration complete ===
EXIT

View File

@@ -0,0 +1,104 @@
-- ============================================================
-- 56_kb_select_ai_router_team.sql
--
-- KB Select AI profile을 호출하는 최소 Agent / Tool / Task / Team 체인.
-- Tool -> Task -> Router Agent -> Team
-- Execute as POC_2 using SQLcl.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Resetting only this Select AI router chain ===
BEGIN
DBMS_CLOUD_AI_AGENT.DROP_TEAM('KB_SELECT_AI_ROUTER_TEAM', force => TRUE);
DBMS_CLOUD_AI_AGENT.DROP_TASK('KB_SELECT_AI_ROUTER_TASK', force => TRUE);
DBMS_CLOUD_AI_AGENT.DROP_AGENT('KB_SELECT_AI_ROUTER_AGENT', force => TRUE);
DBMS_CLOUD_AI_AGENT.DROP_TOOL('KB_SELECT_AI_ROUTER_TOOL', force => TRUE);
END;
/
PROMPT === Creating Select AI SQL tool ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_TOOL(
tool_name => 'KB_SELECT_AI_ROUTER_TOOL',
attributes => q'~{
"tool_type": "SQL",
"tool_params": {
"profile_name": "KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2"
},
"instruction": "Use the configured Select AI profile for structured KB insurance questions. For this demo, generate SHOWSQL only: create a read-only SELECT or WITH statement, do not execute it, and never generate DDL, DML, transaction control, package calls, or data changes."
}~',
status => 'ENABLED',
description => 'Calls KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2 to generate read-only KB SQL.'
);
END;
/
PROMPT === Creating router agent ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_AGENT(
agent_name => 'KB_SELECT_AI_ROUTER_AGENT',
attributes => q'~{
"profile_name": "KB_AIDP_AGENT_GPT55_OCI_PROFILE_V2",
"role": "You are the KB Select AI routing agent. Classify every user request. If it asks for KB structured insurance data, metrics, counts, contracts, claims, customers, products, coverage, holdings, or stakeholders, call the assigned KB Select AI SQL tool. Return a concise Korean answer and the generated SQL. Do not invent data, do not expose internal reasoning, and do not use any unassigned tool.",
"enable_human_tool": false
}~',
status => 'ENABLED',
description => 'Routes KB structured-data questions to the Select AI SQL tool.'
);
END;
/
PROMPT === Registering tool task ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_TASK(
task_name => 'KB_SELECT_AI_ROUTER_TASK',
attributes => q'~{
"instruction": "Route this user request to the KB Select AI SQL tool when it requires structured insurance data: {query}. Use SHOWSQL only. Explain briefly in Korean what the generated SQL will retrieve. If the request is outside the KB structured-data scope, state that this team only handles KB structured-data SQL generation.",
"tools": ["KB_SELECT_AI_ROUTER_TOOL"],
"enable_human_tool": false
}~',
status => 'ENABLED',
description => 'Single Select AI SQL generation task for KB structured-data requests.'
);
END;
/
PROMPT === Creating agent task team ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_TEAM(
team_name => 'KB_SELECT_AI_ROUTER_TEAM',
attributes => q'~{
"agents": [
{
"name": "KB_SELECT_AI_ROUTER_AGENT",
"task": "KB_SELECT_AI_ROUTER_TASK"
}
],
"process": "sequential"
}~',
status => 'ENABLED',
description => 'KB Select AI router Team: SQL tool, task, agent, and team.'
);
END;
/
PROMPT === Registered chain ===
SELECT tool_name, status
FROM USER_AI_AGENT_TOOLS
WHERE tool_name = 'KB_SELECT_AI_ROUTER_TOOL';
SELECT agent_name, status
FROM USER_AI_AGENTS
WHERE agent_name = 'KB_SELECT_AI_ROUTER_AGENT';
SELECT task_name, status
FROM USER_AI_AGENT_TASKS
WHERE task_name = 'KB_SELECT_AI_ROUTER_TASK';
SELECT agent_team_name, status
FROM USER_AI_AGENT_TEAMS
WHERE agent_team_name = 'KB_SELECT_AI_ROUTER_TEAM';
EXIT

View File

@@ -0,0 +1,17 @@
-- Execute as POC_2 using SQLcl after 56.
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET PAGESIZE 100
SET LINESIZE 240
SET LONG 30000
SET LONGCHUNKSIZE 30000
SET FEEDBACK ON
SELECT DBMS_CLOUD_AI_AGENT.RUN_TEAM(
team_name => 'KB_SELECT_AI_ROUTER_TEAM',
user_prompt => 'KB 고객 원장의 전체 고객 수를 계산하는 SQL을 만들어 줘. SQL만 생성하고 실행하지 마.',
params => '{"conversation_id":"kb-select-ai-router-' ||
TO_CHAR(SYSTIMESTAMP, 'YYYYMMDDHH24MISSFF3') || '"}'
) AS agent_response
FROM dual;
EXIT

View File

@@ -0,0 +1,57 @@
-- ============================================================
-- 58_kb_select_ai_router_api.sql
--
-- POC_2 소유 Select AI Team을 ORDS runtime(CB_ORDS)에서 안전하게
-- 호출하기 위한 definer-rights API.
--
-- Execute as POC_2 using SQLcl after 56_kb_select_ai_router_team.sql.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Creating POC_2 Select AI Team API ===
CREATE OR REPLACE PACKAGE kb_select_ai_router_api AUTHID DEFINER AS
FUNCTION run_team(
p_prompt IN CLOB,
p_conversation_id IN VARCHAR2 DEFAULT NULL
) RETURN CLOB;
END kb_select_ai_router_api;
/
CREATE OR REPLACE PACKAGE BODY kb_select_ai_router_api AS
FUNCTION run_team(
p_prompt IN CLOB,
p_conversation_id IN VARCHAR2 DEFAULT NULL
) RETURN CLOB AS
v_params CLOB;
v_conversation_id VARCHAR2(128) := TRIM(p_conversation_id);
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 v_conversation_id IS NOT NULL THEN
IF NOT REGEXP_LIKE(v_conversation_id, '^[A-Za-z0-9._:-]{1,128}$') THEN
RAISE_APPLICATION_ERROR(-20802, 'conversationId contains unsupported characters');
END IF;
v_params := '{"conversation_id":"' || v_conversation_id || '"}';
END IF;
RETURN DBMS_CLOUD_AI_AGENT.RUN_TEAM(
team_name => 'KB_SELECT_AI_ROUTER_TEAM',
user_prompt => p_prompt,
params => v_params
);
END run_team;
END kb_select_ai_router_api;
/
SHOW ERRORS
PROMPT === Granting only the Team API to the ORDS runtime ===
GRANT EXECUTE ON kb_select_ai_router_api TO cb_ords;
PROMPT === POC_2 Select AI Team API ready ===
EXIT

View File

@@ -0,0 +1,213 @@
-- ============================================================
-- 59_kb_select_ai_router_ords.sql
--
-- POST /ords/cb-ords/kb-select-ai-agent/run
-- Authorization: Bearer <VPD token>
-- {"prompt":"...","conversationId":"optional-safe-id"}
--
-- The ORDS runtime validates the bearer and sets the application context
-- before invoking the POC_2-owned Team API. The context is session scoped,
-- so VPD still applies to any SQL action selected by the Select AI tool.
--
-- Execute as CB_ORDS after 22 and 58.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Creating local ORDS Team API wrapper ===
CREATE OR REPLACE PACKAGE cb_select_ai_ords_api AUTHID DEFINER AS
FUNCTION run_team(
p_prompt IN CLOB,
p_conversation_id IN VARCHAR2
) RETURN CLOB;
END cb_select_ai_ords_api;
/
CREATE OR REPLACE PACKAGE BODY cb_select_ai_ords_api AS
FUNCTION run_team(
p_prompt IN CLOB,
p_conversation_id IN VARCHAR2
) RETURN CLOB AS
BEGIN
RETURN poc_2.kb_select_ai_router_api.run_team(
p_prompt => p_prompt,
p_conversation_id => p_conversation_id
);
END run_team;
END cb_select_ai_ords_api;
/
SHOW ERRORS
-- ORDS validates named PL/SQL references through its gateway account before
-- dispatching to the CB_ORDS parsing schema. These are the only two local
-- wrappers exposed to that account; both remain definer-rights and fixed in
-- scope (Bearer context setup and one fixed Team invocation).
GRANT EXECUTE ON cb_ords_handler_pkg TO ords_public_user;
GRANT EXECUTE ON cb_select_ai_ords_api TO ords_public_user;
PROMPT === Resetting only the KB Select AI Agent ORDS module ===
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'kb.select.ai.agent');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
PROMPT === Creating KB Select AI Agent ORDS endpoint ===
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'kb.select.ai.agent',
p_base_path => 'kb-select-ai-agent/',
p_items_per_page => 0,
p_status => 'PUBLISHED'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'run'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'health'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'health',
p_method => 'GET',
p_source_type => ORDS.source_type_plsql,
p_source => q'~
BEGIN
:status_code := 200;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
HTP.P('{"status":"ok","team":"KB_SELECT_AI_ROUTER_TEAM"}');
END;
~',
p_items_per_page => 0
);
ORDS.DEFINE_HANDLER(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'run',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_source => q'~
DECLARE
v_body_text CLOB;
v_prompt VARCHAR2(32767);
v_requested_conversation_id VARCHAR2(128);
v_internal_conversation_id VARCHAR2(128);
v_stakeholder_user_id VARCHAR2(4000);
v_answer CLOB;
v_response CLOB;
v_error_code NUMBER;
v_error_message VARCHAR2(4000);
BEGIN
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
-- ORDS exposes request JSON as a stream bind. Read it once only.
v_body_text := :body_text;
v_prompt := JSON_VALUE(v_body_text, '$.prompt' RETURNING VARCHAR2(32767));
v_requested_conversation_id := JSON_VALUE(v_body_text, '$.conversationId' RETURNING VARCHAR2(128));
IF v_prompt IS NULL OR TRIM(v_prompt) IS NULL THEN
RAISE_APPLICATION_ERROR(-20801, 'prompt is required');
END IF;
IF v_requested_conversation_id IS NULL THEN
v_requested_conversation_id := 'kb-' || LOWER(RAWTOHEX(SYS_GUID()));
ELSIF NOT REGEXP_LIKE(v_requested_conversation_id, '^[A-Za-z0-9._:-]{1,128}$') THEN
RAISE_APPLICATION_ERROR(-20802, 'conversationId contains unsupported characters');
END IF;
-- A connection-pooled ORDS session can be reused. Scope the Team's
-- conversation memory to the VPD subject as well as the caller's public ID.
v_stakeholder_user_id := NVL(SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID'), 'anonymous');
v_internal_conversation_id := 'kb-' || LOWER(RAWTOHEX(STANDARD_HASH(
v_stakeholder_user_id || ':' || v_requested_conversation_id,
'SHA256'
)));
-- ORDS validates static PL/SQL references before executing a Handler.
-- Invoke the local, fixed-scope Team wrapper through bound dynamic PL/SQL
-- so the runtime parser only sees the trusted local context package.
EXECUTE IMMEDIATE
'BEGIN :result := cb_select_ai_ords_api.run_team(:prompt, :conversation_id); END;'
USING OUT v_answer, IN v_prompt, IN v_internal_conversation_id;
SELECT JSON_OBJECT(
'team' VALUE 'KB_SELECT_AI_ROUTER_TEAM',
'conversationId' VALUE v_requested_conversation_id,
'answer' VALUE v_answer
RETURNING CLOB
)
INTO v_response
FROM dual;
:status_code := 200;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
HTP.P(v_response);
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 (-20801, -20802) OR v_error_code BETWEEN -40599 AND -40400 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 v_error_message
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.agent',
p_pattern => 'run',
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.agent',
p_pattern => 'run',
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 === KB Select AI Agent ORDS endpoint ready ===
PROMPT Path: /ords/cb-ords/kb-select-ai-agent/run
EXIT

View File

@@ -0,0 +1,198 @@
-- ============================================================
-- 60_kb_vpd_admin_full_access_token.sql
--
-- Creates the KB_VPD_ADMIN application user and grants the explicit
-- physical condition STATIC_SQL / 1 = 1 for every KB protected object.
--
-- Execute as ADMIN using SQLcl with one positional argument: the newly
-- generated Bearer token. Do not print or commit that argument.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE ON
SET VERIFY OFF
SET SERVEROUTPUT ON
DEFINE bearer_token = '&1'
DECLARE
c_stakeholder_user_id CONSTANT VARCHAR2(30) := 'KB_VPD_ADMIN';
c_role_name CONSTANT VARCHAR2(80) := 'KB_VPD_ADMIN_ROLE';
c_token_description CONSTANT VARCHAR2(200) := 'KB VPD admin full-access token';
v_admin_role_id NUMBER;
v_identity_role_id NUMBER;
v_app_user_id NUMBER;
v_permission_id NUMBER;
v_count NUMBER;
PROCEDURE ensure_role(
p_role_name IN VARCHAR2,
p_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_level);
END;
PROCEDURE ensure_user_role(p_user_id IN NUMBER, p_role_id IN NUMBER) IS
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_full_access(p_target_name IN VARCHAR2) IS
BEGIN
SELECT MIN(perm_id) INTO v_permission_id
FROM cb_permission
WHERE role_id = v_admin_role_id
AND target_name = p_target_name
AND action_name = 'SELECT';
IF v_permission_id IS NULL THEN
SELECT cb_permission_seq.NEXTVAL INTO v_permission_id FROM dual;
INSERT INTO cb_permission(
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
v_permission_id, v_admin_role_id, p_target_name, 'SELECT', 'ALLOW'
);
ELSE
UPDATE cb_permission
SET permission_effect = 'ALLOW'
WHERE perm_id = v_permission_id;
END IF;
DELETE FROM cb_permission_rule WHERE perm_id = v_permission_id;
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
VALUES (
cb_permission_rule_seq.NEXTVAL, v_permission_id, NULL, 'STATIC_SQL', '1 = 1'
);
EXECUTE IMMEDIATE
'GRANT SELECT ON POC_2.' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_target_name) || ' TO CB_ORDS';
END;
PROCEDURE allow_original_column(p_target_name IN VARCHAR2, p_column_name IN VARCHAR2) IS
BEGIN
SELECT MIN(perm_id) INTO v_permission_id
FROM cb_permission
WHERE role_id = v_admin_role_id
AND target_name = p_target_name
AND action_name = 'SELECT';
SELECT COUNT(*) INTO v_count
FROM cb_permission_column
WHERE permission_id = v_permission_id
AND column_name = p_column_name;
IF v_count = 0 THEN
INSERT INTO cb_permission_column(permission_id, column_name)
VALUES (v_permission_id, p_column_name);
END IF;
END;
BEGIN
MERGE INTO poc_2.kb_stakeholders target
USING (
SELECT c_stakeholder_user_id AS user_id,
'VPD 관리자' AS user_nm,
'관리자' AS role,
'관리자채널' AS channel,
'전체 KB 원장 및 민감 컬럼 접근' AS access_scope,
'모든 보호 객체에 STATIC_SQL / 1 = 1 적용' AS rls_filter
FROM dual
) source
ON (target.user_id = source.user_id)
WHEN MATCHED THEN UPDATE SET
target.user_nm = source.user_nm,
target.role = source.role,
target.channel = source.channel,
target.access_scope = source.access_scope,
target.rls_filter = source.rls_filter
WHEN NOT MATCHED THEN INSERT (
user_id, user_nm, role, channel, access_scope, rls_filter
) VALUES (
source.user_id, source.user_nm, source.role, source.channel,
source.access_scope, source.rls_filter
);
BEGIN
SELECT user_id INTO v_app_user_id
FROM cb_app_user
WHERE stakeholder_user_id = c_stakeholder_user_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(user_id), 0) + 1 INTO v_app_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_app_user_id, 'VPD 관리자', c_stakeholder_user_id, '관리자채널',
c_stakeholder_user_id, 'Y', 'Y'
);
END;
UPDATE cb_app_user
SET user_name = 'VPD 관리자',
employee_no = c_stakeholder_user_id,
dept_code = '관리자채널',
can_read_contents = 'Y',
active = 'Y'
WHERE user_id = v_app_user_id;
ensure_role(c_role_name, 'RESTRICTED', v_admin_role_id);
ensure_role('KB_STAKEHOLDER_IDENTITY_ROLE', 'INTERNAL', v_identity_role_id);
ensure_user_role(v_app_user_id, v_admin_role_id);
ensure_user_role(v_app_user_id, v_identity_role_id);
FOR object_name IN (
SELECT column_value
FROM TABLE(sys.odcivarchar2list(
'KB_STAKEHOLDERS', 'KB_CLAIMS', 'KB_CONTRACTS', 'KB_COVERAGES',
'KB_CUSTOMERS', 'KB_EXTERNAL_HOLDINGS', 'KB_PRODUCTS'
))
) LOOP
ensure_full_access(object_name.column_value);
END LOOP;
allow_original_column('KB_CUSTOMERS', 'CUST_NM');
allow_original_column('KB_CUSTOMERS', 'RRN_MASKED');
allow_original_column('KB_CLAIMS', 'CLAIM_AMT');
allow_original_column('KB_CLAIMS', 'PAID_AMT');
allow_original_column('KB_EXTERNAL_HOLDINGS', 'EXT_INSURER');
allow_original_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_GRP');
allow_original_column('KB_EXTERNAL_HOLDINGS', 'EXT_PRODUCT_TYPE');
UPDATE cb_agent_bearer_key
SET active = 'N', revoked_at = SYSDATE
WHERE stakeholder_user_id = c_stakeholder_user_id
AND active = 'Y';
INSERT INTO cb_agent_bearer_key(
key_id, user_id, stakeholder_user_id, key_prefix, key_hash,
issued_at, expires_at, revoked_at, active, description
) VALUES (
cb_agent_bearer_key_seq.NEXTVAL, v_app_user_id, c_stakeholder_user_id,
'kbvpdadm', STANDARD_HASH('&&bearer_token', 'SHA256'),
SYSDATE, ADD_MONTHS(SYSDATE, 12), NULL, 'Y', c_token_description
);
COMMIT;
DBMS_OUTPUT.PUT_LINE('STAKEHOLDER_USER_ID=' || c_stakeholder_user_id);
DBMS_OUTPUT.PUT_LINE('ROLE_NAME=' || c_role_name);
DBMS_OUTPUT.PUT_LINE('OBJECT_PERMISSION_COUNT=7');
END;
/
UNDEFINE bearer_token
EXIT

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 ===

View File

@@ -0,0 +1,47 @@
-- Smilegate PoC backoffice users. These are tool operators, not game players.
declare
table_missing exception;
pragma exception_init(table_missing, -955);
begin
execute immediate q'[
create table sg_tool_user (
user_id varchar2(100) not null,
user_nm varchar2(200) not null,
role_code varchar2(30) not null,
password_hash varchar2(200) not null,
enabled_yn char(1) default 'Y' not null,
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_tool_user_pk primary key (user_id),
constraint sg_tool_user_role_ck check (role_code in ('TEAM_LEAD', 'TEAM_MEMBER')),
constraint sg_tool_user_enabled_ck check (enabled_yn in ('Y', 'N'))
)
]';
exception
when table_missing then null;
end;
/
merge into sg_tool_user target
using (
select 'sg-teamlead' as user_id, 'SG Demo Team Lead' as user_nm,
'TEAM_LEAD' as role_code,
'{bcrypt}$2y$12$esdNRjiRo3ZxBnUGD1xrjuIFjCeCWcxlksDfdZpeImTsVtIdh/IJe' as password_hash
from dual
union all
select 'sg-member', 'SG Demo Team Member', 'TEAM_MEMBER',
'{bcrypt}$2y$12$esdNRjiRo3ZxBnUGD1xrjuIFjCeCWcxlksDfdZpeImTsVtIdh/IJe'
from dual
) source
on (target.user_id = source.user_id)
when matched then update set
target.user_nm = source.user_nm,
target.role_code = source.role_code,
target.password_hash = source.password_hash,
target.enabled_yn = 'Y',
target.updated_at = systimestamp
when not matched then insert (user_id, user_nm, role_code, password_hash, enabled_yn)
values (source.user_id, source.user_nm, source.role_code, source.password_hash, 'Y');
commit;

View File

@@ -0,0 +1,170 @@
-- Smilegate PoC backoffice identity administration model.
-- Tool operators are distinct from game-service users and from the admin login.
set define off
declare
procedure create_if_missing(p_sql varchar2) is
begin
execute immediate p_sql;
exception
when others then
if sqlcode != -955 then raise; end if;
end;
begin
create_if_missing('create table sg_app_user (
user_id number primary key,
login_id varchar2(100) not null unique,
user_name varchar2(200) not null,
employee_no varchar2(100),
dept_code varchar2(100),
can_read_contents char(1) default ''N'' not null,
active char(1) default ''Y'' not null,
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_app_user_active_ck check (active in (''Y'', ''N''))
)');
create_if_missing('create table sg_app_group (
group_id number primary key,
group_code varchar2(100) not null unique,
group_name varchar2(200) not null,
description varchar2(500),
active_yn char(1) default ''Y'' not null,
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_app_group_active_ck check (active_yn in (''Y'', ''N''))
)');
create_if_missing('create table sg_app_role (
role_id number primary key,
role_name varchar2(100) not null unique,
description varchar2(500),
max_sensitivity_level varchar2(30) default ''INTERNAL'' not null,
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_app_role_sensitivity_ck check (max_sensitivity_level in (''PUBLIC'', ''INTERNAL'', ''CONFIDENTIAL'', ''RESTRICTED''))
)');
create_if_missing('create table sg_user_role (
user_id number not null,
role_id number not null,
created_at timestamp default systimestamp not null,
constraint sg_user_role_pk primary key (user_id, role_id),
constraint sg_user_role_user_fk foreign key (user_id) references sg_app_user(user_id),
constraint sg_user_role_role_fk foreign key (role_id) references sg_app_role(role_id)
)');
create_if_missing('create table sg_user_group (
group_id number not null,
user_id number not null,
created_at timestamp default systimestamp not null,
constraint sg_user_group_pk primary key (group_id, user_id),
constraint sg_user_group_group_fk foreign key (group_id) references sg_app_group(group_id),
constraint sg_user_group_user_fk foreign key (user_id) references sg_app_user(user_id)
)');
create_if_missing('create table sg_group_role (
group_id number not null,
role_id number not null,
created_at timestamp default systimestamp not null,
constraint sg_group_role_pk primary key (group_id, role_id),
constraint sg_group_role_group_fk foreign key (group_id) references sg_app_group(group_id),
constraint sg_group_role_role_fk foreign key (role_id) references sg_app_role(role_id)
)');
create_if_missing('create table sg_audit_event (
audit_id number primary key,
event_type varchar2(100) not null,
key_id number,
object_id number,
status varchar2(30) not null,
row_count number,
error_code varchar2(100),
message varchar2(2000),
created_at timestamp default systimestamp not null
)');
create_if_missing('create sequence sg_audit_event_seq start with 1 increment by 1 nocache');
create_if_missing('create table sg_protected_object (
object_id number primary key,
owner varchar2(128) not null,
object_name varchar2(128) not null,
ords_path varchar2(500),
enabled_yn char(1) default ''Y'' not null,
description varchar2(500),
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_protected_object_uq unique (owner, object_name),
constraint sg_protected_object_enabled_ck check (enabled_yn in (''Y'', ''N''))
)');
create_if_missing('create table sg_protected_column (
column_id number primary key,
object_id number not null,
column_name varchar2(128) not null,
sensitive_yn char(1) default ''N'' not null,
visible_role_id number,
sensitivity_level varchar2(30) default ''PUBLIC'' not null,
redaction_method varchar2(30) default ''NONE'' not null,
constraint sg_protected_column_uq unique (object_id, column_name),
constraint sg_protected_column_object_fk foreign key (object_id) references sg_protected_object(object_id),
constraint sg_protected_column_role_fk foreign key (visible_role_id) references sg_app_role(role_id)
)');
create_if_missing('create table sg_permission (
perm_id number primary key,
role_id number not null,
target_name varchar2(128) not null,
action_name varchar2(30) not null,
permission_effect varchar2(30) default ''ALLOW'' not null,
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_permission_role_fk foreign key (role_id) references sg_app_role(role_id),
constraint sg_permission_effect_ck check (permission_effect in (''ALLOW'', ''DENY''))
)');
create_if_missing('create table sg_permission_rule (
rule_id number primary key,
perm_id number not null,
rule_column varchar2(128),
rule_type varchar2(50) not null,
rule_value varchar2(2000),
constraint sg_permission_rule_perm_fk foreign key (perm_id) references sg_permission(perm_id)
)');
create_if_missing('create table sg_permission_column (
permission_id number not null,
column_name varchar2(128) not null,
constraint sg_permission_column_pk primary key (permission_id, column_name),
constraint sg_permission_column_perm_fk foreign key (permission_id) references sg_permission(perm_id)
)');
create_if_missing('create sequence sg_permission_seq start with 1 increment by 1 nocache');
create_if_missing('create sequence sg_permission_rule_seq start with 1 increment by 1 nocache');
end;
/
merge into sg_app_user t
using (
select 1001 user_id, 'sg-teamlead' login_id, 'SG Demo Team Lead' user_name, 'SG-001' employee_no, 'DATA_AI' dept_code from dual
union all
select 1002, 'sg-member', 'SG Demo Team Member', 'SG-002', 'DATA_AI' from dual
) s on (t.user_id = s.user_id)
when matched then update set t.login_id=s.login_id, t.user_name=s.user_name, t.employee_no=s.employee_no, t.dept_code=s.dept_code, t.active='Y', t.updated_at=systimestamp
when not matched then insert (user_id, login_id, user_name, employee_no, dept_code, can_read_contents, active)
values (s.user_id, s.login_id, s.user_name, s.employee_no, s.dept_code, 'N', 'Y');
merge into sg_app_group t
using (select 2001 group_id, 'DATA_AI_TF' group_code, 'Data & AI TF' group_name, 'Smilegate Data and AI proof-of-concept operators' description from dual) s
on (t.group_id = s.group_id)
when matched then update set t.group_code=s.group_code, t.group_name=s.group_name, t.description=s.description, t.active_yn='Y', t.updated_at=systimestamp
when not matched then insert (group_id, group_code, group_name, description, active_yn) values (s.group_id, s.group_code, s.group_name, s.description, 'Y');
merge into sg_app_role t
using (
select 3001 role_id, 'DATA_AI_TEAM_LEAD' role_name, 'Data and AI TF lead' description, 'CONFIDENTIAL' max_sensitivity_level from dual
union all
select 3002, 'DATA_AI_TEAM_MEMBER', 'Data and AI TF member', 'INTERNAL' from dual
) s on (t.role_id=s.role_id)
when matched then update set t.role_name=s.role_name, t.description=s.description, t.max_sensitivity_level=s.max_sensitivity_level, t.updated_at=systimestamp
when not matched then insert (role_id, role_name, description, max_sensitivity_level) values (s.role_id, s.role_name, s.description, s.max_sensitivity_level);
merge into sg_user_group t using (select 2001 group_id, 1001 user_id from dual union all select 2001, 1002 from dual) s
on (t.group_id=s.group_id and t.user_id=s.user_id) when not matched then insert (group_id, user_id) values (s.group_id, s.user_id);
merge into sg_user_role t using (select 1001 user_id, 3001 role_id from dual union all select 1002, 3002 from dual) s
on (t.user_id=s.user_id and t.role_id=s.role_id) when not matched then insert (user_id, role_id) values (s.user_id, s.role_id);
merge into sg_group_role t using (select 2001 group_id, 3002 role_id from dual) s
on (t.group_id=s.group_id and t.role_id=s.role_id) when not matched then insert (group_id, role_id) values (s.group_id, s.role_id);
commit;

View File

@@ -0,0 +1,597 @@
-- ============================================================
-- 72_hmm_leave_team_vpd.sql
--
-- HMM HR leave row security:
-- * HMM_HR_MANAGER -> self and directly managed employees
-- * HMM_HR_VIEWER -> self only
-- * HMM_HR_ADMIN -> all rows when the role is explicitly assigned
--
-- Run as ADMIN after the HMM access-management schema is installed.
-- This script is idempotent and never stores a plain bearer token.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
ALTER SESSION DISABLE PARALLEL DML;
PROMPT === 1. Ensuring HMM roles and demo role assignments ===
MERGE INTO hmm_access_roles dst
USING (
SELECT 1 role_id, 'HMM_HR_VIEWER' role_name,
'HMM employee self-service leave access' description,
'PUBLIC' max_sensitivity_level FROM dual
UNION ALL
SELECT 2, 'HMM_HR_MANAGER',
'HMM manager access to self and direct reports',
'INTERNAL' FROM dual
UNION ALL
SELECT 3, 'HMM_HR_ADMIN',
'HMM HR administrator full leave access',
'CONFIDENTIAL' FROM dual
) src
ON (dst.role_name = src.role_name)
WHEN MATCHED THEN UPDATE SET
dst.description = src.description,
dst.max_sensitivity_level = src.max_sensitivity_level,
dst.active_yn = 'Y',
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
role_id, role_name, description, max_sensitivity_level,
active_yn, created_at, updated_at
) VALUES (
src.role_id, src.role_name, src.description, src.max_sensitivity_level,
'Y', SYSTIMESTAMP, SYSTIMESTAMP
);
MERGE INTO hmm_employee_access_roles dst
USING (
SELECT e.employee_id, r.role_id
FROM hmm_hr_employees e
JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_MANAGER'
WHERE e.employee_code = 'E1001'
AND e.employment_status = 'ACTIVE'
) src
ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id)
WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at)
VALUES (src.employee_id, src.role_id, SYSTIMESTAMP);
MERGE INTO hmm_employee_access_roles dst
USING (
SELECT e.employee_id, r.role_id
FROM hmm_hr_employees e
JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_VIEWER'
WHERE e.manager_employee_id = (
SELECT manager.employee_id
FROM hmm_hr_employees manager
WHERE manager.employee_code = 'E1001'
)
AND e.employment_status = 'ACTIVE'
) src
ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id)
WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at)
VALUES (src.employee_id, src.role_id, SYSTIMESTAMP);
COMMIT;
PROMPT === 2. Ensuring protected leave objects and columns ===
DECLARE
PROCEDURE ensure_object(
p_object_name IN VARCHAR2,
p_description IN VARCHAR2
) AS
v_object_id NUMBER;
BEGIN
BEGIN
SELECT object_id
INTO v_object_id
FROM hmm_access_objects
WHERE owner = 'ADMIN'
AND object_name = p_object_name;
UPDATE hmm_access_objects
SET enabled_yn = 'Y',
description = p_description,
ords_path = 'cb-ords/cb-object-query/admin/' || LOWER(p_object_name)
WHERE object_id = v_object_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(object_id), 0) + 1
INTO v_object_id
FROM hmm_access_objects;
INSERT INTO hmm_access_objects (
object_id, owner, object_name, ords_path, enabled_yn, description
) VALUES (
v_object_id,
'ADMIN',
p_object_name,
'cb-ords/cb-object-query/admin/' || LOWER(p_object_name),
'Y',
p_description
);
END;
FOR c IN (
SELECT column_name
FROM user_tab_columns
WHERE table_name = p_object_name
ORDER BY column_id
) LOOP
MERGE INTO hmm_access_object_columns dst
USING (
SELECT v_object_id object_id, c.column_name column_name
FROM dual
) src
ON (
dst.object_id = src.object_id
AND dst.column_name = src.column_name
)
WHEN NOT MATCHED THEN INSERT (
column_id, object_id, column_name, sensitive_yn,
sensitivity_level, redaction_method
) VALUES (
(SELECT NVL(MAX(column_id), 0) + 1 FROM hmm_access_object_columns),
src.object_id,
src.column_name,
'N',
'PUBLIC',
'NONE'
);
END LOOP;
END;
BEGIN
ensure_object(
'HMM_LEAVE_BALANCES',
'HMM employee leave balance protected by employee hierarchy'
);
ensure_object(
'HMM_LEAVE_REQUESTS',
'HMM employee leave requests protected by employee hierarchy'
);
END;
/
COMMIT;
PROMPT === 3. Ensuring role permission rules ===
DECLARE
PROCEDURE ensure_permission(
p_role_name IN VARCHAR2,
p_target_name IN VARCHAR2,
p_rule_type IN VARCHAR2
) AS
v_role_id NUMBER;
v_perm_id NUMBER;
BEGIN
SELECT role_id
INTO v_role_id
FROM hmm_access_roles
WHERE role_name = p_role_name
AND active_yn = 'Y';
BEGIN
SELECT perm_id
INTO v_perm_id
FROM hmm_access_permissions
WHERE role_id = v_role_id
AND target_name = p_target_name
AND action_name = 'SELECT';
UPDATE hmm_access_permissions
SET permission_effect = 'ALLOW'
WHERE perm_id = v_perm_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT cb_permission_seq.NEXTVAL
INTO v_perm_id
FROM dual;
INSERT INTO hmm_access_permissions (
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
v_perm_id, v_role_id, p_target_name, 'SELECT', 'ALLOW'
);
END;
DELETE FROM hmm_access_permission_rules
WHERE perm_id = v_perm_id;
INSERT INTO hmm_access_permission_rules (
rule_id, perm_id, rule_column, rule_type, rule_value
) VALUES (
cb_permission_rule_seq.NEXTVAL,
v_perm_id,
'EMPLOYEE_ID',
p_rule_type,
NULL
);
END;
BEGIN
ensure_permission('HMM_HR_VIEWER', 'HMM_LEAVE_BALANCES', 'SELF');
ensure_permission('HMM_HR_VIEWER', 'HMM_LEAVE_REQUESTS', 'SELF');
ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_BALANCES', 'MANAGED_TEAM');
ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_REQUESTS', 'MANAGED_TEAM');
ensure_permission('HMM_HR_ADMIN', 'HMM_LEAVE_BALANCES', 'ALL');
ensure_permission('HMM_HR_ADMIN', 'HMM_LEAVE_REQUESTS', 'ALL');
END;
/
COMMIT;
PROMPT === 4. Creating secure HMM access context ===
CREATE OR REPLACE PACKAGE hmm_access_ctx_pkg AUTHID DEFINER AS
PROCEDURE clear_user;
PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2);
END;
/
CREATE OR REPLACE PACKAGE BODY hmm_access_ctx_pkg AS
PROCEDURE clear_user AS
BEGIN
DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID', NULL);
DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_CODE', NULL);
DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'TEAM_ID', NULL);
END;
PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2) AS
v_employee_id hmm_hr_employees.employee_id%TYPE;
v_employee_code hmm_hr_employees.employee_code%TYPE;
v_team_id hmm_hr_employees.team_id%TYPE;
BEGIN
IF p_bearer_token IS NULL OR TRIM(p_bearer_token) IS NULL THEN
clear_user;
RAISE_APPLICATION_ERROR(-20101, 'Bearer token is required');
END IF;
SELECT e.employee_id,
e.employee_code,
e.team_id
INTO v_employee_id,
v_employee_code,
v_team_id
FROM hmm_access_bearer_tokens t
JOIN hmm_hr_employees e
ON e.employee_id = t.employee_id
WHERE t.key_hash = STANDARD_HASH(p_bearer_token, 'SHA256')
AND t.revoked_at IS NULL
AND t.expires_at > CAST(SYSTIMESTAMP AS TIMESTAMP)
AND e.employment_status = 'ACTIVE';
DBMS_SESSION.SET_CONTEXT(
'HMM_ACCESS_CTX', 'EMPLOYEE_ID', TO_CHAR(v_employee_id)
);
DBMS_SESSION.SET_CONTEXT(
'HMM_ACCESS_CTX', 'EMPLOYEE_CODE', v_employee_code
);
DBMS_SESSION.SET_CONTEXT(
'HMM_ACCESS_CTX', 'TEAM_ID', TO_CHAR(v_team_id)
);
EXCEPTION
WHEN NO_DATA_FOUND THEN
clear_user;
RAISE_APPLICATION_ERROR(-20102, 'Invalid or expired bearer token');
WHEN TOO_MANY_ROWS THEN
clear_user;
RAISE_APPLICATION_ERROR(-20103, 'Bearer token mapping is not unique');
END;
END;
/
CREATE OR REPLACE CONTEXT hmm_access_ctx USING hmm_access_ctx_pkg;
PROMPT === 5. Creating the HMM leave VPD filter ===
CREATE OR REPLACE FUNCTION hmm_leave_vpd_filter(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
v_employee_id NUMBER;
v_target VARCHAR2(128);
v_allow_predicate VARCHAR2(32767);
v_deny_predicate VARCHAR2(32767);
v_permission_clause VARCHAR2(32767);
FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
RETURN '''' || REPLACE(NVL(p_value, ''), '''', '''''') || '''';
END;
FUNCTION safe_column(p_column_name IN VARCHAR2) RETURN VARCHAR2 IS
v_column VARCHAR2(128);
v_count NUMBER;
BEGIN
v_column := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(TRIM(p_column_name)));
SELECT COUNT(*)
INTO v_count
FROM all_tab_columns
WHERE owner = UPPER(p_schema)
AND table_name = UPPER(p_object)
AND column_name = v_column;
RETURN CASE WHEN v_count = 1 THEN v_column ELSE NULL END;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
PROCEDURE append_and(p_clause IN VARCHAR2) IS
BEGIN
IF p_clause IS NULL THEN
v_permission_clause := NULL;
ELSIF v_permission_clause IS NULL THEN
v_permission_clause := '(' || p_clause || ')';
ELSE
v_permission_clause := v_permission_clause || ' AND (' || p_clause || ')';
END IF;
END;
PROCEDURE append_permission(
p_effect IN VARCHAR2,
p_clause IN VARCHAR2
) IS
BEGIN
IF p_clause IS NULL THEN
RETURN;
END IF;
IF p_effect = 'DENY' THEN
IF v_deny_predicate IS NULL THEN
v_deny_predicate := '(' || p_clause || ')';
ELSE
v_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')';
END IF;
ELSIF v_allow_predicate IS NULL THEN
v_allow_predicate := '(' || p_clause || ')';
ELSE
v_allow_predicate := v_allow_predicate || ' OR (' || p_clause || ')';
END IF;
END;
BEGIN
BEGIN
v_employee_id := TO_NUMBER(
SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')
);
EXCEPTION
WHEN OTHERS THEN
RETURN '1 = 0';
END;
IF v_employee_id IS NULL OR p_schema IS NULL OR p_object IS NULL THEN
RETURN '1 = 0';
END IF;
v_target := UPPER(TRIM(p_object));
IF v_target NOT IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS') THEN
RETURN '1 = 0';
END IF;
FOR permission_row IN (
SELECT DISTINCT
p.perm_id,
NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') permission_effect
FROM (
SELECT er.role_id
FROM hmm_employee_access_roles er
WHERE er.employee_id = v_employee_id
UNION
SELECT gr.role_id
FROM hmm_access_group_members gm
JOIN hmm_access_groups g
ON g.group_id = gm.group_id
AND g.active_yn = 'Y'
JOIN hmm_group_access_roles gr
ON gr.group_id = gm.group_id
WHERE gm.employee_id = v_employee_id
) effective_role
JOIN hmm_access_roles role
ON role.role_id = effective_role.role_id
AND role.active_yn = 'Y'
JOIN hmm_access_permissions p
ON p.role_id = role.role_id
AND p.target_name = v_target
AND p.action_name = 'SELECT'
ORDER BY p.perm_id
) LOOP
v_permission_clause := NULL;
FOR rule_row IN (
SELECT UPPER(TRIM(rule_type)) rule_type,
UPPER(TRIM(rule_column)) rule_column,
rule_value
FROM hmm_access_permission_rules
WHERE perm_id = permission_row.perm_id
ORDER BY rule_id
) LOOP
DECLARE
v_column VARCHAR2(128);
v_clause VARCHAR2(32767);
BEGIN
v_column := safe_column(NVL(rule_row.rule_column, 'EMPLOYEE_ID'));
v_clause := NULL;
IF rule_row.rule_type = 'ALL' THEN
v_clause := '1 = 1';
ELSIF rule_row.rule_type = 'SELF' AND v_column IS NOT NULL THEN
v_clause := v_column
|| ' = TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))';
ELSIF rule_row.rule_type = 'MANAGED_TEAM' AND v_column IS NOT NULL THEN
v_clause := v_column || ' IN ('
|| 'SELECT employee.employee_id '
|| 'FROM ADMIN.HMM_HR_EMPLOYEES employee '
|| 'WHERE employee.employee_id = '
|| 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID'')) '
|| 'OR employee.manager_employee_id = '
|| 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))'
|| ')';
ELSIF rule_row.rule_type = '='
AND v_column IS NOT NULL
AND rule_row.rule_value IS NOT NULL THEN
v_clause := 'TO_CHAR(' || v_column || ') = '
|| quote_literal(rule_row.rule_value);
ELSIF rule_row.rule_type IN ('!=', '<>')
AND v_column IS NOT NULL
AND rule_row.rule_value IS NOT NULL THEN
v_clause := 'TO_CHAR(' || v_column || ') <> '
|| quote_literal(rule_row.rule_value);
END IF;
IF v_clause IS NULL THEN
v_permission_clause := NULL;
EXIT;
END IF;
append_and(v_clause);
END;
END LOOP;
append_permission(
permission_row.permission_effect,
v_permission_clause
);
END LOOP;
IF v_allow_predicate IS NULL THEN
RETURN '1 = 0';
END IF;
IF v_deny_predicate IS NULL THEN
RETURN v_allow_predicate;
END IF;
RETURN '((' || v_allow_predicate || ') AND NOT ('
|| v_deny_predicate || '))';
EXCEPTION
WHEN OTHERS THEN
RETURN '1 = 0';
END;
/
SHOW ERRORS PACKAGE hmm_access_ctx_pkg
SHOW ERRORS PACKAGE BODY hmm_access_ctx_pkg
SHOW ERRORS FUNCTION hmm_leave_vpd_filter
PROMPT === 6. Attaching context-sensitive VPD policies ===
DECLARE
PROCEDURE replace_policy(p_object_name IN VARCHAR2) AS
BEGIN
BEGIN
DBMS_RLS.DROP_POLICY(
object_schema => 'ADMIN',
object_name => p_object_name,
policy_name => 'HMM_LEAVE_SCOPE_POLICY'
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -28102 THEN
RAISE;
END IF;
END;
DBMS_RLS.ADD_POLICY(
object_schema => 'ADMIN',
object_name => p_object_name,
policy_name => 'HMM_LEAVE_SCOPE_POLICY',
function_schema => 'ADMIN',
policy_function => 'HMM_LEAVE_VPD_FILTER',
statement_types => 'SELECT',
update_check => FALSE,
enable => TRUE,
policy_type => DBMS_RLS.CONTEXT_SENSITIVE
);
END;
BEGIN
replace_policy('HMM_LEAVE_BALANCES');
replace_policy('HMM_LEAVE_REQUESTS');
END;
/
PROMPT === 7. Recording backoffice descriptions ===
MERGE INTO hmm_access_vpd_filter_notes dst
USING (
SELECT 'ADMIN' function_owner,
'HMM_LEAVE_VPD_FILTER' function_name,
'Builds self, managed-team, and administrator leave predicates from HMM roles and access rules.'
description
FROM dual
) src
ON (
dst.function_owner = src.function_owner
AND dst.function_name = src.function_name
)
WHEN MATCHED THEN UPDATE SET
dst.description = src.description,
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
function_owner, function_name, description, updated_at
) VALUES (
src.function_owner, src.function_name, src.description, SYSTIMESTAMP
);
MERGE INTO hmm_access_vpd_policy_notes dst
USING (
SELECT 'ADMIN' object_owner,
object_name,
'HMM_LEAVE_SCOPE_POLICY' policy_name,
'Restricts leave rows to self, direct reports, or explicit HR administrator access.'
description
FROM (
SELECT 'HMM_LEAVE_BALANCES' object_name FROM dual
UNION ALL
SELECT 'HMM_LEAVE_REQUESTS' FROM dual
)
) src
ON (
dst.object_owner = src.object_owner
AND dst.object_name = src.object_name
AND dst.policy_name = src.policy_name
)
WHEN MATCHED THEN UPDATE SET
dst.description = src.description,
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
object_owner, object_name, policy_name, description, updated_at
) VALUES (
src.object_owner, src.object_name, src.policy_name,
src.description, SYSTIMESTAMP
);
COMMIT;
PROMPT === 8. Deployment inventory ===
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
'HMM_ACCESS_CTX',
'HMM_ACCESS_CTX_PKG',
'HMM_LEAVE_VPD_FILTER'
)
ORDER BY object_name, object_type;
SELECT object_name, policy_name, function, sel, enable, policy_type
FROM all_policies
WHERE object_owner = 'ADMIN'
AND object_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
AND policy_name = 'HMM_LEAVE_SCOPE_POLICY'
ORDER BY object_name;
SELECT e.employee_code, r.role_name
FROM hmm_employee_access_roles er
JOIN hmm_hr_employees e ON e.employee_id = er.employee_id
JOIN hmm_access_roles r ON r.role_id = er.role_id
WHERE r.role_name IN ('HMM_HR_VIEWER', 'HMM_HR_MANAGER')
ORDER BY e.employee_code, r.role_name;
SELECT r.role_name,
p.target_name,
pr.rule_column,
pr.rule_type
FROM hmm_access_permissions p
JOIN hmm_access_roles r ON r.role_id = p.role_id
JOIN hmm_access_permission_rules pr ON pr.perm_id = p.perm_id
WHERE p.target_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
ORDER BY r.role_name, p.target_name;
PROMPT === HMM leave team VPD ready ===

View File

@@ -0,0 +1,159 @@
-- #740 HMM MCP VPD execution boundary
-- Run as ADMIN with SQLcl. The password is prompted without echo and is used
-- only when CB_ORDS does not already exist.
set define on verify off echo off feedback on serveroutput on
whenever sqlerror exit sql.sqlcode rollback
accept hmm_cb_ords_password char prompt 'CB_ORDS initial password: ' hide
declare
v_user_count number;
v_password varchar2(4000) := q'~&hmm_cb_ords_password~';
begin
select count(*)
into v_user_count
from dba_users
where username = 'CB_ORDS';
if v_user_count = 0 then
if length(v_password) < 16
or instr(v_password, '"') > 0
or instr(v_password, chr(10)) > 0
or instr(v_password, chr(13)) > 0
or not regexp_like(v_password, '[A-Z]')
or not regexp_like(v_password, '[a-z]')
or not regexp_like(v_password, '[0-9]')
or not regexp_like(v_password, '[^A-Za-z0-9]') then
raise_application_error(
-20001,
'CB_ORDS password must be 16+ chars with upper/lower/digit/special and no quote/newline.');
end if;
execute immediate
'create user CB_ORDS identified by "' || v_password || '" '
|| 'default tablespace DATA temporary tablespace TEMP quota 0 on DATA';
dbms_output.put_line('Created CB_ORDS.');
else
dbms_output.put_line('CB_ORDS already exists; password was not changed.');
end if;
end;
/
alter user CB_ORDS account unlock;
grant create session to CB_ORDS;
create or replace package CB_ORDS_HANDLER_PKG
authid definer
as
procedure SET_VPD_CONTEXT(p_authorization in varchar2);
procedure CLEAR_VPD_CONTEXT;
end CB_ORDS_HANDLER_PKG;
/
create or replace package body CB_ORDS_HANDLER_PKG
as
c_bearer_prefix constant varchar2(7) := 'Bearer ';
procedure CLEAR_VPD_CONTEXT
is
begin
HMM_ACCESS_CTX_PKG.CLEAR_USER;
dbms_session.clear_identifier;
end CLEAR_VPD_CONTEXT;
procedure SET_VPD_CONTEXT(p_authorization in varchar2)
is
v_authorization varchar2(2048) := trim(p_authorization);
v_token varchar2(512);
begin
CLEAR_VPD_CONTEXT;
if v_authorization is null
or length(v_authorization) > 2048
or upper(substr(v_authorization, 1, length(c_bearer_prefix)))
<> upper(c_bearer_prefix) then
raise_application_error(-20002, 'A valid Bearer authorization header is required.');
end if;
v_token := trim(substr(v_authorization, length(c_bearer_prefix) + 1));
if v_token is null or length(v_token) > 512 then
raise_application_error(-20003, 'A valid Bearer token is required.');
end if;
HMM_ACCESS_CTX_PKG.SET_USER_BY_BEARER(v_token);
dbms_session.set_identifier(
sys_context('HMM_ACCESS_CTX', 'EMPLOYEE_CODE'));
exception
when others then
CLEAR_VPD_CONTEXT;
raise;
end SET_VPD_CONTEXT;
end CB_ORDS_HANDLER_PKG;
/
grant execute on CB_ORDS_HANDLER_PKG to CB_ORDS;
grant select on HMM_ORG_TEAMS to CB_ORDS;
grant select on HMM_HR_EMPLOYEES to CB_ORDS;
grant select on HMM_LEAVE_BALANCES to CB_ORDS;
grant select on HMM_LEAVE_REQUESTS to CB_ORDS;
grant select on HMM_ATTENDANCE_DAILY to CB_ORDS;
grant select on HMM_HR_TERMS to CB_ORDS;
create or replace synonym CB_ORDS.CB_ORDS_HANDLER_PKG
for ADMIN.CB_ORDS_HANDLER_PKG;
create or replace synonym CB_ORDS.HMM_ORG_TEAMS
for ADMIN.HMM_ORG_TEAMS;
create or replace synonym CB_ORDS.HMM_HR_EMPLOYEES
for ADMIN.HMM_HR_EMPLOYEES;
create or replace synonym CB_ORDS.HMM_LEAVE_BALANCES
for ADMIN.HMM_LEAVE_BALANCES;
create or replace synonym CB_ORDS.HMM_LEAVE_REQUESTS
for ADMIN.HMM_LEAVE_REQUESTS;
create or replace synonym CB_ORDS.HMM_ATTENDANCE_DAILY
for ADMIN.HMM_ATTENDANCE_DAILY;
create or replace synonym CB_ORDS.HMM_HR_TERMS
for ADMIN.HMM_HR_TERMS;
declare
v_exempt_count number;
v_policy_count number;
begin
select count(*)
into v_exempt_count
from dba_sys_privs
where grantee = 'CB_ORDS'
and privilege = 'EXEMPT ACCESS POLICY';
if v_exempt_count <> 0 then
raise_application_error(-20004, 'CB_ORDS must not have EXEMPT ACCESS POLICY.');
end if;
select count(*)
into v_policy_count
from dba_policies
where object_owner = 'ADMIN'
and object_name in ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
and policy_name = 'HMM_LEAVE_SCOPE_POLICY'
and enable = 'YES'
and sel = 'YES';
if v_policy_count <> 2 then
raise_application_error(-20005, 'Both HMM leave VPD policies must be enabled.');
end if;
end;
/
select username, account_status
from dba_users
where username = 'CB_ORDS';
select privilege
from dba_sys_privs
where grantee = 'CB_ORDS'
order by privilege;
select object_name, object_type, status
from dba_objects
where owner = 'ADMIN'
and object_name = 'CB_ORDS_HANDLER_PKG'
order by object_type;
undefine hmm_cb_ords_password
prompt HMM MCP VPD runtime boundary is ready.

View File

@@ -0,0 +1,32 @@
-- ============================================================
-- HMM ADB -> AWS RDS PostgreSQL federation demo
-- Step 0: precheck
-- Run in Database Actions SQL Worksheet as ADMIN.
-- Redmine: #730
-- ============================================================
SELECT
USER AS session_user,
SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS current_schema,
SYS_CONTEXT('USERENV', 'DB_NAME') AS database_name
FROM dual;
SELECT credential_name, username
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';
SELECT db_link, username, host, created
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY object_name;
-- First recording: the last three queries should return no rows.
-- If objects already exist, run 80_hmm_rds_pg_99_cleanup.sql first.

View File

@@ -0,0 +1,20 @@
-- ============================================================
-- Step 1: create the PostgreSQL credential.
-- Run only this statement after replacing <RDS_PASSWORD> in the
-- Worksheet. Never save the actual password in this file.
-- Redmine: #730
-- ============================================================
BEGIN
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => 'HMM_RDS_PG_CRED',
username => 'postgres',
password => '<RDS_PASSWORD>'
);
END;
/
-- Passwords are never returned by ALL_CREDENTIALS.
SELECT credential_name, username
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';

View File

@@ -0,0 +1,61 @@
-- ============================================================
-- Step 2: register and verify the host-specific ACL.
-- Run as ADMIN.
--
-- Important:
-- Oracle-managed heterogeneous Database Link traffic uses the
-- managed gateway. This ACL records SQL-user host permissions;
-- it does not replace the AWS RDS Security Group rule.
-- Redmine: #730
-- ============================================================
-- DNS resolution privilege must not have a port range.
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com',
ace => XS$ACE_TYPE(
privilege_list => XS$NAME_LIST('resolve'),
principal_name => SYS_CONTEXT('USERENV', 'SESSION_USER'),
principal_type => XS_ACL.PTYPE_DB
)
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -24243 THEN
RAISE;
END IF;
END;
/
-- Permit only the PostgreSQL service port.
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com',
lower_port => 5432,
upper_port => 5432,
ace => XS$ACE_TYPE(
privilege_list => XS$NAME_LIST('connect'),
principal_name => SYS_CONTEXT('USERENV', 'SESSION_USER'),
principal_type => XS_ACL.PTYPE_DB
)
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -24243 THEN
RAISE;
END IF;
END;
/
SELECT
host,
lower_port,
upper_port,
principal,
privilege,
grant_type
FROM dba_host_aces
WHERE host = 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com'
AND principal = USER
AND privilege IN ('resolve', 'connect')
ORDER BY privilege, lower_port;

View File

@@ -0,0 +1,24 @@
-- ============================================================
-- Step 3: create an Oracle-managed PostgreSQL Database Link.
-- Based on FY26 internal training, section 3.2.3.
-- Run as ADMIN.
-- Redmine: #730
-- ============================================================
BEGIN
DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK(
db_link_name => 'HMM_RDS_PG_LINK',
hostname => 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com',
port => 5432,
service_name => 'postgres',
credential_name => 'HMM_RDS_PG_CRED',
ssl_server_cert_dn => NULL,
private_target => FALSE,
gateway_params => JSON_OBJECT('db_type' VALUE 'postgres')
);
END;
/
SELECT db_link, username, host, created
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';

View File

@@ -0,0 +1,34 @@
-- ============================================================
-- Step 4: query PostgreSQL directly through the Database Link.
-- PostgreSQL schema, table, view, and column identifiers are
-- lower-case and must be double quoted.
-- Redmine: #730
-- ============================================================
SELECT COUNT(*) AS carrier_count
FROM "hmm_demo"."carriers"@HMM_RDS_PG_LINK;
SELECT
COUNT(*) AS performance_count,
MIN("performance_month") AS first_month,
MAX("performance_month") AS latest_month
FROM "hmm_demo"."carrier_monthly_performance"@HMM_RDS_PG_LINK;
SELECT
"carrier_code",
"carrier_name",
"service_region",
"performance_tier"
FROM "hmm_demo"."carriers"@HMM_RDS_PG_LINK
ORDER BY "carrier_code";
SELECT
"carrier_code",
"carrier_name",
"performance_month",
"shipped_teu",
"revenue_usd",
"schedule_reliability_pct",
"risk_level"
FROM "hmm_demo"."carrier_performance_latest_v"@HMM_RDS_PG_LINK
ORDER BY "carrier_code";

View File

@@ -0,0 +1,109 @@
-- ============================================================
-- Step 5: create local Oracle Views and Select AI metadata.
-- Explicit aliases normalize PostgreSQL lower-case identifiers
-- into ordinary Oracle identifiers.
-- Redmine: #730
-- ============================================================
CREATE OR REPLACE VIEW HMM_RDS_CARRIERS_V AS
SELECT
"carrier_code" AS carrier_code,
"carrier_name" AS carrier_name,
"country_code" AS country_code,
"alliance_name" AS alliance_name,
"service_region" AS service_region,
"performance_tier" AS performance_tier
FROM "hmm_demo"."carriers"@HMM_RDS_PG_LINK;
COMMENT ON TABLE HMM_RDS_CARRIERS_V IS
'HMM federation demo fictional carrier master. Primary Key: CARRIER_CODE. Referenced by HMM_RDS_CARRIER_PERF_V(CARRIER_CODE).';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.CARRIER_CODE IS
'Primary Key. Fictional carrier identifier such as C001.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.CARRIER_NAME IS
'Fictional carrier display name; not an actual carrier.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.COUNTRY_CODE IS
'Two-letter demo country code.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.ALLIANCE_NAME IS
'Fictional carrier cooperation network.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.SERVICE_REGION IS
'Primary service region for the demo carrier.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.PERFORMANCE_TIER IS
'Demo portfolio tier: CORE, GROWTH, or WATCH.';
CREATE OR REPLACE VIEW HMM_RDS_CARRIER_PERF_V AS
SELECT
"carrier_code" AS carrier_code,
"performance_month" AS performance_month,
"contracted_teu" AS contracted_teu,
"shipped_teu" AS shipped_teu,
"revenue_usd" AS revenue_usd,
"gross_margin_usd" AS gross_margin_usd,
"schedule_reliability_pct" AS schedule_reliability_pct,
"vessel_utilization_pct" AS vessel_utilization_pct,
"claim_rate_pct" AS claim_rate_pct,
"yoy_growth_pct" AS yoy_growth_pct,
"risk_level" AS risk_level
FROM "hmm_demo"."carrier_monthly_performance"@HMM_RDS_PG_LINK;
COMMENT ON TABLE HMM_RDS_CARRIER_PERF_V IS
'HMM federation demo monthly carrier KPI. Composite Primary Key: CARRIER_CODE and PERFORMANCE_MONTH. Foreign Key: CARRIER_CODE references HMM_RDS_CARRIERS_V(CARRIER_CODE). All values are fictional.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.CARRIER_CODE IS
'Foreign Key referencing HMM_RDS_CARRIERS_V(CARRIER_CODE).';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.PERFORMANCE_MONTH IS
'First calendar day of the KPI month; part of the composite Primary Key.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.CONTRACTED_TEU IS
'Contracted container volume in TEU.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.SHIPPED_TEU IS
'Actually shipped container volume in TEU.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.REVENUE_USD IS
'Fictional monthly revenue in USD.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.GROSS_MARGIN_USD IS
'Fictional monthly gross margin in USD; negative values indicate a loss.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.SCHEDULE_RELIABILITY_PCT IS
'Schedule reliability percentage from 0 through 100.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.VESSEL_UTILIZATION_PCT IS
'Vessel capacity utilization percentage from 0 through 100.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.CLAIM_RATE_PCT IS
'Cargo claim rate percentage from 0 through 100.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.YOY_GROWTH_PCT IS
'Year-over-year performance growth percentage.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.RISK_LEVEL IS
'Derived demo risk category: GREEN, AMBER, or RED.';
CREATE OR REPLACE VIEW HMM_RDS_CARRIER_LATEST_V AS
SELECT
"carrier_code" AS carrier_code,
"carrier_name" AS carrier_name,
"country_code" AS country_code,
"alliance_name" AS alliance_name,
"service_region" AS service_region,
"performance_tier" AS performance_tier,
"performance_month" AS performance_month,
"contracted_teu" AS contracted_teu,
"shipped_teu" AS shipped_teu,
"revenue_usd" AS revenue_usd,
"gross_margin_usd" AS gross_margin_usd,
"schedule_reliability_pct" AS schedule_reliability_pct,
"vessel_utilization_pct" AS vessel_utilization_pct,
"claim_rate_pct" AS claim_rate_pct,
"yoy_growth_pct" AS yoy_growth_pct,
"risk_level" AS risk_level
FROM "hmm_demo"."carrier_performance_latest_v"@HMM_RDS_PG_LINK;
COMMENT ON TABLE HMM_RDS_CARRIER_LATEST_V IS
'Latest fictional monthly KPI for each HMM federation demo carrier. Primary Key: CARRIER_CODE.';
COMMENT ON COLUMN HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE IS
'Primary Key for the latest carrier KPI row.';
COMMENT ON COLUMN HMM_RDS_CARRIER_LATEST_V.PERFORMANCE_MONTH IS
'Latest KPI month available in the PostgreSQL source.';
COMMENT ON COLUMN HMM_RDS_CARRIER_LATEST_V.RISK_LEVEL IS
'Latest derived risk category: GREEN, AMBER, or RED.';
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY object_name;

View File

@@ -0,0 +1,86 @@
-- ============================================================
-- Step 6: verify the local federation Views.
-- Expected: carrier/performance/latest = 8/144/8.
-- Redmine: #730
-- ============================================================
DECLARE
v_carrier_count PLS_INTEGER;
v_performance_count PLS_INTEGER;
v_latest_count PLS_INTEGER;
v_risk_count PLS_INTEGER;
BEGIN
SELECT COUNT(*) INTO v_carrier_count
FROM HMM_RDS_CARRIERS_V;
SELECT COUNT(*) INTO v_performance_count
FROM HMM_RDS_CARRIER_PERF_V;
SELECT COUNT(*), COUNT(DISTINCT risk_level)
INTO v_latest_count, v_risk_count
FROM HMM_RDS_CARRIER_LATEST_V;
IF v_carrier_count != 8 THEN
RAISE_APPLICATION_ERROR(
-20001,
'Expected 8 carriers, found ' || v_carrier_count
);
END IF;
IF v_performance_count != 144 THEN
RAISE_APPLICATION_ERROR(
-20002,
'Expected 144 performance rows, found ' || v_performance_count
);
END IF;
IF v_latest_count != 8 OR v_risk_count != 3 THEN
RAISE_APPLICATION_ERROR(
-20003,
'Expected 8 latest rows and 3 risk levels, found '
|| v_latest_count || ' rows and ' || v_risk_count || ' levels'
);
END IF;
END;
/
SELECT
(SELECT COUNT(*) FROM HMM_RDS_CARRIERS_V) AS carrier_count,
(SELECT COUNT(*) FROM HMM_RDS_CARRIER_PERF_V) AS performance_count,
(SELECT COUNT(*) FROM HMM_RDS_CARRIER_LATEST_V) AS latest_count
FROM dual;
SELECT
MIN(performance_month) AS first_month,
MAX(performance_month) AS latest_month,
COUNT(DISTINCT carrier_code) AS carrier_count
FROM HMM_RDS_CARRIER_PERF_V;
SELECT risk_level, COUNT(*) AS carrier_count
FROM HMM_RDS_CARRIER_LATEST_V
GROUP BY risk_level
ORDER BY risk_level;
SELECT
carrier_code,
carrier_name,
performance_month,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
claim_rate_pct,
risk_level
FROM HMM_RDS_CARRIER_LATEST_V
ORDER BY carrier_code;
SELECT
table_name,
comments
FROM user_tab_comments
WHERE table_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY table_name;

View File

@@ -0,0 +1,63 @@
-- ============================================================
-- Optional pre-recording reset.
-- Drops only the three local Views, Database Link, and
-- Credential owned by this demo. Host ACL entries are retained.
-- Redmine: #730
-- ============================================================
DECLARE
v_count PLS_INTEGER;
BEGIN
FOR object_row IN (
SELECT object_name
FROM user_objects
WHERE object_type = 'VIEW'
AND object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY CASE object_name
WHEN 'HMM_RDS_CARRIER_LATEST_V' THEN 1
WHEN 'HMM_RDS_CARRIER_PERF_V' THEN 2
ELSE 3
END
) LOOP
EXECUTE IMMEDIATE 'DROP VIEW ' || object_row.object_name;
END LOOP;
SELECT COUNT(*) INTO v_count
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';
IF v_count = 1 THEN
EXECUTE IMMEDIATE 'DROP DATABASE LINK HMM_RDS_PG_LINK';
END IF;
SELECT COUNT(*) INTO v_count
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';
IF v_count = 1 THEN
DBMS_CLOUD.DROP_CREDENTIAL(
credential_name => 'HMM_RDS_PG_CRED'
);
END IF;
END;
/
SELECT credential_name, username
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';
SELECT db_link, username, host
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';
SELECT object_name, object_type
FROM user_objects
WHERE object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
);

View File

@@ -0,0 +1,209 @@
-- ============================================================
-- HMM carrier assignments using existing access-group tables.
--
-- Existing tables:
-- HMM_HR_EMPLOYEES
-- HMM_ACCESS_GROUPS
-- HMM_ACCESS_GROUP_MEMBERS
--
-- No new assignment table is created.
-- Korean values are reconstructed from UTF-8 base64 so SQLcl
-- cannot corrupt the literals.
-- Redmine: #730
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
SET SERVEROUTPUT ON
MERGE INTO hmm_access_groups dst
USING (
SELECT
base.max_group_id + carrier.seq_no AS group_id,
'CARRIER_' || carrier.carrier_code AS group_code,
carrier.carrier_code || ' ' ||
utl_i18n.raw_to_char(
utl_encode.base64_decode(
utl_raw.cast_to_raw('7ISg7IKsIOuLtOuLuSDqt7jro7k=')
),
'AL32UTF8'
) AS group_name,
utl_i18n.raw_to_char(
utl_encode.base64_decode(
utl_raw.cast_to_raw(
'642w66qo7JqpIOyEoOyCrCDsoJHqt7wg67CPIOuLtOuLueyekCDqt7jro7k='
)
),
'AL32UTF8'
) || ' (' || carrier.carrier_code || ')' AS description
FROM (
SELECT 1 seq_no, 'C001' carrier_code FROM dual
UNION ALL SELECT 2, 'C002' FROM dual
UNION ALL SELECT 3, 'C003' FROM dual
UNION ALL SELECT 4, 'C004' FROM dual
UNION ALL SELECT 5, 'C005' FROM dual
UNION ALL SELECT 6, 'C006' FROM dual
UNION ALL SELECT 7, 'C007' FROM dual
UNION ALL SELECT 8, 'C008' FROM dual
) carrier
CROSS JOIN (
SELECT NVL(MAX(group_id), 0) max_group_id
FROM hmm_access_groups
) base
) src
ON (dst.group_code = src.group_code)
WHEN MATCHED THEN UPDATE SET
dst.group_name = src.group_name,
dst.description = src.description,
dst.active_yn = 'Y',
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
group_id,
group_code,
group_name,
description,
active_yn,
created_at,
updated_at
) VALUES (
src.group_id,
src.group_code,
src.group_name,
src.description,
'Y',
SYSTIMESTAMP,
SYSTIMESTAMP
);
MERGE INTO hmm_access_group_members dst
USING (
SELECT
access_group.group_id,
employee.employee_id
FROM (
SELECT 'E1002' employee_code, 'CARRIER_C001' group_code FROM dual
UNION ALL SELECT 'E1002', 'CARRIER_C002' FROM dual
UNION ALL SELECT 'E1003', 'CARRIER_C003' FROM dual
UNION ALL SELECT 'E1004', 'CARRIER_C004' FROM dual
UNION ALL SELECT 'E1005', 'CARRIER_C005' FROM dual
UNION ALL SELECT 'E1006', 'CARRIER_C006' FROM dual
UNION ALL SELECT 'E1006', 'CARRIER_C007' FROM dual
UNION ALL SELECT 'E1007', 'CARRIER_C008' FROM dual
) assignment
JOIN hmm_hr_employees employee
ON employee.employee_code = assignment.employee_code
AND employee.employment_status = 'ACTIVE'
JOIN hmm_access_groups access_group
ON access_group.group_code = assignment.group_code
AND access_group.active_yn = 'Y'
) src
ON (
dst.group_id = src.group_id
AND dst.employee_id = src.employee_id
)
WHEN NOT MATCHED THEN INSERT (
group_id,
employee_id,
created_at
) VALUES (
src.group_id,
src.employee_id,
SYSTIMESTAMP
);
CREATE OR REPLACE VIEW hmm_carrier_assignments_v AS
SELECT
employee.employee_id,
employee.employee_code,
employee.full_name AS employee_name,
employee.job_title,
employee.team_id,
employee.manager_employee_id,
manager.employee_code AS manager_employee_code,
manager.full_name AS manager_name,
SUBSTR(access_group.group_code, LENGTH('CARRIER_') + 1) AS carrier_code,
access_group.group_code,
access_group.group_name
FROM hmm_access_group_members membership
JOIN hmm_access_groups access_group
ON access_group.group_id = membership.group_id
AND access_group.active_yn = 'Y'
JOIN hmm_hr_employees employee
ON employee.employee_id = membership.employee_id
AND employee.employment_status = 'ACTIVE'
LEFT JOIN hmm_hr_employees manager
ON manager.employee_id = employee.manager_employee_id
WHERE access_group.group_code LIKE 'CARRIER_C%';
COMMENT ON TABLE hmm_carrier_assignments_v IS
'ADB employee-to-carrier assignments from HMM_HR_EMPLOYEES, HMM_ACCESS_GROUPS, and HMM_ACCESS_GROUP_MEMBERS. Join relationship: HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE joins HMM_RDS_CARRIERS_V.CARRIER_CODE, HMM_RDS_CARRIER_PERF_V.CARRIER_CODE, and HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_carrier_assignments_v.EMPLOYEE_CODE IS
'HMM employee code sourced from HMM_HR_EMPLOYEES.EMPLOYEE_CODE. Example: E1002.';
COMMENT ON COLUMN hmm_carrier_assignments_v.MANAGER_EMPLOYEE_CODE IS
'Direct manager employee code sourced from HMM_HR_EMPLOYEES. Use E1001 to identify the HMM demo manager team.';
COMMENT ON COLUMN hmm_carrier_assignments_v.CARRIER_CODE IS
'Join Key and Foreign Key to HMM_RDS_CARRIERS_V(CARRIER_CODE), HMM_RDS_CARRIER_PERF_V(CARRIER_CODE), and HMM_RDS_CARRIER_LATEST_V(CARRIER_CODE).';
COMMENT ON COLUMN hmm_carrier_assignments_v.GROUP_CODE IS
'Existing access-group code in the form CARRIER_C001.';
COMMIT;
DECLARE
v_group_count PLS_INTEGER;
v_member_count PLS_INTEGER;
v_assignment_count PLS_INTEGER;
v_federated_count PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_group_count
FROM hmm_access_groups
WHERE group_code BETWEEN 'CARRIER_C001' AND 'CARRIER_C008'
AND active_yn = 'Y';
SELECT COUNT(*)
INTO v_member_count
FROM hmm_access_group_members membership
JOIN hmm_access_groups access_group
ON access_group.group_id = membership.group_id
WHERE access_group.group_code BETWEEN 'CARRIER_C001' AND 'CARRIER_C008';
SELECT COUNT(*)
INTO v_assignment_count
FROM hmm_carrier_assignments_v;
SELECT COUNT(*)
INTO v_federated_count
FROM hmm_carrier_assignments_v assignment
JOIN hmm_rds_carrier_latest_v performance
ON performance.carrier_code = assignment.carrier_code;
IF v_group_count != 8
OR v_member_count != 8
OR v_assignment_count != 8
OR v_federated_count != 8 THEN
RAISE_APPLICATION_ERROR(
-20081,
'Expected group/member/assignment/federated counts 8/8/8/8; found '
|| v_group_count || '/'
|| v_member_count || '/'
|| v_assignment_count || '/'
|| v_federated_count
);
END IF;
END;
/
SELECT
assignment.employee_code,
assignment.employee_name,
assignment.manager_employee_code,
assignment.carrier_code,
performance.carrier_name,
performance.performance_month,
performance.revenue_usd,
performance.risk_level
FROM hmm_carrier_assignments_v assignment
JOIN hmm_rds_carrier_latest_v performance
ON performance.carrier_code = assignment.carrier_code
ORDER BY assignment.employee_code, assignment.carrier_code;

View File

@@ -0,0 +1,67 @@
-- ============================================================
-- Explicit Select AI relationship metadata for ADB + RDS Views.
-- Redmine: #730
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
COMMENT ON TABLE hmm_carrier_assignments_v IS
'ADB employee-to-carrier assignments from HMM_HR_EMPLOYEES, HMM_ACCESS_GROUPS, and HMM_ACCESS_GROUP_MEMBERS. Join relationship: HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE joins HMM_RDS_CARRIERS_V.CARRIER_CODE, HMM_RDS_CARRIER_PERF_V.CARRIER_CODE, and HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_carrier_assignments_v.EMPLOYEE_CODE IS
'HMM employee code sourced from HMM_HR_EMPLOYEES.EMPLOYEE_CODE. Example: E1002.';
COMMENT ON COLUMN hmm_carrier_assignments_v.MANAGER_EMPLOYEE_CODE IS
'Direct manager employee code sourced from HMM_HR_EMPLOYEES. Use E1001 to identify the HMM demo manager team.';
COMMENT ON COLUMN hmm_carrier_assignments_v.CARRIER_CODE IS
'Join Key and Foreign Key to HMM_RDS_CARRIERS_V(CARRIER_CODE), HMM_RDS_CARRIER_PERF_V(CARRIER_CODE), and HMM_RDS_CARRIER_LATEST_V(CARRIER_CODE).';
COMMENT ON TABLE hmm_rds_carriers_v IS
'PostgreSQL fictional carrier master exposed through HMM_RDS_PG_LINK. Primary Key: CARRIER_CODE. Join relationship: HMM_RDS_CARRIERS_V.CARRIER_CODE joins HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE and HMM_RDS_CARRIER_PERF_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_rds_carriers_v.CARRIER_CODE IS
'Primary Key. Join Key to HMM_CARRIER_ASSIGNMENTS_V(CARRIER_CODE), HMM_RDS_CARRIER_PERF_V(CARRIER_CODE), and HMM_RDS_CARRIER_LATEST_V(CARRIER_CODE).';
COMMENT ON TABLE hmm_rds_carrier_perf_v IS
'PostgreSQL monthly carrier KPI exposed through HMM_RDS_PG_LINK. Composite Primary Key: CARRIER_CODE and PERFORMANCE_MONTH. Foreign Key: CARRIER_CODE references HMM_RDS_CARRIERS_V(CARRIER_CODE). Join relationship: CARRIER_CODE joins HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_rds_carrier_perf_v.CARRIER_CODE IS
'Foreign Key to HMM_RDS_CARRIERS_V(CARRIER_CODE). Join Key to HMM_CARRIER_ASSIGNMENTS_V(CARRIER_CODE).';
COMMENT ON COLUMN hmm_rds_carrier_perf_v.PERFORMANCE_MONTH IS
'Part of the composite Primary Key with CARRIER_CODE. First calendar day of the KPI month.';
COMMENT ON TABLE hmm_rds_carrier_latest_v IS
'Latest PostgreSQL KPI for each fictional carrier exposed through HMM_RDS_PG_LINK. Primary Key: CARRIER_CODE. Join relationship: HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE joins HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE and HMM_RDS_CARRIERS_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_rds_carrier_latest_v.CARRIER_CODE IS
'Primary Key. Join Key to HMM_CARRIER_ASSIGNMENTS_V(CARRIER_CODE) and HMM_RDS_CARRIERS_V(CARRIER_CODE).';
SELECT table_name, comments
FROM user_tab_comments
WHERE table_name IN (
'HMM_CARRIER_ASSIGNMENTS_V',
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY table_name;
SELECT table_name, column_name, comments
FROM user_col_comments
WHERE table_name IN (
'HMM_CARRIER_ASSIGNMENTS_V',
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
AND column_name IN (
'EMPLOYEE_CODE',
'MANAGER_EMPLOYEE_CODE',
'CARRIER_CODE',
'PERFORMANCE_MONTH'
)
ORDER BY table_name, column_name;