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