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

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