Skip to content

Commit 4e937a3

Browse files
Copilotphrocker
andauthored
Implement full ABAC (Attribute-Based Access Control) system with unified policy evaluation and Keycloak attribute management UI (#31)
* Initial plan * Add custom attributes support for LimitAccess annotation - Added customAttributes field to LimitAccess annotation - Updated AccessLimitations DTO to include custom attributes - Enhanced AccessControlAspect to validate custom attributes against user attributes - Created CustomAttributeMapping model, repository, and service for managing mappings - Added REST API endpoints for CRUD operations on custom attribute mappings - Created modern UI for managing custom attribute mappings - Added database migration for custom_attribute_mappings table - Integrated with existing UserAttributeService for attribute validation Co-authored-by: phrocker <[email protected]> * Add comprehensive tests for custom attribute functionality - Added CustomAttributeMappingServiceTest with 17 test cases - Added AccessControlAspectCustomAttributeTest with 10 test cases - Tests cover happy path, edge cases, validation, and error handling - All tests passing (91 tests run, 0 failures) Co-authored-by: phrocker <[email protected]> * Add comprehensive documentation for custom attribute mappings feature - Created detailed CUSTOM_ATTRIBUTE_MAPPINGS.md with usage examples - Documented REST API endpoints and request/response formats - Added quick start guide and configuration instructions - Included use cases, troubleshooting, and best practices - Added security considerations and performance notes Co-authored-by: phrocker <[email protected]> * Fix UserAttributeService dependency injection in AccessControlAspect - Changed UserAttributeService from required constructor parameter to optional @lazy field injection - Added null check in checkCustomAttribute method to handle cases where service is not available - Removed @requiredargsconstructor and manually created constructor - This resolves the KeycloakManager bean creation issue during application startup - All tests passing (91/91) Co-authored-by: phrocker <[email protected]> * fix kafka deployment * Fix layout and add navigation for custom attribute mappings UI - Fixed page layout to use proper container-fluid structure with sidebar - Changed from simple container to row/column layout matching other pages - Added "Custom Attributes" menu item to sidebar under CAN_MANAGE_APPLICATION - Positioned between Trust Policies and Telemetry for better organization - Fixed CSS positioning to prevent content appearing below menu - Build successful and ready for deployment Co-authored-by: phrocker <[email protected]> * Fix attribute endpoint * Add core ABAC models and PolicyEvaluator service - Created AttributeDefinition model for unified attribute schema across all scopes (SUBJECT, RESOURCE, ACTION, ENVIRONMENT) - Created AttributeAssignment model for binding attributes to users, roles, endpoints, data entities - Created AccessPolicy model for defining ABAC policies with priority and rule combination - Created PolicyRule model for individual attribute conditions within policies - Implemented PolicyEvaluator service as centralized evaluation engine - Added EvaluationContext for subject/resource/action/environment attributes - Added PolicyDecision for evaluation results - Created repositories for all ABAC models - Added database migration V30 for ABAC tables with indexes - Build successful, ready for integration work Co-authored-by: phrocker <[email protected]> * Integrate PolicyEvaluator with AccessControlAspect and add ABAC services - Integrated PolicyEvaluator into AccessControlAspect for ABAC-based attribute checking - Added lazy-loaded PolicyEvaluator field to avoid circular dependencies - Refactored checkCustomAttribute to use PolicyEvaluator when available, fallback to UserAttributeService - Created AttributeManagementService for managing attribute definitions and assignments - Implemented Keycloak sync method (syncUserAttributesFromKeycloak) for bidirectional attribute updates - Created CustomAttributeMigrationService to migrate legacy CustomAttributeMapping to ABAC policies - Added migration status tracking and automated policy generation from mappings - All 91 tests passing with new ABAC integration Co-authored-by: phrocker <[email protected]> * Add ABAC REST API endpoints for policy management and migration - Created DTOs for AccessPolicy, PolicyRule, and AttributeDefinition - Implemented AccessPolicyController with CRUD operations - Added migration endpoint (POST /api/v1/abac/policies/migrate) - Added migration status endpoint (GET /api/v1/abac/policies/migration/status) - Added policy listing (GET /api/v1/abac/policies) - Added policy details and rules (GET /api/v1/abac/policies/{id} and {id}/rules) - All endpoints protected with CAN_MANAGE_APPLICATION permission - Build successful across all modules Co-authored-by: phrocker <[email protected]> * Complete Phase 2: Add tests, scheduler, and comprehensive ABAC documentation - Added PolicyEvaluatorTest with 6 comprehensive test cases - Added AttributeManagementServiceTest with 9 test cases covering all service methods - Created KeycloakAttributeSyncScheduler for periodic attribute synchronization - Added ABAC_IMPLEMENTATION_GUIDE.md with complete usage examples and best practices - All 103 tests passing (12 new ABAC tests added) - Build successful across all 15 modules - Phase 2 now 100% complete Co-authored-by: phrocker <[email protected]> * Add comprehensive Keycloak attribute management UI and REST APIs - Created user_attributes.html for managing user attributes from Keycloak - Created attribute_definitions.html for managing attribute schemas - Added AttributeManagementController with REST APIs for CRUD operations - Added UserAttributesViewController for serving UI pages - Created AttributeAssignmentDTO and SyncStatusDTO for data transfer - Enhanced AttributeManagementService with methods for CRUD and sync - Updated KeycloakAttributeSyncScheduler with sync status tracking - Added repository methods for attribute queries - Updated sidebar with links to User Attributes and Attribute Definitions pages - All ABAC modules compile successfully Co-authored-by: phrocker <[email protected]> * Fix issues with build * Fix KeycloakAttributeSyncScheduler dependency injection issue - Made KeycloakAttributeSyncScheduler optional in AttributeManagementController - Removed @requiredargsconstructor and added manual constructor - Added @Autowired(required = false) for optional dependency injection - Added null checks in sync endpoints to handle when scheduler is disabled - Sync endpoints now return appropriate messages when sync is disabled - Build successful with all modules compiling correctly Co-authored-by: phrocker <[email protected]> * Update --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]> Co-authored-by: Marc Parisi <[email protected]>
1 parent 3c6d8bd commit 4e937a3

