Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -5,6 +5,7 @@
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
Expand Down Expand Up @@ -34,4 +35,27 @@ public Response getPassport(@Auth DuosUser duosUser) {
return createExceptionResponse(e);
}
}

/**
* Returns a Data Passport for a given dataset identifier, as proposed in the GA4GH Data Passports
* specification. The response uses the same {@code ga4gh_passport_v1} envelope as a Researcher
* Passport but contains dataset-centric visas: {@code ConsentedDataUseTerms}, {@code
* OversightBodies}, and {@code RequiredAgreements} (when a DAA exists).
*
* @param datasetIdentifier the formatted DUOS identifier, e.g. {@code DUOS-000001}
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({ADMIN})
@Path("dataset/{datasetIdentifier}")
public Response getDataPassport(
@SuppressWarnings("unused") @Auth DuosUser duosUser,
@PathParam("datasetIdentifier") String datasetIdentifier) {
try {
PassportClaim passport = passportService.generateDataPassport(datasetIdentifier);
return Response.ok().entity(passport).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public Long asserted() {
Optional.ofNullable(user.getLibraryCard())
.map(LibraryCard::getCreateDate)
.orElse(user.getCreateDate());
return PassportService.getEpochSeconds(assertedDate.toInstant());
if (assertedDate == null) {
return PassportService.getEpochSeconds(java.time.Instant.now());
}
// java.sql.Date#toInstant throws UnsupportedOperationException; use epoch millis instead.
return PassportService.getEpochSeconds(java.time.Instant.ofEpochMilli(assertedDate.getTime()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.broadinstitute.consent.http.service.passport;

import java.time.Instant;
import org.broadinstitute.consent.http.models.Dataset;

/**
* Data Passport visa encoding the permitted uses of a dataset based on participant consent,
* expressed as a link to the dataset's data use terms. Leverages the Data Use Ontology (DUO) for
* standardization.
*
* @see <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874">GA4GH Data Passports
* specification</a>
*/
public class ConsentedDataUseTermsVisa implements VisaClaimType {

private final Dataset dataset;

public ConsentedDataUseTermsVisa(Dataset dataset) {
this.dataset = dataset;
}

@Override
public String type() {
return VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type;
}

@Override
public Long asserted() {
if (dataset.getCreateDate() != null) {
// java.sql.Date#toInstant throws UnsupportedOperationException; use epoch millis instead.
return PassportService.getEpochSeconds(
Instant.ofEpochMilli(dataset.getCreateDate().getTime()));
}
return PassportService.getEpochSeconds(Instant.now());
}

/**
* Returns a stable URL pointing to the dataset identifier that describes the consented terms
* dereference this URL to retrieve the full DUO-coded data use object for the dataset.
*/
@Override
public String value() {
return "%s/dataset/%s".formatted(PassportService.ISS, dataset.getDatasetIdentifier());
}

@Override
public String source() {
return PassportService.ISS;
}

@Override
public String by() {
return VisaBy.DAC.name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.broadinstitute.consent.http.service.passport;

import java.time.Instant;
import java.util.Calendar;
import java.util.Date;
import org.broadinstitute.consent.http.models.ApprovedDataset;

/**
Expand Down Expand Up @@ -32,8 +32,8 @@ public Long asserted() {
calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) - 1);
return PassportService.getEpochSeconds(calendar.toInstant());
}
// If there is no expiration date, we will use the current time as the asserted time.
return PassportService.getEpochSeconds(new Date().toInstant());
// If there is no expiration date, use the current time as the asserted time.
return PassportService.getEpochSeconds(Instant.now());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.broadinstitute.consent.http.service.passport;

import java.time.Instant;
import org.broadinstitute.consent.http.models.Dac;

/**
* Data Passport visa describing the entity responsible for governing access to the dataset. Maps to
* the DAC (Data Access Committee) that oversees the dataset in DUOS.
*
* @see <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874">GA4GH Data Passports
* specification</a>
*/
public class OversightBodiesVisa implements VisaClaimType {

private final Dac dac;

public OversightBodiesVisa(Dac dac) {
this.dac = dac;
}

@Override
public String type() {
return VisaClaimTypes.OVERSIGHT_BODIES.type;
}

@Override
public Long asserted() {
if (dac.getCreateDate() != null) {
// java.sql.Date#toInstant throws UnsupportedOperationException; use epoch millis instead.
return PassportService.getEpochSeconds(Instant.ofEpochMilli(dac.getCreateDate().getTime()));
}
return PassportService.getEpochSeconds(Instant.now());
}

/**
* Returns a stable URL identifying the DAC within DUOS. Consumers can dereference this URL to
* retrieve details about the oversight body, including its members and chairpersons. TODO: We

Check warning on line 37 in src/main/java/org/broadinstitute/consent/http/service/passport/OversightBodiesVisa.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=DataBiosphere_consent&issues=AZ2OQITtZLn6M-rSpVBi&open=AZ2OQITtZLn6M-rSpVBi&pullRequest=2858
* need a public and stable identifier to point users to for DACs.
*/
@Override
public String value() {
return "%s/dac/%d".formatted(PassportService.ISS, dac.getDacId());
}

@Override
public String source() {
return PassportService.ISS;
}

@Override
public String by() {
return VisaBy.DAC.name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand All @@ -11,9 +12,12 @@
import java.util.stream.Stream;
import org.broadinstitute.consent.http.db.DatasetDAO;
import org.broadinstitute.consent.http.models.ApprovedDataset;
import org.broadinstitute.consent.http.models.Dac;
import org.broadinstitute.consent.http.models.Dataset;
import org.broadinstitute.consent.http.models.DuosUser;
import org.broadinstitute.consent.http.models.User;
import org.broadinstitute.consent.http.models.sam.UserStatusInfo;
import org.broadinstitute.consent.http.service.DacService;
import org.broadinstitute.consent.http.util.ConsentLogger;

/** <a href="https://ga4gh.github.io/data-security/ga4gh-passport">GA4GH Passport</a> */
Expand All @@ -23,10 +27,12 @@ public class PassportService implements ConsentLogger {
public static final int EXPIRATION_SECONDS = 3600;

private final DatasetDAO datasetDAO;
private final DacService dacService;

@Inject
public PassportService(DatasetDAO datasetDAO) {
public PassportService(DatasetDAO datasetDAO, DacService dacService) {
this.datasetDAO = datasetDAO;
this.dacService = dacService;
}

public PassportClaim generatePassport(DuosUser duosUser) {
Expand Down Expand Up @@ -57,6 +63,65 @@ public PassportClaim generatePassport(DuosUser duosUser) {
return new PassportClaim(allVisas);
}

/**
* Generates a Data Passport for a specific dataset, as proposed in the GA4GH Data Passports
* specification (see https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874). The returned
* {@link PassportClaim} uses the same envelope as a Researcher Passport but contains
* dataset-centric visas:
*
* <ul>
* <li>{@link ConsentedDataUseTermsVisa} — links to the dataset's DUO-coded data use terms
* <li>{@link OversightBodiesVisa} — identifies the DAC governing the dataset
* <li>{@link RequiredAgreementsVisa} — references the DAA users must accept (if one exists)
* </ul>
*
* <p>The {@code sub} field of each visa is the dataset identifier (e.g. {@code DUOS-000001})
* rather than a user subject ID, reflecting the dataset-centric nature of the passport.
*
* @param datasetIdentifier the formatted DUOS identifier, e.g. {@code DUOS-000001}
* @return a {@link PassportClaim} containing the Data Passport visas for the dataset
* @throws NotFoundException if the dataset does not exist
*/
public PassportClaim generateDataPassport(String datasetIdentifier) {
Integer alias = Dataset.parseIdentifierToAlias(datasetIdentifier);
Dataset dataset = datasetDAO.findDatasetByAlias(alias);
if (dataset == null) {
throw new NotFoundException("Dataset not found: " + datasetIdentifier);
}

List<Visa> visas = new ArrayList<>();

// ConsentedDataUseTerms — always present if the dataset exists
visas.add(visaFromVisaClaimType(datasetIdentifier, new ConsentedDataUseTermsVisa(dataset)));

// OversightBodies + RequiredAgreements — only when the dataset is associated with a DAC
if (dataset.getDacId() != null) {
try {
Dac dac = dacService.findById(dataset.getDacId());
addDacBackedVisas(datasetIdentifier, visas, dac);
} catch (UnsupportedOperationException e) {
logWarn(
"Unable to build DAC-backed visas for dataset %s; returning consented-data-use visa only"
.formatted(datasetIdentifier),
e);
}
}

return new PassportClaim(visas);
}

private void addDacBackedVisas(String datasetIdentifier, List<Visa> visas, Dac dac) {
if (dac == null) {
return;
}
visas.add(visaFromVisaClaimType(datasetIdentifier, new OversightBodiesVisa(dac)));
if (dac.getAssociatedDaa() != null) {
visas.add(
visaFromVisaClaimType(
datasetIdentifier, new RequiredAgreementsVisa(dac.getAssociatedDaa())));
}
}

protected List<Visa> buildControlledAccessGrants(
String userSubjectId, List<ApprovedDataset> approvedDatasets) {
return approvedDatasets.stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.broadinstitute.consent.http.service.passport;

import java.time.Instant;
import org.broadinstitute.consent.http.models.DataAccessAgreement;

/**
* Data Passport visa listing the Data Access Agreement (DAA) that users must accept in order to
* access the dataset. References the DAA document managed in DUOS by the DAC.
*
* @see <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874">GA4GH Data Passports
* specification</a>
*/
public class RequiredAgreementsVisa implements VisaClaimType {

private final DataAccessAgreement daa;

public RequiredAgreementsVisa(DataAccessAgreement daa) {
this.daa = daa;
}

@Override
public String type() {
return VisaClaimTypes.REQUIRED_AGREEMENTS.type;
}

@Override
public Long asserted() {
if (daa.getCreateDate() != null) {
return PassportService.getEpochSeconds(daa.getCreateDate());
}
return PassportService.getEpochSeconds(Instant.now());
}

/**
* Returns a stable URL pointing to the DAA within DUOS. Consumers can use this to retrieve the
* full agreement document and verify that a researcher's Library Card includes acceptance of this
* agreement before granting access.
*/
@Override
public String value() {
return "%s/daa/%d".formatted(PassportService.ISS, daa.getDaaId());
}

@Override
public String source() {
return PassportService.ISS;
}

@Override
public String by() {
return VisaBy.SO.name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ public Long asserted() {
Optional.ofNullable(user.getLibraryCard())
.map(LibraryCard::getCreateDate)
.orElse(user.getCreateDate());
return PassportService.getEpochSeconds(assertedDate.toInstant());
if (assertedDate == null) {
return PassportService.getEpochSeconds(java.time.Instant.now());
}
// java.sql.Date#toInstant throws UnsupportedOperationException; use epoch millis instead.
return PassportService.getEpochSeconds(java.time.Instant.ofEpochMilli(assertedDate.getTime()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package org.broadinstitute.consent.http.service.passport;

public enum VisaClaimTypes {
// GA4GH Researcher Passport visa types
AFFILIATION_AND_ROLE("AffiliationAndRole"),
CONTROLLED_ACCESS_GRANTS("ControlledAccessGrants"),
RESEARCHER_STATUS("ResearcherStatus");
RESEARCHER_STATUS("ResearcherStatus"),

// GA4GH Data Passport visa types (see
// https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874)
CONSENTED_DATA_USE_TERMS("ConsentedDataUseTerms"),
OVERSIGHT_BODIES("OversightBodies"),
REQUIRED_AGREEMENTS("RequiredAgreements");

public final String type;

Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/assets/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,8 @@ paths:
$ref: './paths/darSummariesByDatasetId.yaml'
/api/passport/userinfo:
$ref: './paths/passportUserInfo.yaml'
/api/passport/dataset/{datasetIdentifier}:
$ref: './paths/passportDataset.yaml'
/api/user:
$ref: './paths/user.yaml'
/api/user/me:
Expand Down
Loading
Loading