[Developer] #424 move sensitive column access to permissions

Refs #424
This commit is contained in:
devmrko
2026-06-23 13:37:43 +09:00
parent 55414aa9e4
commit b6fcc768a3
11 changed files with 719 additions and 20 deletions

View File

@@ -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 <key>.
-- * 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;

View File

@@ -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 <key>');
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;

View File

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

View File

@@ -7,6 +7,7 @@ public record PermissionView(
long objectId,
String objectName,
String action,
String rules
String rules,
String visibleColumns
) {
}

View File

@@ -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
) {
}

View File

@@ -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<String> 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<String> 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);

View File

@@ -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";
}

View File

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

View File

@@ -47,10 +47,7 @@
#{command.username},
#{command.empNo},
#{command.deptCode},
<choose>
<when test="command.canReadContents"> 'Y' </when>
<otherwise> 'N' </otherwise>
</choose>,
'N',
'Y'
)
</insert>

View File

@@ -40,6 +40,10 @@
규칙 값
<input class="form-control" name="ruleValue" placeholder="APAC 또는 HR">
</label>
<label>
표시 컬럼
<input class="form-control" name="visibleColumns" placeholder="CONTENTS">
</label>
<button class="btn btn-primary" type="submit">저장</button>
</form>
</section>
@@ -55,6 +59,7 @@
<th>테이블/뷰</th>
<th>Action</th>
<th>행 규칙</th>
<th>표시 컬럼</th>
<th></th>
</tr>
</thead>
@@ -65,6 +70,7 @@
<td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
<td th:text="${permission.action()}">SELECT</td>
<td th:text="${permission.rules()} ?: '-'">ALL</td>
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
<td>
<form method="post" action="/permissions/delete" class="inline-form">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
@@ -74,7 +80,7 @@
</td>
</tr>
<tr th:if="${#lists.isEmpty(permissions)}">
<td colspan="6" class="text-muted">등록된 권한이 없습니다.</td>
<td colspan="7" class="text-muted">등록된 권한이 없습니다.</td>
</tr>
</tbody>
</table>

View File

@@ -27,10 +27,6 @@
부서코드
<input class="form-control" name="deptCode" required>
</label>
<label class="form-check form-switch switch-field">
<input class="form-check-input" name="canReadContents" type="checkbox" value="true">
<span class="form-check-label">민감 컬럼 표시</span>
</label>
<button class="btn btn-primary" type="submit">추가</button>
</form>
</section>
@@ -45,7 +41,6 @@
<th>사용자명</th>
<th>사번</th>
<th>부서</th>
<th>민감 컬럼</th>
<th>상태</th>
<th></th>
</tr>
@@ -56,7 +51,6 @@
<td th:text="${user.username()}">agent_hr</td>
<td th:text="${user.empNo()}">E10234</td>
<td th:text="${user.deptCode()}">HR</td>
<td><span class="badge" th:classappend="${user.canReadContents() == 'Y'} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${user.canReadContents()}">N</span></td>
<td><span class="badge" th:classappend="${user.active()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${user.activeYn()}">Y</span></td>
<td>
<form method="post" action="/users/active" class="inline-form">
@@ -68,7 +62,7 @@
</td>
</tr>
<tr th:if="${#lists.isEmpty(users)}">
<td colspan="7" class="text-muted">등록된 사용자가 없습니다.</td>
<td colspan="6" class="text-muted">등록된 사용자가 없습니다.</td>
</tr>
</tbody>
</table>