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

7715 signed urls for external tools #8999

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
07b34b0
initial commit
rtreacy Jul 29, 2021
c017fd3
Merge branch 'develop' into 7715-signed-urls-for-external-tools
rtreacy Mar 11, 2022
4c0fce0
rename getQueryParametersForUrl to handleRequest
rtreacy Mar 16, 2022
36fb985
rename getQueryParametersForUrl to handleRequest
rtreacy Mar 16, 2022
b90216f
add UrlSignerUtil.java
rtreacy Mar 16, 2022
fecd8a1
Merge branch 'develop' into 7715-signed-urls-for-external-tools
rtreacy Mar 16, 2022
a7d1767
Merge branch '7715-signed-urls-for-external-tools' of github.com:IQSS…
rtreacy Mar 16, 2022
cb418a7
Merge branch 'develop' into 7715-signed-urls-for-external-tools
rtreacy Apr 8, 2022
ac23437
add signed Url to header and use POST for external tools, in particul…
rtreacy May 2, 2022
7e82009
use signedUrl for getting authenticated user. add allowedUrls field t…
rtreacy Jun 8, 2022
7c9fa06
fix for validation method/comments
qqmyers Jun 9, 2022
39180cc
JSON API call to request signedUrl
qqmyers Jun 9, 2022
55fafa5
json read object/array from string methods from other branches
qqmyers Jun 9, 2022
01973ff
Merge pull request #8788 from GlobalDataverseCommunityConsortium/7715…
rtreacy Jun 9, 2022
881e3db
define/use an additional secret key
qqmyers Jun 14, 2022
208ab95
refactor to allow URL token substitution outside tools framework
qqmyers Jun 21, 2022
8c2f950
Merge pull request #8802 from GlobalDataverseCommunityConsortium/7715…
rtreacy Jun 23, 2022
0c22b18
sending a list of allowed api calls to DPCreator
rtreacy Jul 15, 2022
1b31e6c
tweak json read/write, getString, cleanup, logging
qqmyers Jul 20, 2022
2c90139
Merge remote-tracking branch 'IQSS/develop' into 7715-signed-urls-for…
qqmyers Jul 20, 2022
66355b0
Merge pull request #8850 from GlobalDataverseCommunityConsortium/7715…
rtreacy Jul 21, 2022
d4189f3
add signer tests, flip param order so sign/validate match, fix val bug
qqmyers Aug 4, 2022
6331bec
Merge pull request #8896 from GlobalDataverseCommunityConsortium/7715…
rtreacy Aug 5, 2022
22cdaaf
passes existing query params an signed urls in POST body as json
Sep 11, 2022
42d906b
uses JsonObjectBuilder, elininating some string building that was mes…
rtreacy Sep 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public void generateApiToken() {
ApiToken apiToken = new ApiToken();
User user = session.getUser();
if (user instanceof AuthenticatedUser) {
toolHandler.setUser(((AuthenticatedUser) user).getUserIdentifier());
apiToken = authService.findApiTokenByUser((AuthenticatedUser) user);
if (apiToken == null) {
//No un-expired token
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.util.UrlSignerUtil;
import edu.harvard.iq.dataverse.util.json.JsonParser;
import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder;
import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean;
Expand Down Expand Up @@ -419,10 +420,36 @@ private AuthenticatedUser findAuthenticatedUserOrDie( String key, String wfid )
} else {
throw new WrappedResponse(badWFKey(wfid));
}
} else {
AuthenticatedUser authUser = getAuthenticatedUserFromSignedUrl();
if (authUser != null) {
return authUser;
}
}
//Just send info about the apiKey - workflow users will learn about invocationId elsewhere
throw new WrappedResponse(badApiKey(null));
}

