feat: Initial implementation of permission manager SDK - Add core permission management functionality with @RequiresPermission annotation - Implement permission checking aspect with Spring Security integration - Add comprehensive model classes for permissions, roles, and domains - Create integration builder for permission structure setup - Add configuration support for permission manager client - Implement exception handling for access control - Add extensive test coverage with integration tests - Configure Maven build with Spring Boot/Cloud dependencies

This commit is contained in:
2025-01-08 02:32:57 +01:00
parent f039652d4b
commit 6d4a3e2ea5
48 changed files with 2816 additions and 52 deletions

5
.idea/compiler.xml generated
View File

@ -10,4 +10,9 @@
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="permission-manager-sdk" options="-parameters" />
</option>
</component>
</project>

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic"
}

15
do
View File

@ -2,19 +2,6 @@
set -euo pipefail
script_dir="$(cd "$(dirname "${0}")"; pwd -P)"
app_name='permission-manager'
postgres_image="postgres:16-alpine"
postgres_port=16060
postgres_container_name="pmg-db"
go_junit_report_version='latest'
gocov_version='latest'
gocov_xml_version='latest'
linter_version='v1.62.0'
swag_version='v1.16.3'
grey='\033[0;37m'
red='\033[1;31m'
green='\033[1;32m'
@ -29,7 +16,7 @@ function log {
## full-release : creates and publishes a new release to maven central
function task_full_release() {
gitea_token="$(cat ${script_dir}/.gitea-token)"
gitea_token="$(cat "${script_dir}/.gitea-token")"
task_build
mvn jreleaser:full-release -Dgitea.token="$gitea_token"

106
pom.xml
View File

@ -21,6 +21,8 @@
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<spring-boot.version>3.3.0</spring-boot.version>
<spring-cloud.version>2023.0.2</spring-cloud.version>
</properties>
<licenses>
@ -31,32 +33,108 @@
</license>
</licenses>
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<optional>true</optional>
</dependency>
<!-- Feign -->
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.2</version>
</dependency>
<!-- Jakarta -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Feign -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<!-- Others -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<developers>
@ -68,13 +146,11 @@
<scm>
<connection>scm:git:https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git</connection>
<developerConnection>scm:git:https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git
</developerConnection>
<developerConnection>scm:git:https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git</developerConnection>
<url>https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git</url>
<tag>HEAD</tag>
</scm>
<build>
<pluginManagement>
<plugins>
@ -87,6 +163,11 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -109,13 +190,12 @@
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<version>1.15.0</version>
<configuration>
<jreleaser>
<signing>
<active>ALWAYS</active>
<mode>COMMAND</mode>
<armored>true</armored>#
<armored>true</armored>
<command>
</command>
</signing>

View File

@ -1,13 +1,22 @@
package de.mummeit.common.config;
import feign.Client;
import feign.httpclient.ApacheHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@EnableFeignClients
@ComponentScan(basePackages = "de.mummeit")
@Configuration
@ComponentScan(basePackages = "de.mummeit")
@EnableFeignClients(basePackages = "de.mummeit")
@PropertySource(value = "classpath:permission-manager-sdk-application.yaml")
public class PermissionManagerSdkConfiguration {
@Bean
public Client feignClient() {
return new ApacheHttpClient(HttpClients.createDefault());
}
}

View File

@ -0,0 +1,96 @@
package de.mummeit.pmg.api;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest;
import de.mummeit.pmg.api.model.access.request.PermitRequest;
import de.mummeit.pmg.api.model.access.request.RevokeScopeAccessRequest;
import de.mummeit.pmg.api.model.access.request.RevokeUserAccessRequest;
import de.mummeit.pmg.api.model.access.request.SearchPermitRequest;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.structure.Domain;
import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.api.model.structure.Role;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(
name = "permission-manager",
url = "${permission-manager.url}"
)
public interface PermissionManagerClient {
@GetMapping("/health")
String getHealthStatus();
// Access Management
@PostMapping("/api/v1/access/check")
PermittedResponse checkAccess(@RequestBody CheckAccessRequest request);
@PostMapping("/api/v1/access/permit")
void permitAccess(@RequestBody PermitRequest request);
@PostMapping("/api/v1/access/permits/search")
List<Permission> searchPermits(@RequestBody SearchPermitRequest request);
@PatchMapping("/api/v1/access/revoke")
void revokeAccess(@RequestBody PermitRequest request);
@PatchMapping("/api/v1/access/revoke/scope")
void revokeScopeAccess(@RequestBody RevokeScopeAccessRequest request);
@PatchMapping("/api/v1/access/revoke/user")
void revokeUserAccess(@RequestBody RevokeUserAccessRequest request);
// Domain Management
@PostMapping("/api/v1/domains")
Domain createDomain(@RequestBody Domain domain);
@GetMapping("/api/v1/domains/{domain}")
Domain getDomain(@PathVariable("domain") String domain);
@PutMapping("/api/v1/domains/{domain}")
Domain updateDomain(@PathVariable("domain") String domain, @RequestBody Domain domainRequest);
@DeleteMapping("/api/v1/domains/{domain}")
void deleteDomain(@PathVariable("domain") String domain);
// Permission Management
@PostMapping("/api/v1/domains/{domain}/permissions")
Permission createPermission(@PathVariable("domain") String domain, @RequestBody Permission permission);
@GetMapping("/api/v1/domains/{domain}/permissions/{permission}")
Permission getPermission(@PathVariable("domain") String domain, @PathVariable("permission") String permission);
@PutMapping("/api/v1/domains/{domain}/permissions/{permission}")
Permission updatePermission(
@PathVariable("domain") String domain,
@PathVariable("permission") String permission,
@RequestBody Permission permissionRequest
);
@DeleteMapping("/api/v1/domains/{domain}/permissions/{permission}")
void deletePermission(@PathVariable("domain") String domain, @PathVariable("permission") String permission);
// Role Management
@PostMapping("/api/v1/domains/{domain}/roles")
Role createRole(@PathVariable("domain") String domain, @RequestBody Role role);
@GetMapping("/api/v1/domains/{domain}/roles/{role}")
Role getRole(@PathVariable("domain") String domain, @PathVariable("role") String role);
@PutMapping("/api/v1/domains/{domain}/roles/{role}")
Role updateRole(
@PathVariable("domain") String domain,
@PathVariable("role") String role,
@RequestBody Role roleRequest
);
@DeleteMapping("/api/v1/domains/{domain}/roles/{role}")
void deleteRole(@PathVariable("domain") String domain, @PathVariable("role") String role);
// Integration
@PostMapping("/api/v1/integration/perform")
void performIntegration(List<Integration<?>> integrations);
}

View File

@ -1,14 +0,0 @@
package de.mummeit.pmg.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(
name = "pmg",
url = "${pmg.url}"
)
public interface PmgClient {
@GetMapping("/health")
public String getHealthStatus();
}

View File

@ -0,0 +1,43 @@
package de.mummeit.pmg.api.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* Annotation to enforce permission checks on methods.
* The permission check will be performed before the method is executed.
* If the permission check fails, an AccessDeniedException will be thrown.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(RequiresPermissions.class)
public @interface RequiresPermission {
/**
* The domain to check permissions for.
*/
String domain();
/**
* The permission to check.
*/
String permission();
/**
* SpEL expression to determine the scope of the permission check.
* If empty, no scope will be used.
*/
String scope() default "";
/**
* SpEL expression to determine the user ID for the permission check.
* By default, uses the security service to get the current user ID.
* The expression can reference:
* - Method parameters by name
* - Spring beans with the '@' prefix (e.g., '@securityService.getCurrentUserId()')
* - The HTTP request parameters with '#request.getParameter("paramName")'
*/
String userIdExpression() default "@securityService.getCurrentUserId()";
}

View File

@ -0,0 +1,13 @@
package de.mummeit.pmg.api.annotation;
import java.lang.annotation.*;
/**
* Container annotation for multiple {@link RequiresPermission} annotations.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPermissions {
RequiresPermission[] value();
}

View File

@ -0,0 +1,97 @@
package de.mummeit.pmg.api.aspect;
import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.annotation.RequiresPermission;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
@Aspect
@Component
@RequiredArgsConstructor
public class PermissionCheckAspect {
private final PermissionManagerClient permissionManagerClient;
private final ApplicationContext applicationContext;
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(de.mummeit.pmg.api.annotation.RequiresPermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequiresPermission annotation = method.getAnnotation(RequiresPermission.class);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
// Add method parameters to the context
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
if (parameterNames != null) {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
String userId = resolveUserId(context, annotation);
String scope = annotation.scope().isEmpty() ? "" : parser.parseExpression(annotation.scope()).getValue(context, String.class);
CheckAccessRequest request = new CheckAccessRequest();
request.setDomain(annotation.domain());
request.setPermission(annotation.permission());
request.setUserId(userId);
request.setScope(scope);
if (!permissionManagerClient.checkAccess(request).isPermitted()) {
throw new AccessDeniedException(
"Access denied",
userId,
annotation.domain(),
annotation.permission(),
scope
);
}
return joinPoint.proceed();
}
private String resolveUserId(EvaluationContext context, RequiresPermission annotation) {
// First try to evaluate the expression
try {
String userId = parser.parseExpression(annotation.userIdExpression()).getValue(context, String.class);
if (userId != null) {
return userId;
}
} catch (Exception ignored) {
// Fall through to request parameter check
}
// Fallback to request parameter
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(attributes -> ((ServletRequestAttributes) attributes).getRequest())
.map(request -> request.getParameter("userId"))
.orElseThrow(() -> new IllegalArgumentException("Could not resolve userId"));
}
}

View File

@ -0,0 +1,11 @@
package de.mummeit.pmg.api.model.access.request;
import lombok.Data;
@Data
public class CheckAccessRequest {
private String domain;
private String userId;
private String scope;
private String permission;
}

View File

@ -0,0 +1,12 @@
package de.mummeit.pmg.api.model.access.request;
import lombok.Data;
import java.util.List;
@Data
public class Permit {
private String domain;
private List<String> roles;
private List<String> permissions;
}

View File

@ -0,0 +1,12 @@
package de.mummeit.pmg.api.model.access.request;
import lombok.Data;
import java.util.List;
@Data
public class PermitRequest {
private List<Permit> permits;
private String scope;
private String userId;
}

View File

@ -0,0 +1,8 @@
package de.mummeit.pmg.api.model.access.request;
import lombok.Data;
@Data
public class RevokeScopeAccessRequest {
private String scope;
}

View File

@ -0,0 +1,8 @@
package de.mummeit.pmg.api.model.access.request;
import lombok.Data;
@Data
public class RevokeUserAccessRequest {
private String userId;
}

View File

@ -0,0 +1,9 @@
package de.mummeit.pmg.api.model.access.request;
import lombok.Data;
@Data
public class SearchPermitRequest {
private String scope;
private String userId;
}

View File

@ -0,0 +1,8 @@
package de.mummeit.pmg.api.model.access.response;
import lombok.Data;
@Data
public class PermittedResponse {
private boolean permitted;
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.model.integration;
import lombok.Builder;
public class DomainIntegration extends Integration<DomainIntegration.Data> {
@Builder
public DomainIntegration(String id, Action action, Data data) {
super(id, Entity.domain, action, data);
}
@lombok.Data
@Builder
public static class Data {
private String name, oldName, description;
}
}

View File

@ -0,0 +1,29 @@
package de.mummeit.pmg.api.model.integration;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entity")
@Data
@AllArgsConstructor
public abstract class Integration<T> {
public enum Action {
create,
update,
delete
}
public enum Entity {
domain,
role,
permission,
permission_role_relation,
}
private String id;
private Entity entity;
private Action action;
protected T data;
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.model.integration;
import lombok.Builder;
public class PermissionIntegration extends Integration<PermissionIntegration.Data> {
@Builder
public PermissionIntegration(String id, Action action, Data data) {
super(id, Entity.permission, action, data);
}
@Builder
@lombok.Data
public static class Data {
private String domain, name, oldName, description;
}
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.model.integration;
import lombok.Builder;
public class RoleIntegration extends Integration<RoleIntegration.Data> {
@Builder
public RoleIntegration(String id, Action action, Data data) {
super(id, Entity.role, action, data);
}
@Builder
@lombok.Data
public static class Data {
private String domain, name, oldName, description;
}
}

View File

@ -0,0 +1,20 @@
package de.mummeit.pmg.api.model.integration;
import lombok.Builder;
import java.util.List;
public class RolePermissionRelationIntegration extends Integration<RolePermissionRelationIntegration.Data> {
@Builder
public RolePermissionRelationIntegration(String id, Action action, Data data) {
super(id, Entity.permission_role_relation, action, data);
}
@Builder
@lombok.Data
public static class Data {
private String domain, role;
private List<String> permissions;
}
}

View File

@ -0,0 +1,12 @@
package de.mummeit.pmg.api.model.structure;
import lombok.Data;
import java.util.List;
@Data
public class Domain {
private String name;
private String description;
private List<Permission> permissions;
}

View File

@ -0,0 +1,10 @@
package de.mummeit.pmg.api.model.structure;
import lombok.Data;
@Data
public class Permission {
private Domain domain;
private String name;
private String description;
}

View File

@ -0,0 +1,13 @@
package de.mummeit.pmg.api.model.structure;
import lombok.Data;
import java.util.List;
@Data
public class Role {
private Domain domain;
private String name;
private String description;
private List<Permission> permissions;
}

View File

@ -0,0 +1,27 @@
package de.mummeit.pmg.api.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
/**
* Service to retrieve the current user's ID from Spring Security context.
* This is the default user ID provider for the permission manager.
*/
@Service
public class SecurityService {
/**
* Gets the current user's ID from the Spring Security context.
*
* @return the current user's ID
* @throws IllegalStateException if no authenticated user is found
*/
public String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new IllegalStateException("No authenticated user found in security context");
}
return authentication.getName();
}
}

