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

WIP: Passports #2237

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
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 @@ -66,6 +66,7 @@
import org.broadinstitute.consent.http.resources.MetricsResource;
import org.broadinstitute.consent.http.resources.NihAccountResource;
import org.broadinstitute.consent.http.resources.OAuth2Resource;
import org.broadinstitute.consent.http.resources.PassportResource;
import org.broadinstitute.consent.http.resources.SamResource;
import org.broadinstitute.consent.http.resources.SchemaResource;
import org.broadinstitute.consent.http.resources.StatusResource;
Expand Down Expand Up @@ -225,6 +226,7 @@ public void run(ConsentConfiguration config, Environment env) {
env.jersey().register(new MatchResource(matchService));
env.jersey().register(new MetricsResource(metricsService));
env.jersey().register(new NihAccountResource(nihService, userService));
env.jersey().register(injector.getInstance(PassportResource.class));
env.jersey().register(new SamResource(samService, userService));
env.jersey().register(new SchemaResource());
env.jersey().register(new SwaggerResource(config.getGoogleAuthentication()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.broadinstitute.consent.http.models.passport;

import java.util.Optional;
import org.broadinstitute.consent.http.models.LibraryCard;
import org.broadinstitute.consent.http.models.User;

/**
* https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#affiliationandrole
*/
public class AffiliationAndRole implements VisaClaimType {

private final User user;

public AffiliationAndRole(User user) {
this.user = user;
}

@Override
public String type() {
return "AffiliationAndRole";
}

@Override
public Integer asserted() {
if (user.getLibraryCards() != null) {
Optional<LibraryCard> maybeLc = user.getLibraryCards().stream().findFirst();
return maybeLc
.map(libraryCard -> Long.valueOf(libraryCard.getCreateDate().getTime()).intValue())
.orElseGet(() -> Long.valueOf(user.getCreateDate().getTime()).intValue());
}
return null;
}

// TODO Look for a better way to get the user's institutional domain. This is
// not captured currently in our Institution model so we use email domain as
// a proxy for institutional domain
@Override
public String value() {
String[] splitEmail = user.getEmail().split("@");
if (splitEmail.length > 1) {
String domain = splitEmail[splitEmail.length - 1];
return String.format("duos.researcher@%s", domain);
}
return "[email protected]";
}

@Override
public String source() {
return "https://duos.org/";
}

@Override
public String by() {
if (user.getLibraryCards() == null || user.getLibraryCards().isEmpty()) {
return VisaBy.system.name();
}
return VisaBy.so.name();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.broadinstitute.consent.http.models.passport;

import org.broadinstitute.consent.http.models.ApprovedDataset;

/**
* https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#controlledaccessgrants
*/
public class ControlledAccessGrants implements VisaClaimType {

private final ApprovedDataset approvedDataset;

public ControlledAccessGrants(ApprovedDataset approvedDataset) {
this.approvedDataset = approvedDataset;
}

@Override
public String type() {
return "ControlledAccessGrants";
}

@Override
public Integer asserted() {
if (approvedDataset.getApprovalDate() != null) {
return Long.valueOf(approvedDataset.getApprovalDate().getTime()).intValue();
}
return null;
}

@Override
public String value() {
return String.format("https://duos.org/dataset/%s", approvedDataset.getDatasetIdentifier());
}

@Override
public String source() {
return approvedDataset.getDacName();
}

@Override
public String by() {
return VisaBy.dac.name();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.broadinstitute.consent.http.models.passport;

import java.util.List;

public record PassportClaim(List<Visa> ga4gh_passport_v1) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.broadinstitute.consent.http.models.passport;

public record Visa(String iss, String sub, Integer iat, Integer exp, VisaClaim ga4gh_visa_v1) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.broadinstitute.consent.http.models.passport;

public enum VisaBy {
dac, self, so, system
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.broadinstitute.consent.http.models.passport;

public record VisaClaim(
String type,
Integer asserted,
String value,
String source,
String by) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.broadinstitute.consent.http.models.passport;

import java.util.List;

public interface VisaClaimType {
String type();
Integer asserted();
String value();
String source();
String by();
default List<VisaCondition> conditions() {
return List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.broadinstitute.consent.http.models.passport;

public record VisaCondition(VisaClaimType type, String value, String source, VisaBy by) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.broadinstitute.consent.http.resources;

import com.google.inject.Inject;
import io.dropwizard.auth.Auth;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.broadinstitute.consent.http.models.AuthUser;
import org.broadinstitute.consent.http.models.passport.PassportClaim;
import org.broadinstitute.consent.http.service.PassportService;

@Path("/api/passport")
public class PassportResource extends Resource {

private final PassportService passportService;

@Inject
public PassportResource(PassportService passportService) {
this.passportService = passportService;
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
public Response getPassport(@Auth AuthUser authUser) {
try {
PassportClaim passport = passportService.generatePassport(authUser);
return Response.ok().entity(passport).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.broadinstitute.consent.http.service;

import com.google.inject.Inject;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.broadinstitute.consent.http.db.DatasetDAO;
import org.broadinstitute.consent.http.db.InstitutionDAO;
import org.broadinstitute.consent.http.db.LibraryCardDAO;
import org.broadinstitute.consent.http.db.SamDAO;
import org.broadinstitute.consent.http.db.UserDAO;
import org.broadinstitute.consent.http.models.ApprovedDataset;
import org.broadinstitute.consent.http.models.AuthUser;
import org.broadinstitute.consent.http.models.Institution;
import org.broadinstitute.consent.http.models.LibraryCard;
import org.broadinstitute.consent.http.models.User;
import org.broadinstitute.consent.http.models.passport.AffiliationAndRole;
import org.broadinstitute.consent.http.models.passport.ControlledAccessGrants;
import org.broadinstitute.consent.http.models.passport.PassportClaim;
import org.broadinstitute.consent.http.models.passport.Visa;
import org.broadinstitute.consent.http.models.passport.VisaClaim;
import org.broadinstitute.consent.http.models.passport.VisaClaimType;
import org.broadinstitute.consent.http.models.sam.UserStatusDiagnostics;

public class PassportService {

public static final String ISS = "http://duos.org";

private final DatasetDAO datasetDAO;
private final InstitutionDAO institutionDAO;
private final LibraryCardDAO libraryCardDAO;
private final UserDAO userDAO;
private final SamDAO samDAO;

@Inject
public PassportService(DatasetDAO datasetDAO, InstitutionDAO institutionDAO, LibraryCardDAO libraryCardDAO, UserDAO userDAO, SamDAO samDAO) {
this.datasetDAO = datasetDAO;
this.institutionDAO = institutionDAO;
this.libraryCardDAO = libraryCardDAO;
this.userDAO = userDAO;
this.samDAO = samDAO;
}

// TODO: Flesh out:
// * AcceptedTermsAndPolicies
// * ResearcherStatus
// * LinkedIdentities
public PassportClaim generatePassport(AuthUser authUser) throws Exception {
User user = userDAO.findUserByEmail(authUser.getEmail());
// TODO: We need a fully fleshed out user with Library Cards, Institution, etc.
if (user == null) {
return new PassportClaim(List.of());
}
List<LibraryCard> libraryCards = libraryCardDAO.findLibraryCardsByUserId(user.getUserId());
user.setLibraryCards(libraryCards);
if (user.getInstitutionId() != null) {
Institution i = institutionDAO.findInstitutionById(user.getInstitutionId());
user.setInstitution(i);
}

// Affiliation and Role
Visa roleVisa = buildAffiliationAndRoleVisa(authUser, user);

// Controlled Access Grants
List<ApprovedDataset> approvedDatasets = datasetDAO.getApprovedDatasets(user.getUserId());
List<Visa> grantVisas = buildControlledAccessGrants(user, approvedDatasets);

List<Visa> allVisas = Stream.of(grantVisas, List.of(roleVisa)).flatMap(List::stream).toList();
return new PassportClaim(allVisas);
}

protected List<Visa> buildControlledAccessGrants(User user, List<ApprovedDataset> approvedDatasets) {
return approvedDatasets
.stream()
// A user can be approved for a dataset on multiple DARs so filter them here.
.filter(distinctByKey(ApprovedDataset::getDatasetIdentifier))
.map(d -> {
VisaClaimType grant = new ControlledAccessGrants(d);
VisaClaim claim = new VisaClaim(grant.type(), grant.asserted(), grant.value(), grant.source(), grant.by());
Instant now = Instant.now();
Integer iat = Long.valueOf(now.toEpochMilli()).intValue();
Integer exp = Long.valueOf(now.plusSeconds(3600).toEpochMilli()).intValue();
return new Visa(ISS, user.getEmail(), iat, exp, claim);
}).toList();
}

private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}

// TODO: We need the Sam user id which is not collected in any current Sam call that
// Consent makes ... look into whatever endpoint provides that info and build it out
// so we can create a valid SUB value.
// Look at https://sam.dsde-dev.broadinstitute.org/api/users/v2/self
// which gives us a little more info:
// {
// "allowed": <boolean>,
// "azureB2CId": "<azure UUID>",
// "createdAt": "1970-01-01T00:00:00Z",
// "email": "<email>",
// "googleSubjectId": "<google id>",
// "id": "103740509117808340318", <--- I think this is the new Sam ID
// "updatedAt": "2023-09-20T16:26:40.930521Z"
//}
protected Visa buildAffiliationAndRoleVisa(AuthUser authUser, User user) throws Exception {
UserStatusDiagnostics samUser = samDAO.getSelfDiagnostics(authUser);
VisaClaimType affiliationAndRole = new AffiliationAndRole(user);
VisaClaim affiliationClaim = new VisaClaim(affiliationAndRole.type(), affiliationAndRole.asserted(), affiliationAndRole.value(), affiliationAndRole.source(), affiliationAndRole.by());
Instant now = Instant.now();
Integer iat = Long.valueOf(now.toEpochMilli()).intValue();
Integer exp = Long.valueOf(now.plusSeconds(3600).toEpochMilli()).intValue();
return new Visa(ISS, user.getEmail(), iat, exp, affiliationClaim);
}
}
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 @@ -716,6 +716,8 @@ paths:
description: Server error.
/api/match/purpose/batch/:
$ref: './paths/getMatchesForLatestDataAccessElectionsByPurposeIds.yaml'
/api/passport:
$ref: './paths/passport.yaml'
/api/user:
$ref: './paths/user.yaml'
/api/user/me:
Expand Down
15 changes: 15 additions & 0 deletions src/main/resources/assets/paths/passport.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
get:
summary: Get Passport
description: |
Get a DUOS Passport that adheres to the [GA4GH Passport spec](https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md)
tags:
- Passport
responses:
200:
description: A Passport Object representing the authenticated user's known DUOS visas
content:
application/json:
schema:
$ref: '../schemas/PassportClaim.yaml'
400:
description: Bad Request. Make sure limit and offset are not negative numbers.
9 changes: 9 additions & 0 deletions src/main/resources/assets/schemas/PassportClaim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type: object
description: |
A [GA4GH Passport Claim](https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#passport-claim)
that contains an array of Visas
properties:
ga4gh_passport_v1:
type: array
items:
$ref: './Visa.yaml'
17 changes: 17 additions & 0 deletions src/main/resources/assets/schemas/Visa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type: object
description: A wrapper object containing a Visa and Visa metadata
properties:
iss:
type: string
description: The Issuer, part of the [Visa Identity](https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#visa-identity)
sub:
type: string
description: The Subject, part of the [Visa Identity](https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#visa-identity)
iat:
type: number
description: The Issued-At timestamp
exp:
type: number
description: The Expiration timestamp
ga4gh_visa_v1:
$ref: './VisaClaim.yaml'
Loading
Loading