File tree

48 files changed

+7006
-18
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+7006
-18
lines changed

api/src/main/java/io/sentrius/sso/controllers/CustomErrorHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public Object handleError(HttpServletRequest request, HttpServletResponse respon
4242

4343
// Log as needed
4444
log.error("Error occurred: Status code {}, message {}", statusCode, message, ex);
45+
log.error(response.toString());
4546

4647
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
4748
if (isAjax) {
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package io.sentrius.sso.controllers.api;
2+
3+
import io.sentrius.sso.core.annotations.LimitAccess;
4+
import io.sentrius.sso.core.config.SystemOptions;
5+
import io.sentrius.sso.core.controllers.BaseController;
6+
import io.sentrius.sso.core.dto.CustomAttributeMappingDTO;
7+
import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum;
8+
import io.sentrius.sso.core.model.customattributes.CustomAttributeMapping;
9+
import io.sentrius.sso.core.services.ErrorOutputService;
10+
import io.sentrius.sso.core.services.UserService;
11+
import io.sentrius.sso.core.services.customattributes.CustomAttributeMappingService;
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import jakarta.servlet.http.HttpServletResponse;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.http.HttpStatus;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
import java.util.List;
20+
import java.util.stream.Collectors;
21+
22+
@Slf4j
23+
@RestController
24+
@RequestMapping("/api/v1/custom-attribute-mappings")
25+
public class CustomAttributeMappingController extends BaseController {
26+
27+
private final CustomAttributeMappingService customAttributeMappingService;
28+
29+
public CustomAttributeMappingController(
30+
CustomAttributeMappingService customAttributeMappingService,
31+
UserService userService,
32+
SystemOptions systemOptions,
33+
ErrorOutputService errorOutputService) {
34+
super(userService, systemOptions, errorOutputService);
35+
this.customAttributeMappingService = customAttributeMappingService;
36+
}
37+
38+
/**
39+
* Get all custom attribute mappings
40+
*/
41+
@GetMapping
42+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
43+
public ResponseEntity<List<CustomAttributeMappingDTO>> getAllMappings(
44+
HttpServletRequest request,
45+
HttpServletResponse response) {
46+
47+
log.debug("Getting all custom attribute mappings");
48+
49+
try {
50+
List<CustomAttributeMapping> mappings = customAttributeMappingService.getAllMappings();
51+
List<CustomAttributeMappingDTO> dtos = mappings.stream()
52+
.map(this::convertToDTO)
53+
.collect(Collectors.toList());
54+
55+
return ResponseEntity.ok(dtos);
56+
57+
} catch (Exception e) {
58+
log.error("Error getting custom attribute mappings", e);
59+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
60+
}
61+
}
62+
63+
/**
64+
* Get mappings for a specific endpoint
65+
*/
66+
@GetMapping("/endpoint")
67+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
68+
public ResponseEntity<List<CustomAttributeMappingDTO>> getMappingsByEndpoint(
69+
@RequestParam String endpoint,
70+
HttpServletRequest request,
71+
HttpServletResponse response) {
72+
73+
log.debug("Getting custom attribute mappings for endpoint: {}", endpoint);
74+
75+
try {
76+
List<CustomAttributeMapping> mappings = customAttributeMappingService.getMappingsByEndpoint(endpoint);
77+
List<CustomAttributeMappingDTO> dtos = mappings.stream()
78+
.map(this::convertToDTO)
79+
.collect(Collectors.toList());
80+
81+
return ResponseEntity.ok(dtos);
82+
83+
} catch (Exception e) {
84+
log.error("Error getting custom attribute mappings for endpoint: {}", endpoint, e);
85+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
86+
}
87+
}
88+
89+
/**
90+
* Create a new custom attribute mapping
91+
*/
92+
@PostMapping
93+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
94+
public ResponseEntity<CustomAttributeMappingDTO> createMapping(
95+
@RequestBody CustomAttributeMappingDTO dto,
96+
HttpServletRequest request,
97+
HttpServletResponse response) {
98+
99+
log.info("Creating custom attribute mapping for endpoint: {}", dto.getEndpoint());
100+
101+
try {
102+
CustomAttributeMapping mapping = customAttributeMappingService.createMapping(
103+
dto.getEndpoint(),
104+
dto.getAttributeName(),
105+
dto.getRequiredValue(),
106+
dto.getDescription()
107+
);
108+
109+
CustomAttributeMappingDTO responseDTO = convertToDTO(mapping);
110+
return ResponseEntity.ok(responseDTO);
111+
112+
} catch (IllegalArgumentException e) {
113+
log.warn("Invalid mapping data: {}", e.getMessage());
114+
return ResponseEntity.badRequest().build();
115+
} catch (Exception e) {
116+
log.error("Error creating custom attribute mapping", e);
117+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
118+
}
119+
}
120+
121+
/**
122+
* Update an existing custom attribute mapping
123+
*/
124+
@PutMapping("/{id}")
125+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
126+
public ResponseEntity<CustomAttributeMappingDTO> updateMapping(
127+
@PathVariable Long id,
128+
@RequestBody CustomAttributeMappingDTO dto,
129+
HttpServletRequest request,
130+
HttpServletResponse response) {
131+
132+
log.info("Updating custom attribute mapping: {}", id);
133+
134+
try {
135+
CustomAttributeMapping mapping = customAttributeMappingService.updateMapping(
136+
id,
137+
dto.getEndpoint(),
138+
dto.getAttributeName(),
139+
dto.getRequiredValue(),
140+
dto.getDescription(),
141+
dto.getIsActive()
142+
);
143+
144+
if (mapping == null) {
145+
return ResponseEntity.notFound().build();
146+
}
147+
148+
CustomAttributeMappingDTO responseDTO = convertToDTO(mapping);
149+
return ResponseEntity.ok(responseDTO);
150+
151+
} catch (IllegalArgumentException e) {
152+
log.warn("Invalid mapping data: {}", e.getMessage());
153+
return ResponseEntity.badRequest().build();
154+
} catch (Exception e) {
155+
log.error("Error updating custom attribute mapping: {}", id, e);
156+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
157+
}
158+
}
159+
160+
/**
161+
* Delete a custom attribute mapping
162+
*/
163+
@DeleteMapping("/{id}")
164+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
165+
public ResponseEntity<Void> deleteMapping(
166+
@PathVariable Long id,
167+
HttpServletRequest request,
168+
HttpServletResponse response) {
169+
170+
log.info("Deleting custom attribute mapping: {}", id);
171+
172+
try {
173+
boolean deleted = customAttributeMappingService.deleteMapping(id);
174+
175+
if (deleted) {
176+
return ResponseEntity.ok().build();
177+
} else {
178+
return ResponseEntity.notFound().build();
179+
}
180+
181+
} catch (Exception e) {
182+
log.error("Error deleting custom attribute mapping: {}", id, e);
183+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
184+
}
185+
}
186+
187+
/**
188+
* Get all unique endpoints that have custom attribute mappings
189+
*/
190+
@GetMapping("/endpoints")
191+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
192+
public ResponseEntity<List<String>> getAllEndpoints(
193+
HttpServletRequest request,
194+
HttpServletResponse response) {
195+
196+
log.debug("Getting all endpoints with custom attribute mappings");
197+
198+
try {
199+
List<String> endpoints = customAttributeMappingService.getAllEndpoints();
200+
return ResponseEntity.ok(endpoints);
201+
202+
} catch (Exception e) {
203+
log.error("Error getting endpoints", e);
204+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
205+
}
206+
}
207+
208+
/**
209+
* Get all unique attribute names used in mappings
210+
*/
211+
@GetMapping("/attribute-names")
212+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_MANAGE_APPLICATION})
213+
public ResponseEntity<List<String>> getAllAttributeNames(
214+
HttpServletRequest request,
215+
HttpServletResponse response) {
216+
217+
log.debug("Getting all attribute names");
218+
219+
try {
220+
List<String> attributeNames = customAttributeMappingService.getAllAttributeNames();
221+
return ResponseEntity.ok(attributeNames);
222+
223+
} catch (Exception e) {
224+
log.error("Error getting attribute names", e);
225+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
226+
}
227+
}
228+
229+
/**
230+
* Convert entity to DTO
231+
*/
232+
private CustomAttributeMappingDTO convertToDTO(CustomAttributeMapping mapping) {
233+
return CustomAttributeMappingDTO.builder()
234+
.id(mapping.getId())
235+
.endpoint(mapping.getEndpoint())
236+
.attributeName(mapping.getAttributeName())
237+
.requiredValue(mapping.getRequiredValue())
238+
.description(mapping.getDescription())
239+
.isActive(mapping.getIsActive())
240+
.createdAt(mapping.getCreatedAt())
241+
.updatedAt(mapping.getUpdatedAt())
242+
.build();
243+
}
244+
}

0 commit comments

Comments
 (0)