View File

@ -0,0 +1,299 @@
package de.mummeit.pmg.service.builder;
import de.mummeit.pmg.api.model.integration.*;
import java.util.ArrayList;
import java.util.List;
/**
* A fluent builder for creating Permission Manager integrations.
* This builder provides a type-safe way to create various types of integrations including:
* - Domain integrations (create, update, delete)
* - Permission integrations (create, update, delete)
* - Role integrations (create, update, delete)
* - Role-Permission relation integrations
*
* Example usage:
* {@code
* List<Integration<?>> integrations = IntegrationBuilder.create()
* .createDomain("my-domain", "My Domain")
* .addPermission("read", "Read Permission")
* .addRole("user", "User Role")
* .assignPermissionsToRole("user", Arrays.asList("read"))
* .build();
* }
*/
public class IntegrationBuilder {
private final List<Integration<?>> integrations = new ArrayList<>();
private String currentDomain;
/**
* Creates a new instance of the IntegrationBuilder.
*
* @return A new IntegrationBuilder instance
*/
public static IntegrationBuilder create() {
return new IntegrationBuilder();
}
/**
* Selects a domain as the context for subsequent operations without creating an integration.
* This is useful when you need to perform operations on an existing domain.
*
* @param name The name of the domain to select
* @return This builder instance for method chaining
*/
public IntegrationBuilder selectDomain(String name) {
this.currentDomain = name;
return this;
}
/**
* Creates a new domain integration with the specified name and description.
* This also sets the domain context for subsequent operations.
*
* @param name The name of the domain
* @param description The description of the domain
* @return This builder instance for method chaining
*/
public IntegrationBuilder createDomain(String name, String description) {
this.currentDomain = name;
integrations.add(DomainIntegration.builder()
.id(generateIntegrationId("domain", name, Integration.Action.create))
.action(Integration.Action.create)
.data(DomainIntegration.Data.builder()
.name(name)
.description(description)
.build())
.build());
return this;
}
/**
* Updates an existing domain with a new name and/or description.
* This also sets the domain context to the new name for subsequent operations.
*
* @param oldName The current name of the domain to update
* @param newName The new name for the domain
* @param description The new description for the domain
* @return This builder instance for method chaining
*/
public IntegrationBuilder updateDomain(String oldName, String newName, String description) {
integrations.add(DomainIntegration.builder()
.id(generateIntegrationId("domain", oldName, Integration.Action.update))
.action(Integration.Action.update)
.data(DomainIntegration.Data.builder()
.name(newName)
.oldName(oldName)
.description(description)
.build())
.build());
this.currentDomain = newName;
return this;
}
/**
* Deletes a domain.
*
* @param name The name of the domain to delete
* @return This builder instance for method chaining
*/
public IntegrationBuilder deleteDomain(String name) {
integrations.add(DomainIntegration.builder()
.id(generateIntegrationId("domain", name, Integration.Action.delete))
.action(Integration.Action.delete)
.data(DomainIntegration.Data.builder()
.name(name)
.build())
.build());
return this;
}
/**
* Adds a new permission to the current domain.
*
* @param name The name of the permission
* @param description The description of the permission
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder addPermission(String name, String description) {
validateDomain();
integrations.add(PermissionIntegration.builder()
.id(generateIntegrationId("permission", currentDomain + ":" + name, Integration.Action.create))
.action(Integration.Action.create)
.data(PermissionIntegration.Data.builder()
.domain(currentDomain)
.name(name)
.description(description)
.build())
.build());
return this;
}
/**
* Updates an existing permission in the current domain.
*
* @param oldName The current name of the permission to update
* @param newName The new name for the permission
* @param description The new description for the permission
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder updatePermission(String oldName, String newName, String description) {
validateDomain();
integrations.add(PermissionIntegration.builder()
.id(generateIntegrationId("permission", currentDomain + ":" + oldName, Integration.Action.update))
.action(Integration.Action.update)
.data(PermissionIntegration.Data.builder()
.domain(currentDomain)
.name(newName)
.oldName(oldName)
.description(description)
.build())
.build());
return this;
}
/**
* Removes a permission from the current domain.
*
* @param name The name of the permission to remove
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder removePermission(String name) {
validateDomain();
integrations.add(PermissionIntegration.builder()
.id(generateIntegrationId("permission", currentDomain + ":" + name, Integration.Action.delete))
.action(Integration.Action.delete)
.data(PermissionIntegration.Data.builder()
.domain(currentDomain)
.name(name)
.build())
.build());
return this;
}
/**
* Adds a new role to the current domain.
*
* @param name The name of the role
* @param description The description of the role
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder addRole(String name, String description) {
validateDomain();
integrations.add(RoleIntegration.builder()
.id(generateIntegrationId("role", currentDomain + ":" + name, Integration.Action.create))
.action(Integration.Action.create)
.data(RoleIntegration.Data.builder()
.domain(currentDomain)
.name(name)
.description(description)
.build())
.build());
return this;
}
/**
* Updates an existing role in the current domain.
*
* @param oldName The current name of the role to update
* @param newName The new name for the role
* @param description The new description for the role
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder updateRole(String oldName, String newName, String description) {
validateDomain();
integrations.add(RoleIntegration.builder()
.id(generateIntegrationId("role", currentDomain + ":" + oldName, Integration.Action.update))
.action(Integration.Action.update)
.data(RoleIntegration.Data.builder()
.domain(currentDomain)
.name(newName)
.oldName(oldName)
.description(description)
.build())
.build());
return this;
}
/**
* Removes a role from the current domain.
*
* @param name The name of the role to remove
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder removeRole(String name) {
validateDomain();
integrations.add(RoleIntegration.builder()
.id(generateIntegrationId("role", currentDomain + ":" + name, Integration.Action.delete))
.action(Integration.Action.delete)
.data(RoleIntegration.Data.builder()
.domain(currentDomain)
.name(name)
.build())
.build());
return this;
}
/**
* Assigns a list of permissions to a role in the current domain.
*
* @param role The name of the role to assign permissions to
* @param permissions List of permission names to assign to the role
* @return This builder instance for method chaining
* @throws IllegalStateException if no domain context has been set
*/
public IntegrationBuilder assignPermissionsToRole(String role, List<String> permissions) {
validateDomain();
String permissionList = String.join(",", permissions);
integrations.add(RolePermissionRelationIntegration.builder()
.id(generateIntegrationId("role-permissions", currentDomain + ":" + role + ":" + permissionList, Integration.Action.create))
.action(Integration.Action.create)
.data(RolePermissionRelationIntegration.Data.builder()
.domain(currentDomain)
.role(role)
.permissions(permissions)
.build())
.build());
return this;
}
/**
* Builds and returns the list of integrations created by this builder.
*
* @return A new list containing all the integrations created by this builder
*/
public List<Integration<?>> build() {
return new ArrayList<>(integrations);
}
/**
* Validates that a domain context has been set by a previous domain operation.
*
* @throws IllegalStateException if no domain context has been set
*/
private void validateDomain() {
if (currentDomain == null || currentDomain.trim().isEmpty()) {
throw new IllegalStateException("No domain context set. Create, update, or select a domain first.");
}
}
/**
* Generates a deterministic integration ID based on the integration type, name, and action.
* This ensures that the same integration will always have the same ID across different runs.
*
* @param type The type of integration (domain, permission, role, etc.)
* @param name The unique name or identifier for this integration
* @param action The action being performed
* @return A deterministic ID string
*/
private String generateIntegrationId(String type, String name, Integration.Action action) {
return String.format("%s:%s:%s", type, name, action.name().toLowerCase());
}
}