private AuthenticatedUser getAuthenticatedUserFromSignedUrl() {
AuthenticatedUser authUser = null;
// The signUrl contains a param telling which user this is supposed to be for.
// We don't trust this. So we lookup that user, and get their API key, and use
// that as a secret in validation the signedURL. If the signature can't be
// validating with their key, the user (or their API key) has been changed and
// we reject the request.
//ToDo - add null checks/ verify that calling methods catch things.
String user = httpRequest.getParameter("user");
AuthenticatedUser targetUser = authSvc.getAuthenticatedUser(user);
String key = System.getProperty(SystemConfig.API_SIGNING_SECRET,"") + authSvc.findApiTokenByUser(targetUser).getTokenString();
String signedUrl = httpRequest.getRequestURL().toString()+"?"+httpRequest.getQueryString();
String method = httpRequest.getMethod();
String queryString = httpRequest.getQueryString();
boolean validated = UrlSignerUtil.isValidUrl(signedUrl, user, method, key);
if (validated){
authUser = targetUser;
}
return authUser;
}

protected Dataverse findDataverseOrDie( String dvIdtf ) throws WrappedResponse {
Dataverse dv = findDataverse(dvIdtf);
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Admin.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider;
import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil;
import edu.harvard.iq.dataverse.authorization.users.ApiToken;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailData;
import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailException;
Expand All @@ -47,6 +48,7 @@
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
Expand Down Expand Up @@ -98,6 +100,7 @@
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.FileUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.util.UrlSignerUtil;

import java.io.IOException;
import java.io.OutputStream;
Expand Down Expand Up @@ -2139,4 +2142,43 @@ public Response getBannerMessages(@PathParam("id") Long id) throws WrappedRespon
.collect(toJsonArray()));

}

