feat #466: add deny permission effect
This commit is contained in:
30
docs/design/466-permission-deny-priority/README.md
Normal file
30
docs/design/466-permission-deny-priority/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 설계서: 역할/권한 모델에 deny, 우선순위, 충돌 해소 규칙 추가 (#466)
|
||||
|
||||
> **상태**: Approved
|
||||
> **작성**: [AI] Architect · **최종수정**: 2026-06-25
|
||||
> **추적성** — Redmine: #466 · 관련 ADR: 없음
|
||||
> · 구현 파일: `cb_permission.permission_effect`, `PermissionService`, `PermissionMapper`, `cb_agent_doc_vpd_filter` · 테스트: SQLcl `27`, `mvn test`
|
||||
|
||||
## 1. 목적
|
||||
|
||||
역할이 여러 개 붙은 사용자에게 allow 권한과 deny 권한이 동시에 존재할 때 최종 접근 범위를 명확히 계산한다.
|
||||
|
||||
## 2. 범위
|
||||
|
||||
- 포함: permission 단위 `ALLOW/DENY` 효과 추가, deny 우선 VPD predicate 생성, UI 저장/목록 표시, 테스트.
|
||||
- 제외: role 자체 우선순위 숫자, 승인 workflow, rule별 deny.
|
||||
|
||||
## 3. 인수조건
|
||||
|
||||
- [ ] `cb_permission.permission_effect`가 `ALLOW` 기본값으로 추가된다.
|
||||
- [ ] deny permission은 같은 object/action의 allow 결과에서 제외된다.
|
||||
- [ ] deny `ALL`이 있으면 최종 결과는 `1 = 0`이다.
|
||||
- [ ] allow가 없으면 deny가 있어도 `1 = 0`이다.
|
||||
- [ ] UI에서 권한 효과를 선택하고 목록에서 확인할 수 있다.
|
||||
|
||||
## 4. 설계 결정
|
||||
|
||||
- 효과는 `cb_permission` 단위로 둔다. 하나의 permission set이 allow인지 deny인지 명확히 구분한다.
|
||||
- 최종 predicate는 `(<allow predicate>) AND NOT (<deny predicate>)`이다.
|
||||
- allow `ALL`은 `1 = 1`, deny `ALL`은 즉시 `1 = 0`이다.
|
||||
- deny만 있고 allow가 없으면 whitelist 기본값에 따라 `1 = 0`이다.
|
||||
@@ -65,7 +65,8 @@ 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
|
||||
action_name VARCHAR2(30) NOT NULL,
|
||||
permission_effect VARCHAR2(10) DEFAULT 'ALLOW' NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE cb_permission_rule (
|
||||
@@ -106,9 +107,9 @@ 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 VALUES (100, 10, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
|
||||
INSERT INTO cb_permission VALUES (200, 20, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
|
||||
INSERT INTO cb_permission VALUES (300, 30, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
|
||||
|
||||
INSERT INTO cb_permission_rule VALUES (1000, 100, 'MY_DEPT', NULL);
|
||||
INSERT INTO cb_permission_rule VALUES (2000, 200, 'SELF', NULL);
|
||||
@@ -256,7 +257,8 @@ AUTHID DEFINER
|
||||
AS
|
||||
v_user_id NUMBER;
|
||||
v_target VARCHAR2(128);
|
||||
v_predicate VARCHAR2(32767);
|
||||
v_allow_predicate VARCHAR2(32767);
|
||||
v_deny_predicate VARCHAR2(32767);
|
||||
|
||||
FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
|
||||
BEGIN
|
||||
@@ -294,16 +296,22 @@ AS
|
||||
RETURN NULL;
|
||||
END;
|
||||
|
||||
PROCEDURE append_or(p_clause IN VARCHAR2) IS
|
||||
PROCEDURE append_or(p_effect IN VARCHAR2, p_clause IN VARCHAR2) IS
|
||||
BEGIN
|
||||
IF p_clause IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_predicate IS NULL THEN
|
||||
v_predicate := '(' || p_clause || ')';
|
||||
IF p_effect = 'DENY' THEN
|
||||
IF v_deny_predicate IS NULL THEN
|
||||
v_deny_predicate := '(' || p_clause || ')';
|
||||
ELSE
|
||||
v_predicate := v_predicate || ' OR (' || p_clause || ')';
|
||||
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
|
||||
@@ -323,6 +331,7 @@ BEGIN
|
||||
FOR r IN (
|
||||
SELECT UPPER(TRIM(r.rule_type)) AS rule_type,
|
||||
UPPER(TRIM(r.rule_column)) AS rule_column,
|
||||
NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') AS permission_effect,
|
||||
r.rule_value
|
||||
FROM cb_user_role ur
|
||||
JOIN cb_permission p
|
||||
@@ -338,46 +347,56 @@ BEGIN
|
||||
v_column VARCHAR2(128);
|
||||
BEGIN
|
||||
IF r.rule_type = 'ALL' THEN
|
||||
RETURN '1 = 1';
|
||||
IF r.permission_effect = 'DENY' THEN
|
||||
RETURN '1 = 0';
|
||||
END IF;
|
||||
v_allow_predicate := '(1 = 1)';
|
||||
ELSIF r.rule_type = 'MY_DEPT' THEN
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
|
||||
append_or(r.permission_effect, v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
|
||||
END IF;
|
||||
ELSIF r.rule_type = 'SELF' THEN
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
|
||||
append_or(r.permission_effect, v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
|
||||
END IF;
|
||||
ELSIF r.rule_type = 'DEPT' THEN
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, v_column || ' = ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
ELSIF r.rule_type = 'EMP_NO' THEN
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, v_column || ' = ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
ELSIF r.rule_type = '=' THEN
|
||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, 'TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
ELSIF r.rule_type IN ('!=', '<>') THEN
|
||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, 'TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
IF v_predicate IS NULL THEN
|
||||
IF v_allow_predicate IS NULL THEN
|
||||
RETURN '1 = 0';
|
||||
END IF;
|
||||
|
||||
RETURN '(' || v_predicate || ')';
|
||||
IF v_deny_predicate IS NULL THEN
|
||||
IF v_allow_predicate = '(1 = 1)' THEN
|
||||
RETURN '1 = 1';
|
||||
END IF;
|
||||
RETURN '(' || v_allow_predicate || ')';
|
||||
END IF;
|
||||
|
||||
RETURN '((' || v_allow_predicate || ') AND NOT (' || v_deny_predicate || '))';
|
||||
END;
|
||||
/
|
||||
|
||||
|
||||
@@ -52,6 +52,16 @@ EXCEPTION
|
||||
END;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'ALTER TABLE cb_permission ADD (permission_effect VARCHAR2(10) DEFAULT ''ALLOW'' NOT NULL)';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE != -1430 THEN
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
CREATE TABLE cb_protected_object (
|
||||
object_id NUMBER PRIMARY KEY,
|
||||
owner VARCHAR2(128) NOT NULL,
|
||||
|
||||
@@ -22,7 +22,8 @@ AS
|
||||
-- no matched stored row rule means no rows are visible.
|
||||
v_user_id NUMBER;
|
||||
v_target VARCHAR2(128);
|
||||
v_predicate VARCHAR2(32767);
|
||||
v_allow_predicate VARCHAR2(32767);
|
||||
v_deny_predicate VARCHAR2(32767);
|
||||
|
||||
FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
|
||||
BEGIN
|
||||
@@ -60,16 +61,22 @@ AS
|
||||
RETURN NULL;
|
||||
END;
|
||||
|
||||
PROCEDURE append_or(p_clause IN VARCHAR2) IS
|
||||
PROCEDURE append_or(p_effect IN VARCHAR2, p_clause IN VARCHAR2) IS
|
||||
BEGIN
|
||||
IF p_clause IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_predicate IS NULL THEN
|
||||
v_predicate := '(' || p_clause || ')';
|
||||
IF p_effect = 'DENY' THEN
|
||||
IF v_deny_predicate IS NULL THEN
|
||||
v_deny_predicate := '(' || p_clause || ')';
|
||||
ELSE
|
||||
v_predicate := v_predicate || ' OR (' || p_clause || ')';
|
||||
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
|
||||
@@ -89,6 +96,7 @@ BEGIN
|
||||
FOR r IN (
|
||||
SELECT UPPER(TRIM(r.rule_type)) AS rule_type,
|
||||
UPPER(TRIM(r.rule_column)) AS rule_column,
|
||||
NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') AS permission_effect,
|
||||
r.rule_value
|
||||
FROM cb_user_role ur
|
||||
JOIN cb_permission p
|
||||
@@ -104,48 +112,58 @@ BEGIN
|
||||
v_column VARCHAR2(128);
|
||||
BEGIN
|
||||
IF r.rule_type = 'ALL' THEN
|
||||
RETURN '1 = 1';
|
||||
IF r.permission_effect = 'DENY' THEN
|
||||
RETURN '1 = 0';
|
||||
END IF;
|
||||
v_allow_predicate := '(1 = 1)';
|
||||
ELSIF r.rule_type = 'MY_DEPT' THEN
|
||||
-- NULL rule_column keeps backward compatibility with the demo seed.
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
|
||||
append_or(r.permission_effect, v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')');
|
||||
END IF;
|
||||
ELSIF r.rule_type = 'SELF' THEN
|
||||
-- NULL rule_column keeps backward compatibility with the demo seed.
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
|
||||
append_or(r.permission_effect, v_column || ' = SYS_CONTEXT(''CB_AGENT_CTX'', ''EMP_NO'')');
|
||||
END IF;
|
||||
ELSIF r.rule_type = 'DEPT' THEN
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'DEPT_CODE'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, v_column || ' = ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
ELSIF r.rule_type = 'EMP_NO' THEN
|
||||
v_column := safe_column(p_schema, p_object, NVL(r.rule_column, 'OWNER_EMP_NO'));
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or(v_column || ' = ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, v_column || ' = ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
ELSIF r.rule_type = '=' THEN
|
||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or('TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, 'TO_CHAR(' || v_column || ') = ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
ELSIF r.rule_type IN ('!=', '<>') THEN
|
||||
v_column := safe_column(p_schema, p_object, r.rule_column);
|
||||
IF v_column IS NOT NULL THEN
|
||||
append_or('TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value));
|
||||
append_or(r.permission_effect, 'TO_CHAR(' || v_column || ') <> ' || quote_literal(r.rule_value));
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
IF v_predicate IS NULL THEN
|
||||
IF v_allow_predicate IS NULL THEN
|
||||
RETURN '1 = 0';
|
||||
END IF;
|
||||
|
||||
RETURN '(' || v_predicate || ')';
|
||||
IF v_deny_predicate IS NULL THEN
|
||||
IF v_allow_predicate = '(1 = 1)' THEN
|
||||
RETURN '1 = 1';
|
||||
END IF;
|
||||
RETURN '(' || v_allow_predicate || ')';
|
||||
END IF;
|
||||
|
||||
RETURN '((' || v_allow_predicate || ') AND NOT (' || v_deny_predicate || '))';
|
||||
END;
|
||||
/
|
||||
|
||||
|
||||
@@ -61,6 +61,12 @@ BEGIN
|
||||
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
|
||||
VALUES (456110, 'rule_test_target_mismatch', 'E456110', 'QA', 'N', 'Y');
|
||||
|
||||
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
|
||||
VALUES (456111, 'rule_test_allow_deny', 'E456111', 'HR', 'N', 'Y');
|
||||
|
||||
INSERT INTO cb_app_user(user_id, user_name, employee_no, dept_code, can_read_contents, active)
|
||||
VALUES (456112, 'rule_test_deny_only', 'E456112', 'HR', 'N', 'Y');
|
||||
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456101, 'RULE_TEST_MY_DEPT');
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456102, 'RULE_TEST_SELF');
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456103, 'RULE_TEST_ALL');
|
||||
@@ -71,6 +77,8 @@ BEGIN
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456108, 'RULE_TEST_OR_INJECTION');
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456109, 'RULE_TEST_COLUMN_INJECTION');
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456110, 'RULE_TEST_TARGET_MISMATCH');
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456111, 'RULE_TEST_ALLOW_DENY');
|
||||
INSERT INTO cb_app_role(role_id, role_name) VALUES (456112, 'RULE_TEST_DENY_ONLY');
|
||||
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456101, 456101);
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456102, 456102);
|
||||
@@ -82,6 +90,8 @@ BEGIN
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456108, 456108);
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456109, 456109);
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456110, 456110);
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456111, 456111);
|
||||
INSERT INTO cb_user_role(user_id, role_id) VALUES (456112, 456112);
|
||||
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
|
||||
VALUES (456101, 456101, 'CB_V_SEARCH_DOCUMENTS', 'SELECT');
|
||||
@@ -113,6 +123,15 @@ BEGIN
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name)
|
||||
VALUES (456110, 456110, 'BOARD_POSTS', 'SELECT');
|
||||
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
||||
VALUES (456111, 456111, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'ALLOW');
|
||||
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
||||
VALUES (456112, 456111, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'DENY');
|
||||
|
||||
INSERT INTO cb_permission(perm_id, role_id, target_name, action_name, permission_effect)
|
||||
VALUES (456113, 456112, 'CB_V_SEARCH_DOCUMENTS', 'SELECT', 'DENY');
|
||||
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (456101, 456101, NULL, 'MY_DEPT', NULL);
|
||||
|
||||
@@ -143,6 +162,15 @@ BEGIN
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (456110, 456110, NULL, 'ALL', NULL);
|
||||
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (456111, 456111, NULL, 'ALL', NULL);
|
||||
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (456112, 456112, 'DEPT_CODE', '=', 'HR');
|
||||
|
||||
INSERT INTO cb_permission_rule(rule_id, perm_id, rule_column, rule_type, rule_value)
|
||||
VALUES (456113, 456113, 'DEPT_CODE', '=', 'HR');
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
@@ -229,6 +257,9 @@ BEGIN
|
||||
assert_equals('COLUMN_INJECTION_DENIES', 456109, '1 = 0');
|
||||
assert_equals_for_object('TARGET_MISMATCH_DENIES', 456110, 'CB_V_SEARCH_DOCUMENTS', '1 = 0');
|
||||
assert_equals_for_object('TARGET_MATCH_ALL_ALLOWS', 456110, 'BOARD_POSTS', '1 = 1');
|
||||
assert_contains('ALLOW_ALL_MINUS_DENY_DEPT', 456111, 'AND NOT');
|
||||
assert_contains('ALLOW_ALL_MINUS_DENY_DEPT_DETAIL', 456111, 'TO_CHAR(DEPT_CODE) = ''HR''');
|
||||
assert_equals('DENY_ONLY_DENIES', 456112, '1 = 0');
|
||||
cb_agent_ctx_pkg.clear_user;
|
||||
END;
|
||||
/
|
||||
|
||||
@@ -7,6 +7,7 @@ public record PermissionSet(
|
||||
long roleId,
|
||||
long objectId,
|
||||
String action,
|
||||
String permissionEffect,
|
||||
List<PermissionRule> rules,
|
||||
List<String> visibleColumns
|
||||
) {
|
||||
|
||||
@@ -9,6 +9,7 @@ public record PermissionSetCommand(
|
||||
@Positive long roleId,
|
||||
@Positive long objectId,
|
||||
@NotBlank String action,
|
||||
String permissionEffect,
|
||||
@NotEmpty List<RuleCommand> rules,
|
||||
List<String> visibleColumns
|
||||
) {
|
||||
|
||||
@@ -7,6 +7,7 @@ public record PermissionView(
|
||||
long objectId,
|
||||
String objectName,
|
||||
String action,
|
||||
String permissionEffect,
|
||||
String rules,
|
||||
String visibleColumns,
|
||||
String filterPreview,
|
||||
|
||||
@@ -36,10 +36,14 @@ public interface PermissionMapper {
|
||||
void insertPermission(@Param("permissionId") long permissionId,
|
||||
@Param("roleId") long roleId,
|
||||
@Param("objectId") long objectId,
|
||||
@Param("action") String action);
|
||||
@Param("action") String action,
|
||||
@Param("permissionEffect") String permissionEffect);
|
||||
|
||||
void updatePermissionAction(@Param("permissionId") long permissionId, @Param("action") String action);
|
||||
|
||||
void updatePermissionEffect(@Param("permissionId") long permissionId,
|
||||
@Param("permissionEffect") String permissionEffect);
|
||||
|
||||
void deleteRules(@Param("permissionId") long permissionId);
|
||||
|
||||
void insertRule(PermissionRule rule);
|
||||
|
||||
@@ -56,9 +56,12 @@ public class BackofficeSchemaService {
|
||||
perm_id NUMBER PRIMARY KEY,
|
||||
role_id NUMBER NOT NULL,
|
||||
target_name VARCHAR2(128) NOT NULL,
|
||||
action_name VARCHAR2(30) DEFAULT 'SELECT' NOT NULL
|
||||
action_name VARCHAR2(30) DEFAULT 'SELECT' NOT NULL,
|
||||
permission_effect VARCHAR2(10) DEFAULT 'ALLOW' NOT NULL
|
||||
)
|
||||
""");
|
||||
addColumn(results, "cb_permission", "permission_effect",
|
||||
"ALTER TABLE cb_permission ADD (permission_effect VARCHAR2(10) DEFAULT 'ALLOW' NOT NULL)");
|
||||
createTable(results, "cb_permission_rule", """
|
||||
CREATE TABLE cb_permission_rule (
|
||||
rule_id NUMBER PRIMARY KEY,
|
||||
|
||||
@@ -22,6 +22,7 @@ public class PermissionService {
|
||||
private static final Set<String> RULE_TYPES = Set.of("ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO");
|
||||
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of("=", "!=", "DEPT", "EMP_NO");
|
||||
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of("MY_DEPT", "SELF", "DEPT", "EMP_NO");
|
||||
private static final Set<String> PERMISSION_EFFECTS = Set.of("ALLOW", "DENY");
|
||||
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
@@ -69,6 +70,7 @@ public class PermissionService {
|
||||
if (!"SELECT".equalsIgnoreCase(command.action())) {
|
||||
throw new AppException("초기 구현에서는 SELECT 권한만 저장할 수 있습니다.");
|
||||
}
|
||||
String permissionEffect = normalizePermissionEffect(command.permissionEffect());
|
||||
AppRole role = permissionMapper.findRole(command.roleId());
|
||||
if (role == null) {
|
||||
throw new AppException("역할을 찾을 수 없습니다.");
|
||||
@@ -80,9 +82,10 @@ public class PermissionService {
|
||||
Long existingId = permissionMapper.findPermissionId(command.roleId(), command.objectId());
|
||||
long permissionId = existingId == null ? permissionMapper.nextPermissionId() : existingId;
|
||||
if (existingId == null) {
|
||||
permissionMapper.insertPermission(permissionId, command.roleId(), command.objectId(), "SELECT");
|
||||
permissionMapper.insertPermission(permissionId, command.roleId(), command.objectId(), "SELECT", permissionEffect);
|
||||
} else {
|
||||
permissionMapper.updatePermissionAction(permissionId, "SELECT");
|
||||
permissionMapper.updatePermissionEffect(permissionId, permissionEffect);
|
||||
}
|
||||
|
||||
permissionMapper.deleteRules(permissionId);
|
||||
@@ -107,7 +110,7 @@ public class PermissionService {
|
||||
"PERMISSION_SAVED", null, command.objectId(), "SUCCESS", null, null,
|
||||
"roleId=" + command.roleId()
|
||||
));
|
||||
return new PermissionSet(permissionId, command.roleId(), command.objectId(), "SELECT", List.of(), List.of());
|
||||
return new PermissionSet(permissionId, command.roleId(), command.objectId(), "SELECT", permissionEffect, List.of(), List.of());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -185,6 +188,14 @@ public class PermissionService {
|
||||
return clean(value).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizePermissionEffect(String value) {
|
||||
String effect = clean(value).isBlank() ? "ALLOW" : normalize(value);
|
||||
if (!PERMISSION_EFFECTS.contains(effect)) {
|
||||
throw new AppException("허용되지 않은 권한 효과입니다: " + effect);
|
||||
}
|
||||
return effect;
|
||||
}
|
||||
|
||||
private String normalizeNullable(String value) {
|
||||
String cleaned = clean(value);
|
||||
return cleaned.isBlank() ? null : cleaned.toUpperCase(Locale.ROOT);
|
||||
|
||||
@@ -102,6 +102,7 @@ public class PermissionController {
|
||||
public String save(
|
||||
@RequestParam long roleId,
|
||||
@RequestParam String objectRef,
|
||||
@RequestParam(defaultValue = "ALLOW") String permissionEffect,
|
||||
@RequestParam(required = false) List<String> ruleColumn,
|
||||
@RequestParam List<String> ruleType,
|
||||
@RequestParam(required = false) List<String> ruleValue,
|
||||
@@ -113,6 +114,7 @@ public class PermissionController {
|
||||
roleId,
|
||||
objectId,
|
||||
"SELECT",
|
||||
permissionEffect,
|
||||
buildRules(ruleColumn, ruleType, ruleValue),
|
||||
splitColumns(visibleColumns)
|
||||
));
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
o.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 ||
|
||||
@@ -74,7 +75,7 @@
|
||||
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
|
||||
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
|
||||
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>
|
||||
|
||||
@@ -104,7 +105,8 @@
|
||||
SELECT p.perm_id AS permission_id,
|
||||
p.role_id,
|
||||
o.object_id,
|
||||
p.action_name AS action
|
||||
p.action_name AS action,
|
||||
NVL(p.permission_effect, 'ALLOW') AS permission_effect
|
||||
FROM cb_permission p
|
||||
JOIN cb_protected_object o ON o.object_name = p.target_name
|
||||
WHERE p.role_id = #{roleId}
|
||||
@@ -120,8 +122,8 @@
|
||||
</select>
|
||||
|
||||
<insert id="insertPermission">
|
||||
INSERT INTO cb_permission (perm_id, role_id, target_name, action_name)
|
||||
SELECT #{permissionId}, #{roleId}, object_name, #{action}
|
||||
INSERT INTO cb_permission (perm_id, role_id, target_name, action_name, permission_effect)
|
||||
SELECT #{permissionId}, #{roleId}, object_name, #{action}, #{permissionEffect}
|
||||
FROM cb_protected_object
|
||||
WHERE object_id = #{objectId}
|
||||
</insert>
|
||||
@@ -132,6 +134,12 @@
|
||||
WHERE perm_id = #{permissionId}
|
||||
</update>
|
||||
|
||||
<update id="updatePermissionEffect">
|
||||
UPDATE cb_permission
|
||||
SET permission_effect = #{permissionEffect}
|
||||
WHERE perm_id = #{permissionId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteRules">
|
||||
DELETE FROM cb_permission_rule
|
||||
WHERE perm_id = #{permissionId}
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
권한 효과
|
||||
<select class="form-select" name="permissionEffect" required>
|
||||
<option value="ALLOW">허용</option>
|
||||
<option value="DENY">거부</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="span-2">
|
||||
<div class="field-block-title">행 규칙</div>
|
||||
<div id="rowRuleList" class="rule-list">
|
||||
@@ -80,6 +87,7 @@
|
||||
<th>역할</th>
|
||||
<th>테이블/뷰</th>
|
||||
<th>Action</th>
|
||||
<th>Effect</th>
|
||||
<th>행 규칙</th>
|
||||
<th>적용 필터</th>
|
||||
<th>NULL 제외 컬럼</th>
|
||||
@@ -92,6 +100,11 @@
|
||||
<td th:text="${permission.roleName()}">HR_DEPT_ROLE</td>
|
||||
<td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
|
||||
<td th:text="${permission.action()}">SELECT</td>
|
||||
<td>
|
||||
<span class="badge"
|
||||
th:classappend="${permission.permissionEffect() == 'DENY'} ? ' text-bg-danger' : ' text-bg-success'"
|
||||
th:text="${permission.permissionEffect()}">ALLOW</span>
|
||||
</td>
|
||||
<td th:text="${permission.rules()} ?: '-'">ALL</td>
|
||||
<td><pre class="table-pre" th:text="${permission.filterPreview()} ?: '-'">ALL ROWS</pre></td>
|
||||
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
|
||||
@@ -104,7 +117,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(permissions)}">
|
||||
<td colspan="8" class="text-muted">등록된 권한이 없습니다.</td>
|
||||
<td colspan="9" class="text-muted">등록된 권한이 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -52,6 +52,7 @@ class PermissionServiceTest {
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(new RuleCommand(null, "ALL", null), new RuleCommand("DEPT_CODE", "=", "APAC")),
|
||||
List.of()
|
||||
);
|
||||
@@ -67,6 +68,7 @@ class PermissionServiceTest {
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(new RuleCommand("DEPT_CODE", "CUSTOM_PREDICATE", "1=1")),
|
||||
List.of()
|
||||
);
|
||||
@@ -82,6 +84,7 @@ class PermissionServiceTest {
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(new RuleCommand(null, "MY_DEPT", null), new RuleCommand(null, "SELF", null)),
|
||||
List.of()
|
||||
);
|
||||
@@ -103,6 +106,7 @@ class PermissionServiceTest {
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(new RuleCommand(null, "DEPT", "HR"), new RuleCommand(null, "EMP_NO", "E2001")),
|
||||
List.of()
|
||||
);
|
||||
@@ -124,6 +128,7 @@ class PermissionServiceTest {
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"ALLOW",
|
||||
List.of(new RuleCommand(null, "DEPT", "")),
|
||||
List.of()
|
||||
);
|
||||
@@ -133,6 +138,23 @@ class PermissionServiceTest {
|
||||
.hasMessageContaining("값이 필요");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsDenyPermissionEffect() {
|
||||
var command = new PermissionSetCommand(
|
||||
10L,
|
||||
1L,
|
||||
"SELECT",
|
||||
"DENY",
|
||||
List.of(new RuleCommand("DEPT_CODE", "=", "HR")),
|
||||
List.of()
|
||||
);
|
||||
|
||||
permissionService.savePermissionSet(command);
|
||||
|
||||
FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper;
|
||||
assertThat(mapper.insertedEffect).isEqualTo("DENY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void disablesProtectedObjectWhenLastPermissionIsDeleted() {
|
||||
var mapper = new FakePermissionMapper();
|
||||
@@ -209,14 +231,22 @@ class PermissionServiceTest {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private String insertedEffect;
|
||||
|
||||
@Override
|
||||
public void insertPermission(long permissionId, long roleId, long objectId, String action) {
|
||||
public void insertPermission(long permissionId, long roleId, long objectId, String action, String permissionEffect) {
|
||||
insertedEffect = permissionEffect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePermissionAction(long permissionId, String action) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePermissionEffect(long permissionId, String permissionEffect) {
|
||||
insertedEffect = permissionEffect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRules(long permissionId) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user