├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── cz │ └── polankam │ └── security │ └── acl │ ├── AclPermissionEvaluator.java │ ├── AuthorizatorService.java │ ├── IPermissionsService.java │ ├── IResourceRepository.java │ ├── JaclpSpringConfiguration.java │ ├── PermissionRule.java │ ├── Role.java │ ├── RoleBuilder.java │ ├── conditions │ ├── AndCondition.java │ ├── ConditionsFactory.java │ ├── OrCondition.java │ ├── PermissionCondition.java │ └── TrueCondition.java │ └── exceptions │ ├── PermissionException.java │ └── ResourceNotFoundException.java └── test └── java └── cz └── polankam └── security └── acl ├── AclPermissionEvaluatorTest.java ├── PermissionRuleTest.java ├── RoleBuilderTest.java ├── RoleTest.java ├── conditions ├── AndConditionTest.java ├── OrConditionTest.java └── TrueConditionTest.java └── test_utils ├── DemoGroup.java ├── DemoGroupConditions.java ├── DemoGroupRepository.java ├── DemoPermissionsService.java └── DemoUser.java /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-22.04 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | java: [17,21] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup java ${{ matrix.java }} 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'adopt' 18 | java-version: ${{ matrix.java }} 19 | 20 | - run: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V 21 | - run: mvn test -B 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch # Run workflow manually 4 | 5 | jobs: 6 | release: 7 | name: Release on Sonatype OSS 8 | runs-on: ubuntu-22.04 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | token: ${{ secrets.RELEASE_GH_TOKEN }} 14 | - run: | 15 | git config user.name github-actions 16 | git config user.email github-actions@github.com 17 | 18 | - name: Setup Java 17 and Apache Maven Central 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'adopt' 22 | java-version: 17 23 | server-id: ossrh 24 | server-username: OSSRH_USERNAME 25 | server-password: OSSRH_PASSWORD 26 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 27 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 28 | 29 | - name: Publish to Apache Maven Central 30 | run: mvn -B clean release:clean release:prepare release:perform 31 | env: 32 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 33 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 34 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,maven,eclipse,intellij,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=java,maven,eclipse,intellij,visualstudiocode 4 | 5 | ### Eclipse ### 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # CDT- autotools 31 | .autotools 32 | 33 | # Java annotation processor (APT) 34 | .factorypath 35 | 36 | # PDT-specific (PHP Development Tools) 37 | .buildpath 38 | 39 | # sbteclipse plugin 40 | .target 41 | 42 | # Tern plugin 43 | .tern-project 44 | 45 | # TeXlipse plugin 46 | .texlipse 47 | 48 | # STS (Spring Tool Suite) 49 | .springBeans 50 | 51 | # Code Recommenders 52 | .recommenders/ 53 | 54 | # Annotation Processing 55 | .apt_generated/ 56 | 57 | # Scala IDE specific (Scala & Java development for Eclipse) 58 | .cache-main 59 | .scala_dependencies 60 | .worksheet 61 | 62 | ### Eclipse Patch ### 63 | # Eclipse Core 64 | .project 65 | 66 | # JDT-specific (Eclipse Java Development Tools) 67 | .classpath 68 | 69 | # Annotation Processing 70 | .apt_generated 71 | 72 | .sts4-cache/ 73 | 74 | ### Intellij ### 75 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 76 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 77 | 78 | .idea 79 | *.iml 80 | 81 | # Gradle and Maven with auto-import 82 | # When using Gradle or Maven with auto-import, you should exclude module files, 83 | # since they will be recreated, and may cause churn. Uncomment if using 84 | # auto-import. 85 | # .idea/modules.xml 86 | # .idea/*.iml 87 | # .idea/modules 88 | # *.iml 89 | # *.ipr 90 | 91 | # CMake 92 | cmake-build-*/ 93 | 94 | # File-based project format 95 | *.iws 96 | 97 | # IntelliJ 98 | out/ 99 | 100 | # mpeltonen/sbt-idea plugin 101 | .idea_modules/ 102 | 103 | # JIRA plugin 104 | atlassian-ide-plugin.xml 105 | 106 | # Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | ### Intellij Patch ### 113 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 114 | 115 | # *.iml 116 | # modules.xml 117 | # .idea/misc.xml 118 | # *.ipr 119 | 120 | ### Java ### 121 | # Compiled class file 122 | *.class 123 | 124 | # Log file 125 | *.log 126 | 127 | # BlueJ files 128 | *.ctxt 129 | 130 | # Mobile Tools for Java (J2ME) 131 | .mtj.tmp/ 132 | 133 | # Package Files # 134 | *.jar 135 | *.war 136 | *.nar 137 | *.ear 138 | *.zip 139 | *.tar.gz 140 | *.rar 141 | 142 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 143 | hs_err_pid* 144 | 145 | ### Maven ### 146 | target/ 147 | pom.xml.tag 148 | pom.xml.releaseBackup 149 | pom.xml.versionsBackup 150 | pom.xml.next 151 | release.properties 152 | dependency-reduced-pom.xml 153 | buildNumber.properties 154 | .mvn/timing.properties 155 | .mvn/wrapper/maven-wrapper.jar 156 | 157 | ### VisualStudioCode ### 158 | .vscode/* 159 | 160 | ### VisualStudioCode Patch ### 161 | # Ignore all local history of files 162 | .history 163 | 164 | # End of https://www.gitignore.io/api/java,maven,eclipse,intellij,visualstudiocode 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Martin Polanka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JACLP: Java ACL Permissions library 2 | 3 | [![Build Status](https://github.com/Neloop/jaclp/actions/workflows/build.yml/badge.svg)](https://github.com/Neloop/jaclp/actions/workflows/build.yml) 4 | [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/Neloop/jaclp/blob/master/LICENSE) 5 | [![Maven Central Release](https://img.shields.io/maven-central/v/cz.polankam.security.acl/jaclp?color=orange)](https://mvnrepository.com/artifact/cz.polankam.security.acl/jaclp) 6 | [![GitHub Release](https://img.shields.io/github/release/neloop/jaclp.svg)](https://github.com/Neloop/jaclp/releases) 7 | 8 | **JACLP: ACL Permission library for Spring Security** introduces static 9 | _ACL-based_ role permission system with a touch of _ABAC_ (Attribute-based 10 | access control) over resources. It is integrated within Spring Security and its 11 | expression based permission control which might be used from `Authorize`-like 12 | annotations over endpoints or generally methods in services. Built with 13 | **Java 17**. 14 | 15 | ## Installation 16 | 17 | Installation of the library is possible through maven dependencies, it is hosted 18 | on [Maven Central](https://mvnrepository.com/artifact/cz.polankam.security.acl/jaclp). 19 | Be sure to fill in the latest version: 20 | 21 | ```xml 22 | 23 | cz.polankam.security.acl 24 | jaclp 25 | !!VERSION!! 26 | 27 | ``` 28 | 29 | ## Example Usage 30 | 31 | With `jaclp` library you can define roles with _ACL_ permissions or _ABAC_ 32 | authorization. Role-based _ACL_ defines if action is allowed on resource or not. 33 | _ABAC_ in this implementation is created on top of ACL and adds condition to the 34 | authorization. Condition is resource-specific action which has to be checked 35 | against particular resource object obtained from resource repository. Examples 36 | of simple and complex definition of _ACL_ and _ABAC_ permissions follows. 37 | 38 | **Define role-based ACL permissions:** 39 | 40 | ```java 41 | Role userRole = RoleBuilder.create("user") 42 | .addAllowedRule("group", "viewAll") 43 | .build(); 44 | ``` 45 | 46 | **Define simple ABAC permissions on resource:** 47 | 48 | ```java 49 | Role userRole = RoleBuilder.create("user") 50 | .addAllowedRule("group", 51 | (UserDetails user, GroupEntity group) -> group.isPublic(), 52 | "viewDetail") 53 | .build(); 54 | ``` 55 | 56 | **Define permissions with wildcards:** 57 | 58 | There is one defined wildcard, the asterisk, it can be used as a resource or as 59 | an action. If asterisk is used all resources or actions used in `hasPermission` 60 | calls are matched against specified permission. 61 | 62 | ```java 63 | Role superadminRole = RoleBuilder.create("superadmin") 64 | .addAllowedRule("*", "*") 65 | .build(); 66 | ``` 67 | 68 | **Define complex ABAC permissions on resource:** 69 | 70 | ```java 71 | Role userRole = RoleBuilder.create("user") 72 | .addAllowedRule("group") 73 | .addAction("viewStats") 74 | .condition(ConditionsFactory.and( 75 | (UserDetails user, GroupEntity group) -> group.isPublic(), 76 | ConditionsFactory.or( 77 | GroupConditions::isVisibleFromNow, 78 | GroupConditions::isSuperGlobal 79 | ) 80 | )) 81 | .endRule() 82 | .build(); 83 | ``` 84 | 85 | After you defined roles used within your application, the next this is to use 86 | them to actually protect some endpoints or internal APIs. After successful 87 | [integration](#integration-into-spring-application) of `jaclp` library to Spring 88 | application, permissions are used whenever Spring Security permission expression 89 | `hasPermission` is called. Therefore we can use permissions in `Authorize` 90 | annotations, these annotations should be preferably placed on public endpoints 91 | of your application. 92 | 93 | **Sample GET endpoints using permission evaluation:** 94 | 95 | ```java 96 | @GetMapping("groups") 97 | @PreAuthorize("hasPermission('group', 'viewAll')") 98 | public List getCurrentUser() { 99 | return this.groupService.findAllGroups(); 100 | } 101 | 102 | @GetMapping("groups/{id}") 103 | @PreAuthorize("hasPermission(#id, 'group', 'viewDetail')") 104 | public GroupDetailDTO getGroupDetail(@PathVariable long id) { 105 | return this.groupService.getGroupDetail(id); 106 | } 107 | ``` 108 | 109 | ## Example Project 110 | 111 | There is example project which demonstrates usage and integration of JACLP into 112 | the Spring Boot, Spring Data JPA and Spring Security stack. This example is 113 | located in separated repository [jaclp-demo](https://github.com/Neloop/jaclp-demo). 114 | 115 | ## Integration into Spring Application 116 | 117 | There are two steps which needs to be done after installing `jaclp` dependency. 118 | Former is ideally import pre-defined permissions configuration, latter defining 119 | `PermissionService`. Configuration is used for creating permission expression 120 | evaluator and integrate it in your project. Permission service on the other hand 121 | should implement `IPermissionService` interface and define all user roles and 122 | their permissions within your project. 123 | 124 | ### Permission Configuration Example 125 | 126 | ```java 127 | package app.config; 128 | 129 | import cz.polankam.security.acl.AclPermissionEvaluator; 130 | import cz.polankam.security.acl.AuthorizatorService; 131 | import cz.polankam.security.acl.IPermissionsService; 132 | import org.springframework.beans.factory.annotation.Autowired; 133 | import org.springframework.context.annotation.Bean; 134 | import org.springframework.context.annotation.Configuration; 135 | import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; 136 | import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; 137 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 138 | import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; 139 | 140 | /** 141 | * Enable and set method security, most importantly define custom behavior for 142 | * hasPermission authorization methods within authorize 143 | * annotations. 144 | */ 145 | @Configuration 146 | @Import(JaclpSpringConfiguration.class) 147 | @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) 148 | public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { 149 | 150 | private final AclPermissionEvaluator permissionEvaluator; 151 | 152 | /** 153 | * Note: @Lazy annotation is very important here, it protects evaluator and 154 | * potential autowired classes from not being able to be processed by 155 | * BeanPostProcessor, which handles for example Spring AOP. 156 | */ 157 | @Autowired 158 | public MethodSecurityConfig(@Lazy AclPermissionEvaluator permissionEvaluator) { 159 | this.permissionEvaluator = permissionEvaluator; 160 | } 161 | 162 | @Override 163 | protected MethodSecurityExpressionHandler createExpressionHandler() { 164 | // set custom permission evaluator for hasPermission expressions 165 | DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); 166 | handler.setPermissionEvaluator(permissionEvaluator); 167 | return handler; 168 | } 169 | } 170 | ``` 171 | 172 | ### Permission Service Example 173 | 174 | Following implementation is only example and it should be different for every 175 | project. The important thing is to implement `getRole()` and `getResource()` 176 | methods to comply with `IPermissionService` interface. Get role method should 177 | return `Role` object which contains defined permission rules for the given role 178 | identification. Get resource method is used for _ABAC_ authorization and should 179 | return resource repository for given resource identification. If project does 180 | not use _ABAC_ authorization `getResource()` can return empty list. 181 | 182 | ```java 183 | package app.security.acl; 184 | 185 | import app.repositories.FileRepository; 186 | import app.repositories.GroupRepository; 187 | import app.security.acl.conditions.GroupConditions; 188 | import cz.polankam.security.acl.IPermissionsService; 189 | import cz.polankam.security.acl.IResourceRepository; 190 | import cz.polankam.security.acl.Role; 191 | import org.springframework.beans.factory.annotation.Autowired; 192 | import org.springframework.stereotype.Service; 193 | 194 | import java.util.HashMap; 195 | import java.util.Map; 196 | 197 | @Service 198 | public class PermissionsService implements IPermissionsService { 199 | 200 | private final Map roles = new HashMap<>(); 201 | private final Map resources = new HashMap<>(); 202 | 203 | /** 204 | * Default constructor which initialize all user roles used within 205 | * application and assign permission rules to them. 206 | * @param groupRepository 207 | * @param fileRepository 208 | */ 209 | @Autowired 210 | public PermissionsService( 211 | GroupRepository groupRepository, 212 | FileRepository fileRepository 213 | ) { 214 | Role user = new Role(Roles.USER); 215 | Role admin = new Role(Roles.ADMINISTRATOR, user); 216 | 217 | user.addPermissionRules( 218 | true, 219 | "group", 220 | new String[] {"view"}, 221 | GroupConditions::isMember 222 | ).addPermissionRules( 223 | true, 224 | "group", 225 | new String[] {"update"}, 226 | GroupConditions::isManager 227 | ); 228 | 229 | admin.addPermissionRules( 230 | true, 231 | "group", 232 | "create" 233 | ); 234 | 235 | roles.put(user.getName(), user); 236 | roles.put(admin.getName(), admin); 237 | 238 | // repositories which will be used to find resources by identification 239 | resources.put("group", groupRepository); 240 | resources.put("file", fileRepository); 241 | } 242 | 243 | public boolean roleExists(String role) { 244 | return roles.containsKey(role); 245 | } 246 | 247 | public Role getRole(String roleString) { 248 | Role role = roles.get(roleString); 249 | if (role == null) { 250 | throw new RuntimeException("Role '" + roleString + "' not found"); 251 | } 252 | 253 | return role; 254 | } 255 | 256 | public IResourceRepository getResource(String resource) { 257 | IResourceRepository repository = resources.get(resource); 258 | if (repository == null) { 259 | throw new RuntimeException("Resource '" + resource + "' not found"); 260 | } 261 | 262 | return repository; 263 | } 264 | } 265 | ``` 266 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | cz.polankam.security.acl 6 | jaclp 7 | 4.2-SNAPSHOT 8 | 9 | JACLP: Java ACL Permissions library 10 | JACLP: ACL Permission library for Spring Security introduces static ACL-based role permission system with a touch of ABAC (Attribute-based access control) over resources. It is integrated within Spring Security and its expression based permission control which might be used from Authorize-like annotations over endpoints or generally methods in components. 11 | https://github.com/Neloop/jaclp 12 | 13 | 14 | 15 | MIT License 16 | https://www.opensource.org/licenses/mit-license.php 17 | 18 | 19 | 20 | 21 | 22 | Martin Polanka 23 | PolankaMartin@gmail.com 24 | polankam.cz 25 | http://www.polankam.cz 26 | 27 | 28 | 29 | 30 | UTF-8 31 | UTF-8 32 | false 33 | 17 34 | 17 35 | 36 | 37 | 38 | 39 | org.springframework.security 40 | spring-security-web 41 | 6.2.0 42 | provided 43 | 44 | 45 | 46 | org.springframework 47 | spring-tx 48 | 6.1.1 49 | provided 50 | 51 | 52 | 53 | 54 | 55 | org.junit.jupiter 56 | junit-jupiter-engine 57 | 5.10.1 58 | test 59 | 60 | 61 | 62 | org.mockito 63 | mockito-core 64 | 5.7.0 65 | test 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-surefire-plugin 75 | 3.2.2 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-release-plugin 80 | 3.0.1 81 | 82 | v@{project.version} 83 | true 84 | false 85 | release 86 | deploy 87 | 88 | 89 | 90 | org.sonatype.plugins 91 | nexus-staging-maven-plugin 92 | 1.6.13 93 | true 94 | 95 | ossrh 96 | https://oss.sonatype.org/ 97 | true 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | release 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-source-plugin 111 | 3.3.0 112 | 113 | 114 | attach-sources 115 | 116 | jar-no-fork 117 | 118 | 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-javadoc-plugin 124 | 3.6.2 125 | 126 | 127 | attach-javadocs 128 | 129 | jar 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-gpg-plugin 137 | 3.1.0 138 | 139 | 140 | 141 | --pinentry-mode 142 | loopback 143 | 144 | 145 | 146 | 147 | sign-artifacts 148 | verify 149 | 150 | sign 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | scm:git:https://github.com/Neloop/jaclp.git 162 | scm:git:https://github.com/Neloop/jaclp.git 163 | https://github.com/Neloop/jaclp/tree/master 164 | HEAD 165 | 166 | 167 | 168 | 169 | ossrh 170 | https://oss.sonatype.org/content/repositories/snapshots 171 | 172 | 173 | ossrh 174 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/AclPermissionEvaluator.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.exceptions.PermissionException; 4 | import cz.polankam.security.acl.exceptions.ResourceNotFoundException; 5 | import org.springframework.security.access.PermissionEvaluator; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.transaction.PlatformTransactionManager; 10 | import org.springframework.transaction.support.TransactionTemplate; 11 | 12 | import java.io.Serializable; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.Optional; 17 | import java.util.stream.Collectors; 18 | 19 | /** 20 | * Custom permission evaluator used for 'hasPermission' expressions within 21 | * authorize annotations. The implementation is based on the user roles and 22 | * permission rules created by the {@link IPermissionsService} which has to be 23 | * given at construction. 24 | *

