[Developer] #424 hide orphan protected objects in permission picker

This commit is contained in:
devmrko
2026-06-25 14:11:13 +09:00
parent 0a48273b6d
commit 30bd12c9a5
8 changed files with 73 additions and 1 deletions

View File

@@ -29,6 +29,10 @@ public interface PermissionMapper {
Long findPermissionId(@Param("roleId") long roleId, @Param("objectId") long objectId); Long findPermissionId(@Param("roleId") long roleId, @Param("objectId") long objectId);
Long findObjectIdByPermissionId(@Param("permissionId") long permissionId);
int countPermissionsByObjectId(@Param("objectId") long objectId);
void insertPermission(@Param("permissionId") long permissionId, void insertPermission(@Param("permissionId") long permissionId,
@Param("roleId") long roleId, @Param("roleId") long roleId,
@Param("objectId") long objectId, @Param("objectId") long objectId,

View File

@@ -13,6 +13,8 @@ public interface ProtectedObjectMapper {
List<ProtectedObject> findEnabled(); List<ProtectedObject> findEnabled();
List<ProtectedObject> findEnabledWithPermissions();
ProtectedObject findById(@Param("objectId") long objectId); ProtectedObject findById(@Param("objectId") long objectId);
ProtectedObject findByOwnerAndName(@Param("owner") String owner, @Param("objectName") String objectName); ProtectedObject findByOwnerAndName(@Param("owner") String owner, @Param("objectName") String objectName);

View File

@@ -110,12 +110,16 @@ public class PermissionService {
@Transactional @Transactional
public void deletePermission(long permissionId) { public void deletePermission(long permissionId) {
Long objectId = permissionMapper.findObjectIdByPermissionId(permissionId);
permissionMapper.deleteRules(permissionId); permissionMapper.deleteRules(permissionId);
permissionMapper.deleteVisibleColumns(permissionId); permissionMapper.deleteVisibleColumns(permissionId);
int deleted = permissionMapper.deletePermission(permissionId); int deleted = permissionMapper.deletePermission(permissionId);
if (deleted == 0) { if (deleted == 0) {
throw new AppException("삭제할 권한을 찾을 수 없습니다."); throw new AppException("삭제할 권한을 찾을 수 없습니다.");
} }
if (objectId != null && permissionMapper.countPermissionsByObjectId(objectId) == 0) {
protectedObjectService.disableObject(objectId);
}
auditService.record(new AuditEvent("PERMISSION_DELETED", null, null, "SUCCESS", null, null, auditService.record(new AuditEvent("PERMISSION_DELETED", null, null, "SUCCESS", null, null,
"permissionId=" + permissionId)); "permissionId=" + permissionId));
} }

View File

@@ -35,6 +35,10 @@ public class ProtectedObjectService {
return mapper.findEnabled(); return mapper.findEnabled();
} }
public List<ProtectedObject> findEnabledWithPermissions() {
return mapper.findEnabledWithPermissions();
}
public List<DatabaseObjectOption> findDatabaseObjects() { public List<DatabaseObjectOption> findDatabaseObjects() {
CacheEntry<List<DatabaseObjectOption>> cached = databaseObjectsCache; CacheEntry<List<DatabaseObjectOption>> cached = databaseObjectsCache;
if (cached != null && !cached.expired()) { if (cached != null && !cached.expired()) {
@@ -83,6 +87,7 @@ public class ProtectedObjectService {
for (String column : splitCsv(normalized.columns())) { for (String column : splitCsv(normalized.columns())) {
mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N"); mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N");
} }
databaseObjectsCache = null;
protectedColumnsCache.remove(objectId); protectedColumnsCache.remove(objectId);
auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null, auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null,
normalized.objectName())); normalized.objectName()));
@@ -96,6 +101,8 @@ public class ProtectedObjectService {
if (existing != null) { if (existing != null) {
if (!existing.enabled()) { if (!existing.enabled()) {
mapper.enableObject(existing.objectId()); mapper.enableObject(existing.objectId());
databaseObjectsCache = null;
protectedColumnsCache.remove(existing.objectId());
auditService.record(new AuditEvent("PROTECTED_OBJECT_RE_ENABLED", null, existing.objectId(), "SUCCESS", null, auditService.record(new AuditEvent("PROTECTED_OBJECT_RE_ENABLED", null, existing.objectId(), "SUCCESS", null,
null, existing.displayName())); null, existing.displayName()));
return mapper.findById(existing.objectId()); return mapper.findById(existing.objectId());
@@ -119,6 +126,7 @@ public class ProtectedObjectService {
for (String column : columns) { for (String column : columns) {
mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N"); mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N");
} }
databaseObjectsCache = null;
protectedColumnsCache.remove(objectId); protectedColumnsCache.remove(objectId);
auditService.record(new AuditEvent("PROTECTED_OBJECT_AUTO_CREATED", null, objectId, "SUCCESS", null, null, auditService.record(new AuditEvent("PROTECTED_OBJECT_AUTO_CREATED", null, objectId, "SUCCESS", null, null,
command.objectName())); command.objectName()));
@@ -162,6 +170,8 @@ public class ProtectedObjectService {
if (updated == 0) { if (updated == 0) {
throw new AppException("보호 객체를 찾을 수 없습니다."); throw new AppException("보호 객체를 찾을 수 없습니다.");
} }
databaseObjectsCache = null;
protectedColumnsCache.remove(objectId);
auditService.record(new AuditEvent("PROTECTED_OBJECT_DISABLED", null, objectId, "SUCCESS", null, null, null)); auditService.record(new AuditEvent("PROTECTED_OBJECT_DISABLED", null, objectId, "SUCCESS", null, null, null));
} }

View File

@@ -63,7 +63,7 @@ public class PermissionController {
@GetMapping("/permissions") @GetMapping("/permissions")
public String permissions(Model model) { public String permissions(Model model) {
long started = System.nanoTime(); long started = System.nanoTime();
var objects = protectedObjectService.findEnabled(); var objects = protectedObjectService.findEnabledWithPermissions();
long objectsAt = System.nanoTime(); long objectsAt = System.nanoTime();
var roles = permissionService.findRoles(); var roles = permissionService.findRoles();
long rolesAt = System.nanoTime(); long rolesAt = System.nanoTime();

View File

@@ -84,6 +84,20 @@
AND o.object_id = #{objectId} AND o.object_id = #{objectId}
</select> </select>
<select id="findObjectIdByPermissionId" resultType="long">
SELECT o.object_id
FROM cb_permission p
JOIN cb_protected_object o ON o.object_name = p.target_name
WHERE p.perm_id = #{permissionId}
</select>
<select id="countPermissionsByObjectId" resultType="int">
SELECT COUNT(*)
FROM cb_permission p
JOIN cb_protected_object o ON o.object_name = p.target_name
WHERE o.object_id = #{objectId}
</select>
<select id="findPermissionSet" resultType="com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet"> <select id="findPermissionSet" resultType="com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet">
SELECT p.perm_id AS permission_id, SELECT p.perm_id AS permission_id,
p.role_id, p.role_id,

View File

@@ -9,6 +9,14 @@
ORDER BY owner, object_name ORDER BY owner, object_name
</select> </select>
<select id="findEnabledWithPermissions" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject">
SELECT DISTINCT po.object_id, po.owner, po.object_name, po.ords_path, po.enabled_yn
FROM cb_protected_object po
JOIN cb_permission p ON p.target_name = po.object_name
WHERE po.enabled_yn = 'Y'
ORDER BY po.owner, po.object_name
</select>
<select id="findById" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject"> <select id="findById" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject">
SELECT object_id, owner, object_name, ords_path, enabled_yn SELECT object_id, owner, object_name, ords_path, enabled_yn
FROM cb_protected_object FROM cb_protected_object
@@ -31,6 +39,7 @@
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 SELECT 1
FROM cb_protected_object po FROM cb_protected_object po
JOIN cb_permission p ON p.target_name = po.object_name
WHERE po.owner = all_objects.owner WHERE po.owner = all_objects.owner
AND po.object_name = all_objects.object_name AND po.object_name = all_objects.object_name
AND po.enabled_yn = 'Y' AND po.enabled_yn = 'Y'

View File

@@ -1,5 +1,6 @@
package com.cloudhandson.vpdbackoffice.service; package com.cloudhandson.vpdbackoffice.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent; import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
@@ -74,6 +75,24 @@ class PermissionServiceTest {
.hasMessageContaining("허용되지 않은"); .hasMessageContaining("허용되지 않은");
} }
@Test
void disablesProtectedObjectWhenLastPermissionIsDeleted() {
var mapper = new FakePermissionMapper();
boolean[] disabled = {false};
AuditService auditService = new AuditService(new NoopAuditMapper());
ProtectedObjectService objectService = new ProtectedObjectService(null, auditService) {
@Override
public void disableObject(long objectId) {
disabled[0] = objectId == 1L;
}
};
PermissionService service = new PermissionService(mapper, objectService, auditService);
service.deletePermission(1000L);
assertThat(disabled[0]).isTrue();
}
private static class NoopAuditMapper implements AuditMapper { private static class NoopAuditMapper implements AuditMapper {
@Override @Override
public void insert(AuditEvent event) { public void insert(AuditEvent event) {
@@ -120,6 +139,16 @@ class PermissionServiceTest {
return null; return null;
} }
@Override
public Long findObjectIdByPermissionId(long permissionId) {
return permissionId == 1000L ? 1L : null;
}
@Override
public int countPermissionsByObjectId(long objectId) {
return 0;
}
@Override @Override
public void insertPermission(long permissionId, long roleId, long objectId, String action) { public void insertPermission(long permissionId, long roleId, long objectId, String action) {
} }