Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ALS-6921] Implement Open Access in PSAMA #211

Merged
merged 7 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion pic-sure-auth-services/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,19 @@
<artifactId>jackson-annotations</artifactId>
<version>2.17.0</version>
</dependency>

</dependencies>
<profiles>
<profile>
<id>dev</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/authentication",
"/authentication/**",
"/swagger.yaml",
"/swagger.json"
"/swagger.json",
"/user/me/queryTemplate",
"/user/me/queryTemplate/**",
"/open/validate"
).permitAll()
.anyRequest().authenticated()
)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.logout((logout) -> logout.logoutUrl("/logout").addLogoutHandler(customLogoutHandler()));

.logout((logout) -> logout.logoutUrl("/logout").addLogoutHandler(customLogoutHandler()).logoutSuccessHandler((request, response, authentication) -> {
// We don't want to redirect to a login page, we just want to return a 200
// We leave it to the client to handle the redirect
response.setStatus(200);
}));

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
package edu.harvard.hms.dbmi.avillach.auth.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Transient;

import java.lang.reflect.Field;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServlet

// Check if user is attempting to access the correct introspect endpoint. If not reject the request
// log an error indicating the user's token may be being used by a malicious actor.
if (!request.getRequestURI().endsWith("token/inspect")) {
if (!request.getRequestURI().endsWith("token/inspect") && !request.getRequestURI().endsWith("open/validate")) {
logger.error("{} attempted to perform request {} token may be compromised.", userId, request.getRequestURI());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User is deactivated");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package edu.harvard.hms.dbmi.avillach.auth.model;

import edu.harvard.hms.dbmi.avillach.auth.entity.AccessRule;

import java.util.Set;

public record EvaluateAccessRuleResult(boolean result, Set<AccessRule> failedRules, String passRuleName) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package edu.harvard.hms.dbmi.avillach.auth.rest;

import edu.harvard.hms.dbmi.avillach.auth.service.impl.authorization.AuthorizationService;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Map;

@Controller
@RequestMapping(value = "/open")
public class OpenAccessController {

private final AuthorizationService authorizationService;

@Autowired
public OpenAccessController(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}

@RequestMapping(value = "/validate", produces = "application/json")
public ResponseEntity<?> validate(@Parameter(required = true, description = "A JSON object that at least" +
" include a user the token for validation")
@RequestBody Map<String, Object> inputMap) {
boolean isValid = authorizationService.openAccessRequestIsValid(inputMap);
return ResponseEntity.ok(isValid);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public Role createRole(String roleName, String roleDescription) {
Role role = findByName(roleName);
if (role != null) {
// Role already exists
logger.info("upsertRole() role already exists");
logger.debug("upsertRole() role already exists");
} else {
logger.info("createRole() New PSAMA role name:{}", roleName);
// This is a new Role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,14 @@ public class UserService {
private final long tokenExpirationTime;
private static final long defaultTokenExpirationTime = 1000L * 60 * 60; // 1 hour
private final SessionService sessionService;
private final boolean openAccessIsEnabled;

public long longTermTokenExpirationTime;

private final String applicationUUID;
private final ObjectMapper objectMapper = new ObjectMapper();
private final JWTUtil jwtUtil;

private final Set<String> openAccessIdpValues = Set.of("fence", "ras");

@Autowired
public UserService(BasicMailService basicMailService, TOSService tosService,
UserRepository userRepository,
Expand All @@ -74,7 +73,8 @@ public UserService(BasicMailService basicMailService, TOSService tosService,
@Value("${application.token.expiration.time}") long tokenExpirationTime,
@Value("${application.default.uuid}") String applicationUUID,
@Value("${application.long.term.token.expiration.time}") long longTermTokenExpirationTime,
JWTUtil jwtUtil, SessionService sessionService) {
JWTUtil jwtUtil, SessionService sessionService,
@Value("${open.idp.provider.is.enabled}") boolean openIdpProviderIsEnabled) {
this.basicMailService = basicMailService;
this.tosService = tosService;
this.userRepository = userRepository;
Expand All @@ -89,6 +89,7 @@ public UserService(BasicMailService basicMailService, TOSService tosService,
long defaultLongTermTokenExpirationTime = 1000L * 60 * 60 * 24 * 30;
this.longTermTokenExpirationTime = longTermTokenExpirationTime > 0 ? longTermTokenExpirationTime : defaultLongTermTokenExpirationTime;
this.sessionService = sessionService;
this.openAccessIsEnabled = openIdpProviderIsEnabled;
}

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

SecurityContext securityContext = SecurityContextHolder.getContext();
Optional<CustomUserDetails> customUserDetails = Optional.ofNullable((CustomUserDetails) securityContext.getAuthentication().getPrincipal());
if (customUserDetails.isEmpty() || customUserDetails.get().getUser() == null) {
logger.error("Security context didn't have a user stored.");
return Optional.empty();
if ((customUserDetails.isEmpty() || customUserDetails.get().getUser() == null) && openAccessIsEnabled) {
Optional<Application> application = this.applicationRepository.findById(UUID.fromString(applicationId));
if (application.isEmpty()) {
logger.error("getQueryTemplate() cannot find corresponding application by UUID: {}", UUID.fromString(applicationId));
throw new IllegalArgumentException("Cannot find application by input UUID: " + UUID.fromString(applicationId));
}

return Optional.ofNullable(openMergeTemplate(application.orElse(null)));
} else {
if (customUserDetails.isEmpty() || customUserDetails.get().getUser() == null) {
logger.error("Security context didn't have a user stored.");
return Optional.empty();
}

User user = customUserDetails.get().getUser();
Optional<Application> application = this.applicationRepository.findById(UUID.fromString(applicationId));
if (application.isEmpty()) {
logger.error("getQueryTemplate() cannot find corresponding application by UUID: {}", UUID.fromString(applicationId));
throw new IllegalArgumentException("Cannot find application by input UUID: " + UUID.fromString(applicationId));
}

return Optional.ofNullable(mergeTemplate(user, application.orElse(null)));
}
}

User user = customUserDetails.get().getUser();
Optional<Application> application = this.applicationRepository.findById(UUID.fromString(applicationId));
if (application.isEmpty()) {
logger.error("getQueryTemplate() cannot find corresponding application by UUID: {}", UUID.fromString(applicationId));
throw new IllegalArgumentException("Cannot find application by input UUID: " + UUID.fromString(applicationId));
private String openMergeTemplate(Application application) {
Set<Privilege> applicationPrivileges = application.getPrivileges();
Role openAccessRole = roleService.findByName(MANAGED_OPEN_ACCESS_ROLE_NAME);
Set<Privilege> privileges = openAccessRole.getPrivileges();
privileges.addAll(applicationPrivileges);
Map mergedTemplateMap = getMergedQueryTemplateMap(privileges);
String resultJSON;
try {
resultJSON = objectMapper.writeValueAsString(mergedTemplateMap);
} catch (JsonProcessingException ex) {
logger.error("mergeTemplate() cannot convert map to json string. The map mergedTemplate is: {}", mergedTemplateMap);
throw new IllegalArgumentException("Inner application error, please contact admin.");
}

return Optional.ofNullable(mergeTemplate(user, application.orElse(null)));
return resultJSON;
}

public Map<String, String> getDefaultQueryTemplate() {
Expand All @@ -416,8 +444,22 @@ public Map<String, String> getDefaultQueryTemplate() {
@Cacheable(value = "mergedTemplateCache", keyGenerator = "customKeyGenerator")
public String mergeTemplate(User user, Application application) {
String resultJSON;
Set<Privilege> privilegesByApplication = user.getPrivilegesByApplication(application);
Map mergedTemplateMap = getMergedQueryTemplateMap(privilegesByApplication);

try {
resultJSON = objectMapper.writeValueAsString(mergedTemplateMap);
} catch (JsonProcessingException ex) {
logger.error("mergeTemplate() cannot convert map to json string. The map mergedTemplate is: {}", mergedTemplateMap);
throw new IllegalArgumentException("Inner application error, please contact admin.");
}

return resultJSON;
}

private Map getMergedQueryTemplateMap(Set<Privilege> privileges) {
Map mergedTemplateMap = null;
for (Privilege privilege : user.getPrivilegesByApplication(application)) {
for (Privilege privilege : privileges) {
String template = privilege.getQueryTemplate();
logger.debug("mergeTemplate() processing template:{}", template);
if (template == null || template.trim().isEmpty()) {
Expand All @@ -442,15 +484,7 @@ public String mergeTemplate(User user, Application application) {

mergedTemplateMap = JsonUtils.mergeTemplateMap(mergedTemplateMap, templateMap);
}

try {
resultJSON = objectMapper.writeValueAsString(mergedTemplateMap);
} catch (JsonProcessingException ex) {
logger.error("mergeTemplate() cannot convert map to json string. The map mergedTemplate is: {}", mergedTemplateMap);
throw new IllegalArgumentException("Inner application error, please contact admin.");
}

return resultJSON;
return mergedTemplateMap;
}

@CacheEvict(value = "mergedTemplateCache")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public class FENCEAuthenticationService implements AuthenticationService {
private final Logger logger = LoggerFactory.getLogger(FENCEAuthenticationService.class);

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

@Autowired
public FENCEAuthenticationService(UserService userService,
RoleService roleService,
ConnectionWebService connectionService,
RestClientUtil restClientUtil,
@Value("${fence.idp.provider.is.enabled}") boolean isFenceEnabled,
Expand All @@ -62,7 +60,6 @@ public FENCEAuthenticationService(UserService userService,
AccessRuleService accessRuleService,
FenceMappingUtility fenceMappingUtility) {
this.userService = userService;
this.roleService = roleService;
this.connectionService = connectionService;
this.idp_provider_uri = idpProviderUri;
this.fence_client_id = fenceClientId;
Expand All @@ -82,7 +79,7 @@ public void initializeFenceService() {

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

logger.debug("getFENCEProfile() starting...");
String fence_code = authRequest.get("code");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public OktaAuthenticationService(String idpProviderUri, String clientId, String
* @return The response from the token endpoint as a JsonNode
*/
protected JsonNode handleCodeTokenExchange(String host, String code) {
String redirectUri = "https://" + host + "/psamaui/login";
String redirectUri = "https://" + host + "/login/loading";
String queryString = "grant_type=authorization_code" + "&code=" + code + "&redirect_uri=" + redirectUri;
String oktaTokenUrl = "https://" + this.idp_provider_uri + "/oauth2/default/v1/token";

Expand Down

This file was deleted.

Loading