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

@ -0,0 +1,254 @@
package de.mummeit.pmg.api;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest;
import de.mummeit.pmg.api.model.access.request.Permit;
import de.mummeit.pmg.api.model.access.request.PermitRequest;
import de.mummeit.pmg.api.model.access.request.RevokeScopeAccessRequest;
import de.mummeit.pmg.api.model.access.request.RevokeUserAccessRequest;
import de.mummeit.pmg.api.model.access.request.SearchPermitRequest;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.model.integration.DomainIntegration;
import de.mummeit.pmg.api.model.integration.Integration;
import de.mummeit.pmg.api.model.integration.PermissionIntegration;
import de.mummeit.pmg.api.model.integration.RoleIntegration;
import de.mummeit.pmg.api.model.integration.RolePermissionRelationIntegration;
import de.mummeit.pmg.api.model.structure.Domain;
import de.mummeit.pmg.api.model.structure.Permission;
import de.mummeit.pmg.api.model.structure.Role;
import de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class PermissionManagerClientIntegrationTest extends BaseIntegrationTest {
@Autowired
private PermissionManagerClient permissionManagerClient;
private static final String TEST_DOMAIN = "test-domain";
private static final String TEST_PERMISSION = "test-permission";
private static final String TEST_ROLE = "test-role";
private static final String TEST_USER = "test-user";
private static final String TEST_SCOPE = "test-scope";
@Test
@DisplayName("should return health status")
void getHealthStatus() {
String healthStatus = permissionManagerClient.getHealthStatus();
assertNotNull(healthStatus);
}
@Test
@DisplayName("should successfully perform integrations")
void integrationShouldRun() {
List<Integration<?>> integrations = List.of(
DomainIntegration.builder()
.id("1")
.action(Integration.Action.create)
.data(DomainIntegration.Data.builder()
.name("test")
.description("test")
.build())
.build(),
PermissionIntegration.builder()
.id("2")
.action(Integration.Action.create)
.data(PermissionIntegration.Data.builder()
.domain("test")
.name("test")
.description("test")
.build())
.build(),
RoleIntegration.builder()
.id("3")
.action(Integration.Action.create)
.data(RoleIntegration.Data.builder()
.domain("test")
.name("test")
.description("test")
.build())
.build(),
RolePermissionRelationIntegration.builder()
.id("4")
.action(Integration.Action.create)
.data(RolePermissionRelationIntegration.Data.builder()
.domain("test")
.role("test")
.permissions(List.of("test"))
.build())
.build()
);
assertDoesNotThrow(() -> permissionManagerClient.performIntegration(integrations));
}
@Test
@DisplayName("should manage domains successfully")
void domainManagement() {
// Create domain
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
domain.setDescription("Test Domain Description");
Domain createdDomain = permissionManagerClient.createDomain(domain);
assertNotNull(createdDomain);
assertEquals(TEST_DOMAIN, createdDomain.getName());
// Get domain
Domain retrievedDomain = permissionManagerClient.getDomain(TEST_DOMAIN);
assertNotNull(retrievedDomain);
assertEquals(TEST_DOMAIN, retrievedDomain.getName());
// Update domain
domain.setDescription("Updated Description");
Domain updatedDomain = permissionManagerClient.updateDomain(TEST_DOMAIN, domain);
assertNotNull(updatedDomain);
assertEquals("Updated Description", updatedDomain.getDescription());
// Delete domain
assertDoesNotThrow(() -> permissionManagerClient.deleteDomain(TEST_DOMAIN));
}
@Test
@DisplayName("should manage permissions successfully")
void permissionManagement() {
// First create a domain
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
domain.setDescription("Test Domain Description");
permissionManagerClient.createDomain(domain);
// Create permission
Permission permission = new Permission();
permission.setName(TEST_PERMISSION);
permission.setDescription("Test Permission Description");
Permission createdPermission = permissionManagerClient.createPermission(TEST_DOMAIN, permission);
assertNotNull(createdPermission);
assertEquals(TEST_PERMISSION, createdPermission.getName());
// Get permission
Permission retrievedPermission = permissionManagerClient.getPermission(TEST_DOMAIN, TEST_PERMISSION);
assertNotNull(retrievedPermission);
assertEquals(TEST_PERMISSION, retrievedPermission.getName());
// Update permission
permission.setDescription("Updated Permission Description");
Permission updatedPermission = permissionManagerClient.updatePermission(TEST_DOMAIN, TEST_PERMISSION, permission);
assertNotNull(updatedPermission);
assertEquals("Updated Permission Description", updatedPermission.getDescription());
// Delete permission
assertDoesNotThrow(() -> permissionManagerClient.deletePermission(TEST_DOMAIN, TEST_PERMISSION));
// Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN);
}
@Test
@DisplayName("should manage roles successfully")
void roleManagement() {
// First create a domain
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
domain.setDescription("Test Domain Description");
permissionManagerClient.createDomain(domain);
// Create role
Role role = new Role();
role.setName(TEST_ROLE);
role.setDescription("Test Role Description");
role.setPermissions(List.of());
Role createdRole = permissionManagerClient.createRole(TEST_DOMAIN, role);
assertNotNull(createdRole);
assertEquals(TEST_ROLE, createdRole.getName());
// Get role
Role retrievedRole = permissionManagerClient.getRole(TEST_DOMAIN, TEST_ROLE);
assertNotNull(retrievedRole);
assertEquals(TEST_ROLE, retrievedRole.getName());
// Update role
role.setDescription("Updated Role Description");
Role updatedRole = permissionManagerClient.updateRole(TEST_DOMAIN, TEST_ROLE, role);
assertNotNull(updatedRole);
assertEquals("Updated Role Description", updatedRole.getDescription());
// Delete role
assertDoesNotThrow(() -> permissionManagerClient.deleteRole(TEST_DOMAIN, TEST_ROLE));
// Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN);
}
@Test
@DisplayName("should manage access successfully")
void accessManagement() {
// Setup: Create domain, permission, and role
Domain domain = new Domain();
domain.setName(TEST_DOMAIN);
permissionManagerClient.createDomain(domain);
Permission permission = new Permission();
permission.setName(TEST_PERMISSION);
permissionManagerClient.createPermission(TEST_DOMAIN, permission);
Role role = new Role();
role.setName(TEST_ROLE);
role.setPermissions(List.of());
permissionManagerClient.createRole(TEST_DOMAIN, role);
// Test permit access
PermitRequest permitRequest = new PermitRequest();
Permit permit = new Permit();
permit.setDomain(TEST_DOMAIN);
permit.setRoles(List.of(TEST_ROLE));
permit.setPermissions(List.of(TEST_PERMISSION));
permitRequest.setPermits(List.of(permit));
permitRequest.setUserId(TEST_USER);
permitRequest.setScope(TEST_SCOPE);
assertDoesNotThrow(() -> permissionManagerClient.permitAccess(permitRequest));
// Test check access
CheckAccessRequest checkRequest = new CheckAccessRequest();
checkRequest.setDomain(TEST_DOMAIN);
checkRequest.setPermission(TEST_PERMISSION);
checkRequest.setUserId(TEST_USER);
checkRequest.setScope(TEST_SCOPE);
PermittedResponse response = permissionManagerClient.checkAccess(checkRequest);
assertNotNull(response);
assertTrue(response.isPermitted());
// Test search permits
SearchPermitRequest searchRequest = new SearchPermitRequest();
searchRequest.setUserId(TEST_USER);
searchRequest.setScope(TEST_SCOPE);
List<Permission> permits = permissionManagerClient.searchPermits(searchRequest);
assertNotNull(permits);
assertFalse(permits.isEmpty());
// Test revoke access
assertDoesNotThrow(() -> permissionManagerClient.revokeAccess(permitRequest));
// Test revoke scope access
RevokeScopeAccessRequest revokeScopeRequest = new RevokeScopeAccessRequest();
revokeScopeRequest.setScope(TEST_SCOPE);
assertDoesNotThrow(() -> permissionManagerClient.revokeScopeAccess(revokeScopeRequest));
// Test revoke user access
RevokeUserAccessRequest revokeUserRequest = new RevokeUserAccessRequest();
revokeUserRequest.setUserId(TEST_USER);
assertDoesNotThrow(() -> permissionManagerClient.revokeUserAccess(revokeUserRequest));
// Clean up
permissionManagerClient.deleteDomain(TEST_DOMAIN);
}
}

