Skip to content

Commit 24acd51

Browse files
authored
[ALS-6921] Implement Open Access in PSAMA (#211)
- The logout handler now returns a `200 OK` status upon success. - Added a new `OpenAccessController` that includes a single `open/validate` endpoint. - `JWTFilter` now permits the `open/validate` endpoint when using an application token. - Added a new `record` named `EvaluateAccessRuleResult`, which is returned from `passesAccessRuleEvaluation` after evaluating the access rules against the query. - Introduced a new method named `openAccessRequestIsValid`, which is used to validate open access requests. - Updated the callback URLs to use the new UI path `login/loading/`. - Removed `OpenAuthenticationService` and `OpenAuthenticationServiceTest` as they are no longer needed. - A new open access query template is returned for request that do not have an associated `uuid` or `user`.
1 parent 21e6b28 commit 24acd51

19 files changed

+194
-254
lines changed

pic-sure-auth-services/pom.xml

+12-1
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,19 @@
198198
<artifactId>jackson-annotations</artifactId>
199199
<version>2.17.0</version>
200200
</dependency>
201-
202201
</dependencies>
202+
<profiles>
203+
<profile>
204+
<id>dev</id>
205+
<dependencies>
206+
<dependency>
207+
<groupId>org.springframework.boot</groupId>
208+
<artifactId>spring-boot-devtools</artifactId>
209+
<optional>true</optional>
210+
</dependency>
211+
</dependencies>
212+
</profile>
213+
</profiles>
203214
<build>
204215
<finalName>${project.artifactId}</finalName>
205216
<plugins>

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
5757
"/authentication",
5858
"/authentication/**",
5959
"/swagger.yaml",
60-
"/swagger.json"
60+
"/swagger.json",
61+
"/user/me/queryTemplate",
62+
"/user/me/queryTemplate/**",
63+
"/open/validate"
6164
).permitAll()
6265
.anyRequest().authenticated()
6366
)
6467
.httpBasic(AbstractHttpConfigurer::disable)
6568
.formLogin(AbstractHttpConfigurer::disable)
6669
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
67-
.logout((logout) -> logout.logoutUrl("/logout").addLogoutHandler(customLogoutHandler()));
68-
70+
.logout((logout) -> logout.logoutUrl("/logout").addLogoutHandler(customLogoutHandler()).logoutSuccessHandler((request, response, authentication) -> {
71+
// We don't want to redirect to a login page, we just want to return a 200
72+
// We leave it to the client to handle the redirect
73+
response.setStatus(200);
74+
}));
6975

7076
return http.build();
7177
}

pic-sure-auth-services/src/main/java/edu/harvard/hms/dbmi/avillach/auth/entity/AccessRule.java

-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
package edu.harvard.hms.dbmi.avillach.auth.entity;
22

3-
import com.fasterxml.jackson.annotation.JsonIgnore;
4-
import com.fasterxml.jackson.annotation.JsonProperty;
5-
63
import jakarta.persistence.Column;
74
import jakarta.persistence.Entity;
85
import jakarta.persistence.FetchType;
96
import jakarta.persistence.JoinColumn;
107
import jakarta.persistence.JoinTable;
118
import jakarta.persistence.ManyToMany;
12-
import jakarta.persistence.ManyToOne;
13-
import jakarta.persistence.OneToMany;
149
import jakarta.persistence.Transient;
1510

1611
import java.lang.reflect.Field;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServlet
114114

