diff --git a/README.md b/README.md index 7065ba0..84a404f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A Spring Boot SDK for integrating permission management into your Java applicati - Scope-based access control - Multi-domain permission management - Flexible user ID resolution +- API key authentication support ## Requirements @@ -54,13 +55,179 @@ Add the following properties to your Spring Boot application properties file (`a ```yaml permission-manager: - base-url: http://your-permission-manager-url # Required: URL of your Permission Manager instance + url: http://your-permission-manager-url # Required: URL of your Permission Manager instance + auth: + enabled: false # Optional: Enable/disable API key authentication (defaults to false) + api-key: your-api-key # Required when auth.enabled is true security: enabled: true # Optional: Enable/disable security checks (defaults to true) ``` These properties can be configured in any valid Spring Boot configuration source (application.yml, application.properties, environment variables, etc.) following Spring Boot's standard property resolution order. +### 3. API Key Authentication + +The SDK supports API key authentication for requests to the Permission Manager service. When enabled, the SDK will automatically add an `x-api-key` header to all requests. + +To enable API key authentication: + +1. Set `permission-manager.auth.enabled` to `true` +2. Provide your API key in `permission-manager.auth.api-key` + +Example configuration: +```yaml +permission-manager: + auth: + enabled: true + api-key: your-secret-api-key-here +``` + +Note: When authentication is enabled, the API key is required. The SDK will throw an `IllegalStateException` if authentication is enabled but no API key is provided. + +### 4. Integration Configuration + +The SDK provides a flexible way to configure and perform integrations on application startup through the `AbstractPermissionManagerConfiguration` class. You can use this to automatically set up your permission structure (domains, permissions, roles, and their relationships) when your application starts. + +There are two ways to provide integrations: + +#### Option 1: JSON Configuration + +Create a JSON file containing your integration configuration: + +```json +[ + { + "id": "domain-1", + "entity": "domain", + "action": "create", + "data": { + "name": "users", + "description": "User management domain" + } + }, + { + "id": "permission-1", + "entity": "permission", + "action": "create", + "data": { + "domain": "users", + "name": "read", + "description": "Permission to read user data" + } + }, + { + "id": "role-1", + "entity": "role", + "action": "create", + "data": { + "domain": "users", + "name": "user_viewer", + "description": "Role for viewing user data" + } + }, + { + "id": "role-permission-1", + "entity": "role_permission_relation", + "action": "create", + "data": { + "domain": "users", + "role": "user_viewer", + "permissions": ["read"] + } + } +] +``` + +Then create a configuration class that extends `AbstractPermissionManagerConfiguration`: + +```java +import de.mummeit.pmg.config.AbstractPermissionManagerConfiguration; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class PermissionManagerIntegrationConfig extends AbstractPermissionManagerConfiguration { + + @Override + protected String getIntegrationsJsonPath() { + return "classpath:permission-integrations.json"; + } +} +``` + +#### Option 2: Programmatic Configuration + +Alternatively, you can build your integrations programmatically using the fluent `IntegrationBuilder`: + +```java +import de.mummeit.pmg.builder.IntegrationBuilder; +import de.mummeit.pmg.config.AbstractPermissionManagerConfiguration; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class PermissionManagerIntegrationConfig extends AbstractPermissionManagerConfiguration { + + @Override + protected List> getIntegrations() { + return IntegrationBuilder.create() + // Create a domain and set it as the current context + .createDomain("users", "User management domain") + // Add permissions to the current domain + .addPermission("read", "Permission to read user data") + .addPermission("write", "Permission to write user data") + // Add a role to the current domain + .addRole("user_viewer", "Role for viewing user data") + // Assign permissions to the role + .assignPermissionsToRole("user_viewer", List.of("read")) + // Create another domain with its permissions and roles + .createDomain("orders", "Order management domain") + .addPermission("view", "Permission to view orders") + .addPermission("create", "Permission to create orders") + .addRole("order_manager", "Role for managing orders") + .assignPermissionsToRole("order_manager", List.of("view", "create")) + .build(); + } +} +``` + +The `IntegrationBuilder` provides a fluent API for creating integrations with features like: + +- Domain context management (automatically tracks the current domain) +- Method chaining for building complex permission structures +- Type-safe operations for all integration types +- Automatic generation of integration IDs + +Available builder methods: +- Domain operations: + - `createDomain(name, description)` + - `updateDomain(oldName, newName, description)` + - `deleteDomain(name)` + - `selectDomain(name)` - Switch context without creating a domain +- Permission operations: + - `addPermission(name, description)` + - `updatePermission(oldName, newName, description)` + - `removePermission(name)` +- Role operations: + - `addRole(name, description)` + - `updateRole(oldName, newName, description)` + - `removeRole(name)` +- Role-Permission relations: + - `assignPermissionsToRole(role, permissions)` + +The builder ensures that operations are performed in the correct context by requiring a domain to be selected or created before performing domain-specific operations. + +The integrations will be performed automatically when your application starts up. You can implement either `getIntegrations()` or `getIntegrationsJsonPath()` - if both are implemented, `getIntegrations()` takes precedence. + +Available integration types: +- `DomainIntegration`: Create/update domains +- `PermissionIntegration`: Create/update permissions within domains +- `RoleIntegration`: Create/update roles within domains +- `RolePermissionRelationIntegration`: Manage relationships between roles and permissions + +Each integration requires: +- `id`: A unique identifier for the integration +- `action`: The operation to perform (`create`, `update`, or `delete`) +- `data`: The entity-specific data for the integration + ## Usage ### 1. Annotation-Based Permission Checks diff --git a/pom.xml b/pom.xml index bb3207f..9337860 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ de.mumme-it permission-manager-sdk - 0.1.1 + 0.1.2 Mumme-IT https://mumme-it.de diff --git a/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java b/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java index df90027..f208dbd 100644 --- a/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java +++ b/src/main/java/de/mummeit/common/config/PermissionManagerSdkConfiguration.java @@ -1,8 +1,10 @@ package de.mummeit.common.config; +import de.mummeit.pmg.config.PermissionManagerAuthConfiguration; import feign.Client; import feign.httpclient.ApacheHttpClient; import org.apache.http.impl.client.HttpClients; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -13,6 +15,7 @@ import org.springframework.context.annotation.PropertySource; @ComponentScan(basePackages = "de.mummeit") @EnableFeignClients(basePackages = "de.mummeit") @PropertySource(value = "classpath:permission-manager-sdk-application.yaml") +@EnableConfigurationProperties(PermissionManagerAuthConfiguration.class) public class PermissionManagerSdkConfiguration { @Bean diff --git a/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java b/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java index 85285ea..a714508 100644 --- a/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java +++ b/src/main/java/de/mummeit/pmg/api/PermissionManagerClient.java @@ -5,7 +5,9 @@ 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.request.ListPermittedScopesRequest; import de.mummeit.pmg.api.model.access.response.PermittedResponse; +import de.mummeit.pmg.api.model.access.response.ListPermittedScopesResponse; import de.mummeit.pmg.api.model.integration.Integration; import de.mummeit.pmg.api.model.structure.Domain; import de.mummeit.pmg.api.model.structure.Permission; @@ -43,6 +45,9 @@ public interface PermissionManagerClient { @PatchMapping("/api/v1/access/revoke/user") void revokeUserAccess(@RequestBody RevokeUserAccessRequest request); + @PostMapping("/api/v1/access/scopes") + ListPermittedScopesResponse listPermittedScopes(@RequestBody ListPermittedScopesRequest request); + // Domain Management @PostMapping("/api/v1/domains") Domain createDomain(@RequestBody Domain domain); diff --git a/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java b/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java index a365d44..2739ff2 100644 --- a/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java +++ b/src/main/java/de/mummeit/pmg/api/annotation/RequiresPermission.java @@ -1,7 +1,5 @@ package de.mummeit.pmg.api.annotation; -import org.springframework.core.annotation.AliasFor; - import java.lang.annotation.*; /** diff --git a/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java b/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java index 9634ea3..5d4187f 100644 --- a/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java +++ b/src/main/java/de/mummeit/pmg/api/aspect/PermissionCheckAspect.java @@ -3,7 +3,7 @@ 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 de.mummeit.pmg.exception.AccessDeniedException; import lombok.RequiredArgsConstructor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -11,7 +11,6 @@ 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; @@ -22,9 +21,7 @@ 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 diff --git a/src/main/java/de/mummeit/pmg/api/model/access/request/ListPermittedScopesRequest.java b/src/main/java/de/mummeit/pmg/api/model/access/request/ListPermittedScopesRequest.java new file mode 100644 index 0000000..9b2c8c5 --- /dev/null +++ b/src/main/java/de/mummeit/pmg/api/model/access/request/ListPermittedScopesRequest.java @@ -0,0 +1,10 @@ +package de.mummeit.pmg.api.model.access.request; + +import lombok.Data; + +@Data +public class ListPermittedScopesRequest { + private String userId; + private String domain; + private String permission; +} \ No newline at end of file diff --git a/src/main/java/de/mummeit/pmg/api/model/access/response/ListPermittedScopesResponse.java b/src/main/java/de/mummeit/pmg/api/model/access/response/ListPermittedScopesResponse.java new file mode 100644 index 0000000..ee6e83e --- /dev/null +++ b/src/main/java/de/mummeit/pmg/api/model/access/response/ListPermittedScopesResponse.java @@ -0,0 +1,10 @@ +package de.mummeit.pmg.api.model.access.response; + +import lombok.Data; + +import java.util.List; + +@Data +public class ListPermittedScopesResponse { + private List scopes; +} \ 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 index 8d21fcd..1f5552d 100644 --- a/src/main/java/de/mummeit/pmg/builder/IntegrationBuilder.java +++ b/src/main/java/de/mummeit/pmg/builder/IntegrationBuilder.java @@ -1,4 +1,4 @@ -package de.mummeit.pmg.service.builder; +package de.mummeit.pmg.builder; import de.mummeit.pmg.api.model.integration.*; diff --git a/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java b/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java index 91d9a55..8688925 100644 --- a/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java +++ b/src/main/java/de/mummeit/pmg/config/AbstractPermissionManagerConfiguration.java @@ -1,4 +1,4 @@ -package de.mummeit.pmg.service.config; +package de.mummeit.pmg.config; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -6,6 +6,8 @@ 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.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.core.io.Resource; @@ -24,8 +26,13 @@ import java.util.List; @RequiredArgsConstructor public abstract class AbstractPermissionManagerConfiguration { + @Autowired private final PermissionManager permissionManager; + + @Autowired private final ResourceLoader resourceLoader; + + @Autowired private final ObjectMapper objectMapper; /** diff --git a/src/main/java/de/mummeit/pmg/config/PermissionManagerAuthConfiguration.java b/src/main/java/de/mummeit/pmg/config/PermissionManagerAuthConfiguration.java new file mode 100644 index 0000000..f8ea687 --- /dev/null +++ b/src/main/java/de/mummeit/pmg/config/PermissionManagerAuthConfiguration.java @@ -0,0 +1,34 @@ +package de.mummeit.pmg.config; + +import feign.RequestInterceptor; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +@Data +@Configuration +@ConfigurationProperties(prefix = "permission-manager") +public class PermissionManagerAuthConfiguration { + + private Auth auth = new Auth(); + + @Data + public static class Auth { + private boolean enabled = false; + private String apiKey; + } + + @Bean + public RequestInterceptor apiKeyInterceptor() { + return requestTemplate -> { + if (auth.isEnabled()) { + if (!StringUtils.hasText(auth.getApiKey())) { + throw new IllegalStateException("API key is required when authentication is enabled"); + } + requestTemplate.header("x-api-key", auth.getApiKey()); + } + }; + } +} \ 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 index 666ca71..0aa8f4e 100644 --- a/src/main/java/de/mummeit/pmg/exception/AccessDeniedException.java +++ b/src/main/java/de/mummeit/pmg/exception/AccessDeniedException.java @@ -1,4 +1,4 @@ -package de.mummeit.pmg.service.exception; +package de.mummeit.pmg.exception; /** * Exception thrown when access is denied for a user. diff --git a/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java b/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java index 378e86a..4dd1169 100644 --- a/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java +++ b/src/main/java/de/mummeit/pmg/exception/IntegrationFailedException.java @@ -1,4 +1,4 @@ -package de.mummeit.pmg.service.exception; +package de.mummeit.pmg.exception; import de.mummeit.pmg.api.model.integration.Integration; diff --git a/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java b/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java index cb686cf..db6d5e9 100644 --- a/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java +++ b/src/main/java/de/mummeit/pmg/exception/InvalidPermissionRequestException.java @@ -1,4 +1,4 @@ -package de.mummeit.pmg.service.exception; +package de.mummeit.pmg.exception; /** * Exception thrown when a permission request is invalid. diff --git a/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java b/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java index 3a54831..17c61ce 100644 --- a/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java +++ b/src/main/java/de/mummeit/pmg/exception/PermissionManagerException.java @@ -1,4 +1,4 @@ -package de.mummeit.pmg.service.exception; +package de.mummeit.pmg.exception; /** * Base exception class for all Permission Manager related exceptions. diff --git a/src/main/java/de/mummeit/pmg/service/PermissionManager.java b/src/main/java/de/mummeit/pmg/service/PermissionManager.java index 60014ec..b3c473e 100644 --- a/src/main/java/de/mummeit/pmg/service/PermissionManager.java +++ b/src/main/java/de/mummeit/pmg/service/PermissionManager.java @@ -3,11 +3,11 @@ 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.access.response.ListPermittedScopesResponse; 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 de.mummeit.pmg.exception.IntegrationFailedException; +import de.mummeit.pmg.exception.InvalidPermissionRequestException; import feign.FeignException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -31,7 +31,6 @@ public class PermissionManager { * @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); @@ -46,12 +45,9 @@ public class PermissionManager { PermittedResponse response = client.checkAccess(request); return response.isPermitted(); } catch (FeignException e) { - throw new AccessDeniedException( - "Failed to check access", - userId, - domain, - permission, - scope, + throw new InvalidPermissionRequestException( + "Technical error while checking access permissions", + "Failed to communicate with permission service: " + e.getMessage(), e ); } @@ -276,6 +272,35 @@ public class PermissionManager { } } + /** + * Lists all scopes where a user has access to a specific permission in a domain. + * + * @param userId The ID of the user + * @param domain The domain name + * @param permission The permission name + * @return List of scopes where the user has access + * @throws InvalidPermissionRequestException if any of the required parameters are invalid + */ + public List listPermittedScopes(String userId, String domain, String permission) { + validateParameters(userId, domain, permission); + + try { + ListPermittedScopesRequest request = new ListPermittedScopesRequest(); + request.setUserId(userId); + request.setDomain(domain); + request.setPermission(permission); + + ListPermittedScopesResponse response = client.listPermittedScopes(request); + return response.getScopes(); + } catch (FeignException e) { + throw new InvalidPermissionRequestException( + "Failed to list permitted scopes", + "Error occurred while listing permitted scopes", + e + ); + } + } + private void validateParameters(String userId, String domain, String permission, String scope) { validateUserId(userId); validateDomain(domain); diff --git a/src/main/resources/permission-manager-sdk-application.yaml b/src/main/resources/permission-manager-sdk-application.yaml index cf11edf..a79a4ab 100644 --- a/src/main/resources/permission-manager-sdk-application.yaml +++ b/src/main/resources/permission-manager-sdk-application.yaml @@ -10,5 +10,9 @@ spring: permission-manager: connect-timeout: 1000 read-timeout: 2000 + permission-manager: - url: http://localhost:6060 \ No newline at end of file + url: http://localhost:6060 + auth: + enabled: false + api-key: \ 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 index 84198d6..3b1047a 100644 --- a/src/test/java/de/mummeit/pmg/api/PermissionManagerClientIntegrationTest.java +++ b/src/test/java/de/mummeit/pmg/api/PermissionManagerClientIntegrationTest.java @@ -1,14 +1,10 @@ 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.request.*; import de.mummeit.pmg.api.model.access.response.PermittedResponse; -import de.mummeit.pmg.api.model.integration.DomainIntegration; +import de.mummeit.pmg.api.model.access.response.ListPermittedScopesResponse; import de.mummeit.pmg.api.model.integration.Integration; +import de.mummeit.pmg.api.model.integration.DomainIntegration; import de.mummeit.pmg.api.model.integration.PermissionIntegration; import de.mummeit.pmg.api.model.integration.RoleIntegration; import de.mummeit.pmg.api.model.integration.RolePermissionRelationIntegration; @@ -16,6 +12,7 @@ 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.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -35,6 +32,15 @@ class PermissionManagerClientIntegrationTest extends BaseIntegrationTest { private static final String TEST_USER = "test-user"; private static final String TEST_SCOPE = "test-scope"; + @AfterEach + void cleanup() { + try { + permissionManagerClient.deleteDomain(TEST_DOMAIN); + } catch (Exception e) { + // Ignore errors during cleanup + } + } + @Test @DisplayName("should return health status") void getHealthStatus() { @@ -251,4 +257,62 @@ class PermissionManagerClientIntegrationTest extends BaseIntegrationTest { // Clean up permissionManagerClient.deleteDomain(TEST_DOMAIN); } + + @Test + @DisplayName("should list permitted scopes successfully") + void listPermittedScopes() { + // 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); + + // Grant access in multiple scopes + PermitRequest permitRequest1 = new PermitRequest(); + Permit permit1 = new Permit(); + permit1.setDomain(TEST_DOMAIN); + permit1.setRoles(List.of(TEST_ROLE)); + permit1.setPermissions(List.of(TEST_PERMISSION)); + permitRequest1.setPermits(List.of(permit1)); + permitRequest1.setUserId(TEST_USER); + permitRequest1.setScope(TEST_SCOPE); + permissionManagerClient.permitAccess(permitRequest1); + + String secondScope = TEST_SCOPE + "-2"; + PermitRequest permitRequest2 = new PermitRequest(); + Permit permit2 = new Permit(); + permit2.setDomain(TEST_DOMAIN); + permit2.setRoles(List.of(TEST_ROLE)); + permit2.setPermissions(List.of(TEST_PERMISSION)); + permitRequest2.setPermits(List.of(permit2)); + permitRequest2.setUserId(TEST_USER); + permitRequest2.setScope(secondScope); + permissionManagerClient.permitAccess(permitRequest2); + + // Test listing permitted scopes + ListPermittedScopesRequest request = new ListPermittedScopesRequest(); + request.setUserId(TEST_USER); + request.setDomain(TEST_DOMAIN); + request.setPermission(TEST_PERMISSION); + + ListPermittedScopesResponse response = permissionManagerClient.listPermittedScopes(request); + assertNotNull(response); + assertNotNull(response.getScopes()); + assertEquals(2, response.getScopes().size()); + assertTrue(response.getScopes().contains(TEST_SCOPE)); + assertTrue(response.getScopes().contains(secondScope)); + + // Clean up + permissionManagerClient.deleteRole(TEST_DOMAIN, TEST_ROLE); + permissionManagerClient.deletePermission(TEST_DOMAIN, TEST_PERMISSION); + permissionManagerClient.deleteDomain(TEST_DOMAIN); + } } \ 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 index 30a7e5a..e815c52 100644 --- a/src/test/java/de/mummeit/pmg/api/controller/TestControllerIntegrationTest.java +++ b/src/test/java/de/mummeit/pmg/api/controller/TestControllerIntegrationTest.java @@ -6,7 +6,7 @@ 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.pmg.exception.AccessDeniedException; import de.mummeit.utility.BaseIntegrationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java b/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java index 4ce192d..868ea29 100644 --- a/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java +++ b/src/test/java/de/mummeit/pmg/api/controller/TestExceptionHandler.java @@ -1,6 +1,6 @@ package de.mummeit.pmg.api.controller; -import de.mummeit.pmg.service.exception.AccessDeniedException; +import de.mummeit.pmg.exception.AccessDeniedException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; diff --git a/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java b/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java index fac14fc..4f28be7 100644 --- a/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java +++ b/src/test/java/de/mummeit/pmg/service/PermissionManagerTest.java @@ -3,11 +3,12 @@ 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.access.response.ListPermittedScopesResponse; 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 de.mummeit.pmg.exception.AccessDeniedException; +import de.mummeit.pmg.exception.IntegrationFailedException; +import de.mummeit.pmg.exception.InvalidPermissionRequestException; import feign.FeignException; import feign.Request; import feign.RequestTemplate; @@ -25,6 +26,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -60,12 +62,25 @@ class PermissionManagerTest { } @Test - @DisplayName("hasAccess should throw AccessDeniedException when client throws FeignException") - void hasAccessShouldThrowAccessDeniedExceptionWhenClientThrowsFeignException() { + @DisplayName("hasAccess should return false when access is denied") + void hasAccessShouldReturnFalseWhenAccessIsDenied() { + PermittedResponse response = new PermittedResponse(); + response.setPermitted(false); + when(client.checkAccess(any(CheckAccessRequest.class))).thenReturn(response); + + boolean hasAccess = permissionManager.hasAccess(TEST_USER, TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE); + assertFalse(hasAccess); + + verify(client).checkAccess(any(CheckAccessRequest.class)); + } + + @Test + @DisplayName("hasAccess should throw InvalidPermissionRequestException when client throws FeignException") + void hasAccessShouldThrowInvalidPermissionRequestExceptionWhenClientThrowsFeignException() { FeignException feignException = new FeignException.NotFound("Not Found", createRequest(), null, null); when(client.checkAccess(any(CheckAccessRequest.class))).thenThrow(feignException); - assertThrows(AccessDeniedException.class, () -> + assertThrows(InvalidPermissionRequestException.class, () -> permissionManager.hasAccess(TEST_USER, TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE) ); } @@ -254,6 +269,62 @@ class PermissionManagerTest { )); } + @Test + @DisplayName("listPermittedScopes should return scopes from client") + void listPermittedScopesShouldReturnScopesFromClient() { + ListPermittedScopesResponse response = new ListPermittedScopesResponse(); + response.setScopes(Arrays.asList("scope1", "scope2")); + when(client.listPermittedScopes(any(ListPermittedScopesRequest.class))).thenReturn(response); + + List scopes = permissionManager.listPermittedScopes(TEST_USER, TEST_DOMAIN, TEST_PERMISSION); + + assertEquals(Arrays.asList("scope1", "scope2"), scopes); + verify(client).listPermittedScopes(argThat(request -> + TEST_USER.equals(request.getUserId()) && + TEST_DOMAIN.equals(request.getDomain()) && + TEST_PERMISSION.equals(request.getPermission()) + )); + } + + @Test + @DisplayName("listPermittedScopes should throw InvalidPermissionRequestException when client throws FeignException") + void listPermittedScopesShouldThrowInvalidPermissionRequestExceptionWhenClientThrowsFeignException() { + FeignException feignException = new FeignException.InternalServerError("Internal Server Error", createRequest(), null, null); + when(client.listPermittedScopes(any(ListPermittedScopesRequest.class))).thenThrow(feignException); + + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes(TEST_USER, TEST_DOMAIN, TEST_PERMISSION) + ); + } + + @Test + @DisplayName("listPermittedScopes should throw InvalidPermissionRequestException when parameters are invalid") + void listPermittedScopesShouldThrowInvalidPermissionRequestExceptionWhenParametersAreInvalid() { + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes(null, TEST_DOMAIN, TEST_PERMISSION) + ); + + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes("", TEST_DOMAIN, TEST_PERMISSION) + ); + + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes(TEST_USER, null, TEST_PERMISSION) + ); + + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes(TEST_USER, "", TEST_PERMISSION) + ); + + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes(TEST_USER, TEST_DOMAIN, null) + ); + + assertThrows(InvalidPermissionRequestException.class, () -> + permissionManager.listPermittedScopes(TEST_USER, TEST_DOMAIN, "") + ); + } + private Request createRequest() { return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate()); } diff --git a/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java b/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java index 40eefed..5ac1eae 100644 --- a/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java +++ b/src/test/java/de/mummeit/pmg/service/builder/IntegrationBuilderTest.java @@ -1,6 +1,8 @@ package de.mummeit.pmg.service.builder; import de.mummeit.pmg.api.model.integration.*; +import de.mummeit.pmg.builder.IntegrationBuilder; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java b/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java index aee2a22..7882a83 100644 --- a/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java +++ b/src/test/java/de/mummeit/pmg/service/config/AbstractPermissionManagerConfigurationTest.java @@ -4,6 +4,7 @@ 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.config.AbstractPermissionManagerConfiguration; import de.mummeit.pmg.service.PermissionManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/de/mummeit/utility/TestContainer.java b/src/test/java/de/mummeit/utility/TestContainer.java index c7f2bd3..3b26153 100644 --- a/src/test/java/de/mummeit/utility/TestContainer.java +++ b/src/test/java/de/mummeit/utility/TestContainer.java @@ -1,18 +1,19 @@ package de.mummeit.utility; -import org.testcontainers.containers.GenericContainer; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.Network; import org.testcontainers.containers.PostgreSQLContainer; - +import org.testcontainers.containers.GenericContainer; 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"; + private static final String TEST_API_KEY = "test-api-key-123"; public static Network network = Network.newNetwork(); - public static GenericContainer postgresContainer = new PostgreSQLContainer("postgres:latest") .withDatabaseName(DB_DATABASE) .withUsername(DB_USER) @@ -27,15 +28,21 @@ public abstract class TestContainer { .withEnv("DB_PASSWORD", DB_PASSWORD) .withEnv("DB_HOST", "testcontainer-db") .withEnv("DB_PORT", "5432") + .withEnv("AUTH_ENABLED", "true") + .withEnv("AUTH_APIKEY", TEST_API_KEY) .withNetwork(network) .withNetworkAliases("permission-manager"); - static { - postgresContainer.start(); - permissionManagerContainer.start(); - System.setProperty("permission-manager.url", "http://localhost:" + permissionManagerContainer.getFirstMappedPort()); + } + + @DynamicPropertySource + static void registerProperties(DynamicPropertyRegistry registry) { + registry.add("permission-manager.url", + () -> String.format("http://localhost:%d", permissionManagerContainer.getFirstMappedPort())); + registry.add("permission-manager.auth.enabled", () -> "true"); + registry.add("permission-manager.auth.api-key", () -> TEST_API_KEY); } }