View File

@ -0,0 +1,20 @@
package de.mummeit.pmg.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class TestSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}

View File

@ -0,0 +1,19 @@
package de.mummeit.pmg.api.controller;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
@EnableAutoConfiguration
@EnableWebMvc
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {
"de.mummeit.pmg.api.controller",
"de.mummeit.pmg.api.aspect"
})
public class TestConfig implements WebMvcConfigurer {
}

View File

@ -0,0 +1,65 @@
package de.mummeit.pmg.api.controller;
import de.mummeit.pmg.api.annotation.RequiresPermission;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/resource/{resourceId}")
@RequiresPermission(
domain = "test-domain",
permission = "read",
scope = "#resourceId",
userIdExpression = "#userId"
)
public String getResource(@PathVariable String resourceId, @RequestParam String userId) {
return "Access granted to resource " + resourceId + " for user " + userId;
}
@PostMapping("/resource/{resourceId}/action")
@RequiresPermission(
domain = "test-domain",
permission = "write",
scope = "#resourceId",
userIdExpression = "#request.userId"
)
public String performAction(
@PathVariable String resourceId,
@RequestBody ActionRequest request
) {
return "Action performed on resource " + resourceId + " by user " + request.getUserId();
}
@GetMapping("/secured-resource/{resourceId}")
@RequiresPermission(
domain = "test-domain",
permission = "read",
scope = "#resourceId"
)
public String getSecuredResource(@PathVariable String resourceId) {
return "Access granted to secured resource " + resourceId;
}
public static class ActionRequest {
private String userId;
private String action;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}
}

