Skip to content

Commit a72186d

Browse files
committed
Update
1 parent fdfce86 commit a72186d

File tree

18 files changed

+639
-126
lines changed

18 files changed

+639
-126
lines changed

.gcp.env

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SENTRIUS_VERSION=1.0.37
1+
SENTRIUS_VERSION=1.0.39
22
SENTRIUS_SSH_VERSION=1.0.4
3-
SENTRIUS_KEYCLOAK_VERSION=1.0.6
4-
SENTRIUS_AGENT_VERSION=1.0.16
3+
SENTRIUS_KEYCLOAK_VERSION=1.0.7
4+
SENTRIUS_AGENT_VERSION=1.0.17

api/src/main/java/io/sentrius/sso/config/SecurityConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class SecurityConfig {
3737

3838
private final CustomUserDetailsService userDetailsService;
3939
private final CustomAuthenticationSuccessHandler successHandler;
40+
private final KeycloakAuthSuccessHandler keycloakAuthSuccessHandler;
4041
final UserService userService;
4142

4243
@Value("${https.required:false}") // Default is false
@@ -55,6 +56,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
5556
)
5657
.oauth2Login(oauth2 -> oauth2
5758
.loginPage("/oauth2/authorization/keycloak")
59+
.successHandler(keycloakAuthSuccessHandler)
5860
)
5961
.cors(Customizer.withDefaults());
6062

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentrius.sso.controllers.view;
22

33
import java.lang.reflect.Field;
4+
import java.security.GeneralSecurityException;
45
import java.util.ArrayList;
56
import java.util.HashSet;
67
import java.util.List;
@@ -15,12 +16,15 @@
1516
import io.sentrius.sso.core.model.WorkHours;
1617
import io.sentrius.sso.core.model.dto.DayOfWeekDTO;
1718
import io.sentrius.sso.core.model.dto.SystemOption;
19+
import io.sentrius.sso.core.model.dto.UserDTO;
1820
import io.sentrius.sso.core.model.dto.UserTypeDTO;
1921
import io.sentrius.sso.core.model.security.UserType;
2022
import io.sentrius.sso.core.model.security.enums.UserAccessEnum;
2123
import io.sentrius.sso.core.model.users.User;
2224
import io.sentrius.sso.core.model.users.UserConfig;
2325
import io.sentrius.sso.core.model.users.UserSettings;
26+
import io.sentrius.sso.core.repository.UserTypeRepository;
27+
import io.sentrius.sso.core.security.service.CryptoService;
2428
import io.sentrius.sso.core.services.ErrorOutputService;
2529
import io.sentrius.sso.core.services.UserCustomizationService;
2630
import io.sentrius.sso.core.services.UserService;
@@ -34,7 +38,9 @@
3438
import org.springframework.ui.Model;
3539
import org.springframework.web.bind.annotation.GetMapping;
3640
import org.springframework.web.bind.annotation.ModelAttribute;
41+
import org.springframework.web.bind.annotation.PathVariable;
3742
import org.springframework.web.bind.annotation.RequestMapping;
43+
import org.springframework.web.bind.annotation.RequestParam;
3844

