-- ============================================================ -- 72_hmm_leave_team_vpd.sql -- -- HMM HR leave row security: -- * HMM_HR_MANAGER -> self and directly managed employees -- * HMM_HR_VIEWER -> self only -- * HMM_HR_ADMIN -> all rows when the role is explicitly assigned -- -- Run as ADMIN after the HMM access-management schema is installed. -- This script is idempotent and never stores a plain bearer token. -- ============================================================ WHENEVER SQLERROR EXIT SQL.SQLCODE SET ECHO OFF SET FEEDBACK ON SET DEFINE OFF ALTER SESSION DISABLE PARALLEL DML; PROMPT === 1. Ensuring HMM roles and demo role assignments === MERGE INTO hmm_access_roles dst USING ( SELECT 1 role_id, 'HMM_HR_VIEWER' role_name, 'HMM employee self-service leave access' description, 'PUBLIC' max_sensitivity_level FROM dual UNION ALL SELECT 2, 'HMM_HR_MANAGER', 'HMM manager access to self and direct reports', 'INTERNAL' FROM dual UNION ALL SELECT 3, 'HMM_HR_ADMIN', 'HMM HR administrator full leave access', 'CONFIDENTIAL' FROM dual ) src ON (dst.role_name = src.role_name) WHEN MATCHED THEN UPDATE SET dst.description = src.description, dst.max_sensitivity_level = src.max_sensitivity_level, dst.active_yn = 'Y', dst.updated_at = SYSTIMESTAMP WHEN NOT MATCHED THEN INSERT ( role_id, role_name, description, max_sensitivity_level, active_yn, created_at, updated_at ) VALUES ( src.role_id, src.role_name, src.description, src.max_sensitivity_level, 'Y', SYSTIMESTAMP, SYSTIMESTAMP ); MERGE INTO hmm_employee_access_roles dst USING ( SELECT e.employee_id, r.role_id FROM hmm_hr_employees e JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_MANAGER' WHERE e.employee_code = 'E1001' AND e.employment_status = 'ACTIVE' ) src ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id) WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at) VALUES (src.employee_id, src.role_id, SYSTIMESTAMP); MERGE INTO hmm_employee_access_roles dst USING ( SELECT e.employee_id, r.role_id FROM hmm_hr_employees e JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_VIEWER' WHERE e.manager_employee_id = ( SELECT manager.employee_id FROM hmm_hr_employees manager WHERE manager.employee_code = 'E1001' ) AND e.employment_status = 'ACTIVE' ) src ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id) WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at) VALUES (src.employee_id, src.role_id, SYSTIMESTAMP); COMMIT; PROMPT === 2. Ensuring protected leave objects and columns === DECLARE PROCEDURE ensure_object( p_object_name IN VARCHAR2, p_description IN VARCHAR2 ) AS v_object_id NUMBER; BEGIN BEGIN SELECT object_id INTO v_object_id FROM hmm_access_objects WHERE owner = 'ADMIN' AND object_name = p_object_name; UPDATE hmm_access_objects SET enabled_yn = 'Y', description = p_description, ords_path = 'cb-ords/cb-object-query/admin/' || LOWER(p_object_name) WHERE object_id = v_object_id; EXCEPTION WHEN NO_DATA_FOUND THEN SELECT NVL(MAX(object_id), 0) + 1 INTO v_object_id FROM hmm_access_objects; INSERT INTO hmm_access_objects ( object_id, owner, object_name, ords_path, enabled_yn, description ) VALUES ( v_object_id, 'ADMIN', p_object_name, 'cb-ords/cb-object-query/admin/' || LOWER(p_object_name), 'Y', p_description ); END; FOR c IN ( SELECT column_name FROM user_tab_columns WHERE table_name = p_object_name ORDER BY column_id ) LOOP MERGE INTO hmm_access_object_columns dst USING ( SELECT v_object_id object_id, c.column_name column_name FROM dual ) src ON ( dst.object_id = src.object_id AND dst.column_name = src.column_name ) WHEN NOT MATCHED THEN INSERT ( column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method ) VALUES ( (SELECT NVL(MAX(column_id), 0) + 1 FROM hmm_access_object_columns), src.object_id, src.column_name, 'N', 'PUBLIC', 'NONE' ); END LOOP; END; BEGIN ensure_object( 'HMM_LEAVE_BALANCES', 'HMM employee leave balance protected by employee hierarchy' ); ensure_object( 'HMM_LEAVE_REQUESTS', 'HMM employee leave requests protected by employee hierarchy' ); END; / COMMIT; PROMPT === 3. Ensuring role permission rules === DECLARE PROCEDURE ensure_permission( p_role_name IN VARCHAR2, p_target_name IN VARCHAR2, p_rule_type IN VARCHAR2 ) AS v_role_id NUMBER; v_perm_id NUMBER; BEGIN SELECT role_id INTO v_role_id FROM hmm_access_roles WHERE role_name = p_role_name AND active_yn = 'Y'; BEGIN SELECT perm_id INTO v_perm_id FROM hmm_access_permissions WHERE role_id = v_role_id AND target_name = p_target_name AND action_name = 'SELECT'; UPDATE hmm_access_permissions SET permission_effect = 'ALLOW' WHERE perm_id = v_perm_id; EXCEPTION WHEN NO_DATA_FOUND THEN SELECT cb_permission_seq.NEXTVAL INTO v_perm_id FROM dual; INSERT INTO hmm_access_permissions ( perm_id, role_id, target_name, action_name, permission_effect ) VALUES ( v_perm_id, v_role_id, p_target_name, 'SELECT', 'ALLOW' ); END; DELETE FROM hmm_access_permission_rules WHERE perm_id = v_perm_id; INSERT INTO hmm_access_permission_rules ( rule_id, perm_id, rule_column, rule_type, rule_value ) VALUES ( cb_permission_rule_seq.NEXTVAL, v_perm_id, 'EMPLOYEE_ID', p_rule_type, NULL ); END; BEGIN ensure_permission('HMM_HR_VIEWER', 'HMM_LEAVE_BALANCES', 'SELF'); ensure_permission('HMM_HR_VIEWER', 'HMM_LEAVE_REQUESTS', 'SELF'); ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_BALANCES', 'MANAGED_TEAM'); ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_REQUESTS', 'MANAGED_TEAM'); ensure_permission('HMM_HR_ADMIN', 'HMM_LEAVE_BALANCES', 'ALL'); ensure_permission('HMM_HR_ADMIN', 'HMM_LEAVE_REQUESTS', 'ALL'); END; / COMMIT; PROMPT === 4. Creating secure HMM access context === CREATE OR REPLACE PACKAGE hmm_access_ctx_pkg AUTHID DEFINER AS PROCEDURE clear_user; PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2); END; / CREATE OR REPLACE PACKAGE BODY hmm_access_ctx_pkg AS PROCEDURE clear_user AS BEGIN DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID', NULL); DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_CODE', NULL); DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'TEAM_ID', NULL); END; PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2) AS v_employee_id hmm_hr_employees.employee_id%TYPE; v_employee_code hmm_hr_employees.employee_code%TYPE; v_team_id hmm_hr_employees.team_id%TYPE; BEGIN IF p_bearer_token IS NULL OR TRIM(p_bearer_token) IS NULL THEN clear_user; RAISE_APPLICATION_ERROR(-20101, 'Bearer token is required'); END IF; SELECT e.employee_id, e.employee_code, e.team_id INTO v_employee_id, v_employee_code, v_team_id FROM hmm_access_bearer_tokens t JOIN hmm_hr_employees e ON e.employee_id = t.employee_id WHERE t.key_hash = STANDARD_HASH(p_bearer_token, 'SHA256') AND t.revoked_at IS NULL AND t.expires_at > CAST(SYSTIMESTAMP AS TIMESTAMP) AND e.employment_status = 'ACTIVE'; DBMS_SESSION.SET_CONTEXT( 'HMM_ACCESS_CTX', 'EMPLOYEE_ID', TO_CHAR(v_employee_id) ); DBMS_SESSION.SET_CONTEXT( 'HMM_ACCESS_CTX', 'EMPLOYEE_CODE', v_employee_code ); DBMS_SESSION.SET_CONTEXT( 'HMM_ACCESS_CTX', 'TEAM_ID', TO_CHAR(v_team_id) ); EXCEPTION WHEN NO_DATA_FOUND THEN clear_user; RAISE_APPLICATION_ERROR(-20102, 'Invalid or expired bearer token'); WHEN TOO_MANY_ROWS THEN clear_user; RAISE_APPLICATION_ERROR(-20103, 'Bearer token mapping is not unique'); END; END; / CREATE OR REPLACE CONTEXT hmm_access_ctx USING hmm_access_ctx_pkg; PROMPT === 5. Creating the HMM leave VPD filter === CREATE OR REPLACE FUNCTION hmm_leave_vpd_filter( p_schema IN VARCHAR2, p_object IN VARCHAR2 ) RETURN VARCHAR2 AUTHID DEFINER AS v_employee_id NUMBER; v_target VARCHAR2(128); v_allow_predicate VARCHAR2(32767); v_deny_predicate VARCHAR2(32767); v_permission_clause VARCHAR2(32767); FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS BEGIN RETURN '''' || REPLACE(NVL(p_value, ''), '''', '''''') || ''''; END; FUNCTION safe_column(p_column_name IN VARCHAR2) RETURN VARCHAR2 IS v_column VARCHAR2(128); v_count NUMBER; BEGIN v_column := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(TRIM(p_column_name))); SELECT COUNT(*) INTO v_count FROM all_tab_columns WHERE owner = UPPER(p_schema) AND table_name = UPPER(p_object) AND column_name = v_column; RETURN CASE WHEN v_count = 1 THEN v_column ELSE NULL END; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; PROCEDURE append_and(p_clause IN VARCHAR2) IS BEGIN IF p_clause IS NULL THEN v_permission_clause := NULL; ELSIF v_permission_clause IS NULL THEN v_permission_clause := '(' || p_clause || ')'; ELSE v_permission_clause := v_permission_clause || ' AND (' || p_clause || ')'; END IF; END; PROCEDURE append_permission( p_effect IN VARCHAR2, p_clause IN VARCHAR2 ) IS BEGIN IF p_clause IS NULL THEN RETURN; END IF; IF p_effect = 'DENY' THEN IF v_deny_predicate IS NULL THEN v_deny_predicate := '(' || p_clause || ')'; ELSE v_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')'; END IF; ELSIF v_allow_predicate IS NULL THEN v_allow_predicate := '(' || p_clause || ')'; ELSE v_allow_predicate := v_allow_predicate || ' OR (' || p_clause || ')'; END IF; END; BEGIN BEGIN v_employee_id := TO_NUMBER( SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID') ); EXCEPTION WHEN OTHERS THEN RETURN '1 = 0'; END; IF v_employee_id IS NULL OR p_schema IS NULL OR p_object IS NULL THEN RETURN '1 = 0'; END IF; v_target := UPPER(TRIM(p_object)); IF v_target NOT IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS') THEN RETURN '1 = 0'; END IF; FOR permission_row IN ( SELECT DISTINCT p.perm_id, NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') permission_effect FROM ( SELECT er.role_id FROM hmm_employee_access_roles er WHERE er.employee_id = v_employee_id UNION SELECT gr.role_id FROM hmm_access_group_members gm JOIN hmm_access_groups g ON g.group_id = gm.group_id AND g.active_yn = 'Y' JOIN hmm_group_access_roles gr ON gr.group_id = gm.group_id WHERE gm.employee_id = v_employee_id ) effective_role JOIN hmm_access_roles role ON role.role_id = effective_role.role_id AND role.active_yn = 'Y' JOIN hmm_access_permissions p ON p.role_id = role.role_id AND p.target_name = v_target AND p.action_name = 'SELECT' ORDER BY p.perm_id ) LOOP v_permission_clause := NULL; FOR rule_row IN ( SELECT UPPER(TRIM(rule_type)) rule_type, UPPER(TRIM(rule_column)) rule_column, rule_value FROM hmm_access_permission_rules WHERE perm_id = permission_row.perm_id ORDER BY rule_id ) LOOP DECLARE v_column VARCHAR2(128); v_clause VARCHAR2(32767); BEGIN v_column := safe_column(NVL(rule_row.rule_column, 'EMPLOYEE_ID')); v_clause := NULL; IF rule_row.rule_type = 'ALL' THEN v_clause := '1 = 1'; ELSIF rule_row.rule_type = 'SELF' AND v_column IS NOT NULL THEN v_clause := v_column || ' = TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))'; ELSIF rule_row.rule_type = 'MANAGED_TEAM' AND v_column IS NOT NULL THEN v_clause := v_column || ' IN (' || 'SELECT employee.employee_id ' || 'FROM ADMIN.HMM_HR_EMPLOYEES employee ' || 'WHERE employee.employee_id = ' || 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID'')) ' || 'OR employee.manager_employee_id = ' || 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))' || ')'; ELSIF rule_row.rule_type = '=' AND v_column IS NOT NULL AND rule_row.rule_value IS NOT NULL THEN v_clause := 'TO_CHAR(' || v_column || ') = ' || quote_literal(rule_row.rule_value); ELSIF rule_row.rule_type IN ('!=', '<>') AND v_column IS NOT NULL AND rule_row.rule_value IS NOT NULL THEN v_clause := 'TO_CHAR(' || v_column || ') <> ' || quote_literal(rule_row.rule_value); END IF; IF v_clause IS NULL THEN v_permission_clause := NULL; EXIT; END IF; append_and(v_clause); END; END LOOP; append_permission( permission_row.permission_effect, v_permission_clause ); END LOOP; IF v_allow_predicate IS NULL THEN RETURN '1 = 0'; END IF; IF v_deny_predicate IS NULL THEN RETURN v_allow_predicate; END IF; RETURN '((' || v_allow_predicate || ') AND NOT (' || v_deny_predicate || '))'; EXCEPTION WHEN OTHERS THEN RETURN '1 = 0'; END; / SHOW ERRORS PACKAGE hmm_access_ctx_pkg SHOW ERRORS PACKAGE BODY hmm_access_ctx_pkg SHOW ERRORS FUNCTION hmm_leave_vpd_filter PROMPT === 6. Attaching context-sensitive VPD policies === DECLARE PROCEDURE replace_policy(p_object_name IN VARCHAR2) AS BEGIN BEGIN DBMS_RLS.DROP_POLICY( object_schema => 'ADMIN', object_name => p_object_name, policy_name => 'HMM_LEAVE_SCOPE_POLICY' ); EXCEPTION WHEN OTHERS THEN IF SQLCODE != -28102 THEN RAISE; END IF; END; DBMS_RLS.ADD_POLICY( object_schema => 'ADMIN', object_name => p_object_name, policy_name => 'HMM_LEAVE_SCOPE_POLICY', function_schema => 'ADMIN', policy_function => 'HMM_LEAVE_VPD_FILTER', statement_types => 'SELECT', update_check => FALSE, enable => TRUE, policy_type => DBMS_RLS.CONTEXT_SENSITIVE ); END; BEGIN replace_policy('HMM_LEAVE_BALANCES'); replace_policy('HMM_LEAVE_REQUESTS'); END; / PROMPT === 7. Recording backoffice descriptions === MERGE INTO hmm_access_vpd_filter_notes dst USING ( SELECT 'ADMIN' function_owner, 'HMM_LEAVE_VPD_FILTER' function_name, 'Builds self, managed-team, and administrator leave predicates from HMM roles and access rules.' description FROM dual ) src ON ( dst.function_owner = src.function_owner AND dst.function_name = src.function_name ) WHEN MATCHED THEN UPDATE SET dst.description = src.description, dst.updated_at = SYSTIMESTAMP WHEN NOT MATCHED THEN INSERT ( function_owner, function_name, description, updated_at ) VALUES ( src.function_owner, src.function_name, src.description, SYSTIMESTAMP ); MERGE INTO hmm_access_vpd_policy_notes dst USING ( SELECT 'ADMIN' object_owner, object_name, 'HMM_LEAVE_SCOPE_POLICY' policy_name, 'Restricts leave rows to self, direct reports, or explicit HR administrator access.' description FROM ( SELECT 'HMM_LEAVE_BALANCES' object_name FROM dual UNION ALL SELECT 'HMM_LEAVE_REQUESTS' FROM dual ) ) src ON ( dst.object_owner = src.object_owner AND dst.object_name = src.object_name AND dst.policy_name = src.policy_name ) WHEN MATCHED THEN UPDATE SET dst.description = src.description, dst.updated_at = SYSTIMESTAMP WHEN NOT MATCHED THEN INSERT ( object_owner, object_name, policy_name, description, updated_at ) VALUES ( src.object_owner, src.object_name, src.policy_name, src.description, SYSTIMESTAMP ); COMMIT; PROMPT === 8. Deployment inventory === SELECT object_name, object_type, status FROM user_objects WHERE object_name IN ( 'HMM_ACCESS_CTX', 'HMM_ACCESS_CTX_PKG', 'HMM_LEAVE_VPD_FILTER' ) ORDER BY object_name, object_type; SELECT object_name, policy_name, function, sel, enable, policy_type FROM all_policies WHERE object_owner = 'ADMIN' AND object_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS') AND policy_name = 'HMM_LEAVE_SCOPE_POLICY' ORDER BY object_name; SELECT e.employee_code, r.role_name FROM hmm_employee_access_roles er JOIN hmm_hr_employees e ON e.employee_id = er.employee_id JOIN hmm_access_roles r ON r.role_id = er.role_id WHERE r.role_name IN ('HMM_HR_VIEWER', 'HMM_HR_MANAGER') ORDER BY e.employee_code, r.role_name; SELECT r.role_name, p.target_name, pr.rule_column, pr.rule_type FROM hmm_access_permissions p JOIN hmm_access_roles r ON r.role_id = p.role_id JOIN hmm_access_permission_rules pr ON pr.perm_id = p.perm_id WHERE p.target_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS') ORDER BY r.role_name, p.target_name; PROMPT === HMM leave team VPD ready ===