From c8326f954dc09282668be233e3ea79fc4fe33d69 Mon Sep 17 00:00:00 2001 From: devmrko Date: Fri, 26 Jun 2026 14:14:22 +0900 Subject: [PATCH] fix #493: add effective access matrix --- .../493-effective-access-matrix/README.md | 54 ++++ .../domain/effective/EffectiveMatrixView.java | 11 + .../effective/GroupEffectiveAccessView.java | 15 + .../effective/RoleEffectiveImpactView.java | 16 + .../effective/UserEffectiveAccessView.java | 18 ++ .../service/EffectiveMatrixService.java | 296 ++++++++++++++++++ .../web/EffectiveMatrixController.java | 31 ++ src/main/resources/static/css/app.css | 51 +++ src/main/resources/static/js/app.js | 33 ++ .../resources/templates/effective-matrix.html | 201 ++++++++++++ .../resources/templates/fragments/layout.html | 1 + .../service/EffectiveMatrixServiceTest.java | 78 +++++ 12 files changed, 805 insertions(+) create mode 100644 docs/design/493-effective-access-matrix/README.md create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/EffectiveMatrixView.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/GroupEffectiveAccessView.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/RoleEffectiveImpactView.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/UserEffectiveAccessView.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixService.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixController.java create mode 100644 src/main/resources/templates/effective-matrix.html create mode 100644 src/test/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixServiceTest.java diff --git a/docs/design/493-effective-access-matrix/README.md b/docs/design/493-effective-access-matrix/README.md new file mode 100644 index 0000000..31c3597 --- /dev/null +++ b/docs/design/493-effective-access-matrix/README.md @@ -0,0 +1,54 @@ +# Redmine #493 - 그룹/사용자/역할 Effective Matrix 설계 + +## 프로젝트 개요 + +VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이블로 제어하기 위한 Spring Boot 관리 도구다. 백오피스 사용자는 DB schema user가 아니라 Bearer Token으로 식별되는 애플리케이션 사용자이며, ORDS handler가 사용자 컨텍스트를 설정하면 VPD policy function이 사용자/그룹/역할/권한 테이블을 기준으로 행과 컬럼 접근을 제한한다. + +## 목표 + +운영자가 사용자, 그룹, 역할 사이의 권한 상속 결과를 한 화면에서 확인할 수 있게 한다. + +## 문제 + +- 사용자 화면은 직접 역할만 보여준다. +- 그룹 화면은 그룹 사용자와 그룹 역할이 분리되어 있어 상속 결과를 한 번에 보기 어렵다. +- 역할 화면은 역할이 어느 사용자/그룹에 영향을 주는지 보이지 않는다. +- 신규 운영자가 DB user와 백오피스 application user의 차이를 혼동할 수 있다. + +## 설계 + +- `권한 관리 > 유효 권한` 메뉴와 `/effective-matrix` 화면을 추가한다. +- 신규 DB 테이블 없이 기존 조회 결과를 서비스에서 집계한다. +- 사용자 기준: + - 직접 역할 + - 소속 그룹 + - 그룹 상속 역할 + - 최종 effective roles + - 연결 권한 수와 보호 객체 목록 +- 그룹 기준: + - 포함 사용자 + - 그룹에 부여된 역할 + - 그룹 역할이 제공하는 권한 수와 보호 객체 목록 +- 역할 기준: + - 직접 부여 사용자 + - 역할이 부여된 그룹 + - 그룹을 통해 영향을 받는 사용자 + - 연결 권한 수와 보호 객체 목록 +- 각 기준에는 select 필터를 둬서 한 항목만 빠르게 확인할 수 있게 한다. +- 빈 상태에는 등록 순서를 안내한다. + +## 완료 기준 + +- `/effective-matrix`에서 사용자 선택 시 직접 역할과 그룹 상속 역할이 분리되어 보인다. +- 그룹 선택 시 포함 사용자, 부여 역할, 연결 권한 수가 한 화면에 보인다. +- 역할 선택 시 직접 사용자, 연결 그룹, 그룹 상속 사용자가 보인다. +- 메뉴에서 접근할 수 있다. +- 모바일 390px에서 가로 overflow가 없다. + +## 검증 + +- `mvn test` +- Playwright: + - `/effective-matrix` desktop/mobile 렌더링 + - 사용자/그룹/역할 select 변경 시 table filter 동작 + - 모바일 overflow 없음 diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/EffectiveMatrixView.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/EffectiveMatrixView.java new file mode 100644 index 0000000..f3531fc --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/EffectiveMatrixView.java @@ -0,0 +1,11 @@ +package com.cloudhandson.vpdbackoffice.domain.effective; + +import java.util.List; + +public record EffectiveMatrixView( + List users, + List groups, + List roles, + int permissionCount +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/GroupEffectiveAccessView.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/GroupEffectiveAccessView.java new file mode 100644 index 0000000..6f1cb40 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/GroupEffectiveAccessView.java @@ -0,0 +1,15 @@ +package com.cloudhandson.vpdbackoffice.domain.effective; + +import java.util.List; + +public record GroupEffectiveAccessView( + long groupId, + String groupCode, + String groupName, + boolean active, + List users, + List roles, + int permissionCount, + List objectNames +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/RoleEffectiveImpactView.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/RoleEffectiveImpactView.java new file mode 100644 index 0000000..24c55d2 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/RoleEffectiveImpactView.java @@ -0,0 +1,16 @@ +package com.cloudhandson.vpdbackoffice.domain.effective; + +import java.util.List; + +public record RoleEffectiveImpactView( + long roleId, + String roleName, + String maxSensitivityLevel, + List directUsers, + List groups, + List inheritedUsers, + List affectedUsers, + int permissionCount, + List objectNames +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/UserEffectiveAccessView.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/UserEffectiveAccessView.java new file mode 100644 index 0000000..fd5f4a1 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/effective/UserEffectiveAccessView.java @@ -0,0 +1,18 @@ +package com.cloudhandson.vpdbackoffice.domain.effective; + +import java.util.List; + +public record UserEffectiveAccessView( + long userId, + String username, + String empNo, + String deptCode, + boolean active, + List directRoles, + List groups, + List inheritedRoles, + List effectiveRoles, + int permissionCount, + List objectNames +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixService.java new file mode 100644 index 0000000..bbb691d --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixService.java @@ -0,0 +1,296 @@ +package com.cloudhandson.vpdbackoffice.service; + +import com.cloudhandson.vpdbackoffice.domain.effective.EffectiveMatrixView; +import com.cloudhandson.vpdbackoffice.domain.effective.GroupEffectiveAccessView; +import com.cloudhandson.vpdbackoffice.domain.effective.RoleEffectiveImpactView; +import com.cloudhandson.vpdbackoffice.domain.effective.UserEffectiveAccessView; +import com.cloudhandson.vpdbackoffice.domain.group.AppGroup; +import com.cloudhandson.vpdbackoffice.domain.group.GroupRoleView; +import com.cloudhandson.vpdbackoffice.domain.group.GroupUserView; +import com.cloudhandson.vpdbackoffice.domain.permission.AppRole; +import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView; +import com.cloudhandson.vpdbackoffice.domain.user.AppUser; +import com.cloudhandson.vpdbackoffice.domain.user.UserRoleView; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.stereotype.Service; + +@Service +public class EffectiveMatrixService { + + private final UserService userService; + private final GroupService groupService; + private final PermissionService permissionService; + + public EffectiveMatrixService( + UserService userService, + GroupService groupService, + PermissionService permissionService + ) { + this.userService = userService; + this.groupService = groupService; + this.permissionService = permissionService; + } + + public EffectiveMatrixView matrix() { + List users = userService.findAll(); + List groups = groupService.findAll(); + List roles = permissionService.findRoles(); + List userRoles = userService.findUserRoles(); + List groupUsers = groupService.findGroupUsers(); + List groupRoles = groupService.findGroupRoles(); + List permissions = permissionService.findPermissionViews(); + + Map> directRoleIdsByUser = directRoleIdsByUser(userRoles); + Map> directRoleNamesByUser = directRoleNamesByUser(userRoles); + Map> groupIdsByUser = groupIdsByUser(groupUsers); + Map> groupNamesByUser = groupNamesByUser(groupUsers); + Map> userNamesByGroup = userNamesByGroup(groupUsers); + Map> roleIdsByGroup = roleIdsByGroup(groupRoles); + Map> roleNamesByGroup = roleNamesByGroup(groupRoles); + Map> groupNamesByRole = groupNamesByRole(groupRoles); + Map> directUserNamesByRole = directUserNamesByRole(userRoles); + Map> permissionsByRole = permissionsByRole(permissions); + Map roleNameById = roleNameById(roles); + + List userViews = users.stream() + .map(user -> userView( + user, + directRoleIdsByUser, + directRoleNamesByUser, + groupIdsByUser, + groupNamesByUser, + roleIdsByGroup, + roleNameById, + permissionsByRole + )) + .toList(); + + List groupViews = groups.stream() + .map(group -> groupView(group, userNamesByGroup, roleIdsByGroup, roleNamesByGroup, permissionsByRole)) + .toList(); + + List roleViews = roles.stream() + .map(role -> roleView(role, directUserNamesByRole, groupNamesByRole, groupUsers, groupRoles, permissionsByRole)) + .toList(); + + return new EffectiveMatrixView(userViews, groupViews, roleViews, permissions.size()); + } + + private UserEffectiveAccessView userView( + AppUser user, + Map> directRoleIdsByUser, + Map> directRoleNamesByUser, + Map> groupIdsByUser, + Map> groupNamesByUser, + Map> roleIdsByGroup, + Map roleNameById, + Map> permissionsByRole + ) { + Set directRoleIds = directRoleIdsByUser.getOrDefault(user.userId(), Set.of()); + Set inheritedRoleIds = new LinkedHashSet<>(); + for (Long groupId : groupIdsByUser.getOrDefault(user.userId(), Set.of())) { + inheritedRoleIds.addAll(roleIdsByGroup.getOrDefault(groupId, Set.of())); + } + + Set effectiveRoleIds = new LinkedHashSet<>(directRoleIds); + effectiveRoleIds.addAll(inheritedRoleIds); + + return new UserEffectiveAccessView( + user.userId(), + user.username(), + user.empNo(), + user.deptCode(), + user.active(), + directRoleNamesByUser.getOrDefault(user.userId(), List.of()), + groupNamesByUser.getOrDefault(user.userId(), List.of()), + names(inheritedRoleIds, roleNameById), + names(effectiveRoleIds, roleNameById), + permissionCount(effectiveRoleIds, permissionsByRole), + objectNames(effectiveRoleIds, permissionsByRole) + ); + } + + private GroupEffectiveAccessView groupView( + AppGroup group, + Map> userNamesByGroup, + Map> roleIdsByGroup, + Map> roleNamesByGroup, + Map> permissionsByRole + ) { + Set roleIds = roleIdsByGroup.getOrDefault(group.groupId(), Set.of()); + return new GroupEffectiveAccessView( + group.groupId(), + group.groupCode(), + group.groupName(), + group.active(), + userNamesByGroup.getOrDefault(group.groupId(), List.of()), + roleNamesByGroup.getOrDefault(group.groupId(), List.of()), + permissionCount(roleIds, permissionsByRole), + objectNames(roleIds, permissionsByRole) + ); + } + + private RoleEffectiveImpactView roleView( + AppRole role, + Map> directUserNamesByRole, + Map> groupNamesByRole, + List groupUsers, + List groupRoles, + Map> permissionsByRole + ) { + Set roleIds = Set.of(role.roleId()); + Set inheritedUsers = new LinkedHashSet<>(); + Set roleGroupIds = new LinkedHashSet<>(); + for (GroupRoleView groupRole : groupRoles) { + if (groupRole.roleId() == role.roleId()) { + roleGroupIds.add(groupRole.groupId()); + } + } + for (GroupUserView groupUser : groupUsers) { + if (roleGroupIds.contains(groupUser.groupId())) { + inheritedUsers.add(groupUser.username()); + } + } + + Set affectedUsers = new LinkedHashSet<>(directUserNamesByRole.getOrDefault(role.roleId(), List.of())); + affectedUsers.addAll(inheritedUsers); + + return new RoleEffectiveImpactView( + role.roleId(), + role.roleName(), + role.maxSensitivityLevel(), + directUserNamesByRole.getOrDefault(role.roleId(), List.of()), + groupNamesByRole.getOrDefault(role.roleId(), List.of()), + new ArrayList<>(inheritedUsers), + new ArrayList<>(affectedUsers), + permissionCount(roleIds, permissionsByRole), + objectNames(roleIds, permissionsByRole) + ); + } + + private Map> directRoleIdsByUser(List userRoles) { + Map> result = new LinkedHashMap<>(); + for (UserRoleView view : userRoles) { + result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.roleId()); + } + return result; + } + + private Map> directRoleNamesByUser(List userRoles) { + Map> result = new LinkedHashMap<>(); + for (UserRoleView view : userRoles) { + result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.roleName()); + } + return toListMap(result); + } + + private Map> groupIdsByUser(List groupUsers) { + Map> result = new LinkedHashMap<>(); + for (GroupUserView view : groupUsers) { + result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.groupId()); + } + return result; + } + + private Map> groupNamesByUser(List groupUsers) { + Map> result = new LinkedHashMap<>(); + for (GroupUserView view : groupUsers) { + result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(groupLabel(view.groupCode(), view.groupName())); + } + return toListMap(result); + } + + private Map> userNamesByGroup(List groupUsers) { + Map> result = new LinkedHashMap<>(); + for (GroupUserView view : groupUsers) { + result.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.username()); + } + return toListMap(result); + } + + private Map> roleIdsByGroup(List groupRoles) { + Map> result = new LinkedHashMap<>(); + for (GroupRoleView view : groupRoles) { + result.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.roleId()); + } + return result; + } + + private Map> roleNamesByGroup(List groupRoles) { + Map> result = new LinkedHashMap<>(); + for (GroupRoleView view : groupRoles) { + result.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.roleName()); + } + return toListMap(result); + } + + private Map> groupNamesByRole(List groupRoles) { + Map> result = new LinkedHashMap<>(); + for (GroupRoleView view : groupRoles) { + result.computeIfAbsent(view.roleId(), key -> new LinkedHashSet<>()).add(groupLabel(view.groupCode(), view.groupName())); + } + return toListMap(result); + } + + private Map> directUserNamesByRole(List userRoles) { + Map> result = new LinkedHashMap<>(); + for (UserRoleView view : userRoles) { + result.computeIfAbsent(view.roleId(), key -> new LinkedHashSet<>()).add(view.username()); + } + return toListMap(result); + } + + private Map> permissionsByRole(List permissions) { + Map> result = new LinkedHashMap<>(); + for (PermissionView permission : permissions) { + result.computeIfAbsent(permission.roleId(), key -> new ArrayList<>()).add(permission); + } + return result; + } + + private Map roleNameById(List roles) { + Map result = new LinkedHashMap<>(); + for (AppRole role : roles) { + result.put(role.roleId(), role.roleName()); + } + return result; + } + + private List names(Set ids, Map nameById) { + return ids.stream() + .map(nameById::get) + .filter(name -> name != null && !name.isBlank()) + .toList(); + } + + private int permissionCount(Set roleIds, Map> permissionsByRole) { + return roleIds.stream() + .mapToInt(roleId -> permissionsByRole.getOrDefault(roleId, List.of()).size()) + .sum(); + } + + private List objectNames(Set roleIds, Map> permissionsByRole) { + Set result = new LinkedHashSet<>(); + for (Long roleId : roleIds) { + for (PermissionView permission : permissionsByRole.getOrDefault(roleId, List.of())) { + result.add(permission.objectName() + " / " + permission.permissionEffect()); + } + } + return new ArrayList<>(result); + } + + private Map> toListMap(Map> source) { + Map> result = new LinkedHashMap<>(); + source.forEach((key, value) -> result.put(key, new ArrayList<>(value))); + return result; + } + + private String groupLabel(String groupCode, String groupName) { + return groupCode + " / " + groupName; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixController.java new file mode 100644 index 0000000..0750a41 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixController.java @@ -0,0 +1,31 @@ +package com.cloudhandson.vpdbackoffice.web; + +import com.cloudhandson.vpdbackoffice.domain.effective.EffectiveMatrixView; +import com.cloudhandson.vpdbackoffice.service.EffectiveMatrixService; +import java.util.List; +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class EffectiveMatrixController { + + private final EffectiveMatrixService matrixService; + + public EffectiveMatrixController(EffectiveMatrixService matrixService) { + this.matrixService = matrixService; + } + + @GetMapping("/effective-matrix") + public String matrix(Model model) { + try { + model.addAttribute("matrix", matrixService.matrix()); + } catch (DataAccessException e) { + RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(e); + model.addAttribute("matrix", new EffectiveMatrixView(List.of(), List.of(), List.of(), 0)); + model.addAttribute("runtimeError", message); + } + return "effective-matrix"; + } +} diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index 3c7589b..66f03b2 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -587,6 +587,57 @@ body { margin-bottom: 0; } +.matrix-summary { + display: grid; + gap: .75rem; + grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr)); + margin-bottom: 1rem; +} + +.matrix-summary > div { + background: var(--rw-surface-muted); + border: 1px solid var(--rw-border); + border-radius: 8px; + padding: .85rem; +} + +.matrix-summary span { + color: var(--rw-muted); + display: block; + font-size: .78rem; + font-weight: 800; + text-transform: uppercase; +} + +.matrix-summary strong { + display: block; + font-size: 1.6rem; + line-height: 1.1; + margin-top: .25rem; +} + +.setup-steps { + color: var(--rw-muted); + margin: 0; + padding-left: 1.25rem; +} + +.setup-steps li + li { + margin-top: .25rem; +} + +.matrix-filter { + color: var(--rw-muted); + font-size: .82rem; + font-weight: 700; + min-width: min(280px, 100%); +} + +.matrix-list { + max-width: 28rem; + overflow-wrap: anywhere; +} + .policy-apply-flow { align-items: center; display: flex; diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js index c76a12e..18b92de 100644 --- a/src/main/resources/static/js/app.js +++ b/src/main/resources/static/js/app.js @@ -486,6 +486,38 @@ function initTokenContextPreviews() { }); } +function filterEffectiveMatrixTable(select) { + const table = document.getElementById(select.dataset.effectiveFilter || ''); + if (!table) { + return; + } + const value = select.value || ''; + table.querySelectorAll('.effective-empty-row').forEach((row) => row.remove()); + let visibleRows = 0; + table.querySelectorAll('tbody tr[data-effective-id]').forEach((row) => { + const visible = !value || row.dataset.effectiveId === value; + row.hidden = !visible; + if (visible) { + visibleRows += 1; + } + }); + if (value && visibleRows === 0) { + const body = table.querySelector('tbody'); + const columnCount = table.querySelectorAll('thead th').length || 1; + const row = document.createElement('tr'); + row.className = 'effective-empty-row'; + row.innerHTML = `${table.dataset.emptyMessage || '선택한 항목이 없습니다.'}`; + body?.appendChild(row); + } +} + +function initEffectiveMatrixFilters() { + document.querySelectorAll('[data-effective-filter]').forEach((select) => { + select.addEventListener('change', () => filterEffectiveMatrixTable(select)); + filterEffectiveMatrixTable(select); + }); +} + function sqlLiteral(value) { return `'${String(value || '').replaceAll("'", "''")}'`; } @@ -791,6 +823,7 @@ document.addEventListener('DOMContentLoaded', () => { filterGroupDetail(select.id); }); initTokenContextPreviews(); + initEffectiveMatrixFilters(); const objectSelect = document.querySelector('select[name="objectRef"]'); if (objectSelect) { objectSelect.addEventListener('change', () => { diff --git a/src/main/resources/templates/effective-matrix.html b/src/main/resources/templates/effective-matrix.html new file mode 100644 index 0000000..471dd9b --- /dev/null +++ b/src/main/resources/templates/effective-matrix.html @@ -0,0 +1,201 @@ + + + + + +
+
+