View File

@ -0,0 +1,96 @@
package de.mummeit.pmg.service.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.service.PermissionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* Abstract configuration class for Permission Manager integration.
* Implementing classes can provide integrations either by overriding getIntegrations()
* or by providing a JSON file path in getIntegrationsJsonPath().
*/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractPermissionManagerConfiguration {
private final PermissionManager permissionManager;
private final ResourceLoader resourceLoader;
private final ObjectMapper objectMapper;
/**
* Override this method to provide integrations programmatically.
* By default, returns null, which means no integrations will be performed unless
* getIntegrationsJsonPath() returns a valid path.
*
* @return List of integrations to perform, or null if no integrations should be performed
*/
protected List<Integration<?>> getIntegrations() {
return null;
}
/**
* Override this method to provide the path to a JSON file containing integrations.
* The JSON file should contain an array of Integration objects.
* By default, returns null, which means no JSON file will be loaded.
*
* @return Path to the JSON file, or null if no file should be loaded
*/
protected String getIntegrationsJsonPath() {
return null;
}
/**
* Loads integrations from a JSON file.
*
* @param path Path to the JSON file
* @return List of integrations
* @throws IOException if the file cannot be read or parsed
*/
protected List<Integration<?>> loadIntegrationsFromJson(String path) throws IOException {
Resource resource = resourceLoader.getResource(path);
try (InputStream inputStream = resource.getInputStream()) {
return objectMapper.readValue(inputStream, new TypeReference<List<Integration<?>>>() {});
}
}
/**
* Event listener that performs integrations when the application is ready.
* This method will first try to get integrations from getIntegrations(),
* and if that returns null, it will try to load integrations from the JSON file
* specified by getIntegrationsJsonPath().
*/
@EventListener(ApplicationReadyEvent.class)
public void performIntegrationsOnStartup() {
try {
List<Integration<?>> integrations = getIntegrations();
if (integrations == null) {
String jsonPath = getIntegrationsJsonPath();
if (jsonPath != null) {
integrations = loadIntegrationsFromJson(jsonPath);
}
}
if (integrations != null && !integrations.isEmpty()) {
log.info("Performing {} integrations on startup", integrations.size());
permissionManager.performIntegration(integrations);
log.info("Successfully performed integrations");
} else {
log.info("No integrations to perform");
}
} catch (Exception e) {
log.error("Failed to perform integrations on startup", e);
throw new RuntimeException("Failed to perform integrations on startup", e);
}
}
}

View File

@ -0,0 +1,43 @@
package de.mummeit.pmg.service.exception;
/**
* Exception thrown when access is denied for a user.
*/
public class AccessDeniedException extends PermissionManagerException {
private final String userId;
private final String domain;
private final String permission;
private final String scope;
public AccessDeniedException(String message, String userId, String domain, String permission, String scope) {
super(message);
this.userId = userId;
this.domain = domain;
this.permission = permission;
this.scope = scope;
}
public AccessDeniedException(String message, String userId, String domain, String permission, String scope, Throwable cause) {
super(message, cause);
this.userId = userId;
this.domain = domain;
this.permission = permission;
this.scope = scope;
}
public String getUserId() {
return userId;
}
public String getDomain() {
return domain;
}
public String getPermission() {
return permission;
}
public String getScope() {
return scope;
}
}

View File

@ -0,0 +1,26 @@
package de.mummeit.pmg.service.exception;
import de.mummeit.pmg.api.model.integration.Integration;
import java.util.List;
/**
* Exception thrown when an integration operation fails.
*/
public class IntegrationFailedException extends PermissionManagerException {
private final List<Integration<?>> failedIntegrations;
public IntegrationFailedException(String message, List<Integration<?>> failedIntegrations) {
super(message);
this.failedIntegrations = failedIntegrations;
}
public IntegrationFailedException(String message, List<Integration<?>> failedIntegrations, Throwable cause) {
super(message, cause);
this.failedIntegrations = failedIntegrations;
}
public List<Integration<?>> getFailedIntegrations() {
return failedIntegrations;
}
}

View File

@ -0,0 +1,22 @@
package de.mummeit.pmg.service.exception;
/**
* Exception thrown when a permission request is invalid.
*/
public class InvalidPermissionRequestException extends PermissionManagerException {
private final String reason;
public InvalidPermissionRequestException(String message, String reason) {
super(message);
this.reason = reason;
}
public InvalidPermissionRequestException(String message, String reason, Throwable cause) {
super(message, cause);
this.reason = reason;
}
public String getReason() {
return reason;
}
}

View File

