refs #707: enforce HMM leave team VPD
This commit is contained in:
116
docs/design/707-hmm-leave-team-vpd/README.md
Normal file
116
docs/design/707-hmm-leave-team-vpd/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# HMM 휴가 원장 팀 계층 VPD 설계 (#707)
|
||||
|
||||
> 상태: 구현·ADB 적용·배포·검증 완료
|
||||
> 대상: HMMAIPOC `ADMIN`, `hmm-backoffice.cloud-handson.com`
|
||||
> Git 기준: `hmm-backoffice`
|
||||
|
||||
## 목적
|
||||
|
||||
HMM 백오피스의 사용자·역할·보호 객체·행 접근 규칙을 실제 Oracle VPD와 연결한다.
|
||||
팀장 E1001은 자신과 직접 보고 팀원의 휴가 잔여 및 휴가 신청을 조회하고, 일반 팀원은 자기 행만
|
||||
조회한다. 유효한 사용자 컨텍스트나 허용 규칙이 없으면 모든 행을 차단한다.
|
||||
|
||||
## 조사 결과
|
||||
|
||||
- 직원 7명은 `HMM_HR_EMPLOYEES`에 있으며 E1001의 직접 보고 관계가
|
||||
`MANAGER_EMPLOYEE_ID = 1`로 저장되어 있다.
|
||||
- `HMM_LEAVE_BALANCES`는 직원별 1행씩 총 7행이고, `HMM_LEAVE_REQUESTS`는 팀원 6명의 총 8행이다.
|
||||
- HMM 역할 `HMM_HR_VIEWER`, `HMM_HR_MANAGER`, `HMM_HR_ADMIN`은 존재한다.
|
||||
- 적용 전 사용자 역할, HMM 토큰, 보호 객체, 행 접근 규칙, VPD 정책은 0건이었다.
|
||||
- `ADMIN`은 `EXEMPT ACCESS POLICY` 권한이 있어 정책 적용 여부를 검증할 때는 별도의 비면제
|
||||
검증 사용자가 필요하다.
|
||||
|
||||
## 접근 정책
|
||||
|
||||
| 사용자 유형 | 역할 | 저장 규칙 | 결과 |
|
||||
|---|---|---|---|
|
||||
| 일반 팀원 | `HMM_HR_VIEWER` | `SELF` | 현재 직원 ID와 같은 `EMPLOYEE_ID` 행 |
|
||||
| 팀장 | `HMM_HR_MANAGER` | `MANAGED_TEAM` | 현재 직원 본인과 `MANAGER_EMPLOYEE_ID`가 현재 직원인 직접 보고자 행 |
|
||||
| HR 관리자 | `HMM_HR_ADMIN` | `ALL` | 전체 행. 역할이 실제로 부여된 경우에만 적용 |
|
||||
| 미인증/무권한 | 없음 | 없음 | `1 = 0` |
|
||||
|
||||
E1001에는 `HMM_HR_MANAGER`, E1002~E1007에는 `HMM_HR_VIEWER`를 부여한다. HR 관리자 역할은
|
||||
미리 규칙만 준비하고 현재 데모 사용자에게 자동 부여하지 않는다.
|
||||
|
||||
## DB 구성
|
||||
|
||||
### 컨텍스트
|
||||
|
||||
`HMM_ACCESS_CTX`는 `HMM_ACCESS_CTX_PKG.SET_USER_BY_BEARER`만이 설정한다.
|
||||
패키지는 전달된 토큰의 SHA-256 해시를 `HMM_ACCESS_BEARER_TOKENS`와 비교하고 다음 조건을 모두
|
||||
만족한 경우에만 직원 ID, 사번, 팀 ID를 세션 컨텍스트에 기록한다.
|
||||
|
||||
- 토큰이 회수되지 않음
|
||||
- 만료 시각이 현재보다 이후
|
||||
- 직원이 `ACTIVE`
|
||||
|
||||
오류 또는 누락 토큰은 컨텍스트를 먼저 지우고 실패한다.
|
||||
|
||||
### 정책 함수
|
||||
|
||||
`HMM_LEAVE_VPD_FILTER`는 현재 컨텍스트의 직원에게 직접 또는 접근 그룹으로 부여된 활성 역할을
|
||||
구한다. 역할과 `CB_PERMISSION`, `CB_PERMISSION_RULE`에 저장된 대상별 규칙을 결합해
|
||||
`HMM_LEAVE_BALANCES`와 `HMM_LEAVE_REQUESTS`의 `EMPLOYEE_ID` 조건을 반환한다.
|
||||
|
||||
- 한 권한 안의 규칙은 `AND`
|
||||
- 서로 다른 허용 권한은 `OR`
|
||||
- 거부 규칙은 허용 결과에서 제외
|
||||
- 알 수 없는 규칙, 존재하지 않는 컬럼, 빈 컨텍스트는 fail-closed
|
||||
|
||||
### 정책 연결
|
||||
|
||||
두 휴가 테이블에 `HMM_LEAVE_SCOPE_POLICY`를 `SELECT`, `CONTEXT_SENSITIVE`로 연결한다.
|
||||
정책 메타데이터와 설명은 백오피스의 보호 객체 및 VPD 화면에서 조회한다.
|
||||
|
||||
## 백오피스 변경
|
||||
|
||||
- `/permissions`가 HMM 역할과 `CB_PERMISSION*` 호환 저장소의 실제 규칙을 표시한다.
|
||||
- HMM 행 규칙 선택지는 `ALL`, `MANAGED_TEAM`, `SELF`, `STATIC_SQL`을 우선 제공한다.
|
||||
- 미리보기는 `HMM_ACCESS_CTX.EMPLOYEE_ID`와 직접 보고 관계를 보여준다.
|
||||
- 휴가 테이블의 기본 정책 연결은 `HMM_LEAVE_VPD_FILTER`를 선택한다.
|
||||
- 보호 객체·역할·권한 수정은 기존 감사 및 외부 권한 변경 알림 흐름을 유지한다.
|
||||
|
||||
## 배포 순서
|
||||
|
||||
1. 애플리케이션 테스트와 SQL 구문 검사를 수행한다.
|
||||
2. 멱등 SQL `sql/adb/72_hmm_leave_team_vpd.sql`을 `ADMIN`으로 실행한다.
|
||||
3. VPD 정책·컴파일 상태·백오피스 화면을 확인한다.
|
||||
4. 백오피스와 같은 SHA-256 저장 규칙으로 E1001과 E1002 임시 검증 토큰을 만든다.
|
||||
5. 비면제 검증 사용자에서 토큰별 조회 결과를 확인하고 검증 토큰과 사용자를 제거한다.
|
||||
6. 애플리케이션 JAR를 배포한 뒤 전 메뉴 회귀검사를 수행한다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- E1001: 휴가 잔여 7행, 휴가 신청 8행
|
||||
- E1002: 휴가 잔여 1행, 휴가 신청 1행
|
||||
- E1002가 E1003의 행을 조건에 추가 조회해도 0행
|
||||
- 무컨텍스트: 두 테이블 모두 0행
|
||||
- 두 정책과 컨텍스트/패키지/함수가 `VALID` 및 `ENABLE=YES`
|
||||
- `/users`, `/roles`, `/objects`, `/permissions`, `/vpd-policies`, `/vpd-filter-runtime`,
|
||||
`/tokens`, `/probe`를 포함한 백오피스 메뉴에 데이터 처리 오류가 없음
|
||||
|
||||
## 검증 결과
|
||||
|
||||
2026-07-23 배포에서 모든 완료 기준을 충족했다.
|
||||
|
||||
- `HMM_ACCESS_CTX_PKG`와 `HMM_LEAVE_VPD_FILTER`는 `VALID`
|
||||
- 두 `HMM_LEAVE_SCOPE_POLICY`는 `CONTEXT_SENSITIVE`, `ENABLE=YES`
|
||||
- 비면제 임시 DB 사용자와 원문을 저장하지 않는 임시 토큰으로 확인:
|
||||
- 무컨텍스트: 잔여 0행, 신청 0행
|
||||
- E1001 팀장: 잔여 7행, 신청 8행
|
||||
- E1002 팀원: 잔여 1행, 신청 1행
|
||||
- E1002가 E1003 신청을 조건 조회: 0행
|
||||
- 무효 토큰: 0행
|
||||
- 검증용 DB 사용자와 임시 토큰은 즉시 제거했다.
|
||||
- 자동 테스트 105건이 통과했다.
|
||||
- 실제 브라우저로 관리 화면 26개와 HMM 정형 원장 6개를 전수 확인했으며 HTTP/화면/브라우저
|
||||
오류는 0건이었다.
|
||||
|
||||
상세 증거는 `docs/reports/2026-07-23-hmm-leave-team-vpd-verification.md`에 기록한다.
|
||||
|
||||
## 보안 및 롤백
|
||||
|
||||
- 토큰 원문과 DB 비밀번호를 SQL, Git, 로그에 기록하지 않는다.
|
||||
- 검증용 DB 사용자와 검증 토큰은 테스트가 끝나면 제거 또는 회수한다.
|
||||
- 롤백은 두 `HMM_LEAVE_SCOPE_POLICY`를 비활성화하거나 제거한 뒤 사용자 역할·권한 메타데이터를
|
||||
유지하는 방식으로 수행한다. 휴가 원장 데이터는 수정하거나 삭제하지 않는다.
|
||||
61
docs/reports/2026-07-23-hmm-leave-team-vpd-verification.md
Normal file
61
docs/reports/2026-07-23-hmm-leave-team-vpd-verification.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# HMM 휴가 팀 계층 VPD 적용·검증 보고서
|
||||
|
||||
- 일자: 2026-07-23
|
||||
- Redmine: #707
|
||||
- 브랜치: `hmm-backoffice`
|
||||
- 대상 DB: HMMAIPOC `ADMIN`
|
||||
- 대상 서비스: `https://hmm-backoffice.cloud-handson.com`
|
||||
|
||||
## 적용 결과
|
||||
|
||||
`sql/adb/72_hmm_leave_team_vpd.sql`을 적용해 다음 구성을 활성화했다.
|
||||
|
||||
| 구성 | 결과 |
|
||||
|---|---|
|
||||
| 신뢰 컨텍스트 | `HMM_ACCESS_CTX` |
|
||||
| 토큰 검증 패키지 | `HMM_ACCESS_CTX_PKG` / `VALID` |
|
||||
| 정책 함수 | `HMM_LEAVE_VPD_FILTER` / `VALID` |
|
||||
| 휴가 잔여 정책 | `HMM_LEAVE_BALANCES.HMM_LEAVE_SCOPE_POLICY` / `ENABLE=YES` |
|
||||
| 휴가 신청 정책 | `HMM_LEAVE_REQUESTS.HMM_LEAVE_SCOPE_POLICY` / `ENABLE=YES` |
|
||||
| 팀장 역할 | E1001 → `HMM_HR_MANAGER` |
|
||||
| 팀원 역할 | E1002~E1007 → `HMM_HR_VIEWER` |
|
||||
| 저장 권한 | 두 객체 × `ALL`, `MANAGED_TEAM`, `SELF` = 6건 |
|
||||
|
||||
## 실제 행 접근 검증
|
||||
|
||||
`ADMIN`에는 `EXEMPT ACCESS POLICY`가 있으므로 이를 결과 검증에 사용하지 않았다. 별도의 비면제
|
||||
임시 DB 사용자를 만들고, SHA-256 해시만 DB에 저장한 임시 E1001/E1002 토큰으로 조회한 뒤
|
||||
사용자와 토큰을 즉시 제거했다.
|
||||
|
||||
| 검증 세션 | 휴가 잔여 | 휴가 신청 | 교차 사용자 조건 조회 |
|
||||
|---|---:|---:|---:|
|
||||
| 컨텍스트 없음 | 0 | 0 | - |
|
||||
| E1001 팀장 | 7 | 8 | 직접 보고 팀원 포함 |
|
||||
| E1002 팀원 | 1 | 1 | E1003 신청 0 |
|
||||
| 무효 토큰 | 0 | - | 컨텍스트 초기화·차단 |
|
||||
|
||||
결과: `VPD_VERIFY=PASS`
|
||||
|
||||
## 애플리케이션 검증
|
||||
|
||||
- Maven: 105 tests, 0 failures, 0 errors
|
||||
- 배포 JAR SHA-256:
|
||||
`1b10582e72e4639902ea8f65156af1067941af3ef50cde1d9f767c88f048fb85`
|
||||
- systemd: `vpd-backoffice.service` active
|
||||
- 공개 로그인: HTTP 200
|
||||
- 실제 브라우저 전수 검사:
|
||||
- 관리 화면 26개: 실패 0
|
||||
- HMM 정형 원장 6개: 실패 0
|
||||
- `ORA-*`, 데이터 처리 오류, KB/POC 잔존 문구: 0
|
||||
- page error: 0
|
||||
- console error: 0
|
||||
|
||||
특히 `/roles`, `/permissions`, `/vpd-policies`, `/vpd-filter-runtime`에서 HMM 역할 3개,
|
||||
휴가 원장 권한 6개, 정책 2개와 `HMM_LEAVE_VPD_FILTER`가 표시되는 것을 화면으로 확인했다.
|
||||
|
||||
## 보안 정리
|
||||
|
||||
- DB 비밀번호, 토큰 원문, 해시는 Git·Redmine·보고서에 기록하지 않았다.
|
||||
- 임시 비면제 DB 사용자는 검증 종료 시 `DROP USER ... CASCADE`로 제거했다.
|
||||
- 임시 토큰 2건은 검증 종료 시 삭제했다.
|
||||
- HR 관리자 역할은 전체 접근 규칙만 준비하고 데모 사용자에게 자동 부여하지 않았다.
|
||||
597
sql/adb/72_hmm_leave_team_vpd.sql
Normal file
597
sql/adb/72_hmm_leave_team_vpd.sql
Normal file
@@ -0,0 +1,597 @@
|
||||
-- ============================================================
|
||||
-- 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 ===
|
||||
@@ -67,11 +67,14 @@ public record PermissionView(
|
||||
if (upper.startsWith("STATIC_SQL ")) {
|
||||
return "정적 SQL 조건: " + rawRule.substring("STATIC_SQL ".length());
|
||||
}
|
||||
if (upper.contains(" MY_DEPT")) {
|
||||
if ("MY_DEPT".equals(upper) || upper.contains(" MY_DEPT")) {
|
||||
return "내 부서 행";
|
||||
}
|
||||
if (upper.contains(" SELF")) {
|
||||
return "내 사번/소유자 행";
|
||||
if ("MANAGED_TEAM".equals(upper) || upper.contains(" MANAGED_TEAM")) {
|
||||
return "본인 및 직접 보고 팀원 행";
|
||||
}
|
||||
if ("SELF".equals(upper) || upper.contains(" SELF")) {
|
||||
return "본인 직원 행";
|
||||
}
|
||||
if (upper.contains(" TAG ")) {
|
||||
return "태그 조건: " + rawRule;
|
||||
|
||||
@@ -17,6 +17,7 @@ public record VpdFunctionOption(
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,21 @@ public record VpdPolicyFormOptions(
|
||||
|
||||
public String defaultPermissionFunctionKey() {
|
||||
return functions.stream()
|
||||
.filter(function -> "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(function.functionName()))
|
||||
.filter(VpdFunctionOption::permissionSystemDefault)
|
||||
.findFirst()
|
||||
.map(VpdFunctionOption::value)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
public String defaultPermissionFunctionKey(String objectName) {
|
||||
String preferredFunction = objectName != null
|
||||
&& objectName.toUpperCase(java.util.Locale.ROOT).startsWith("HMM_LEAVE_")
|
||||
? "HMM_LEAVE_VPD_FILTER"
|
||||
: "CB_AGENT_DOC_VPD_FILTER";
|
||||
return functions.stream()
|
||||
.filter(function -> preferredFunction.equalsIgnoreCase(function.functionName()))
|
||||
.findFirst()
|
||||
.map(VpdFunctionOption::value)
|
||||
.orElseGet(this::defaultPermissionFunctionKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public record VpdPolicyView(
|
||||
}
|
||||
|
||||
public boolean permissionSystemDefault() {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,18 +333,16 @@ public class BackofficeSchemaService {
|
||||
);
|
||||
}
|
||||
|
||||
addCountCheck(checks, "VPD Runtime", "CB_AGENT_CTX", "CONTEXT",
|
||||
"SELECT COUNT(*) FROM all_context WHERE namespace = 'CB_AGENT_CTX'",
|
||||
"sqlcl에서 17_agent_ords_security_local_vpd_setup.sql 또는 CREATE CONTEXT를 실행하세요.",
|
||||
addCountCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX", "CONTEXT",
|
||||
"SELECT COUNT(*) FROM all_context WHERE namespace = 'HMM_ACCESS_CTX'",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.",
|
||||
true);
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG", "PACKAGE",
|
||||
"sqlcl에서 cb_agent_ctx_pkg package를 생성/컴파일하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG BODY", "PACKAGE BODY",
|
||||
"sqlcl에서 cb_agent_ctx_pkg package body를 생성/컴파일하세요.", "CB_AGENT_CTX_PKG");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_DOC_VPD_FILTER", "FUNCTION",
|
||||
"sqlcl에서 26_agent_ords_security_dynamic_vpd_filter.sql을 실행하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CAN_READ_COLUMN", "FUNCTION",
|
||||
"sqlcl에서 backoffice support redaction function을 생성/컴파일하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX_PKG", "PACKAGE",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX_PKG BODY", "PACKAGE BODY",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.", "HMM_ACCESS_CTX_PKG");
|
||||
addUserObjectCheck(checks, "VPD Runtime", "HMM_LEAVE_VPD_FILTER", "FUNCTION",
|
||||
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.");
|
||||
addPolicyCheck(checks);
|
||||
|
||||
addCountCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "USER",
|
||||
@@ -360,8 +358,6 @@ public class BackofficeSchemaService {
|
||||
addAllObjectCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "CB_ORDS_HANDLER_PKG", "PACKAGE",
|
||||
"CB_ORDS로 접속해 22_agent_ords_security_ords_handler_setup.sql을 실행하세요.");
|
||||
|
||||
addGrantCheck(checks, currentUser, "CB_AGENT_CTX_PKG", "EXECUTE");
|
||||
addGrantCheck(checks, currentUser, "CB_AGENT_CAN_READ_COLUMN", "EXECUTE");
|
||||
addProtectedObjectGrantCheck(checks);
|
||||
|
||||
return new SchemaPreflightView(currentUser, ORDS_SCHEMA, checks, appDdlPreview(), sqlclScript(currentUser));
|
||||
@@ -597,14 +593,14 @@ public class BackofficeSchemaService {
|
||||
SELECT COUNT(*)
|
||||
FROM all_policies
|
||||
WHERE pf_owner = USER
|
||||
AND function = 'CB_AGENT_DOC_VPD_FILTER'
|
||||
AND function = 'HMM_LEAVE_VPD_FILTER'
|
||||
""");
|
||||
checks.add(new SchemaPreflightCheck(
|
||||
"VPD Runtime",
|
||||
"CB_AGENT_DOC_VPD_FILTER 적용 policy",
|
||||
"HMM_LEAVE_VPD_FILTER 적용 policy",
|
||||
"DBMS_RLS POLICY",
|
||||
count > 0 ? "OK" : "WARN",
|
||||
count > 0 ? count + "개 policy가 기본 filter를 사용합니다." : "기본 filter가 적용된 TABLE/VIEW가 없습니다.",
|
||||
count > 0 ? count + "개 HMM 휴가 policy가 기본 filter를 사용합니다." : "HMM 휴가 filter가 적용된 TABLE/VIEW가 없습니다.",
|
||||
count > 0 ? "조치 없음" : "운영 > VPD 설정에서 개별 또는 벌크 적용을 실행하세요.",
|
||||
false
|
||||
));
|
||||
@@ -640,9 +636,14 @@ public class BackofficeSchemaService {
|
||||
}
|
||||
|
||||
private void addProtectedObjectGrantCheck(List<SchemaPreflightCheck> checks) {
|
||||
if (!exists("SELECT COUNT(*) FROM user_tables WHERE table_name = 'CB_PROTECTED_OBJECT'")) {
|
||||
if (!exists("""
|
||||
SELECT COUNT(*)
|
||||
FROM user_objects
|
||||
WHERE object_name = 'CB_PROTECTED_OBJECT'
|
||||
AND object_type IN ('TABLE', 'VIEW')
|
||||
""")) {
|
||||
checks.add(new SchemaPreflightCheck("Grants", "보호 객체 SELECT grants", "GRANT", "WARN",
|
||||
"CB_PROTECTED_OBJECT 테이블이 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
|
||||
"CB_PROTECTED_OBJECT 객체가 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
|
||||
"지원 테이블 생성/확인 후 다시 점검하세요.", false));
|
||||
return;
|
||||
}
|
||||
@@ -706,26 +707,16 @@ public class BackofficeSchemaService {
|
||||
private String sqlclScript(String currentUser) {
|
||||
String owner = currentUser == null || currentUser.isBlank() ? "ADMIN" : currentUser;
|
||||
return """
|
||||
-- sqlcl에서 실행할 VPD/ORDS runtime 준비 순서
|
||||
-- 1. ADMIN 또는 보호 객체 owner로 접속
|
||||
@sql/adb/17_agent_ords_security_local_vpd_setup.sql
|
||||
-- sqlcl에서 실행할 HMM VPD runtime 준비 순서
|
||||
-- 1. ADMIN으로 접속
|
||||
@sql/adb/25_agent_ords_security_backoffice_support.sql
|
||||
@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
|
||||
@sql/adb/62_kb_aso_masking_backoffice_metadata.sql
|
||||
@sql/adb/21_agent_ords_security_ords_enable_schema.sql
|
||||
@sql/adb/72_hmm_leave_team_vpd.sql
|
||||
|
||||
-- 2. ORDS parsing schema로 접속
|
||||
CONNECT cb_ords/<password>@<tns_alias>
|
||||
@sql/adb/22_agent_ords_security_ords_handler_setup.sql
|
||||
|
||||
-- 3. 대표 권한 부여 SQL
|
||||
-- 2. 비면제 업무 runtime 사용자에 필요한 최소 권한
|
||||
CONNECT %s/<password>@<tns_alias>
|
||||
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
|
||||
GRANT SELECT ON <owner>.<table_or_view> TO cb_ords;
|
||||
|
||||
-- 4. 마스킹 규칙을 UI에서 컬럼에 연결한 뒤 실행
|
||||
@sql/adb/64_kb_aso_masking_default_column_rules.sql
|
||||
@sql/adb/63_kb_aso_masking_rule_runtime.sql
|
||||
GRANT EXECUTE ON hmm_access_ctx_pkg TO <hmm_runtime_user>;
|
||||
GRANT SELECT ON hmm_leave_balances TO <hmm_runtime_user>;
|
||||
GRANT SELECT ON hmm_leave_requests TO <hmm_runtime_user>;
|
||||
""".formatted(owner.toLowerCase());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class PermissionService {
|
||||
|
||||
private static final Set<String> RULE_TYPES = Set.of(
|
||||
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||
"ALL", "=", "!=", "MANAGED_TEAM", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
|
||||
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL",
|
||||
"TOKEN_SUBJECT", "OWN_CONTRACT", "CHANNEL_CONTRACT", "OWN_CUSTOMER", "CHANNEL_CUSTOMER",
|
||||
"STATIC_SQL");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
|
||||
"=", "!=", "DEPT", "EMP_NO", "TAG", "STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL", "STATIC_SQL");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
|
||||
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
|
||||
"MANAGED_TEAM", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
|
||||
private static final Set<String> STATIC_SQL_KEYWORDS = Set.of(
|
||||
"AND", "OR", "NOT", "NULL", "IS", "IN", "LIKE", "BETWEEN", "ESCAPE", "TRUE", "FALSE",
|
||||
"UPPER", "LOWER", "TRIM", "NVL", "COALESCE", "TO_CHAR", "TO_DATE", "REGEXP_LIKE", "INSTR",
|
||||
|
||||
@@ -39,6 +39,8 @@ public class VpdPolicyService {
|
||||
private static final long CATALOG_CACHE_MILLIS = 15 * 60_000L;
|
||||
private static final String COMMON_POLICY_NAME = "CB_PERMISSION_SELECT_POLICY";
|
||||
private static final String DEFAULT_PERMISSION_FILTER_FUNCTION = "CB_AGENT_DOC_VPD_FILTER";
|
||||
private static final String HMM_LEAVE_POLICY_NAME = "HMM_LEAVE_SCOPE_POLICY";
|
||||
private static final String HMM_LEAVE_FILTER_FUNCTION = "HMM_LEAVE_VPD_FILTER";
|
||||
/*
|
||||
* DBMS_RLS.ADD_POLICY accepts PL/SQL BOOLEAN arguments. Keep all four
|
||||
* permitted flag combinations as fixed statements: database object names
|
||||
@@ -154,8 +156,8 @@ public class VpdPolicyService {
|
||||
@Transactional
|
||||
public void saveFilterFunction(String functionOwnerValue, String functionNameValue, String filterPredicateValue) {
|
||||
String functionName = requiredIdentifier(functionNameValue, "Function name");
|
||||
if (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
|
||||
if (isSystemManagedPermissionFilter(functionName)) {
|
||||
throw new AppException("기본 동적 권한 필터 " + functionName
|
||||
+ "는 이 화면에서 수정할 수 없습니다. 권한체계는 사용자·그룹·역할·행 접근 규칙 화면에서 변경하세요.");
|
||||
}
|
||||
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
|
||||
@@ -180,15 +182,18 @@ public class VpdPolicyService {
|
||||
if (objectParts.length != 2) {
|
||||
throw new AppException("보호할 객체 형식이 올바르지 않습니다: " + objectKey);
|
||||
}
|
||||
String functionKey = formOptions().defaultPermissionFunctionKey();
|
||||
String functionKey = formOptions().defaultPermissionFunctionKey(objectParts[1]);
|
||||
if (functionKey.isBlank()) {
|
||||
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
|
||||
throw new AppException("대상 객체에 사용할 기본 동적 권한 필터"
|
||||
+ "가 설치되어 있지 않습니다. 운영 상태에서 동적 권한 필터 설치 여부를 확인한 뒤 다시 적용하세요.");
|
||||
}
|
||||
String policyName = isHmmLeaveObject(objectParts[1])
|
||||
? HMM_LEAVE_POLICY_NAME
|
||||
: COMMON_POLICY_NAME;
|
||||
createPolicy(new VpdPolicyCreateCommand(
|
||||
objectParts[0],
|
||||
objectParts[1],
|
||||
COMMON_POLICY_NAME,
|
||||
policyName,
|
||||
functionKey,
|
||||
null,
|
||||
null,
|
||||
@@ -352,8 +357,8 @@ public class VpdPolicyService {
|
||||
description = null;
|
||||
}
|
||||
return description == null || description.isBlank()
|
||||
? (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
|
||||
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
|
||||
? (isSystemManagedPermissionFilter(functionName)
|
||||
? defaultPermissionFilterDescription(functionName)
|
||||
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.")
|
||||
: description;
|
||||
}
|
||||
@@ -377,7 +382,7 @@ public class VpdPolicyService {
|
||||
*/
|
||||
public String findFilterPredicate(String owner, String packageName, String functionName) {
|
||||
if (packageName != null && !packageName.isBlank()
|
||||
|| DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
|| isSystemManagedPermissionFilter(functionName)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
@@ -493,10 +498,11 @@ public class VpdPolicyService {
|
||||
) {
|
||||
List<VpdPolicyTemplateOption> templates = new ArrayList<>();
|
||||
functions.stream()
|
||||
.filter(function -> DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(function.functionName()))
|
||||
.findFirst()
|
||||
.ifPresent(function -> templates.add(new VpdPolicyTemplateOption(
|
||||
COMMON_POLICY_NAME,
|
||||
.filter(VpdFunctionOption::permissionSystemDefault)
|
||||
.forEach(function -> templates.add(new VpdPolicyTemplateOption(
|
||||
HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(function.functionName())
|
||||
? HMM_LEAVE_POLICY_NAME
|
||||
: COMMON_POLICY_NAME,
|
||||
function.owner(),
|
||||
function.packageName(),
|
||||
function.functionName(),
|
||||
@@ -746,6 +752,23 @@ public class VpdPolicyService {
|
||||
return formOptions().defaultPermissionFunctionKey();
|
||||
}
|
||||
|
||||
private boolean isSystemManagedPermissionFilter(String functionName) {
|
||||
return DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
|
||||
|| HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(functionName);
|
||||
}
|
||||
|
||||
private boolean isHmmLeaveObject(String objectName) {
|
||||
return objectName != null
|
||||
&& objectName.toUpperCase(Locale.ROOT).startsWith("HMM_LEAVE_");
|
||||
}
|
||||
|
||||
private String defaultPermissionFilterDescription(String functionName) {
|
||||
if (HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|
||||
return "HMM 직원 본인·직접 보고 팀원·HR 관리자 역할을 행 조건으로 변환합니다.";
|
||||
}
|
||||
return "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다.";
|
||||
}
|
||||
|
||||
private String normalizeStatementTypes(String value) {
|
||||
String raw = value == null || value.isBlank() ? "SELECT" : value;
|
||||
List<String> statements = List.of(raw.split(",")).stream()
|
||||
|
||||
@@ -153,9 +153,13 @@ public class VpdPolicyController {
|
||||
}
|
||||
|
||||
private static String defaultFilterDescription(String functionName) {
|
||||
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|
||||
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
|
||||
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
|
||||
if ("HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName)) {
|
||||
return "HMM 직원 본인·직접 보고 팀원·HR 관리자 역할을 행 조건으로 변환합니다.";
|
||||
}
|
||||
if ("CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)) {
|
||||
return "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다.";
|
||||
}
|
||||
return "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
|
||||
}
|
||||
|
||||
private static long elapsedMillis(long started, long finished) {
|
||||
|
||||
@@ -61,24 +61,67 @@
|
||||
|
||||
<select id="countPermissionsByRoleId" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM dual
|
||||
WHERE 1 = 0
|
||||
FROM cb_permission
|
||||
WHERE role_id = #{roleId,jdbcType=NUMERIC}
|
||||
</select>
|
||||
|
||||
<select id="findPermissionViews" resultType="com.cloudhandson.vpdbackoffice.domain.permission.PermissionView">
|
||||
SELECT CAST(NULL AS NUMBER) AS permission_id,
|
||||
CAST(NULL AS NUMBER) AS role_id,
|
||||
CAST(NULL AS VARCHAR2(80)) AS role_name,
|
||||
CAST(NULL AS NUMBER) AS object_id,
|
||||
CAST(NULL AS VARCHAR2(200)) AS object_name,
|
||||
CAST(NULL AS VARCHAR2(30)) AS action,
|
||||
CAST(NULL AS VARCHAR2(20)) AS permission_effect,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS rules,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS visible_columns,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS filter_preview,
|
||||
CAST(NULL AS VARCHAR2(4000)) AS null_policy_preview
|
||||
FROM dual
|
||||
WHERE 1 = 0
|
||||
SELECT p.perm_id AS permission_id,
|
||||
r.role_id,
|
||||
r.role_name,
|
||||
NVL(o.object_id, -1) AS object_id,
|
||||
p.target_name AS object_name,
|
||||
p.action_name AS action,
|
||||
NVL(p.permission_effect, 'ALLOW') AS permission_effect,
|
||||
LISTAGG(
|
||||
NVL2(pr.rule_column, pr.rule_column || ' ', '') ||
|
||||
pr.rule_type ||
|
||||
NVL2(pr.rule_value, ' ' || pr.rule_value, ''),
|
||||
', '
|
||||
) 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,
|
||||
(
|
||||
SELECT LISTAGG(
|
||||
CASE
|
||||
WHEN pr2.rule_type = 'ALL' THEN '1 = 1'
|
||||
WHEN pr2.rule_type = 'MANAGED_TEAM' THEN
|
||||
NVL(pr2.rule_column, 'EMPLOYEE_ID')
|
||||
|| ' IN (current employee and direct reports)'
|
||||
WHEN pr2.rule_type = 'SELF' THEN
|
||||
NVL(pr2.rule_column, 'EMPLOYEE_ID')
|
||||
|| ' = SYS_CONTEXT(HMM_ACCESS_CTX.EMPLOYEE_ID)'
|
||||
WHEN pr2.rule_type = 'STATIC_SQL' THEN pr2.rule_value
|
||||
ELSE NVL2(pr2.rule_column, pr2.rule_column || ' ', '')
|
||||
|| pr2.rule_type
|
||||
|| NVL2(pr2.rule_value, ' ' || pr2.rule_value, '')
|
||||
END,
|
||||
CHR(10) || 'AND '
|
||||
) WITHIN GROUP (ORDER BY pr2.rule_id)
|
||||
FROM cb_permission_rule pr2
|
||||
WHERE pr2.perm_id = p.perm_id
|
||||
) AS filter_preview,
|
||||
'컬럼 원문/마스킹은 컬럼 마스킹에서 관리' AS null_policy_preview
|
||||
FROM cb_permission p
|
||||
JOIN hmm_access_roles r
|
||||
ON r.role_id = p.role_id
|
||||
LEFT JOIN cb_protected_object o
|
||||
ON o.object_name = p.target_name
|
||||
LEFT JOIN cb_permission_rule pr
|
||||
ON pr.perm_id = p.perm_id
|
||||
GROUP BY
|
||||
p.perm_id,
|
||||
r.role_id,
|
||||
r.role_name,
|
||||
o.object_id,
|
||||
p.target_name,
|
||||
p.action_name,
|
||||
p.permission_effect
|
||||
ORDER BY r.role_name, p.target_name
|
||||
</select>
|
||||
|
||||
<select id="findPermissionId" resultType="long">
|
||||
|
||||
@@ -396,9 +396,10 @@ function syncRuleTypeHints(root = document) {
|
||||
|
||||
const type = typeSelect.value;
|
||||
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG', 'STAKEHOLDER_SELF', 'STAKEHOLDER_CHANNEL', 'STATIC_SQL'].includes(type);
|
||||
const columnOptional = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
const columnOptional = ['ALL', 'MANAGED_TEAM', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
const placeholderByType = {
|
||||
ALL: '값 불필요',
|
||||
MANAGED_TEAM: '값 불필요',
|
||||
MY_DEPT: '값 불필요',
|
||||
SELF: '값 불필요',
|
||||
DEPT: '예: HR',
|
||||
@@ -417,10 +418,10 @@ function syncRuleTypeHints(root = document) {
|
||||
};
|
||||
|
||||
columnSelect.required = !columnOptional;
|
||||
columnSelect.disabled = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
columnSelect.disabled = ['ALL', 'MANAGED_TEAM', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG', 'STATIC_SQL'].includes(type);
|
||||
valueInput.required = valueRequired;
|
||||
valueInput.disabled = [
|
||||
'ALL', 'MY_DEPT', 'SELF', 'TOKEN_SUBJECT', 'OWN_CONTRACT',
|
||||
'ALL', 'MANAGED_TEAM', 'MY_DEPT', 'SELF', 'TOKEN_SUBJECT', 'OWN_CONTRACT',
|
||||
'CHANNEL_CONTRACT', 'OWN_CUSTOMER', 'CHANNEL_CUSTOMER'
|
||||
].includes(type);
|
||||
valueInput.placeholder = placeholderByType[type] || '비교 값';
|
||||
@@ -708,8 +709,9 @@ function sqlLiteral(value) {
|
||||
|
||||
function permissionRuleBusinessLabel(type, column, value) {
|
||||
const displayColumn = column || {
|
||||
MANAGED_TEAM: 'EMPLOYEE_ID',
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
SELF: 'EMPLOYEE_ID',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
@@ -724,6 +726,9 @@ function permissionRuleBusinessLabel(type, column, value) {
|
||||
if (type === 'ALL') {
|
||||
return '전체 행';
|
||||
}
|
||||
if (type === 'MANAGED_TEAM') {
|
||||
return `본인 및 직접 보고 팀원 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'TOKEN_SUBJECT') {
|
||||
return `토큰 이해관계자 본인 행 (${displayColumn})`;
|
||||
}
|
||||
@@ -746,7 +751,7 @@ function permissionRuleBusinessLabel(type, column, value) {
|
||||
return `내 부서 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'SELF') {
|
||||
return `내 사번/소유자 행 (${displayColumn})`;
|
||||
return `본인 직원 행 (${displayColumn})`;
|
||||
}
|
||||
if (type === 'TAG') {
|
||||
return `태그 조건 ${displayColumn} contains ${value || '-'}`;
|
||||
@@ -760,8 +765,9 @@ function collectWizardRules(root) {
|
||||
const type = row.querySelector('[name="ruleType"]')?.value || '';
|
||||
const value = row.querySelector('[name="ruleValue"]')?.value || '';
|
||||
const displayColumn = column || {
|
||||
MANAGED_TEAM: 'EMPLOYEE_ID',
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
SELF: 'EMPLOYEE_ID',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
@@ -785,8 +791,9 @@ function collectWizardPredicates(root) {
|
||||
const type = row.querySelector('[name="ruleType"]')?.value || '';
|
||||
const value = row.querySelector('[name="ruleValue"]')?.value || '';
|
||||
const displayColumn = column || {
|
||||
MANAGED_TEAM: 'EMPLOYEE_ID',
|
||||
MY_DEPT: 'DEPT_CODE',
|
||||
SELF: 'OWNER_EMP_NO',
|
||||
SELF: 'EMPLOYEE_ID',
|
||||
DEPT: 'DEPT_CODE',
|
||||
EMP_NO: 'OWNER_EMP_NO',
|
||||
TAG: 'TECH_TAG',
|
||||
@@ -803,6 +810,9 @@ function collectWizardPredicates(root) {
|
||||
if (type === 'ALL') {
|
||||
return '1 = 1';
|
||||
}
|
||||
if (type === 'MANAGED_TEAM') {
|
||||
return `${displayColumn} IN (현재 직원 및 직접 보고 팀원)`;
|
||||
}
|
||||
if (type === 'STATIC_SQL') {
|
||||
return value;
|
||||
}
|
||||
@@ -810,7 +820,7 @@ function collectWizardPredicates(root) {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE')`;
|
||||
}
|
||||
if (type === 'SELF') {
|
||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
||||
return `${displayColumn} = SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')`;
|
||||
}
|
||||
if (type === 'STAKEHOLDER_SELF') {
|
||||
return `EXISTS (KB_CONTRACTS c: c.${displayColumn} = <현재행>.${displayColumn} AND TOKEN_ROLE = '${value}' AND c.FC_ID = TOKEN_STAKEHOLDER_ID)`;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<h1>행 접근 규칙</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. 저장값은 최종 SQL이 아니라 <code>CB_AGENT_DOC_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꾸는 매핑 데이터입니다.</p>
|
||||
<p>컬럼 원문/마스킹은 이 화면에서 처리하지 않습니다. 주민번호, 보험료, 청구금액 같은 컬럼 표시는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 관리합니다.</p>
|
||||
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. HMM 휴가 원장의 저장값은 <code>HMM_LEAVE_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꿉니다.</p>
|
||||
<p>팀장은 본인과 직속 팀원, 팀원은 본인 행만 조회하도록 설정합니다. 컬럼 원문/마스킹은 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 별도로 관리합니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -157,19 +157,14 @@
|
||||
<option value="">객체 컬럼 선택</option>
|
||||
</select>
|
||||
<select class="form-select rule-type-select" name="ruleType">
|
||||
<option value="ALL">ALL</option>
|
||||
<optgroup label="KB · 본인 범위">
|
||||
<option value="OWN_CONTRACT">본인 담당 계약</option>
|
||||
<option value="OWN_CUSTOMER">본인 담당 고객</option>
|
||||
<optgroup label="HMM HR · 표준 범위">
|
||||
<option value="SELF">본인 직원 행</option>
|
||||
<option value="MANAGED_TEAM">본인 및 직접 보고 팀원</option>
|
||||
<option value="ALL">전체 행</option>
|
||||
</optgroup>
|
||||
<optgroup label="KB · 채널 범위">
|
||||
<option value="CHANNEL_CONTRACT">토큰 채널 계약</option>
|
||||
<option value="CHANNEL_CUSTOMER">토큰 채널 고객</option>
|
||||
</optgroup>
|
||||
<optgroup label="KB · 토큰 식별">
|
||||
<option value="TOKEN_SUBJECT">토큰 사용자 본인</option>
|
||||
</optgroup>
|
||||
<optgroup label="SQL · 정적 조건">
|
||||
<optgroup label="고급 조건">
|
||||
<option value="=">선택 컬럼 값 일치</option>
|
||||
<option value="!=">선택 컬럼 값 불일치</option>
|
||||
<option value="STATIC_SQL">정적 SQL 조건식</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
@@ -179,12 +174,12 @@
|
||||
</div>
|
||||
<details class="explanation-details">
|
||||
<summary>행 규칙의 두 가지 적용 방식 보기</summary>
|
||||
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong>은 <code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 최종 WHERE 절에 직접 들어가지 않습니다.</p>
|
||||
<p class="wizard-hint"><strong>표준 범위</strong>는 유효한 HMM 토큰에서 만든 직원 context로 치환됩니다. <code>SELF</code>는 현재 직원의 행만, <code>MANAGED_TEAM</code>은 현재 직원과 <code>MANAGER_EMPLOYEE_ID</code>로 연결된 직접 보고자의 행만 남깁니다. <strong>정적 SQL 조건식</strong>은 현재 객체 컬럼을 사용한 고정 WHERE 조건을 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다.</p>
|
||||
<p class="wizard-hint"><strong>컬럼 원문/마스킹은 제외했습니다.</strong> 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.</p>
|
||||
</details>
|
||||
<details class="explanation-details">
|
||||
<summary>조건 코드가 실제 VPD WHERE 조각으로 바뀌는 방식</summary>
|
||||
<p class="wizard-hint">이 표는 저장 규칙이 <code>CB_AGENT_DOC_VPD_FILTER</code>에서 어떤 predicate로 변환되는지 설명하기 위한 예시입니다. 실제 컬럼 존재 여부와 대상 객체는 DB에서 다시 검증됩니다.</p>
|
||||
<p class="wizard-hint">이 표는 저장 규칙이 <code>HMM_LEAVE_VPD_FILTER</code>에서 어떤 predicate로 변환되는지 설명하기 위한 예시입니다. 실제 컬럼 존재 여부와 대상 객체는 DB에서 다시 검증됩니다.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>저장 조건 코드</th><th>업무 의미</th><th>VPD filter가 만드는 조건</th></tr></thead>
|
||||
@@ -195,29 +190,19 @@
|
||||
<td><code>1 = 1</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TOKEN_SUBJECT</code></td>
|
||||
<td>토큰으로 식별된 이해관계자 본인 행</td>
|
||||
<td><code>USER_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')</code></td>
|
||||
<td><code>SELF</code></td>
|
||||
<td>토큰으로 식별된 HMM 직원 본인 행</td>
|
||||
<td><code>EMPLOYEE_ID = SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>OWN_CONTRACT</code></td>
|
||||
<td>토큰 사용자가 담당 설계사인 계약</td>
|
||||
<td><code>FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>CHANNEL_CONTRACT</code></td>
|
||||
<td>토큰 사용자의 채널에 속한 계약</td>
|
||||
<td><code>FC_CHANNEL = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>OWN_CUSTOMER</code> / <code>CHANNEL_CUSTOMER</code></td>
|
||||
<td>계약원장에서 담당자 또는 채널 기준으로 연결되는 고객·청구·담보·외부보유 행</td>
|
||||
<td><code>EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS ...)</code></td>
|
||||
<td><code>MANAGED_TEAM</code></td>
|
||||
<td>팀장 본인과 직접 보고 팀원의 행</td>
|
||||
<td><code>EMPLOYEE_ID IN (현재 직원 및 MANAGER_EMPLOYEE_ID가 현재 직원인 직원)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>STATIC_SQL</code></td>
|
||||
<td>현재 객체 컬럼으로 표현한 고정 조건</td>
|
||||
<td><code>CONTRACT_STATUS = '정상'</code>처럼 검증된 현재 객체 컬럼 조건</td>
|
||||
<td><code>REQUEST_STATUS = 'PENDING'</code>처럼 검증된 현재 객체 컬럼 조건</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<h1 class="mt-2">행 접근 필터 구조</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>기본 동적 필터 <code>CB_AGENT_DOC_VPD_FILTER</code>의 연결 상태와 현재 DB에 배포된 함수를 읽기 전용으로 보여줍니다. 행 접근 변경은 <a href="/permissions">행 접근 규칙</a>에서 수행합니다.</p>
|
||||
<p>HMM 기본 동적 필터 <code>HMM_LEAVE_VPD_FILTER</code>의 연결 상태와 현재 DB에 배포된 함수를 읽기 전용으로 보여줍니다. 행 접근 변경은 <a href="/permissions">행 접근 규칙</a>에서 수행합니다.</p>
|
||||
<section class="content-band mt-3 mb-0">
|
||||
<h2>행 필터(VPD) 적용 흐름</h2>
|
||||
<p><strong>Oracle VPD 정책 함수는 Bearer Token을 직접 해석하지 않습니다.</strong> 앞 단계의 <code>cb_agent_ctx_pkg.set_user_by_bearer</code>가 만든 <code>CB_AGENT_CTX</code>를 읽어, 현재 SELECT에 붙일 WHERE predicate를 생성합니다.</p>
|
||||
<p><strong>Oracle VPD 정책 함수는 Bearer Token을 직접 해석하지 않습니다.</strong> 앞 단계의 <code>hmm_access_ctx_pkg.set_user_by_bearer</code>가 만든 <code>HMM_ACCESS_CTX</code>를 읽어, 현재 SELECT에 붙일 WHERE predicate를 생성합니다.</p>
|
||||
<div class="diagram-canvas">
|
||||
<svg class="product-flow-diagram" viewBox="0 0 1120 410" role="img" aria-labelledby="vpd-help-flow-title vpd-help-flow-desc">
|
||||
<title id="vpd-help-flow-title">Bearer 토큰 기반 행 필터 적용 흐름</title>
|
||||
@@ -29,15 +29,15 @@
|
||||
<path class="diagram-link" marker-end="url(#flow-arrow)" d="M900 136 V280 H755 V315"/>
|
||||
<g class="diagram-node"><rect x="25" y="48" width="160" height="88" rx="12"/><text x="105" y="79" class="diagram-node-title">Bearer Token</text><text x="105" y="106" class="diagram-node-detail">ORDS 요청</text></g>
|
||||
<g class="diagram-node"><rect x="225" y="48" width="180" height="88" rx="12"/><text x="315" y="76" class="diagram-node-title">토큰 검증·해석</text><text x="315" y="103" class="diagram-node-detail">set_user_by_bearer</text><text x="315" y="120" class="diagram-node-note">실패 시 context 정리</text></g>
|
||||
<g class="diagram-node"><rect x="445" y="48" width="180" height="88" rx="12"/><text x="535" y="76" class="diagram-node-title">CB_AGENT_CTX</text><text x="535" y="103" class="diagram-node-detail">USER_ID · DEPT_CODE · EMP_NO</text><text x="535" y="120" class="diagram-node-note">STAKEHOLDER_USER_ID · CHANNEL</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="665" y="48" width="180" height="88" rx="12"/><text x="755" y="76" class="diagram-node-title">행 접근 함수 호출</text><text x="755" y="103" class="diagram-node-detail">CB_AGENT_DOC_VPD_FILTER</text><text x="755" y="120" class="diagram-node-note">p_object = 조회 대상</text></g>
|
||||
<g class="diagram-node"><rect x="445" y="48" width="180" height="88" rx="12"/><text x="535" y="76" class="diagram-node-title">HMM_ACCESS_CTX</text><text x="535" y="103" class="diagram-node-detail">EMPLOYEE_ID · CODE</text><text x="535" y="120" class="diagram-node-note">TEAM_ID</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="665" y="48" width="180" height="88" rx="12"/><text x="755" y="76" class="diagram-node-title">행 접근 함수 호출</text><text x="755" y="103" class="diagram-node-detail">HMM_LEAVE_VPD_FILTER</text><text x="755" y="120" class="diagram-node-note">p_object = 휴가 원장</text></g>
|
||||
<g class="diagram-node diagram-node-accent"><rect x="885" y="48" width="185" height="88" rx="12"/><text x="977" y="76" class="diagram-node-title">WHERE predicate</text><text x="977" y="103" class="diagram-node-detail">ALLOW / DENY 결합</text><text x="977" y="120" class="diagram-node-note">원래 SELECT에 자동 추가</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="590" y="212" width="310" height="72" rx="12"/><text x="745" y="239" class="diagram-node-title">역할·권한·규칙 메타데이터</text><text x="745" y="263" class="diagram-node-detail">cb_user_role · cb_group_role · cb_permission_rule</text></g>
|
||||
<g class="diagram-node"><rect x="665" y="315" width="180" height="64" rx="12"/><text x="755" y="341" class="diagram-node-title">ALLOW 없음 / USER_ID 없음</text><text x="755" y="363" class="diagram-node-detail">RETURN 1 = 0 · 행 없음</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="590" y="212" width="310" height="72" rx="12"/><text x="745" y="239" class="diagram-node-title">HMM 역할·권한·규칙</text><text x="745" y="263" class="diagram-node-detail">employee/group roles · permission rules</text></g>
|
||||
<g class="diagram-node"><rect x="665" y="315" width="180" height="64" rx="12"/><text x="755" y="341" class="diagram-node-title">ALLOW/직원 context 없음</text><text x="755" y="363" class="diagram-node-detail">RETURN 1 = 0 · 행 없음</text></g>
|
||||
<g class="diagram-node diagram-node-data"><rect x="885" y="315" width="185" height="64" rx="12"/><text x="977" y="341" class="diagram-node-title">ALLOW 조건 충족</text><text x="977" y="363" class="diagram-node-detail">조건에 맞는 행만 반환</text></g>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="form-hint mb-0"><code>rule_type</code>별 WHERE 조각은 권한 내부에서 AND, ALLOW 권한끼리는 OR, DENY 권한은 최종적으로 <code>AND NOT (...)</code>으로 결합됩니다. ASO는 같은 context의 <code>MR_<column_id></code>로 <em>컬럼 표시</em>를 정하고, 행 접근 필터는 <code>USER_ID</code>·<code>DEPT_CODE</code>·<code>EMP_NO</code>·<code>STAKEHOLDER_*</code>로 <em>보이는 행</em>을 정합니다.</p>
|
||||
<p class="form-hint mb-0"><code>rule_type</code>별 WHERE 조각은 권한 내부에서 AND, ALLOW 권한끼리는 OR, DENY 권한은 최종적으로 <code>AND NOT (...)</code>으로 결합됩니다. 행 접근 필터는 <code>EMPLOYEE_ID</code>와 직원의 직접 보고 관계로 <em>보이는 행</em>을 정합니다.</p>
|
||||
</section>
|
||||
</details>
|
||||
</section>
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="macro-micro-grid">
|
||||
<div>
|
||||
<h3>1. 신뢰 컨텍스트 설정</h3>
|
||||
<p>ORDS가 Bearer Token을 검증하고 <code>CB_AGENT_CTX</code>에 사용자·이해관계자·채널 정보를 설정합니다.</p>
|
||||
<p>애플리케이션이 Bearer Token을 검증하고 <code>HMM_ACCESS_CTX</code>에 직원·팀 정보를 설정합니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>2. 권한과 규칙 계산</h3>
|
||||
@@ -102,10 +102,10 @@
|
||||
<table class="table table-sm align-middle">
|
||||
<thead><tr><th>점검 항목</th><th>확인 기준</th><th>조치 위치</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 ORDS에서 검증하고 요청 종료 시 컨텍스트를 정리합니다.</td><td>ORDS Handler / <code>CB_AGENT_CTX_PKG</code></td></tr>
|
||||
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 DB 패키지에서 해시·만료·회수·재직 상태를 검증합니다.</td><td><code>HMM_ACCESS_CTX_PKG</code></td></tr>
|
||||
<tr><td>권한 결합</td><td>권한 내부 규칙은 AND, ALLOW 권한은 OR, DENY 조건은 최종적으로 제외합니다.</td><td><a href="/permissions">행 접근 규칙</a></td></tr>
|
||||
<tr><td>오류·미권한</td><td>유효한 컨텍스트나 ALLOW 권한이 없으면 행 접근은 차단돼야 합니다.</td><td><a href="/probe">접근 검증</a></td></tr>
|
||||
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 보험료·주민번호 등의 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
||||
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 개인정보 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.permission;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PermissionViewTest {
|
||||
|
||||
@Test
|
||||
void translatesHmmLeaveScopeRulesForBackofficeUsers() {
|
||||
PermissionView view = new PermissionView(
|
||||
1L,
|
||||
2L,
|
||||
"HMM_HR_MANAGER",
|
||||
10L,
|
||||
"HMM_LEAVE_REQUESTS",
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
"EMPLOYEE_ID MANAGED_TEAM,EMPLOYEE_ID SELF",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
assertThat(view.businessRuleSummary())
|
||||
.contains("본인 및 직접 보고 팀원 행")
|
||||
.contains("본인 직원 행");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class VpdPolicyFormOptionsTest {
|
||||
|
||||
@Test
|
||||
void choosesHmmFilterForHmmLeaveObjects() {
|
||||
VpdPolicyFormOptions options = new VpdPolicyFormOptions(
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(
|
||||
new VpdFunctionOption("ADMIN", null, "CB_AGENT_DOC_VPD_FILTER", "FUNCTION"),
|
||||
new VpdFunctionOption("ADMIN", null, "HMM_LEAVE_VPD_FILTER", "FUNCTION")
|
||||
),
|
||||
List.of(),
|
||||
List.of("SELECT")
|
||||
);
|
||||
|
||||
assertThat(options.defaultPermissionFunctionKey("HMM_LEAVE_BALANCES"))
|
||||
.isEqualTo("ADMIN.HMM_LEAVE_VPD_FILTER");
|
||||
assertThat(options.defaultPermissionFunctionKey("HMM_LEAVE_REQUESTS"))
|
||||
.isEqualTo("ADMIN.HMM_LEAVE_VPD_FILTER");
|
||||
assertThat(options.defaultPermissionFunctionKey("CB_AGENT_DOCUMENTS"))
|
||||
.isEqualTo("ADMIN.CB_AGENT_DOC_VPD_FILTER");
|
||||
}
|
||||
}
|
||||
@@ -184,8 +184,10 @@ class GuidedFlowTemplateTest {
|
||||
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
|
||||
|
||||
assertThat(html)
|
||||
.contains("value=\"OWN_CUSTOMER\">본인 담당 고객")
|
||||
.contains("value=\"MANAGED_TEAM\">본인 및 직접 보고 팀원")
|
||||
.contains("value=\"SELF\">본인 직원 행")
|
||||
.contains("value=\"STATIC_SQL\">정적 SQL 조건식")
|
||||
.contains("HMM_LEAVE_VPD_FILTER")
|
||||
.contains("행 규칙의 두 가지 적용 방식 보기")
|
||||
.contains("한 권한 안의 규칙은 모두 AND")
|
||||
.contains("서로 다른 역할의 ALLOW 권한은 OR")
|
||||
|
||||
Reference in New Issue
Block a user