@POST
@Consumes("application/json")
@Path("/requestSignedUrl")
public Response getSignedUrl(JsonObject urlInfo) throws WrappedResponse {
AuthenticatedUser superuser = authSvc.getAdminUser();

if (superuser == null) {
return error(Response.Status.FORBIDDEN, "Requesting signed URLs is restricted to superusers.");
}

String userId = urlInfo.getString("user");
String key=null;
if(userId!=null) {
AuthenticatedUser user = authSvc.getAuthenticatedUser(userId);
if(user!=null) {
ApiToken apiToken = authSvc.findApiTokenByUser(user);
if(apiToken!=null && !apiToken.isExpired() && ! apiToken.isDisabled()) {
key = apiToken.getTokenString();
}
} else {
userId=superuser.getIdentifier();
//We ~know this exists - the superuser just used it and it was unexpired/not disabled. (ToDo - if we want this to work with workflow tokens (or as a signed URL, we should do more checking as for the user above))
}
key = System.getProperty(SystemConfig.API_SIGNING_SECRET,"") + authSvc.findApiTokenByUser(superuser).getTokenString();
}
if(key==null) {
return error(Response.Status.CONFLICT, "Do not have a valid user with apiToken");
}

String baseUrl = urlInfo.getString("url");
int timeout = urlInfo.getInt("timeout", 10);
String method = urlInfo.getString("method", "GET");

String signedUrl = UrlSignerUtil.signUrl(baseUrl, timeout, userId, method, key);

return ok(Json.createObjectBuilder().add("signedUrl", signedUrl));
}

}
10 changes: 9 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/Users.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.json.JsonArray;
Expand Down Expand Up @@ -201,7 +202,14 @@ public Response getAuthenticatedUserByToken() {

AuthenticatedUser authenticatedUser = findUserByApiToken(tokenFromRequestAPI);
if (authenticatedUser == null) {
return error(Response.Status.BAD_REQUEST, "User with token " + tokenFromRequestAPI + " not found.");
try {
authenticatedUser = findAuthenticatedUserOrDie();
return ok(json(authenticatedUser));
} catch (WrappedResponse ex) {
Logger.getLogger(Users.class.getName()).log(Level.SEVERE, null, ex);
return error(Response.Status.BAD_REQUEST, "User with token " + tokenFromRequestAPI + " not found.");
}

} else {
return ok(json(authenticatedUser));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Transient;

/**
* A specification or definition for how an external tool is intended to
Expand All @@ -30,8 +29,6 @@
@Entity
public class ExternalTool implements Serializable {

private static final Logger logger = Logger.getLogger(ExternalToolServiceBean.class.getCanonicalName());

public static final String DISPLAY_NAME = "displayName";
public static final String DESCRIPTION = "description";
public static final String LEGACY_SINGLE_TYPE = "type";
Expand All @@ -41,6 +38,7 @@ public class ExternalTool implements Serializable {
public static final String TOOL_PARAMETERS = "toolParameters";
public static final String CONTENT_TYPE = "contentType";
public static final String TOOL_NAME = "toolName";
public static final String ALLOWED_API_CALLS = "allowedApiCalls";

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down Expand Up @@ -97,6 +95,14 @@ public class ExternalTool implements Serializable {
@Column(nullable = true, columnDefinition = "TEXT")
private String contentType;

/**
* Set of API calls the tool would like to be able to use (e,.g. for retrieving
* data through the Dataverse REST api). Used to build signedUrls for POST
* headers, as in DPCreator
*/
@Column(nullable = true, columnDefinition = "TEXT")
private String allowedApiCalls;

/**
* This default constructor is only here to prevent this error at
* deployment:
Expand All @@ -122,6 +128,18 @@ public ExternalTool(String displayName, String toolName, String description, Lis
this.contentType = contentType;
}

public ExternalTool(String displayName, String toolName, String description, List<ExternalToolType> externalToolTypes, Scope scope, String toolUrl, String toolParameters, String contentType, String allowedApiCalls) {
this.displayName = displayName;
this.toolName = toolName;
this.description = description;
this.externalToolTypes = externalToolTypes;
this.scope = scope;
this.toolUrl = toolUrl;
this.toolParameters = toolParameters;
this.contentType = contentType;
this.allowedApiCalls = allowedApiCalls;
}

public enum Type {

EXPLORE("explore"),
Expand Down Expand Up @@ -273,64 +291,10 @@ public JsonObjectBuilder toJson() {
if (getContentType() != null) {
jab.add(CONTENT_TYPE, getContentType());
}
return jab;
}

public enum ReservedWord {

// TODO: Research if a format like "{reservedWord}" is easily parse-able or if another format would be
// better. The choice of curly braces is somewhat arbitrary, but has been observed in documenation for
// various REST APIs. For example, "Variable substitutions will be made when a variable is named in {brackets}."
// from https://swagger.io/specification/#fixed-fields-29 but that's for URLs.
FILE_ID("fileId"),
FILE_PID("filePid"),
SITE_URL("siteUrl"),
API_TOKEN("apiToken"),
// datasetId is the database id
DATASET_ID("datasetId"),
// datasetPid is the DOI or Handle
DATASET_PID("datasetPid"),
DATASET_VERSION("datasetVersion"),
FILE_METADATA_ID("fileMetadataId"),
LOCALE_CODE("localeCode");

private final String text;
private final String START = "{";
private final String END = "}";

private ReservedWord(final String text) {
this.text = START + text + END;
}

/**
* This is a centralized method that enforces that only reserved words
* are allowed to be used by external tools. External tool authors
* cannot pass their own query parameters through Dataverse such as
* "mode=mode1".
*
* @throws IllegalArgumentException
*/
public static ReservedWord fromString(String text) throws IllegalArgumentException {
if (text != null) {
for (ReservedWord reservedWord : ReservedWord.values()) {
if (text.equals(reservedWord.text)) {
return reservedWord;
}
}
}
// TODO: Consider switching to a more informative message that enumerates the valid reserved words.
boolean moreInformativeMessage = false;
if (moreInformativeMessage) {
throw new IllegalArgumentException("Unknown reserved word: " + text + ". A reserved word must be one of these values: " + Arrays.asList(ReservedWord.values()) + ".");
} else {
throw new IllegalArgumentException("Unknown reserved word: " + text);
}
}

@Override
public String toString() {
return text;
if (getAllowedApiCalls()!= null) {
jab.add(ALLOWED_API_CALLS,getAllowedApiCalls());
}
return jab;
}

public String getDescriptionLang() {
Expand All @@ -355,5 +319,19 @@ public String getDisplayNameLang() {
return displayName;
}

/**
* @return the allowedApiCalls
*/
public String getAllowedApiCalls() {
return allowedApiCalls;
}

/**
* @param allowedApiCalls the allowedApiCalls to set
*/
public void setAllowedApiCalls(String allowedApiCalls) {
this.allowedApiCalls = allowedApiCalls;
}


}
Loading