@ -0,0 +1,14 @@
package de.mummeit.pmg.service.exception;
/**
* Base exception class for all Permission Manager related exceptions.
*/
public class PermissionManagerException extends RuntimeException {
public PermissionManagerException(String message) {
super(message);
}
public PermissionManagerException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,376 @@
package de.mummeit.pmg.service;
import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.model.access.request.*;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import de.mummeit.pmg.service.exception.IntegrationFailedException;
import de.mummeit.pmg.service.exception.InvalidPermissionRequestException;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@Service
@RequiredArgsConstructor
public class PermissionManager {
private final PermissionManagerClient client;
/**
* Checks if a user has access to a specific permission in a domain and scope.
*
* @param userId The ID of the user
* @param domain The domain name
* @param permission The permission name
* @param scope The scope
* @return true if the user has access, false otherwise
* @throws InvalidPermissionRequestException if any of the required parameters are null or empty
* @throws AccessDeniedException if the access check fails
*/
public boolean hasAccess(String userId, String domain, String permission, String scope) {
validateParameters(userId, domain, permission, scope);
try {
CheckAccessRequest request = new CheckAccessRequest();
request.setUserId(userId);
request.setDomain(domain);
request.setPermission(permission);
request.setScope(scope);
PermittedResponse response = client.checkAccess(request);
return response.isPermitted();
} catch (FeignException e) {
throw new AccessDeniedException(
"Failed to check access",
userId,
domain,
permission,
scope,
e
);
}
}
/**
* Grants a user access to specific permissions and roles in a domain and scope.
*
* @param userId The ID of the user
* @param domain The domain name
* @param permissions List of permission names to grant
* @param roles List of role names to grant
* @param scope The scope
* @throws InvalidPermissionRequestException if any of the required parameters are invalid
*/
public void grantAccess(String userId, String domain, List<String> permissions, List<String> roles, String scope) {
validateParameters(userId, domain, scope);
validatePermissionsAndRoles(permissions, roles);
try {
PermitRequest request = new PermitRequest();
request.setUserId(userId);
request.setScope(scope);
Permit permit = new Permit();
permit.setDomain(domain);
permit.setPermissions(permissions);
permit.setRoles(roles);
request.setPermits(Collections.singletonList(permit));
client.permitAccess(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to grant access",
"Error occurred while granting permissions",
e
);
}
}
/**
* Revokes a user's access to specific permissions and roles in a domain and scope.
*
* @param userId The ID of the user
* @param domain The domain name
* @param permissions List of permission names to revoke
* @param roles List of role names to revoke
* @param scope The scope
* @throws InvalidPermissionRequestException if any of the required parameters are invalid
*/
public void revokeAccess(String userId, String domain, List<String> permissions, List<String> roles, String scope) {
validateParameters(userId, domain, scope);
validatePermissionsAndRoles(permissions, roles);
try {
PermitRequest request = new PermitRequest();
request.setUserId(userId);
request.setScope(scope);
Permit permit = new Permit();
permit.setDomain(domain);
permit.setPermissions(permissions);
permit.setRoles(roles);
request.setPermits(Collections.singletonList(permit));
client.revokeAccess(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to revoke access",
"Error occurred while revoking permissions",
e
);
}
}
/**
* Revokes all access for a specific user.
*
* @param userId The ID of the user
* @throws InvalidPermissionRequestException if the userId is null or empty
*/
public void revokeAllUserAccess(String userId) {
validateUserId(userId);
try {
RevokeUserAccessRequest request = new RevokeUserAccessRequest();
request.setUserId(userId);
client.revokeUserAccess(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to revoke all user access",
"Error occurred while revoking all user permissions",
e
);
}
}
/**
* Revokes all access for a specific scope.
*
* @param scope The scope to revoke all access from
* @throws InvalidPermissionRequestException if the scope is null or empty
*/
public void revokeAllScopeAccess(String scope) {
validateScope(scope);
try {
RevokeScopeAccessRequest request = new RevokeScopeAccessRequest();
request.setScope(scope);
client.revokeScopeAccess(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to revoke all scope access",
"Error occurred while revoking all scope permissions",
e
);
}
}
/**
* Searches for all permissions granted to a user in a specific scope.
*
* @param userId The ID of the user
* @param scope The scope to search in
* @return List of permissions granted to the user
* @throws InvalidPermissionRequestException if any of the required parameters are invalid
*/
public List<Permission> findUserPermissions(String userId, String scope) {
validateParameters(userId, scope);
try {
SearchPermitRequest request = new SearchPermitRequest();
request.setUserId(userId);
request.setScope(scope);
return client.searchPermits(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to find user permissions",
"Error occurred while searching for user permissions",
e
);
}
}
/**
* Performs integration operations.
*
* @param integrations List of integrations to perform
* @throws IntegrationFailedException if the integration operations fail
* @throws InvalidPermissionRequestException if the integrations list is null or empty
*/
public void performIntegration(List<Integration<?>> integrations) {
if (integrations == null || integrations.isEmpty()) {
throw new InvalidPermissionRequestException(
"Invalid integration request",
"Integrations list cannot be null or empty"
);
}
try {
client.performIntegration(integrations);
} catch (FeignException e) {
throw new IntegrationFailedException(
"Failed to perform integrations",
integrations,
e
);
}
}
/**
* Grants multiple permissions and roles across different domains to a user.
*
* @param userId The ID of the user
* @param permits List of permits containing domain-specific permissions and roles
* @param scope The scope
* @throws InvalidPermissionRequestException if any of the required parameters are invalid
*/
public void grantMultiDomainAccess(String userId, List<Permit> permits, String scope) {
validateParameters(userId, scope);
validatePermits(permits);
try {
PermitRequest request = new PermitRequest();
request.setUserId(userId);
request.setScope(scope);
request.setPermits(permits);
client.permitAccess(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to grant multi-domain access",
"Error occurred while granting multi-domain permissions",
e
);
}
}
/**
* Revokes multiple permissions and roles across different domains from a user.
*
* @param userId The ID of the user
* @param permits List of permits containing domain-specific permissions and roles
* @param scope The scope
* @throws InvalidPermissionRequestException if any of the required parameters are invalid
*/
public void revokeMultiDomainAccess(String userId, List<Permit> permits, String scope) {
validateParameters(userId, scope);
validatePermits(permits);
try {
PermitRequest request = new PermitRequest();
request.setUserId(userId);
request.setScope(scope);
request.setPermits(permits);
client.revokeAccess(request);
} catch (FeignException e) {
throw new InvalidPermissionRequestException(
"Failed to revoke multi-domain access",
"Error occurred while revoking multi-domain permissions",
e
);
}
}
private void validateParameters(String userId, String domain, String permission, String scope) {
validateUserId(userId);
validateDomain(domain);
validatePermission(permission);
validateScope(scope);
}
private void validateParameters(String userId, String domain, String scope) {
validateUserId(userId);
validateDomain(domain);
validateScope(scope);
}
private void validateParameters(String userId, String scope) {
validateUserId(userId);
validateScope(scope);
}
private void validateUserId(String userId) {
if (userId == null || userId.trim().isEmpty()) {
throw new InvalidPermissionRequestException(
"Invalid user ID",
"User ID cannot be null or empty"
);
}
}
private void validateDomain(String domain) {
if (domain == null || domain.trim().isEmpty()) {
throw new InvalidPermissionRequestException(
"Invalid domain",
"Domain cannot be null or empty"
);
}
}
private void validatePermission(String permission) {
if (permission == null || permission.trim().isEmpty()) {
throw new InvalidPermissionRequestException(
"Invalid permission",
"Permission cannot be null or empty"
);
}
}
private void validateScope(String scope) {
if (scope == null || scope.trim().isEmpty()) {
throw new InvalidPermissionRequestException(
"Invalid scope",
"Scope cannot be null or empty"
);
}
}
private void validatePermissionsAndRoles(List<String> permissions, List<String> roles) {
if ((permissions == null || permissions.isEmpty()) && (roles == null || roles.isEmpty())) {
throw new InvalidPermissionRequestException(
"Invalid permissions and roles",
"At least one permission or role must be specified"
);
}
if (permissions != null && permissions.stream().anyMatch(p -> p == null || p.trim().isEmpty())) {
throw new InvalidPermissionRequestException(
"Invalid permissions",
"Permissions cannot contain null or empty values"
);
}
if (roles != null && roles.stream().anyMatch(r -> r == null || r.trim().isEmpty())) {
throw new InvalidPermissionRequestException(
"Invalid roles",
"Roles cannot contain null or empty values"
);
}
}
private void validatePermits(List<Permit> permits) {
if (permits == null || permits.isEmpty()) {
throw new InvalidPermissionRequestException(
"Invalid permits",
"Permits list cannot be null or empty"
);
}
if (permits.stream().anyMatch(Objects::isNull)) {
throw new InvalidPermissionRequestException(
"Invalid permits",
"Permits list cannot contain null values"
);
}
permits.forEach(permit -> {
validateDomain(permit.getDomain());
validatePermissionsAndRoles(permit.getPermissions(), permit.getRoles());
});
}
}

View File

@ -1,12 +1,14 @@
spring:
application:
name: permisssion-manager-sdk
feign:
client:
config:
randomjokeapi:
connect-timeout: 1000
read-timeout: 2000
randomjokeapi:
url: https://official-joke-api.appspot.com/random_joke
main:
allow-bean-definition-overriding: true
cloud:
openfeign:
client:
config:
permission-manager:
connect-timeout: 1000
read-timeout: 2000
permission-manager:
url: http://localhost:6060

View File

@ -0,0 +1,254 @@
package de.mummeit.pmg.api;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest;
import de.mummeit.pmg.api.model.access.request.Permit;
import de.mummeit.pmg.api.model.access.request.PermitRequest;
import de.mummeit.pmg.api.model.access.request.RevokeScopeAccessRequest;
import de.mummeit.pmg.api.model.access.request.RevokeUserAccessRequest;
import de.mummeit.pmg.api.model.access.request.SearchPermitRequest;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.model.integration.DomainIntegration;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.integration.PermissionIntegration;
import de.mummeit.pmg.api.model.integration.RoleIntegration;
import de.mummeit.pmg.api.model.integration.RolePermissionRelationIntegration;
import de.mummeit.pmg.api.model.structure.Domain;
import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.api.model.structure.Role;
import de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class PermissionManagerClientIntegrationTest extends BaseIntegrationTest {
@Autowired
private PermissionManagerClient permissionManagerClient;
private static final String TEST_DOMAIN = "test-domain";
private static final String TEST_PERMISSION = "test-permission";
private static final String TEST_ROLE = "test-role";
private static final String TEST_USER = "test-user";
private static final String TEST_SCOPE = "test-scope";
@Test
@DisplayName("should return health status")
void getHealthStatus() {
String healthStatus = permissionManagerClient.getHealthStatus();
assertNotNull(healthStatus);
}
@Test
@DisplayName("should successfully perform integrations")
void integrationShouldRun() {
List<Integration<?>> integrations = List.of(
DomainIntegration.builder()
.id("1")
.action(Integration.Action.create)
.data(DomainIntegration.Data.builder()
.name("test")
.description("test")
.build())
.build(),
PermissionIntegration.builder()
.id("2")
.action(Integration.Action.create)
.data(PermissionIntegration.Data.builder()
.domain("test")
.name("test")
.description("test")
.build())
.build(),
RoleIntegration.builder()
.id("3")
.action(Integration.Action.create)
.data(RoleIntegration.Data.builder()
.domain("test")
.name("test")
.description("test")
.build())
.build(),
RolePermissionRelationIntegration.builder()
.id("4")
.action(Integration.Action.create)
.data(RolePermissionRelationIntegration.Data.builder()
.domain("test")
.role("test")
.permissions(List.of("test"))
.build())
.build()
);
assertDoesNotThrow(() -> permissionManagerClient.performIntegration(integrations));
}
@Test
@DisplayName("should manage domains successfully")
void domainManagement() {
// Create domain
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
domain.setDescription("Test Domain Description");
Domain createdDomain = permissionManagerClient.createDomain(domain);
assertNotNull(createdDomain);
assertEquals(TEST_DOMAIN, createdDomain.getName());
// Get domain
Domain retrievedDomain = permissionManagerClient.getDomain(TEST_DOMAIN);
assertNotNull(retrievedDomain);
assertEquals(TEST_DOMAIN, retrievedDomain.getName());
// Update domain
domain.setDescription("Updated Description");
Domain updatedDomain = permissionManagerClient.updateDomain(TEST_DOMAIN, domain);
assertNotNull(updatedDomain);
assertEquals("Updated Description", updatedDomain.getDescription());
// Delete domain
assertDoesNotThrow(() -> permissionManagerClient.deleteDomain(TEST_DOMAIN));
}
@Test
@DisplayName("should manage permissions successfully")
void permissionManagement() {
// First create a domain
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
domain.setDescription("Test Domain Description");
permissionManagerClient.createDomain(domain);
// Create permission
Permission permission = new Permission();
permission.setName(TEST_PERMISSION);
permission.setDescription("Test Permission Description");
Permission createdPermission = permissionManagerClient.createPermission(TEST_DOMAIN, permission);
assertNotNull(createdPermission);
assertEquals(TEST_PERMISSION, createdPermission.getName());
// Get permission
Permission retrievedPermission = permissionManagerClient.getPermission(TEST_DOMAIN, TEST_PERMISSION);
assertNotNull(retrievedPermission);
assertEquals(TEST_PERMISSION, retrievedPermission.getName());
// Update permission
permission.setDescription("Updated Permission Description");
Permission updatedPermission = permissionManagerClient.updatePermission(TEST_DOMAIN, TEST_PERMISSION, permission);
assertNotNull(updatedPermission);
assertEquals("Updated Permission Description", updatedPermission.getDescription());
// Delete permission
assertDoesNotThrow(() -> permissionManagerClient.deletePermission(TEST_DOMAIN, TEST_PERMISSION));
// Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN);
}
@Test
@DisplayName("should manage roles successfully")
void roleManagement() {
// First create a domain
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
domain.setDescription("Test Domain Description");
permissionManagerClient.createDomain(domain);
// Create role
Role role = new Role();
role.setName(TEST_ROLE);
role.setDescription("Test Role Description");
role.setPermissions(List.of());
Role createdRole = permissionManagerClient.createRole(TEST_DOMAIN, role);
assertNotNull(createdRole);
assertEquals(TEST_ROLE, createdRole.getName());
// Get role
Role retrievedRole = permissionManagerClient.getRole(TEST_DOMAIN, TEST_ROLE);
assertNotNull(retrievedRole);
assertEquals(TEST_ROLE, retrievedRole.getName());
// Update role
role.setDescription("Updated Role Description");
Role updatedRole = permissionManagerClient.updateRole(TEST_DOMAIN, TEST_ROLE, role);
assertNotNull(updatedRole);
assertEquals("Updated Role Description", updatedRole.getDescription());
// Delete role
assertDoesNotThrow(() -> permissionManagerClient.deleteRole(TEST_DOMAIN, TEST_ROLE));
// Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN);
}
@Test
@DisplayName("should manage access successfully")
void accessManagement() {
// Setup: Create domain, permission, and role
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
permissionManagerClient.createDomain(domain);
Permission permission = new Permission();
permission.setName(TEST_PERMISSION);
permissionManagerClient.createPermission(TEST_DOMAIN, permission);
Role role = new Role();
role.setName(TEST_ROLE);
role.setPermissions(List.of());
permissionManagerClient.createRole(TEST_DOMAIN, role);
// Test permit access
PermitRequest permitRequest = new PermitRequest();
Permit permit = new Permit();
permit.setDomain(TEST_DOMAIN);
permit.setRoles(List.of(TEST_ROLE));
permit.setPermissions(List.of(TEST_PERMISSION));
permitRequest.setPermits(List.of(permit));
permitRequest.setUserId(TEST_USER);
permitRequest.setScope(TEST_SCOPE);
assertDoesNotThrow(() -> permissionManagerClient.permitAccess(permitRequest));
// Test check access
CheckAccessRequest checkRequest = new CheckAccessRequest();
checkRequest.setDomain(TEST_DOMAIN);
checkRequest.setPermission(TEST_PERMISSION);
checkRequest.setUserId(TEST_USER);
checkRequest.setScope(TEST_SCOPE);
PermittedResponse response = permissionManagerClient.checkAccess(checkRequest);
assertNotNull(response);
assertTrue(response.isPermitted());
// Test search permits
SearchPermitRequest searchRequest = new SearchPermitRequest();
searchRequest.setUserId(TEST_USER);
searchRequest.setScope(TEST_SCOPE);
List<Permission> permits = permissionManagerClient.searchPermits(searchRequest);
assertNotNull(permits);
assertFalse(permits.isEmpty());
// Test revoke access
assertDoesNotThrow(() -> permissionManagerClient.revokeAccess(permitRequest));
// Test revoke scope access
RevokeScopeAccessRequest revokeScopeRequest = new RevokeScopeAccessRequest();
revokeScopeRequest.setScope(TEST_SCOPE);
assertDoesNotThrow(() -> permissionManagerClient.revokeScopeAccess(revokeScopeRequest));
// Test revoke user access
RevokeUserAccessRequest revokeUserRequest = new RevokeUserAccessRequest();
revokeUserRequest.setUserId(TEST_USER);
assertDoesNotThrow(() -> permissionManagerClient.revokeUserAccess(revokeUserRequest));
// Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN);
}
}

View File

@ -0,0 +1,20 @@
package de.mummeit.pmg.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class TestSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}

