diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 51285de2f44..5aae0977c28 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1033,6 +1033,34 @@ Starting the release 4.10 the size of the saved original file (for an ingested t Note the optional "limit" parameter. Without it, the API will attempt to populate the sizes for all the saved originals that don't have them in the database yet. Otherwise it will do so for the first N such datafiles. +Users Token Management +---------------------- + +The following endpoints will allow users to manage their API tokens. + +Find a Token's Expiration Date +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to obtain the expiration date of a token use:: + + curl -H X-Dataverse-key:$API_TOKEN -X GET $SERVER_URL/api/users/token + +Recreate a Token +~~~~~~~~~~~~~~~~ + +In order to obtain a new token use:: + + curl -H X-Dataverse-key:$API_TOKEN -X POST $SERVER_URL/api/users/token/recreate + +Delete a Token +~~~~~~~~~~~~~~~~ + +In order to delete a token use:: + + curl -H X-Dataverse-key:$API_TOKEN -X DELETE $SERVER_URL/api/users/token + + + Builtin Users ------------- diff --git a/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java b/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java index b2ee4299c2a..05923b9e13a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java @@ -69,16 +69,8 @@ public void generate() { if (apiToken != null) { authSvc.removeApiToken(au); } - /** - * @todo DRY! Stolen from BuiltinUsers API page - */ - ApiToken newToken = new ApiToken(); - newToken.setTokenString(java.util.UUID.randomUUID().toString()); - newToken.setAuthenticatedUser(au); - Calendar c = Calendar.getInstance(); - newToken.setCreateTime(new Timestamp(c.getTimeInMillis())); - c.roll(Calendar.YEAR, 1); - newToken.setExpireTime(new Timestamp(c.getTimeInMillis())); + + ApiToken newToken = authSvc.generateApiTokenForUser(au); authSvc.save(newToken); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java index 515184e50b8..0477c1030b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java @@ -155,15 +155,7 @@ private Response internalSave(BuiltinUser user, String password, String key) { UserNotification.Type.CREATEACC, null); } - ApiToken token = new ApiToken(); - - token.setTokenString(java.util.UUID.randomUUID().toString()); - token.setAuthenticatedUser(au); - - Calendar c = Calendar.getInstance(); - token.setCreateTime(new Timestamp(c.getTimeInMillis())); - c.roll(Calendar.YEAR, 1); - token.setExpireTime(new Timestamp(c.getTimeInMillis())); + ApiToken token = authSvc.generateApiTokenForUser(au); authSvc.save(token); JsonObjectBuilder resp = Json.createObjectBuilder(); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Users.java b/src/main/java/edu/harvard/iq/dataverse/api/Users.java index dbce8004925..1ffdb441ca9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Users.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Users.java @@ -6,12 +6,15 @@ package edu.harvard.iq.dataverse.api; import static edu.harvard.iq.dataverse.api.AbstractApiBean.error; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.impl.ChangeUserIdentifierCommand; import edu.harvard.iq.dataverse.engine.command.impl.MergeInAccountCommand; import java.util.logging.Logger; import javax.ejb.Stateless; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -98,4 +101,78 @@ public Response changeAuthenticatedUserIdentifier(@PathParam("identifier") Strin return ok("UserIdentifier changed from " + oldIdentifier + " to " + newIdentifier); } + @Path("token") + @DELETE + public Response deleteToken() { + User u; + + try { + u = findUserOrDie(); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + AuthenticatedUser au; + + try{ + au = (AuthenticatedUser) u; + } catch (ClassCastException e){ + //if we have a non-authenticated user we stop here. + return notFound("Token for " + u.getIdentifier() + " not eligible for deletion."); + } + + authSvc.removeApiToken(au); + return ok("Token for " + au.getUserIdentifier() + " deleted."); + + } + + @Path("token") + @GET + public Response getTokenExpirationDate() { + User u; + + try { + u = findUserOrDie(); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + + ApiToken token = authSvc.findApiToken(getRequestApiKey()); + + if (token == null) { + return notFound("Token " + getRequestApiKey() + " not found."); + } + + return ok("Token " + getRequestApiKey() + " expires on " + token.getExpireTime()); + + } + + @Path("token/recreate") + @POST + public Response recreateToken() { + User u; + + try { + u = findUserOrDie(); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + + AuthenticatedUser au; + try{ + au = (AuthenticatedUser) u; + } catch (ClassCastException e){ + //if we have a non-authenticated user we stop here. + return notFound("Token for " + u.getIdentifier() + " is not eligible for recreation."); + } + + + authSvc.removeApiToken(au); + + ApiToken newToken = authSvc.generateApiTokenForUser(au); + authSvc.save(newToken); + + return ok("New token for " + au.getUserIdentifier() + " is " + newToken.getTokenString()); + + } + } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java index c17a1c689a6..35799add309 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java @@ -5,15 +5,10 @@ import com.jayway.restassured.http.ContentType; import com.jayway.restassured.path.json.JsonPath; import com.jayway.restassured.response.Response; -import static edu.harvard.iq.dataverse.api.AccessIT.apiToken; -import static edu.harvard.iq.dataverse.api.AccessIT.datasetId; -import static edu.harvard.iq.dataverse.api.AccessIT.tabFile3NameRestricted; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import java.util.ArrayList; import java.util.List; import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.json.Json; import javax.json.JsonObjectBuilder; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; @@ -22,6 +17,7 @@ import static javax.ws.rs.core.Response.Status.OK; import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; import static junit.framework.Assert.assertEquals; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertTrue; import org.junit.BeforeClass; @@ -352,6 +348,89 @@ public void testUsernameCaseSensitivity() { .body("message", equalTo("username '" + uppercaseUsername + "' already exists")); ; } + + @Test + public void testAPITokenEndpoints() { + + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + assertEquals(200, createUser.getStatusCode()); + + String userApiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response getExpiration = UtilIT.getTokenExpiration("BAD-TOKEN-692134794"); + getExpiration.prettyPrint(); + getExpiration.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + getExpiration = UtilIT.getTokenExpiration(userApiToken); + getExpiration.prettyPrint(); + getExpiration.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", containsString(userApiToken)) + .body("data.message", containsString("expires on")); + + Response recreateToken = UtilIT.recreateToken("BAD-Token-blah-89234"); + recreateToken.prettyPrint(); + recreateToken.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + recreateToken = UtilIT.recreateToken(userApiToken); + recreateToken.prettyPrint(); + recreateToken.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", containsString("New token for")); + + createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + assertEquals(200, createUser.getStatusCode()); + + String userApiTokenForDelete = UtilIT.getApiTokenFromResponse(createUser); + + /* + Add tests for Private URL + */ + + createUser = UtilIT.createRandomUser(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + + Response createPrivateUrl = UtilIT.privateUrlCreate(datasetId, apiToken); + createPrivateUrl.prettyPrint(); + assertEquals(OK.getStatusCode(), createPrivateUrl.getStatusCode()); + + Response shouldExist = UtilIT.privateUrlGet(datasetId, apiToken); + shouldExist.prettyPrint(); + assertEquals(OK.getStatusCode(), shouldExist.getStatusCode()); + + String tokenForPrivateUrlUser = JsonPath.from(shouldExist.body().asString()).getString("data.token"); + + getExpiration = UtilIT.getTokenExpiration(tokenForPrivateUrlUser); + getExpiration.prettyPrint(); + getExpiration.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + + + Response deleteToken = UtilIT.deleteToken(userApiTokenForDelete); + deleteToken.prettyPrint(); + deleteToken.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", containsString(" deleted.")); + + //Make sure it's deleted + getExpiration = UtilIT.getTokenExpiration(userApiTokenForDelete); + getExpiration.prettyPrint(); + getExpiration.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + } private Response convertUserFromBcryptToSha1(long idOfBcryptUserToConvert, String password) { JsonObjectBuilder data = Json.createObjectBuilder(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 338ae2fe3a0..9c9f0a3defd 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1985,6 +1985,27 @@ static Response sitemapDownload() { return given() .get("/sitemap.xml"); } + + static Response deleteToken( String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .delete("api/users/token"); + return response; + } + + static Response getTokenExpiration( String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("api/users/token"); + return response; + } + + static Response recreateToken( String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .post("api/users/token/recreate"); + return response; + } @Test public void testGetFileIdFromSwordStatementWithNoFiles() {