Skip to content

Commit

Permalink
Enable open access validation and improve logging
Browse files Browse the repository at this point in the history
Added new configuration and logic for open access requests validation. Refactored logging statements to use placeholder-based formatting for consistency.
  • Loading branch information
Gcolon021 committed Sep 17, 2024
1 parent 58c8d83 commit 4e6944f
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.Singleton;
import javax.enterprise.context.ApplicationScoped;
import java.net.ProxySelector;

@Singleton
@ApplicationScoped
Expand All @@ -28,6 +27,19 @@ public class PicSureWarInit {
@Resource(mappedName = "java:global/defaultApplicationUUID")
private String default_application_uuid;

@Resource(mappedName = "java:global/openAccessEnabled")
private String open_access_enabled_str;

private boolean open_access_enabled;

@Resource(mappedName = "java:global/openAccessValidateUrl")
private String open_access_validate_url;

@PostConstruct
public void init() {
this.open_access_enabled = Boolean.parseBoolean(open_access_enabled_str);
}

// to be able to pre modified
public static final ObjectMapper objectMapper = new ObjectMapper();

Expand Down Expand Up @@ -61,4 +73,13 @@ public String getToken_introspection_token() {
public String getDefaultApplicationUUID() {
return this.default_application_uuid;
}

public boolean isOpenAccessEnabled() {
return open_access_enabled;
}

public String getOpenAccessValidateUrl() {
return this.open_access_validate_url;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import edu.harvard.dbmi.avillach.util.exception.ApplicationException;
import edu.harvard.dbmi.avillach.util.response.PICSUREResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
Expand Down Expand Up @@ -84,37 +85,55 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
} else {
// Everything else goes through PSAMA token introspection
String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || authorizationHeader.isEmpty()) {
throw new NotAuthorizedException("No authorization header found.");
}
String token = authorizationHeader.substring(6).trim();

String userForLogging = null;
boolean isOpenAccessEnabled = picSureWarInit.isOpenAccessEnabled();
if (
(StringUtils.isBlank(authorizationHeader) && isOpenAccessEnabled)
|| (StringUtils.isNotBlank(authorizationHeader) && authorizationHeader.length() <= 7 && isOpenAccessEnabled)
) {
boolean isAuthorized = callOpenAccessValidationEndpoint(requestContext);
if (!isAuthorized) {
logger.error("User is not authorized.");
requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized."));
}

try {
AuthUser authenticatedUser = null;
// There is no user associated with open access request. In order to provide traceability,
// we set the username to OPEN_ACCESS:<request IP>
requestContext.setProperty("username", "OPEN_ACCESS:" + requestContext.getUriInfo().getRequestUri().getHost());
} else {
if (authorizationHeader == null || authorizationHeader.isEmpty()) {
throw new NotAuthorizedException("No authorization header found.");
}

authenticatedUser = callTokenIntroEndpoint(requestContext, token, userIdClaim);
if (authenticatedUser == null) {
logger.error("Cannot extract a user from token: " + token);
throw new NotAuthorizedException("Cannot find or create a user");
String token = authorizationHeader.substring(6).trim();
if (token.isEmpty()) {
throw new NotAuthorizedException("No token found in authorization header.");
}

userForLogging = authenticatedUser.getUserId();

// The request context wants to remember who the user is
requestContext.setProperty("username", userForLogging);
requestContext.setSecurityContext(new AuthSecurityContext(authenticatedUser, uriInfo.getRequestUri().getScheme()));
logger.info("User - " + userForLogging + " - has just passed all the authentication and authorization layers.");
} catch (NotAuthorizedException e) {
// the detail of this exception should be logged right before the exception thrown out
// logger.error("User - " + userForLogging + " - is not authorized. " + e.getChallenges());
// we should show different response based on role
requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized. " + e.getChallenges()));
} catch (Exception e) {
// we should show different response based on role
e.printStackTrace();
requestContext.abortWith(PICSUREResponse.applicationError("Inner application error, please contact system admin"));
String userForLogging = null;
try {
AuthUser authenticatedUser = null;

authenticatedUser = callTokenIntroEndpoint(requestContext, token, userIdClaim);
if (authenticatedUser == null) {
logger.error("Cannot extract a user from token: {}", token);
throw new NotAuthorizedException("Cannot find or create a user");
}

userForLogging = authenticatedUser.getUserId();

// The request context wants to remember who the user is
requestContext.setProperty("username", userForLogging);
requestContext.setSecurityContext(new AuthSecurityContext(authenticatedUser, uriInfo.getRequestUri().getScheme()));
logger.info("User - {} - has just passed all the authentication and authorization layers.", userForLogging);
} catch (NotAuthorizedException e) {
// the detail of this exception should be logged right before the exception thrown out
logger.error("User - {} - is not authorized. {}", userForLogging, e.getChallenges());
requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized. " + e.getChallenges()));
} catch (Exception e) {
logger
.error("User - {} - is not authorized {} and an Inner application error occurred.", userForLogging, e.getMessage());
requestContext.abortWith(PICSUREResponse.applicationError("Inner application error, please contact system admin"));
}
}
}
}
Expand Down Expand Up @@ -147,7 +166,66 @@ private AuthUser callTokenIntroEndpoint(ContainerRequestContext requestContext,
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("token", token);

Map<String, Object> requestMap = prepareRequestMap(requestContext);
tokenMap.put("request", requestMap);

StringEntity entity = null;
try {
entity = new StringEntity(json.writeValueAsString(tokenMap));
} catch (IOException e) {
logger.error("callTokenIntroEndpoint() - " + e.getClass().getSimpleName() + " when composing post");
return null;
}
post.setEntity(entity);
post.setHeader("Content-Type", "application/json");
// Authorize into the token introspection endpoint
post.setHeader("Authorization", "Bearer " + token_introspection_token);
CloseableHttpResponse response = null;
try {
response = client.execute(post, buildHttpClientContext());
if (response.getStatusLine().getStatusCode() != 200) {
logger.error(
"callTokenIntroEndpoint() error back from token intro host server [" + token_introspection_url + "]: "
+ EntityUtils.toString(response.getEntity())
);
logger.info(
"This callTokenIntroEndpoint error can happen when your introspection token has expired. "
+ "You can fix this by running the Configure PIC-SURE Token Introspection Token job in Jenkins."
);
throw new ApplicationException(
"Token Introspection host server return " + response.getStatusLine().getStatusCode() + ". Please see the log"
);
}
JsonNode responseContent = json.readTree(response.getEntity().getContent());
if (!responseContent.get("active").asBoolean()) {
logger.error("callTokenIntroEndpoint() Token intro endpoint return invalid token, content: " + responseContent);
throw new NotAuthorizedException("Token invalid or expired");
}

if (responseContent.has("tokenRefreshed") && responseContent.get("tokenRefreshed").asBoolean()) {
requestContext.setProperty("refreshedToken", responseContent.get("token"));
}

String userId = responseContent.get(userIdClaim) != null ? responseContent.get(userIdClaim).asText() : null;
String sub = responseContent.get("sub") != null ? responseContent.get("sub").asText() : null;
String email = responseContent.get("email") != null ? responseContent.get("email").asText() : null;
String roles = responseContent.get("roles") != null ? responseContent.get("roles").asText() : null;
AuthUser user = new AuthUser().setUserId(userId).setSubject(sub).setEmail(email).setRoles(roles);
return user;
} catch (IOException ex) {
logger.error("callTokenIntroEndpoint() IOException when hitting url: " + post + " with exception msg: " + ex.getMessage());
} finally {
try {
if (response != null) response.close();
} catch (IOException ex) {
logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage());
}
}

return null;
}

private HashMap<String, Object> prepareRequestMap(ContainerRequestContext requestContext) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
HashMap<String, Object> requestMap = new HashMap<String, Object>();
try {
Expand Down Expand Up @@ -218,70 +296,85 @@ private AuthUser callTokenIntroEndpoint(ContainerRequestContext requestContext,
}
}
}
tokenMap.put("request", requestMap);
return requestMap;
} catch (JsonParseException ex) {
requestMap.put("query", buffer.toString());
tokenMap.put("request", requestMap);
return requestMap;
} catch (IOException e1) {
logger.error("IOException caught trying to build requestMap for auditing.", e1);
throw new NotAuthorizedException(
"The request could not be properly audited. If you recieve this error multiple times, please contact an administrator."
"The request could not be properly audited. If you receive this error multiple times, please contact an administrator."
);
}
}

