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()); + } +}