View File

@ -0,0 +1,168 @@
package de.mummeit.pmg.api.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.mummeit.pmg.api.PermissionManagerClient;
import de.mummeit.pmg.api.config.TestSecurityConfig;
import de.mummeit.pmg.api.model.access.request.CheckAccessRequest;
import de.mummeit.pmg.api.model.access.response.PermittedResponse;
import de.mummeit.pmg.api.service.SecurityService;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import de.mummeit.utility.BaseIntegrationTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
@ComponentScan(basePackages = {
"de.mummeit.pmg.api.controller",
"de.mummeit.pmg.api.aspect",
"de.mummeit.pmg.api.service"
})
@Import(TestSecurityConfig.class)
class TestControllerIntegrationTest extends BaseIntegrationTest {
@TestConfiguration
@EnableWebMvc
static class TestConfig {
@Bean
public TestController testController() {
return new TestController();
}
@Bean
public TestExceptionHandler testExceptionHandler() {
return new TestExceptionHandler();
}
@Bean
public SecurityService securityService() {
return new SecurityService();
}
}
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private PermissionManagerClient permissionManagerClient;
@Test
void getResource_whenPermitted_shouldSucceed() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/resource/123")
.param("userId", "user1"))
.andExpect(status().isOk())
.andExpect(content().string("Access granted to resource 123 for user user1"));
}
@Test
void getResource_whenDenied_shouldFail() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(false);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/resource/123")
.param("userId", "user1"))
.andExpect(status().isForbidden())
.andExpect(result -> result.getResolvedException().getClass().equals(AccessDeniedException.class));
}
@Test
void performAction_whenPermitted_shouldSucceed() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
TestController.ActionRequest request = new TestController.ActionRequest();
request.setUserId("user1");
request.setAction("test");
// When/Then
mockMvc.perform(post("/api/test/resource/123/action")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(content().string("Action performed on resource 123 by user user1"));
}
@Test
void performAction_whenDenied_shouldFail() throws Exception {
// Given
PermittedResponse response = new PermittedResponse();
response.setPermitted(false);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
TestController.ActionRequest request = new TestController.ActionRequest();
request.setUserId("user1");
request.setAction("test");
// When/Then
mockMvc.perform(post("/api/test/resource/123/action")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isForbidden())
.andExpect(result -> result.getResolvedException().getClass().equals(AccessDeniedException.class));
}
@Test
void getSecuredResource_whenPermitted_shouldSucceed() throws Exception {
// Given
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user1", null)
);
PermittedResponse response = new PermittedResponse();
response.setPermitted(true);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/secured-resource/123"))
.andExpect(status().isOk())
.andExpect(content().string("Access granted to secured resource 123"));
}
@Test
void getSecuredResource_whenDenied_shouldFail() throws Exception {
// Given
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user1", null)
);
PermittedResponse response = new PermittedResponse();
response.setPermitted(false);
when(permissionManagerClient.checkAccess(any(CheckAccessRequest.class))).thenReturn(response);
// When/Then
mockMvc.perform(get("/api/test/secured-resource/123"))
.andExpect(status().isForbidden())
.andExpect(result -> result.getResolvedException().getClass().equals(AccessDeniedException.class));
}
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.controller;
import de.mummeit.pmg.service.exception.AccessDeniedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice(basePackageClasses = TestController.class)
public class TestExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public void handleAccessDeniedException() {
// No response body needed, just return 403 status
}
}

View File

@ -0,0 +1,17 @@
package de.mummeit.pmg.api.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class SecurityService {
public String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new IllegalStateException("No authenticated user found");
}
return authentication.getName();
}
}