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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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