Skip to content

Commit

Permalink
API token can be passed in a HTTP header (re: #2123)
Browse files Browse the repository at this point in the history
  • Loading branch information
michbarsinai committed Jun 12, 2015
1 parent bef83c2 commit fa26ec8
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 271 deletions.
19 changes: 9 additions & 10 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Native API
==========

Dataverse 4.0 exposes most of its GUI functionality via a REST-based API. Some API calls do not require authentication. Calls that do require authentication take an extra query parameter, ``key``, which should contain the API key of the user issuing the command.
Dataverse 4.0 exposes most of its GUI functionality via a REST-based API. Some API calls do not require authentication. Calls that do require authentication require the user's API key. That key can be passed either via an extra query parameter, ``key``, as in ``ENPOINT?key=API_KEY``, or via the HTTP header ``X-Dataverse-key``. Note that while the header option normally requires more work on client side, it is considered safer, as the API key is not logged in the server access logs.

.. warning:: Dataverse 4.0's API is versioned at the URI - all API calls may include the version number like so: ``http://server-address//api/v1/...``. Omitting the ``v1`` part would default to the latest API version (currently 1). When writing scripts/applications that will be used for a long time, make sure to specify the API version, so they don't break when the API is upgraded.

Expand All @@ -10,7 +10,7 @@ Dataverse 4.0 exposes most of its GUI functionality via a REST-based API. Some A
Endpoints
---------

Dataverses
Dataverses
~~~~~~~~~~~
Generates a new dataverse under ``$id``. Expects a json content describing the dataverse.
If ``$id`` is omitted, a root dataverse is created. ``$id`` can either be a dataverse id (long) or a dataverse alias (more robust). ::
Expand Down Expand Up @@ -112,7 +112,7 @@ List versions of the dataset::
GET http://$SERVER/api/datasets/$id/versions?key=$apiKey

Show a version of the dataset. The Dataset also include any metadata blocks the data might have::

GET http://$SERVER/api/datasets/$id/versions/$versionNumber?key=$apiKey

Lists all the file metadata, for the given dataset and version::
Expand Down Expand Up @@ -158,7 +158,7 @@ Roles
~~~~~

Creates a new role in dataverse object whose Id is ``dataverseIdtf`` (that's an id/alias)::

POST http://$SERVER/api/roles?dvo=$dataverseIdtf&key=$apiKey

Shows the role with ``id``::
Expand All @@ -174,9 +174,9 @@ Explicit Groups
~~~~~~~~~~~~~~~
Explicit groups list their members explicitly. These groups are defined in dataverses, which is why their API endpoint is under ``api/dvn/$id/``, where ``$id`` is the id of the dataverse.


Create a new explicit group under dataverse ``$id``::

POST http://$server/api/dataverses/$id/groups

Data being POSTed is json-formatted description of the group::
Expand Down Expand Up @@ -212,7 +212,7 @@ Add a single role assignee to a group. Request body is ignored::
PUT http://$server/api/dataverses/$dv/groups/$groupAlias/roleAssignees/$roleAssigneeIdentifier

Remove a single role assignee from an explicit group::

DELETE http://$server/api/dataverses/$dv/groups/$groupAlias/roleAssignees/$roleAssigneeIdentifier


Expand All @@ -229,7 +229,7 @@ Return data about the block whose ``identifier`` is passed. ``identifier`` can e
GET http://$SERVER/api/metadatablocks/$identifier


Admin
Admin
~~~~~~~~~~~~~~~~
This is the administrative part of the API. It is probably a good idea to block it before allowing public access to a Dataverse installation. Blocking can be done using settings. See the ``post-install-api-block.sh`` script in the ``scripts/api`` folder for details.

Expand Down Expand Up @@ -259,7 +259,7 @@ List all the authentication providers in the system (both enabled and disabled):

Add new authentication provider. The POST data is in JSON format, similar to the JSON retrieved from this command's ``GET`` counterpart. ::

POST http://$SERVER/api/admin/authenticationProviders
POST http://$SERVER/api/admin/authenticationProviders

Show data about an authentication provider::

Expand Down Expand Up @@ -307,4 +307,3 @@ Returns a the group in a JSON format. ``groupIdtf`` can either be the group id i
Deletes the group specified by ``groupIdtf``. ``groupIdtf`` can either be the group id in the database (in case it is numeric), or the group alias. Note that a group can be deleted only if there are no roles assigned to it. ::

DELETE http://$SERVER/api/admin/groups/ip/$groupIdtf

10 changes: 4 additions & 6 deletions scripts/api/setup-dvs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ echo

echo Uma
echo Pete creates top-level for Uma
curl -s -H "Content-type:application/json" -X POST -d @data/dv-uma-top.json "$SERVER/dataverses/root?key=$1"
curl -s -H "Content-type:application/json" -H "X-Dataverse-key:$1" -X POST -d @data/dv-uma-top.json "$SERVER/dataverses/root"
echo
echo Pete makes Uma an admin on her own DV
curl -s -H "Content-type:application/json" -X POST -d"{\"assignee\":\"@uma\",\"role\":\"admin\"}" $SERVER/dataverses/umaTop/assignments/?key=$1
curl -s -H "Content-type:application/json" -H "X-Dataverse-key:$1" -X POST -d"{\"assignee\":\"@uma\",\"role\":\"admin\"}" $SERVER/dataverses/umaTop/assignments/
echo
curl -s -H "Content-type:application/json" -X POST -d @data/dv-uma-sub1.json "$SERVER/dataverses/umaTop?key=$2"
curl -s -H "Content-type:application/json" -H "X-Dataverse-key:$2" -X POST -d @data/dv-uma-sub1.json "$SERVER/dataverses/umaTop"
echo
curl -s -H "Content-type:application/json" -X POST -d @data/dv-uma-sub2.json "$SERVER/dataverses/umaTop?key=$2"
curl -s -H "Content-type:application/json" -H "X-Dataverse-key:$2" -X POST -d @data/dv-uma-sub2.json "$SERVER/dataverses/umaTop"
echo


72 changes: 49 additions & 23 deletions src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.GuestUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.authorization.users.UserRequestMetadata;
import edu.harvard.iq.dataverse.engine.command.Command;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
Expand All @@ -34,13 +36,12 @@
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
Expand All @@ -51,7 +52,9 @@
* @author michael
*/
public abstract class AbstractApiBean {


private static final String DATAVERSE_KEY_HEADER_NAME = "X-Dataverse-key";

/**
* Utility class to convey a proper error response using Java's exceptions.
*/
Expand Down Expand Up @@ -118,6 +121,10 @@ public Response getResponse() {
@Context
HttpServletRequest request;

@QueryParam("key")
private String queryParamApiKey;


/**
* For pretty printing (indenting) of JSON output.
*/
Expand Down Expand Up @@ -147,16 +154,49 @@ protected AuthenticatedUser findUserByApiToken( String apiKey ) {
return authSvc.lookupUser(apiKey);
}

protected AuthenticatedUser findUserOrDie( String apiKey ) throws WrappedResponse {
AuthenticatedUser u = authSvc.lookupUser(apiKey);
protected String getRequestApiKey() {
String headerParamApiKey = request.getHeader(DATAVERSE_KEY_HEADER_NAME);
return headerParamApiKey!=null ? headerParamApiKey : queryParamApiKey;
}

/**
* 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.
*/
protected User findUserOrDie() throws WrappedResponse {
return ( getRequestApiKey() == null )
? new GuestUser()
: findAuthenticatedUserOrDie();
}

/**
* Finds the authenticated user, based on (in order):
* <ol>
* <li>The key in the HTTP header {@link #DATAVERSE_KEY_HEADER_NAME}</li>
* <li>The key in the query parameter {@code key}
* </ol>
*
* If no user is found, throws a wrapped bad api key (HTTP UNAUTHORIZED) response.
*
* @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.
*/
protected AuthenticatedUser findAuthenticatedUserOrDie() throws WrappedResponse {
return findAuthenticatedUserOrDie(getRequestApiKey());
}

protected AuthenticatedUser findAuthenticatedUserOrDie( String key ) throws WrappedResponse {
AuthenticatedUser u = authSvc.lookupUser(key);
if ( u != null ) {
u.setRequestMetadata( new UserRequestMetadata(request) );
return u;
}
throw new WrappedResponse( badApiKey(apiKey) );
throw new WrappedResponse( badApiKey(key) );
}



protected Dataverse findDataverse( String idtf ) {
return isNumeric(idtf) ? dataverseSvc.find(Long.parseLong(idtf))
: dataverseSvc.findByAlias(idtf);
Expand Down Expand Up @@ -225,14 +265,6 @@ protected Response okResponse( JsonArrayBuilder bld ) {
.add("data", bld).build()).build();
}

protected Response okResponse(JsonArrayBuilder bld, Format format) {
return Response.ok(Util.jsonObject2prettyString(
Json.createObjectBuilder()
.add("status", "OK")
.add("data", bld).build()), MediaType.APPLICATION_JSON_TYPE
).build();
}

protected Response createdResponse( String uri, JsonObjectBuilder bld ) {
return Response.created( URI.create(uri) )
.entity( Json.createObjectBuilder()
Expand Down Expand Up @@ -265,9 +297,9 @@ protected Response okResponse( String msg ) {
* @return a HTTP OK response with the passed value as data.
*/
protected Response okResponseWithValue( String value ) {
return Response.ok(Util.jsonObject2prettyString(Json.createObjectBuilder()
return Response.ok(Json.createObjectBuilder()
.add("status", "OK")
.add("data", value).build()), MediaType.APPLICATION_JSON_TYPE ).build();
.add("data", value).build(), MediaType.APPLICATION_JSON_TYPE ).build();
}

protected Response okResponseWithValue( boolean value ) {
Expand Down Expand Up @@ -296,7 +328,7 @@ protected Response badRequest( String msg ) {
}

protected Response badApiKey( String apiKey ) {
return errorResponse(Status.UNAUTHORIZED, (apiKey != null ) ? "Bad api key '" + apiKey +"'" : "Please provide a key query parameter (?key=XXX)");
return errorResponse(Status.UNAUTHORIZED, (apiKey != null ) ? "Bad api key '" + apiKey +"'" : "Please provide a key query parameter (?key=XXX) or via the HTTP header " + DATAVERSE_KEY_HEADER_NAME );
}

protected Response permissionError( PermissionException pe ) {
Expand Down Expand Up @@ -325,12 +357,6 @@ protected Response execute( Command c ) {
}

protected boolean isNumeric( String str ) { return Util.isNumeric(str); };
protected String error( String msg ) { return Util.error(msg); }
protected String ok( String msg ) { return Util.ok(msg); }
protected String ok( JsonObject jo ) { return Util.ok(jo); }
protected String ok( JsonArray jo ) { return Util.ok(jo); }
protected String ok( JsonObjectBuilder jo ) { return ok(jo.build()); }
protected String ok( JsonArrayBuilder jo ) { return ok(jo.build()); }
}

class LazyRef<T> {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/Access.java
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ private void checkAuthorization(DataFile df, String apiToken) throws WebApplicat
// Even if it's a totally public object.
// So, checking for that first:
logger.info("checking if either a session or a token supplied.");
if (session == null || session.getUser() == null) { // || !session.getUser().isAuthenticated()) {
if (session == null || session.getUser() == null) {
logger.info("session is null, or unauthenticated.");
if (apiToken == null || findUserByApiToken(apiToken) == null) {
logger.info("token null or not supplied.");
Expand Down
Loading

0 comments on commit fa26ec8

Please sign in to comment.