25 | * Created by Martin Polanka 26 | */ 27 | public class AclPermissionEvaluator implements PermissionEvaluator { 28 | 29 | /** 30 | * Wildcard which can be used when specifying resource or action 31 | */ 32 | public static final String WILDCARD = "*"; 33 | 34 | 35 | /** 36 | * Permission service which contains definition of roles and resource 37 | * repositories used for evaluation. 38 | */ 39 | private final IPermissionsService permissionsService; 40 | /** 41 | * Transaction template for this class. 42 | */ 43 | private final TransactionTemplate transactionTemplate; 44 | 45 | /** 46 | * Constructor. 47 | * 48 | * @param permissionsService roles definition service 49 | * @param transactionManager transaction manager, if null transactions will not be used 50 | */ 51 | public AclPermissionEvaluator(IPermissionsService permissionsService, 52 | PlatformTransactionManager transactionManager) { 53 | this.permissionsService = permissionsService; 54 | // create transaction template for this class 55 | if (transactionManager != null) { 56 | this.transactionTemplate = new TransactionTemplate(transactionManager); 57 | this.transactionTemplate.setReadOnly(true); 58 | } else { 59 | this.transactionTemplate = null; 60 | } 61 | } 62 | 63 | 64 | /** 65 | * Determine if the given user with defined roles can perform action on the 66 | * resource. 67 | * 68 | * @param authentication authentication containing currently logged user 69 | * @param targetDomainObject textual representation of the resource 70 | * @param permission textual representation of the action on the resource 71 | * @return true if user can perform the action on the given resource 72 | */ 73 | @Override 74 | public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { 75 | if (transactionTemplate == null) { 76 | return hasPermissionInternal(authentication, targetDomainObject, permission); 77 | } else { 78 | Boolean result = transactionTemplate.execute(status -> 79 | hasPermissionInternal(authentication, targetDomainObject, permission)); 80 | return result != null && result; 81 | } 82 | } 83 | 84 | 85 | /** 86 | * Determine if the given user with defined roles can perform action on the 87 | * resource with given identification. 88 | * 89 | * @param authentication authentication containing currently logged user 90 | * @param targetId identification of the resource which should be acquired 91 | * @param targetType textual representation of the resource 92 | * @param permission textual representation of the action on the resource 93 | * @return true if user can perform the action on the given resource 94 | */ 95 | @Override 96 | public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { 97 | if (transactionTemplate == null) { 98 | return hasPermissionInternal(authentication, targetId, targetType, permission); 99 | } else { 100 | Boolean result = transactionTemplate.execute(status -> 101 | hasPermissionInternal(authentication, targetId, targetType, permission)); 102 | return result != null && result; 103 | } 104 | } 105 | 106 | //////////////////////////////////////////////////////////////////////////// 107 | 108 | private boolean hasPermissionInternal(Authentication authentication, Object targetDomainObject, Object permission) { 109 | if (authentication == null || 110 | !(authentication.getPrincipal() instanceof UserDetails) || 111 | !(targetDomainObject instanceof String) || 112 | !(permission instanceof String)) { 113 | return false; 114 | } 115 | 116 | UserDetails user = (UserDetails) authentication.getPrincipal(); 117 | String targetResource = (String) targetDomainObject; 118 | String permissionString = (String) permission; 119 | 120 | // check the permissions against all user roles 121 | for (GrantedAuthority authority : user.getAuthorities()) { 122 | Role role = permissionsService.getRole(authority.getAuthority()); 123 | if (role == null) { 124 | // not defined role in permission service, strange, but let us continue... 125 | continue; 126 | } 127 | 128 | List rules = findMatching(role.getPermissionRules(), targetResource, permissionString); 129 | Optional firstRule = rules.stream().findFirst(); 130 | if (firstRule.isPresent()) { 131 | if (firstRule.get().getCondition() != null) { 132 | throw new PermissionException("ABAC permission rule for resource '" + targetResource + 133 | "' and action '" + permissionString + "' was used in non-ABAC context"); 134 | } 135 | 136 | // at least one matching rule was found, allow it or not 137 | return firstRule.get().isAllowed(); 138 | } 139 | } 140 | return false; 141 | } 142 | 143 | private boolean hasPermissionInternal(Authentication authentication, Serializable targetId, String targetType, Object permission) { 144 | if (authentication == null || 145 | !(authentication.getPrincipal() instanceof UserDetails) || 146 | !(permission instanceof String)) { 147 | return false; 148 | } 149 | 150 | UserDetails user = (UserDetails) authentication.getPrincipal(); 151 | String permissionString = (String) permission; 152 | 153 | // check the permissions against all user roles 154 | for (GrantedAuthority authority : user.getAuthorities()) { 155 | Role role = permissionsService.getRole(authority.getAuthority()); 156 | if (role == null) { 157 | // not defined role in permission service, strange, but let us continue... 158 | continue; 159 | } 160 | 161 | List rules = findMatching(role.getPermissionRules(), targetType, permissionString); 162 | Optional firstRule = rules.stream().findFirst(); 163 | if (firstRule.isPresent()) { 164 | // at least one matching rule was found 165 | PermissionRule rule = firstRule.get(); 166 | if (rule.getCondition() != null) { 167 | // we have to find resource repository, because we were 168 | // given resource identification, after that resource is 169 | // acquired from the repository and evaluated in specified 170 | // condition 171 | IResourceRepository repository = permissionsService.getResource(rule.getResource()); 172 | Optional resource = repository.findById(targetId); 173 | if (resource.isEmpty()) { 174 | throw new ResourceNotFoundException("Resource with identification '" + targetId + "' not found"); 175 | } 176 | 177 | // condition was given, so evaluate it 178 | if (rule.getCondition().test(user, resource.get())) { 179 | return rule.isAllowed(); 180 | } 181 | 182 | // if condition was false, we have to continue 183 | // evaluating another rules, because some of them might 184 | // be truthy and grant access to resource 185 | } else { 186 | // condition was not given, so the behaviour is the same 187 | // as for regular id-less permission check, allow it or not 188 | return rule.isAllowed(); 189 | } 190 | } 191 | } 192 | return false; 193 | } 194 | 195 | /** 196 | * Find all matching rules with given resource and action. 197 | * 198 | * @param rules source rules 199 | * @param resource resource which should be found 200 | * @param action action which should be found 201 | * @return filtered collection of matching rules 202 | */ 203 | private List findMatching(Collection rules, String resource, String action) { 204 | return rules.stream().filter(rule -> { 205 | boolean resourceMatch = Objects.equals(rule.getResource(), resource) || 206 | Objects.equals(rule.getResource(), WILDCARD); 207 | boolean actionsMatch = rule.getActions().stream().anyMatch(ruleAction -> 208 | Objects.equals(ruleAction, action) || Objects.equals(ruleAction, WILDCARD)); 209 | return resourceMatch && actionsMatch; 210 | }).collect(Collectors.toList()); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/AuthorizatorService.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Authorizator service, which can be used within whole application for custom 10 | * authorization checks. Internally it uses custom defined permission evaluator 11 | * based on user roles and acl permission rules. 12 | *

13 | * Created by Martin Polanka 14 | */ 15 | public class AuthorizatorService { 16 | 17 | /** Evaluates all permission related requests */ 18 | private final AclPermissionEvaluator permissionEvaluator; 19 | 20 | /** 21 | * Constructor. 22 | * @param permissionEvaluator evaluator 23 | */ 24 | public AuthorizatorService(AclPermissionEvaluator permissionEvaluator) { 25 | this.permissionEvaluator = permissionEvaluator; 26 | } 27 | 28 | 29 | /** 30 | * For the given resource and action determine if currently logged user is 31 | * allowed to perform the action. 32 | * @param resource resource which user wants to access 33 | * @param action action which user wants to take 34 | * @return true if the actions is allowed on given resource, false otherwise 35 | */ 36 | public boolean isAllowed(String resource, String action) { 37 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 38 | return permissionEvaluator.hasPermission(authentication, resource, action); 39 | } 40 | 41 | /** 42 | * For the given resource, its identification and action determine if 43 | * currently logged user is allowed to perform the action. 44 | * @param resource resource which user wants to access 45 | * @param resourceId identification of the resource 46 | * @param action action which user wants to take 47 | * @return true if the actions is allowed on given resource, false otherwise 48 | */ 49 | public boolean isAllowed(String resource, Serializable resourceId, String action) { 50 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 51 | return permissionEvaluator.hasPermission(authentication, resourceId, resource, action); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/IPermissionsService.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | /** 4 | * Used for management of user roles and privileges, should be the entry point 5 | * for adding or changing permission related stuff. It is the base for any other 6 | * authorization services which handles user roles and permissions. 7 | * Has to be implemented by the one who uses this library. 8 | *

9 | * Created by Martin Polanka 10 | */ 11 | public interface IPermissionsService { 12 | 13 | /** 14 | * Determine if the given role is defined within permission service. 15 | * @param role textual role representation 16 | * @return true if role exists, false otherwise 17 | */ 18 | boolean roleExists(String role); 19 | 20 | /** 21 | * For the given textual role get its structured representation containing 22 | * permission rules. 23 | * @param roleString textual role representation 24 | * @return structured representation of given textual role or null if not defined 25 | */ 26 | Role getRole(String roleString); 27 | 28 | /** 29 | * For the given textual representation of resource return its resource 30 | * repository. 31 | * Should throw in case of not defined resource. 32 | * @param resource textual resource representation 33 | * @return repository from which resource object can be acquired 34 | */ 35 | IResourceRepository getResource(String resource); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/IResourceRepository.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | * Interface for resource repository which can be used to acquire resources in 7 | * permission evaluator. 8 | *

9 | * Created by Martin Polanka 10 | */ 11 | public interface IResourceRepository { 12 | 13 | /** 14 | * Find resource entity based on given identification. 15 | * 16 | * @param id identification of resource 17 | * @return entity resource 18 | */ 19 | Optional findById(Object id); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/JaclpSpringConfiguration.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.transaction.PlatformTransactionManager; 7 | 8 | import java.util.Optional; 9 | 10 | /** 11 | * Spring configuration support for JACLP library, which initializes all needed 12 | * beans. 13 | */ 14 | @Configuration 15 | public class JaclpSpringConfiguration { 16 | 17 | @Bean 18 | @Autowired 19 | public AclPermissionEvaluator aclPermissionEvaluator(IPermissionsService permissionsService, Optional transactionManager) { 20 | return new AclPermissionEvaluator(permissionsService, transactionManager.orElse(null)); 21 | } 22 | 23 | @Bean 24 | public AuthorizatorService authorizatorService(AclPermissionEvaluator permissionEvaluator) { 25 | return new AuthorizatorService(permissionEvaluator); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/PermissionRule.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.conditions.PermissionCondition; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Permission rule which should be applied for the given resource and action on 10 | * this resource. The resulting allowance is determined on the isAllowed flag, 11 | * therefore the rule can be either positive or negative. 12 | *

13 | * Created by Martin Polanka 14 | */ 15 | public class PermissionRule { 16 | 17 | /** 18 | * Is this rule allowing access or not allowing 19 | */ 20 | private final boolean isAllowed; 21 | /** 22 | * Textual representation of resource of this rule 23 | */ 24 | private final String resource; 25 | /** 26 | * Actions list which this rule allows or not 27 | */ 28 | private final List actions; 29 | /** 30 | * Condition applied to resource object, might be null 31 | */ 32 | private final PermissionCondition condition; 33 | 34 | /** 35 | * Construct permission rule with given parameters. 36 | * 37 | * @param isAllowed if the action on the resource is allowed or not 38 | * @param resource resource for which the rule should be applied 39 | * @param action action for which the rule should be applied 40 | * @param condition condition applied to resource object, might be null 41 | */ 42 | public PermissionRule(boolean isAllowed, String resource, String action, PermissionCondition condition) { 43 | this.isAllowed = isAllowed; 44 | this.resource = resource; 45 | this.actions = new ArrayList<>(); 46 | this.actions.add(action); 47 | this.condition = condition; 48 | } 49 | 50 | /** 51 | * Construct permission rule with given parameters. 52 | * 53 | * @param isAllowed if the action on the resource is allowed or not 54 | * @param resource resource for which the rule should be applied 55 | * @param actions list of actions for which the rule should be applied 56 | * @param condition condition applied to resource object, might be null 57 | */ 58 | public PermissionRule(boolean isAllowed, String resource, List actions, PermissionCondition condition) { 59 | this.isAllowed = isAllowed; 60 | this.resource = resource; 61 | this.actions = actions; 62 | this.condition = condition; 63 | } 64 | 65 | 66 | /** 67 | * Determines if the rule on the resource and action is allowed or not. 68 | * 69 | * @return true if allowed, false otherwise 70 | */ 71 | public boolean isAllowed() { 72 | return isAllowed; 73 | } 74 | 75 | /** 76 | * Get the resource associated with this permission rule. 77 | * 78 | * @return textual representation of resource 79 | */ 80 | public String getResource() { 81 | return resource; 82 | } 83 | 84 | /** 85 | * Get list of actions associated with this permission rule. 86 | * 87 | * @return textual representation of actions 88 | */ 89 | public List getActions() { 90 | return actions; 91 | } 92 | 93 | /** 94 | * Condition applied to resource object, might be null if resource 95 | * identification is out of scope of the permission. 96 | * 97 | * @return condition functional interface 98 | */ 99 | public PermissionCondition getCondition() { 100 | return condition; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/Role.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.conditions.PermissionCondition; 4 | 5 | import java.util.*; 6 | 7 | /** 8 | * Representation of the role which contains its name and permission rules which 9 | * should be applied for the role. 10 | *

11 | * Created by Martin Polanka 12 | */ 13 | public final class Role { 14 | 15 | /** 16 | * Name of the role 17 | */ 18 | private final String name; 19 | /** 20 | * Parent of this role or null 21 | */ 22 | private final Role parent; 23 | /** 24 | * Associative array of permission rules indexed by resource textual representation 25 | */ 26 | private final Map> permissionRules = new HashMap<>(); 27 | 28 | 29 | /** 30 | * Constructor with the role name. 31 | * 32 | * @param name role name 33 | */ 34 | public Role(String name) { 35 | this(name, null); 36 | } 37 | 38 | /** 39 | * Constructor with the role name and parent. 40 | * 41 | * @param name role name 42 | * @param parent parent of this role 43 | */ 44 | public Role(String name, Role parent) { 45 | this.name = name; 46 | this.parent = parent; 47 | } 48 | 49 | 50 | /** 51 | * If given resource is not initialized in internal map of permission rules, 52 | * initialize it with empty list. 53 | * 54 | * @param resource to be initialized 55 | */ 56 | private void initializeResource(String resource) { 57 | permissionRules.computeIfAbsent(resource, ignored -> new ArrayList<>()); 58 | } 59 | 60 | /** 61 | * Get the name of the role. 62 | * 63 | * @return role identifier 64 | */ 65 | public String getName() { 66 | return name; 67 | } 68 | 69 | /** 70 | * Get parent of this role, can be null. 71 | * 72 | * @return parent role 73 | */ 74 | public Role getParent() { 75 | return parent; 76 | } 77 | 78 | /** 79 | * Add given permission rules structures to this role. 80 | * 81 | * @param rules array of rules 82 | * @return this 83 | */ 84 | public Role addPermissionRules(PermissionRule... rules) { 85 | return addPermissionRules(Arrays.asList(rules)); 86 | } 87 | 88 | /** 89 | * Add given permission rules structures to this role. 90 | * 91 | * @param rules list of rules 92 | * @return this 93 | */ 94 | public Role addPermissionRules(List rules) { 95 | for (PermissionRule rule : rules) { 96 | initializeResource(rule.getResource()); 97 | permissionRules.get(rule.getResource()).add(rule); 98 | } 99 | return this; 100 | } 101 | 102 | /** 103 | * Add permission rules for the given resource, which is either allowed or 104 | * not for the given actions. 105 | * 106 | * @param isAllowed determine if the rule should be allowed for the role or not 107 | * @param resource resource for which the rule should be applied 108 | * @param actions actions on the resource for which the rule should be applied 109 | * @return this 110 | */ 111 | public Role addPermissionRules(boolean isAllowed, String resource, String... actions) { 112 | initializeResource(resource); 113 | permissionRules.get(resource).add(new PermissionRule(isAllowed, resource, Arrays.asList(actions), null)); 114 | return this; 115 | } 116 | 117 | /** 118 | * Add permission rules for the given resource, which is either allowed or 119 | * not for the given actions. Condition should be used on the acquired 120 | * resource object. 121 | * 122 | * @param isAllowed determine if the rule should be allowed for the user or not 123 | * @param resource resource for which the rule should be applied 124 | * @param actions actions on the resource for which the rule should be applied 125 | * @param condition condition applied to resource object 126 | * @return this 127 | */ 128 | public Role addPermissionRules(boolean isAllowed, String resource, String[] actions, PermissionCondition condition) { 129 | initializeResource(resource); 130 | permissionRules.get(resource).add(new PermissionRule(isAllowed, resource, Arrays.asList(actions), condition)); 131 | return this; 132 | } 133 | 134 | /** 135 | * Add permission rules for the given resource, which is either allowed or 136 | * not for the given actions. Condition should be used on the acquired 137 | * resource object. 138 | * 139 | * @param isAllowed determine if the rule should be allowed for the user or not 140 | * @param resource resource for which the rule should be applied 141 | * @param condition condition applied to resource object 142 | * @param actions actions on the resource for which the rule should be applied 143 | * @return this 144 | */ 145 | public Role addPermissionRules(boolean isAllowed, String resource, PermissionCondition condition, String... actions) { 146 | initializeResource(resource); 147 | permissionRules.get(resource).add(new PermissionRule(isAllowed, resource, Arrays.asList(actions), condition)); 148 | return this; 149 | } 150 | 151 | /** 152 | * Get the list of permission rules for this role and its parents. 153 | * 154 | * @return unmodifiable list of permissions 155 | */ 156 | public List getPermissionRules() { 157 | List rules = new ArrayList<>(); 158 | // add all permission rules from map 159 | permissionRules.values().forEach(rules::addAll); 160 | 161 | if (this.parent != null) { 162 | // if there is parent add also its permission rules 163 | rules.addAll(this.parent.getPermissionRules()); 164 | } 165 | 166 | // return unmodifiable list, just to be sure 167 | return Collections.unmodifiableList(rules); 168 | } 169 | 170 | /** 171 | * Get permission rules unmodifiable list for given resource. Rules are 172 | * taken also from parent role of this role. 173 | * 174 | * @param resource resource for which rules are returned 175 | * @return unmodifiable list of permissions 176 | */ 177 | public List getPermissionRules(String resource) { 178 | List rules = new ArrayList<>(); 179 | if (permissionRules.containsKey(resource)) { 180 | // if current role contains resource, add it to resulting collection 181 | rules.addAll(permissionRules.get(resource)); 182 | } 183 | 184 | if (this.parent != null) { 185 | // if there is parent add also its permission rules 186 | rules.addAll(this.parent.getPermissionRules(resource)); 187 | } 188 | 189 | // return unmodifiable list, just to be sure 190 | return Collections.unmodifiableList(rules); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/RoleBuilder.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.conditions.PermissionCondition; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Builder which can be used for convenient Role structure creation. 11 | *

12 | * Created by Martin Polanka on 23.06.2020. 13 | */ 14 | public final class RoleBuilder { 15 | 16 | private String name; 17 | private Role parent; 18 | private final List rules = new ArrayList<>(); 19 | 20 | private RoleBuilder() { 21 | // nothing to see here 22 | } 23 | 24 | /** 25 | * Create role builder for the role with specified name. 26 | */ 27 | public static RoleBuilder create(String name) { 28 | RoleBuilder builder = new RoleBuilder(); 29 | builder.name = name; 30 | return builder; 31 | } 32 | 33 | //////////////////////////////////////////////////////////////////////////// 34 | 35 | /** 36 | * Set parent of the constructed role. 37 | */ 38 | public RoleBuilder parent(Role parent) { 39 | this.parent = parent; 40 | return this; 41 | } 42 | 43 | /** 44 | * Add allowed rule to the role and return permission rule builder. 45 | */ 46 | public PermissionRuleBuilder addAllowedRule(String resource) { 47 | return new PermissionRuleBuilder(this, true, resource); 48 | } 49 | 50 | /** 51 | * Add denied rule to the role and return permission rule builder. 52 | */ 53 | public PermissionRuleBuilder addDeniedRule(String resource) { 54 | return new PermissionRuleBuilder(this, false, resource); 55 | } 56 | 57 | /** 58 | * Add rule to the role and return permission rule builder. 59 | */ 60 | public PermissionRuleBuilder addRule(boolean allowed, String resource) { 61 | return new PermissionRuleBuilder(this, allowed, resource); 62 | } 63 | 64 | /** 65 | * Add allowed rule to the role. 66 | */ 67 | public RoleBuilder addAllowedRule(String resource, String... actions) { 68 | rules.add(new PermissionRule(true, resource, Arrays.asList(actions), null)); 69 | return this; 70 | } 71 | 72 | /** 73 | * Add denied rule to the role. 74 | */ 75 | public RoleBuilder addDeniedRule(String resource, String... actions) { 76 | rules.add(new PermissionRule(false, resource, Arrays.asList(actions), null)); 77 | return this; 78 | } 79 | 80 | /** 81 | * Add rule to the role. 82 | */ 83 | public RoleBuilder addRule(boolean allowed, 84 | String resource, 85 | String... actions) { 86 | rules.add(new PermissionRule(allowed, resource, Arrays.asList(actions), null)); 87 | return this; 88 | } 89 | 90 | /** 91 | * Add allowed rule with specified condition to the role. 92 | */ 93 | public RoleBuilder addAllowedRule(String resource, 94 | PermissionCondition condition, 95 | String... actions) { 96 | rules.add(new PermissionRule(true, resource, Arrays.asList(actions), condition)); 97 | return this; 98 | } 99 | 100 | /** 101 | * Add denied rule with specified condition to the role. 102 | */ 103 | public RoleBuilder addDeniedRule(String resource, 104 | PermissionCondition condition, 105 | String... actions) { 106 | rules.add(new PermissionRule(false, resource, Arrays.asList(actions), condition)); 107 | return this; 108 | } 109 | 110 | /** 111 | * Add rule with specified condition to the role. 112 | */ 113 | public RoleBuilder addPermissionRule(boolean allowed, 114 | String resource, 115 | PermissionCondition condition, 116 | String... actions) { 117 | rules.add(new PermissionRule(allowed, resource, Arrays.asList(actions), condition)); 118 | return this; 119 | } 120 | 121 | /** 122 | * Build the role. 123 | */ 124 | public Role build() { 125 | Role role = new Role(name, parent); 126 | role.addPermissionRules(rules); 127 | return role; 128 | } 129 | 130 | //////////////////////////////////////////////////////////////////////////// 131 | 132 | public static final class PermissionRuleBuilder { 133 | 134 | private final RoleBuilder roleBuilder; 135 | private final boolean isAllowed; 136 | private final String resource; 137 | private final List actions = new ArrayList<>(); 138 | private PermissionCondition condition; 139 | 140 | private PermissionRuleBuilder(RoleBuilder roleBuilder, 141 | boolean isAllowed, 142 | String resource) { 143 | this.roleBuilder = roleBuilder; 144 | this.isAllowed = isAllowed; 145 | this.resource = resource; 146 | } 147 | 148 | /** 149 | * Set specified condition to the permission rule. 150 | */ 151 | public PermissionRuleBuilder condition(PermissionCondition condition) { 152 | this.condition = condition; 153 | return this; 154 | } 155 | 156 | /** 157 | * Add specified action to the permission rule. 158 | */ 159 | public PermissionRuleBuilder addAction(String action) { 160 | actions.add(action); 161 | return this; 162 | } 163 | 164 | /** 165 | * Add specified actions to the permission rule. 166 | */ 167 | public PermissionRuleBuilder addActions(String... actions) { 168 | this.actions.addAll(Arrays.asList(actions)); 169 | return this; 170 | } 171 | 172 | /** 173 | * Add specified actions to the permission rule. 174 | */ 175 | public PermissionRuleBuilder addActions(List actions) { 176 | this.actions.addAll(actions); 177 | return this; 178 | } 179 | 180 | /** 181 | * Add permission rule to the role and continue with role building. 182 | */ 183 | public RoleBuilder endRule() { 184 | PermissionRule rule = new PermissionRule(isAllowed, resource, actions, condition); 185 | roleBuilder.rules.add(rule); 186 | return roleBuilder; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/conditions/AndCondition.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.springframework.security.core.userdetails.UserDetails; 4 | 5 | import java.util.Arrays; 6 | 7 | /** 8 | * And condition which takes array of other conditions on construction and 9 | * evaluates them on logical AND operation during testing. 10 | * Creation is done by provided factory {@link ConditionsFactory}. 11 | * @param type of resource given in testing method 12 | * 13 | * Created by Martin Polanka 14 | */ 15 | final class AndCondition implements PermissionCondition { 16 | 17 | /** 18 | * Array of conditions which will be evaluated on testing. 19 | */ 20 | private PermissionCondition[] conditions; 21 | 22 | /** 23 | * Constructor. 24 | * @param conditions conditions which will be evaluated 25 | */ 26 | @SafeVarargs 27 | AndCondition(PermissionCondition... conditions) { 28 | this.conditions = conditions; 29 | } 30 | 31 | 32 | @Override 33 | public boolean test(UserDetails user, T resource) { 34 | return Arrays.stream(conditions).allMatch(condition -> condition.test(user, resource)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/conditions/ConditionsFactory.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | /** 4 | * Public factory for common permission conditions. 5 | * 6 | * Created by Martin Polanka 7 | */ 8 | public class ConditionsFactory { 9 | 10 | private ConditionsFactory() {} 11 | 12 | /** 13 | * Create and return Or condition which will evaluate given conditions. 14 | * @param conditions conditions which will be evaluated by And condition 15 | * @param type of resource given in testing method 16 | * @return created or condition 17 | */ 18 | @SafeVarargs 19 | public static PermissionCondition or(PermissionCondition... conditions) { 20 | return new OrCondition<>(conditions); 21 | } 22 | 23 | /** 24 | * Create and return And condition which will evaluate given conditions. 25 | * @param conditions conditions which will be evaluated by Or condition 26 | * @param type of resource given in testing method 27 | * @return created and condition 28 | */ 29 | @SafeVarargs 30 | public static PermissionCondition and(PermissionCondition... conditions) { 31 | return new AndCondition<>(conditions); 32 | } 33 | 34 | /** 35 | * Factory method for condition which is always evaluated to true. 36 | * @param type of resource given in testing method 37 | * @return created condition 38 | */ 39 | public static PermissionCondition truthy() { 40 | return new TrueCondition<>(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/conditions/OrCondition.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.springframework.security.core.userdetails.UserDetails; 4 | 5 | import java.util.Arrays; 6 | 7 | /** 8 | * Or condition which takes array of other conditions on construction and 9 | * evaluates them on logical OR operation during testing. 10 | * Creation is done by provided factory {@link ConditionsFactory}. 11 | * @param type of resource given in testing method 12 | * 13 | * Created by Martin Polanka 14 | */ 15 | final class OrCondition implements PermissionCondition { 16 | 17 | /** 18 | * Array of conditions which will be evaluated on testing. 19 | */ 20 | private PermissionCondition[] conditions; 21 | 22 | /** 23 | * Constructor. 24 | * @param conditions conditions which will be evaluated 25 | */ 26 | @SafeVarargs 27 | OrCondition(PermissionCondition... conditions) { 28 | this.conditions = conditions; 29 | } 30 | 31 | 32 | @Override 33 | public boolean test(UserDetails user, T resource) { 34 | return Arrays.stream(conditions).anyMatch(condition -> condition.test(user, resource)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/conditions/PermissionCondition.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.springframework.security.core.userdetails.UserDetails; 4 | 5 | /** 6 | * Functional interface for permission conditions. Main test method is used for 7 | * evaluation of condition with given user and resource. 8 | * @param type of resource given in testing method 9 | * 10 | * Created by Martin Polanka 11 | */ 12 | @FunctionalInterface 13 | public interface PermissionCondition { 14 | 15 | /** 16 | * Test method which evaluates condition against given user and resource. 17 | * @param user user against which condition is evaluated 18 | * @param resource resource against which condition is evaluated 19 | * @return true if condition is truthy, false otherwise 20 | */ 21 | boolean test(UserDetails user, T resource); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/conditions/TrueCondition.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.springframework.security.core.userdetails.UserDetails; 4 | 5 | /** 6 | * Condition which is always validated to true. 7 | * Creation is done by provided factory {@link ConditionsFactory}. 8 | * @param type of resource given in testing method 9 | * 10 | * Created by Martin Polanka 11 | */ 12 | final class TrueCondition implements PermissionCondition { 13 | 14 | @Override 15 | public boolean test(UserDetails user, T resource) { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/exceptions/PermissionException.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Exception which is thrown in case of error within library. 8 | * 9 | * Created by Martin Polanka 10 | */ 11 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 12 | public class PermissionException extends RuntimeException { 13 | 14 | /** 15 | * Construct exception with given cause message. 16 | * @param message description of error 17 | */ 18 | public PermissionException(String message) { 19 | super(message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cz/polankam/security/acl/exceptions/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Exception which is thrown in case of resource not found. 8 | * 9 | * Created by Martin Polanka 10 | */ 11 | @ResponseStatus(HttpStatus.NOT_FOUND) 12 | public class ResourceNotFoundException extends RuntimeException { 13 | 14 | /** 15 | * Construct exception with given cause message. 16 | * @param message description of error 17 | */ 18 | public ResourceNotFoundException(String message) { 19 | super(message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/AclPermissionEvaluatorTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.test_utils.DemoPermissionsService; 4 | import cz.polankam.security.acl.test_utils.DemoUser; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.security.core.Authentication; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | import static org.mockito.Mockito.*; 12 | 13 | class AclPermissionEvaluatorTest { 14 | 15 | private DemoPermissionsService permissionsService; 16 | private AclPermissionEvaluator evaluator; 17 | private Authentication authenticationMock; 18 | 19 | @BeforeEach 20 | void setUp() { 21 | permissionsService = new DemoPermissionsService(); 22 | evaluator = new AclPermissionEvaluator(permissionsService, null); 23 | authenticationMock = mock(Authentication.class); 24 | } 25 | 26 | 27 | @Test 28 | void hasPermission_AuthenticationNull() { 29 | assertFalse(evaluator.hasPermission(null, "user", "view")); 30 | assertFalse(evaluator.hasPermission(null, 123L, "user", "view")); 31 | } 32 | 33 | @Test 34 | void hasPermission_UserMember() { 35 | when(authenticationMock.getPrincipal()).thenReturn(new DemoUser("user", "USER")); 36 | 37 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "view")); 38 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "group", "edit")); 39 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "group", "non-existing")); 40 | assertFalse(evaluator.hasPermission(authenticationMock, "instance", "view")); 41 | assertFalse(evaluator.hasPermission(authenticationMock, "instance", "edit")); 42 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "join")); 43 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "non-existing", "view")); 44 | } 45 | 46 | @Test 47 | void hasPermission_UserManager() { 48 | when(authenticationMock.getPrincipal()).thenReturn(new DemoUser("manager", "USER")); 49 | 50 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "view")); 51 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "edit")); 52 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "group", "non-existing")); 53 | assertFalse(evaluator.hasPermission(authenticationMock, "instance", "view")); 54 | assertFalse(evaluator.hasPermission(authenticationMock, "instance", "edit")); 55 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "join")); 56 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "non-existing", "view")); 57 | } 58 | 59 | @Test 60 | void hasPermission_AdminManager() { 61 | when(authenticationMock.getPrincipal()).thenReturn(new DemoUser("manager", "ADMIN")); 62 | 63 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "view")); 64 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "edit")); 65 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "group", "non-existing")); 66 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "view")); 67 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "edit")); 68 | assertFalse(evaluator.hasPermission(authenticationMock, "instance", "join")); 69 | assertFalse(evaluator.hasPermission(authenticationMock, 123L, "non-existing", "view")); 70 | } 71 | 72 | @Test 73 | void hasPermission_Superadmin() { 74 | when(authenticationMock.getPrincipal()).thenReturn(new DemoUser("superadmin", "SUPERADMIN")); 75 | 76 | // superadmin can do literally everything 77 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "view")); 78 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "edit")); 79 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "group", "non-existing")); 80 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "view")); 81 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "edit")); 82 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "join")); 83 | assertTrue(evaluator.hasPermission(authenticationMock, "instance", "non-existing")); 84 | assertTrue(evaluator.hasPermission(authenticationMock, 123L, "non-existing", "view")); 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/PermissionRuleTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.conditions.PermissionCondition; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Arrays; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | class PermissionRuleTest { 12 | 13 | @Test 14 | void testSingleAction() { 15 | PermissionCondition condition = (user, resource) -> true; 16 | PermissionRule rule = new PermissionRule(true, "resource", "action", condition); 17 | 18 | assertTrue(rule.isAllowed()); 19 | assertEquals("resource", rule.getResource()); 20 | assertEquals(1, rule.getActions().size()); 21 | assertEquals("action", rule.getActions().get(0)); 22 | assertEquals(condition, rule.getCondition()); 23 | } 24 | 25 | @Test 26 | void testMultipleActions() { 27 | PermissionCondition condition = (user, resource) -> true; 28 | PermissionRule rule = new PermissionRule(true, 29 | "resource", Arrays.asList("action1", "action2"), condition); 30 | 31 | assertTrue(rule.isAllowed()); 32 | assertEquals("resource", rule.getResource()); 33 | assertEquals(2, rule.getActions().size()); 34 | assertEquals("action1", rule.getActions().get(0)); 35 | assertEquals("action2", rule.getActions().get(1)); 36 | assertEquals(condition, rule.getCondition()); 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/RoleBuilderTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.conditions.PermissionCondition; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | class RoleBuilderTest { 12 | 13 | @Test 14 | void getNameAndParent() { 15 | Role parent = RoleBuilder.create("parent_role_name").build(); 16 | Role role = RoleBuilder.create("role_name").parent(parent).build(); 17 | 18 | assertEquals("role_name", role.getName()); 19 | assertEquals(parent, role.getParent()); 20 | assertNull(parent.getParent()); 21 | } 22 | 23 | @Test 24 | void addPermissionRules() { 25 | Role role = RoleBuilder.create("role") 26 | .addAllowedRule("res1", "action1", "action2") 27 | .addDeniedRule("res2", "action3") 28 | .build(); 29 | 30 | List rules = role.getPermissionRules(); 31 | assertEquals(2, rules.size()); 32 | 33 | PermissionRule rule1 = rules.get(0); 34 | assertTrue(rule1.isAllowed()); 35 | assertEquals("res1", rule1.getResource()); 36 | assertEquals(2, rule1.getActions().size()); 37 | assertEquals("action1", rule1.getActions().get(0)); 38 | assertEquals("action2", rule1.getActions().get(1)); 39 | 40 | PermissionRule rule2 = rules.get(1); 41 | assertFalse(rule2.isAllowed()); 42 | assertEquals("res2", rule2.getResource()); 43 | assertEquals(1, rule2.getActions().size()); 44 | assertEquals("action3", rule2.getActions().get(0)); 45 | } 46 | 47 | @Test 48 | void addRulesWithPermissionBuilder() { 49 | Role role = RoleBuilder.create("role") 50 | .addAllowedRule("res1") 51 | .addAction("action1") 52 | .addActions("action2") 53 | .endRule() 54 | .addDeniedRule("res2") 55 | .addAction("action3") 56 | .endRule() 57 | .build(); 58 | 59 | List rules = role.getPermissionRules(); 60 | assertEquals(2, rules.size()); 61 | 62 | PermissionRule rule1 = rules.get(0); 63 | assertTrue(rule1.isAllowed()); 64 | assertEquals("res1", rule1.getResource()); 65 | assertEquals(2, rule1.getActions().size()); 66 | assertEquals("action1", rule1.getActions().get(0)); 67 | assertEquals("action2", rule1.getActions().get(1)); 68 | 69 | PermissionRule rule2 = rules.get(1); 70 | assertFalse(rule2.isAllowed()); 71 | assertEquals("res2", rule2.getResource()); 72 | assertEquals(1, rule2.getActions().size()); 73 | assertEquals("action3", rule2.getActions().get(0)); 74 | } 75 | 76 | @Test 77 | void addPermissionRulesCondition() { 78 | PermissionCondition condition1 = (user, condition) -> true; 79 | PermissionCondition condition2 = (user, condition) -> false; 80 | 81 | Role role = RoleBuilder.create("role") 82 | .addAllowedRule("res1", condition1, "action1", "action2") 83 | .addDeniedRule("res2", condition2, "action3") 84 | .build(); 85 | 86 | List rules = role.getPermissionRules(); 87 | assertEquals(2, rules.size()); 88 | 89 | PermissionRule rule1 = rules.get(0); 90 | assertTrue(rule1.isAllowed()); 91 | assertEquals("res1", rule1.getResource()); 92 | assertEquals(2, rule1.getActions().size()); 93 | assertEquals("action1", rule1.getActions().get(0)); 94 | assertEquals("action2", rule1.getActions().get(1)); 95 | assertEquals(condition1, rule1.getCondition()); 96 | 97 | PermissionRule rule2 = rules.get(1); 98 | assertFalse(rule2.isAllowed()); 99 | assertEquals("res2", rule2.getResource()); 100 | assertEquals(1, rule2.getActions().size()); 101 | assertEquals("action3", rule2.getActions().get(0)); 102 | assertEquals(condition2, rule2.getCondition()); 103 | } 104 | 105 | @Test 106 | void addRulesConditionWithPermissionBuilder() { 107 | PermissionCondition condition1 = (user, condition) -> true; 108 | PermissionCondition condition2 = (user, condition) -> false; 109 | 110 | Role role = RoleBuilder.create("role") 111 | .addAllowedRule("res1") 112 | .condition(condition1) 113 | .addAction("action1") 114 | .addActions(Arrays.asList("action2")) 115 | .endRule() 116 | .addDeniedRule("res2") 117 | .condition(condition2) 118 | .addAction("action3") 119 | .endRule() 120 | .build(); 121 | 122 | List rules = role.getPermissionRules(); 123 | assertEquals(2, rules.size()); 124 | 125 | PermissionRule rule1 = rules.get(0); 126 | assertTrue(rule1.isAllowed()); 127 | assertEquals("res1", rule1.getResource()); 128 | assertEquals(2, rule1.getActions().size()); 129 | assertEquals("action1", rule1.getActions().get(0)); 130 | assertEquals("action2", rule1.getActions().get(1)); 131 | assertEquals(condition1, rule1.getCondition()); 132 | 133 | PermissionRule rule2 = rules.get(1); 134 | assertFalse(rule2.isAllowed()); 135 | assertEquals("res2", rule2.getResource()); 136 | assertEquals(1, rule2.getActions().size()); 137 | assertEquals("action3", rule2.getActions().get(0)); 138 | assertEquals(condition2, rule2.getCondition()); 139 | } 140 | 141 | @Test 142 | void getPermissionRulesFromParent() { 143 | Role parent = RoleBuilder.create("parent") 144 | .addDeniedRule("res2", "action3") 145 | .build(); 146 | 147 | Role role = RoleBuilder.create("role") 148 | .parent(parent) 149 | .addAllowedRule("res1", "action1", "action2") 150 | .build(); 151 | 152 | List rules = role.getPermissionRules(); 153 | assertEquals(2, rules.size()); 154 | 155 | PermissionRule rule1 = rules.get(0); 156 | assertTrue(rule1.isAllowed()); 157 | assertEquals("res1", rule1.getResource()); 158 | assertEquals(2, rule1.getActions().size()); 159 | assertEquals("action1", rule1.getActions().get(0)); 160 | assertEquals("action2", rule1.getActions().get(1)); 161 | 162 | PermissionRule rule2 = rules.get(1); 163 | assertFalse(rule2.isAllowed()); 164 | assertEquals("res2", rule2.getResource()); 165 | assertEquals(1, rule2.getActions().size()); 166 | assertEquals("action3", rule2.getActions().get(0)); 167 | } 168 | 169 | @Test 170 | void getPermissionRulesByResource() { 171 | Role role = RoleBuilder.create("role") 172 | .addAllowedRule("res1", "action1", "action2") 173 | .addDeniedRule("res2", "action3") 174 | .build(); 175 | 176 | List res1Rules = role.getPermissionRules("res1"); 177 | assertEquals(1, res1Rules.size()); 178 | 179 | PermissionRule rule1 = res1Rules.get(0); 180 | assertTrue(rule1.isAllowed()); 181 | assertEquals("res1", rule1.getResource()); 182 | assertEquals(2, rule1.getActions().size()); 183 | assertEquals("action1", rule1.getActions().get(0)); 184 | assertEquals("action2", rule1.getActions().get(1)); 185 | 186 | List res2Rules = role.getPermissionRules("res2"); 187 | assertEquals(1, res2Rules.size()); 188 | 189 | PermissionRule rule2 = res2Rules.get(0); 190 | assertFalse(rule2.isAllowed()); 191 | assertEquals("res2", rule2.getResource()); 192 | assertEquals(1, rule2.getActions().size()); 193 | assertEquals("action3", rule2.getActions().get(0)); 194 | } 195 | 196 | @Test 197 | void getPermissionRulesByResourceFromParent() { 198 | Role parent = RoleBuilder.create("parent") 199 | .addDeniedRule("res2", "action3") 200 | .build(); 201 | 202 | Role role = RoleBuilder.create("role") 203 | .parent(parent) 204 | .addAllowedRule("res1", "action1", "action2") 205 | .build(); 206 | 207 | List res1Rules = role.getPermissionRules("res1"); 208 | assertEquals(1, res1Rules.size()); 209 | 210 | PermissionRule rule1 = res1Rules.get(0); 211 | assertTrue(rule1.isAllowed()); 212 | assertEquals("res1", rule1.getResource()); 213 | assertEquals(2, rule1.getActions().size()); 214 | assertEquals("action1", rule1.getActions().get(0)); 215 | assertEquals("action2", rule1.getActions().get(1)); 216 | 217 | List res2Rules = role.getPermissionRules("res2"); 218 | assertEquals(1, res2Rules.size()); 219 | 220 | PermissionRule rule2 = res2Rules.get(0); 221 | assertFalse(rule2.isAllowed()); 222 | assertEquals("res2", rule2.getResource()); 223 | assertEquals(1, rule2.getActions().size()); 224 | assertEquals("action3", rule2.getActions().get(0)); 225 | } 226 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/RoleTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl; 2 | 3 | import cz.polankam.security.acl.conditions.PermissionCondition; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class RoleTest { 11 | 12 | @Test 13 | void getNameAndParent() { 14 | Role parent = new Role("parent_role_name"); 15 | Role role = new Role("role_name", parent); 16 | 17 | assertEquals("role_name", role.getName()); 18 | assertEquals(parent, role.getParent()); 19 | assertNull(parent.getParent()); 20 | } 21 | 22 | @Test 23 | void addPermissionRules() { 24 | Role role = new Role("role"); 25 | role.addPermissionRules(true, "res1", new String[]{"action1", "action2"}); 26 | role.addPermissionRules(false, "res2", new String[]{"action3"}); 27 | 28 | List rules = role.getPermissionRules(); 29 | assertEquals(2, rules.size()); 30 | 31 | PermissionRule rule1 = rules.get(0); 32 | assertTrue(rule1.isAllowed()); 33 | assertEquals("res1", rule1.getResource()); 34 | assertEquals(2, rule1.getActions().size()); 35 | assertEquals("action1", rule1.getActions().get(0)); 36 | assertEquals("action2", rule1.getActions().get(1)); 37 | 38 | PermissionRule rule2 = rules.get(1); 39 | assertFalse(rule2.isAllowed()); 40 | assertEquals("res2", rule2.getResource()); 41 | assertEquals(1, rule2.getActions().size()); 42 | assertEquals("action3", rule2.getActions().get(0)); 43 | 44 | } 45 | 46 | @Test 47 | void addPermissionRulesCondition() { 48 | PermissionCondition condition1 = (user, condition) -> true; 49 | PermissionCondition condition2 = (user, condition) -> false; 50 | 51 | Role role = new Role("role"); 52 | role.addPermissionRules(true, "res1", new String[]{"action1", "action2"}, condition1); 53 | role.addPermissionRules(false, "res2", new String[]{"action3"}, condition2); 54 | 55 | List rules = role.getPermissionRules(); 56 | assertEquals(2, rules.size()); 57 | 58 | PermissionRule rule1 = rules.get(0); 59 | assertTrue(rule1.isAllowed()); 60 | assertEquals("res1", rule1.getResource()); 61 | assertEquals(2, rule1.getActions().size()); 62 | assertEquals("action1", rule1.getActions().get(0)); 63 | assertEquals("action2", rule1.getActions().get(1)); 64 | assertEquals(condition1, rule1.getCondition()); 65 | 66 | PermissionRule rule2 = rules.get(1); 67 | assertFalse(rule2.isAllowed()); 68 | assertEquals("res2", rule2.getResource()); 69 | assertEquals(1, rule2.getActions().size()); 70 | assertEquals("action3", rule2.getActions().get(0)); 71 | assertEquals(condition2, rule2.getCondition()); 72 | } 73 | 74 | @Test 75 | void addPermissionRulesConditionVarargs() { 76 | PermissionCondition condition1 = (user, condition) -> true; 77 | PermissionCondition condition2 = (user, condition) -> false; 78 | 79 | Role role = new Role("role"); 80 | role.addPermissionRules(true, "res1", condition1, "action1", "action2"); 81 | role.addPermissionRules(false, "res2", condition2, "action3"); 82 | 83 | List rules = role.getPermissionRules(); 84 | assertEquals(2, rules.size()); 85 | 86 | PermissionRule rule1 = rules.get(0); 87 | assertTrue(rule1.isAllowed()); 88 | assertEquals("res1", rule1.getResource()); 89 | assertEquals(2, rule1.getActions().size()); 90 | assertEquals("action1", rule1.getActions().get(0)); 91 | assertEquals("action2", rule1.getActions().get(1)); 92 | assertEquals(condition1, rule1.getCondition()); 93 | 94 | PermissionRule rule2 = rules.get(1); 95 | assertFalse(rule2.isAllowed()); 96 | assertEquals("res2", rule2.getResource()); 97 | assertEquals(1, rule2.getActions().size()); 98 | assertEquals("action3", rule2.getActions().get(0)); 99 | assertEquals(condition2, rule2.getCondition()); 100 | } 101 | 102 | @Test 103 | void getPermissionRulesFromParent() { 104 | Role parent = new Role("parent"); 105 | parent.addPermissionRules(false, "res2", new String[]{"action3"}); 106 | 107 | Role role = new Role("role", parent); 108 | role.addPermissionRules(true, "res1", new String[]{"action1", "action2"}); 109 | 110 | List rules = role.getPermissionRules(); 111 | assertEquals(2, rules.size()); 112 | 113 | PermissionRule rule1 = rules.get(0); 114 | assertTrue(rule1.isAllowed()); 115 | assertEquals("res1", rule1.getResource()); 116 | assertEquals(2, rule1.getActions().size()); 117 | assertEquals("action1", rule1.getActions().get(0)); 118 | assertEquals("action2", rule1.getActions().get(1)); 119 | 120 | PermissionRule rule2 = rules.get(1); 121 | assertFalse(rule2.isAllowed()); 122 | assertEquals("res2", rule2.getResource()); 123 | assertEquals(1, rule2.getActions().size()); 124 | assertEquals("action3", rule2.getActions().get(0)); 125 | } 126 | 127 | @Test 128 | void getPermissionRulesByResource() { 129 | Role role = new Role("role"); 130 | role.addPermissionRules(true, "res1", new String[]{"action1", "action2"}); 131 | role.addPermissionRules(false, "res2", new String[]{"action3"}); 132 | 133 | List res1Rules = role.getPermissionRules("res1"); 134 | assertEquals(1, res1Rules.size()); 135 | 136 | PermissionRule rule1 = res1Rules.get(0); 137 | assertTrue(rule1.isAllowed()); 138 | assertEquals("res1", rule1.getResource()); 139 | assertEquals(2, rule1.getActions().size()); 140 | assertEquals("action1", rule1.getActions().get(0)); 141 | assertEquals("action2", rule1.getActions().get(1)); 142 | 143 | List res2Rules = role.getPermissionRules("res2"); 144 | assertEquals(1, res2Rules.size()); 145 | 146 | PermissionRule rule2 = res2Rules.get(0); 147 | assertFalse(rule2.isAllowed()); 148 | assertEquals("res2", rule2.getResource()); 149 | assertEquals(1, rule2.getActions().size()); 150 | assertEquals("action3", rule2.getActions().get(0)); 151 | } 152 | 153 | @Test 154 | void getPermissionRulesByResourceFromParent() { 155 | Role parent = new Role("parent"); 156 | parent.addPermissionRules(false, "res2", new String[]{"action3"}); 157 | 158 | Role role = new Role("role", parent); 159 | role.addPermissionRules(true, "res1", new String[]{"action1", "action2"}); 160 | 161 | List res1Rules = role.getPermissionRules("res1"); 162 | assertEquals(1, res1Rules.size()); 163 | 164 | PermissionRule rule1 = res1Rules.get(0); 165 | assertTrue(rule1.isAllowed()); 166 | assertEquals("res1", rule1.getResource()); 167 | assertEquals(2, rule1.getActions().size()); 168 | assertEquals("action1", rule1.getActions().get(0)); 169 | assertEquals("action2", rule1.getActions().get(1)); 170 | 171 | List res2Rules = role.getPermissionRules("res2"); 172 | assertEquals(1, res2Rules.size()); 173 | 174 | PermissionRule rule2 = res2Rules.get(0); 175 | assertFalse(rule2.isAllowed()); 176 | assertEquals("res2", rule2.getResource()); 177 | assertEquals(1, rule2.getActions().size()); 178 | assertEquals("action3", rule2.getActions().get(0)); 179 | } 180 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/conditions/AndConditionTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class AndConditionTest { 8 | 9 | @Test 10 | void test_Empty() { 11 | AndCondition condition = new AndCondition<>(); 12 | assertTrue(condition.test(null, "resource")); 13 | } 14 | 15 | @Test 16 | void test_Correct() { 17 | PermissionCondition pCondition1 = (user, res) -> res.equals("resource"); 18 | PermissionCondition pCondition2 = (user, res) -> !res.equals("resource1"); 19 | PermissionCondition pCondition3 = (user, res) -> !res.equals("res"); 20 | 21 | AndCondition condition = new AndCondition<>(pCondition1, pCondition2, pCondition3); 22 | assertTrue(condition.test(null, "resource")); 23 | } 24 | 25 | @Test 26 | void test_OneBad() { 27 | PermissionCondition pCondition1 = (user, res) -> res.equals("resource"); 28 | PermissionCondition pCondition2 = (user, res) -> res.equals("resource1"); 29 | PermissionCondition pCondition3 = (user, res) -> !res.equals("res"); 30 | 31 | AndCondition condition = new AndCondition<>(pCondition1, pCondition2, pCondition3); 32 | assertFalse(condition.test(null, "resource")); 33 | } 34 | 35 | @Test 36 | void test_TwoBad() { 37 | PermissionCondition pCondition1 = (user, res) -> res.equals("resource"); 38 | PermissionCondition pCondition2 = (user, res) -> res.equals("resource1"); 39 | PermissionCondition pCondition3 = (user, res) -> res.equals("res"); 40 | 41 | AndCondition condition = new AndCondition<>(pCondition1, pCondition2, pCondition3); 42 | assertFalse(condition.test(null, "resource")); 43 | } 44 | 45 | @Test 46 | void test_AllBad() { 47 | PermissionCondition pCondition1 = (user, res) -> !res.equals("resource"); 48 | PermissionCondition pCondition2 = (user, res) -> res.equals("resource1"); 49 | PermissionCondition pCondition3 = (user, res) -> res.equals("res"); 50 | 51 | AndCondition condition = new AndCondition<>(pCondition1, pCondition2, pCondition3); 52 | assertFalse(condition.test(null, "resource")); 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/conditions/OrConditionTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class OrConditionTest { 8 | 9 | @Test 10 | void test_Empty() { 11 | OrCondition condition = new OrCondition<>(); 12 | assertFalse(condition.test(null, "resource")); 13 | } 14 | 15 | @Test 16 | void test_Correct() { 17 | PermissionCondition pCondition1 = (user, res) -> res.equals("resource"); 18 | PermissionCondition pCondition2 = (user, res) -> !res.equals("resource1"); 19 | PermissionCondition pCondition3 = (user, res) -> !res.equals("res"); 20 | 21 | OrCondition condition = new OrCondition<>(pCondition1, pCondition2, pCondition3); 22 | assertTrue(condition.test(null, "resource")); 23 | } 24 | 25 | @Test 26 | void test_OneBad() { 27 | PermissionCondition pCondition1 = (user, res) -> res.equals("resource"); 28 | PermissionCondition pCondition2 = (user, res) -> res.equals("resource1"); 29 | PermissionCondition pCondition3 = (user, res) -> !res.equals("res"); 30 | 31 | OrCondition condition = new OrCondition<>(pCondition1, pCondition2, pCondition3); 32 | assertTrue(condition.test(null, "resource")); 33 | } 34 | 35 | @Test 36 | void test_TwoBad() { 37 | PermissionCondition pCondition1 = (user, res) -> res.equals("resource"); 38 | PermissionCondition pCondition2 = (user, res) -> res.equals("resource1"); 39 | PermissionCondition pCondition3 = (user, res) -> res.equals("res"); 40 | 41 | OrCondition condition = new OrCondition<>(pCondition1, pCondition2, pCondition3); 42 | assertTrue(condition.test(null, "resource")); 43 | } 44 | 45 | @Test 46 | void test_AllBad() { 47 | PermissionCondition pCondition1 = (user, res) -> !res.equals("resource"); 48 | PermissionCondition pCondition2 = (user, res) -> res.equals("resource1"); 49 | PermissionCondition pCondition3 = (user, res) -> res.equals("res"); 50 | 51 | OrCondition condition = new OrCondition<>(pCondition1, pCondition2, pCondition3); 52 | assertFalse(condition.test(null, "resource")); 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/conditions/TrueConditionTest.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.conditions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class TrueConditionTest { 8 | 9 | @Test 10 | void test_Correct() { 11 | TrueCondition condition = new TrueCondition<>(); 12 | assertTrue(condition.test(null, null)); 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/test_utils/DemoGroup.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.test_utils; 2 | 3 | /** 4 | * Demo group entity. 5 | */ 6 | public class DemoGroup { 7 | 8 | public boolean isMember(DemoUser user) { 9 | return true; 10 | } 11 | 12 | public boolean isManager(DemoUser user) { 13 | return user.getUsername().equals("manager"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/test_utils/DemoGroupConditions.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.test_utils; 2 | 3 | import org.springframework.security.core.userdetails.UserDetails; 4 | 5 | public class DemoGroupConditions { 6 | 7 | public static boolean isMember(UserDetails userDetails, DemoGroup resource) { 8 | if (!(userDetails instanceof DemoUser) || 9 | resource == null) { 10 | return false; 11 | } 12 | 13 | return resource.isMember((DemoUser) userDetails); 14 | } 15 | 16 | public static boolean isManager(UserDetails userDetails, DemoGroup resource) { 17 | if (!(userDetails instanceof DemoUser) || 18 | resource == null) { 19 | return false; 20 | } 21 | 22 | return resource.isManager((DemoUser) userDetails); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/test_utils/DemoGroupRepository.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.test_utils; 2 | 3 | import cz.polankam.security.acl.IResourceRepository; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * Demo group service for getting DemoGroup. 9 | */ 10 | public class DemoGroupRepository implements IResourceRepository { 11 | 12 | @Override 13 | public Optional findById(Object id) { 14 | return Optional.of(new DemoGroup()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/test_utils/DemoPermissionsService.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.test_utils; 2 | 3 | import cz.polankam.security.acl.IPermissionsService; 4 | import cz.polankam.security.acl.IResourceRepository; 5 | import cz.polankam.security.acl.Role; 6 | import cz.polankam.security.acl.conditions.ConditionsFactory; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * Demo permission service implementation. 14 | */ 15 | public class DemoPermissionsService implements IPermissionsService { 16 | 17 | private Map roles = new HashMap<>(); 18 | private Map resources = new HashMap<>(); 19 | 20 | /** 21 | * Default constructor which initialize all user roles used within 22 | * application and assign permission rules to them. 23 | */ 24 | public DemoPermissionsService() { 25 | Role user = new Role("USER"); 26 | Role admin = new Role("ADMIN", user); 27 | Role superadmin = new Role("SUPERADMIN"); 28 | 29 | user.addPermissionRules( 30 | true, 31 | "group", 32 | new String[] {"view"}, 33 | ConditionsFactory.and( 34 | ConditionsFactory.truthy(), // just to show off and condition 35 | DemoGroupConditions::isMember // actual condition 36 | ) 37 | ).addPermissionRules( 38 | true, 39 | "group", 40 | ConditionsFactory.or( 41 | (UserDetails userDetails, DemoGroup group) -> false, // just to show off or condition 42 | DemoGroupConditions::isManager 43 | ), 44 | "edit" 45 | ).addPermissionRules( 46 | false, 47 | "instance", 48 | "view", "edit" 49 | ).addPermissionRules( 50 | true, 51 | "instance", 52 | "join" 53 | ); 54 | 55 | admin.addPermissionRules( 56 | true, 57 | "instance", 58 | "view", "edit" 59 | ).addPermissionRules( 60 | false, 61 | "instance", 62 | "join" // not allowed for testing purposes 63 | ); 64 | 65 | superadmin.addPermissionRules( 66 | true, "*", "*" // superadmin can do everything... literally 67 | ); 68 | 69 | roles.put(user.getName(), user); 70 | roles.put(admin.getName(), admin); 71 | roles.put(superadmin.getName(), superadmin); 72 | resources.put("group", new DemoGroupRepository()); 73 | } 74 | 75 | public boolean roleExists(String role) { 76 | return roles.containsKey(role); 77 | } 78 | 79 | public Role getRole(String roleString) { 80 | Role role = roles.get(roleString); 81 | if (role == null) { 82 | throw new RuntimeException("Role '" + roleString + "' not found"); 83 | } 84 | 85 | return role; 86 | } 87 | 88 | public IResourceRepository getResource(String resource) { 89 | IResourceRepository repository = resources.get(resource); 90 | if (repository == null) { 91 | throw new RuntimeException("Resource '" + resource + "' not found"); 92 | } 93 | 94 | return repository; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/cz/polankam/security/acl/test_utils/DemoUser.java: -------------------------------------------------------------------------------- 1 | package cz.polankam.security.acl.test_utils; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | 10 | /** 11 | * Demo user entity. 12 | */ 13 | public class DemoUser implements UserDetails { 14 | 15 | private String username; 16 | private String role; 17 | 18 | public DemoUser(String username, String role) { 19 | this.username = username; 20 | this.role = role; 21 | } 22 | 23 | 24 | @Override 25 | public Collection getAuthorities() { 26 | return Collections.singletonList(new SimpleGrantedAuthority(role)); 27 | } 28 | 29 | @Override 30 | public String getPassword() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public String getUsername() { 36 | return username; 37 | } 38 | 39 | @Override 40 | public boolean isAccountNonExpired() { 41 | return false; 42 | } 43 | 44 | @Override 45 | public boolean isAccountNonLocked() { 46 | return false; 47 | } 48 | 49 | @Override 50 | public boolean isCredentialsNonExpired() { 51 | return false; 52 | } 53 | 54 | @Override 55 | public boolean isEnabled() { 56 | return false; 57 | } 58 | } 59 | --------------------------------------------------------------------------------