diff --git a/sql/adb/17_agent_ords_security_local_vpd_setup.sql b/sql/adb/17_agent_ords_security_local_vpd_setup.sql new file mode 100644 index 0000000..7abdfdf --- /dev/null +++ b/sql/adb/17_agent_ords_security_local_vpd_setup.sql @@ -0,0 +1,369 @@ +-- ============================================================ +-- 17_agent_ords_security_local_vpd_setup.sql +-- Local-only executable VPD example for Agent ORDS security. +-- +-- Scenario: +-- * ORDS connects as CB_ORDS. +-- * ORDS receives Authorization: Bearer . +-- * ORDS handler calls ADMIN.CB_AGENT_CTX_PKG.SET_USER_BY_BEARER. +-- * VPD reads SYS_CONTEXT and filters ADMIN.CB_V_SEARCH_DOCUMENTS. +-- * DBMS_REDACT masks the CONTENTS column unless the key user is allowed. +-- ============================================================ +WHENEVER SQLERROR EXIT SQL.SQLCODE +SET ECHO OFF +SET FEEDBACK ON +SET DEFINE OFF + +PROMPT === 1. Creating local base table and sample documents === +CREATE TABLE cb_search_documents ( + doc_id NUMBER PRIMARY KEY, + title VARCHAR2(100) NOT NULL, + owner_emp_no VARCHAR2(20) NOT NULL, + dept_code VARCHAR2(20) NOT NULL, + contents VARCHAR2(4000), + created_at DATE DEFAULT SYSDATE NOT NULL +); + +INSERT INTO cb_search_documents VALUES (1, 'HR payroll guide', 'E1001', 'HR', 'Payroll policy and HR guide', SYSDATE); +INSERT INTO cb_search_documents VALUES (2, 'HR recruiting plan', 'E1002', 'HR', 'Recruiting plan for HR team', SYSDATE); +INSERT INTO cb_search_documents VALUES (3, 'Finance close checklist','E2001', 'FIN', 'Monthly close checklist', SYSDATE); +INSERT INTO cb_search_documents VALUES (4, 'Finance audit memo', 'E2002', 'FIN', 'Audit memo for finance team', SYSDATE); +INSERT INTO cb_search_documents VALUES (5, 'Sales forecast', 'E3001', 'SALES', 'Quarterly sales forecast', SYSDATE); +INSERT INTO cb_search_documents VALUES (6, 'HR benefits notice', 'E1003', 'HR', 'Benefits notice for employees', SYSDATE); + +CREATE OR REPLACE VIEW cb_v_search_documents AS +SELECT doc_id, + title, + owner_emp_no, + dept_code, + contents, + created_at +FROM cb_search_documents; + +PROMPT === 2. Creating user/role/permission tables === +CREATE TABLE cb_app_user ( + user_id NUMBER PRIMARY KEY, + user_name VARCHAR2(50) NOT NULL, + employee_no VARCHAR2(20) NOT NULL, + dept_code VARCHAR2(20) NOT NULL, + can_read_contents CHAR(1) DEFAULT 'N' CHECK (can_read_contents IN ('Y','N')) NOT NULL, + active CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL +); + +CREATE TABLE cb_app_role ( + role_id NUMBER PRIMARY KEY, + role_name VARCHAR2(50) NOT NULL +); + +CREATE TABLE cb_user_role ( + user_id NUMBER NOT NULL REFERENCES cb_app_user(user_id), + role_id NUMBER NOT NULL REFERENCES cb_app_role(role_id), + CONSTRAINT cb_user_role_pk PRIMARY KEY (user_id, role_id) +); + +CREATE TABLE cb_permission ( + perm_id NUMBER PRIMARY KEY, + role_id NUMBER NOT NULL REFERENCES cb_app_role(role_id), + target_name VARCHAR2(128) NOT NULL, + action_name VARCHAR2(30) NOT NULL +); + +CREATE TABLE cb_permission_rule ( + rule_id NUMBER PRIMARY KEY, + perm_id NUMBER NOT NULL REFERENCES cb_permission(perm_id), + rule_type VARCHAR2(30) NOT NULL, + rule_value VARCHAR2(100) +); + +CREATE TABLE cb_permission_column ( + permission_id NUMBER NOT NULL REFERENCES cb_permission(perm_id), + column_name VARCHAR2(128) NOT NULL, + CONSTRAINT cb_permission_column_pk PRIMARY KEY (permission_id, column_name) +); + +CREATE TABLE cb_agent_bearer_key ( + key_id NUMBER PRIMARY KEY, + user_id NUMBER NOT NULL REFERENCES cb_app_user(user_id), + key_hash VARCHAR2(128) NOT NULL UNIQUE, + key_prefix VARCHAR2(16), + issued_at DATE DEFAULT SYSDATE NOT NULL, + expires_at DATE, + revoked_at DATE, + active CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL +); + +PROMPT === 3. Seeding permissions === +INSERT INTO cb_app_user VALUES (101, 'agent_hr', 'E10234', 'HR', 'N', 'Y'); +INSERT INTO cb_app_user VALUES (102, 'agent_fin_self', 'E2001', 'FIN', 'N', 'Y'); +INSERT INTO cb_app_user VALUES (103, 'agent_all', 'E99999', 'HQ', 'Y', 'Y'); + +INSERT INTO cb_app_role VALUES (10, 'HR_DEPT_ROLE'); +INSERT INTO cb_app_role VALUES (20, 'FIN_SELF_ROLE'); +INSERT INTO cb_app_role VALUES (30, 'ALL_DOC_ROLE'); + +INSERT INTO cb_user_role VALUES (101, 10); +INSERT INTO cb_user_role VALUES (102, 20); +INSERT INTO cb_user_role VALUES (103, 30); + +INSERT INTO cb_permission VALUES (100, 10, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); +INSERT INTO cb_permission VALUES (200, 20, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); +INSERT INTO cb_permission VALUES (300, 30, 'CB_V_SEARCH_DOCUMENTS', 'SELECT'); + +INSERT INTO cb_permission_rule VALUES (1000, 100, 'MY_DEPT', NULL); +INSERT INTO cb_permission_rule VALUES (2000, 200, 'SELF', NULL); +INSERT INTO cb_permission_rule VALUES (3000, 300, 'ALL', NULL); + +INSERT INTO cb_permission_column VALUES (300, 'CONTENTS'); + +INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix) +VALUES (1, 101, STANDARD_HASH('cb_hr_key', 'SHA256'), 'cb_hr'); +INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix) +VALUES (2, 102, STANDARD_HASH('cb_fin_key', 'SHA256'), 'cb_fin'); +INSERT INTO cb_agent_bearer_key(key_id, user_id, key_hash, key_prefix) +VALUES (3, 103, STANDARD_HASH('cb_all_key', 'SHA256'), 'cb_all'); + +COMMIT; + +PROMPT === 4. Creating secure application context package === +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 CONTEXT cb_agent_ctx USING cb_agent_ctx_pkg; + +CREATE OR REPLACE PACKAGE BODY cb_agent_ctx_pkg AS + 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); + 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); + 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; + 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 + INTO v_user_id, v_emp_no, v_dept_code, v_can_read_contents + 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'; + + set_user_values(v_user_id, v_emp_no, v_dept_code, v_can_read_contents); + EXCEPTION + WHEN NO_DATA_FOUND THEN + clear_user; + RAISE_APPLICATION_ERROR(-20002, 'Invalid or expired Bearer key'); + END; +END; +/ + +SHOW ERRORS + +PROMPT === 5. Creating VPD policy function === +CREATE OR REPLACE FUNCTION cb_agent_doc_vpd_filter( + p_schema IN VARCHAR2, + p_object IN VARCHAR2 +) RETURN VARCHAR2 +AUTHID DEFINER +AS + v_user_id VARCHAR2(30); + v_target_name VARCHAR2(128); +BEGIN + v_user_id := SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'); + + IF v_user_id IS NULL THEN + RETURN '1 = 0'; + END IF; + + v_target_name := REPLACE(UPPER(p_object), '''', ''''''); + + RETURN + 'EXISTS ( + SELECT 1 + FROM admin.cb_user_role ur + JOIN admin.cb_permission p + ON p.role_id = ur.role_id + JOIN admin.cb_permission_rule r + ON r.perm_id = p.perm_id + WHERE ur.user_id = TO_NUMBER(SYS_CONTEXT(''CB_AGENT_CTX'', ''USER_ID'')) + AND p.target_name = ''' || v_target_name || ''' + AND p.action_name = ''SELECT'' + AND ( + r.rule_type = ''ALL'' + OR ( + r.rule_type = ''MY_DEPT'' + AND dept_code = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'') + ) + OR ( + r.rule_type = ''DEPT'' + AND r.rule_value = dept_code + ) + OR ( + r.rule_type = ''SELF'' + AND owner_emp_no = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'') + ) + OR ( + r.rule_type = ''EMP_NO'' + AND r.rule_value = owner_emp_no + ) + ) + )'; +END; +/ + +SHOW ERRORS + +PROMPT === 6. Attaching VPD policy to the protected view === +BEGIN + DBMS_RLS.ADD_POLICY( + object_schema => USER, + object_name => 'CB_V_SEARCH_DOCUMENTS', + policy_name => 'CB_AGENT_DOC_POLICY', + function_schema => USER, + policy_function => 'CB_AGENT_DOC_VPD_FILTER', + statement_types => 'SELECT', + enable => TRUE + ); +END; +/ + +PROMPT === 7. Adding column redaction on CONTENTS === +CREATE OR REPLACE FUNCTION cb_agent_can_read_column( + p_target_name IN VARCHAR2, + p_column_name IN VARCHAR2 +) RETURN VARCHAR2 +AUTHID DEFINER +AS + v_allowed NUMBER; +BEGIN + IF SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID') IS NULL THEN + RETURN 'N'; + END IF; + + SELECT COUNT(*) + INTO v_allowed + 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 = TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID')) + AND p.target_name = UPPER(p_target_name) + AND p.action_name = 'SELECT' + AND pc.column_name = UPPER(p_column_name); + + RETURN CASE WHEN v_allowed > 0 THEN 'Y' ELSE 'N' END; +END; +/ + +SHOW ERRORS + +BEGIN + DBMS_REDACT.ADD_POLICY( + object_schema => USER, + object_name => 'CB_V_SEARCH_DOCUMENTS', + column_name => 'CONTENTS', + policy_name => 'CB_CONTENTS_REDACT', + function_type => DBMS_REDACT.NULLIFY, + expression => 'SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') != ''Y''' + ); +END; +/ + +PROMPT === 8. Creating ORDS runtime schema === +CREATE USER cb_ords IDENTIFIED BY "CbOrdS#2026Local1"; +GRANT CREATE SESSION TO cb_ords; +GRANT CREATE PROCEDURE TO cb_ords; +GRANT SELECT ON cb_v_search_documents TO cb_ords; +GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords; +GRANT EXECUTE ON cb_agent_can_read_column TO cb_ords; + +PROMPT === Local VPD setup complete === +EXIT; diff --git a/sql/adb/22_agent_ords_security_ords_handler_setup.sql b/sql/adb/22_agent_ords_security_ords_handler_setup.sql new file mode 100644 index 0000000..dc3a9fc --- /dev/null +++ b/sql/adb/22_agent_ords_security_ords_handler_setup.sql @@ -0,0 +1,256 @@ +-- ============================================================ +-- 22_agent_ords_security_ords_handler_setup.sql +-- ORDS module/handler package for Agent ORDS security. +-- +-- Run as CB_ORDS after: +-- * ADMIN VPD setup +-- * ADMIN DDS setup +-- * ADMIN ORDS schema enablement +-- +-- Purpose: +-- * VPD path: Authorization header -> Bearer key -> SYS_CONTEXT -> VPD. +-- * DDS probe: Authorization header -> local mapping only. This does not +-- attach a DDS EndUserSecurityContext, so DDS cannot treat the request +-- as the mapped END USER. +-- ============================================================ +WHENEVER SQLERROR EXIT SQL.SQLCODE +SET ECHO OFF +SET FEEDBACK ON + +PROMPT === Creating CB_ORDS handler package === +CREATE OR REPLACE PACKAGE cb_ords_handler_pkg AUTHID DEFINER AS + FUNCTION vpd_search_json(p_authorization IN VARCHAR2) RETURN CLOB; + FUNCTION dds_bearer_probe_json(p_authorization IN VARCHAR2) RETURN CLOB; +END; +/ + +CREATE OR REPLACE PACKAGE BODY cb_ords_handler_pkg AS + FUNCTION extract_bearer_key(p_authorization IN VARCHAR2) RETURN VARCHAR2 AS + v_auth VARCHAR2(4000); + BEGIN + v_auth := TRIM(p_authorization); + + IF v_auth IS NULL OR LOWER(SUBSTR(v_auth, 1, 7)) != 'bearer ' THEN + RAISE_APPLICATION_ERROR(-20101, 'Authorization header must be Bearer '); + END IF; + + RETURN TRIM(SUBSTR(v_auth, 8)); + END; + + FUNCTION mapped_dds_end_user(p_bearer_key IN VARCHAR2) RETURN VARCHAR2 AS + BEGIN + CASE p_bearer_key + WHEN 'cb_hr_key' THEN RETURN 'cb_dds_hr'; + WHEN 'cb_fin_key' THEN RETURN 'cb_dds_fin'; + WHEN 'cb_all_key' THEN RETURN 'cb_dds_all'; + ELSE RETURN NULL; + END CASE; + END; + + FUNCTION vpd_search_json(p_authorization IN VARCHAR2) RETURN CLOB AS + v_bearer_key VARCHAR2(4000); + v_rows_json CLOB; + v_response CLOB; + v_context_read_contents VARCHAR2(1); + BEGIN + admin.cb_agent_ctx_pkg.clear_user; + v_bearer_key := extract_bearer_key(p_authorization); + admin.cb_agent_ctx_pkg.set_user_by_bearer(v_bearer_key); + + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'doc_id' VALUE doc_id, + 'title' VALUE title, + 'owner_emp_no' VALUE owner_emp_no, + 'dept_code' VALUE dept_code, + 'contents' VALUE contents + RETURNING CLOB + ) + ORDER BY doc_id + RETURNING CLOB + ) + INTO v_rows_json + FROM admin.cb_v_search_documents; + + IF v_rows_json IS NULL THEN + v_rows_json := '[]'; + END IF; + + v_context_read_contents := admin.cb_agent_can_read_column('CB_V_SEARCH_DOCUMENTS', 'CONTENTS'); + + SELECT JSON_OBJECT( + 'scenario' VALUE 'VPD_BEARER_HEADER', + 'db_user' VALUE SYS_CONTEXT('USERENV', 'SESSION_USER'), + 'context_user_id' VALUE SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID'), + 'context_emp_no' VALUE SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO'), + 'context_dept_code' VALUE SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE'), + 'context_read_contents' VALUE v_context_read_contents, + 'rows' VALUE v_rows_json FORMAT JSON + RETURNING CLOB + ) + INTO v_response + FROM dual; + + admin.cb_agent_ctx_pkg.clear_user; + RETURN v_response; + EXCEPTION + WHEN OTHERS THEN + admin.cb_agent_ctx_pkg.clear_user; + RAISE; + END; + + FUNCTION dds_bearer_probe_json(p_authorization IN VARCHAR2) RETURN CLOB AS + v_bearer_key VARCHAR2(4000); + v_mapped_end_user VARCHAR2(128); + v_dds_context_username VARCHAR2(128); + v_dds_context_error VARCHAR2(4000); + v_rows_visible NUMBER; + v_query_error VARCHAR2(4000); + v_result VARCHAR2(40); + v_response CLOB; + BEGIN + v_bearer_key := extract_bearer_key(p_authorization); + v_mapped_end_user := mapped_dds_end_user(v_bearer_key); + + BEGIN + SELECT ORA_END_USER_CONTEXT.username + INTO v_dds_context_username + FROM dual; + EXCEPTION + WHEN OTHERS THEN + v_dds_context_error := SQLERRM; + END; + + BEGIN + EXECUTE IMMEDIATE + 'SELECT COUNT(*) FROM admin.cb_dds_v_search_documents' + INTO v_rows_visible; + + v_result := 'UNEXPECTED_ACCESS'; + EXCEPTION + WHEN OTHERS THEN + v_query_error := SQLERRM; + v_result := 'EXPECTED_BLOCKED'; + END; + + SELECT JSON_OBJECT( + 'scenario' VALUE 'DDS_BEARER_HANDLER_PROBE', + 'db_user' VALUE SYS_CONTEXT('USERENV', 'SESSION_USER'), + 'bearer_mapped_end_user' VALUE v_mapped_end_user, + 'dds_context_username' VALUE v_dds_context_username, + 'dds_context_error' VALUE v_dds_context_error, + 'rows_visible' VALUE v_rows_visible, + 'query_error' VALUE v_query_error, + 'result' VALUE v_result + RETURNING CLOB + ) + INTO v_response + FROM dual; + + RETURN v_response; + END; +END; +/ + +SHOW ERRORS + +PROMPT === Defining ORDS module and handlers === +BEGIN + ORDS.DELETE_MODULE(p_module_name => 'cb.agent.security'); +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +BEGIN + ORDS.DEFINE_MODULE( + p_module_name => 'cb.agent.security', + p_base_path => 'cb-agent-security/', + p_items_per_page => 25, + p_status => 'PUBLISHED' + ); + + ORDS.DEFINE_TEMPLATE( + p_module_name => 'cb.agent.security', + p_pattern => 'vpd/documents' + ); + + ORDS.DEFINE_HANDLER( + p_module_name => 'cb.agent.security', + p_pattern => 'vpd/documents', + p_method => 'POST', + p_source_type => ORDS.source_type_plsql, + p_source => q'[ +DECLARE + v_response CLOB; +BEGIN + v_response := cb_ords_handler_pkg.vpd_search_json(:auth_header); + :status_code := 200; + OWA_UTIL.MIME_HEADER('application/json', TRUE); + HTP.P(v_response); +EXCEPTION + WHEN OTHERS THEN + :status_code := 403; + OWA_UTIL.MIME_HEADER('application/json', TRUE); + HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '\"') || '"}'); +END; +]', + p_items_per_page => 0 + ); + + ORDS.DEFINE_PARAMETER( + p_module_name => 'cb.agent.security', + p_pattern => 'vpd/documents', + p_method => 'POST', + p_name => 'Authorization', + p_bind_variable_name => 'auth_header', + p_source_type => 'HEADER', + p_param_type => 'STRING', + p_access_method => 'IN' + ); + + ORDS.DEFINE_TEMPLATE( + p_module_name => 'cb.agent.security', + p_pattern => 'dds/bearer-probe' + ); + + ORDS.DEFINE_HANDLER( + p_module_name => 'cb.agent.security', + p_pattern => 'dds/bearer-probe', + p_method => 'POST', + p_source_type => ORDS.source_type_plsql, + p_source => q'[ +DECLARE + v_response CLOB; +BEGIN + v_response := cb_ords_handler_pkg.dds_bearer_probe_json(:auth_header); + :status_code := 200; + OWA_UTIL.MIME_HEADER('application/json', TRUE); + HTP.P(v_response); +EXCEPTION + WHEN OTHERS THEN + :status_code := 403; + OWA_UTIL.MIME_HEADER('application/json', TRUE); + HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '\"') || '"}'); +END; +]', + p_items_per_page => 0 + ); + + ORDS.DEFINE_PARAMETER( + p_module_name => 'cb.agent.security', + p_pattern => 'dds/bearer-probe', + p_method => 'POST', + p_name => 'Authorization', + p_bind_variable_name => 'auth_header', + p_source_type => 'HEADER', + p_param_type => 'STRING', + p_access_method => 'IN' + ); + + COMMIT; +END; +/ + +PROMPT === ORDS handler setup complete === +EXIT; diff --git a/sql/adb/25_agent_ords_security_backoffice_support.sql b/sql/adb/25_agent_ords_security_backoffice_support.sql index 5407054..c33e837 100644 --- a/sql/adb/25_agent_ords_security_backoffice_support.sql +++ b/sql/adb/25_agent_ords_security_backoffice_support.sql @@ -137,6 +137,68 @@ WHEN MATCHED THEN UPDATE SET dst.sensitive_yn = src.sensitive_yn WHEN NOT MATCHED THEN INSERT (column_id, object_id, column_name, sensitive_yn) VALUES (src.column_id, src.object_id, src.column_name, src.sensitive_yn); +MERGE INTO cb_permission_column dst +USING ( + SELECT 300 permission_id, 'CONTENTS' column_name FROM dual +) src +ON (dst.permission_id = src.permission_id AND dst.column_name = src.column_name) +WHEN NOT MATCHED THEN INSERT (permission_id, column_name) +VALUES (src.permission_id, src.column_name); + +PROMPT === Replacing CONTENTS redaction with role column permission check === +CREATE OR REPLACE FUNCTION cb_agent_can_read_column( + p_target_name IN VARCHAR2, + p_column_name IN VARCHAR2 +) RETURN VARCHAR2 +AUTHID DEFINER +AS + v_allowed NUMBER; +BEGIN + IF SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID') IS NULL THEN + RETURN 'N'; + END IF; + + SELECT COUNT(*) + INTO v_allowed + 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 = TO_NUMBER(SYS_CONTEXT('CB_AGENT_CTX', 'USER_ID')) + AND p.target_name = UPPER(p_target_name) + AND p.action_name = 'SELECT' + AND pc.column_name = UPPER(p_column_name); + + RETURN CASE WHEN v_allowed > 0 THEN 'Y' ELSE 'N' END; +END; +/ + +BEGIN + DBMS_REDACT.DROP_POLICY( + object_schema => USER, + object_name => 'CB_V_SEARCH_DOCUMENTS', + policy_name => 'CB_CONTENTS_REDACT' + ); +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +BEGIN + DBMS_REDACT.ADD_POLICY( + object_schema => USER, + object_name => 'CB_V_SEARCH_DOCUMENTS', + column_name => 'CONTENTS', + policy_name => 'CB_CONTENTS_REDACT', + function_type => DBMS_REDACT.NULLIFY, + expression => 'SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') IS NULL OR SYS_CONTEXT(''CB_AGENT_CTX'', ''CAN_READ_CONTENTS'') != ''Y''' + ); +END; +/ + +GRANT EXECUTE ON cb_agent_can_read_column TO cb_ords; + COMMIT; PROMPT === Backoffice support setup complete === diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/PermissionView.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/PermissionView.java index 293a6ef..52fabe4 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/PermissionView.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/permission/PermissionView.java @@ -7,6 +7,7 @@ public record PermissionView( long objectId, String objectName, String action, - String rules + String rules, + String visibleColumns ) { } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/user/UserCreateCommand.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/user/UserCreateCommand.java index c0a721c..5a99197 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/user/UserCreateCommand.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/user/UserCreateCommand.java @@ -5,7 +5,6 @@ import jakarta.validation.constraints.NotBlank; public record UserCreateCommand( @NotBlank String username, @NotBlank String empNo, - @NotBlank String deptCode, - boolean canReadContents + @NotBlank String deptCode ) { } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java index c3635f5..5130b61 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java @@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand; import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand; import com.cloudhandson.vpdbackoffice.service.PermissionService; import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService; +import java.util.Arrays; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -40,7 +41,7 @@ public class PermissionController { @RequestParam long objectId, @RequestParam String ruleType, @RequestParam(required = false) String ruleValue, - @RequestParam(required = false) List visibleColumns, + @RequestParam(required = false) String visibleColumns, RedirectAttributes redirectAttributes ) { permissionService.savePermissionSet(new PermissionSetCommand( @@ -48,12 +49,22 @@ public class PermissionController { objectId, "SELECT", List.of(new RuleCommand(ruleType, ruleValue)), - visibleColumns == null ? List.of() : visibleColumns + splitColumns(visibleColumns) )); redirectAttributes.addFlashAttribute("message", "권한을 저장했습니다."); return "redirect:/permissions"; } + private List splitColumns(String visibleColumns) { + if (visibleColumns == null || visibleColumns.isBlank()) { + return List.of(); + } + return Arrays.stream(visibleColumns.split(",")) + .map(String::trim) + .filter(value -> !value.isBlank()) + .toList(); + } + @PostMapping("/permissions/delete") public String delete(@RequestParam long permissionId, RedirectAttributes redirectAttributes) { permissionService.deletePermission(permissionId); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/UserController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/UserController.java index 45cc52b..00984dd 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/UserController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/UserController.java @@ -34,10 +34,9 @@ public class UserController { @RequestParam String username, @RequestParam String empNo, @RequestParam String deptCode, - @RequestParam(defaultValue = "false") boolean canReadContents, RedirectAttributes redirectAttributes ) { - userService.createUser(new UserCreateCommand(username, empNo, deptCode, canReadContents)); + userService.createUser(new UserCreateCommand(username, empNo, deptCode)); redirectAttributes.addFlashAttribute("message", "사용자를 추가했습니다."); return "redirect:/users"; } diff --git a/src/main/resources/mapper/PermissionMapper.xml b/src/main/resources/mapper/PermissionMapper.xml index b5e503c..794c38d 100644 --- a/src/main/resources/mapper/PermissionMapper.xml +++ b/src/main/resources/mapper/PermissionMapper.xml @@ -22,7 +22,12 @@ p.target_name AS object_name, p.action_name AS action, LISTAGG(pr.rule_type || NVL2(pr.rule_value, ':' || pr.rule_value, ''), ', ') - WITHIN GROUP (ORDER BY pr.rule_id) AS rules + WITHIN GROUP (ORDER BY pr.rule_id) AS rules, + ( + SELECT LISTAGG(pc.column_name, ', ') WITHIN GROUP (ORDER BY pc.column_name) + FROM cb_permission_column pc + WHERE pc.permission_id = p.perm_id + ) AS visible_columns FROM cb_permission p JOIN cb_app_role r ON r.role_id = p.role_id LEFT JOIN cb_protected_object o ON o.object_name = p.target_name diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml index 0b6c819..e6b7937 100644 --- a/src/main/resources/mapper/UserMapper.xml +++ b/src/main/resources/mapper/UserMapper.xml @@ -47,10 +47,7 @@ #{command.username}, #{command.empNo}, #{command.deptCode}, - - 'Y' - 'N' - , + 'N', 'Y' ) diff --git a/src/main/resources/templates/permissions.html b/src/main/resources/templates/permissions.html index 04bcab7..9bf966d 100644 --- a/src/main/resources/templates/permissions.html +++ b/src/main/resources/templates/permissions.html @@ -40,6 +40,10 @@ 규칙 값 + @@ -55,6 +59,7 @@ 테이블/뷰 Action 행 규칙 + 표시 컬럼 @@ -65,6 +70,7 @@ CB_V_SEARCH_DOCUMENTS SELECT ALL + CONTENTS
@@ -74,7 +80,7 @@ - 등록된 권한이 없습니다. + 등록된 권한이 없습니다. diff --git a/src/main/resources/templates/users.html b/src/main/resources/templates/users.html index 8c4b513..0d7a35f 100644 --- a/src/main/resources/templates/users.html +++ b/src/main/resources/templates/users.html @@ -27,10 +27,6 @@ 부서코드 -
@@ -45,7 +41,6 @@ 사용자명 사번 부서 - 민감 컬럼 상태 @@ -56,7 +51,6 @@ agent_hr E10234 HR - N Y
@@ -68,7 +62,7 @@ - 등록된 사용자가 없습니다. + 등록된 사용자가 없습니다.