Skip to content
28 changes: 28 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------

Expand Down
12 changes: 2 additions & 10 deletions src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Expand Down
10 changes: 1 addition & 9 deletions src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
77 changes: 77 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Users.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());

}

}
89 changes: 84 additions & 5 deletions src/test/java/edu/harvard/iq/dataverse/api/UsersIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down