Compare commits

4 Commits
v0.1.1 ... main

Author SHA1 Message Date
91bc0b3b6f Add functionality for all pmg features 2025-03-25 01:37:58 +01:00
aa4de7cbe2 update readme 2025-01-09 15:34:28 +01:00
a1e95dc038 raise version 2025-01-09 15:30:58 +01:00
1a6f101be9 add readme 2025-01-08 02:48:54 +01:00
24 changed files with 697 additions and 46 deletions

413
README.md Normal file
View File

@ -0,0 +1,413 @@
# Permission Manager SDK for Java
A Spring Boot SDK for integrating permission management into your Java applications. This library provides a robust and flexible way to manage user permissions across different domains and scopes.
## Features
- Annotation-based permission checks
- Programmatic permission management
- Integration with Spring Security
- Support for domain-based permissions
- Scope-based access control
- Multi-domain permission management
- Flexible user ID resolution
- API key authentication support
## Requirements
- Java 21 or higher
- Spring Boot 3.3.0 or higher
- Spring Cloud 2023.0.2 or higher
## Installation
Add the following dependency to your `pom.xml`:
```xml
<dependency>
<groupId>de.mumme-it</groupId>
<artifactId>permission-manager-sdk</artifactId>
<version>0.1.1</version>
</dependency>
```
## Configuration
### 1. Enable the SDK
Add the `@EnablePermissionManager` annotation to your Spring Boot application class:
```java
import de.mummeit.common.annotations.EnablePermissionManager;
@SpringBootApplication
@EnablePermissionManager
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
```
### 2. Configuration Properties
Add the following properties to your Spring Boot application properties file (`application.yml` or `application.properties`):
```yaml
permission-manager:
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<Integration<?>> 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
Use the `@RequiresPermission` annotation to protect your methods:
```java
import de.mummeit.pmg.api.annotation.RequiresPermission;
@RestController
@RequestMapping("/api")
public class YourController {
@RequiresPermission(domain = "users", permission = "read", scope = "*")
@GetMapping("/users")
public List<User> getUsers() {
// This method will only execute if the user has the "read" permission in the "users" domain
// The "*" scope means this permission applies to all scopes
return userService.findAll();
}
@RequiresPermission(
domain = "orders",
permission = "update",
scope = "region-#orderId", // Example of prefix-based scope
userIdExpression = "#request.getHeader('X-User-Id')"
)
@PutMapping("/orders/{orderId}")
public Order updateOrder(@PathVariable String orderId, @RequestBody Order order) {
// This method checks permissions with a specific scope and custom user ID resolution
return orderService.update(orderId, order);
}
@RequiresPermission(
domain = "reports",
permission = "view",
scope = "region-*" // Example of wildcard scope matching all regions
)
@GetMapping("/reports")
public List<Report> getReports() {
// This method allows access to users with permission for any region
return reportService.findAll();
}
}
```
### 2. Multiple Permission Requirements
You can require multiple permissions using repeated annotations:
```java
@RequiresPermission(domain = "users", permission = "read")
@RequiresPermission(domain = "orders", permission = "write")
public void methodRequiringMultiplePermissions() {
// This method requires both permissions
}
```
### 3. Programmatic Permission Management
Use the `PermissionManager` class to manage permissions programmatically:
```java
@Service
@RequiredArgsConstructor
public class YourService {
private final PermissionManager permissionManager;
public void grantUserAccess(String userId) {
// Grant access to all user-related operations
permissionManager.grantAccess(
userId,
"users",
List.of("read", "write"),
List.of("user_role"),
"*" // Wildcard scope - applies to all scopes
);
// Grant access to specific region
permissionManager.grantAccess(
userId,
"reports",
List.of("view"),
List.of("reporter"),
"region-europe" // Specific region scope
);
// Grant access to all regions
permissionManager.grantAccess(
userId,
"reports",
List.of("view"),
List.of("global_reporter"),
"region-*" // Wildcard scope - applies to all regions
);
}
public boolean checkAccess(String userId) {
// Check access for all scopes
boolean hasGlobalAccess = permissionManager.hasAccess(
userId,
"users",
"read",
"*"
);
// Check access for specific region
boolean hasRegionAccess = permissionManager.hasAccess(
userId,
"reports",
"view",
"region-europe"
);
return hasGlobalAccess && hasRegionAccess;
}
public void revokeAccess(String userId) {
permissionManager.revokeAccess(
userId,
"users",
List.of("read", "write"),
List.of("user_role"),
"global"
);
}
}
```
### 4. Multi-Domain Permission Management
```java
List<Permit> permits = List.of(
new Permit("users", List.of("read"), List.of("user_role")),
new Permit("orders", List.of("write"), List.of("order_manager"))
);
permissionManager.grantMultiDomainAccess(userId, permits, "global");
```
### 5. User Permission Queries
```java
List<Permission> userPermissions = permissionManager.findUserPermissions(userId, "global");
```
## Exception Handling
The SDK throws the following exceptions:
- `AccessDeniedException`: When a permission check fails
- `InvalidPermissionRequestException`: When invalid parameters are provided
- `IntegrationFailedException`: When integration operations fail
Example exception handler:
```java
@ControllerAdvice
public class PermissionExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<String> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Access denied: " + e.getMessage());
}
}
```
## Best Practices
1. **Scope Usage**:
- Use scopes to implement fine-grained access control at the resource level
- Leverage wildcards (`*`) for global access
- Use prefix-based wildcards (e.g., `region-*`) for category-wide access
- Be consistent with scope naming conventions (e.g., `region-europe`, `region-asia`)
2. **User ID Resolution**: Customize user ID resolution using SpEL expressions when needed.
3. **Error Handling**: Always handle permission-related exceptions appropriately.
4. **Permission Granularity**: Design permissions with appropriate granularity for your use case.
5. **Security Context**: Ensure proper security context is available when using default user ID resolution.
## License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

View File

@ -8,7 +8,7 @@
<groupId>de.mumme-it</groupId> <groupId>de.mumme-it</groupId>
<artifactId>permission-manager-sdk</artifactId> <artifactId>permission-manager-sdk</artifactId>
<version>0.1.0</version> <version>0.1.2</version>
<organization> <organization>
<name>Mumme-IT</name> <name>Mumme-IT</name>
<url>https://mumme-it.de</url> <url>https://mumme-it.de</url>

View File

@ -1,8 +1,10 @@
package de.mummeit.common.config; package de.mummeit.common.config;
import de.mummeit.pmg.config.PermissionManagerAuthConfiguration;
import feign.Client; import feign.Client;
import feign.httpclient.ApacheHttpClient; import feign.httpclient.ApacheHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
@ -13,6 +15,7 @@ import org.springframework.context.annotation.PropertySource;
@ComponentScan(basePackages = "de.mummeit") @ComponentScan(basePackages = "de.mummeit")
@EnableFeignClients(basePackages = "de.mummeit") @EnableFeignClients(basePackages = "de.mummeit")
@PropertySource(value = "classpath:permission-manager-sdk-application.yaml") @PropertySource(value = "classpath:permission-manager-sdk-application.yaml")
@EnableConfigurationProperties(PermissionManagerAuthConfiguration.class)
public class PermissionManagerSdkConfiguration { public class PermissionManagerSdkConfiguration {
@Bean @Bean

View File

@ -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.RevokeScopeAccessRequest;
import de.mummeit.pmg.api.model.access.request.RevokeUserAccessRequest; 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.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.PermittedResponse;
import de.mummeit.pmg.api.model.access.response.ListPermittedScopesResponse;
import de.mummeit.pmg.api.model.integration.Integration; import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.structure.Domain; import de.mummeit.pmg.api.model.structure.Domain;
import de.mummeit.pmg.api.model.structure.Permission; import de.mummeit.pmg.api.model.structure.Permission;
@ -43,6 +45,9 @@ public interface PermissionManagerClient {
@PatchMapping("/api/v1/access/revoke/user") @PatchMapping("/api/v1/access/revoke/user")
void revokeUserAccess(@RequestBody RevokeUserAccessRequest request); void revokeUserAccess(@RequestBody RevokeUserAccessRequest request);
@PostMapping("/api/v1/access/scopes")
ListPermittedScopesResponse listPermittedScopes(@RequestBody ListPermittedScopesRequest request);
// Domain Management // Domain Management
@PostMapping("/api/v1/domains") @PostMapping("/api/v1/domains")
Domain createDomain(@RequestBody Domain domain); Domain createDomain(@RequestBody Domain domain);

View File

@ -1,7 +1,5 @@
package de.mummeit.pmg.api.annotation; package de.mummeit.pmg.api.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*; import java.lang.annotation.*;
/** /**

View File

@ -3,7 +3,7 @@ package de.mummeit.pmg.api.aspect;
import de.mummeit.pmg.api.PermissionManagerClient; import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.annotation.RequiresPermission; import de.mummeit.pmg.api.annotation.RequiresPermission;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest; 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 lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
@ -11,7 +11,6 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext; 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.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
@Aspect @Aspect

View File

@ -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;
}

View File

@ -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<String> scopes;
}

View File

@ -1,4 +1,4 @@
package de.mummeit.pmg.service.builder; package de.mummeit.pmg.builder;
import de.mummeit.pmg.api.model.integration.*; import de.mummeit.pmg.api.model.integration.*;

View File

@ -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.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; 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 de.mummeit.pmg.service.PermissionManager;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@ -24,8 +26,13 @@ import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class AbstractPermissionManagerConfiguration { public abstract class AbstractPermissionManagerConfiguration {
@Autowired
private final PermissionManager permissionManager; private final PermissionManager permissionManager;
@Autowired
private final ResourceLoader resourceLoader; private final ResourceLoader resourceLoader;
@Autowired
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
/** /**

View File

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

View File

@ -1,4 +1,4 @@
package de.mummeit.pmg.service.exception; package de.mummeit.pmg.exception;
/** /**
* Exception thrown when access is denied for a user. * Exception thrown when access is denied for a user.

View File

@ -1,4 +1,4 @@
package de.mummeit.pmg.service.exception; package de.mummeit.pmg.exception;
import de.mummeit.pmg.api.model.integration.Integration; import de.mummeit.pmg.api.model.integration.Integration;

View File

@ -1,4 +1,4 @@
package de.mummeit.pmg.service.exception; package de.mummeit.pmg.exception;
/** /**
* Exception thrown when a permission request is invalid. * Exception thrown when a permission request is invalid.

View File

@ -1,4 +1,4 @@
package de.mummeit.pmg.service.exception; package de.mummeit.pmg.exception;
/** /**
* Base exception class for all Permission Manager related exceptions. * Base exception class for all Permission Manager related exceptions.

View File

@ -3,11 +3,11 @@ package de.mummeit.pmg.service;
import de.mummeit.pmg.api.PermissionManagerClient; import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.model.access.request.*; 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.PermittedResponse;
import de.mummeit.pmg.api.model.access.response.ListPermittedScopesResponse;
import de.mummeit.pmg.api.model.integration.Integration; import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.structure.Permission; import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.service.exception.AccessDeniedException; import de.mummeit.pmg.exception.IntegrationFailedException;
import de.mummeit.pmg.service.exception.IntegrationFailedException; import de.mummeit.pmg.exception.InvalidPermissionRequestException;
import de.mummeit.pmg.service.exception.InvalidPermissionRequestException;
import feign.FeignException; import feign.FeignException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -31,7 +31,6 @@ public class PermissionManager {
* @param scope The scope * @param scope The scope
* @return true if the user has access, false otherwise * @return true if the user has access, false otherwise
* @throws InvalidPermissionRequestException if any of the required parameters are null or empty * @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) { public boolean hasAccess(String userId, String domain, String permission, String scope) {
validateParameters(userId, domain, permission, scope); validateParameters(userId, domain, permission, scope);
@ -46,12 +45,9 @@ public class PermissionManager {
PermittedResponse response = client.checkAccess(request); PermittedResponse response = client.checkAccess(request);
return response.isPermitted(); return response.isPermitted();
} catch (FeignException e) { } catch (FeignException e) {
throw new AccessDeniedException( throw new InvalidPermissionRequestException(
"Failed to check access", "Technical error while checking access permissions",
userId, "Failed to communicate with permission service: " + e.getMessage(),
domain,
permission,
scope,
e 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<String> 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) { private void validateParameters(String userId, String domain, String permission, String scope) {
validateUserId(userId); validateUserId(userId);
validateDomain(domain); validateDomain(domain);

View File

@ -10,5 +10,9 @@ spring:
permission-manager: permission-manager:
connect-timeout: 1000 connect-timeout: 1000
read-timeout: 2000 read-timeout: 2000
permission-manager: permission-manager:
url: http://localhost:6060 url: http://localhost:6060
auth:
enabled: false
api-key:

View File

@ -1,14 +1,10 @@
package de.mummeit.pmg.api; package de.mummeit.pmg.api;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest; import de.mummeit.pmg.api.model.access.request.*;
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.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.Integration;
import de.mummeit.pmg.api.model.integration.DomainIntegration;
import de.mummeit.pmg.api.model.integration.PermissionIntegration; import de.mummeit.pmg.api.model.integration.PermissionIntegration;
import de.mummeit.pmg.api.model.integration.RoleIntegration; import de.mummeit.pmg.api.model.integration.RoleIntegration;
import de.mummeit.pmg.api.model.integration.RolePermissionRelationIntegration; 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.Permission;
import de.mummeit.pmg.api.model.structure.Role; import de.mummeit.pmg.api.model.structure.Role;
import de.mummeit.utility.BaseIntegrationTest; import de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; 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_USER = "test-user";
private static final String TEST_SCOPE = "test-scope"; private static final String TEST_SCOPE = "test-scope";
@AfterEach
void cleanup() {
try {
permissionManagerClient.deleteDomain(TEST_DOMAIN);
} catch (Exception e) {
// Ignore errors during cleanup
}
}
@Test @Test
@DisplayName("should return health status") @DisplayName("should return health status")
void getHealthStatus() { void getHealthStatus() {
@ -251,4 +257,62 @@ class PermissionManagerClientIntegrationTest extends BaseIntegrationTest {
// Clean up // Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN); 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);
}
} }

View File

@ -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.request.CheckAccessRequest;
import de.mummeit.pmg.api.model.access.response.PermittedResponse; import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.service.SecurityService; 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 de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@ -1,6 +1,6 @@
package de.mummeit.pmg.api.controller; 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.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;

View File

@ -3,11 +3,12 @@ package de.mummeit.pmg.service;
import de.mummeit.pmg.api.PermissionManagerClient; import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.model.access.request.*; 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.PermittedResponse;
import de.mummeit.pmg.api.model.access.response.ListPermittedScopesResponse;
import de.mummeit.pmg.api.model.integration.Integration; import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.structure.Permission; import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.service.exception.AccessDeniedException; import de.mummeit.pmg.exception.AccessDeniedException;
import de.mummeit.pmg.service.exception.IntegrationFailedException; import de.mummeit.pmg.exception.IntegrationFailedException;
import de.mummeit.pmg.service.exception.InvalidPermissionRequestException; import de.mummeit.pmg.exception.InvalidPermissionRequestException;
import feign.FeignException; import feign.FeignException;
import feign.Request; import feign.Request;
import feign.RequestTemplate; import feign.RequestTemplate;
@ -25,6 +26,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -60,12 +62,25 @@ class PermissionManagerTest {
} }
@Test @Test
@DisplayName("hasAccess should throw AccessDeniedException when client throws FeignException") @DisplayName("hasAccess should return false when access is denied")
void hasAccessShouldThrowAccessDeniedExceptionWhenClientThrowsFeignException() { 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); FeignException feignException = new FeignException.NotFound("Not Found", createRequest(), null, null);
when(client.checkAccess(any(CheckAccessRequest.class))).thenThrow(feignException); when(client.checkAccess(any(CheckAccessRequest.class))).thenThrow(feignException);
assertThrows(AccessDeniedException.class, () -> assertThrows(InvalidPermissionRequestException.class, () ->
permissionManager.hasAccess(TEST_USER, TEST_DOMAIN, TEST_PERMISSION, TEST_SCOPE) 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<String> 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() { private Request createRequest() {
return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate()); return Request.create(Request.HttpMethod.GET, "url", new HashMap<>(), null, new RequestTemplate());
} }

View File

@ -1,6 +1,8 @@
package de.mummeit.pmg.service.builder; package de.mummeit.pmg.service.builder;
import de.mummeit.pmg.api.model.integration.*; import de.mummeit.pmg.api.model.integration.*;
import de.mummeit.pmg.builder.IntegrationBuilder;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.mummeit.pmg.api.model.integration.DomainIntegration; import de.mummeit.pmg.api.model.integration.DomainIntegration;
import de.mummeit.pmg.api.model.integration.Integration; import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.config.AbstractPermissionManagerConfiguration;
import de.mummeit.pmg.service.PermissionManager; import de.mummeit.pmg.service.PermissionManager;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;

View File

@ -1,18 +1,19 @@
package de.mummeit.utility; 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.Network;
import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.GenericContainer;
public abstract class TestContainer { public abstract class TestContainer {
private static final String DB_DATABASE = "test"; private static final String DB_DATABASE = "test";
private static final String DB_USER = "postgres"; private static final String DB_USER = "postgres";
private static final String DB_PASSWORD = "super"; 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 Network network = Network.newNetwork();
public static GenericContainer<?> postgresContainer = new PostgreSQLContainer("postgres:latest") public static GenericContainer<?> postgresContainer = new PostgreSQLContainer("postgres:latest")
.withDatabaseName(DB_DATABASE) .withDatabaseName(DB_DATABASE)
.withUsername(DB_USER) .withUsername(DB_USER)
@ -27,15 +28,21 @@ public abstract class TestContainer {
.withEnv("DB_PASSWORD", DB_PASSWORD) .withEnv("DB_PASSWORD", DB_PASSWORD)
.withEnv("DB_HOST", "testcontainer-db") .withEnv("DB_HOST", "testcontainer-db")
.withEnv("DB_PORT", "5432") .withEnv("DB_PORT", "5432")
.withEnv("AUTH_ENABLED", "true")
.withEnv("AUTH_APIKEY", TEST_API_KEY)
.withNetwork(network) .withNetwork(network)
.withNetworkAliases("permission-manager"); .withNetworkAliases("permission-manager");
static { static {
postgresContainer.start(); postgresContainer.start();
permissionManagerContainer.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);
} }
} }