유효 권한 매트릭스

+

백오피스 사용자, 그룹, 역할, 권한의 최종 상속 결과를 확인합니다.

+
+ +
+ DB 연결 설정이 필요합니다. + message +
+ ./run.sh backoffice-support +
+
+ +
+ +
+
+
+

권한 해석 기준

+

여기의 사용자는 Oracle DB schema user가 아니라 Bearer Token으로 식별되는 백오피스 application user입니다.

+
+ 권한 관리로 이동 +
+
+
+ 사용자 + 0 +
+
+ 그룹 + 0 +
+
+ 역할 + 0 +
+
+ 권한 + 0 +
+
+
    +
  1. 사용자 관리에서 application user를 등록합니다.
  2. +
  3. 그룹 관리에서 사용자를 그룹에 넣고 그룹에 역할을 부여합니다.
  4. +
  5. 권한 관리에서 역할에 TABLE/VIEW 행 규칙과 컬럼 NULL 정책을 부여합니다.
  6. +
  7. 토큰을 발급하면 ORDS handler가 token user context를 설정하고 VPD가 이 권한을 적용합니다.
  8. +
+
+ +
+
+
+

사용자 기준

+

직접 역할과 그룹을 통해 상속된 역할을 분리해서 봅니다.

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
사용자직접 역할소속 그룹그룹 상속 역할최종 역할권한보호 객체
+ agent_hr +
E100 / HR
+ ACTIVE +
----0objects
등록된 사용자가 없습니다. 사용자 관리에서 application user를 먼저 등록하세요.
+
+
+ +
+
+
+