View File

@ -0,0 +1,19 @@
package de.mummeit.pmg.api.controller;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
@EnableAutoConfiguration
@EnableWebMvc
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {
"de.mummeit.pmg.api.controller",
"de.mummeit.pmg.api.aspect"
})
public class TestConfig implements WebMvcConfigurer {
}

View File

@ -0,0 +1,65 @@
package de.mummeit.pmg.api.controller;
import de.mummeit.pmg.api.annotation.RequiresPermission;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/resource/{resourceId}")
@RequiresPermission(
domain = "test-domain",
permission = "read",
scope = "#resourceId",
userIdExpression = "#userId"
)
public String getResource(@PathVariable String resourceId, @RequestParam String userId) {
return "Access granted to resource " + resourceId + " for user " + userId;
}
@PostMapping("/resource/{resourceId}/action")
@RequiresPermission(
domain = "test-domain",
permission = "write",
scope = "#resourceId",
userIdExpression = "#request.userId"
)
public String performAction(
@PathVariable String resourceId,
@RequestBody ActionRequest request
) {
return "Action performed on resource " + resourceId + " by user " + request.getUserId();
}
@GetMapping("/secured-resource/{resourceId}")
@RequiresPermission(
domain = "test-domain",
permission = "read",
scope = "#resourceId"
)
public String getSecuredResource(@PathVariable String resourceId) {
return "Access granted to secured resource " + resourceId;
}
public static class ActionRequest {
private String userId;
private String action;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}
}

View File

