diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 2c778f9..8ae318a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -10,4 +10,9 @@
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..d53ecaf
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "java.compile.nullAnalysis.mode": "automatic",
+ "java.configuration.updateBuildConfiguration": "automatic"
+}
\ No newline at end of file
diff --git a/do b/do
index f219a8d..8368c50 100755
--- a/do
+++ b/do
@@ -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"
diff --git a/pom.xml b/pom.xml
index 881a525..5e7d6e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,8 @@
21
21
21
+ 3.3.0
+ 2023.0.2
@@ -31,32 +33,108 @@
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+
- junit
- junit
- 4.13.1
- test
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+ true
-
+
org.springframework.cloud
spring-cloud-starter-openfeign
- 4.1.2
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
io.github.openfeign.form
feign-form
3.8.0
-
+
+ io.github.openfeign
+ feign-httpclient
+
+
+
org.projectlombok
lombok
- 1.18.32
provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.testcontainers
+ postgresql
+ 1.20.0
+ test
+
+
+ org.testcontainers
+ testcontainers
+ 1.20.0
+ test
+
@@ -68,13 +146,11 @@
scm:git:https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git
- scm:git:https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git
-
+ scm:git:https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git
https://git.mumme-it.de/Mumme-IT/permission-manager-sdk-java.git
HEAD
-
@@ -87,6 +163,11 @@
org.apache.maven.plugins
maven-compiler-plugin
3.13.0
+
+
+ -parameters
+
+
org.apache.maven.plugins
@@ -109,13 +190,12 @@
org.jreleaser
jreleaser-maven-plugin
- 1.15.0
ALWAYS
COMMAND
- true#
+ true
diff --git a/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java b/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java
index 73057c0..df90027 100644
--- a/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java
+++ b/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java
@@ -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());
+ }
}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java b/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java
new file mode 100644
index 0000000..85285ea
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java
@@ -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 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> integrations);
+}
diff --git a/src/main/java/de/mummeit/pmg/api/PmgClient.java b/src/main/java/de/mummeit/pmg/api/PmgClient.java
deleted file mode 100644
index d15a143..0000000
--- a/src/main/java/de/mummeit/pmg/api/PmgClient.java
+++ /dev/null
@@ -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();
-}
diff --git a/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java b/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java
new file mode 100644
index 0000000..a365d44
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java
@@ -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()";
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermissions.java b/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermissions.java
new file mode 100644
index 0000000..a1f67e1
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermissions.java
@@ -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();
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java b/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java
new file mode 100644
index 0000000..9634ea3
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java
@@ -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"));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/CheckAccessRequest.java b/src/main/java/de/mummeit/pmg/api/model/access/request/CheckAccessRequest.java
new file mode 100644
index 0000000..7f2c80c
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/request/CheckAccessRequest.java
@@ -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;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/Permit.java b/src/main/java/de/mummeit/pmg/api/model/access/request/Permit.java
new file mode 100644
index 0000000..ead8cfd
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/request/Permit.java
@@ -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 roles;
+ private List permissions;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/PermitRequest.java b/src/main/java/de/mummeit/pmg/api/model/access/request/PermitRequest.java
new file mode 100644
index 0000000..cd3b536
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/request/PermitRequest.java
@@ -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 permits;
+ private String scope;
+ private String userId;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/RevokeScopeAccessRequest.java b/src/main/java/de/mummeit/pmg/api/model/access/request/RevokeScopeAccessRequest.java
new file mode 100644
index 0000000..a0d717d
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/request/RevokeScopeAccessRequest.java
@@ -0,0 +1,8 @@
+package de.mummeit.pmg.api.model.access.request;
+
+import lombok.Data;
+
+@Data
+public class RevokeScopeAccessRequest {
+ private String scope;
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/RevokeUserAccessRequest.java b/src/main/java/de/mummeit/pmg/api/model/access/request/RevokeUserAccessRequest.java
new file mode 100644
index 0000000..1e1a2e0
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/request/RevokeUserAccessRequest.java
@@ -0,0 +1,8 @@
+package de.mummeit.pmg.api.model.access.request;
+
+import lombok.Data;
+
+@Data
+public class RevokeUserAccessRequest {
+ private String userId;
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/SearchPermitRequest.java b/src/main/java/de/mummeit/pmg/api/model/access/request/SearchPermitRequest.java
new file mode 100644
index 0000000..174414e
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/request/SearchPermitRequest.java
@@ -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;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/access/response/PermittedResponse.java b/src/main/java/de/mummeit/pmg/api/model/access/response/PermittedResponse.java
new file mode 100644
index 0000000..1726d37
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/access/response/PermittedResponse.java
@@ -0,0 +1,8 @@
+package de.mummeit.pmg.api.model.access.response;
+
+import lombok.Data;
+
+@Data
+public class PermittedResponse {
+ private boolean permitted;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/integration/DomainIntegration.java b/src/main/java/de/mummeit/pmg/api/model/integration/DomainIntegration.java
new file mode 100644
index 0000000..c631985
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/integration/DomainIntegration.java
@@ -0,0 +1,17 @@
+package de.mummeit.pmg.api.model.integration;
+
+import lombok.Builder;
+
+public class DomainIntegration extends Integration {
+
+ @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;
+ }
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/integration/Integration.java b/src/main/java/de/mummeit/pmg/api/model/integration/Integration.java
new file mode 100644
index 0000000..26d241a
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/integration/Integration.java
@@ -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 {
+ 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;
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/api/model/integration/PermissionIntegration.java b/src/main/java/de/mummeit/pmg/api/model/integration/PermissionIntegration.java
new file mode 100644
index 0000000..72a1d2c
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/integration/PermissionIntegration.java
@@ -0,0 +1,17 @@
+package de.mummeit.pmg.api.model.integration;
+
+import lombok.Builder;
+
+public class PermissionIntegration extends Integration {
+
+ @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;
+ }
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/integration/RoleIntegration.java b/src/main/java/de/mummeit/pmg/api/model/integration/RoleIntegration.java
new file mode 100644
index 0000000..cbc0811
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/integration/RoleIntegration.java
@@ -0,0 +1,17 @@
+package de.mummeit.pmg.api.model.integration;
+
+import lombok.Builder;
+
+public class RoleIntegration extends Integration {
+
+ @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;
+ }
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/integration/RolePermissionRelationIntegration.java b/src/main/java/de/mummeit/pmg/api/model/integration/RolePermissionRelationIntegration.java
new file mode 100644
index 0000000..368853a
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/integration/RolePermissionRelationIntegration.java
@@ -0,0 +1,20 @@
+package de.mummeit.pmg.api.model.integration;
+
+import lombok.Builder;
+
+import java.util.List;
+
+public class RolePermissionRelationIntegration extends Integration {
+
+ @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 permissions;
+ }
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/structure/Domain.java b/src/main/java/de/mummeit/pmg/api/model/structure/Domain.java
new file mode 100644
index 0000000..751f41d
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/structure/Domain.java
@@ -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 permissions;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/structure/Permission.java b/src/main/java/de/mummeit/pmg/api/model/structure/Permission.java
new file mode 100644
index 0000000..4ce0814
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/structure/Permission.java
@@ -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;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/model/structure/Role.java b/src/main/java/de/mummeit/pmg/api/model/structure/Role.java
new file mode 100644
index 0000000..bfd600e
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/model/structure/Role.java
@@ -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 permissions;
+}
diff --git a/src/main/java/de/mummeit/pmg/api/service/SecurityService.java b/src/main/java/de/mummeit/pmg/api/service/SecurityService.java
new file mode 100644
index 0000000..3eee8ca
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/api/service/SecurityService.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/builder/IntegrationBuilder.java b/src/main/java/de/mummeit/pmg/builder/IntegrationBuilder.java
new file mode 100644
index 0000000..8d21fcd
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/builder/IntegrationBuilder.java
@@ -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> 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> 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 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> 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());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java b/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java
new file mode 100644
index 0000000..91d9a55
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java
@@ -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> 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> loadIntegrationsFromJson(String path) throws IOException {
+ Resource resource = resourceLoader.getResource(path);
+ try (InputStream inputStream = resource.getInputStream()) {
+ return objectMapper.readValue(inputStream, new TypeReference>>() {});
+ }
+ }
+
+ /**
+ * 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> 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/exception/AccessDeniedException.java b/src/main/java/de/mummeit/pmg/exception/AccessDeniedException.java
new file mode 100644
index 0000000..666ca71
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/exception/AccessDeniedException.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java b/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java
new file mode 100644
index 0000000..378e86a
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java
@@ -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> failedIntegrations;
+
+ public IntegrationFailedException(String message, List> failedIntegrations) {
+ super(message);
+ this.failedIntegrations = failedIntegrations;
+ }
+
+ public IntegrationFailedException(String message, List> failedIntegrations, Throwable cause) {
+ super(message, cause);
+ this.failedIntegrations = failedIntegrations;
+ }
+
+ public List> getFailedIntegrations() {
+ return failedIntegrations;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java b/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java
new file mode 100644
index 0000000..cb686cf
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java b/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java
new file mode 100644
index 0000000..3a54831
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/mummeit/pmg/service/PermissionManager.java b/src/main/java/de/mummeit/pmg/service/PermissionManager.java
new file mode 100644
index 0000000..60014ec
--- /dev/null
+++ b/src/main/java/de/mummeit/pmg/service/PermissionManager.java
@@ -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 permissions, List 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 permissions, List 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 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> 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 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 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 permissions, List 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 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());
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/permission-manager-sdk-application.yaml b/src/main/resources/permission-manager-sdk-application.yaml
index f0d9216..cf11edf 100644
--- a/src/main/resources/permission-manager-sdk-application.yaml
+++ b/src/main/resources/permission-manager-sdk-application.yaml
@@ -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
\ No newline at end of file
+ main:
+ allow-bean-definition-overriding: true
+ cloud:
+ openfeign:
+ client:
+ config:
+ permission-manager:
+ connect-timeout: 1000
+ read-timeout: 2000
+permission-manager:
+ url: http://localhost:6060
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/PermissionManagerClientIntegrationTest.java b/src/test/java/de/mummeit/pmg/api/PermissionManagerClientIntegrationTest.java
new file mode 100644
index 0000000..84198d6
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/PermissionManagerClientIntegrationTest.java
@@ -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> 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 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);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/config/TestSecurityConfig.java b/src/test/java/de/mummeit/pmg/api/config/TestSecurityConfig.java
new file mode 100644
index 0000000..442a382
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/config/TestSecurityConfig.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/controller/TestConfig.java b/src/test/java/de/mummeit/pmg/api/controller/TestConfig.java
new file mode 100644
index 0000000..989e048
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/controller/TestConfig.java
@@ -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 {
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/controller/TestController.java b/src/test/java/de/mummeit/pmg/api/controller/TestController.java
new file mode 100644
index 0000000..ab04701
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/controller/TestController.java
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/controller/TestControllerIntegrationTest.java b/src/test/java/de/mummeit/pmg/api/controller/TestControllerIntegrationTest.java
new file mode 100644
index 0000000..30a7e5a
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/controller/TestControllerIntegrationTest.java
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java b/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java
new file mode 100644
index 0000000..4ce192d
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/api/service/SecurityService.java b/src/test/java/de/mummeit/pmg/api/service/SecurityService.java
new file mode 100644
index 0000000..80e79ef
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/api/service/SecurityService.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java b/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java
new file mode 100644
index 0000000..fac14fc
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java
@@ -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> 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 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 permissions = Collections.singletonList(TEST_PERMISSION);
+ List 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 permissions = Collections.singletonList(TEST_PERMISSION);
+ List 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 expectedPermissions = Collections.singletonList(permission);
+
+ when(client.searchPermits(any(SearchPermitRequest.class))).thenReturn(expectedPermissions);
+
+ List 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 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 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());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java b/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java
new file mode 100644
index 0000000..40eefed
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java
@@ -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> 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> 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> 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> 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> 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> 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> 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> 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> 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 permissions = Arrays.asList(TEST_PERMISSION, TEST_PERMISSION + "-2");
+ List> 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> 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> 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> 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> firstRun = IntegrationBuilder.create()
+ .createDomain(TEST_DOMAIN, TEST_DESCRIPTION)
+ .addPermission(TEST_PERMISSION, TEST_DESCRIPTION)
+ .build();
+
+ List> 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());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java b/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java
new file mode 100644
index 0000000..aee2a22
--- /dev/null
+++ b/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java
@@ -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> 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> 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> programmaticIntegrations;
+ private String jsonPath;
+
+ public TestPermissionManagerConfiguration(
+ PermissionManager permissionManager,
+ ResourceLoader resourceLoader,
+ ObjectMapper objectMapper
+ ) {
+ super(permissionManager, resourceLoader, objectMapper);
+ }
+
+ public void setProgrammaticIntegrations(List> integrations) {
+ this.programmaticIntegrations = integrations;
+ }
+
+ public void setJsonPath(String path) {
+ this.jsonPath = path;
+ }
+
+ @Override
+ protected List> getIntegrations() {
+ return programmaticIntegrations;
+ }
+
+ @Override
+ protected String getIntegrationsJsonPath() {
+ return jsonPath;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/mummeit/springboot/SpringBootContextTest.java b/src/test/java/de/mummeit/springboot/SpringBootContextTest.java
new file mode 100644
index 0000000..0318725
--- /dev/null
+++ b/src/test/java/de/mummeit/springboot/SpringBootContextTest.java
@@ -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
+ }
+}
diff --git a/src/test/java/de/mummeit/utility/BaseIntegrationTest.java b/src/test/java/de/mummeit/utility/BaseIntegrationTest.java
new file mode 100644
index 0000000..b0ea8c1
--- /dev/null
+++ b/src/test/java/de/mummeit/utility/BaseIntegrationTest.java
@@ -0,0 +1,8 @@
+package de.mummeit.utility;
+
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(classes = SpringBootContextSpinner.class)
+public class BaseIntegrationTest extends TestContainer {
+
+}
diff --git a/src/test/java/de/mummeit/utility/SpringBootContextSpinner.java b/src/test/java/de/mummeit/utility/SpringBootContextSpinner.java
new file mode 100644
index 0000000..80f2e3b
--- /dev/null
+++ b/src/test/java/de/mummeit/utility/SpringBootContextSpinner.java
@@ -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);
+ }
+}
diff --git a/src/test/java/de/mummeit/utility/TestContainer.java b/src/test/java/de/mummeit/utility/TestContainer.java
new file mode 100644
index 0000000..c7f2bd3
--- /dev/null
+++ b/src/test/java/de/mummeit/utility/TestContainer.java
@@ -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());
+ }
+}