diff --git a/.local.env b/.local.env index a94ea226..a03b0e56 100644 --- a/.local.env +++ b/.local.env @@ -1,4 +1,4 @@ -SENTRIUS_VERSION=1.1.334 +SENTRIUS_VERSION=1.1.341 SENTRIUS_SSH_VERSION=1.1.41 SENTRIUS_KEYCLOAK_VERSION=1.1.53 SENTRIUS_AGENT_VERSION=1.1.42 diff --git a/.local.env.bak b/.local.env.bak index a94ea226..a03b0e56 100644 --- a/.local.env.bak +++ b/.local.env.bak @@ -1,4 +1,4 @@ -SENTRIUS_VERSION=1.1.334 +SENTRIUS_VERSION=1.1.341 SENTRIUS_SSH_VERSION=1.1.41 SENTRIUS_KEYCLOAK_VERSION=1.1.53 SENTRIUS_AGENT_VERSION=1.1.42 diff --git a/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java b/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java index 45e17f2c..71d88542 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.BooleanNode; @@ -15,18 +16,24 @@ import io.sentrius.sso.core.annotations.LimitAccess; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.controllers.BaseController; +import io.sentrius.sso.core.dto.HostGroupDTO; +import io.sentrius.sso.core.dto.UserPublicKeyDTO; import io.sentrius.sso.core.exceptions.ZtatException; +import io.sentrius.sso.core.model.hostgroup.HostGroup; import io.sentrius.sso.core.model.security.UserType; import io.sentrius.sso.core.model.security.enums.UserAccessEnum; import io.sentrius.sso.core.model.users.User; import io.sentrius.sso.core.dto.UserDTO; import io.sentrius.sso.core.dto.UserTypeDTO; import io.sentrius.sso.core.model.users.UserConfig; +import io.sentrius.sso.core.model.users.UserPublicKey; import io.sentrius.sso.core.model.users.UserSettings; +import io.sentrius.sso.core.services.ConfigurationService; import io.sentrius.sso.core.services.ErrorOutputService; import io.sentrius.sso.core.services.HostGroupService; import io.sentrius.sso.core.services.SessionService; import io.sentrius.sso.core.services.UserCustomizationService; +import io.sentrius.sso.core.services.UserPublicKeyService; import io.sentrius.sso.core.services.UserService; import io.sentrius.sso.core.services.agents.AgentService; import io.sentrius.sso.core.services.agents.ZeroTrustClientService; @@ -45,8 +52,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -63,6 +72,7 @@ public class UserApiController extends BaseController { final CryptoService cryptoService; private final MessagingUtil messagingUtil; final UserCustomizationService userThemeService; + final UserPublicKeyService userPublicKeyService; final ZeroTrustRequestService ztatRequestService; final ZeroTrustAccessTokenService ztatService; final AgentService agentService; @@ -87,6 +97,7 @@ protected UserApiController( HostGroupService hostGroupService, CryptoService cryptoService, MessagingUtil messagingUtil, UserCustomizationService userThemeService, + UserPublicKeyService userPublicKeyService, SessionService sessionService, ZeroTrustRequestService ztatRequestService, ZeroTrustAccessTokenService ztatService, AgentService agentService, @@ -97,6 +108,7 @@ protected UserApiController( this.cryptoService = cryptoService; this.messagingUtil = messagingUtil; this.userThemeService = userThemeService; + this.userPublicKeyService = userPublicKeyService; this.sessionService = sessionService; this.ztatRequestService = ztatRequestService; this.ztatService = ztatService; @@ -428,5 +440,131 @@ private List> getAgentSessionDurations() { return agentSessions; } + // Public Key Management Endpoints + + @GetMapping("/publickeys") + public ResponseEntity> getUserPublicKeys(HttpServletRequest request, + HttpServletResponse response) { + var user = userService.getOperatingUser(request, response, null); + var publicKeys = userPublicKeyService.getPublicKeysForUser(user.getId()); + List publicKeyDTOs = new ArrayList<>(); + for (UserPublicKey key : publicKeys) { + UserPublicKeyDTO dto = UserPublicKeyDTO.builder().build(); + //dto.setId(key.getId()); + dto.setKeyName(key.getKeyName()); + dto.setKeyType(key.getKeyType()); + dto.setPublicKey(key.getPublicKey()); + dto.setIsEnabled(key.getIsEnabled()); + if (key.getHostGroup() != null) { + dto.setHostGroup( HostGroupDTO.builder().groupId(key.getHostGroup().getId()).build()); + //dto.setHostGroupName(key.getHostGroup().getName()); + } + publicKeyDTOs.add(dto); + } + return ResponseEntity.ok(publicKeyDTOs); + } + + @PostMapping("/publickeys") + public ResponseEntity addPublicKey(HttpServletRequest request, HttpServletResponse response, @RequestBody + UserPublicKeyDTO publicKey) { + ObjectNode node = JsonUtil.MAPPER.createObjectNode(); + try { + + HostGroup hostGroup = hostGroupService.getHostGroup(publicKey.getHostGroup().getGroupId()); + + + UserPublicKey key = new UserPublicKey(); + key.setKeyName(publicKey.getKeyName()); + key.setKeyType(publicKey.getKeyType()); + key.setPublicKey(publicKey.getPublicKey()); + key.setIsEnabled(publicKey.getIsEnabled()); + if (null != hostGroup) { + key.setHostGroup(hostGroup); + } + var user = userService.getOperatingUser(request, response, null); + key.setUser(user); + + if (key.getCreatedAt() == null) { + key.setCreatedAt(new java.sql.Timestamp(System.currentTimeMillis())); + } + + var savedKey = userPublicKeyService.addPublicKey(key); + node.put("status", "Public key successfully added"); + node.put("id", savedKey.getId()); + return ResponseEntity.ok(node); + } catch (Exception e) { + log.error("Error adding public key", e); + node.put("status", "Error adding public key"); + return ResponseEntity.internalServerError().body(node); + } + } + + @PostMapping("/publickeys/{keyId}/assign") + public ResponseEntity assignPublicKeyToHostGroup( + HttpServletRequest request, HttpServletResponse response, + @PathVariable Long keyId, @RequestParam Long hostGroupId) { + ObjectNode node = JsonUtil.MAPPER.createObjectNode(); + try { + var user = userService.getOperatingUser(request, response, null); + var publicKeyOpt = userPublicKeyService.getPublicKeyById(keyId); + + if (publicKeyOpt.isEmpty()) { + node.put("status", "Public key not found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(node); + } + + var publicKey = publicKeyOpt.get(); + + // Verify the key belongs to the current user + if (!publicKey.getUser().getId().equals(user.getId())) { + node.put("status", "Unauthorized"); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(node); + } + + var hostGroup = hostGroupService.getHostGroup(hostGroupId); + publicKey.setHostGroup(hostGroup); + userPublicKeyService.addPublicKey(publicKey); + + node.put("status", "Public key successfully assigned to host group"); + return ResponseEntity.ok(node); + } catch (Exception e) { + log.error("Error assigning public key to host group", e); + node.put("status", "Error assigning public key to host group"); + return ResponseEntity.internalServerError().body(node); + } + } + + @DeleteMapping("/publickeys/{keyId}") + public ResponseEntity deletePublicKey( + HttpServletRequest request, HttpServletResponse response, + @PathVariable Long keyId) { + ObjectNode node = JsonUtil.MAPPER.createObjectNode(); + try { + var user = userService.getOperatingUser(request, response, null); + var publicKeyOpt = userPublicKeyService.getPublicKeyById(keyId); + + if (publicKeyOpt.isEmpty()) { + node.put("status", "Public key not found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(node); + } + + var publicKey = publicKeyOpt.get(); + + // Verify the key belongs to the current user + if (!publicKey.getUser().getId().equals(user.getId())) { + node.put("status", "Unauthorized"); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(node); + } + + userPublicKeyService.deletePublicKey(keyId); + node.put("status", "Public key successfully deleted"); + return ResponseEntity.ok(node); + } catch (Exception e) { + log.error("Error deleting public key", e); + node.put("status", "Error deleting public key"); + return ResponseEntity.internalServerError().body(node); + } + } + } diff --git a/api/src/main/java/io/sentrius/sso/controllers/view/UserController.java b/api/src/main/java/io/sentrius/sso/controllers/view/UserController.java index c2448c8f..3eb30a22 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/view/UserController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/view/UserController.java @@ -23,7 +23,9 @@ import io.sentrius.sso.core.model.users.UserConfig; import io.sentrius.sso.core.model.users.UserSettings; import io.sentrius.sso.core.services.ErrorOutputService; +import io.sentrius.sso.core.services.HostGroupService; import io.sentrius.sso.core.services.UserCustomizationService; +import io.sentrius.sso.core.services.UserPublicKeyService; import io.sentrius.sso.core.services.UserService; import io.sentrius.sso.core.services.WorkHoursService; import io.sentrius.sso.core.services.security.CryptoService; @@ -44,16 +46,21 @@ public class UserController extends BaseController { final UserCustomizationService userThemeService; + final UserPublicKeyService userPublicKeyService; + final HostGroupService hostGroupService; final WorkHoursService workHoursService; final CryptoService cryptoService; protected UserController( UserService userService, SystemOptions systemOptions, - ErrorOutputService errorOutputService, UserCustomizationService userThemeService, WorkHoursService workHoursService, - CryptoService cryptoService + ErrorOutputService errorOutputService, UserCustomizationService userThemeService, + UserPublicKeyService userPublicKeyService, HostGroupService hostGroupService, + WorkHoursService workHoursService, CryptoService cryptoService ) { super(userService, systemOptions, errorOutputService); this.userThemeService = userThemeService; + this.userPublicKeyService = userPublicKeyService; + this.hostGroupService = hostGroupService; this.workHoursService = workHoursService; this.cryptoService = cryptoService; } @@ -186,8 +193,16 @@ public String getUserSettings(Model model, HttpServletRequest request, HttpServl Map userWorkHours = workHoursList.stream() .collect(Collectors.toMap(WorkHours::getDayOfWeek, wh -> wh)); + // Get user's public keys + var publicKeys = userPublicKeyService.getPublicKeysForUser(user.getId()); + + // Get available host groups for this user + var hostGroups = hostGroupService.getHostGroupsForUser(user.getId()); + // Pass data to Thymeleaf model.addAttribute("userWorkHours", userWorkHours); + model.addAttribute("publicKeys", publicKeys); + model.addAttribute("hostGroups", hostGroups); model.addAttribute("daysOfWeek", List.of( new DayOfWeekDTO(0, "Sunday"), new DayOfWeekDTO(1, "Monday"), diff --git a/api/src/main/resources/templates/sso/users/user_settings.html b/api/src/main/resources/templates/sso/users/user_settings.html index 8d9bc4d8..d2264d8d 100755 --- a/api/src/main/resources/templates/sso/users/user_settings.html +++ b/api/src/main/resources/templates/sso/users/user_settings.html @@ -1,179 +1,556 @@ - - - - - - - - [[${systemOptions.systemLogoName}]] - User Settings - - - -
-
- - - - -
-
-
- - -
-

Your Settings

-
- - - - -
- - -
- -
- -
-
-

Work Hours

- - - - - - - - - - - - - - - - - -
DayEnableStart TimeEnd Time
- - - - - -
- -
- -
-
-
-
-
- - - - + + + + + + + [[${systemOptions.systemLogoName}]] - User Settings + + + +
+
+ + + + + +
+
+
+ + +
+

Your Settings

+ + +
+

User Preferences

+
+ + + + +
+ +
+ +
+
+ + +
+

SSH Public Key Management

+ + + + + +
+
+
+
+ Key Name + RSA + Enabled + Disabled +
+
+ +
+ + Fingerprint +
+ +
+ + Assigned to: Host Group +
+
+ + Not assigned to any host group +
+ +
+ + +
+
+
+ +
+ +

No SSH public keys configured. Add your first key to get started!

+
+
+ + +
+

Work Hours

+ + + + + + + + + + + + + + + + + +
DayEnableStart TimeEnd Time
+ + + + + +
+ + +
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/api/src/test/java/io/sentrius/sso/controllers/api/UserPublicKeyApiControllerTest.java b/api/src/test/java/io/sentrius/sso/controllers/api/UserPublicKeyApiControllerTest.java new file mode 100644 index 00000000..759986b8 --- /dev/null +++ b/api/src/test/java/io/sentrius/sso/controllers/api/UserPublicKeyApiControllerTest.java @@ -0,0 +1,236 @@ +package io.sentrius.sso.controllers.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.sentrius.sso.core.config.SystemOptions; +import io.sentrius.sso.core.dto.UserPublicKeyDTO; +import io.sentrius.sso.core.model.users.User; +import io.sentrius.sso.core.model.users.UserPublicKey; +import io.sentrius.sso.core.model.hostgroup.HostGroup; +import io.sentrius.sso.core.services.ErrorOutputService; +import io.sentrius.sso.core.services.UserPublicKeyService; +import io.sentrius.sso.core.services.UserService; +import io.sentrius.sso.core.services.HostGroupService; +import io.sentrius.sso.core.services.UserCustomizationService; +import io.sentrius.sso.core.services.SessionService; +import io.sentrius.sso.core.services.security.CryptoService; +import io.sentrius.sso.core.services.security.ZeroTrustRequestService; +import io.sentrius.sso.core.services.security.ZeroTrustAccessTokenService; +import io.sentrius.sso.core.services.agents.AgentService; +import io.sentrius.sso.core.services.agents.ZeroTrustClientService; +import io.sentrius.sso.core.utils.MessagingUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.Arrays; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UserPublicKeyApiControllerTest { + + @Mock + private UserPublicKeyService userPublicKeyService; + + @Mock + private UserService userService; + + @Mock + private HostGroupService hostGroupService; + + @Mock + private SystemOptions systemOptions; + + @Mock + private ErrorOutputService errorOutputService; + + @Mock + private CryptoService cryptoService; + + @Mock + private UserCustomizationService userCustomizationService; + + @Mock + private SessionService sessionService; + + @Mock + private ZeroTrustRequestService zeroTrustRequestService; + + @Mock + private ZeroTrustAccessTokenService zeroTrustAccessTokenService; + + @Mock + private AgentService agentService; + + @Mock + private ZeroTrustClientService zeroTrustClientService; + + @Mock + private MessagingUtil messagingUtil; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private UserApiController controller; + + @BeforeEach + void setUp() { + controller = new UserApiController( + userService, + systemOptions, + errorOutputService, + hostGroupService, + cryptoService, + messagingUtil, + userCustomizationService, + userPublicKeyService, + sessionService, + zeroTrustRequestService, + zeroTrustAccessTokenService, + agentService, + zeroTrustClientService + ); + } + + @Test + public void testGetUserPublicKeys() { + // Setup + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + + UserPublicKey key1 = new UserPublicKey(); + key1.setId(1L); + key1.setKeyName("Test Key 1"); + key1.setKeyType("RSA"); + key1.setPublicKey("ssh-rsa AAAAB3NzaC1yc2EAAA..."); + + UserPublicKey key2 = new UserPublicKey(); + key2.setId(2L); + key2.setKeyName("Test Key 2"); + key2.setKeyType("Ed25519"); + key2.setPublicKey("ssh-ed25519 AAAAC3NzaC1l..."); + + when(userService.getOperatingUser(any(), any(), any())).thenReturn(user); + when(userPublicKeyService.getPublicKeysForUser(1L)).thenReturn(Arrays.asList(key1, key2)); + + // Execute + ResponseEntity result = controller.getUserPublicKeys(request, response); + + // Verify + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertNotNull(result.getBody()); + assertEquals(2, ((java.util.List) result.getBody()).size()); + } + + @Test + public void testAddPublicKey() { + // Setup + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + + UserPublicKeyDTO newKey = UserPublicKeyDTO.builder().build(); + newKey.setKeyName("New Test Key"); + newKey.setKeyType("RSA"); + newKey.setPublicKey("ssh-rsa AAAAB3NzaC1yc2EAAA..."); + newKey.setIsEnabled(true); + + UserPublicKey savedKey = new UserPublicKey(); + savedKey.setId(3L); + savedKey.setKeyName("New Test Key"); + savedKey.setKeyType("RSA"); + savedKey.setPublicKey("ssh-rsa AAAAB3NzaC1yc2EAAA..."); + savedKey.setIsEnabled(true); + + when(userService.getOperatingUser(any(), any(), any())).thenReturn(user); + when(userPublicKeyService.addPublicKey(any(UserPublicKey.class))).thenReturn(savedKey); + + // Execute + ResponseEntity result = controller.addPublicKey(request, response, newKey); + + // Verify + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertNotNull(result.getBody()); + } + + @Test + public void testDeletePublicKey() { + // Setup + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + + UserPublicKey existingKey = new UserPublicKey(); + existingKey.setId(1L); + existingKey.setUser(user); + + when(userService.getOperatingUser(any(), any(), any())).thenReturn(user); + when(userPublicKeyService.getPublicKeyById(1L)).thenReturn(Optional.of(existingKey)); + + // Execute + ResponseEntity result = controller.deletePublicKey(request, response, 1L); + + // Verify + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertNotNull(result.getBody()); + } + + @Test + public void testAssignPublicKeyToHostGroup() { + // Setup + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + + UserPublicKey existingKey = new UserPublicKey(); + existingKey.setId(1L); + existingKey.setUser(user); + + HostGroup hostGroup = new HostGroup(); + hostGroup.setId(1L); + hostGroup.setName("Test Host Group"); + + when(userService.getOperatingUser(any(), any(), any())).thenReturn(user); + when(userPublicKeyService.getPublicKeyById(1L)).thenReturn(Optional.of(existingKey)); + when(hostGroupService.getHostGroup(1L)).thenReturn(hostGroup); + when(userPublicKeyService.addPublicKey(any(UserPublicKey.class))).thenReturn(existingKey); + + // Execute + ResponseEntity result = controller.assignPublicKeyToHostGroup(request, response, 1L, 1L); + + // Verify + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertNotNull(result.getBody()); + } + + @Test + public void testDeleteNonExistentPublicKey() { + // Setup + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + + when(userService.getOperatingUser(any(), any(), any())).thenReturn(user); + when(userPublicKeyService.getPublicKeyById(999L)).thenReturn(Optional.empty()); + + // Execute + ResponseEntity result = controller.deletePublicKey(request, response, 999L); + + // Verify + assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + assertNotNull(result.getBody()); + } +} \ No newline at end of file diff --git a/core/src/main/java/io/sentrius/sso/core/dto/UserPublicKeyDTO.java b/core/src/main/java/io/sentrius/sso/core/dto/UserPublicKeyDTO.java new file mode 100644 index 00000000..21d53c7f --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/dto/UserPublicKeyDTO.java @@ -0,0 +1,19 @@ +package io.sentrius.sso.core.dto; + +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class UserPublicKeyDTO { + private String keyName; + private String keyType; + private String publicKey; + private Boolean isEnabled; + @Builder.Default + private HostGroupDTO hostGroup = new HostGroupDTO(); +} \ No newline at end of file diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/HostGroupService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/HostGroupService.java index 1049e890..f45fb658 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/HostGroupService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/HostGroupService.java @@ -195,6 +195,11 @@ public void removeHostSystemFromHostGroup(Long hostGroupId, Long hostSystemId) { public List getAllHostGroups() { return hostGroupRepository.findAll(); } + + @Transactional(readOnly = true) + public List getHostGroupsForUser(Long userId) { + return userRepository.findHostGroupsByUserId(userId).stream().toList(); + } public List getHostGroupsByName(String name) { return hostGroupRepository.findByName(name);