@ -0,0 +1,168 @@
package de.mummeit.pmg.api.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.config.TestSecurityConfig;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.service.SecurityService;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
@ComponentScan(basePackages = {
"de.mummeit.pmg.api.controller",
"de.mummeit.pmg.api.aspect",
"de.mummeit.pmg.api.service"
})
@Import(TestSecurityConfig.class)
class TestControllerIntegrationTest extends BaseIntegrationTest {
@TestConfiguration
@EnableWebMvc
static class TestConfig {
@Bean
public TestController testController() {
return new TestController();
}
@Bean
public TestExceptionHandler testExceptionHandler() {
return new TestExceptionHandler();
}
@Bean
public SecurityService securityService() {
return new SecurityService();
}
}
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private PermissionManagerClient permissionManagerClient;
@Test
void getResource_whenPermitted_shouldSucceed() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/resource/123")
.param("userId", "user1"))
.andExpect(status().isOk())
.andExpect(content().string("Access granted to resource 123 for user user1"));
}
@Test
void getResource_whenDenied_shouldFail() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(false);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/resource/123")
.param("userId", "user1"))
.andExpect(status().isForbidden())
.andExpect(result -> result.getResolvedException().getClass().equals(AccessDeniedException.class));
}
@Test
void performAction_whenPermitted_shouldSucceed() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
TestController.ActionRequest request = new TestController.ActionRequest();
request.setUserId("user1");
request.setAction("test");
// When/Then
mockMvc.perform(post("/api/test/resource/123/action")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(content().string("Action performed on resource 123 by user user1"));
}
@Test
void performAction_whenDenied_shouldFail() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(false);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
TestController.ActionRequest request = new TestController.ActionRequest();
request.setUserId("user1");
request.setAction("test");
// When/Then
mockMvc.perform(post("/api/test/resource/123/action")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isForbidden())
.andExpect(result -> result.getResolvedException().getClass().equals(AccessDeniedException.class));
}
@Test
void getSecuredResource_whenPermitted_shouldSucceed() throws Exception {
// Given
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user1", null)
);
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/secured-resource/123"))
.andExpect(status().isOk())
.andExpect(content().string("Access granted to secured resource 123"));
}
@Test
void getSecuredResource_whenDenied_shouldFail() throws Exception {
// Given
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user1", null)
);
PermittedResponse response = new PermittedResponse();
response.setPermitted(false);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/secured-resource/123"))
.andExpect(status().isForbidden())
.andExpect(result -> result.getResolvedException().getClass().equals(AccessDeniedException.class));
}
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.controller;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice(basePackageClasses = TestController.class)
public class TestExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public void handleAccessDeniedException() {
// No response body needed, just return 403 status
}
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class SecurityService {
public String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new IllegalStateException("No authenticated user found");
}
return authentication.getName();
}
}

View File

@ -0,0 +1,260 @@
package de.mummeit.pmg.service;
import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.model.access.request.*;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import de.mummeit.pmg.service.exception.IntegrationFailedException;
import de.mummeit.pmg.service.exception.InvalidPermissionRequestException;
import feign.FeignException;
import feign.Request;
import feign.RequestTemplate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PermissionManagerTest {
@Mock
private PermissionManagerClient client;
private PermissionManager permissionManager;
private static final String TEST_USER = "test-user";
private static final String TEST_DOMAIN = "test-domain";
private static final String TEST_PERMISSION = "test-permission";
private static final String TEST_ROLE = "test-role";
private static final String TEST_SCOPE = "test-scope";
@BeforeEach
void setUp() {
permissionManager = new PermissionManager(client);
}
@Test
@DisplayName("hasAccess should return true when user has permission")
void hasAccessShouldReturnTrueWhenUserHasPermission() {
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(client.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
boolean hasAccess = permissionManager.hasAccess(TEST_USER, TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE);
assertTrue(hasAccess);
verify(client).checkAccess(any(CheckAccessRequest.class));
}
@Test
@DisplayName("hasAccess should throw AccessDeniedException when client throws FeignException")
void hasAccessShouldThrowAccessDeniedExceptionWhenClientThrowsFeignException() {
FeignException feignException = new FeignException.NotFound("Not Found", createRequest(), null, null);
when(client.checkAccess(any(CheckAccessRequest.class))).thenThrow(feignException);
assertThrows(AccessDeniedException.class, () ->
permissionManager.hasAccess(TEST_USER, TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE)
);
}
@Test
@DisplayName("hasAccess should throw InvalidPermissionRequestException when parameters are invalid")
void hasAccessShouldThrowInvalidPermissionRequestExceptionWhenParametersAreInvalid() {
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.hasAccess(null, TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE)
);
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.hasAccess("", TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE)
);
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.hasAccess(TEST_USER, null, TEST_PERMISSION, TEST_SCOPE)
);
}
@Test
@DisplayName("grantAccess should throw InvalidPermissionRequestException when permissions and roles are empty")
void grantAccessShouldThrowInvalidPermissionRequestExceptionWhenPermissionsAndRolesAreEmpty() {
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.grantAccess(TEST_USER, TEST_DOMAIN, Collections.emptyList(), Collections.emptyList(), TEST_SCOPE)
);
}
@Test
@DisplayName("performIntegration should throw IntegrationFailedException when client throws exception")
void performIntegrationShouldThrowIntegrationFailedExceptionWhenClientThrowsException() {
List<Integration<?>> integrations = Collections.singletonList(mock(Integration.class));
FeignException feignException = new FeignException.InternalServerError("Internal Server Error", createRequest(), null, null);
doThrow(feignException).when(client).performIntegration(any());
IntegrationFailedException exception = assertThrows(IntegrationFailedException.class, () ->
permissionManager.performIntegration(integrations)
);
assertEquals(integrations, exception.getFailedIntegrations());
}
@Test
@DisplayName("grantMultiDomainAccess should validate permits")
void grantMultiDomainAccessShouldValidatePermits() {
Permit invalidPermit = new Permit();
invalidPermit.setDomain(""); // Invalid domain
List<Permit> permits = Collections.singletonList(invalidPermit);
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.grantMultiDomainAccess(TEST_USER, permits, TEST_SCOPE)
);
}
@Test
@DisplayName("revokeMultiDomainAccess should validate permits")
void revokeMultiDomainAccessShouldValidatePermits() {
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.revokeMultiDomainAccess(TEST_USER, null, TEST_SCOPE)
);
assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.revokeMultiDomainAccess(TEST_USER, Collections.emptyList(), TEST_SCOPE)
);
}
@Test
@DisplayName("grantAccess should call client with correct request")
void grantAccessShouldCallClientWithCorrectRequest() {
List<String> permissions = Collections.singletonList(TEST_PERMISSION);
List<String> roles = Collections.singletonList(TEST_ROLE);
permissionManager.grantAccess(TEST_USER, TEST_DOMAIN, permissions, roles, TEST_SCOPE);
verify(client).permitAccess(argThat(request -> {
Permit permit = request.getPermits().get(0);
return TEST_USER.equals(request.getUserId()) &&
TEST_SCOPE.equals(request.getScope()) &&
TEST_DOMAIN.equals(permit.getDomain()) &&
permit.getPermissions().contains(TEST_PERMISSION) &&
permit.getRoles().contains(TEST_ROLE);
}));
}
@Test
@DisplayName("revokeAccess should call client with correct request")
void revokeAccessShouldCallClientWithCorrectRequest() {
List<String> permissions = Collections.singletonList(TEST_PERMISSION);
List<String> roles = Collections.singletonList(TEST_ROLE);
permissionManager.revokeAccess(TEST_USER, TEST_DOMAIN, permissions, roles, TEST_SCOPE);
verify(client).revokeAccess(argThat(request -> {
Permit permit = request.getPermits().get(0);
return TEST_USER.equals(request.getUserId()) &&
TEST_SCOPE.equals(request.getScope()) &&
TEST_DOMAIN.equals(permit.getDomain()) &&
permit.getPermissions().contains(TEST_PERMISSION) &&
permit.getRoles().contains(TEST_ROLE);
}));
}
@Test
@DisplayName("revokeAllUserAccess should call client with correct request")
void revokeAllUserAccessShouldCallClientWithCorrectRequest() {
permissionManager.revokeAllUserAccess(TEST_USER);
verify(client).revokeUserAccess(argThat(request ->
TEST_USER.equals(request.getUserId())
));
}
@Test
@DisplayName("revokeAllScopeAccess should call client with correct request")
void revokeAllScopeAccessShouldCallClientWithCorrectRequest() {
permissionManager.revokeAllScopeAccess(TEST_SCOPE);
verify(client).revokeScopeAccess(argThat(request ->
TEST_SCOPE.equals(request.getScope())
));
}
@Test
@DisplayName("findUserPermissions should return permissions from client")
void findUserPermissionsShouldReturnPermissionsFromClient() {
Permission permission = new Permission();
permission.setName(TEST_PERMISSION);
List<Permission> expectedPermissions = Collections.singletonList(permission);
when(client.searchPermits(any(SearchPermitRequest.class))).thenReturn(expectedPermissions);
List<Permission> actualPermissions = permissionManager.findUserPermissions(TEST_USER, TEST_SCOPE);
assertEquals(expectedPermissions, actualPermissions);
verify(client).searchPermits(argThat(request ->
TEST_USER.equals(request.getUserId()) &&
TEST_SCOPE.equals(request.getScope())
));
}
@Test
@DisplayName("grantMultiDomainAccess should call client with correct request")
void grantMultiDomainAccessShouldCallClientWithCorrectRequest() {
Permit permit1 = new Permit();
permit1.setDomain(TEST_DOMAIN);
permit1.setPermissions(Collections.singletonList(TEST_PERMISSION));
permit1.setRoles(Collections.singletonList(TEST_ROLE));
Permit permit2 = new Permit();
permit2.setDomain(TEST_DOMAIN + "-2");
permit2.setPermissions(Collections.singletonList(TEST_PERMISSION + "-2"));
permit2.setRoles(Collections.singletonList(TEST_ROLE + "-2"));
List<Permit> permits = Arrays.asList(permit1, permit2);
permissionManager.grantMultiDomainAccess(TEST_USER, permits, TEST_SCOPE);
verify(client).permitAccess(argThat(request ->
TEST_USER.equals(request.getUserId()) &&
TEST_SCOPE.equals(request.getScope()) &&
request.getPermits().equals(permits)
));
}
@Test
@DisplayName("revokeMultiDomainAccess should call client with correct request")
void revokeMultiDomainAccessShouldCallClientWithCorrectRequest() {
Permit permit1 = new Permit();
permit1.setDomain(TEST_DOMAIN);
permit1.setPermissions(Collections.singletonList(TEST_PERMISSION));
permit1.setRoles(Collections.singletonList(TEST_ROLE));
Permit permit2 = new Permit();
permit2.setDomain(TEST_DOMAIN + "-2");
permit2.setPermissions(Collections.singletonList(TEST_PERMISSION + "-2"));
permit2.setRoles(Collections.singletonList(TEST_ROLE + "-2"));
List<Permit> permits = Arrays.asList(permit1, permit2);
permissionManager.revokeMultiDomainAccess(TEST_USER, permits, TEST_SCOPE);
verify(client).revokeAccess(argThat(request ->
TEST_USER.equals(request.getUserId()) &&
TEST_SCOPE.equals(request.getScope()) &&
request.getPermits().equals(permits)
));
}
private Request createRequest() {
return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate());
}
}

