Skip to content

Commit c665b4e

Browse files
authored
Fix Jakarta Issue (#248)
* Enable JSR-250 annotations in SecurityConfig JSR-250 annotations (like @RolesAllowed) are now enabled in the SecurityConfig by setting `jsr250Enabled` to true. This enhances the flexibility of role-based access control within the application. * Refactor role-based authority handling in CustomUserDetails Updated the constructor to properly map user roles and dynamically add top admin privileges when applicable. Introduced new imports for improved role management and logging for debugging purposes. These changes enhance clarity and ensure more robust role-based access control. * Add UUID initialization for Role and Privilege Prevents test failing when using logging statements. The toString requires a uuid. * Prevent security context setup after error responses Added early return statements in JWTFilter to ensure the security context is not set up after sending error responses for unauthorized or forbidden access. This change avoids potential security issues and ensures proper request handling. * Force eager loading of Privilege.accessRules to avoid LazyInitializationException After switching granted authorities from privileges to roles, privilege.accessRules are no longer loaded into the Hibernate session when building CustomUserDetails. This change forces the fetch type to EAGER so that accessRules are preloaded with each Privilege entity and prevents LazyInitializationException. This will likely have negative consequences in our BioDataCatalyst environment and require a technical debt ticket to revert to lazy loading. * Replace @secured with @RolesAllowed annotations for authorization * Refactor role handling and remove default "ROLE_" prefix. Updated `CustomUserDetails` to use privileges instead of roles and removed redundant logic for adding admin roles. Added `GrantedAuthorityDefaults` bean in `SecurityConfig` to eliminate the default "ROLE_" prefix for better alignment with custom authorities. * Refactor role naming to use centralized AuthNaming constants Removed the SecurityRoles enum and replaced its usage with centralized constants from AuthNaming.AuthRoleNaming. * Revert EAGER fetch * Remove SecurityRoles import across test files File has been deleted * Remove "ROLE_" prefix from role names and update usage Changed role constants to remove the "ROLE_" prefix and updated the role naming convention. Added a GrantedAuthorityDefaults bean in SecurityConfig to ensure role-based security annotations match the updated role names. * Update AuthRoleNaming documentation and remove unused constant Refined the JavaDoc for AuthRoleNaming to clarify its purpose within @RolesAllowed annotations. Removed the unused constant `PIC_SURE_TOP_ADMIN`. * Simplify role deletion logic Removed security context checks in `RoleService` to streamline role deletion. The removeById RoleController endpoint already verifies the user is a SUPER_ADMIN. The privilege is only assigned to PIC-SURE Top Admins. * Refactor RoleService tests and remove unnecessary methods Consolidated RoleService test cases by removing duplicate methods and irrelevant tests. * Cleaned up JWTFilter comments
1 parent a9e551e commit c665b4e

File tree

14 files changed

+55
-418
lines changed

14 files changed

+55
-418
lines changed

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/config/SecurityConfig.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1313
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1414
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
15+
import org.springframework.security.config.core.GrantedAuthorityDefaults;
1516
import org.springframework.security.web.SecurityFilterChain;
1617
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
1718

1819
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
1920

2021
@Configuration
21-
@EnableMethodSecurity(prePostEnabled = false)
22+
@EnableMethodSecurity(prePostEnabled = false, jsr250Enabled = true)
2223
@EnableWebSecurity
2324
public class SecurityConfig {
2425

@@ -75,4 +76,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
7576
return http.build();
7677
}
7778

79+
/**
80+
* Remove the default "ROLE_" prefix so that hasRole("ADMIN")
81+
* and @RolesAllowed("ADMIN") match a GrantedAuthority("ADMIN").
82+
*/
83+
@Bean
84+
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
85+
return new GrantedAuthorityDefaults("");
86+
}
87+
7888
}

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/enums/SecurityRoles.java

Lines changed: 0 additions & 20 deletions
This file was deleted.

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/filter/JWTFilter.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ public JWTFilter(TOSService tosService,
7070
}
7171

