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

9293 - New filter-based design for the API authentication mechanisms (1/2) #9303

Merged
merged 37 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
03ae358
Added: API security filter with annotation
GPortas Jan 18, 2023
83f3aa0
Added: ApiKeyAuthMechanism with key validation through filter. Pendin…
GPortas Jan 18, 2023
1f3b917
Added: Private URL user support within ApiKeyAuthMechanism
GPortas Jan 18, 2023
ad7f9e3
Added: ApiKeyAuthMechanismTest with initial PrivateUrlUser (not anony…
GPortas Jan 20, 2023
f8bfd26
Added: rest of PrivateUrlUser-related test cases to ApiKeyAuthMechani…
GPortas Jan 20, 2023
d870241
Added: rest of test cases for ApiKeyAuthMechanismTest (empty Api Key,…
GPortas Jan 20, 2023
54d9c0a
Refactor: ApiKeyAuthMechanismTest cases naming structure
GPortas Jan 20, 2023
cc8521c
Added: composite of AuthMechanism implementations
GPortas Jan 20, 2023
60cebc6
Refactor: new ApiConstants for common API constant values
GPortas Jan 23, 2023
9d037e8
Refactor: Auth-components renamed
GPortas Jan 23, 2023
d4d69b7
Refactor: deleteDataset endpoint uses AuthFilter instead of findUserO…
GPortas Jan 23, 2023
a5b27c2
Refactor: new AbstractApiBean method for retrieving user from Contain…
GPortas Jan 23, 2023
4b8e7d4
Refactor: new AbstractApiBean method for retrieving user from Contain…
GPortas Jan 23, 2023
cf57f14
Refactor: all Datasets API findUserOrDie calls replaced by AuthFilter
GPortas Jan 23, 2023
17653b3
Fixed: missing AuthFilter setup for Datasets getVersionJsonLDMetadata…
GPortas Jan 23, 2023
642ffb3
Merge branch 'develop' into 9293-filter-api-auth
GPortas Jan 23, 2023
219beed
Refactor: from findAuthenticatedUserOrDie to AuthFilter
GPortas Jan 24, 2023
f097d13
Added: new WorkflowKeyAuthMechanism to AuthFilter authentication mech…
GPortas Jan 24, 2023
c766afd
Added: new SignedUrlAuthMechanism to AuthFilter mechanisms and Compou…
GPortas Jan 25, 2023
c3087a8
Added: missing test case and tests refactoring for SignedUrlAuthMecha…
GPortas Jan 25, 2023
164bcef
Added: SignedUrlAuthMechanism to CompoundAuthMechanism
GPortas Jan 25, 2023
6e9d446
Fixed: signed URL authentication when provided id does not correspond…
GPortas Jan 25, 2023
f7d7172
Fixed: failing test test_004_AddFileBadToken
GPortas Jan 25, 2023
de6beff
Refactor: new response method within AbstractApiBean for handling use…
GPortas Jan 26, 2023
c020e98
Changed: SignedUrlAuthMechanism now handles the case when a token is …
GPortas Jan 26, 2023
a0f63c1
Doc: Javadocs with deprecation warnings added to AbstractApiBean
GPortas Jan 26, 2023
368a860
Doc: Javadocs and deprecation warnings to AuthenticatedUser retrieval…
GPortas Jan 26, 2023
b00deea
Refactor: using ApiConstants in all places for common API statuses
GPortas Jan 27, 2023
fb4ec8f
Merge branch 'develop' into 9293-filter-api-auth
GPortas Jan 27, 2023
a3f8c4e
Fixed: failing call due to relocated constant
GPortas Jan 27, 2023
4b5f293
Fixed: wrong response status code within test_004_AddFileBadToken ass…
GPortas Jan 27, 2023
bd0a673
Doc: api.auth components Javadocs
GPortas Jan 29, 2023
b379119
Doc: improved Javadocs for AuthMechanism interface
GPortas Jan 30, 2023
353aad9
Merge branch 'develop' into 9293-filter-api-auth
GPortas Jan 30, 2023
ba31255
Merge branch 'develop' into 9293-filter-api-auth
GPortas Feb 2, 2023
e0c5af3
Merge branch 'develop' into 9293-filter-api-auth
GPortas Feb 15, 2023
fdab1b3
Fixed: URL decoding issue in SignedUrlAuthMechanism causing signed UR…
GPortas Feb 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 97 additions & 34 deletions src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;

import static org.apache.commons.lang3.StringUtils.isNumeric;

/**
Expand All @@ -94,8 +95,6 @@ public abstract class AbstractApiBean {
private static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key";
private static final String PERSISTENT_ID_KEY=":persistentId";
private static final String ALIAS_KEY=":alias";
public static final String STATUS_ERROR = "ERROR";
public static final String STATUS_OK = "OK";
public static final String STATUS_WF_IN_PROGRESS = "WORKFLOW_IN_PROGRESS";
public static final String DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME = "X-Dataverse-invocationID";

Expand Down Expand Up @@ -329,6 +328,31 @@ protected String getRequestWorkflowInvocationID() {
return headerParamWFKey!=null ? headerParamWFKey : queryParamWFKey;
}

protected User getRequestUser(ContainerRequestContext crc) {
return (User) crc.getProperty(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER);
}

/**
* Gets the authenticated user from the ContainerRequestContext user property. If the user from the property
* is not authenticated, throws a wrapped "authenticated user required" user (HTTP UNAUTHORIZED) response.
* @param crc a ContainerRequestContext implementation
* @return The authenticated user
* @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse in case the user is not authenticated.
*
* TODO:
* This method is designed to comply with existing authorization logic, based on the old findAuthenticatedUserOrDie method.
* Ideally, as for authentication, a filter could be implemented for authorization, which would extract and encapsulate the
* authorization logic from the AbstractApiBean.
*/
protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestContext crc) throws WrappedResponse {
User requestUser = (User) crc.getProperty(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER);
if (requestUser.isAuthenticated()) {
return (AuthenticatedUser) requestUser;
} else {
throw new WrappedResponse(authenticatedUserRequired());
}
}

/* ========= *\
* Finders *
\* ========= */
Expand Down Expand Up @@ -360,7 +384,14 @@ protected AuthenticatedUser findUserByApiToken( String apiKey ) {
* Returns the user of pointed by the API key, or the guest user
* @return a user, may be a guest user.
* @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse iff there is an api key present, but it is invalid.
*
* @deprecated Do not use this method.
* This method is expected to be removed once all API endpoints use the filter-based authentication.
* @see <a href="https://github.com/IQSS/dataverse/issues/9293">#9293</a>
* In case you are implementing a new endpoint that requires user authentication, here is an example of how to apply the new filter-based authentication:
* {@link edu.harvard.iq.dataverse.api.Datasets#getDataset(ContainerRequestContext, String, UriInfo, HttpHeaders, HttpServletResponse)}
*/
@Deprecated
protected User findUserOrDie() throws WrappedResponse {
final String requestApiKey = getRequestApiKey();
final String requestWFKey = getRequestWorkflowInvocationID();
Expand Down Expand Up @@ -393,9 +424,16 @@ protected User findUserOrDie() throws WrappedResponse {
*
* If no user is found, throws a wrapped bad api key (HTTP UNAUTHORIZED) response.
*
* @return The authenticated user which owns the passed api key
* @return The authenticated user which owns the passed api key.
* @throws edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse in case said user is not found.
*
* @deprecated Do not use this method.
* This method is expected to be removed once all API endpoints use the filter-based authentication.
* @see <a href="https://github.com/IQSS/dataverse/issues/9293">#9293</a>
* Replaced by:
* {@link #getRequestAuthenticatedUserOrDie(ContainerRequestContext)}
*/
@Deprecated
protected AuthenticatedUser findAuthenticatedUserOrDie() throws WrappedResponse {
return findAuthenticatedUserOrDie(getRequestApiKey(), getRequestWorkflowInvocationID());
}
Expand Down Expand Up @@ -703,16 +741,7 @@ protected Response response( Callable<Response> hdl ) {
} catch ( WrappedResponse rr ) {
return rr.getResponse();
} catch ( Exception ex ) {
String incidentId = UUID.randomUUID().toString();
logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex);
return Response.status(500)
.entity( Json.createObjectBuilder()
.add("status", "ERROR")
.add("code", 500)
.add("message", "Internal server error. More details available at the server logs.")
.add("incidentId", incidentId)
.build())
.type("application/json").build();
return handleDataverseRequestHandlerException(ex);
}
}

Expand All @@ -728,24 +757,53 @@ protected Response response( Callable<Response> hdl ) {
*
* @param hdl handling code block.
* @return HTTP Response appropriate for the way {@code hdl} executed.
*
* @deprecated Do not use this method.
* This method is expected to be removed once all API endpoints use the filter-based authentication.
* @see <a href="https://github.com/IQSS/dataverse/issues/9293">#9293</a>
* Replaced by:
* {@link #response(DataverseRequestHandler, User)}
*/
@Deprecated
protected Response response( DataverseRequestHandler hdl ) {
try {
return hdl.handle(createDataverseRequest(findUserOrDie()));
} catch ( WrappedResponse rr ) {
return rr.getResponse();
} catch ( Exception ex ) {
String incidentId = UUID.randomUUID().toString();
logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex);
return Response.status(500)
.entity( Json.createObjectBuilder()
.add("status", "ERROR")
.add("code", 500)
.add("message", "Internal server error. More details available at the server logs.")
.add("incidentId", incidentId)
return handleDataverseRequestHandlerException(ex);
}
}

/***
* The preferred way of handling a request that requires a user. The method
* receives a user and handles it to the handler for doing the actual work.
*
* @param hdl handling code block.
* @param user the associated request user.
* @return HTTP Response appropriate for the way {@code hdl} executed.
*/
protected Response response(DataverseRequestHandler hdl, User user) {
try {
return hdl.handle(createDataverseRequest(user));
} catch ( WrappedResponse rr ) {
return rr.getResponse();
} catch ( Exception ex ) {
return handleDataverseRequestHandlerException(ex);
}
}

private Response handleDataverseRequestHandlerException(Exception ex) {
String incidentId = UUID.randomUUID().toString();
logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex);
return Response.status(500)
.entity(Json.createObjectBuilder()
.add("status", "ERROR")
.add("code", 500)
.add("message", "Internal server error. More details available at the server logs.")
.add("incidentId", incidentId)
.build())
.type("application/json").build();
}
}

/* ====================== *\
Expand All @@ -754,45 +812,45 @@ protected Response response( DataverseRequestHandler hdl ) {

protected Response ok( JsonArrayBuilder bld ) {
return Response.ok(Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("data", bld).build())
.type(MediaType.APPLICATION_JSON).build();
}

protected Response ok( JsonArray ja ) {
return Response.ok(Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("data", ja).build())
.type(MediaType.APPLICATION_JSON).build();
}

protected Response ok( JsonObjectBuilder bld ) {
return Response.ok( Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("data", bld).build() )
.type(MediaType.APPLICATION_JSON)
.build();
}

protected Response ok( JsonObject jo ) {
return Response.ok( Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("data", jo).build() )
.type(MediaType.APPLICATION_JSON)
.build();
}

protected Response ok( String msg ) {
return Response.ok().entity(Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("data", Json.createObjectBuilder().add("message",msg)).build() )
.type(MediaType.APPLICATION_JSON)
.build();
}

protected Response ok( String msg, JsonObjectBuilder bld ) {
return Response.ok().entity(Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("message", Json.createObjectBuilder().add("message",msg))
.add("data", bld).build())
.type(MediaType.APPLICATION_JSON)
Expand All @@ -801,7 +859,7 @@ protected Response ok( String msg, JsonObjectBuilder bld ) {

protected Response ok( boolean value ) {
return Response.ok().entity(Json.createObjectBuilder()
.add("status", STATUS_OK)
.add("status", ApiConstants.STATUS_OK)
.add("data", value).build() ).build();
}

Expand Down Expand Up @@ -867,7 +925,12 @@ protected Response badWFKey( String wfId ) {
String message = (wfId != null ) ? "Bad workflow invocationId " : "Please provide an invocationId query parameter (?invocationId=XXX) or via the HTTP header " + DATAVERSE_WORKFLOW_INVOCATION_HEADER_NAME;
return error(Status.UNAUTHORIZED, message );
}


protected Response authenticatedUserRequired() {
String message = "Only authenticated users can perform the requested operation";
return error(Status.UNAUTHORIZED, message );
}

protected Response permissionError( PermissionException pe ) {
return permissionError( pe.getMessage() );
}
Expand All @@ -883,7 +946,7 @@ protected Response unauthorized( String message ) {
protected static Response error( Status sts, String msg ) {
return Response.status(sts)
.entity( NullSafeJsonBuilder.jsonObjectBuilder()
.add("status", STATUS_ERROR)
.add("status", ApiConstants.STATUS_ERROR)
.add( "message", msg ).build()
).type(MediaType.APPLICATION_JSON_TYPE).build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package edu.harvard.iq.dataverse.api;

import javax.ws.rs.ApplicationPath;

import edu.harvard.iq.dataverse.api.auth.AuthFilter;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;

Expand All @@ -11,9 +13,6 @@ public ApiConfiguration() {
packages("edu.harvard.iq.dataverse.api");
packages("edu.harvard.iq.dataverse.mydata");
register(MultiPartFeature.class);
register(AuthFilter.class);
}
}
/*
public class ApiConfiguration extends ResourceConfi {
}
*/
15 changes: 15 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/ApiConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.harvard.iq.dataverse.api;

public final class ApiConstants {

private ApiConstants() {
// Restricting instantiation
}

// Statuses
public static final String STATUS_OK = "OK";
public static final String STATUS_ERROR = "ERROR";

// Authentication
public static final String CONTAINER_REQUEST_CONTEXT_USER = "user";
}