Skip to content

Commit 366d775

Browse files
Copilotphrocker
andauthored
Implement KeycloakAttributeSyncScheduler and bidirectional user synchronization with protocol mapper support and UI-driven custom attribute mapping access control (#36)
* Initial plan * Fully implement KeycloakAttributeSyncScheduler and user synchronization - Implemented full sync of all users from Keycloak with pagination support - Implemented sync of specific user from Keycloak - Added attribute filtering to exclude internal Keycloak attributes - Enhanced KeycloakService with methods for listing users, getting users, creating users, and updating attributes - Modified UserService to sync attributes from Keycloak when new users are created - Added method to create users in Keycloak when they are created in Sentrius - Added comprehensive tests for KeycloakAttributeSyncScheduler - Added comprehensive tests for KeycloakService - All tests passing Co-authored-by: phrocker <[email protected]> * update * Add password support for Keycloak user creation in UserApiController - Enhanced KeycloakService.createUser() with password parameter support - Added setUserPassword() method to set/reset user passwords in Keycloak - Updated UserApiController.addUser() to create users in Keycloak with passwords - Users created through UI now automatically get created in Keycloak with their password - Password is not temporary, allowing immediate login - Added 4 new tests for password functionality - All 17 tests passing in KeycloakServiceTest Co-authored-by: phrocker <[email protected]> * uipdate * Fix attribute handling for Keycloak 22+ user profile compatibility - Modified createUser() to set attributes AFTER user creation (Keycloak 22+ best practice) - Attributes are no longer set directly during UserRepresentation.create() - Instead, attributes are set via updateUserAttributes() after user creation - This ensures compatibility with Keycloak 22+ User Profile validation - Enhanced updateUserAttributes() with better error handling and logging - Updated test to reflect new attribute handling behavior - All 17 tests passing Co-authored-by: phrocker <[email protected]> * Remove attribute writing to Keycloak - attributes must be managed in User Profile - Removed automatic attribute setting during user creation - Added deprecation warning to updateUserAttributes() method - Clarified that custom attributes must be defined in Keycloak User Profile (Admin Console) - Attributes are READ-ONLY from Sentrius perspective - managed in Keycloak, synced to Sentrius - Enhanced documentation explaining Keycloak 22+ User Profile workflow - Updated test to verify attributes are NOT set during user creation - All 17 tests passing For Keycloak 22+ User Profile: 1. Define attributes in Keycloak Admin Console > Realm Settings > User Profile 2. Fill in attribute values for users through Keycloak UI 3. Sentrius sync scheduler reads attributes from Keycloak 4. Attributes are stored in Sentrius ABAC system for policy evaluation Co-authored-by: phrocker <[email protected]> * Enable bidirectional attribute sync with protocol mappers - Re-enabled attribute writing to Keycloak (reverted read-only approach) - Added protocol mappers for common ABAC attributes to realm template: - department, team_role, clearance_level, location, cost_center, project - Protocol mappers ensure attributes appear in OIDC tokens - Updated KeycloakService to set attributes after user creation - Added syncUserAttributesToKeycloak() method in AttributeManagementService - Attributes set in Sentrius can now be synced to Keycloak - Bidirectional sync: Sentrius ↔ Keycloak - Updated tests to match new behavior - All tests passing (17 KeycloakService tests, full test suite) Co-authored-by: phrocker <[email protected]> * Fix updates * update * Fix custom attribute check to pass endpoint parameter - Fixed checkCustomAttribute() call in AccessControlAspect to include endpoint parameter - This enables ABAC PolicyEvaluator to properly evaluate custom attributes from database - Custom attributes defined via ABAC page can now be evaluated within the aspect - All custom attribute checks now work correctly with both PolicyEvaluator and UserAttributeService Co-authored-by: phrocker <[email protected]> * Add database endpoint attribute checking to AccessControlAspect - Added AttributeAssignmentRepository injection to AccessControlAspect - Implemented checkDatabaseEndpointAttributes() method to check attributes from database - Retrieves attributes assigned to endpoints via ABAC page (TargetType.ENDPOINT) - Validates that users have required attribute values for the endpoint - Integrates with existing checkCustomAttribute() for consistency - Now checks BOTH annotation-defined AND database-defined custom attributes - Attributes defined via ABAC page are now properly evaluated for access control Co-authored-by: phrocker <[email protected]> * Fix database endpoint attribute checking to use ABAC policies - Replaced AttributeAssignment lookup with PolicyEvaluator - Now properly uses AccessPolicy and PolicyRule to determine endpoint requirements - PolicyEvaluator evaluates user attributes against policy rules - Supports complex policy logic (AND/OR combinations, various operators) - Correctly implements the ABAC workflow: 1. Attribute Definitions - define available attributes 2. User Assignments - assign attribute values to users 3. Access Mappings (Policies) - define rules for endpoint access - Removed AttributeAssignmentRepository injection (not needed) - Access Mappings from ABAC page now properly enforced Co-authored-by: phrocker <[email protected]> * Fix database endpoint attribute checking to use CustomAttributeMapping - Replaced PolicyEvaluator with CustomAttributeMappingService - Now properly queries custom_attribute_mappings table from UI - CustomAttributeMapping stores endpoint requirements (e.g., /api/v1/chat/** requires department=engineering) - Integrates with existing checkCustomAttribute() method for validation - Properly implements UI-driven access control workflow: 1. Admin creates mapping via CustomAttributeMappingController 2. Mapping stored in custom_attribute_mappings table 3. AccessControlAspect retrieves and enforces mappings - Custom mappings defined via ABAC UI are now properly enforced Co-authored-by: phrocker <[email protected]> * Fix custom attribute checking to verify user actually has required attributes - Fixed checkCustomAttribute() to properly check if user has the required attribute value - Previously was incorrectly adding the required value to user's context, making all checks pass - Now correctly: 1. Builds context which loads user's AttributeAssignments from database 2. Checks if user's attribute value matches the required value 3. Returns true only if user actually has the attribute with the required value - Fixed test file compilation errors (duplicate class definitions) - Addresses issue where mappings like "clearance_level=low" were found but not validated against user's actual attributes - All tests passing Co-authored-by: phrocker <[email protected]> * commit --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]> Co-authored-by: Marc Parisi <[email protected]>
1 parent 3fc3b23 commit 366d775

File tree

24 files changed

+2178
-213
lines changed

24 files changed

+2178
-213
lines changed

api/src/main/java/io/sentrius/sso/controllers/api/abac/AttributeManagementController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public ResponseEntity<AttributeAssignmentDTO> createUserAttribute(@RequestBody A
8989
if (dto.isSyncToKeycloak() && dto.getTargetType().equals("USER")) {
9090
Map<String, String> attrs = new HashMap<>();
9191
attrs.put(dto.getAttributeName(), dto.getAttributeValue());
92-
attributeManagementService.syncUserAttributesFromKeycloak(dto.getTargetId(), attrs);
92+
//attributeManagementService.syncUserAttributesToKeycloak(dto.getTargetId(), attrs);
9393
}
9494

9595
return ResponseEntity.ok(toAssignmentDTO(assignment));
@@ -244,6 +244,7 @@ private AttributeAssignmentDTO toAssignmentDTO(AttributeAssignment assignment) {
244244
dto.setTargetId(assignment.getTargetId());
245245
dto.setAttributeName(assignment.getAttributeDefinition().getAttributeName());
246246
dto.setAttributeValue(assignment.getAttributeValue());
247+
dto.setSource(assignment.getSource().name());
247248
if (assignment.getValidFrom() != null) {
248249
dto.setValidFrom(LocalDateTime.ofInstant(assignment.getValidFrom(), ZoneOffset.UTC));
249250
}

api/src/main/java/io/sentrius/sso/controllers/api/users/UserApiController.java

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.sentrius.sso.core.services.agents.AgentService;
3939
import io.sentrius.sso.core.services.agents.ZeroTrustClientService;
4040
import io.sentrius.sso.core.services.security.CryptoService;
41+
import io.sentrius.sso.core.services.security.KeycloakService;
4142
import io.sentrius.sso.core.services.security.ZeroTrustAccessTokenService;
4243
import io.sentrius.sso.core.services.security.ZeroTrustRequestService;
4344
import io.sentrius.sso.core.services.users.UserAttributeService;
@@ -76,6 +77,7 @@ public class UserApiController extends BaseController {
7677
final ZeroTrustAccessTokenService ztatService;
7778
final AgentService agentService;
7879
final ZeroTrustClientService zeroTrustClientService;
80+
final KeycloakService keycloakService;
7981

8082

8183
@Value("${agentproxy.externalUrl:}")
@@ -100,7 +102,7 @@ protected UserApiController(
100102
SessionService sessionService,
101103
ZeroTrustRequestService ztatRequestService,
102104
ZeroTrustAccessTokenService ztatService, AgentService agentService,
103-
ZeroTrustClientService zeroTrustClientService
105+
ZeroTrustClientService zeroTrustClientService, KeycloakService keycloakService
104106
) {
105107
super(userService, systemOptions, errorOutputService);
106108
this.hostGroupService = hostGroupService;
@@ -113,6 +115,7 @@ protected UserApiController(
113115
this.ztatService = ztatService;
114116
this.agentService = agentService;
115117
this.zeroTrustClientService = zeroTrustClientService;
118+
this.keycloakService = keycloakService;
116119
}
117120

118121
@GetMapping("list")
@@ -171,13 +174,48 @@ public ResponseEntity<ObjectNode> addUser(HttpServletRequest request, HttpServle
171174
ObjectNode node = JsonUtil.MAPPER.createObjectNode();
172175

173176
try {
174-
user.setPassword(userService.encodePassword( user.getPassword()));
175-
// Save user using service
176-
userService.addUscer(user);
177-
node.put("status","User successfully added.");
177+
// Store the original password before encoding
178+
String originalPassword = user.getPassword();
179+
180+
// Encode password for Sentrius database
181+
user.setPassword(userService.encodePassword(user.getPassword()));
182+
183+
// Save user in Sentrius
184+
user = userService.addUscer(user);
185+
186+
// Create user in Keycloak with the original password
187+
// Keycloak will hash the password itself
188+
String[] nameParts = user.getName() != null ? user.getName().split(" ", 2) : new String[]{user.getUsername(), ""};
189+
String firstName = nameParts[0];
190+
String lastName = nameParts.length > 1 ? nameParts[1] : "";
191+
192+
String keycloakUserId = keycloakService.createUser(
193+
user.getUsername(),
194+
user.getEmailAddress(),
195+
firstName,
196+
lastName,
197+
null, // No custom attributes initially
198+
originalPassword, // Use original password (not encoded)
199+
false // Not temporary - user can use this password
200+
);
201+
202+
if (keycloakUserId != null) {
203+
// Update the user with the Keycloak ID if not already set
204+
if (user.getUserId() == null || user.getUserId().isEmpty()) {
205+
user.setUserId(keycloakUserId);
206+
userService.save(user);
207+
}
208+
node.put("status", "User successfully added to Sentrius and Keycloak.");
209+
log.info("User {} created in both Sentrius and Keycloak with ID {}", user.getUsername(), keycloakUserId);
210+
} else {
211+
node.put("status", "User added to Sentrius but failed to create in Keycloak. User may not be able to log in.");
212+
log.warn("User {} created in Sentrius but failed to create in Keycloak", user.getUsername());
213+
}
214+
178215
return ResponseEntity.ok(node);
179216
} catch (Exception e) {
180-
node.put("status","Error adding user");
217+
log.error("Error adding user", e);
218+
node.put("status", "Error adding user: " + e.getMessage());
181219
return ResponseEntity.internalServerError().body(node);
182220
}
183221
}
@@ -231,6 +269,7 @@ public String deleteUser(@RequestParam("userId") String userId, @RequestParam(re
231269
return "redirect:/sso/v1/users/list?message=" + MessagingUtil.getMessageId(MessagingUtil.UNEXPECTED_ERROR);
232270

233271
}
272+
keycloakService.deleteUser(usr.getUsername());
234273
userService.deleteUser(usr.getId());
235274
} else {
236275
Long id = Long.parseLong(cryptoService.decrypt(userId));
@@ -239,7 +278,10 @@ public String deleteUser(@RequestParam("userId") String userId, @RequestParam(re
239278
return "redirect:/sso/v1/users/list?message=" +
240279
MessagingUtil.getMessageId(MessagingUtil.UNEXPECTED_ERROR);
241280
}
281+
var user = userService.getUserById(id);
282+
keycloakService.deleteUser(user.getUsername());
242283
userService.deleteUser(id);
284+
243285
}
244286
return "redirect:/sso/v1/users/list?message=" + MessagingUtil.getMessageId(MessagingUtil.USER_DELETE_SUCCESS);
245287
}

api/src/main/java/io/sentrius/sso/controllers/view/UserController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public UserSettings getUserSettingsAttribute(HttpServletRequest request, HttpSer
7979
@ModelAttribute("typeList")
8080
public List<UserTypeDTO> getUserTypeList() {
8181
var types = userService.getUserTypeList();
82-
log.info("UserTypeList: {}", types);
8382
return types;
8483
}
8584

api/src/main/resources/static/js/add_user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ document.addEventListener('DOMContentLoaded', function () {
3535
.catch((error) => {
3636
$("#alertTop").hide();
3737
$("#alertTopError").text("User Not Added").show().delay(3000).fadeOut();
38-
cd });
38+
});
3939
});
4040
}
4141
});

api/src/main/resources/templates/fragments/add_user.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ <h5 class="modal-title" id="userFormModalLabel">Add User</h5>
2626
</div>
2727
<div class="mb-3">
2828
<label for="emailAddress" class="form-label">E-mail Address</label>
29-
<input type="text" class="form-control" th:field="*{emailAddress}" required>
29+
<input type="email"
30+
class="form-control"
31+
th:field="*{emailAddress}"
32+
placeholder="[email protected]"
33+
required
34+
pattern="^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$">
3035
</div>
3136
<div class="mb-3">
3237
<label for="password" class="form-label">Password</label>

api/src/main/resources/templates/fragments/sidebar.html

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@
1919
<i class="fas fa-server"></i> <span class="ms-1 d-none d-sm-inline">SSH Servers</span>
2020
</a>
2121
</li>-->
22-
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_VIEW_USERS')}">
23-
<a href="/sso/v1/users/list" class="nav-link px-0 align-middle">
24-
<i class="fas fa-users"></i> <span class="ms-1 d-none d-sm-inline">Users</span>
25-
</a>
26-
</li>
2722
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
2823
<a href="/sso/v1/system/settings" class="nav-link px-0 align-middle">
2924
<i class="fas fa-cog"></i> <span class="ms-1 d-none d-sm-inline">Settings</span>

api/src/main/resources/templates/sso/attributes_unified.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ <h5 style="color: #28a745; margin-bottom: 15px;">📋 How to Use ABAC - Step by
714714
<select id="filterSource" onchange="filterAttributesTable()">
715715
<option value="">All Sources</option>
716716
<option value="keycloak">Keycloak</option>
717-
<option value="local">Local</option>
717+
<option value="sentrius">Sentrius</option>
718718
</select>
719719
</div>
720720
<div class="filter-item">
@@ -1229,7 +1229,7 @@ <h2 class="modal-title" id="mappingModalTitle">Add Custom Attribute Mapping</h2>
12291229
<td>${escapeHtml(attr.attributeName)}</td>
12301230
<td>${escapeHtml(attr.attributeValue)}</td>
12311231
<td><span class="badge ${attr.syncedFromKeycloak ? 'badge-keycloak' : 'badge-local'}">
1232-
${attr.syncedFromKeycloak ? 'Keycloak' : 'Local'}
1232+
${attr.syncedFromKeycloak ? 'Keycloak' : 'Sentrius'}
12331233
</span></td>
12341234
<td>${attr.validFrom ? new Date(attr.validFrom).toLocaleDateString() : '-'}</td>
12351235
<td>${attr.validUntil ? new Date(attr.validUntil).toLocaleDateString() : '-'}</td>

api/src/test/java/io/sentrius/sso/controllers/api/users/UserPublicKeyApiControllerTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.sentrius.sso.core.services.UserCustomizationService;
1313
import io.sentrius.sso.core.services.SessionService;
1414
import io.sentrius.sso.core.services.security.CryptoService;
15+
import io.sentrius.sso.core.services.security.KeycloakService;
1516
import io.sentrius.sso.core.services.security.ZeroTrustRequestService;
1617
import io.sentrius.sso.core.services.security.ZeroTrustAccessTokenService;
1718
import io.sentrius.sso.core.services.agents.AgentService;
@@ -82,6 +83,9 @@ public class UserPublicKeyApiControllerTest {
8283
@Mock
8384
private HttpServletResponse response;
8485

86+
@Mock
87+
private KeycloakService keycloakService;
88+
8589
private UserApiController controller;
8690

8791
@BeforeEach
@@ -99,7 +103,8 @@ void setUp() {
99103
zeroTrustRequestService,
100104
zeroTrustAccessTokenService,
101105
agentService,
102-
zeroTrustClientService
106+
zeroTrustClientService,
107+
keycloakService
103108
);
104109
}
105110

core/src/main/java/io/sentrius/sso/core/dto/abac/AttributeAssignmentDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class AttributeAssignmentDTO {
1212
private String username; // For display
1313
private String attributeName;
1414
private String attributeValue;
15+
private String source;
1516
private LocalDateTime validFrom;
1617
private LocalDateTime validUntil;
1718
private boolean syncedFromKeycloak;

0 commit comments

Comments
 (0)