그룹 기준

+

그룹에 속한 사용자와 그룹에 부여된 역할이 제공하는 권한을 봅니다.

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
그룹포함 사용자부여 역할권한보호 객체
+ GROUP +
그룹명
+ ACTIVE +
usersroles0objects
등록된 그룹이 없습니다. 그룹 관리에서 그룹을 먼저 생성하세요.
+
+
+ +
+
+
+

역할 기준

+

역할이 직접 사용자와 그룹 사용자에게 어떤 영향을 주는지 봅니다.

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
역할직접 사용자연결 그룹그룹 상속 사용자영향 사용자권한보호 객체
+ ROLE +
민감도 PUBLIC
+
usersgroupsgroup usersaffected0objects
등록된 역할이 없습니다. 역할 관리에서 역할을 먼저 생성하세요.
+
+
+
+ + diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index f157a56..2bc51e7 100644 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -21,6 +21,7 @@ 사용자 그룹 역할 + 유효 권한 권한 토큰 diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixServiceTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixServiceTest.java new file mode 100644 index 0000000..0884107 --- /dev/null +++ b/src/test/java/com/cloudhandson/vpdbackoffice/service/EffectiveMatrixServiceTest.java @@ -0,0 +1,78 @@ +package com.cloudhandson.vpdbackoffice.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.cloudhandson.vpdbackoffice.domain.effective.EffectiveMatrixView; +import com.cloudhandson.vpdbackoffice.domain.group.AppGroup; +import com.cloudhandson.vpdbackoffice.domain.group.GroupRoleView; +import com.cloudhandson.vpdbackoffice.domain.group.GroupUserView; +import com.cloudhandson.vpdbackoffice.domain.permission.AppRole; +import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView; +import com.cloudhandson.vpdbackoffice.domain.user.AppUser; +import com.cloudhandson.vpdbackoffice.domain.user.UserRoleView; +import java.util.List; +import org.junit.jupiter.api.Test; + +class EffectiveMatrixServiceTest { + + @Test + void combinesDirectAndGroupInheritedRoles() { + EffectiveMatrixService service = new EffectiveMatrixService( + new UserService(null, null) { + @Override + public List findAll() { + return List.of(new AppUser(1L, "agent_ops", "E100", "OPS", "N", "Y")); + } + + @Override + public List findUserRoles() { + return List.of(new UserRoleView(1L, "agent_ops", 10L, "DIRECT_ROLE")); + } + }, + new GroupService(null, null) { + @Override + public List findAll() { + return List.of(new AppGroup(100L, "OPS_GROUP", "운영그룹", null, "Y")); + } + + @Override + public List findGroupUsers() { + return List.of(new GroupUserView(100L, "OPS_GROUP", "운영그룹", 1L, "agent_ops")); + } + + @Override + public List findGroupRoles() { + return List.of(new GroupRoleView(100L, "OPS_GROUP", "운영그룹", 20L, "GROUP_ROLE")); + } + }, + new PermissionService(null, null, null) { + @Override + public List findRoles() { + return List.of( + new AppRole(10L, "DIRECT_ROLE", null, "PUBLIC"), + new AppRole(20L, "GROUP_ROLE", null, "INTERNAL") + ); + } + + @Override + public List findPermissionViews() { + return List.of( + new PermissionView(1000L, 10L, "DIRECT_ROLE", 1L, "BOARD_POSTS", "SELECT", "ALLOW", "ALL", null, "ALL ROWS", "모든 민감 컬럼 NULL 처리"), + new PermissionView(1001L, 20L, "GROUP_ROLE", 2L, "BOARD_ASSIGNMENTS", "SELECT", "ALLOW", "ALL", null, "ALL ROWS", "모든 민감 컬럼 NULL 처리") + ); + } + } + ); + + EffectiveMatrixView matrix = service.matrix(); + + assertThat(matrix.users()).hasSize(1); + assertThat(matrix.users().getFirst().directRoles()).containsExactly("DIRECT_ROLE"); + assertThat(matrix.users().getFirst().inheritedRoles()).containsExactly("GROUP_ROLE"); + assertThat(matrix.users().getFirst().effectiveRoles()).containsExactly("DIRECT_ROLE", "GROUP_ROLE"); + assertThat(matrix.users().getFirst().permissionCount()).isEqualTo(2); + assertThat(matrix.groups().getFirst().users()).containsExactly("agent_ops"); + assertThat(matrix.groups().getFirst().roles()).containsExactly("GROUP_ROLE"); + assertThat(matrix.roles().get(1).affectedUsers()).containsExactly("agent_ops"); + } +}