Files
vpd-permission-poc/database/adb/44_dds_mcp_local_end_user_setup.sql

245 lines
9.3 KiB
MySQL

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