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