115115
// Check if user is attempting to access the correct introspect endpoint. If not reject the request
116116
// log an error indicating the user's token may be being used by a malicious actor.
117-
if (!request.getRequestURI().endsWith("token/inspect")) {
117+
if (!request.getRequestURI().endsWith("token/inspect") && !request.getRequestURI().endsWith("open/validate")) {
118118
logger.error("{} attempted to perform request {} token may be compromised.", userId, request.getRequestURI());
119119
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User is deactivated");
120120
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package edu.harvard.hms.dbmi.avillach.auth.model;
2+
3+
import edu.harvard.hms.dbmi.avillach.auth.entity.AccessRule;
4+
5+
import java.util.Set;
6+
7+
public record EvaluateAccessRuleResult(boolean result, Set<AccessRule> failedRules, String passRuleName) {
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package edu.harvard.hms.dbmi.avillach.auth.rest;
2+
3+
import edu.harvard.hms.dbmi.avillach.auth.service.impl.authorization.AuthorizationService;
4+
import io.swagger.v3.oas.annotations.Parameter;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.stereotype.Controller;
8+
import org.springframework.web.bind.annotation.RequestBody;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
11+
import java.util.Map;
12+
13+
@Controller
14+
@RequestMapping(value = "/open")
15+
public class OpenAccessController {
16+
17+
private final AuthorizationService authorizationService;
18+
19+
@Autowired
20+
public OpenAccessController(AuthorizationService authorizationService) {
21+
this.authorizationService = authorizationService;
22+
}
23+
24+
@RequestMapping(value = "/validate", produces = "application/json")
25+
public ResponseEntity<?> validate(@Parameter(required = true, description = "A JSON object that at least" +
26+
" include a user the token for validation")
27+
@RequestBody Map<String, Object> inputMap) {
28+
boolean isValid = authorizationService.openAccessRequestIsValid(inputMap);
29+
return ResponseEntity.ok(isValid);
30+
}
31+
32+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public Role createRole(String roleName, String roleDescription) {
201201
Role role = findByName(roleName);
202202
if (role != null) {
203203
// Role already exists
204-
logger.info("upsertRole() role already exists");
204+
logger.debug("upsertRole() role already exists");
205205
} else {
206206
logger.info("createRole() New PSAMA role name:{}", roleName);
207207
// This is a new Role

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

+56-22
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,14 @@ public class UserService {
5656
private final long tokenExpirationTime;
5757
private static final long defaultTokenExpirationTime = 1000L * 60 * 60; // 1 hour
5858
private final SessionService sessionService;
59+
private final boolean openAccessIsEnabled;
5960

6061
public long longTermTokenExpirationTime;
6162

6263
private final String applicationUUID;
6364
private final ObjectMapper objectMapper = new ObjectMapper();
6465
private final JWTUtil jwtUtil;
6566

66-
private final Set<String> openAccessIdpValues = Set.of("fence", "ras");
67-
6867
@Autowired
6968
public UserService(BasicMailService basicMailService, TOSService tosService,
7069
UserRepository userRepository,
@@ -74,7 +73,8 @@ public UserService(BasicMailService basicMailService, TOSService tosService,
7473
@Value("${application.token.expiration.time}") long tokenExpirationTime,
7574
@Value("${application.default.uuid}") String applicationUUID,
7675
@Value("${application.long.term.token.expiration.time}") long longTermTokenExpirationTime,
77-
JWTUtil jwtUtil, SessionService sessionService) {
76+
JWTUtil jwtUtil, SessionService sessionService,
77+
@Value("${open.idp.provider.is.enabled}") boolean openIdpProviderIsEnabled) {
7878
this.basicMailService = basicMailService;
7979
this.tosService = tosService;
8080
this.userRepository = userRepository;
@@ -89,6 +89,7 @@ public UserService(BasicMailService basicMailService, TOSService tosService,
8989
long defaultLongTermTokenExpirationTime = 1000L * 60 * 60 * 24 * 30;
9090
this.longTermTokenExpirationTime = longTermTokenExpirationTime > 0 ? longTermTokenExpirationTime : defaultLongTermTokenExpirationTime;
9191
this.sessionService = sessionService;
92+
this.openAccessIsEnabled = openIdpProviderIsEnabled;
9293
}
9394

9495
public HashMap<String, String> getUserProfileResponse(Map<String, Object> claims) {
@@ -387,19 +388,46 @@ public Optional<String> getQueryTemplate(String applicationId) {
387388

388389
SecurityContext securityContext = SecurityContextHolder.getContext();
389390
Optional<CustomUserDetails> customUserDetails = Optional.ofNullable((CustomUserDetails) securityContext.getAuthentication().getPrincipal());
390-
if (customUserDetails.isEmpty() || customUserDetails.get().getUser() == null) {
391-
logger.error("Security context didn't have a user stored.");
392-
return Optional.empty();
391+
if ((customUserDetails.isEmpty() || customUserDetails.get().getUser() == null) && openAccessIsEnabled) {
392+
Optional<Application> application = this.applicationRepository.findById(UUID.fromString(applicationId));
393+
if (application.isEmpty()) {
394+
logger.error("getQueryTemplate() cannot find corresponding application by UUID: {}", UUID.fromString(applicationId));
395+
throw new IllegalArgumentException("Cannot find application by input UUID: " + UUID.fromString(applicationId));
396+
}
397+
398+
return Optional.ofNullable(openMergeTemplate(application.orElse(null)));
399+
} else {
400+
if (customUserDetails.isEmpty() || customUserDetails.get().getUser() == null) {
401+
logger.error("Security context didn't have a user stored.");
402+
return Optional.empty();
403+
}
404+
405+
User user = customUserDetails.get().getUser();
406+
Optional<Application> application = this.applicationRepository.findById(UUID.fromString(applicationId));
407+
if (application.isEmpty()) {
408+
logger.error("getQueryTemplate() cannot find corresponding application by UUID: {}", UUID.fromString(applicationId));
409+
throw new IllegalArgumentException("Cannot find application by input UUID: " + UUID.fromString(applicationId));
410+
}
411+
412+
return Optional.ofNullable(mergeTemplate(user, application.orElse(null)));
393413
}
414+
}
394415

395-
User user = customUserDetails.get().getUser();
396-
Optional<Application> application = this.applicationRepository.findById(UUID.fromString(applicationId));
397-
if (application.isEmpty()) {
398-
logger.error("getQueryTemplate() cannot find corresponding application by UUID: {}", UUID.fromString(applicationId));
399-
throw new IllegalArgumentException("Cannot find application by input UUID: " + UUID.fromString(applicationId));
416+
private String openMergeTemplate(Application application) {
417+
Set<Privilege> applicationPrivileges = application.getPrivileges();
418+
Role openAccessRole = roleService.findByName(MANAGED_OPEN_ACCESS_ROLE_NAME);
419+
Set<Privilege> privileges = openAccessRole.getPrivileges();
420+
privileges.addAll(applicationPrivileges);
421+
Map mergedTemplateMap = getMergedQueryTemplateMap(privileges);
422+
String resultJSON;
423+
try {
424+
resultJSON = objectMapper.writeValueAsString(mergedTemplateMap);
425+
} catch (JsonProcessingException ex) {
426+
logger.error("mergeTemplate() cannot convert map to json string. The map mergedTemplate is: {}", mergedTemplateMap);
427+
throw new IllegalArgumentException("Inner application error, please contact admin.");
400428
}
401429

402-
return Optional.ofNullable(mergeTemplate(user, application.orElse(null)));
430+
return resultJSON;
403431
}
404432

405433
public Map<String, String> getDefaultQueryTemplate() {
@@ -416,8 +444,22 @@ public Map<String, String> getDefaultQueryTemplate() {
416444
@Cacheable(value = "mergedTemplateCache", keyGenerator = "customKeyGenerator")
417445
public String mergeTemplate(User user, Application application) {
418446
String resultJSON;
447+
Set<Privilege> privileges = user.getPrivilegesByApplication(application);
448+
Map mergedTemplateMap = getMergedQueryTemplateMap(privileges);
449+
450+
try {
451+
resultJSON = objectMapper.writeValueAsString(mergedTemplateMap);
452+
} catch (JsonProcessingException ex) {
453+
logger.error("mergeTemplate() cannot convert map to json string. The map mergedTemplate is: {}", mergedTemplateMap);
454+
throw new IllegalArgumentException("Inner application error, please contact admin.");
455+
}
456+
457+
return resultJSON;
458+
}
459+
460+
private Map getMergedQueryTemplateMap(Set<Privilege> privileges) {
419461
Map mergedTemplateMap = null;
420-
for (Privilege privilege : user.getPrivilegesByApplication(application)) {
462+
for (Privilege privilege : privileges) {
421463
String template = privilege.getQueryTemplate();
422464
logger.debug("mergeTemplate() processing template:{}", template);
423465
if (template == null || template.trim().isEmpty()) {
@@ -442,15 +484,7 @@ public String mergeTemplate(User user, Application application) {
442484

443485
mergedTemplateMap = JsonUtils.mergeTemplateMap(mergedTemplateMap, templateMap);
444486
}
445-
446-
try {
447-
resultJSON = objectMapper.writeValueAsString(mergedTemplateMap);
448-
} catch (JsonProcessingException ex) {
449-
logger.error("mergeTemplate() cannot convert map to json string. The map mergedTemplate is: {}", mergedTemplateMap);
450-
throw new IllegalArgumentException("Inner application error, please contact admin.");
451-
}
452-
453-
return resultJSON;
487+
return mergedTemplateMap;
454488
}
455489

456490
@CacheEvict(value = "mergedTemplateCache")

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public class FENCEAuthenticationService implements AuthenticationService {
3535
private final Logger logger = LoggerFactory.getLogger(FENCEAuthenticationService.class);
3636

3737
private final UserService userService;
38-
private final RoleService roleService;
3938
private final ConnectionWebService connectionService; // We will need to investigate if the ConnectionWebService will need to be versioned as well.
4039
private final AccessRuleService accessRuleService;
4140
private final FenceMappingUtility fenceMappingUtility;
@@ -52,7 +51,6 @@ public class FENCEAuthenticationService implements AuthenticationService {
5251

5352
@Autowired
5453
public FENCEAuthenticationService(UserService userService,
55-
RoleService roleService,
5654
ConnectionWebService connectionService,
5755
RestClientUtil restClientUtil,
5856
@Value("${fence.idp.provider.is.enabled}") boolean isFenceEnabled,
@@ -62,7 +60,6 @@ public FENCEAuthenticationService(UserService userService,
6260
AccessRuleService accessRuleService,
6361
FenceMappingUtility fenceMappingUtility) {
6462
this.userService = userService;
65-
this.roleService = roleService;
6663
this.connectionService = connectionService;
6764
this.idp_provider_uri = idpProviderUri;
6865
this.fence_client_id = fenceClientId;
@@ -82,7 +79,7 @@ public void initializeFenceService() {
8279

8380
@Override
8481
public HashMap<String, String> authenticate(Map<String, String> authRequest, String host) {
85-
String callBackUrl = "https://" + host + "/psamaui/login/";
82+
String callBackUrl = "https://" + host + "/login/loading/";
8683

8784
logger.debug("getFENCEProfile() starting...");
8885
String fence_code = authRequest.get("code");

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public OktaAuthenticationService(String idpProviderUri, String clientId, String
3636
* @return The response from the token endpoint as a JsonNode
3737
*/
3838
protected JsonNode handleCodeTokenExchange(String host, String code) {
39-
String redirectUri = "https://" + host + "/psamaui/login";
39+
String redirectUri = "https://" + host + "/login/loading";
4040
String queryString = "grant_type=authorization_code" + "&code=" + code + "&redirect_uri=" + redirectUri;
4141
String oktaTokenUrl = "https://" + this.idp_provider_uri + "/oauth2/default/v1/token";
4242

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

-85
This file was deleted.

0 commit comments

Comments
 (0)