refs #742: merge HMM application layout into main
# Conflicts: # src/main/java/com/cloudhandson/vpdbackoffice/service/StructuredDataService.java # vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataService.java # vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/service/VectorKnowledgeService.java # vpd-backoffice/src/main/resources/mapper/MaskingRuleMapper.xml # vpd-backoffice/src/main/resources/templates/schema-metadata.html # vpd-backoffice/src/main/resources/templates/structured-data.html
This commit is contained in:
93
database/adb/00_cleanup.sql
Normal file
93
database/adb/00_cleanup.sql
Normal 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
119
database/adb/01_dblinks.sql
Normal 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
|
||||
56
database/adb/02_perm_tables.sql
Normal file
56
database/adb/02_perm_tables.sql
Normal 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
87
database/adb/03_seed.sql
Normal 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;
|
||||
70
database/adb/04_secure_ctx.sql
Normal file
70
database/adb/04_secure_ctx.sql
Normal 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
31
database/adb/05_views.sql
Normal 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;
|
||||
84
database/adb/06_policy.sql
Normal file
84
database/adb/06_policy.sql
Normal 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;
|
||||
87
database/adb/06a_redaction.sql
Normal file
87
database/adb/06a_redaction.sql
Normal 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;
|
||||
77
database/adb/07_end_users.sql
Normal file
77
database/adb/07_end_users.sql
Normal 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;
|
||||
75
database/adb/08_tests_user_my.sql
Normal file
75
database/adb/08_tests_user_my.sql
Normal 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;
|
||||
44
database/adb/09_tests_user_pg.sql
Normal file
44
database/adb/09_tests_user_pg.sql
Normal 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;
|
||||
45
database/adb/10_tests_user_both.sql
Normal file
45
database/adb/10_tests_user_both.sql
Normal 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;
|
||||
53
database/adb/11_tests_user_none.sql
Normal file
53
database/adb/11_tests_user_none.sql
Normal 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;
|
||||
83
database/adb/12_tests_admin_audit.sql
Normal file
83
database/adb/12_tests_admin_audit.sql
Normal 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;
|
||||
192
database/adb/13_dds_variant.sql
Normal file
192
database/adb/13_dds_variant.sql
Normal 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;
|
||||
61
database/adb/14_tests_dds_user.sql
Normal file
61
database/adb/14_tests_dds_user.sql
Normal 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;
|
||||
53
database/adb/15_dds_cleanup.sql
Normal file
53
database/adb/15_dds_cleanup.sql
Normal 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;
|
||||
94
database/adb/16_agent_ords_security_local_cleanup.sql
Normal file
94
database/adb/16_agent_ords_security_local_cleanup.sql
Normal 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;
|
||||
489
database/adb/17_agent_ords_security_local_vpd_setup.sql
Normal file
489
database/adb/17_agent_ords_security_local_vpd_setup.sql
Normal 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;
|
||||
115
database/adb/18_agent_ords_security_local_vpd_test.sql
Normal file
115
database/adb/18_agent_ords_security_local_vpd_test.sql
Normal 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;
|
||||
89
database/adb/19_agent_ords_security_local_dds_setup.sql
Normal file
89
database/adb/19_agent_ords_security_local_dds_setup.sql
Normal 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;
|
||||
46
database/adb/20_agent_ords_security_local_dds_test.sql
Normal file
46
database/adb/20_agent_ords_security_local_dds_test.sql
Normal 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;
|
||||
25
database/adb/21_agent_ords_security_ords_enable_schema.sql
Normal file
25
database/adb/21_agent_ords_security_ords_enable_schema.sql
Normal 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;
|
||||
196
database/adb/22_agent_ords_security_ords_handler_setup.sql
Normal file
196
database/adb/22_agent_ords_security_ords_handler_setup.sql
Normal 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;
|
||||
44
database/adb/23_agent_ords_security_ords_handler_test.sql
Normal file
44
database/adb/23_agent_ords_security_ords_handler_test.sql
Normal 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;
|
||||
99
database/adb/24_agent_ords_security_inventory.sql
Normal file
99
database/adb/24_agent_ords_security_inventory.sql
Normal 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;
|
||||
418
database/adb/25_agent_ords_security_backoffice_support.sql
Normal file
418
database/adb/25_agent_ords_security_backoffice_support.sql
Normal 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;
|
||||
379
database/adb/26_agent_ords_security_dynamic_vpd_filter.sql
Normal file
379
database/adb/26_agent_ords_security_dynamic_vpd_filter.sql
Normal 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 ===
|
||||
318
database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql
Normal file
318
database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql
Normal 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 ===
|
||||
268
database/adb/28_agent_ords_vector_tag_vpd_setup.sql
Normal file
268
database/adb/28_agent_ords_vector_tag_vpd_setup.sql
Normal 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;
|
||||
194
database/adb/29_agent_ords_vector_search_ords.sql
Normal file
194
database/adb/29_agent_ords_vector_search_ords.sql
Normal 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;
|
||||
130
database/adb/30_agent_ords_vector_tag_vpd_test.sql
Normal file
130
database/adb/30_agent_ords_vector_tag_vpd_test.sql
Normal 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;
|
||||
58
database/adb/31_agent_ords_backoffice_ux_metadata.sql
Normal file
58
database/adb/31_agent_ords_backoffice_ux_metadata.sql
Normal 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;
|
||||
119
database/adb/31_dds_standalone_demo_setup.sql
Normal file
119
database/adb/31_dds_standalone_demo_setup.sql
Normal 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;
|
||||
156
database/adb/32_dds_vector_tag_setup.sql
Normal file
156
database/adb/32_dds_vector_tag_setup.sql
Normal 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;
|
||||
21
database/adb/33_agent_ords_sql_trace_grant.sql
Normal file
21
database/adb/33_agent_ords_sql_trace_grant.sql
Normal 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;
|
||||
28
database/adb/34_agent_ords_execution_evidence_grant.sql
Normal file
28
database/adb/34_agent_ords_execution_evidence_grant.sql
Normal 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;
|
||||
163
database/adb/34_dds_token_data_grant_common_auth.sql
Normal file
163
database/adb/34_dds_token_data_grant_common_auth.sql
Normal 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;
|
||||
24
database/adb/35_dds_token_data_grant_cleanup.sql
Normal file
24
database/adb/35_dds_token_data_grant_cleanup.sql
Normal 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;
|
||||
173
database/adb/36_dds_sales_knowledge_scenario.sql
Normal file
173
database/adb/36_dds_sales_knowledge_scenario.sql
Normal 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;
|
||||
18
database/adb/37_dds_sales_knowledge_scenario_cleanup.sql
Normal file
18
database/adb/37_dds_sales_knowledge_scenario_cleanup.sql
Normal 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;
|
||||
@@ -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;
|
||||
47
database/adb/39_agent_ords_default_user_display_name.sql
Normal file
47
database/adb/39_agent_ords_default_user_display_name.sql
Normal 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;
|
||||
211
database/adb/40_dds_protection_evidence.sql
Normal file
211
database/adb/40_dds_protection_evidence.sql
Normal 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;
|
||||
24
database/adb/41_dds_row_column_fixture.sql
Normal file
24
database/adb/41_dds_row_column_fixture.sql
Normal 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.
|
||||
70
database/adb/42_agent_ords_fga_execution_audit.sql
Normal file
70
database/adb/42_agent_ords_fga_execution_audit.sql
Normal 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;
|
||||
32
database/adb/42_dds_fga_execution_evidence.sql
Normal file
32
database/adb/42_dds_fga_execution_evidence.sql
Normal 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;
|
||||
59
database/adb/43_agent_ords_probe_id_handler_patch.sql
Normal file
59
database/adb/43_agent_ords_probe_id_handler_patch.sql
Normal 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;
|
||||
244
database/adb/44_dds_mcp_local_end_user_setup.sql
Normal file
244
database/adb/44_dds_mcp_local_end_user_setup.sql
Normal 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;
|
||||
59
database/adb/45_dds_mcp_local_end_user_test.sql
Normal file
59
database/adb/45_dds_mcp_local_end_user_test.sql
Normal 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;
|
||||
471
database/adb/46_kb_stakeholder_token_permission_example.sql
Normal file
471
database/adb/46_kb_stakeholder_token_permission_example.sql
Normal 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 ===
|
||||
104
database/adb/47_kb_stakeholder_permission_or_branches.sql
Normal file
104
database/adb/47_kb_stakeholder_permission_or_branches.sql
Normal file
@@ -0,0 +1,104 @@
|
||||
-- ============================================================
|
||||
-- 47_kb_stakeholder_permission_or_branches.sql
|
||||
--
|
||||
-- Refines the KB example so the permission table carries the expected
|
||||
-- stakeholder role and each role branch becomes an OR candidate in the
|
||||
-- dynamic VPD filter. Execute after 26 and 46 as ADMIN.
|
||||
-- ============================================================
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET DEFINE OFF
|
||||
|
||||
PROMPT === 1. Storing expected stakeholder role in example rules ===
|
||||
UPDATE cb_permission_rule r
|
||||
SET rule_value = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
WHERE p.perm_id = r.perm_id
|
||||
AND ar.role_name = 'KB_DESIGNER_ROLE'
|
||||
) THEN '설계사'
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
WHERE p.perm_id = r.perm_id
|
||||
AND ar.role_name = 'KB_BRANCH_MANAGER_ROLE'
|
||||
) THEN '지점장'
|
||||
END
|
||||
WHERE r.rule_type IN ('STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL')
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role ar ON ar.role_id = p.role_id
|
||||
WHERE p.perm_id = r.perm_id
|
||||
AND ar.role_name IN ('KB_DESIGNER_ROLE', 'KB_BRANCH_MANAGER_ROLE')
|
||||
);
|
||||
|
||||
PROMPT === 2. Adding an identity role so every stakeholder token can resolve itself ===
|
||||
DECLARE
|
||||
v_role_id NUMBER;
|
||||
v_permission_id NUMBER;
|
||||
v_count NUMBER;
|
||||
BEGIN
|
||||
BEGIN
|
||||
SELECT role_id INTO v_role_id
|
||||
FROM cb_app_role
|
||||
WHERE role_name = 'KB_STAKEHOLDER_IDENTITY_ROLE';
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
SELECT NVL(MAX(role_id), 0) + 1 INTO v_role_id FROM cb_app_role;
|
||||
INSERT INTO cb_app_role(role_id, role_name, max_sensitivity_level)
|
||||
VALUES (v_role_id, 'KB_STAKEHOLDER_IDENTITY_ROLE', 'INTERNAL');
|
||||
END;
|
||||
|
||||
FOR u IN (
|
||||
SELECT user_id
|
||||
FROM cb_app_user
|
||||
WHERE stakeholder_user_id IS NOT NULL
|
||||
AND active = 'Y'
|
||||
) LOOP
|
||||
SELECT COUNT(*) INTO v_count
|
||||
FROM cb_user_role
|
||||
WHERE user_id = u.user_id
|
||||
AND role_id = v_role_id;
|
||||
IF v_count = 0 THEN
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (u.user_id, v_role_id);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
BEGIN
|
||||
SELECT perm_id INTO v_permission_id
|
||||
FROM cb_permission
|
||||
WHERE role_id = v_role_id
|
||||
AND target_name = 'KB_STAKEHOLDERS'
|
||||
AND action_name = 'SELECT';
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
SELECT cb_permission_seq.NEXTVAL INTO v_permission_id FROM dual;
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
||||
VALUES (v_permission_id, v_role_id, 'KB_STAKEHOLDERS', 'SELECT', 'ALLOW');
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (cb_permission_rule_seq.NEXTVAL, v_permission_id, 'USER_ID', '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 ===
|
||||
137
database/adb/48_kb_permission_condition_codes.sql
Normal file
137
database/adb/48_kb_permission_condition_codes.sql
Normal 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 ===
|
||||
35
database/adb/49_permission_static_sql_condition.sql
Normal file
35
database/adb/49_permission_static_sql_condition.sql
Normal 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 ===
|
||||
228
database/adb/51_kb_vpd_admin_token.sql
Normal file
228
database/adb/51_kb_vpd_admin_token.sql
Normal 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 ===
|
||||
223
database/adb/52_cleanup_to_kb_vpd_poc.sql
Normal file
223
database/adb/52_cleanup_to_kb_vpd_poc.sql
Normal 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 ===
|
||||
101
database/adb/53_drop_legacy_poc_objects.sql
Normal file
101
database/adb/53_drop_legacy_poc_objects.sql
Normal 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 ===
|
||||
47
database/adb/54_drop_legacy_dds_ords_examples.sql
Normal file
47
database/adb/54_drop_legacy_dds_ords_examples.sql
Normal 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 ===
|
||||
174
database/adb/55_reapply_kb_vpd_after_table_reload.sql
Normal file
174
database/adb/55_reapply_kb_vpd_after_table_reload.sql
Normal 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
|
||||
104
database/adb/56_kb_select_ai_router_team.sql
Normal file
104
database/adb/56_kb_select_ai_router_team.sql
Normal 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
|
||||
17
database/adb/57_kb_select_ai_router_team_test.sql
Normal file
17
database/adb/57_kb_select_ai_router_team_test.sql
Normal 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
|
||||
57
database/adb/58_kb_select_ai_router_api.sql
Normal file
57
database/adb/58_kb_select_ai_router_api.sql
Normal 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
|
||||
213
database/adb/59_kb_select_ai_router_ords.sql
Normal file
213
database/adb/59_kb_select_ai_router_ords.sql
Normal 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
|
||||
198
database/adb/60_kb_vpd_admin_full_access_token.sql
Normal file
198
database/adb/60_kb_vpd_admin_full_access_token.sql
Normal 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
|
||||
100
database/adb/62_kb_aso_masking_backoffice_metadata.sql
Normal file
100
database/adb/62_kb_aso_masking_backoffice_metadata.sql
Normal 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
|
||||
679
database/adb/63_kb_aso_masking_rule_runtime.sql
Normal file
679
database/adb/63_kb_aso_masking_rule_runtime.sql
Normal 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
|
||||
54
database/adb/64_kb_aso_masking_default_column_rules.sql
Normal file
54
database/adb/64_kb_aso_masking_default_column_rules.sql
Normal 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
|
||||
221
database/adb/65_kb_select_ai_vpd_query_api.sql
Normal file
221
database/adb/65_kb_select_ai_vpd_query_api.sql
Normal 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
|
||||
149
database/adb/66_kb_select_ai_vpd_query_ords.sql
Normal file
149
database/adb/66_kb_select_ai_vpd_query_ords.sql
Normal 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
|
||||
@@ -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 ===
|
||||
47
database/adb/70_sg_tool_user.sql
Normal file
47
database/adb/70_sg_tool_user.sql
Normal 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;
|
||||
329
database/adb/71_sg_identity_administration.sql
Normal file
329
database/adb/71_sg_identity_administration.sql
Normal file
@@ -0,0 +1,329 @@
|
||||
-- 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');
|
||||
create_if_missing('create table sg_vpd_policy_note (
|
||||
object_owner varchar2(128) not null,
|
||||
object_name varchar2(128) not null,
|
||||
policy_name varchar2(128) not null,
|
||||
description varchar2(2000),
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_vpd_policy_note_pk primary key (object_owner, object_name, policy_name)
|
||||
)');
|
||||
create_if_missing('create table sg_vpd_filter_note (
|
||||
function_owner varchar2(128) not null,
|
||||
function_name varchar2(128) not null,
|
||||
description varchar2(2000),
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_vpd_filter_note_pk primary key (function_owner, function_name)
|
||||
)');
|
||||
create_if_missing('create table sg_masking_rule (
|
||||
rule_id number primary key,
|
||||
rule_code varchar2(100) not null unique,
|
||||
rule_name varchar2(200) not null,
|
||||
template_code varchar2(100) not null,
|
||||
description varchar2(2000),
|
||||
enabled_yn char(1) default ''Y'' not null,
|
||||
created_at timestamp default systimestamp not null,
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_masking_rule_enabled_ck check (enabled_yn in (''Y'', ''N''))
|
||||
)');
|
||||
create_if_missing('create table sg_access_bearer_token (
|
||||
key_id number primary key, user_id number not null, key_prefix varchar2(100) not null,
|
||||
key_hash varchar2(256) not null unique, expires_at timestamp not null, revoked_at timestamp,
|
||||
description varchar2(500), created_at timestamp default systimestamp not null,
|
||||
constraint sg_access_bearer_token_user_fk foreign key (user_id) references sg_app_user(user_id)
|
||||
)');
|
||||
create_if_missing('create table sg_backoffice_setting (
|
||||
setting_key varchar2(200) primary key, setting_value varchar2(4000),
|
||||
updated_at timestamp default systimestamp not null
|
||||
)');
|
||||
create_if_missing('create table sg_vector_document_chunk (
|
||||
chunk_id number primary key, document_id varchar2(200) not null, chunk_no number not null,
|
||||
content clob not null, embedding vector(1536, float32), created_at timestamp default systimestamp not null
|
||||
)');
|
||||
create_if_missing('create table sg_vector_document_tag (
|
||||
chunk_id number not null, tech_tag varchar2(200) not null,
|
||||
constraint sg_vector_document_tag_pk primary key (chunk_id, tech_tag),
|
||||
constraint sg_vector_document_tag_chunk_fk foreign key (chunk_id) references sg_vector_document_chunk(chunk_id)
|
||||
)');
|
||||
create_if_missing('create sequence sg_vector_chunk_seq start with 1 increment by 1 nocache');
|
||||
create_if_missing('create table sg_column_masking_rule (
|
||||
column_id number primary key,
|
||||
rule_id number not null,
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_column_masking_rule_column_fk foreign key (column_id) references sg_protected_column(column_id),
|
||||
constraint sg_column_masking_rule_rule_fk foreign key (rule_id) references sg_masking_rule(rule_id)
|
||||
)');
|
||||
create_if_missing('create table sg_user_masking_rule (
|
||||
user_id number not null,
|
||||
column_id number not null,
|
||||
decision varchar2(30) not null,
|
||||
active_yn char(1) default ''Y'' not null,
|
||||
updated_at timestamp default systimestamp not null,
|
||||
constraint sg_user_masking_rule_pk primary key (user_id, column_id),
|
||||
constraint sg_user_masking_rule_user_fk foreign key (user_id) references sg_app_user(user_id),
|
||||
constraint sg_user_masking_rule_column_fk foreign key (column_id) references sg_protected_column(column_id)
|
||||
)');
|
||||
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;
|
||||
|
||||
-- Compatibility layer for remaining backoffice modules. The data is stored
|
||||
-- only in SG_* tables; these views prevent older controller paths from
|
||||
-- querying non-existent CB_* physical tables during the Smilegate transition.
|
||||
create or replace view cb_app_user as
|
||||
select user_id, user_name, employee_no, dept_code, can_read_contents, active from sg_app_user;
|
||||
create or replace view cb_app_group as
|
||||
select group_id, group_code, group_name, description, active_yn from sg_app_group;
|
||||
create or replace view cb_app_role as
|
||||
select role_id, role_name, max_sensitivity_level from sg_app_role;
|
||||
create or replace view cb_user_role as select user_id, role_id from sg_user_role;
|
||||
create or replace view cb_user_group as select group_id, user_id from sg_user_group;
|
||||
create or replace view cb_group_role as select group_id, role_id from sg_group_role;
|
||||
create or replace view cb_protected_object as
|
||||
select object_id, owner, object_name, ords_path, enabled_yn, description from sg_protected_object;
|
||||
create or replace view cb_protected_column as
|
||||
select column_id, object_id, column_name, sensitive_yn, visible_role_id, sensitivity_level, redaction_method from sg_protected_column;
|
||||
create or replace view cb_permission as
|
||||
select perm_id, role_id, target_name, action_name, permission_effect from sg_permission;
|
||||
create or replace view cb_permission_rule as
|
||||
select rule_id, perm_id, rule_column, rule_type, rule_value from sg_permission_rule;
|
||||
create or replace view cb_permission_column as
|
||||
select permission_id, column_name from sg_permission_column;
|
||||
create or replace view cb_vpd_policy_note as
|
||||
select object_owner, object_name, policy_name, description, updated_at from sg_vpd_policy_note;
|
||||
create or replace view cb_vpd_filter_note as
|
||||
select function_owner, function_name, description, updated_at from sg_vpd_filter_note;
|
||||
create or replace view cb_masking_rule as
|
||||
select rule_id, rule_code, rule_name, template_code, description, enabled_yn from sg_masking_rule;
|
||||
create or replace view cb_column_masking_rule as
|
||||
select column_id, rule_id, updated_at from sg_column_masking_rule;
|
||||
create or replace view cb_user_masking_rule as
|
||||
select user_id, column_id, decision, active_yn, updated_at from sg_user_masking_rule;
|
||||
create or replace view cb_ords_probe_audit as
|
||||
select audit_id, event_type, key_id, object_id, status, row_count, error_code, message, created_at from sg_audit_event;
|
||||
create or replace view cb_backoffice_setting as
|
||||
select setting_key, setting_value, updated_at from sg_backoffice_setting;
|
||||
|
||||
-- PoC administrator access: both demo operators can manage and query every
|
||||
-- Smilegate game-data object registered in SGMP_POC.
|
||||
merge into sg_app_role t
|
||||
using (select 3099 role_id, 'DATA_AI_POC_ADMIN' role_name,
|
||||
'Full access to all Smilegate PoC game-data objects' description,
|
||||
'RESTRICTED' max_sensitivity_level 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_role t
|
||||
using (select 1001 user_id, 3099 role_id from dual union all select 1002, 3099 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);
|
||||
|
||||
declare
|
||||
l_object_id number;
|
||||
l_permission_id number;
|
||||
begin
|
||||
for source_object in (
|
||||
select table_name
|
||||
from all_tables
|
||||
where owner = 'SGMP_POC'
|
||||
and table_name not in ('SEMANTIC_METADATA_CHANGE_LOG', 'SGMP_TERM_CONTEXT_CACHE',
|
||||
'SGMP_TERM_DICTIONARY', 'SGMP_TERM_SEARCH_LOG', 'SGMP_TERM_SYNONYM')
|
||||
order by table_name
|
||||
) loop
|
||||
begin
|
||||
select object_id into l_object_id
|
||||
from sg_protected_object
|
||||
where owner = 'SGMP_POC' and object_name = source_object.table_name;
|
||||
exception
|
||||
when no_data_found then
|
||||
select nvl(max(object_id), 0) + 1 into l_object_id from sg_protected_object;
|
||||
insert into sg_protected_object (object_id, owner, object_name, ords_path, enabled_yn, description)
|
||||
values (l_object_id, 'SGMP_POC', source_object.table_name,
|
||||
'/sgmp-poc/' || lower(source_object.table_name), 'Y',
|
||||
'Smilegate PoC game-data object');
|
||||
end;
|
||||
|
||||
begin
|
||||
select perm_id into l_permission_id
|
||||
from sg_permission
|
||||
where role_id = 3099 and target_name = source_object.table_name;
|
||||
exception
|
||||
when no_data_found then
|
||||
l_permission_id := sg_permission_seq.nextval;
|
||||
insert into sg_permission (perm_id, role_id, target_name, action_name, permission_effect)
|
||||
values (l_permission_id, 3099, source_object.table_name, 'SELECT', 'ALLOW');
|
||||
insert into sg_permission_rule (rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
values (sg_permission_rule_seq.nextval, l_permission_id, null, 'ALL', null);
|
||||
end;
|
||||
end loop;
|
||||
commit;
|
||||
end;
|
||||
/
|
||||
597
database/adb/72_hmm_leave_team_vpd.sql
Normal file
597
database/adb/72_hmm_leave_team_vpd.sql
Normal 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 ===
|
||||
159
database/adb/73_hmm_mcp_vpd_runtime.sql
Normal file
159
database/adb/73_hmm_mcp_vpd_runtime.sql
Normal 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.
|
||||
32
database/adb/73_hmm_rds_pg_00_precheck.sql
Normal file
32
database/adb/73_hmm_rds_pg_00_precheck.sql
Normal 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.
|
||||
20
database/adb/74_hmm_rds_pg_01_credential.sql
Normal file
20
database/adb/74_hmm_rds_pg_01_credential.sql
Normal 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';
|
||||
61
database/adb/75_hmm_rds_pg_02_acl.sql
Normal file
61
database/adb/75_hmm_rds_pg_02_acl.sql
Normal 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;
|
||||
24
database/adb/76_hmm_rds_pg_03_dblink.sql
Normal file
24
database/adb/76_hmm_rds_pg_03_dblink.sql
Normal 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';
|
||||
34
database/adb/77_hmm_rds_pg_04_remote_query.sql
Normal file
34
database/adb/77_hmm_rds_pg_04_remote_query.sql
Normal 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";
|
||||
109
database/adb/78_hmm_rds_pg_05_views.sql
Normal file
109
database/adb/78_hmm_rds_pg_05_views.sql
Normal 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;
|
||||
86
database/adb/79_hmm_rds_pg_06_verify.sql
Normal file
86
database/adb/79_hmm_rds_pg_06_verify.sql
Normal 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;
|
||||
63
database/adb/80_hmm_rds_pg_99_cleanup.sql
Normal file
63
database/adb/80_hmm_rds_pg_99_cleanup.sql
Normal 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'
|
||||
);
|
||||
209
database/adb/81_hmm_carrier_access_groups.sql
Normal file
209
database/adb/81_hmm_carrier_access_groups.sql
Normal 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;
|
||||
67
database/adb/82_hmm_federation_relationship_comments.sql
Normal file
67
database/adb/82_hmm_federation_relationship_comments.sql
Normal 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;
|
||||
51
database/source/mysql_setup.sql
Normal file
51
database/source/mysql_setup.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
-- ============================================================
|
||||
-- database/source/mysql_setup.sql
|
||||
-- Run on the REMOTE MySQL instance (mysql CLI).
|
||||
--
|
||||
-- Creates the customers table in the `ecommerce_poc` schema with
|
||||
-- sample data spanning multiple regions (APAC / EMEA / AMER).
|
||||
--
|
||||
-- 동일 customer_id 가 Postgres seed 와 겹치지만, 다른 DB 이므로 무관.
|
||||
-- 의도적으로 일부 row 의 region 을 다르게 줘서 두 소스가 독립적임을 보임.
|
||||
--
|
||||
-- 멱등성: INSERT IGNORE 로 중복 PK skip.
|
||||
--
|
||||
-- 호출 예시 (run.sh source 가 자동 실행):
|
||||
-- mysql -h $MY_HOST -P $MY_PORT -u $MY_USER -p"$MY_PASSWORD" \
|
||||
-- $MY_DB < database/source/mysql_setup.sql
|
||||
-- ============================================================
|
||||
|
||||
SELECT '=== Creating ecommerce_poc.customers (idempotent) ===' AS status;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
customer_id INT NOT NULL PRIMARY KEY,
|
||||
full_name VARCHAR(80) NOT NULL,
|
||||
email VARCHAR(120) NOT NULL,
|
||||
signup_date DATE NOT NULL,
|
||||
region VARCHAR(8) NOT NULL,
|
||||
CONSTRAINT chk_region CHECK (region IN ('APAC','EMEA','AMER'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
SELECT '=== Seeding sample rows (12 rows across 3 regions) ===' AS status;
|
||||
|
||||
INSERT IGNORE INTO customers (customer_id, full_name, email, signup_date, region) VALUES
|
||||
(101, 'Aki Tanaka', 'aki.tanaka@shop.example', '2024-01-20', 'APAC'),
|
||||
(102, 'Bohyun Lee', 'bohyun.lee@shop.example', '2024-02-08', 'APAC'),
|
||||
(103, 'Cheng Ho', 'cheng.ho@shop.example', '2024-02-25', 'APAC'),
|
||||
(104, 'Devi Patel', 'devi.patel@shop.example', '2024-03-12', 'APAC'),
|
||||
(105, 'Erik Johansson', 'erik.j@shop.example', '2024-03-22', 'EMEA'),
|
||||
(106, 'Fatima Al-Hassan','fatima.h@shop.example', '2024-04-05', 'EMEA'),
|
||||
(107, 'Greta Lindqvist', 'greta.l@shop.example', '2024-04-18', 'EMEA'),
|
||||
(108, 'Hans Becker', 'hans.becker@shop.example', '2024-05-01', 'EMEA'),
|
||||
(109, 'Ines Martinez', 'ines.martinez@shop.example', '2024-05-13', 'AMER'),
|
||||
(110, 'Jorge Silva', 'jorge.silva@shop.example', '2024-05-25', 'AMER'),
|
||||
(111, 'Kayla Anderson', 'kayla.a@shop.example', '2024-06-07', 'AMER'),
|
||||
(112, 'Lucas Costa', 'lucas.costa@shop.example', '2024-06-19', 'AMER');
|
||||
|
||||
SELECT '=== Verifying ===' AS status;
|
||||
SELECT region, COUNT(*) AS row_count
|
||||
FROM customers
|
||||
GROUP BY region
|
||||
ORDER BY region;
|
||||
|
||||
SELECT '=== mysql_setup done ===' AS status;
|
||||
300
database/source/postgres_hmm_carrier_performance.sql
Normal file
300
database/source/postgres_hmm_carrier_performance.sql
Normal file
@@ -0,0 +1,300 @@
|
||||
-- ============================================================
|
||||
-- HMM demo carrier performance source data for PostgreSQL.
|
||||
--
|
||||
-- Target: AWS RDS for PostgreSQL, database postgres
|
||||
-- Schema: hmm_demo
|
||||
-- Redmine: #729
|
||||
--
|
||||
-- This script is idempotent. It owns only C001..C008 and their
|
||||
-- 2025-02 through 2026-07 monthly demo rows. It never stores an
|
||||
-- RDS endpoint, password, or certificate path.
|
||||
--
|
||||
-- Example:
|
||||
-- PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-- "host=$PG_HOST port=5432 dbname=postgres user=$PG_USER \
|
||||
-- sslmode=verify-full sslrootcert=$PG_SSL_ROOT_CERT" \
|
||||
-- -X -f database/source/postgres_hmm_carrier_performance.sql
|
||||
-- ============================================================
|
||||
|
||||
\set ON_ERROR_STOP on
|
||||
\echo === HMM carrier performance demo: begin ===
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS hmm_demo AUTHORIZATION postgres;
|
||||
COMMENT ON SCHEMA hmm_demo IS
|
||||
'HMM federation demonstration data. All carrier names and KPI values are fictional.';
|
||||
|
||||
REVOKE CREATE ON SCHEMA hmm_demo FROM PUBLIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hmm_demo.carriers (
|
||||
carrier_code VARCHAR(10) PRIMARY KEY,
|
||||
carrier_name VARCHAR(120) NOT NULL UNIQUE,
|
||||
country_code CHAR(2) NOT NULL CHECK (country_code ~ '^[A-Z]{2}$'),
|
||||
alliance_name VARCHAR(80),
|
||||
service_region VARCHAR(40) NOT NULL,
|
||||
performance_tier VARCHAR(10) NOT NULL
|
||||
CHECK (performance_tier IN ('CORE', 'GROWTH', 'WATCH')),
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE hmm_demo.carriers IS
|
||||
'Fictional carrier master data for the HMM ADB and PostgreSQL federation demo.';
|
||||
COMMENT ON COLUMN hmm_demo.carriers.carrier_code IS 'Fictional carrier identifier.';
|
||||
COMMENT ON COLUMN hmm_demo.carriers.performance_tier IS
|
||||
'Demo portfolio tier: CORE, GROWTH, or WATCH.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hmm_demo.carrier_monthly_performance (
|
||||
carrier_code VARCHAR(10) NOT NULL
|
||||
REFERENCES hmm_demo.carriers (carrier_code),
|
||||
performance_month DATE NOT NULL
|
||||
CHECK (performance_month = date_trunc('month', performance_month)::DATE),
|
||||
contracted_teu INTEGER NOT NULL CHECK (contracted_teu >= 0),
|
||||
shipped_teu INTEGER NOT NULL CHECK (shipped_teu >= 0),
|
||||
revenue_usd NUMERIC(16,2) NOT NULL CHECK (revenue_usd >= 0),
|
||||
gross_margin_usd NUMERIC(16,2) NOT NULL,
|
||||
schedule_reliability_pct NUMERIC(5,2) NOT NULL
|
||||
CHECK (schedule_reliability_pct BETWEEN 0 AND 100),
|
||||
vessel_utilization_pct NUMERIC(5,2) NOT NULL
|
||||
CHECK (vessel_utilization_pct BETWEEN 0 AND 100),
|
||||
claim_rate_pct NUMERIC(5,2) NOT NULL
|
||||
CHECK (claim_rate_pct BETWEEN 0 AND 100),
|
||||
yoy_growth_pct NUMERIC(7,2) NOT NULL,
|
||||
risk_level VARCHAR(10) NOT NULL
|
||||
CHECK (risk_level IN ('GREEN', 'AMBER', 'RED')),
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
PRIMARY KEY (carrier_code, performance_month)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE hmm_demo.carrier_monthly_performance IS
|
||||
'Deterministic fictional monthly carrier KPIs for 2025-02 through 2026-07.';
|
||||
COMMENT ON COLUMN hmm_demo.carrier_monthly_performance.performance_month IS
|
||||
'First calendar day of the KPI month.';
|
||||
COMMENT ON COLUMN hmm_demo.carrier_monthly_performance.risk_level IS
|
||||
'Derived demo risk level based on margin, reliability, and claim rate.';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_carrier_perf_month
|
||||
ON hmm_demo.carrier_monthly_performance (performance_month, carrier_code);
|
||||
CREATE INDEX IF NOT EXISTS ix_carrier_perf_risk
|
||||
ON hmm_demo.carrier_monthly_performance (risk_level, performance_month);
|
||||
|
||||
INSERT INTO hmm_demo.carriers (
|
||||
carrier_code,
|
||||
carrier_name,
|
||||
country_code,
|
||||
alliance_name,
|
||||
service_region,
|
||||
performance_tier,
|
||||
active
|
||||
) VALUES
|
||||
('C001', 'Bluewave Maritime', 'SG', 'North Pacific Network', 'ASIA-PACIFIC', 'CORE', TRUE),
|
||||
('C002', 'Pacific Star Lines', 'JP', 'East Ocean Connect', 'ASIA-PACIFIC', 'CORE', TRUE),
|
||||
('C003', 'Korea Ocean Link', 'KR', 'East Ocean Connect', 'ASIA-PACIFIC', 'GROWTH', TRUE),
|
||||
('C004', 'Nordic Seaways', 'DK', 'Northern Trade Group', 'EUROPE', 'CORE', TRUE),
|
||||
('C005', 'Orient Bridge Shipping', 'TW', 'North Pacific Network', 'ASIA-PACIFIC', 'WATCH', TRUE),
|
||||
('C006', 'Atlantic Merchant Line', 'DE', 'Atlantic Cargo Forum', 'EUROPE-AMERICAS', 'WATCH', TRUE),
|
||||
('C007', 'Global Horizon Cargo', 'US', 'Atlantic Cargo Forum', 'AMERICAS', 'GROWTH', TRUE),
|
||||
('C008', 'Southern Cross Marine', 'FR', 'Southern Ocean Group', 'OCEANIA', 'WATCH', TRUE)
|
||||
ON CONFLICT (carrier_code) DO UPDATE SET
|
||||
carrier_name = EXCLUDED.carrier_name,
|
||||
country_code = EXCLUDED.country_code,
|
||||
alliance_name = EXCLUDED.alliance_name,
|
||||
service_region = EXCLUDED.service_region,
|
||||
performance_tier = EXCLUDED.performance_tier,
|
||||
active = EXCLUDED.active;
|
||||
|
||||
WITH month_series AS (
|
||||
SELECT
|
||||
month_index,
|
||||
(DATE '2025-02-01' + make_interval(months => month_index))::DATE AS performance_month,
|
||||
CASE month_index % 6
|
||||
WHEN 0 THEN -1
|
||||
WHEN 1 THEN 0
|
||||
WHEN 2 THEN 1
|
||||
WHEN 3 THEN 2
|
||||
WHEN 4 THEN 1
|
||||
ELSE -1
|
||||
END::NUMERIC AS seasonal_factor
|
||||
FROM generate_series(0, 17) AS series(month_index)
|
||||
),
|
||||
profiles (
|
||||
carrier_code,
|
||||
base_teu,
|
||||
monthly_teu_step,
|
||||
seasonal_teu,
|
||||
utilization_base,
|
||||
utilization_step,
|
||||
revenue_per_teu,
|
||||
revenue_step,
|
||||
margin_pct_base,
|
||||
margin_pct_step,
|
||||
reliability_base,
|
||||
reliability_step,
|
||||
claim_rate_base,
|
||||
claim_rate_step,
|
||||
yoy_growth_base,
|
||||
yoy_growth_step
|
||||
) AS (
|
||||
VALUES
|
||||
('C001', 1800::NUMERIC, 25::NUMERIC, 80::NUMERIC, 91.5::NUMERIC, 0.10::NUMERIC, 1850::NUMERIC, 4::NUMERIC, 15.0::NUMERIC, 0.10::NUMERIC, 95.0::NUMERIC, 0.05::NUMERIC, 0.35::NUMERIC, -0.005::NUMERIC, 8.0::NUMERIC, 0.25::NUMERIC),
|
||||
('C002', 1500::NUMERIC, 5::NUMERIC, 60::NUMERIC, 88.5::NUMERIC, 0.02::NUMERIC, 1780::NUMERIC, 3::NUMERIC, 12.0::NUMERIC, 0.00::NUMERIC, 92.5::NUMERIC, 0.00::NUMERIC, 0.45::NUMERIC, 0.005::NUMERIC, 3.0::NUMERIC, 0.05::NUMERIC),
|
||||
('C003', 1050::NUMERIC, 15::NUMERIC, 50::NUMERIC, 83.0::NUMERIC, 0.20::NUMERIC, 1920::NUMERIC, 2::NUMERIC, 8.0::NUMERIC, 0.15::NUMERIC, 88.5::NUMERIC, 0.25::NUMERIC, 1.20::NUMERIC, -0.040::NUMERIC, -4.0::NUMERIC, 0.60::NUMERIC),
|
||||
('C004', 1350::NUMERIC, 0::NUMERIC, 180::NUMERIC, 86.0::NUMERIC, 0.00::NUMERIC, 1810::NUMERIC, 3::NUMERIC, 10.0::NUMERIC, 0.00::NUMERIC, 93.0::NUMERIC, 0.00::NUMERIC, 0.55::NUMERIC, 0.010::NUMERIC, 1.0::NUMERIC, 0.10::NUMERIC),
|
||||
('C005', 1300::NUMERIC, -20::NUMERIC, 70::NUMERIC, 88.0::NUMERIC, -0.10::NUMERIC, 1850::NUMERIC, -2::NUMERIC, 8.0::NUMERIC, -0.20::NUMERIC, 91.0::NUMERIC, -0.15::NUMERIC, 0.80::NUMERIC, 0.030::NUMERIC, -2.0::NUMERIC, -0.50::NUMERIC),
|
||||
('C006', 1250::NUMERIC, -10::NUMERIC, 90::NUMERIC, 84.0::NUMERIC, -0.12::NUMERIC, 1800::NUMERIC, -3::NUMERIC, 6.0::NUMERIC, -0.15::NUMERIC, 87.0::NUMERIC, -0.25::NUMERIC, 1.50::NUMERIC, 0.050::NUMERIC, -5.0::NUMERIC, -0.70::NUMERIC),
|
||||
('C007', 800::NUMERIC, 30::NUMERIC, 100::NUMERIC, 82.0::NUMERIC, 0.25::NUMERIC, 2050::NUMERIC, 5::NUMERIC, 9.0::NUMERIC, 0.18::NUMERIC, 90.5::NUMERIC, 0.15::NUMERIC, 0.70::NUMERIC, -0.020::NUMERIC, 12.0::NUMERIC, 0.50::NUMERIC),
|
||||
('C008', 1150::NUMERIC, -25::NUMERIC, 60::NUMERIC, 80.0::NUMERIC, -0.15::NUMERIC, 1750::NUMERIC, -4::NUMERIC, 3.0::NUMERIC, -0.20::NUMERIC, 85.0::NUMERIC, -0.30::NUMERIC, 2.00::NUMERIC, 0.080::NUMERIC, -8.0::NUMERIC, -0.80::NUMERIC)
|
||||
),
|
||||
base_metrics AS (
|
||||
SELECT
|
||||
p.carrier_code,
|
||||
m.performance_month,
|
||||
ROUND(
|
||||
p.base_teu
|
||||
+ p.monthly_teu_step * m.month_index
|
||||
+ p.seasonal_teu * m.seasonal_factor
|
||||
)::INTEGER AS contracted_teu,
|
||||
LEAST(
|
||||
98.0::NUMERIC,
|
||||
GREATEST(
|
||||
60.0::NUMERIC,
|
||||
p.utilization_base
|
||||
+ p.utilization_step * m.month_index
|
||||
+ m.seasonal_factor * 0.50
|
||||
)
|
||||
) AS utilization_pct,
|
||||
p.revenue_per_teu + p.revenue_step * m.month_index AS unit_revenue,
|
||||
p.margin_pct_base + p.margin_pct_step * m.month_index AS margin_pct,
|
||||
LEAST(
|
||||
99.5::NUMERIC,
|
||||
GREATEST(
|
||||
60.0::NUMERIC,
|
||||
p.reliability_base
|
||||
+ p.reliability_step * m.month_index
|
||||
+ m.seasonal_factor * 0.20
|
||||
)
|
||||
) AS reliability_pct,
|
||||
LEAST(
|
||||
100.0::NUMERIC,
|
||||
GREATEST(
|
||||
0.0::NUMERIC,
|
||||
p.claim_rate_base + p.claim_rate_step * m.month_index
|
||||
)
|
||||
) AS claim_pct,
|
||||
p.yoy_growth_base + p.yoy_growth_step * m.month_index AS yoy_pct
|
||||
FROM profiles p
|
||||
CROSS JOIN month_series m
|
||||
),
|
||||
volume_metrics AS (
|
||||
SELECT
|
||||
b.*,
|
||||
ROUND(b.contracted_teu * b.utilization_pct / 100.0)::INTEGER AS shipped_teu
|
||||
FROM base_metrics b
|
||||
),
|
||||
financial_metrics AS (
|
||||
SELECT
|
||||
v.*,
|
||||
ROUND(v.shipped_teu * v.unit_revenue, 2) AS revenue_usd
|
||||
FROM volume_metrics v
|
||||
),
|
||||
final_metrics AS (
|
||||
SELECT
|
||||
f.*,
|
||||
ROUND(f.revenue_usd * f.margin_pct / 100.0, 2) AS gross_margin_usd
|
||||
FROM financial_metrics f
|
||||
)
|
||||
INSERT INTO hmm_demo.carrier_monthly_performance (
|
||||
carrier_code,
|
||||
performance_month,
|
||||
contracted_teu,
|
||||
shipped_teu,
|
||||
revenue_usd,
|
||||
gross_margin_usd,
|
||||
schedule_reliability_pct,
|
||||
vessel_utilization_pct,
|
||||
claim_rate_pct,
|
||||
yoy_growth_pct,
|
||||
risk_level,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
f.carrier_code,
|
||||
f.performance_month,
|
||||
f.contracted_teu,
|
||||
f.shipped_teu,
|
||||
f.revenue_usd,
|
||||
f.gross_margin_usd,
|
||||
ROUND(f.reliability_pct, 2),
|
||||
ROUND(f.utilization_pct, 2),
|
||||
ROUND(f.claim_pct, 2),
|
||||
ROUND(f.yoy_pct, 2),
|
||||
CASE
|
||||
WHEN f.gross_margin_usd < 0
|
||||
OR f.reliability_pct < 87
|
||||
OR f.claim_pct >= 2 THEN 'RED'
|
||||
WHEN f.margin_pct < 7
|
||||
OR f.reliability_pct < 92
|
||||
OR f.claim_pct >= 1 THEN 'AMBER'
|
||||
ELSE 'GREEN'
|
||||
END,
|
||||
TIMESTAMPTZ '2026-07-24 00:00:00+00'
|
||||
FROM final_metrics f
|
||||
ON CONFLICT (carrier_code, performance_month) DO UPDATE SET
|
||||
contracted_teu = EXCLUDED.contracted_teu,
|
||||
shipped_teu = EXCLUDED.shipped_teu,
|
||||
revenue_usd = EXCLUDED.revenue_usd,
|
||||
gross_margin_usd = EXCLUDED.gross_margin_usd,
|
||||
schedule_reliability_pct = EXCLUDED.schedule_reliability_pct,
|
||||
vessel_utilization_pct = EXCLUDED.vessel_utilization_pct,
|
||||
claim_rate_pct = EXCLUDED.claim_rate_pct,
|
||||
yoy_growth_pct = EXCLUDED.yoy_growth_pct,
|
||||
risk_level = EXCLUDED.risk_level,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
CREATE OR REPLACE VIEW hmm_demo.carrier_performance_latest_v AS
|
||||
SELECT
|
||||
c.carrier_code,
|
||||
c.carrier_name,
|
||||
c.country_code,
|
||||
c.alliance_name,
|
||||
c.service_region,
|
||||
c.performance_tier,
|
||||
p.performance_month,
|
||||
p.contracted_teu,
|
||||
p.shipped_teu,
|
||||
p.revenue_usd,
|
||||
p.gross_margin_usd,
|
||||
p.schedule_reliability_pct,
|
||||
p.vessel_utilization_pct,
|
||||
p.claim_rate_pct,
|
||||
p.yoy_growth_pct,
|
||||
p.risk_level
|
||||
FROM hmm_demo.carriers c
|
||||
JOIN hmm_demo.carrier_monthly_performance p
|
||||
ON p.carrier_code = c.carrier_code
|
||||
WHERE p.performance_month = (
|
||||
SELECT MAX(latest.performance_month)
|
||||
FROM hmm_demo.carrier_monthly_performance latest
|
||||
);
|
||||
|
||||
COMMENT ON VIEW hmm_demo.carrier_performance_latest_v IS
|
||||
'Latest fictional monthly KPI for each active demo carrier.';
|
||||
|
||||
DO $role$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hmm_federation_reader') THEN
|
||||
CREATE ROLE hmm_federation_reader NOLOGIN;
|
||||
END IF;
|
||||
END
|
||||
$role$;
|
||||
|
||||
GRANT USAGE ON SCHEMA hmm_demo TO hmm_federation_reader;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA hmm_demo TO hmm_federation_reader;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA hmm_demo
|
||||
GRANT SELECT ON TABLES TO hmm_federation_reader;
|
||||
|
||||
COMMIT;
|
||||
|
||||
\echo === HMM carrier performance demo: complete ===
|
||||
\echo Next: run database/source/postgres_hmm_carrier_performance_verify.sql
|
||||
324
database/source/postgres_hmm_carrier_performance_verify.sql
Normal file
324
database/source/postgres_hmm_carrier_performance_verify.sql
Normal file
@@ -0,0 +1,324 @@
|
||||
-- ============================================================
|
||||
-- Verification for HMM demo carrier performance data.
|
||||
-- Redmine: #729
|
||||
-- ============================================================
|
||||
|
||||
\set ON_ERROR_STOP on
|
||||
\pset pager off
|
||||
\echo === HMM carrier performance demo: verification begin ===
|
||||
|
||||
DO $verify$
|
||||
DECLARE
|
||||
carrier_count INTEGER;
|
||||
performance_count INTEGER;
|
||||
invalid_month_count INTEGER;
|
||||
latest_month DATE;
|
||||
earliest_month DATE;
|
||||
wrong_series_count INTEGER;
|
||||
latest_risk_count INTEGER;
|
||||
wrong_trend_count INTEGER;
|
||||
reader_can_login BOOLEAN;
|
||||
BEGIN
|
||||
SELECT COUNT(*)
|
||||
INTO carrier_count
|
||||
FROM hmm_demo.carriers
|
||||
WHERE carrier_code BETWEEN 'C001' AND 'C008';
|
||||
|
||||
SELECT COUNT(*), MIN(performance_month), MAX(performance_month)
|
||||
INTO performance_count, earliest_month, latest_month
|
||||
FROM hmm_demo.carrier_monthly_performance
|
||||
WHERE carrier_code BETWEEN 'C001' AND 'C008'
|
||||
AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01';
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO invalid_month_count
|
||||
FROM hmm_demo.carrier_monthly_performance
|
||||
WHERE performance_month <> date_trunc('month', performance_month)::DATE;
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO wrong_series_count
|
||||
FROM (
|
||||
SELECT carrier_code, COUNT(*) AS month_count
|
||||
FROM hmm_demo.carrier_monthly_performance
|
||||
WHERE carrier_code BETWEEN 'C001' AND 'C008'
|
||||
AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01'
|
||||
GROUP BY carrier_code
|
||||
HAVING COUNT(*) <> 18
|
||||
) wrong_series;
|
||||
|
||||
SELECT COUNT(DISTINCT risk_level)
|
||||
INTO latest_risk_count
|
||||
FROM hmm_demo.carrier_performance_latest_v;
|
||||
|
||||
WITH boundaries AS (
|
||||
SELECT
|
||||
carrier_code,
|
||||
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2025-02-01')
|
||||
AS first_revenue,
|
||||
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2026-07-01')
|
||||
AS latest_revenue
|
||||
FROM hmm_demo.carrier_monthly_performance
|
||||
WHERE carrier_code BETWEEN 'C001' AND 'C008'
|
||||
GROUP BY carrier_code
|
||||
)
|
||||
SELECT COUNT(*)
|
||||
INTO wrong_trend_count
|
||||
FROM boundaries
|
||||
WHERE (carrier_code IN ('C001', 'C002', 'C003', 'C004', 'C007')
|
||||
AND latest_revenue <= first_revenue)
|
||||
OR (carrier_code IN ('C005', 'C006', 'C008')
|
||||
AND latest_revenue >= first_revenue);
|
||||
|
||||
SELECT rolcanlogin
|
||||
INTO reader_can_login
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'hmm_federation_reader';
|
||||
|
||||
IF carrier_count <> 8 THEN
|
||||
RAISE EXCEPTION 'Expected 8 carriers, found %', carrier_count;
|
||||
END IF;
|
||||
IF performance_count <> 144 THEN
|
||||
RAISE EXCEPTION 'Expected 144 monthly performance rows, found %', performance_count;
|
||||
END IF;
|
||||
IF earliest_month <> DATE '2025-02-01' OR latest_month <> DATE '2026-07-01' THEN
|
||||
RAISE EXCEPTION 'Unexpected period: % through %', earliest_month, latest_month;
|
||||
END IF;
|
||||
IF invalid_month_count <> 0 OR wrong_series_count <> 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'Invalid monthly series: invalid dates %, wrong carrier series %',
|
||||
invalid_month_count,
|
||||
wrong_series_count;
|
||||
END IF;
|
||||
IF latest_risk_count <> 3 THEN
|
||||
RAISE EXCEPTION 'Expected GREEN, AMBER, RED latest risks, found % levels',
|
||||
latest_risk_count;
|
||||
END IF;
|
||||
IF wrong_trend_count <> 0 THEN
|
||||
RAISE EXCEPTION 'Unexpected first-to-latest trend for % carriers', wrong_trend_count;
|
||||
END IF;
|
||||
IF reader_can_login IS DISTINCT FROM FALSE THEN
|
||||
RAISE EXCEPTION 'hmm_federation_reader must remain a NOLOGIN role';
|
||||
END IF;
|
||||
|
||||
IF NOT has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'USAGE') THEN
|
||||
RAISE EXCEPTION 'hmm_federation_reader lacks schema USAGE';
|
||||
END IF;
|
||||
IF has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'CREATE') THEN
|
||||
RAISE EXCEPTION 'hmm_federation_reader unexpectedly has schema CREATE';
|
||||
END IF;
|
||||
IF NOT has_table_privilege(
|
||||
'hmm_federation_reader',
|
||||
'hmm_demo.carrier_monthly_performance',
|
||||
'SELECT'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'hmm_federation_reader lacks SELECT';
|
||||
END IF;
|
||||
IF has_table_privilege(
|
||||
'hmm_federation_reader',
|
||||
'hmm_demo.carrier_monthly_performance',
|
||||
'INSERT'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'hmm_federation_reader unexpectedly has INSERT';
|
||||
END IF;
|
||||
END
|
||||
$verify$;
|
||||
|
||||
DO $constraint_tests$
|
||||
BEGIN
|
||||
BEGIN
|
||||
INSERT INTO hmm_demo.carrier_monthly_performance (
|
||||
carrier_code,
|
||||
performance_month,
|
||||
contracted_teu,
|
||||
shipped_teu,
|
||||
revenue_usd,
|
||||
gross_margin_usd,
|
||||
schedule_reliability_pct,
|
||||
vessel_utilization_pct,
|
||||
claim_rate_pct,
|
||||
yoy_growth_pct,
|
||||
risk_level,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'C001',
|
||||
DATE '2026-07-02',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
90,
|
||||
90,
|
||||
1,
|
||||
1,
|
||||
'GREEN',
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
RAISE EXCEPTION 'Month-first-day constraint did not reject invalid input';
|
||||
EXCEPTION
|
||||
WHEN check_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
INSERT INTO hmm_demo.carrier_monthly_performance (
|
||||
carrier_code,
|
||||
performance_month,
|
||||
contracted_teu,
|
||||
shipped_teu,
|
||||
revenue_usd,
|
||||
gross_margin_usd,
|
||||
schedule_reliability_pct,
|
||||
vessel_utilization_pct,
|
||||
claim_rate_pct,
|
||||
yoy_growth_pct,
|
||||
risk_level,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'UNKNOWN',
|
||||
DATE '2026-08-01',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
90,
|
||||
90,
|
||||
1,
|
||||
1,
|
||||
'GREEN',
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
RAISE EXCEPTION 'Foreign key constraint did not reject unknown carrier';
|
||||
EXCEPTION
|
||||
WHEN foreign_key_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
INSERT INTO hmm_demo.carrier_monthly_performance (
|
||||
carrier_code,
|
||||
performance_month,
|
||||
contracted_teu,
|
||||
shipped_teu,
|
||||
revenue_usd,
|
||||
gross_margin_usd,
|
||||
schedule_reliability_pct,
|
||||
vessel_utilization_pct,
|
||||
claim_rate_pct,
|
||||
yoy_growth_pct,
|
||||
risk_level,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'C001',
|
||||
DATE '2026-08-01',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
101,
|
||||
90,
|
||||
1,
|
||||
1,
|
||||
'GREEN',
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
RAISE EXCEPTION 'Percentage constraint did not reject invalid input';
|
||||
EXCEPTION
|
||||
WHEN check_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
END
|
||||
$constraint_tests$;
|
||||
|
||||
\echo --- Row counts and period ---
|
||||
SELECT
|
||||
COUNT(DISTINCT carrier_code) AS carrier_count,
|
||||
COUNT(*) AS monthly_row_count,
|
||||
MIN(performance_month) AS first_month,
|
||||
MAX(performance_month) AS latest_month
|
||||
FROM hmm_demo.carrier_monthly_performance
|
||||
WHERE carrier_code BETWEEN 'C001' AND 'C008'
|
||||
AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01';
|
||||
|
||||
\echo --- Per-carrier series ---
|
||||
SELECT
|
||||
c.carrier_code,
|
||||
c.carrier_name,
|
||||
COUNT(p.*) AS month_count,
|
||||
MIN(p.performance_month) AS first_month,
|
||||
MAX(p.performance_month) AS latest_month
|
||||
FROM hmm_demo.carriers c
|
||||
JOIN hmm_demo.carrier_monthly_performance p
|
||||
ON p.carrier_code = c.carrier_code
|
||||
WHERE c.carrier_code BETWEEN 'C001' AND 'C008'
|
||||
GROUP BY c.carrier_code, c.carrier_name
|
||||
ORDER BY c.carrier_code;
|
||||
|
||||
\echo --- Latest KPI and risk distribution ---
|
||||
SELECT
|
||||
carrier_code,
|
||||
carrier_name,
|
||||
performance_month,
|
||||
shipped_teu,
|
||||
revenue_usd,
|
||||
gross_margin_usd,
|
||||
schedule_reliability_pct,
|
||||
claim_rate_pct,
|
||||
risk_level
|
||||
FROM hmm_demo.carrier_performance_latest_v
|
||||
ORDER BY carrier_code;
|
||||
|
||||
SELECT risk_level, COUNT(*) AS carrier_count
|
||||
FROM hmm_demo.carrier_performance_latest_v
|
||||
GROUP BY risk_level
|
||||
ORDER BY risk_level;
|
||||
|
||||
\echo --- First-to-latest revenue trend ---
|
||||
WITH boundaries AS (
|
||||
SELECT
|
||||
carrier_code,
|
||||
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2025-02-01') AS first_revenue,
|
||||
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2026-07-01') AS latest_revenue
|
||||
FROM hmm_demo.carrier_monthly_performance
|
||||
WHERE carrier_code BETWEEN 'C001' AND 'C008'
|
||||
GROUP BY carrier_code
|
||||
)
|
||||
SELECT
|
||||
carrier_code,
|
||||
first_revenue,
|
||||
latest_revenue,
|
||||
CASE
|
||||
WHEN latest_revenue > first_revenue THEN 'UP'
|
||||
WHEN latest_revenue < first_revenue THEN 'DOWN'
|
||||
ELSE 'FLAT'
|
||||
END AS trend
|
||||
FROM boundaries
|
||||
ORDER BY carrier_code;
|
||||
|
||||
\echo --- TLS session ---
|
||||
SELECT
|
||||
ssl,
|
||||
version AS tls_version,
|
||||
cipher,
|
||||
bits
|
||||
FROM pg_stat_ssl
|
||||
WHERE pid = pg_backend_pid();
|
||||
|
||||
\echo --- Read-only role privileges ---
|
||||
SELECT
|
||||
(SELECT NOT rolcanlogin
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'hmm_federation_reader') AS no_login,
|
||||
has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'USAGE') AS schema_usage,
|
||||
has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'CREATE') AS schema_create,
|
||||
has_table_privilege(
|
||||
'hmm_federation_reader',
|
||||
'hmm_demo.carrier_monthly_performance',
|
||||
'SELECT'
|
||||
) AS can_select,
|
||||
has_table_privilege(
|
||||
'hmm_federation_reader',
|
||||
'hmm_demo.carrier_monthly_performance',
|
||||
'INSERT'
|
||||
) AS can_insert;
|
||||
|
||||
\echo === HMM carrier performance demo: verification complete ===
|
||||
49
database/source/postgres_setup.sql
Normal file
49
database/source/postgres_setup.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- ============================================================
|
||||
-- database/source/postgres_setup.sql
|
||||
-- Run on the REMOTE PostgreSQL instance (psql).
|
||||
--
|
||||
-- Creates the customers table in the `public` schema with sample
|
||||
-- data spanning multiple regions (APAC / EMEA / AMER). The VPD
|
||||
-- policy on ADB will filter rows from this table per logged-in user.
|
||||
--
|
||||
-- 멱등성: 기존 데이터가 있으면 그대로 두고 INSERT 시 ON CONFLICT 로 skip.
|
||||
--
|
||||
-- 호출 예시 (run.sh source 가 자동 실행):
|
||||
-- PGPASSWORD=$PG_PASSWORD psql -h $PG_HOST -U $PG_USER -d $PG_DB \
|
||||
-- -f database/source/postgres_setup.sql
|
||||
-- ============================================================
|
||||
|
||||
\echo === Creating public.customers (idempotent) ===
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.customers (
|
||||
customer_id INTEGER PRIMARY KEY,
|
||||
full_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
signup_date DATE NOT NULL,
|
||||
region VARCHAR(8) NOT NULL CHECK (region IN ('APAC','EMEA','AMER'))
|
||||
);
|
||||
|
||||
\echo === Seeding sample rows (12 rows across 3 regions) ===
|
||||
|
||||
INSERT INTO public.customers (customer_id, full_name, email, signup_date, region) VALUES
|
||||
( 1, 'Alex Kim', 'alex.kim@example.com', DATE '2024-01-15', 'APAC'),
|
||||
( 2, 'Bora Park', 'bora.park@example.com', DATE '2024-02-03', 'APAC'),
|
||||
( 3, 'Chen Wei', 'chen.wei@example.com', DATE '2024-02-21', 'APAC'),
|
||||
( 4, 'Daisuke Sato', 'daisuke.sato@example.com', DATE '2024-03-09', 'APAC'),
|
||||
( 5, 'Emma Mueller', 'emma.mueller@example.com', DATE '2024-03-18', 'EMEA'),
|
||||
( 6, 'Francois Dubois', 'francois.d@example.com', DATE '2024-04-02', 'EMEA'),
|
||||
( 7, 'Giulia Rossi', 'giulia.rossi@example.com', DATE '2024-04-14', 'EMEA'),
|
||||
( 8, 'Henry Smith', 'henry.smith@example.com', DATE '2024-04-27', 'EMEA'),
|
||||
( 9, 'Isabella Garcia', 'isabella.g@example.com', DATE '2024-05-08', 'AMER'),
|
||||
(10, 'James Brown', 'james.brown@example.com', DATE '2024-05-19', 'AMER'),
|
||||
(11, 'Karen Lopez', 'karen.lopez@example.com', DATE '2024-06-01', 'AMER'),
|
||||
(12, 'Liam Wilson', 'liam.wilson@example.com', DATE '2024-06-13', 'AMER')
|
||||
ON CONFLICT (customer_id) DO NOTHING;
|
||||
|
||||
\echo === Verifying ===
|
||||
SELECT region, COUNT(*) AS row_count
|
||||
FROM public.customers
|
||||
GROUP BY region
|
||||
ORDER BY region;
|
||||
|
||||
\echo === postgres_setup done ===
|
||||
Reference in New Issue
Block a user