245 lines
9.3 KiB
MySQL
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;
|