View File

@ -0,0 +1,286 @@
package de.mummeit.pmg.service.builder;
import de.mummeit.pmg.api.model.integration.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class IntegrationBuilderTest {
private static final String TEST_DOMAIN = "test-domain";
private static final String TEST_DESCRIPTION = "test-description";
private static final String TEST_PERMISSION = "test-permission";
private static final String TEST_ROLE = "test-role";
@Test
@DisplayName("should build domain integrations")
void shouldBuildDomainIntegrations() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.build();
assertEquals(1, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof DomainIntegration);
DomainIntegration domainIntegration = (DomainIntegration) integration;
assertEquals(Integration.Action.create, domainIntegration.getAction());
assertEquals(TEST_DOMAIN, domainIntegration.getData().getName());
assertEquals(TEST_DESCRIPTION, domainIntegration.getData().getDescription());
assertEquals("domain:" + TEST_DOMAIN + ":create", domainIntegration.getId());
}
@Test
@DisplayName("should update domain integrations")
void shouldUpdateDomainIntegrations() {
String newName = "new-domain";
String newDescription = "Updated Description";
List<Integration<?>> integrations = IntegrationBuilder.create()
.updateDomain(TEST_DOMAIN, newName, newDescription)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.build();
assertEquals(2, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof DomainIntegration);
DomainIntegration domainIntegration = (DomainIntegration) integration;
assertEquals(Integration.Action.update, domainIntegration.getAction());
assertEquals(newName, domainIntegration.getData().getName());
assertEquals(TEST_DOMAIN, domainIntegration.getData().getOldName());
assertEquals(newDescription, domainIntegration.getData().getDescription());
assertEquals("domain:" + TEST_DOMAIN + ":update", domainIntegration.getId());
PermissionIntegration permissionIntegration = (PermissionIntegration) integrations.get(1);
assertEquals(newName, permissionIntegration.getData().getDomain());
}
@Test
@DisplayName("should delete domain integrations")
void shouldDeleteDomainIntegrations() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.deleteDomain(TEST_DOMAIN)
.build();
assertEquals(1, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof DomainIntegration);
DomainIntegration domainIntegration = (DomainIntegration) integration;
assertEquals(Integration.Action.delete, domainIntegration.getAction());
assertEquals(TEST_DOMAIN, domainIntegration.getData().getName());
assertEquals("domain:" + TEST_DOMAIN + ":delete", domainIntegration.getId());
}
@Test
@DisplayName("should build permission integrations")
void shouldBuildPermissionIntegrations() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.build();
assertEquals(2, integrations.size());
Integration<?> integration = integrations.get(1);
assertTrue(integration instanceof PermissionIntegration);
PermissionIntegration permissionIntegration = (PermissionIntegration) integration;
assertEquals(Integration.Action.create, permissionIntegration.getAction());
assertEquals(TEST_DOMAIN, permissionIntegration.getData().getDomain());
assertEquals(TEST_PERMISSION, permissionIntegration.getData().getName());
assertEquals(TEST_DESCRIPTION, permissionIntegration.getData().getDescription());
assertEquals("permission:" + TEST_DOMAIN + ":" + TEST_PERMISSION + ":create", permissionIntegration.getId());
}
@Test
@DisplayName("should update permission integrations")
void shouldUpdatePermissionIntegrations() {
String newName = "new-permission";
String newDescription = "Updated Permission";
List<Integration<?>> integrations = IntegrationBuilder.create()
.selectDomain(TEST_DOMAIN)
.updatePermission(TEST_PERMISSION, newName, newDescription)
.build();
assertEquals(1, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof PermissionIntegration);
PermissionIntegration permissionIntegration = (PermissionIntegration) integration;
assertEquals(Integration.Action.update, permissionIntegration.getAction());
assertEquals(TEST_DOMAIN, permissionIntegration.getData().getDomain());
assertEquals(newName, permissionIntegration.getData().getName());
assertEquals(TEST_PERMISSION, permissionIntegration.getData().getOldName());
assertEquals(newDescription, permissionIntegration.getData().getDescription());
assertEquals("permission:" + TEST_DOMAIN + ":" + TEST_PERMISSION + ":update", permissionIntegration.getId());
}
@Test
@DisplayName("should remove permission integrations")
void shouldRemovePermissionIntegrations() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.selectDomain(TEST_DOMAIN)
.removePermission(TEST_PERMISSION)
.build();
assertEquals(1, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof PermissionIntegration);
PermissionIntegration permissionIntegration = (PermissionIntegration) integration;
assertEquals(Integration.Action.delete, permissionIntegration.getAction());
assertEquals(TEST_DOMAIN, permissionIntegration.getData().getDomain());
assertEquals(TEST_PERMISSION, permissionIntegration.getData().getName());
assertEquals("permission:" + TEST_DOMAIN + ":" + TEST_PERMISSION + ":delete", permissionIntegration.getId());
}
@Test
@DisplayName("should build role integrations")
void shouldBuildRoleIntegrations() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.addRole(TEST_ROLE, TEST_DESCRIPTION)
.build();
assertEquals(2, integrations.size());
Integration<?> integration = integrations.get(1);
assertTrue(integration instanceof RoleIntegration);
RoleIntegration roleIntegration = (RoleIntegration) integration;
assertEquals(Integration.Action.create, roleIntegration.getAction());
assertEquals(TEST_DOMAIN, roleIntegration.getData().getDomain());
assertEquals(TEST_ROLE, roleIntegration.getData().getName());
assertEquals(TEST_DESCRIPTION, roleIntegration.getData().getDescription());
assertEquals("role:" + TEST_DOMAIN + ":" + TEST_ROLE + ":create", roleIntegration.getId());
}
@Test
@DisplayName("should update role integrations")
void shouldUpdateRoleIntegrations() {
String newName = "new-role";
String newDescription = "Updated Role";
List<Integration<?>> integrations = IntegrationBuilder.create()
.selectDomain(TEST_DOMAIN)
.updateRole(TEST_ROLE, newName, newDescription)
.build();
assertEquals(1, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof RoleIntegration);
RoleIntegration roleIntegration = (RoleIntegration) integration;
assertEquals(Integration.Action.update, roleIntegration.getAction());
assertEquals(TEST_DOMAIN, roleIntegration.getData().getDomain());
assertEquals(newName, roleIntegration.getData().getName());
assertEquals(TEST_ROLE, roleIntegration.getData().getOldName());
assertEquals(newDescription, roleIntegration.getData().getDescription());
assertEquals("role:" + TEST_DOMAIN + ":" + TEST_ROLE + ":update", roleIntegration.getId());
}
@Test
@DisplayName("should remove role integrations")
void shouldRemoveRoleIntegrations() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.selectDomain(TEST_DOMAIN)
.removeRole(TEST_ROLE)
.build();
assertEquals(1, integrations.size());
Integration<?> integration = integrations.get(0);
assertTrue(integration instanceof RoleIntegration);
RoleIntegration roleIntegration = (RoleIntegration) integration;
assertEquals(Integration.Action.delete, roleIntegration.getAction());
assertEquals(TEST_DOMAIN, roleIntegration.getData().getDomain());
assertEquals(TEST_ROLE, roleIntegration.getData().getName());
assertEquals("role:" + TEST_DOMAIN + ":" + TEST_ROLE + ":delete", roleIntegration.getId());
}
@Test
@DisplayName("should build role permission relation integrations")
void shouldBuildRolePermissionRelationIntegrations() {
List<String> permissions = Arrays.asList(TEST_PERMISSION, TEST_PERMISSION + "-2");
List<Integration<?>> integrations = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.assignPermissionsToRole(TEST_ROLE, permissions)
.build();
assertEquals(2, integrations.size());
Integration<?> integration = integrations.get(1);
assertTrue(integration instanceof RolePermissionRelationIntegration);
RolePermissionRelationIntegration relationIntegration = (RolePermissionRelationIntegration) integration;
assertEquals(Integration.Action.create, relationIntegration.getAction());
assertEquals(TEST_DOMAIN, relationIntegration.getData().getDomain());
assertEquals(TEST_ROLE, relationIntegration.getData().getRole());
assertEquals(permissions, relationIntegration.getData().getPermissions());
assertEquals("role-permissions:" + TEST_DOMAIN + ":" + TEST_ROLE + ":" + String.join(",", permissions) + ":create", relationIntegration.getId());
}
@Test
@DisplayName("should build multiple integrations in sequence")
void shouldBuildMultipleIntegrationsInSequence() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.addRole(TEST_ROLE, TEST_DESCRIPTION)
.assignPermissionsToRole(TEST_ROLE, Arrays.asList(TEST_PERMISSION))
.build();
assertEquals(4, integrations.size());
assertTrue(integrations.get(0) instanceof DomainIntegration);
assertTrue(integrations.get(1) instanceof PermissionIntegration);
assertTrue(integrations.get(2) instanceof RoleIntegration);
assertTrue(integrations.get(3) instanceof RolePermissionRelationIntegration);
}
@Test
@DisplayName("should throw exception when no domain context is set")
void shouldThrowExceptionWhenNoDomainContextIsSet() {
assertThrows(IllegalStateException.class, () ->
IntegrationBuilder.create().addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
);
}
@Test
@DisplayName("should allow switching domain context")
void shouldAllowSwitchingDomainContext() {
String secondDomain = "second-domain";
List<Integration<?>> integrations = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.createDomain(secondDomain, TEST_DESCRIPTION)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.build();
assertEquals(4, integrations.size());
PermissionIntegration firstPermission = (PermissionIntegration) integrations.get(1);
PermissionIntegration secondPermission = (PermissionIntegration) integrations.get(3);
assertEquals(TEST_DOMAIN, firstPermission.getData().getDomain());
assertEquals(secondDomain, secondPermission.getData().getDomain());
}
@Test
@DisplayName("should allow selecting domain without creating it")
void shouldAllowSelectingDomainWithoutCreatingIt() {
List<Integration<?>> integrations = IntegrationBuilder.create()
.selectDomain(TEST_DOMAIN)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.build();
assertEquals(1, integrations.size());
PermissionIntegration permission = (PermissionIntegration) integrations.get(0);
assertEquals(TEST_DOMAIN, permission.getData().getDomain());
}
@Test
@DisplayName("should generate consistent IDs for same integrations")
void shouldGenerateConsistentIdsForSameIntegrations() {
List<Integration<?>> firstRun = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.build();
List<Integration<?>> secondRun = IntegrationBuilder.create()
.createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
.addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
.build();
assertEquals(firstRun.get(0).getId(), secondRun.get(0).getId());
assertEquals(firstRun.get(1).getId(), secondRun.get(1).getId());
}
}

