Skip to content
103 changes: 92 additions & 11 deletions src/main/java/edu/harvard/iq/dataverse/api/Access.java

Large diffs are not rendered by default.

444 changes: 380 additions & 64 deletions src/main/java/edu/harvard/iq/dataverse/api/Admin.java

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/ApiConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

import jakarta.ws.rs.ApplicationPath;

import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeIn;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("api/v1")
@SecurityScheme(
securitySchemeName = "DataverseApiKey",
type = SecuritySchemeType.APIKEY,
in = SecuritySchemeIn.HEADER,
apiKeyName = "X-Dataverse-key",
description = "Dataverse API token."
)
public class ApiConfiguration extends ResourceConfig {

public ApiConfiguration() {
Expand Down
41 changes: 38 additions & 3 deletions src/main/java/edu/harvard/iq/dataverse/api/BatchImport.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

@Stateless
@Path("batch")
@SecurityRequirement(name = "DataverseApiKey")
@Tag(name = "Admin", description = "Administrative Dataverse operations.")
public class BatchImport extends AbstractApiBean {

@EJB
Expand All @@ -48,7 +55,17 @@ public class BatchImport extends AbstractApiBean {
@GET
@AuthRequired
@Path("harvest")
public Response harvest(@Context ContainerRequestContext crc, @QueryParam("path") String fileDir, @QueryParam("dv") String parentIdtf, @QueryParam("createDV") Boolean createDV, @QueryParam("key") String apiKey) throws IOException {
@Operation(summary = "Starts a harvest batch import",
description = "Starts a background harvest import from a server-side file or directory into the specified dataverse.")
public Response harvest(@Context ContainerRequestContext crc,
@Parameter(description = "Server-side file or directory path to import.", required = true)
@QueryParam("path") String fileDir,
@Parameter(description = "Target dataverse id or alias; defaults to root when omitted.")
@QueryParam("dv") String parentIdtf,
@Parameter(description = "Create the target dataverse when it does not exist.")
@QueryParam("createDV") Boolean createDV,
@Parameter(hidden = true)
@QueryParam("key") String apiKey) throws IOException {
try {
return startBatchJob(getRequestAuthenticatedUserOrDie(crc), fileDir, parentIdtf, apiKey, ImportType.HARVEST, createDV);
} catch (WrappedResponse wr) {
Expand All @@ -67,7 +84,15 @@ public Response harvest(@Context ContainerRequestContext crc, @QueryParam("path"
@POST
@AuthRequired
@Path("import")
public Response postImport(@Context ContainerRequestContext crc, String body, @QueryParam("dv") String parentIdtf, @QueryParam("key") String apiKey) {
@Operation(summary = "Imports a dataset from DDI XML",
description = "Imports a new dataset into the specified dataverse from DDI XML supplied in the request body.")
public Response postImport(@Context ContainerRequestContext crc,
@RequestBody(description = "DDI XML dataset metadata to import as a new dataset.")
String body,
@Parameter(description = "Target dataverse id or alias; defaults to root when omitted.")
@QueryParam("dv") String parentIdtf,
@Parameter(hidden = true)
@QueryParam("key") String apiKey) {

DataverseRequest dataverseRequest;
try {
Expand Down Expand Up @@ -105,7 +130,17 @@ public Response postImport(@Context ContainerRequestContext crc, String body, @Q
@GET
@AuthRequired
@Path("import")
public Response getImport(@Context ContainerRequestContext crc, @QueryParam("path") String fileDir, @QueryParam("dv") String parentIdtf, @QueryParam("createDV") Boolean createDV, @QueryParam("key") String apiKey) {
@Operation(summary = "Starts a dataset batch import",
description = "Starts a background import of one or more datasets from a server-side file or directory into the specified dataverse.")
public Response getImport(@Context ContainerRequestContext crc,
@Parameter(description = "Server-side file or directory path to import.", required = true)
@QueryParam("path") String fileDir,
@Parameter(description = "Target dataverse id or alias; defaults to root when omitted.")
@QueryParam("dv") String parentIdtf,
@Parameter(description = "Create the target dataverse when it does not exist.")
@QueryParam("createDV") Boolean createDV,
@Parameter(hidden = true)
@QueryParam("key") String apiKey) {
try {
return startBatchJob(getRequestAuthenticatedUserOrDie(crc), fileDir, parentIdtf, apiKey, ImportType.NEW, createDV);
} catch (WrappedResponse wr) {
Expand Down
47 changes: 43 additions & 4 deletions src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@
import jakarta.ws.rs.core.Response.Status;
import java.util.Date;
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

/**
* REST API bean for managing {@link BuiltinUser}s.
*
* @author michael
*/
@Path("builtin-users")
@Tag(name = "Users", description = "User account and authenticated user operations.")
public class BuiltinUsers extends AbstractApiBean {

private static final Logger logger = Logger.getLogger(BuiltinUsers.class.getName());
Expand All @@ -45,7 +50,13 @@ public class BuiltinUsers extends AbstractApiBean {

@GET
@Path("{username}/api-token")
public Response getApiToken( @PathParam("username") String username, @QueryParam("password") String password ) {
@Operation(summary = "Returns a builtin user's API token",
description = "Returns the API token for a builtin user when token lookup is enabled and the supplied password is valid.")
public Response getApiToken(
@Parameter(description = "Builtin username whose API token is requested.", required = true)
@PathParam("username") String username,
@Parameter(description = "Builtin account password used to verify token lookup.")
@QueryParam("password") String password) {
boolean disabled = true;
boolean lookupAllowed = settingsSvc.isTrueForKey(SettingsServiceBean.Key.AllowApiTokenLookupViaApi, false);
if (lookupAllowed) {
Expand Down Expand Up @@ -80,7 +91,17 @@ public Response getApiToken( @PathParam("username") String username, @QueryParam
//and use the values to create BuiltinUser/AuthenticatedUser.
//--MAD 4.9.3
@POST
public Response save(BuiltinUser user, @QueryParam("password") String password, @QueryParam("key") String key, @QueryParam("sendEmailNotification") Boolean sendEmailNotification) {
@Operation(summary = "Creates a builtin user",
description = "Creates a builtin user, matching authenticated user, and API token when the builtin-users management key is valid.")
public Response save(
@RequestBody(description = "Builtin user account data used to create the builtin and authenticated user records.")
BuiltinUser user,
@Parameter(description = "Password assigned to the builtin user.")
@QueryParam("password") String password,
@Parameter(description = "Builtin-users management key required to create the account.")
@QueryParam("key") String key,
@Parameter(description = "Send a create-account email notification to the new user.")
@QueryParam("sendEmailNotification") Boolean sendEmailNotification) {
if( sendEmailNotification == null )
sendEmailNotification = true;

Expand All @@ -100,7 +121,15 @@ public Response save(BuiltinUser user, @QueryParam("password") String password,
*/
@POST
@Path("{password}/{key}")
public Response create(BuiltinUser user, @PathParam("password") String password, @PathParam("key") String key) {
@Operation(summary = "Creates a builtin user with path credentials",
description = "Creates a builtin user, matching authenticated user, and API token using password and management key path values.")
public Response create(
@RequestBody(description = "Builtin user account data used to create the builtin and authenticated user records.")
BuiltinUser user,
@Parameter(description = "Password assigned to the builtin user.", required = true)
@PathParam("password") String password,
@Parameter(description = "Builtin-users management key required to create the account.", required = true)
@PathParam("key") String key) {
return internalSave(user, password, key);
}

Expand All @@ -117,7 +146,17 @@ public Response create(BuiltinUser user, @PathParam("password") String password,
*/
@POST
@Path("{password}/{key}/{sendEmailNotification}")
public Response createWithNotification(BuiltinUser user, @PathParam("password") String password, @PathParam("key") String key, @PathParam("sendEmailNotification") Boolean sendEmailNotification) {
@Operation(summary = "Creates a builtin user with notification control",
description = "Creates a builtin user, matching authenticated user, and API token while controlling whether a create-account notification is sent.")
public Response createWithNotification(
@RequestBody(description = "Builtin user account data used to create the builtin and authenticated user records.")
BuiltinUser user,
@Parameter(description = "Password assigned to the builtin user.", required = true)
@PathParam("password") String password,
@Parameter(description = "Builtin-users management key required to create the account.", required = true)
@PathParam("key") String key,
@Parameter(description = "Send a create-account email notification to the new user.", required = true)
@PathParam("sendEmailNotification") Boolean sendEmailNotification) {
return internalSave(user, password, key, sendEmailNotification);
}

Expand Down
15 changes: 13 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/api/DataTagsAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

/**
*
* @author Naomi
*/
@Stateless
@Path("datatags")
@Tag(name = "Datasets", description = "Dataset metadata, versions, files, and publishing operations.")
public class DataTagsAPI extends AbstractApiBean {

private static final String TAGGING_SERVER_ENDPOINT = "http://datatags.org/api/1/interviewLink";
Expand Down Expand Up @@ -76,7 +81,13 @@ public String getCallbackURL() {

@POST
@Path("receiveTags/{uniqueCacheId}")
public Response receiveTags(JsonObject tags, @PathParam("uniqueCacheId") String uniqueCacheId) {
@Operation(summary = "Receives DataTags results",
description = "Stores returned DataTags JSON in the callback cache and returns the Dataverse redirect URL.")
public Response receiveTags(
@RequestBody(description = "DataTags result JSON returned by the external tagging interview.")
JsonObject tags,
@Parameter(description = "Callback cache identifier associated with the DataTags interview.", required = true)
@PathParam("uniqueCacheId") String uniqueCacheId) {

// store json tags in the DataTagsContainer holding the dataset name
container.setTag(tags);
Expand All @@ -86,4 +97,4 @@ public Response receiveTags(JsonObject tags, @PathParam("uniqueCacheId") String
return ok( USER_REDIRECT_URL );
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

@Path("admin/datasetfield")
@Tag(name = "Dataset Fields", description = "Dataset field type, controlled vocabulary, and metadata block loading operations.")
public class DatasetFieldServiceApi extends AbstractApiBean {

@EJB
Expand All @@ -72,6 +77,8 @@
private static final Logger logger = Logger.getLogger(DatasetFieldServiceApi.class.getName());

@GET
@Operation(summary = "Lists dataset field groupings",
description = "Returns dataset field type names grouped by parent relationship, multiplicity, and required status.")
public Response getAll() {
try {
List<String> listOfIsHasParentsTrue = new ArrayList<>();
Expand Down Expand Up @@ -118,7 +125,11 @@

@GET
@Path("{name}")
public Response getByName(@PathParam("name") String name) {
@Operation(summary = "Returns a dataset field type",
description = "Returns a dataset field type with metadata block, Solr field names, parent relationship, multiplicity, required state, URI, and controlled vocabulary values.")
public Response getByName(
@Parameter(description = "Dataset field type name to return.", required = true)
@PathParam("name") String name) {
try {
DatasetFieldType dsf = datasetFieldService.findByName(name);
Long id = dsf.getId();
Expand Down Expand Up @@ -190,6 +201,8 @@
*/
@GET
@Path("controlledVocabulary/subject")
@Operation(summary = "Lists subject vocabulary values",
description = "Returns the configured controlled vocabulary display values for the subject dataset field.")
public Response showControlledVocabularyForSubject() {
DatasetFieldType subjectDatasetField = datasetFieldService.findByName(DatasetFieldConstant.subject);
JsonArrayBuilder possibleSubjects = Json.createArrayBuilder();
Expand All @@ -206,6 +219,8 @@
// TODO consider replacing with a @Startup method on the datasetFieldServiceBean
@GET
@Path("loadNAControlledVocabularyValue")
@Operation(summary = "Creates the N/A controlled vocabulary value",
description = "Creates the global N/A controlled vocabulary value when it is missing and reports whether it was created or already existed.")
public Response loadNAControlledVocabularyValue() {
// the find will throw a NoResultException if no values are in db
// datasetFieldService.findNAControlledVocabularyValue();
Expand All @@ -226,11 +241,15 @@
public enum HeaderType {
METADATABLOCK, DATASETFIELD, CONTROLLEDVOCABULARY
}

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
@POST
@Consumes("text/tab-separated-values")
@Path("load")
public Response loadDatasetFields(File file) {
@Operation(summary = "Loads dataset field definitions",
description = "Reads tab-separated metadata block, dataset field, and controlled vocabulary definitions from the uploaded file path and creates or updates those definitions.")
public Response loadDatasetFields(
@RequestBody(description = "Tab-separated definitions for metadata blocks, dataset fields, and controlled vocabulary values.")
File file) {
ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "loadDatasetFields");
alr.setInfo( file.getName() );
BufferedReader br = null;
Expand Down Expand Up @@ -489,7 +508,11 @@
@POST
@Consumes("application/zip")
@Path("loadpropertyfiles")
public Response loadLanguagePropertyFile(File inputFile) {
@Operation(summary = "Loads language property files",
description = "Extracts uploaded language property files into the configured Dataverse language directory while rejecting ZIP entries outside that directory.")
public Response loadLanguagePropertyFile(
@RequestBody(description = "ZIP archive containing language property files to extract.")
File inputFile) {
try (ZipFile file = new ZipFile(inputFile)) {
//Get file entries
Enumeration<? extends ZipEntry> entries = file.entries();
Expand Down Expand Up @@ -551,7 +574,13 @@
*/
@POST
@Path("/setDisplayOnCreate")
public Response setDisplayOnCreate(@QueryParam("datasetFieldType") String datasetFieldTypeIn, @QueryParam("setDisplayOnCreate") Boolean setDisplayOnCreateIn) {
@Operation(summary = "Sets dataset field create-page display",
description = "Updates whether the specified dataset field type is displayed on the dataset creation page.")
public Response setDisplayOnCreate(
@Parameter(description = "Dataset field type name to update.", required = true)
@QueryParam("datasetFieldType") String datasetFieldTypeIn,
@Parameter(description = "New display-on-create setting for the dataset field type.", required = true)
@QueryParam("setDisplayOnCreate") Boolean setDisplayOnCreateIn) {
DatasetFieldType dft = datasetFieldService.findByName(datasetFieldTypeIn);
if (dft == null) {
return error(Status.NOT_FOUND, "Cound not find a DatasetFieldType by looking up " + datasetFieldTypeIn);
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/DatasetFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import jakarta.ws.rs.core.Response;

import java.util.List;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonDatasetFieldTypes;

Expand All @@ -15,13 +17,16 @@
*/
@Path("datasetfields")
@Produces("application/json")
@Tag(name = "Dataset Fields", description = "Dataset field type, controlled vocabulary, and metadata block loading operations.")
public class DatasetFields extends AbstractApiBean {

@EJB
DatasetFieldServiceBean datasetFieldService;

@GET
@Path("facetables")
@Operation(summary = "Lists facetable dataset fields",
description = "Lists all facetable dataset fields defined in the installation.")
public Response listAllFacetableDatasetFields() {
List<DatasetFieldType> datasetFieldTypes = datasetFieldService.findAllFacetableFieldTypes();
return ok(jsonDatasetFieldTypes(datasetFieldTypes));
Expand Down
Loading
Loading