3945
@Slf4j
4046
@Controller
@@ -43,12 +49,16 @@ public class UserController extends BaseController {
4349

4450
final UserCustomizationService userThemeService;
4551
final WorkHoursService workHoursService;
52+
final CryptoService cryptoService;
4653

4754
protected UserController(UserService userService, SystemOptions systemOptions,
48-
ErrorOutputService errorOutputService, UserCustomizationService userThemeService, WorkHoursService workHoursService) {
55+
ErrorOutputService errorOutputService, UserCustomizationService userThemeService, WorkHoursService workHoursService,
56+
CryptoService cryptoService
57+
) {
4958
super(userService, systemOptions, errorOutputService);
5059
this.userThemeService = userThemeService;
5160
this.workHoursService = workHoursService;
61+
this.cryptoService = cryptoService;
5262
}
5363

5464
@ModelAttribute("userSettings")
@@ -155,6 +165,21 @@ public String listUsers(Model model) {
155165
return "sso/users/list_users";
156166
}
157167

168+
169+
@GetMapping("/edit")
170+
@LimitAccess(userAccess = {UserAccessEnum.CAN_EDIT_USERS})
171+
public String editUser(Model model, HttpServletRequest request, HttpServletResponse response,
172+
@RequestParam("userId") String userId) throws GeneralSecurityException {
173+
model.addAttribute("globalAccessSet", UserType.createSuperUser().getAccessSet());
174+
Long id = Long.parseLong(cryptoService.decrypt(userId));
175+
User user = userService.getUserById(id);
176+
UserDTO userDTO = new UserDTO(user);
177+
var types = userService.getUserTypeList();
178+
model.addAttribute("userTypes",types);
179+
model.addAttribute("user", userDTO);
180+
return "sso/users/edit_user";
181+
}
182+
158183
@GetMapping("/settings")
159184
@LimitAccess(userAccess = {UserAccessEnum.CAN_VIEW_USERS})
160185
public String getUserSettings(Model model, HttpServletRequest request, HttpServletResponse response) {

api/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ server.error.whitelabel.enabled=false
6464

6565

6666
keycloak.realm=sentrius
67+
keycloak.base-url=http://192.168.1.162:8180
6768

6869
spring.security.oauth2.client.registration.keycloak.client-id=sentrius-api
6970
spring.security.oauth2.client.registration.keycloak.client-secret=nGkEukexSWTvDzYjSkDmeUlM0FJ5Jhh0
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!DOCTYPE html>
2+
<html xmlns:th="http://www.thymeleaf.org" class="dark" data-bs-theme="dark">
3+
<head>
4+
<meta th:replace="~{fragments/header}">
5+
<title>Edit User</title>
6+
</head>
7+
8+
<body>
9+
<div class="container-fluid">
10+
<div class="row flex-nowrap">
11+
<div th:replace="~{fragments/sidebar}" class="col-auto sidebar" style="width: 180px;"></div>
12+
<div class="col py-4">
13+
<div class="main-content">
14+
<h3>Edit User</h3>
15+
<form th:action="@{/api/v1/users/update}" method="post">
16+
<input type="hidden" name="userId" th:value="${user.userId}"/>
17+
18+
<table class="table table-striped table-dark">
19+
<thead>
20+
<tr>
21+
<th>Setting</th>
22+
<th>Value</th>
23+
</tr>
24+
</thead>
25+
<tbody>
26+
<tr>
27+
<td><label for="name">Name</label></td>
28+
<td><input type="text" id="name" name="name" class="form-control" th:value="${user.name}"
29+
readonly></td>
30+
</tr>
31+
<tr>
32+
<td><label for="username">Username</label></td>
33+
<td><input type="text" id="username" name="username" class="form-control" th:value="${user.username}" readonly></td>
34+
</tr>
35+
<tr>
36+
<td><label for="email">Email Address</label></td>
37+
<td><input type="email" id="email" name="emailAddress" class="form-control" th:value="${user.emailAddress}" readonly></td>
38+
</tr>
39+
<tr>
40+
<td><label for="userType">User Type</label></td>
41+
<td>
42+
<select id="userType" name="userType" class="form-control">
43+
<option th:each="type : ${userTypes}" th:value="${type.id}"
44+
th:text="${type.userTypeName}"
45+
th:selected="${type.id == user.authorizationType.id}"></option>
46+
</select>
47+
</td>
48+
</tr>
49+
<tr>
50+
<td><label for="status">Status</label></td>
51+
<td>
52+
<select id="status" name="status" class="form-control">
53+
<option value="ACTIVE" th:selected="${user.status == 'ACTIVE'}">Active</option>
54+
<option value="LOCKED" th:selected="${user.status == 'LOCKED'}">Locked</option>
55+
</select>
56+
</td>
57+
</tr>
58+
</tbody>
59+
</table>
60+
61+
<button type="submit" class="btn btn-success">Save Changes</button>
62+
<a href="/sso/v1/users/list" class="btn btn-secondary">Cancel</a>
63+
</form>
64+
</div>
65+
</div>
66+
</div>
67+
</div>
68+
69+
</body>
70+
</html>

api/src/main/resources/templates/sso/users/list_users.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
var actions = "";
4848
if (cmu){
4949
actions = `<a href="/sso/v1/users/lock/${myId}" class="btn btn-primary" >Lock</a> &nbsp; | &nbsp;`;
50+
actions += `<a href="/sso/v1/users/edit?userId=${myId}" class="btn btn-primary" >Edit</a> &nbsp; | &nbsp;`;
5051
actions += `<a href="/api/v1/users/delete?userId=${myId}" class="btn btn-primary" >Delete</a>`;
5152
}
5253
return actions;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.sentrius.sso.config;
2+
3+
import com.fasterxml.jackson.databind.node.ObjectNode;
4+
import io.sentrius.sso.core.model.security.UserType;
5+
import io.sentrius.sso.core.model.users.User;
6+
import io.sentrius.sso.core.repository.UserRepository;
7+
import io.sentrius.sso.core.repository.UserTypeRepository;
8+
import io.sentrius.sso.core.services.JwtUtil;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.keycloak.admin.client.Keycloak;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
13+
import org.springframework.stereotype.Component;
14+
import jakarta.servlet.ServletException;
15+
import jakarta.servlet.http.HttpServletRequest;
16+
import jakarta.servlet.http.HttpServletResponse;
17+
import java.io.IOException;
18+
import java.util.List;
19+
import java.util.Optional;
20+
21+
@Component
22+
@Slf4j
23+
public class KeycloakAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
24+
25+
private final Keycloak keycloak;
26+
private final UserRepository userRepository;
27+
private final UserTypeRepository userTypeRepository;
28+
29+
public KeycloakAuthSuccessHandler(Keycloak keycloak, UserRepository userRepository,
30+
UserTypeRepository userTypeRepository
31+
) {
32+
this.keycloak = keycloak;
33+
this.userRepository = userRepository;
34+
this.userTypeRepository = userTypeRepository;
35+
}
36+
37+
@Override
38+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
39+
Authentication authentication) throws IOException, ServletException {
40+
var jwt = JwtUtil.getJWT();
41+
Optional<String> userIdStr = JwtUtil.getUserId(jwt);
42+
Optional<String> usernameStr = JwtUtil.getUsername(jwt);
43+
44+
45+
if (usernameStr.isPresent()) {
46+
log.info(" ** Syncing user attributes for user: {} ", usernameStr.get());
47+
syncUserAttributes(jwt, usernameStr.get());
48+
}
49+
50+
super.onAuthenticationSuccess(request, response, authentication);
51+
}
52+
53+
private void syncUserAttributes(ObjectNode jwtNode, String userId) {
54+
// Get Keycloak attributes
55+
56+
// Get user from DB
57+
Optional<User> user = userRepository.findByUsername(userId);
58+
var userTypeFromJwtNode = JwtUtil.getUserTypeName(jwtNode);
59+
if (user.isPresent() && userTypeFromJwtNode.isPresent()) {
60+
// Example: Syncing "userType" attribute
61+
String keycloakUserType = userTypeFromJwtNode.get();
62+
UserType dbUserType = user.get().getAuthorizationType();
63+
log.info(" ** Syncing user attributes for user: {} and {}", userId, keycloakUserType);
64+
if (keycloakUserType != null && !keycloakUserType.isEmpty()) {
65+
String keycloakValue = keycloakUserType;
66+
if (!keycloakValue.equals(dbUserType.getUserTypeName())) {
67+
Optional<UserType> newType = userTypeRepository.findByUserTypeName(keycloakValue);
68+
if (newType.isPresent()) {
69+
// Update database
70+
user.get().setAuthorizationType(newType.get());
71+
userRepository.save(user.get());
72+
}
73+
}
74+
}
75+
} else {
76+
log.info(" userTypeFromJwtNode not defined {} ", userTypeFromJwtNode);
77+
}
78+
}
79+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.sentrius.sso.config;
2+
import lombok.extern.slf4j.Slf4j;
3+
import org.keycloak.OAuth2Constants;
4+
import org.keycloak.admin.client.Keycloak;
5+
import org.keycloak.admin.client.KeycloakBuilder;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
10+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
11+
12+
@Configuration
13+
@Slf4j
14+
public class KeycloakConfig {
15+
16+
@Value("${keycloak.base-url}")
17+
private String serverUrl;
18+
19+
@Value("${keycloak.realm}")
20+
private String realm;
21+
22+
private final ClientRegistrationRepository clientRegistrationRepository;
23+
24+
public KeycloakConfig(ClientRegistrationRepository clientRegistrationRepository) {
25+
this.clientRegistrationRepository = clientRegistrationRepository;
26+
}
27+
28+
@Bean
29+
public Keycloak keycloak() {
30+
// Retrieve the Keycloak client registration at runtime
31+
ClientRegistration keycloakClientRegistration = clientRegistrationRepository.findByRegistrationId("keycloak");
32+
33+
if (keycloakClientRegistration == null) {
34+
throw new IllegalStateException("Keycloak client registration not found. Check your OAuth2 client configuration.");
35+
}
36+
37+
return KeycloakBuilder.builder()
38+
.serverUrl(serverUrl)
39+
.realm(realm)
40+
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
41+
.clientId(keycloakClientRegistration.getClientId())
42+
.clientSecret(keycloakClientRegistration.getClientSecret())
43+
.build();
44+
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.sentrius.sso.config.security;
2+
3+
import io.sentrius.sso.core.services.KeycloakService;
4+
import io.sentrius.sso.core.services.UserAttributeSyncService;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
7+
import org.springframework.security.core.context.SecurityContextHolder;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.stereotype.Component;
10+
import jakarta.servlet.Filter;
11+
import jakarta.servlet.FilterChain;
12+
import jakarta.servlet.ServletException;
13+
import jakarta.servlet.ServletRequest;
14+
import jakarta.servlet.ServletResponse;
15+
import java.io.IOException;
16+
import java.util.Map;
17+
18+
@Component
19+
@Slf4j
20+
public class KeycloakUserSyncFilter implements Filter {
21+
22+
private final KeycloakService keycloakService;
23+
private final UserAttributeSyncService syncService;
24+
25+
public KeycloakUserSyncFilter(KeycloakService keycloakService, UserAttributeSyncService syncService) {
26+
this.keycloakService = keycloakService;
27+
this.syncService = syncService;
28+
}
29+
30+
@Override
31+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
32+
throws IOException, ServletException {
33+
34+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
35+
36+
if (authentication instanceof KeycloakAuthenticationToken) {
37+
KeycloakAuthenticationToken keycloakAuth = (KeycloakAuthenticationToken) authentication;
38+
String userId = keycloakAuth.getAccount().getKeycloakSecurityContext().getToken().getSubject();
39+
log.info("Syncing user attributes for user: {}", userId);
40+
// Sync user attributes with the database
41+
syncService.syncUserAttribute(userId);
42+
}
43+
44+
chain.doFilter(request, response);
45+
}
46+
}

core/src/main/java/io/sentrius/sso/core/config/SystemOptions.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,13 @@ public boolean setValue(String fieldName, Object fieldValue, boolean save){
186186
Field[] fields = this.getClass().getDeclaredFields();
187187
for (var field : fields) {
188188
if (field.getName().equalsIgnoreCase(fieldName)) {
189-
log.info("Setting field {} to {}", fieldName, fieldValue);
189+
log.debug("Setting field {} to {}", fieldName, fieldValue);
190190
try {
191191
field.set(this, fieldValue);
192192

193193
// Update the AppConfig with the new field value
194194
dynamicPropertiesService.updateProperty(fieldName, fieldValue.toString());
195-
log.info("Set field {} to {}", fieldName, fieldValue);
195+
log.debug("Set field {} to {}", fieldName, fieldValue);
196196
return true;
197197
} catch (IllegalAccessException e) {
198198
log.error("Failed to update field {}", fieldName);
@@ -236,7 +236,7 @@ public Map<String, SystemOption> getOptions() throws IllegalAccessException {
236236
String fieldName = field.getName();
237237
Object fieldValue = field.get(this);
238238

239-
log.info("Field: {} Value: {}", fieldName, fieldValue);
239+
log.debug("Field: {} Value: {}", fieldName, fieldValue);
240240

241241
// Create a SystemOption object with the field details
242242
var sysOpt = SystemOption.builder()

0 commit comments

Comments
 (0)