feat #466: add deny permission effect

This commit is contained in:
devmrko
2026-06-25 22:24:18 +09:00
parent 1f924f67de
commit d6c7533edd
15 changed files with 224 additions and 42 deletions

View 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`이다.

View File

@@ -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_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')';
END IF;
ELSIF v_allow_predicate IS NULL THEN
v_allow_predicate := '(' || p_clause || ')';
ELSE
v_predicate := v_predicate || ' OR (' || p_clause || ')';
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;
/

View File

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

View File

@@ -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_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')';
END IF;
ELSIF v_allow_predicate IS NULL THEN
v_allow_predicate := '(' || p_clause || ')';
ELSE
v_predicate := v_predicate || ' OR (' || p_clause || ')';
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;
/

View File

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

View File

@@ -7,6 +7,7 @@ public record PermissionSet(
long roleId,
long objectId,
String action,
String permissionEffect,
List<PermissionRule> rules,
List<String> visibleColumns
) {

View File

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

View File

@@ -7,6 +7,7 @@ public record PermissionView(
long objectId,
String objectName,
String action,
String permissionEffect,
String rules,
String visibleColumns,
String filterPreview,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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