private boolean callOpenAccessValidationEndpoint(ContainerRequestContext requestContext) {
String openAccessValidateUrl = picSureWarInit.getOpenAccessValidateUrl();
String token_introspection_token = picSureWarInit.getToken_introspection_token();

if (openAccessValidateUrl.isEmpty()) {
throw new ApplicationException("callOpenAccessValidationEndpoint - openAccessValidateUrl is empty in application properties");
}

Map<String, Object> requestMap = new HashMap<>();
Map<String, Object> queryMap = prepareRequestMap(requestContext);
requestMap.put("request", queryMap);
// There is no user associated with open access request. In order to provide traceability,
// we set the username to OPEN_ACCESS:<request IP>
requestMap.put("ipAddress", "OPEN_ACCESS:" + requestContext.getUriInfo().getRequestUri().getHost());
ObjectMapper json = PicSureWarInit.objectMapper;

StringEntity entity = null;
try {
entity = new StringEntity(json.writeValueAsString(tokenMap));
entity = new StringEntity(json.writeValueAsString(requestMap));
} catch (IOException e) {
logger.error("callTokenIntroEndpoint() - " + e.getClass().getSimpleName() + " when composing post");
return null;
logger.error("callOpenAccessValidationEndpoint() - FAILED TO parse requestMap to json", e);
return false;
}

CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT;
HttpPost post = new HttpPost(openAccessValidateUrl);
post.setEntity(entity);
post.setHeader("Content-Type", "application/json");
// Authorize into the token introspection endpoint
post.setHeader("Authorization", "Bearer " + token_introspection_token);
CloseableHttpResponse response = null;
boolean isValid = false;
try {
response = client.execute(post, buildHttpClientContext());
if (response.getStatusLine().getStatusCode() != 200) {

if (response.getStatusLine().getStatusCode() == 200) {

// A 200 is return as long as the request is successful, the actual validation result is in the response body
JsonNode responseContent = json.readTree(response.getEntity().getContent());
if (!responseContent.isBoolean()) {
logger.error(
"callOpenAccessValidateEndpoint() Open access validate endpoint return invalid response, content: {}",
responseContent
);
throw new ApplicationException("Open access validate endpoint returned an invalid response");
}

isValid = responseContent.asBoolean();
} else {
logger.error(
"callTokenIntroEndpoint() error back from token intro host server [" + token_introspection_url + "]: "
+ EntityUtils.toString(response.getEntity())
);
logger.info(
"This callTokenIntroEndpoint error can happen when your introspection token has expired. "
+ "You can fix this by running the Configure PIC-SURE Token Introspection Token job in Jenkins."
"callOpenAccessValidateEndpoint() error returned from psama [{}]: {}", openAccessValidateUrl,
EntityUtils.toString(response.getEntity())
);
throw new ApplicationException(
"Token Introspection host server return " + response.getStatusLine().getStatusCode() + ". Please see the log"
);
}
JsonNode responseContent = json.readTree(response.getEntity().getContent());
if (!responseContent.get("active").asBoolean()) {
logger.error("callTokenIntroEndpoint() Token intro endpoint return invalid token, content: " + responseContent);
throw new NotAuthorizedException("Token invalid or expired");
throw new ApplicationException("Not able to validate open access request");
}

if (responseContent.has("tokenRefreshed") && responseContent.get("tokenRefreshed").asBoolean()) {
requestContext.setProperty("refreshedToken", responseContent.get("token"));
}

String userId = responseContent.get(userIdClaim) != null ? responseContent.get(userIdClaim).asText() : null;
String sub = responseContent.get("sub") != null ? responseContent.get("sub").asText() : null;
String email = responseContent.get("email") != null ? responseContent.get("email").asText() : null;
String roles = responseContent.get("roles") != null ? responseContent.get("roles").asText() : null;
AuthUser user = new AuthUser().setUserId(userId).setSubject(sub).setEmail(email).setRoles(roles);
return user;
} catch (IOException ex) {
logger.error("callTokenIntroEndpoint() IOException when hitting url: " + post + " with exception msg: " + ex.getMessage());
logger.error("callOpenAccessValidateEndpoint() IOException when hitting url: {} with exception msg: {}", post, ex.getMessage());
} finally {
try {
if (response != null) response.close();
} catch (IOException ex) {
logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage());
logger.error("callOpenAccessValidateEndpoint() IOException when closing http response: {}", ex.getMessage());
}
}

return null;
return isValid;
}

void setUserIdClaim(String userIdClaim) {
Expand Down
Loading

0 comments on commit 4e6944f

Please sign in to comment.