7272
/**
73-
* Filter implementation that performs authentication and authorization checks based on the provided request headers.
73+
* Filter that performs authentication and authorization checks based on the provided request headers.
7474
* The filter checks for the presence of the "Authorization" header and validates the token.
75-
* It sets the appropriate security context based on the type of token (long term token or PSAMA application token) and
75+
* It sets the appropriate security context based on the type of token (long-term token or PSAMA application token) and
7676
* performs the necessary checks to ensure that the user or application is authorized to access the requested resource.
7777
* This filter is called by the configured security filter chain in the SecurityConfig class.
7878
*
@@ -91,16 +91,12 @@ protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServlet
9191
// without any authentication or authorization checks
9292
filterChain.doFilter(request, response);
9393
} else {
94-
// If the header is present, we need to check the token
9594
String token = authorizationHeader.substring(6).trim();
96-
97-
// Parse the token
9895
Jws<Claims> jws = this.jwtUtil.parseToken(token);
9996
String userId = jws.getPayload().get(this.userClaimId, String.class);
10097

10198
if (userId.startsWith(AuthNaming.LONG_TERM_TOKEN_PREFIX)) {
102-
// For profile information, we do indeed allow long term token
103-
// to be a valid token.
99+
// For profile information, we do indeed allow a long-term token to be a valid token.
104100
if (request.getRequestURI().startsWith("/auth/user/me")) {
105101
String realClaimsSubject = jws.getPayload().getSubject().substring(AuthNaming.LONG_TERM_TOKEN_PREFIX.length() + 1);
106102

@@ -112,7 +108,7 @@ protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServlet
112108
} else if (userId.startsWith(AuthNaming.PSAMA_APPLICATION_TOKEN_PREFIX)) {
113109
logger.info("User Authentication Starts with {}", AuthNaming.PSAMA_APPLICATION_TOKEN_PREFIX);
114110

115-
// Check if user is attempting to access the correct introspect endpoint. If not reject the request
111+
// Check if the user is attempting to access the correct introspection endpoint. If not reject the request,
116112
// log an error indicating the user's token may be being used by a malicious actor.
117113
if (!request.getRequestURI().endsWith("token/inspect") && !request.getRequestURI().endsWith("open/validate")) {
118114
logger.error("{} attempted to perform request {} token may be compromised.", userId, request.getRequestURI());
@@ -122,7 +118,7 @@ protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServlet
122118
String applicationId = userId.split("\\|")[1];
123119
logger.info("Application ID: {}", applicationId);
124120

125-
// Authenticate as Application
121+
// Create custom application details. Will be used to set the correct security context.
126122
CustomApplicationDetails customApplicationDetails = (CustomApplicationDetails) this.customUserDetailService.loadUserByUsername("application:" + applicationId);
127123
if (customApplicationDetails.getApplication() == null) {
128124
logger.error("Cannot find an application by userId: {}", applicationId);
@@ -160,9 +156,9 @@ private void setSecurityContextForApplication(HttpServletRequest request, Custom
160156

161157
/**
162158
* Sets the security context for the given user.
163-
* This method is responsible for validating the user claims, checking if the user is active,
164-
* ensuring that the user has accepted the terms of service (if enabled), validating user roles and privileges,
165-
* and setting the user object as an attribute in the request.
159+
* This method is responsible for validating the user claims. It checks if the user is active,
160+
* ensuring the user has accepted the terms of service (if enabled), validating user roles and privileges,
161+
* and setting the user object as an attribute in the security context.
166162
*
167163
* @param request the HttpServletRequest object
168164
* @param response the HttpServletResponse object
@@ -192,6 +188,8 @@ private void setSecurityContextForUser(HttpServletRequest request, HttpServletRe
192188
//If user has not accepted terms of service and is attempted to get information other than the terms of service, don't authenticate
193189
try {
194190
response.sendError(HttpServletResponse.SC_FORBIDDEN, "User must accept terms of service");
191+
// Return early to prevent setting up security context
192+
return;
195193
} catch (IOException e) {
196194
logger.error("Failed to send response.", e);
197195
}
@@ -202,6 +200,8 @@ private void setSecurityContextForUser(HttpServletRequest request, HttpServletRe
202200
logger.error("User doesn't have any roles or privileges.");
203201
try {
204202
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User doesn't have any roles or privileges.");
203+
// Return early to prevent setting up security context
204+
return;
205205
} catch (IOException e) {
206206
logger.error("Failed to send response.", e);
207207
}

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/model/CustomUserDetails.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import edu.harvard.hms.dbmi.avillach.auth.entity.User;
44
import org.springframework.security.core.GrantedAuthority;
5+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
56
import org.springframework.security.core.userdetails.UserDetails;
67

78
import java.util.ArrayList;
@@ -15,9 +16,9 @@ public class CustomUserDetails implements UserDetails {
1516
public CustomUserDetails(User user) {
1617
this.user = user;
1718
if (user != null && user.getRoles() != null) {
18-
this.authorities = user.getTotalPrivilege().stream()
19-
.map(privilege -> (GrantedAuthority) privilege::getName)
20-
.toList();
19+
this.authorities = new ArrayList<>(user.getTotalPrivilege().stream()
20+
.map(privilege-> new SimpleGrantedAuthority(privilege.getName()))
21+
.toList());
2122
} else {
2223
this.authorities = new ArrayList<>();
2324
}

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/rest/AccessRuleController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.springframework.beans.factory.annotation.Autowired;
1010
import org.springframework.http.MediaType;
1111
import org.springframework.http.ResponseEntity;
12-
import org.springframework.security.access.annotation.Secured;
1312
import org.springframework.stereotype.Controller;
1413
import org.springframework.web.bind.annotation.*;
1514

@@ -39,7 +38,7 @@ public AccessRuleController(AccessRuleService accessRuleService) {
3938
}
4039

4140
@Operation(description = "GET information of one AccessRule with the UUID, requires ADMIN or SUPER_ADMIN role")
42-
@Secured(value = {ADMIN, SUPER_ADMIN})
41+
@RolesAllowed({ADMIN, SUPER_ADMIN})
4342
@GetMapping(value = "/{accessRuleId}")
4443
public ResponseEntity<?> getAccessRuleById(
4544
@Parameter(description = "The UUID of the accessRule to fetch information about")
@@ -54,7 +53,7 @@ public ResponseEntity<?> getAccessRuleById(
5453
}
5554

5655
@Operation(description = "GET a list of existing AccessRules, requires ADMIN or SUPER_ADMIN role")
57-
@Secured({ADMIN, SUPER_ADMIN})
56+
@RolesAllowed({ADMIN, SUPER_ADMIN})
5857
@GetMapping("")
5958
public ResponseEntity<List<AccessRule>> getAccessRuleAll() {
6059
List<AccessRule> allAccessRules = this.accessRuleService.getAllAccessRules();

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/rest/ConnectionWebController.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import io.swagger.v3.oas.annotations.Operation;
77
import io.swagger.v3.oas.annotations.Parameter;
88
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import jakarta.annotation.security.RolesAllowed;
910
import org.springframework.beans.factory.annotation.Autowired;
1011
import org.springframework.http.ResponseEntity;
11-
import org.springframework.security.access.annotation.Secured;
1212
import org.springframework.stereotype.Controller;
1313
import org.springframework.web.bind.annotation.*;
1414

@@ -36,7 +36,7 @@ public ConnectionWebController(ConnectionWebService connectionWebSerivce) {
3636

3737
@Operation(description = "GET information of one Connection with the UUID, requires ADMIN or SUPER_ADMIN role")
3838
@GetMapping(path = "/{connectionId}", produces = "application/json")
39-
@Secured({SUPER_ADMIN, ADMIN})
39+
@RolesAllowed({SUPER_ADMIN, ADMIN})
4040
public ResponseEntity<?> getConnectionById(
4141
@Parameter(required = true, description = "The UUID of the Connection to fetch information about")
4242
@PathVariable("connectionId") String connectionId) {
@@ -50,14 +50,14 @@ public ResponseEntity<?> getConnectionById(
5050

5151
@Operation(description = "GET a list of existing Connection, requires SUPER_ADMIN or ADMIN role")
5252
@GetMapping
53-
@Secured({SUPER_ADMIN, ADMIN})
53+
@RolesAllowed({SUPER_ADMIN, ADMIN})
5454
public ResponseEntity<List<Connection>> getAllConnections() {
5555
List<Connection> allConnections = connectionWebService.getAllConnections();
5656
return ResponseEntity.ok(allConnections);
5757
}
5858

5959
@Operation(description = "POST a list of Connections, requires SUPER_ADMIN role")
60-
@Secured({SUPER_ADMIN})
60+
@RolesAllowed({SUPER_ADMIN})
6161
@PostMapping(produces = "application/json", consumes = "application/json")
6262
public ResponseEntity<?> addConnection(
6363
@Parameter(required = true, description = "A list of Connections in JSON format")
@@ -72,7 +72,7 @@ public ResponseEntity<?> addConnection(
7272
}
7373

7474
@Operation(description = "Update a list of Connections, will only update the fields listed, requires SUPER_ADMIN role")
75-
@Secured({SUPER_ADMIN})
75+
@RolesAllowed({SUPER_ADMIN})
7676
@PutMapping(produces = "application/json", consumes = "application/json")
7777
public ResponseEntity<List<Connection>> updateConnection(
7878
@Parameter(required = true, description = "A list of Connection with fields to be updated in JSON format")
@@ -82,7 +82,7 @@ public ResponseEntity<List<Connection>> updateConnection(
8282
}
8383

8484
@Operation(description = "DELETE an Connection by Id only if the Connection is not associated by others, requires SUPER_ADMIN role")
85-
@Secured({SUPER_ADMIN})
85+
@RolesAllowed({SUPER_ADMIN})
8686
@DeleteMapping(path = "/{connectionId}", produces = "application/json")
8787
public ResponseEntity<List<Connection>> removeById(
8888
@Parameter(required = true, description = "A valid connection Id")

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/service/impl/RoleService.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import edu.harvard.hms.dbmi.avillach.auth.entity.Privilege;
44
import edu.harvard.hms.dbmi.avillach.auth.entity.Role;
55
import edu.harvard.hms.dbmi.avillach.auth.entity.User;
6-
import edu.harvard.hms.dbmi.avillach.auth.enums.SecurityRoles;
7-
import edu.harvard.hms.dbmi.avillach.auth.model.CustomUserDetails;
86
import edu.harvard.hms.dbmi.avillach.auth.model.fenceMapping.StudyMetaData;
97
import edu.harvard.hms.dbmi.avillach.auth.model.ras.RasDbgapPermission;
108
import edu.harvard.hms.dbmi.avillach.auth.repository.RoleRepository;
@@ -15,8 +13,6 @@
1513
import org.springframework.beans.factory.annotation.Autowired;
1614
import org.springframework.context.event.ContextRefreshedEvent;
1715
import org.springframework.context.event.EventListener;
18-
import org.springframework.security.core.context.SecurityContext;
19-
import org.springframework.security.core.context.SecurityContextHolder;
2016
import org.springframework.stereotype.Service;
2117
import org.springframework.transaction.annotation.Transactional;
2218

@@ -139,20 +135,10 @@ public List<Role> updateRoles(List<Role> roles) {
139135
@Transactional
140136
public Optional<List<Role>> removeRoleById(String roleId) {
141137
Optional<Role> optionalRole = roleRepository.findById(UUID.fromString(roleId));
142-
143138
if (optionalRole.isEmpty()) {
144139
return Optional.empty();
145140
}
146141

147-
SecurityContext context = SecurityContextHolder.getContext();
148-
CustomUserDetails current_user = (CustomUserDetails) context.getAuthentication().getPrincipal();
149-
Set<String> roleNames = current_user.getUser().getRoles().stream().map(Role::getName).collect(Collectors.toSet());
150-
151-
if (!roleNames.contains(SecurityRoles.PIC_SURE_TOP_ADMIN.getRole())){
152-
logger.info("User doesn't have PIC-SURE Top Admin role, can't remove any role");
153-
return Optional.empty();
154-
}
155-
156142
roleRepository.deleteById(optionalRole.get().getUuid());
157143
return Optional.of(roleRepository.findAll());
158144
}

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/utils/AuthNaming.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ public class AuthNaming {
1313
public static final String PSAMA_APPLICATION_TOKEN_PREFIX = "PSAMA_APPLICATION";
1414

1515
/**
16-
* <p>Contains all preset PSAMA role naming conventions.</p>
17-
* <p>Note: Only users with super admin access can edit these role names.</p>
16+
* <p>Constants used to in @RolesAllowed() annotations.</p>
1817
*/
1918
public static class AuthRoleNaming {
2019
public static final String ADMIN = "ADMIN";

0 commit comments

Comments
 (0)