View File

@ -0,0 +1,155 @@
package de.mummeit.pmg.service.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.mummeit.pmg.api.model.integration.DomainIntegration;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.service.PermissionManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class AbstractPermissionManagerConfigurationTest {
@Mock
private PermissionManager permissionManager;
@Mock
private ResourceLoader resourceLoader;
@Mock
private ObjectMapper objectMapper;
@Mock
private Resource resource;
@Mock
private ApplicationReadyEvent event;
private TestPermissionManagerConfiguration configuration;
@BeforeEach
void setUp() {
configuration = new TestPermissionManagerConfiguration(
permissionManager,
resourceLoader,
objectMapper
);
}
@Test
@DisplayName("should perform programmatic integrations on startup")
void shouldPerformProgrammaticIntegrationsOnStartup() {
// Given
List<Integration<?>> integrations = Collections.singletonList(
DomainIntegration.builder()
.id("1")
.action(Integration.Action.create)
.data(DomainIntegration.Data.builder()
.name("test")
.description("test")
.build())
.build()
);
configuration.setProgrammaticIntegrations(integrations);
// When
configuration.performIntegrationsOnStartup();
// Then
verify(permissionManager).performIntegration(integrations);
verify(resourceLoader, never()).getResource(any());
}
@Test
@DisplayName("should perform JSON integrations on startup")
void shouldPerformJsonIntegrationsOnStartup() throws IOException {
// Given
List<Integration<?>> integrations = Collections.singletonList(
DomainIntegration.builder()
.id("1")
.action(Integration.Action.create)
.data(DomainIntegration.Data.builder()
.name("test")
.description("test")
.build())
.build()
);
configuration.setProgrammaticIntegrations(null);
configuration.setJsonPath("classpath:integrations.json");
when(resourceLoader.getResource("classpath:integrations.json")).thenReturn(resource);
when(resource.getInputStream()).thenReturn(new ByteArrayInputStream("[]".getBytes()));
doReturn(integrations).when(objectMapper).readValue(any(InputStream.class), any(TypeReference.class));
// When
configuration.performIntegrationsOnStartup();
// Then
verify(permissionManager).performIntegration(integrations);
verify(resourceLoader).getResource("classpath:integrations.json");
}
@Test
@DisplayName("should handle no integrations gracefully")
void shouldHandleNoIntegrationsGracefully() {
// Given
configuration.setProgrammaticIntegrations(null);
configuration.setJsonPath(null);
// When
configuration.performIntegrationsOnStartup();
// Then
verify(permissionManager, never()).performIntegration(any());
verify(resourceLoader, never()).getResource(any());
}
private static class TestPermissionManagerConfiguration extends AbstractPermissionManagerConfiguration {
private List<Integration<?>> programmaticIntegrations;
private String jsonPath;
public TestPermissionManagerConfiguration(
PermissionManager permissionManager,
ResourceLoader resourceLoader,
ObjectMapper objectMapper
) {
super(permissionManager, resourceLoader, objectMapper);
}
public void setProgrammaticIntegrations(List<Integration<?>> integrations) {
this.programmaticIntegrations = integrations;
}
public void setJsonPath(String path) {
this.jsonPath = path;
}
@Override
protected List<Integration<?>> getIntegrations() {
return programmaticIntegrations;
}
@Override
protected String getIntegrationsJsonPath() {
return jsonPath;
}
}
}

View File

@ -0,0 +1,12 @@
package de.mummeit.springboot;
import de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.Test;
public class SpringBootContextTest extends BaseIntegrationTest {
@Test
public void contextLoads() {
// nothing to do
}
}

View File

@ -0,0 +1,8 @@
package de.mummeit.utility;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = SpringBootContextSpinner.class)
public class BaseIntegrationTest extends TestContainer {
}

View File

@ -0,0 +1,14 @@
package de.mummeit.utility;
import de.mummeit.common.annotations.EnablePermissionManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnablePermissionManager
public class SpringBootContextSpinner {
public static void main(String[] args) {
SpringApplication.run(SpringBootContextSpinner.class, args);
}
}

View File

@ -0,0 +1,41 @@
package de.mummeit.utility;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer;
public abstract class TestContainer {
private static final String DB_DATABASE = "test";
private static final String DB_USER = "postgres";
private static final String DB_PASSWORD = "super";
public static Network network = Network.newNetwork();
public static GenericContainer<?> postgresContainer = new PostgreSQLContainer("postgres:latest")
.withDatabaseName(DB_DATABASE)
.withUsername(DB_USER)
.withPassword(DB_PASSWORD)
.withNetwork(network)
.withNetworkAliases("testcontainer-db");
public static GenericContainer<?> permissionManagerContainer = new GenericContainer("mummeit/permission-manager:latest")
.withExposedPorts(6060)
.withEnv("DB_DATABASE", DB_DATABASE)
.withEnv("DB_USER", DB_USER)
.withEnv("DB_PASSWORD", DB_PASSWORD)
.withEnv("DB_HOST", "testcontainer-db")
.withEnv("DB_PORT", "5432")
.withNetwork(network)
.withNetworkAliases("permission-manager");
static {
postgresContainer.start();
permissionManagerContainer.start();
System.setProperty("permission-manager.url", "http://localhost:" + permissionManagerContainer.getFirstMappedPort());
}
}