fix #493: add effective access matrix

This commit is contained in:
devmrko
2026-06-26 14:14:22 +09:00
parent 210e0bd4d0
commit c8326f954d
12 changed files with 805 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package com.cloudhandson.vpdbackoffice.domain.effective;
import java.util.List;
public record EffectiveMatrixView(
List<UserEffectiveAccessView> users,
List<GroupEffectiveAccessView> groups,
List<RoleEffectiveImpactView> roles,
int permissionCount
) {
}

View File

@@ -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<String> users,
List<String> roles,
int permissionCount,
List<String> objectNames
) {
}

View File

@@ -0,0 +1,16 @@
package com.cloudhandson.vpdbackoffice.domain.effective;
import java.util.List;
public record RoleEffectiveImpactView(
long roleId,
String roleName,
String maxSensitivityLevel,
List<String> directUsers,
List<String> groups,
List<String> inheritedUsers,
List<String> affectedUsers,
int permissionCount,
List<String> objectNames
) {
}

View File

@@ -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<String> directRoles,
List<String> groups,
List<String> inheritedRoles,
List<String> effectiveRoles,
int permissionCount,
List<String> objectNames
) {
}

View File

@@ -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<AppUser> users = userService.findAll();
List<AppGroup> groups = groupService.findAll();
List<AppRole> roles = permissionService.findRoles();
List<UserRoleView> userRoles = userService.findUserRoles();
List<GroupUserView> groupUsers = groupService.findGroupUsers();
List<GroupRoleView> groupRoles = groupService.findGroupRoles();
List<PermissionView> permissions = permissionService.findPermissionViews();
Map<Long, Set<Long>> directRoleIdsByUser = directRoleIdsByUser(userRoles);
Map<Long, List<String>> directRoleNamesByUser = directRoleNamesByUser(userRoles);
Map<Long, Set<Long>> groupIdsByUser = groupIdsByUser(groupUsers);
Map<Long, List<String>> groupNamesByUser = groupNamesByUser(groupUsers);
Map<Long, List<String>> userNamesByGroup = userNamesByGroup(groupUsers);
Map<Long, Set<Long>> roleIdsByGroup = roleIdsByGroup(groupRoles);
Map<Long, List<String>> roleNamesByGroup = roleNamesByGroup(groupRoles);
Map<Long, List<String>> groupNamesByRole = groupNamesByRole(groupRoles);
Map<Long, List<String>> directUserNamesByRole = directUserNamesByRole(userRoles);
Map<Long, List<PermissionView>> permissionsByRole = permissionsByRole(permissions);
Map<Long, String> roleNameById = roleNameById(roles);
List<UserEffectiveAccessView> userViews = users.stream()
.map(user -> userView(
user,
directRoleIdsByUser,
directRoleNamesByUser,
groupIdsByUser,
groupNamesByUser,
roleIdsByGroup,
roleNameById,
permissionsByRole
))
.toList();
List<GroupEffectiveAccessView> groupViews = groups.stream()
.map(group -> groupView(group, userNamesByGroup, roleIdsByGroup, roleNamesByGroup, permissionsByRole))
.toList();
List<RoleEffectiveImpactView> 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<Long, Set<Long>> directRoleIdsByUser,
Map<Long, List<String>> directRoleNamesByUser,
Map<Long, Set<Long>> groupIdsByUser,
Map<Long, List<String>> groupNamesByUser,
Map<Long, Set<Long>> roleIdsByGroup,
Map<Long, String> roleNameById,
Map<Long, List<PermissionView>> permissionsByRole
) {
Set<Long> directRoleIds = directRoleIdsByUser.getOrDefault(user.userId(), Set.of());
Set<Long> inheritedRoleIds = new LinkedHashSet<>();
for (Long groupId : groupIdsByUser.getOrDefault(user.userId(), Set.of())) {
inheritedRoleIds.addAll(roleIdsByGroup.getOrDefault(groupId, Set.of()));
}
Set<Long> 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<Long, List<String>> userNamesByGroup,
Map<Long, Set<Long>> roleIdsByGroup,
Map<Long, List<String>> roleNamesByGroup,
Map<Long, List<PermissionView>> permissionsByRole
) {
Set<Long> 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<Long, List<String>> directUserNamesByRole,
Map<Long, List<String>> groupNamesByRole,
List<GroupUserView> groupUsers,
List<GroupRoleView> groupRoles,
Map<Long, List<PermissionView>> permissionsByRole
) {
Set<Long> roleIds = Set.of(role.roleId());
Set<String> inheritedUsers = new LinkedHashSet<>();
Set<Long> 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<String> 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<Long, Set<Long>> directRoleIdsByUser(List<UserRoleView> userRoles) {
Map<Long, Set<Long>> result = new LinkedHashMap<>();
for (UserRoleView view : userRoles) {
result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.roleId());
}
return result;
}
private Map<Long, List<String>> directRoleNamesByUser(List<UserRoleView> userRoles) {
Map<Long, Set<String>> result = new LinkedHashMap<>();
for (UserRoleView view : userRoles) {
result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.roleName());
}
return toListMap(result);
}
private Map<Long, Set<Long>> groupIdsByUser(List<GroupUserView> groupUsers) {
Map<Long, Set<Long>> result = new LinkedHashMap<>();
for (GroupUserView view : groupUsers) {
result.computeIfAbsent(view.userId(), key -> new LinkedHashSet<>()).add(view.groupId());
}
return result;
}
private Map<Long, List<String>> groupNamesByUser(List<GroupUserView> groupUsers) {
Map<Long, Set<String>> 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<Long, List<String>> userNamesByGroup(List<GroupUserView> groupUsers) {
Map<Long, Set<String>> result = new LinkedHashMap<>();
for (GroupUserView view : groupUsers) {
result.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.username());
}
return toListMap(result);
}
private Map<Long, Set<Long>> roleIdsByGroup(List<GroupRoleView> groupRoles) {
Map<Long, Set<Long>> result = new LinkedHashMap<>();
for (GroupRoleView view : groupRoles) {
result.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.roleId());
}
return result;
}
private Map<Long, List<String>> roleNamesByGroup(List<GroupRoleView> groupRoles) {
Map<Long, Set<String>> result = new LinkedHashMap<>();
for (GroupRoleView view : groupRoles) {
result.computeIfAbsent(view.groupId(), key -> new LinkedHashSet<>()).add(view.roleName());
}
return toListMap(result);
}
private Map<Long, List<String>> groupNamesByRole(List<GroupRoleView> groupRoles) {
Map<Long, Set<String>> 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<Long, List<String>> directUserNamesByRole(List<UserRoleView> userRoles) {
Map<Long, Set<String>> result = new LinkedHashMap<>();
for (UserRoleView view : userRoles) {
result.computeIfAbsent(view.roleId(), key -> new LinkedHashSet<>()).add(view.username());
}
return toListMap(result);
}
private Map<Long, List<PermissionView>> permissionsByRole(List<PermissionView> permissions) {
Map<Long, List<PermissionView>> result = new LinkedHashMap<>();
for (PermissionView permission : permissions) {
result.computeIfAbsent(permission.roleId(), key -> new ArrayList<>()).add(permission);
}
return result;
}
private Map<Long, String> roleNameById(List<AppRole> roles) {
Map<Long, String> result = new LinkedHashMap<>();
for (AppRole role : roles) {
result.put(role.roleId(), role.roleName());
}
return result;
}
private List<String> names(Set<Long> ids, Map<Long, String> nameById) {
return ids.stream()
.map(nameById::get)
.filter(name -> name != null && !name.isBlank())
.toList();
}
private int permissionCount(Set<Long> roleIds, Map<Long, List<PermissionView>> permissionsByRole) {
return roleIds.stream()
.mapToInt(roleId -> permissionsByRole.getOrDefault(roleId, List.of()).size())
.sum();
}
private List<String> objectNames(Set<Long> roleIds, Map<Long, List<PermissionView>> permissionsByRole) {
Set<String> 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<Long, List<String>> toListMap(Map<Long, Set<String>> source) {
Map<Long, List<String>> 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;
}
}

View File

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

View File

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

View File

@@ -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 = `<td colspan="${columnCount}" class="text-muted">${table.dataset.emptyMessage || '선택한 항목이 없습니다.'}</td>`;
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', () => {

View File

@@ -0,0 +1,201 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('유효 권한 매트릭스')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>유효 권한 매트릭스</h1>
<p>백오피스 사용자, 그룹, 역할, 권한의 최종 상속 결과를 확인합니다.</p>
</div>
<div class="alert alert-warning" th:if="${runtimeError}">
<strong th:text="${runtimeError.title()}">DB 연결 설정이 필요합니다.</strong>
<span th:text="${runtimeError.message()}">message</span>
<div th:if="${runtimeError.showSupportCommand()}">
<code>./run.sh backoffice-support</code>
</div>
</div>
<section th:replace="~{fragments/layout :: architectureStrip('permission')}"></section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>권한 해석 기준</h2>
<p class="section-subtitle">여기의 사용자는 Oracle DB schema user가 아니라 Bearer Token으로 식별되는 백오피스 application user입니다.</p>
</div>
<a class="btn btn-sm rw-btn-secondary" href="/permissions">권한 관리로 이동</a>
</div>
<div class="matrix-summary">
<div>
<span>사용자</span>
<strong th:text="${#lists.size(matrix.users())}">0</strong>
</div>
<div>
<span>그룹</span>
<strong th:text="${#lists.size(matrix.groups())}">0</strong>
</div>
<div>
<span>역할</span>
<strong th:text="${#lists.size(matrix.roles())}">0</strong>
</div>
<div>
<span>권한</span>
<strong th:text="${matrix.permissionCount()}">0</strong>
</div>
</div>
<ol class="setup-steps">
<li>사용자 관리에서 application user를 등록합니다.</li>
<li>그룹 관리에서 사용자를 그룹에 넣고 그룹에 역할을 부여합니다.</li>
<li>권한 관리에서 역할에 TABLE/VIEW 행 규칙과 컬럼 NULL 정책을 부여합니다.</li>
<li>토큰을 발급하면 ORDS handler가 token user context를 설정하고 VPD가 이 권한을 적용합니다.</li>
</ol>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>사용자 기준</h2>
<p class="section-subtitle">직접 역할과 그룹을 통해 상속된 역할을 분리해서 봅니다.</p>
</div>
<label class="matrix-filter">
사용자
<select class="form-select form-select-sm" data-effective-filter="user-effective-table">
<option value="">전체 사용자</option>
<option th:each="row : ${matrix.users()}" th:value="${row.userId()}" th:text="${row.username()}"></option>
</select>
</label>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="user-effective-table" data-empty-message="선택한 사용자에 대한 유효 권한이 없습니다. 사용자 등록, 그룹 배정, 역할 부여 순서로 확인하세요.">
<thead>
<tr>
<th>사용자</th>
<th>직접 역할</th>
<th>소속 그룹</th>
<th>그룹 상속 역할</th>
<th>최종 역할</th>
<th>권한</th>
<th>보호 객체</th>
</tr>
</thead>
<tbody>
<tr th:each="row : ${matrix.users()}" th:attr="data-effective-id=${row.userId()}">
<td>
<strong th:text="${row.username()}">agent_hr</strong>
<div class="text-muted small" th:text="${row.empNo() + ' / ' + row.deptCode()}">E100 / HR</div>
<span class="badge" th:classappend="${row.active()} ? ' text-bg-success' : ' text-bg-secondary'"
th:text="${row.active()} ? 'ACTIVE APP USER' : 'INACTIVE APP USER'">ACTIVE</span>
</td>
<td th:text="${#lists.isEmpty(row.directRoles()) ? '-' : #strings.listJoin(row.directRoles(), ', ')}">-</td>
<td th:text="${#lists.isEmpty(row.groups()) ? '-' : #strings.listJoin(row.groups(), ', ')}">-</td>
<td th:text="${#lists.isEmpty(row.inheritedRoles()) ? '-' : #strings.listJoin(row.inheritedRoles(), ', ')}">-</td>
<td><strong th:text="${#lists.isEmpty(row.effectiveRoles()) ? '-' : #strings.listJoin(row.effectiveRoles(), ', ')}">-</strong></td>
<td><span class="badge text-bg-secondary" th:text="${row.permissionCount()}">0</span></td>
<td class="matrix-list" th:text="${#lists.isEmpty(row.objectNames()) ? '권한 객체 없음' : #strings.listJoin(row.objectNames(), ', ')}">objects</td>
</tr>
<tr th:if="${#lists.isEmpty(matrix.users())}">
<td colspan="7" class="text-muted">등록된 사용자가 없습니다. 사용자 관리에서 application user를 먼저 등록하세요.</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>그룹 기준</h2>
<p class="section-subtitle">그룹에 속한 사용자와 그룹에 부여된 역할이 제공하는 권한을 봅니다.</p>
</div>
<label class="matrix-filter">
그룹
<select class="form-select form-select-sm" data-effective-filter="group-effective-table">
<option value="">전체 그룹</option>
<option th:each="row : ${matrix.groups()}" th:value="${row.groupId()}" th:text="${row.groupCode() + ' / ' + row.groupName()}"></option>
</select>
</label>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="group-effective-table" data-empty-message="선택한 그룹의 사용자/역할 매핑이 없습니다. 그룹 사용자와 그룹 역할을 먼저 등록하세요.">
<thead>
<tr>
<th>그룹</th>
<th>포함 사용자</th>
<th>부여 역할</th>
<th>권한</th>
<th>보호 객체</th>
</tr>
</thead>
<tbody>
<tr th:each="row : ${matrix.groups()}" th:attr="data-effective-id=${row.groupId()}">
<td>
<code th:text="${row.groupCode()}">GROUP</code>
<div th:text="${row.groupName()}">그룹명</div>
<span class="badge" th:classappend="${row.active()} ? ' text-bg-success' : ' text-bg-secondary'"
th:text="${row.active()} ? 'ACTIVE' : 'INACTIVE'">ACTIVE</span>
</td>
<td th:text="${#lists.isEmpty(row.users()) ? '사용자 없음' : #strings.listJoin(row.users(), ', ')}">users</td>
<td th:text="${#lists.isEmpty(row.roles()) ? '역할 없음' : #strings.listJoin(row.roles(), ', ')}">roles</td>
<td><span class="badge text-bg-secondary" th:text="${row.permissionCount()}">0</span></td>
<td class="matrix-list" th:text="${#lists.isEmpty(row.objectNames()) ? '권한 객체 없음' : #strings.listJoin(row.objectNames(), ', ')}">objects</td>
</tr>
<tr th:if="${#lists.isEmpty(matrix.groups())}">
<td colspan="5" class="text-muted">등록된 그룹이 없습니다. 그룹 관리에서 그룹을 먼저 생성하세요.</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<h2>역할 기준</h2>
<p class="section-subtitle">역할이 직접 사용자와 그룹 사용자에게 어떤 영향을 주는지 봅니다.</p>
</div>
<label class="matrix-filter">
역할
<select class="form-select form-select-sm" data-effective-filter="role-effective-table">
<option value="">전체 역할</option>
<option th:each="row : ${matrix.roles()}" th:value="${row.roleId()}" th:text="${row.roleName()}"></option>
</select>
</label>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="role-effective-table" data-empty-message="선택한 역할의 사용자/그룹/권한 연결이 없습니다. 사용자 역할, 그룹 역할, 권한 등록을 확인하세요.">
<thead>
<tr>
<th>역할</th>
<th>직접 사용자</th>
<th>연결 그룹</th>
<th>그룹 상속 사용자</th>
<th>영향 사용자</th>
<th>권한</th>
<th>보호 객체</th>
</tr>
</thead>
<tbody>
<tr th:each="row : ${matrix.roles()}" th:attr="data-effective-id=${row.roleId()}">
<td>
<strong th:text="${row.roleName()}">ROLE</strong>
<div class="text-muted small">민감도 <span th:text="${row.maxSensitivityLevel()}">PUBLIC</span></div>
</td>
<td th:text="${#lists.isEmpty(row.directUsers()) ? '직접 사용자 없음' : #strings.listJoin(row.directUsers(), ', ')}">users</td>
<td th:text="${#lists.isEmpty(row.groups()) ? '그룹 없음' : #strings.listJoin(row.groups(), ', ')}">groups</td>
<td th:text="${#lists.isEmpty(row.inheritedUsers()) ? '상속 사용자 없음' : #strings.listJoin(row.inheritedUsers(), ', ')}">group users</td>
<td><strong th:text="${#lists.isEmpty(row.affectedUsers()) ? '-' : #strings.listJoin(row.affectedUsers(), ', ')}">affected</strong></td>
<td><span class="badge text-bg-secondary" th:text="${row.permissionCount()}">0</span></td>
<td class="matrix-list" th:text="${#lists.isEmpty(row.objectNames()) ? '권한 객체 없음' : #strings.listJoin(row.objectNames(), ', ')}">objects</td>
</tr>
<tr th:if="${#lists.isEmpty(matrix.roles())}">
<td colspan="7" class="text-muted">등록된 역할이 없습니다. 역할 관리에서 역할을 먼저 생성하세요.</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</body>
</html>

View File

@@ -21,6 +21,7 @@
<a class="nav-link" href="/users">사용자</a>
<a class="nav-link" href="/groups">그룹</a>
<a class="nav-link" href="/roles">역할</a>
<a class="nav-link" href="/effective-matrix">유효 권한</a>
<a class="nav-link" href="/permissions">권한</a>
<a class="nav-link" href="/tokens">토큰</a>
</div>