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