Skip to content

Commit

Permalink
feat: Configuration structure and naming (#4)
Browse files Browse the repository at this point in the history
* feat: Compatibility improvements

The springboot version has been reduced from 3.2.1 to 2.7.14, supporting jdk8

* feat: Configuration structure and naming

Modification of casbin's "role-user" inheritance relationship, menu name and demonstration video/picture
  • Loading branch information
amikecoXu authored Feb 1, 2024
1 parent 7084451 commit 7f45683
Show file tree
Hide file tree
Showing 18 changed files with 162 additions and 49 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ This document presents an example of dynamic permission menu loading based on jC

**Technology Stack**

- **Backend:** Spring Boot 3.x + jCasbin + Spring Data JPA
- **Backend:** Spring Boot 2.x + jCasbin + Spring Data JPA
- **Frontend:** Bootstrap + Thymeleaf

**Startup Instructions**

1. Build the menu structure you need in the `casbin/policy.csv` file. Specifically, `g2` represents the relationship between menus. For example: `g2, submenu name, parent menu name`.
2. Once the configuration is complete, run the `main` method in `Application.java` located under `org/casbin/`. Access `http://localhost:8080/casbin/menu` for testing.
3. The access control model file for jCasbin is located at `casbin/model.conf`, and the policy file is at `casbin/policy.csv`. Modify them as needed based on your requirements.
1. Build the menu structure you need in the [`casbin/policy.csv`](https://github.com/jcasbin/jcasbin-menu-permission/blob/master/src/main/resources/casbin/policy.csv) file. Specifically, `g2` represents the relationship between menus. For example: `g2, submenu name, parent menu name`.
2. Once the configuration is complete, run the `main` method in [`Application.java`](https://github.com/jcasbin/jcasbin-menu-permission/blob/master/src/main/java/org/casbin/Application.java) located under `org/casbin/`. Access `http://localhost:8080/casbin/menu` for testing.
3. The access control model file for jCasbin is located at [`casbin/model.conf`](https://github.com/jcasbin/jcasbin-menu-permission/blob/master/src/main/resources/casbin/model.conf), and the policy file is at [`casbin/policy.csv`](https://github.com/jcasbin/jcasbin-menu-permission/blob/master/src/main/resources/casbin/policy.csv). Modify them as needed based on your requirements.

## Simple Examples

Expand All @@ -24,14 +24,14 @@ This document presents an example of dynamic permission menu loading based on jC
<source src="examples/example.mp4" type="video/mp4">
</video>

**System user**
**Root user**

<img src="examples/system_example.png" alt="system_example" style="zoom:80%;" />
<img src="examples/root_example.png" alt="system_example" style="zoom:60%;" />

**Admin user**

<img src="examples/admin_example.png" alt="admin_example" style="zoom:80%;" />
<img src="examples/admin_example.png" alt="admin_example" style="zoom:60%;" />

**Normal user**

<img src="examples/user_example.png" alt="user_example" style="zoom:80%;" />
<img src="examples/user_example.png" alt="user_example" style="zoom:60%;" />
Binary file modified examples/admin_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/example.mp4
Binary file not shown.
Binary file added examples/root_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed examples/system_example.png
Binary file not shown.
Binary file modified examples/user_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 1 addition & 4 deletions src/main/java/org/casbin/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,12 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
// Extract the menu name, assuming it is the last part of the URI.
String menuName = requestURI.substring(requestURI.lastIndexOf('/') + 1);

if(menuService.checkUserAccess(username, "ALL_ROOT")) return true;

if (username == null) {
// The user is not logged in
response.sendRedirect(request.getContextPath() + "/denied");
return false;
}

if (!menuService.checkUserAccess(username, menuName)) {
if (!menuService.checkMenuAccess(username, menuName)) {
// Users do not have access to the menu.
response.sendRedirect(request.getContextPath() + "/denied");
return false;
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/org/casbin/controller/TestMenuController.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

@Controller
public class TestMenuController {
@GetMapping(value = "menu/UserMenu")
public String UserMenu(){
return "UserMenu/UserMenu";
}
@GetMapping(value = "menu/UserSubMenu_allow")
public String UserSubMenu_allow(){
return "UserMenu/UserSubMenu_allow";
Expand All @@ -29,6 +33,16 @@ public String UserSubMenu_deny(){
return "UserMenu/UserSubMenu_deny";
}

@GetMapping(value = "menu/UserSubSubMenu")
public String UserSubMenu_sub(){
return "UserMenu/UserSubSubMenu";
}

@GetMapping(value = "menu/AdminMenu")
public String AdminMenu(){
return "AdminMenu/AdminMenu";
}

@GetMapping(value = "menu/AdminSubMenu_allow")
public String AdminSubMenu_allow(){
return "AdminMenu/AdminSubMenu_allow";
Expand All @@ -39,9 +53,9 @@ public String AdminSubMenu_deny(){
return "AdminMenu/AdminSubMenu_deny";
}

@GetMapping(value = "menu/SystemMenu")
@GetMapping(value = "menu/OtherMenu")
public String SystemMenu(){
return "SystemMenu/SystemMenu";
return "OtherMenu/OtherMenu";
}

}
16 changes: 16 additions & 0 deletions src/main/java/org/casbin/entity/MenuEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* Menu Entity
Expand All @@ -46,4 +47,19 @@ public void addSubMenu(MenuEntity subMenu) {
this.subMenus.add(subMenu);
subMenu.setParents(this);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MenuEntity that = (MenuEntity) o;
return Objects.equals(name, that.name) &&
Objects.equals(url, that.url);
}

@Override
public int hashCode() {
return Objects.hash(name, url, subMenus);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
public class AuthenticationService {
public boolean authenticate(String username, String password) {
// Three valid username and password combinations are set here.
return ("system".equals(username) && "system".equals(password)) ||
return ("root".equals(username) && "root".equals(password)) ||
("admin".equals(username) && "admin".equals(password)) ||
("user".equals(username) && "user".equals(password));
}
Expand Down
34 changes: 25 additions & 9 deletions src/main/java/org/casbin/service/MenuService.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* The MenuService class handles menu-related business logic, including permission control and menu filtering, using the jCasbin permission library.
Expand All @@ -53,12 +52,7 @@ public List<MenuEntity> findAccessibleMenus(String username) {
this.menuMap = new HashMap<>();
}
this.accessMap = new HashMap<>();
// Check if there is ALL_ROOT permission
if (checkUserAccess(username, "ALL_ROOT")) {
return menuMap.values().stream()
.filter(this::isTopLevelMenu)
.collect(Collectors.toList());
}

List<MenuEntity> accessibleMenus = new ArrayList<>();
for (MenuEntity menu : menuMap.values()) {
checkAndSetMenuAccess(menu, username);
Expand Down Expand Up @@ -102,9 +96,31 @@ private boolean isTopLevelMenu(MenuEntity menu) {
return menu.getParents() == null;
}

public boolean checkUserAccess(String username, String menuName) {
private boolean checkUserAccess(String username, String menuName) {
// Integrate Casbin to check user access to specific menus
return enforcer.enforce(username, menuName, "read");
}
}

public boolean checkMenuAccess(String username, String menuName) {
List<MenuEntity> accessibleMenus = findAccessibleMenus(username);
for (MenuEntity menu : accessibleMenus) {
if (menuMatches(menu, menuName)) {
return true;
}
}
return false;
}

private boolean menuMatches(MenuEntity menu, String menuName) {
if (menu.getName().equals(menuName)) {
return true;
}
for (MenuEntity subMenu : menu.getSubMenus()) {
if (menuMatches(subMenu, menuName)) {
return true;
}
}
return false;
}

}
19 changes: 12 additions & 7 deletions src/main/java/org/casbin/util/MenuUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,22 @@ public static Map<String, MenuEntity> parseCsvFile(String filePath) throws IOExc
String childName = values[1].trim();
String parentName = values[2].trim();

menuMap.putIfAbsent(childName, new MenuEntity(childName));
if (!parentName.isEmpty()) {
// Check whether the name of the submenu is "(NULL)"
if (!"(NULL)".equals(childName)) {
menuMap.putIfAbsent(childName, new MenuEntity(childName));
if (!parentName.isEmpty()) {
menuMap.putIfAbsent(parentName, new MenuEntity(parentName));
MenuEntity childMenu = menuMap.get(childName);
MenuEntity parentMenu = menuMap.get(parentName);
parentMenu.addSubMenu(childMenu);
}
} else if (!parentName.isEmpty()) {
// Add only the parent menu, no submenu.
menuMap.putIfAbsent(parentName, new MenuEntity(parentName));
MenuEntity childMenu = menuMap.get(childName);
MenuEntity parentMenu = menuMap.get(parentName);
parentMenu.addSubMenu(childMenu);
}
}
}
}
return menuMap;
}
}

}
13 changes: 7 additions & 6 deletions src/main/resources/casbin/policy.csv
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
p, ROLE_SYSTEM, ALL_ROOT, read, allow
p, ROLE_ROOT, OtherMenu, read, allow
p, ROLE_ROOT, AdminMenu, read, allow
p, ROLE_ROOT, UserMenu, read, deny
p, ROLE_ADMIN, UserMenu, read, allow
p, ROLE_ADMIN, AdminMenu, read, allow
p, ROLE_ADMIN, AdminSubMenu_deny, read, deny
p, ROLE_USER, UserSubMenu_allow, read, allow

g, user, ROLE_USER
g, admin, ROLE_USER
g, admin, ROLE_ADMIN
g, system, ROLE_SYSTEM
g, root, ROLE_ROOT
g, ROLE_ADMIN, ROLE_USER

g2, UserSubMenu_allow, UserMenu
g2, UserSubMenu_deny, UserMenu
g2, UserSubSubMenu, UserSubMenu_allow
g2, AdminSubMenu_allow, AdminMenu
g2, AdminSubMenu_deny, AdminMenu
g2, , SystemMenu


g2, (NULL), OtherMenu
13 changes: 13 additions & 0 deletions src/main/resources/templates/AdminMenu/AdminMenu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AdminMenu</title>
</head>
<body>
<div>
AdminMenu
</div>

</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</head>
<body>
<div>
SystemMenu
OtherMenu
</div>

</body>
Expand Down
13 changes: 13 additions & 0 deletions src/main/resources/templates/UserMenu/UserMenu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>UserMenu</title>
</head>
<body>
<div>
UserMenu
</div>

</body>
</html>
13 changes: 13 additions & 0 deletions src/main/resources/templates/UserMenu/UserSubSubMenu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>UserSubSubMenu</title>
</head>
<body>
<div>
UserSubSubMenu
</div>

</body>
</html>
47 changes: 36 additions & 11 deletions src/main/resources/templates/main/menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,60 @@
.card-body p {
padding-left: 20px;
}
.submenu-background {
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
margin-top: 5px;
}

</style>
</head>
<body>

<div id="main" class="container-fluid">
<div id="main_nav" class="d-inline-block">
<!-- Collapse menu -->
<div id="accordion">
<!-- collapsible items -->
<div th:each="menu, menuStat: ${menus}" th:if="${menu.subMenus.size() != 0 && menu.subMenus != null}" class="card">
<!-- Recursive menu fragment -->
<div th:fragment="menuFragment(menuList, isTopLevel)">
<div th:each="menu, menuStat: ${menuList}">
<!-- Items with submenus -->
<div th:if="${menu.subMenus.size() != 0 && menu.subMenus != null}">
<div th:if="${isTopLevel}" class="card">
<div class="card-header">
<h4 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" th:data-target="| #collapse${menuStat.index} |">
<span th:text="${menu.name}">Menu 1</span>
<span th:src="${menu.url}" th:text="${menu.name}">Menu Name</span>
</button>
</h4>
</div>

<div th:id="|collapse${menuStat.index}|" class="collapse" data-parent="#accordion">
<div class="card-body">
<p th:each="subMenu:${menu.subMenus}" th:src="${subMenu.url}" th:text="${subMenu.name}">Submenu 1</p>
<div th:replace="this :: menuFragment(${menu.subMenus}, false)"></div>
</div>
</div>
</div>
<!-- Uncollapsed items -->
<div th:each="menu:${menus}" th:if="${menu.subMenus.size() == 0}" class="non-collapse-item">
<p th:text="${menu.name}" th:src="${menu.url}">Menu 2</p>
<div th:unless="${isTopLevel}" class="submenu-background">
<button class="btn btn-link" type="button" data-toggle="collapse" th:data-target="| #collapseSub${menuStat.index} |">
<span th:src="${menu.url}" th:text="${menu.name}">Submenu Name</span>
</button>
<div th:id="|collapseSub${menuStat.index}|" class="collapse">
<div th:replace="this :: menuFragment(${menu.subMenus}, false)"></div>
</div>
</div>
</div>

<!-- No submenu items -->
<div th:if="${menu.subMenus.size() == 0}">
<a th:src="${menu.url}" th:text="${menu.name}">Menu Item</a>
</div>
</div>
</div>

<div id="main" class="container-fluid">
<div id="main_nav" class="d-inline-block">
<div id="accordion">
<!-- Call the recursive fragment -->
<div th:replace="this :: menuFragment(${menus}, true)"></div>
</div>
<div class="logout-button-container">
<button id="logoutButton" class="btn btn-danger">log out</button>
</div>
Expand Down

0 comments on commit 7f45683

Please sign in to comment.