diff --git a/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java b/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java index 5bdce74ce9..5faf719de4 100644 --- a/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java +++ b/src/main/java/org/broadinstitute/consent/http/ConsentApplication.java @@ -69,6 +69,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; @@ -139,7 +140,8 @@ public static void main(String[] args) throws Exception { LOGGER.error("Unable to bootstrap sentry logging."); } } catch (Exception e) { - LOGGER.error(MessageFormat.format("Exception loading sentry properties: {0}", e.getMessage())); + LOGGER.error( + MessageFormat.format("Exception loading sentry properties: {0}", e.getMessage())); } new ConsentApplication().run(args); LOGGER.info("Consent Application Started"); @@ -163,7 +165,8 @@ public void run(ConsentConfiguration config, Environment env) { // Services final DarCollectionService darCollectionService = injector.getProvider( DarCollectionService.class).get(); - final DataAccessRequestService dataAccessRequestService = injector.getProvider(DataAccessRequestService.class).get(); + final DataAccessRequestService dataAccessRequestService = injector.getProvider( + DataAccessRequestService.class).get(); final DatasetService datasetService = injector.getProvider(DatasetService.class).get(); final ElectionService electionService = injector.getProvider(ElectionService.class).get(); final EmailService emailService = injector.getProvider(EmailService.class).get(); @@ -218,8 +221,7 @@ public void run(ConsentConfiguration config, Environment env) { // Register standard application resources. env.jersey().register(injector.getInstance(DaaResource.class)); env.jersey().register(injector.getInstance(DataAccessRequestResource.class)); - env.jersey().register(new DatasetResource(datasetService, userService, - datasetRegistrationService, elasticSearchService, tdrService, gcsService)); + env.jersey().register(injector.getInstance(DatasetResource.class)); env.jersey().register(injector.getInstance(DacResource.class)); env.jersey().register(injector.getInstance(DACAutomationRuleResource.class)); env.jersey().register(new DACUserResource(userService)); @@ -231,13 +233,15 @@ 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)); + 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())); env.jersey().register(new StatusResource(env.healthChecks())); env.jersey().register(injector.getInstance(SupportResource.class)); env.jersey().register( - new UserResource(samService, userService, datasetService, acknowledgementService, nihService)); + new UserResource(samService, userService, datasetService, acknowledgementService, + nihService)); env.jersey().register(new TosResource(samService)); env.jersey().register(injector.getInstance(VersionResource.class)); env.jersey().register(new VoteResource(userService, voteService, electionService)); @@ -251,12 +255,16 @@ public void run(ConsentConfiguration config, Environment env) { // Authentication filters final OAuthAuthenticator authenticator = injector.getProvider(OAuthAuthenticator.class).get(); - final DuosUserAuthenticator duosUserAuthenticator = injector.getProvider(DuosUserAuthenticator.class).get(); - final AuthorizationHelper authorizationHelper = injector.getProvider(AuthorizationHelper.class).get(); + final DuosUserAuthenticator duosUserAuthenticator = injector.getProvider( + DuosUserAuthenticator.class).get(); + final AuthorizationHelper authorizationHelper = injector.getProvider(AuthorizationHelper.class) + .get(); // Requests annotated with @Auth AuthUser will be authenticated through this filter - final AuthFilter primaryAuthFilter = new OAuthCustomAuthFilter<>(authenticator, authorizationHelper); + final AuthFilter primaryAuthFilter = new OAuthCustomAuthFilter<>( + authenticator, authorizationHelper); // Requests annotated with @Auth DuosUser will be authenticated through this filter and are guaranteed to have a populated User object - final AuthFilter duosAuthUserFilter = new OAuthCustomAuthFilter<>(duosUserAuthenticator, authorizationHelper); + final AuthFilter duosAuthUserFilter = new OAuthCustomAuthFilter<>( + duosUserAuthenticator, authorizationHelper); final PolymorphicAuthDynamicFeature feature = new PolymorphicAuthDynamicFeature<>( Map.of( AuthUser.class, primaryAuthFilter, diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/AcceptedTermsAndPolicies.java b/src/main/java/org/broadinstitute/consent/http/models/passport/AcceptedTermsAndPolicies.java new file mode 100644 index 0000000000..66d5f74d68 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/AcceptedTermsAndPolicies.java @@ -0,0 +1,32 @@ +package org.broadinstitute.consent.http.models.passport; + +import org.broadinstitute.consent.http.service.PassportService; + +public class AcceptedTermsAndPolicies implements VisaClaimType { + + @Override + public String type() { + return VisaClaimTypes.ACCEPTED_TERMS_AND_POLICIES.type; + } + + @Override + public Long asserted() { + return 0L; + } + + @Override + public String value() { + return ""; + } + + @Override + public String source() { + return PassportService.ISS; + } + + @Override + public String by() { + return VisaBy.SELF.name().toLowerCase(); + } + +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/AffiliationAndRole.java b/src/main/java/org/broadinstitute/consent/http/models/passport/AffiliationAndRole.java new file mode 100644 index 0000000000..13a309315c --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/AffiliationAndRole.java @@ -0,0 +1,57 @@ +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; +import org.broadinstitute.consent.http.service.PassportService; + +/** + * AffiliationAndRole + */ +public class AffiliationAndRole implements VisaClaimType { + + private final User user; + + public AffiliationAndRole(User user) { + this.user = user; + } + + @Override + public String type() { + return VisaClaimTypes.AFFILIATION_AND_ROLE.type; + } + + @Override + public Long asserted() { + var assertedDate = Optional.ofNullable(user.getLibraryCard()) + .map(LibraryCard::getCreateDate) + .orElse(user.getCreateDate()); + return assertedDate.getTime(); + } + + // TODO + // Is there a better way to get the user's singular institutional domain? + // Institutions can have multiple domains, e.g. "broadinstitute.org" and "broad.mit.edu". + @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 "duos.researcher@no.organization"; + } + + @Override + public String source() { + return PassportService.ISS; + } + + @Override + public String by() { + if (user.getLibraryCard() == null) { + return VisaBy.SYSTEM.name().toLowerCase(); + } + return VisaBy.SO.name().toLowerCase(); + } +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/ControlledAccessGrants.java b/src/main/java/org/broadinstitute/consent/http/models/passport/ControlledAccessGrants.java new file mode 100644 index 0000000000..cd16d79f90 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/ControlledAccessGrants.java @@ -0,0 +1,48 @@ +package org.broadinstitute.consent.http.models.passport; + +import java.util.Calendar; +import org.broadinstitute.consent.http.models.ApprovedDataset; +import org.broadinstitute.consent.http.service.PassportService; + +/** + * ControlledAccessGrants + */ +public class ControlledAccessGrants implements VisaClaimType { + + private final ApprovedDataset approvedDataset; + + public ControlledAccessGrants(ApprovedDataset approvedDataset) { + this.approvedDataset = approvedDataset; + } + + @Override + public String type() { + return VisaClaimTypes.CONTROLLED_ACCESS_GRANTS.type; + } + + @Override + public Long asserted() { + if (approvedDataset.getExpirationDate() != null) { + var calendar = Calendar.getInstance(); + calendar.setTime(approvedDataset.getExpirationDate()); + calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) - 1); + return calendar.getTimeInMillis(); + } + return null; + } + + @Override + public String value() { + return String.format("%s/dataset/%s", PassportService.ISS, approvedDataset.getDatasetIdentifier()); + } + + @Override + public String source() { + return approvedDataset.getDacName(); + } + + @Override + public String by() { + return VisaBy.DAC.name().toLowerCase(); + } +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/PassportClaim.java b/src/main/java/org/broadinstitute/consent/http/models/passport/PassportClaim.java new file mode 100644 index 0000000000..7bd77bc8b4 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/PassportClaim.java @@ -0,0 +1,10 @@ +package org.broadinstitute.consent.http.models.passport; + +import java.util.List; + +/** + * GA4GH Passport Claim + * @param ga4gh_passport_v1 List of Visa objects representing the user's claims + */ +public record PassportClaim(List ga4gh_passport_v1) { +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/ResearcherStatus.java b/src/main/java/org/broadinstitute/consent/http/models/passport/ResearcherStatus.java new file mode 100644 index 0000000000..2f78af9984 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/ResearcherStatus.java @@ -0,0 +1,47 @@ +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; +import org.broadinstitute.consent.http.service.PassportService; + +/** + * ResearcherStatus + */ +public class ResearcherStatus implements VisaClaimType { + + private final User user; + + public ResearcherStatus(User user) { + this.user = user; + } + + @Override + public String type() { + return VisaClaimTypes.RESEARCHER_STATUS.type; + } + + @Override + public Long asserted() { + var assertedDate = Optional.ofNullable(user.getLibraryCard()) + .map(LibraryCard::getCreateDate) + .orElse(user.getCreateDate()); + return assertedDate.getTime(); + } + + @Override + public String value() { + // TODO Collect public URL for the user's profile such as an ORCID or institutional profile. + return PassportService.ISS; + } + + @Override + public String source() { + return PassportService.ISS; + } + + @Override + public String by() { + return VisaBy.SO.name(); + } +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/Visa.java b/src/main/java/org/broadinstitute/consent/http/models/passport/Visa.java new file mode 100644 index 0000000000..c095b04ed2 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/Visa.java @@ -0,0 +1,4 @@ +package org.broadinstitute.consent.http.models.passport; + +public record Visa(String iss, String sub, Long iat, Long exp, VisaClaim ga4gh_visa_v1) { +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/VisaBy.java b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaBy.java new file mode 100644 index 0000000000..28694074b4 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaBy.java @@ -0,0 +1,5 @@ +package org.broadinstitute.consent.http.models.passport; + +public enum VisaBy { + DAC, SELF, SO, SYSTEM +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaim.java b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaim.java new file mode 100644 index 0000000000..9cea693d58 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaim.java @@ -0,0 +1,9 @@ +package org.broadinstitute.consent.http.models.passport; + +public record VisaClaim( + String type, + Long asserted, + String value, + String source, + String by) { +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaimType.java b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaimType.java new file mode 100644 index 0000000000..2876730b27 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaimType.java @@ -0,0 +1,14 @@ +package org.broadinstitute.consent.http.models.passport; + +import java.util.List; + +public interface VisaClaimType { + String type(); + Long asserted(); + String value(); + String source(); + String by(); + default List conditions() { + return List.of(); + } +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaimTypes.java b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaimTypes.java new file mode 100644 index 0000000000..b05124b0c1 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaClaimTypes.java @@ -0,0 +1,15 @@ +package org.broadinstitute.consent.http.models.passport; + +public enum VisaClaimTypes { + AFFILIATION_AND_ROLE ("AffiliationAndRole"), + CONTROLLED_ACCESS_GRANTS ("ControlledAccessGrants"), + RESEARCHER_STATUS ("ResearcherStatus"), + ACCEPTED_TERMS_AND_POLICIES("AcceptedTermsAndPolicies"), + ; + + public final String type; + + VisaClaimTypes(String type) { + this.type = type; + } +} diff --git a/src/main/java/org/broadinstitute/consent/http/models/passport/VisaCondition.java b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaCondition.java new file mode 100644 index 0000000000..6d5612695a --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/models/passport/VisaCondition.java @@ -0,0 +1,4 @@ +package org.broadinstitute.consent.http.models.passport; + +public record VisaCondition(VisaClaimType type, String value, String source, VisaBy by) { +} diff --git a/src/main/java/org/broadinstitute/consent/http/resources/PassportResource.java b/src/main/java/org/broadinstitute/consent/http/resources/PassportResource.java new file mode 100644 index 0000000000..bbd38b0f38 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/resources/PassportResource.java @@ -0,0 +1,36 @@ +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.DuosUser; +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 DuosUser duosUser) { + try { + PassportClaim passport = passportService.generatePassport(duosUser); + return Response.ok().entity(passport).build(); + } catch (Exception e) { + return createExceptionResponse(e); + } + } +} diff --git a/src/main/java/org/broadinstitute/consent/http/service/PassportService.java b/src/main/java/org/broadinstitute/consent/http/service/PassportService.java new file mode 100644 index 0000000000..78ceb37f08 --- /dev/null +++ b/src/main/java/org/broadinstitute/consent/http/service/PassportService.java @@ -0,0 +1,88 @@ +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.SamDAO; +import org.broadinstitute.consent.http.models.ApprovedDataset; +import org.broadinstitute.consent.http.models.DuosUser; +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.ResearcherStatus; +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.UserStatusInfo; +import org.broadinstitute.consent.http.util.ConsentLogger; + +/** + * GA4GH Passport + * TODO: + * AcceptedTermsAndPolicies + * LinkedIdentities + * Generate a JWT for the PassportClaim + */ +public class PassportService implements ConsentLogger { + + public static final String ISS = "https://duos.org"; + + private final DatasetDAO datasetDAO; + private final SamDAO samDAO; + + @Inject + public PassportService(DatasetDAO datasetDAO, SamDAO samDAO) { + this.datasetDAO = datasetDAO; + this.samDAO = samDAO; + } + + public PassportClaim generatePassport(DuosUser duosUser) throws Exception { + User user = duosUser.getUser(); + UserStatusInfo userStatusInfo = samDAO.getRegistrationInfo(duosUser); + // Affiliation and Role + Visa roleVisa = visaFromVisaClaimType(userStatusInfo, new AffiliationAndRole(user)); + + // Researcher Status + Visa researcherVisa = visaFromVisaClaimType(userStatusInfo, new ResearcherStatus(user)); + + // Controlled Access Grants + List approvedDatasets = datasetDAO.getApprovedDatasets(user.getUserId()); + List grantVisas = buildControlledAccessGrants(userStatusInfo, approvedDatasets); + + List allVisas = Stream.of(grantVisas, List.of(roleVisa), List.of(researcherVisa)) + .flatMap(List::stream).toList(); + PassportClaim claim = new PassportClaim(allVisas); + logInfo("Generated PassportClaim for user: " + user.getEmail() + ": " + claim); + return claim; + } + + protected List buildControlledAccessGrants(UserStatusInfo userStatusInfo, + List 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 -> visaFromVisaClaimType(userStatusInfo, new ControlledAccessGrants(d))).toList(); + } + + private static Predicate distinctByKey(Function keyExtractor) { + Set seen = ConcurrentHashMap.newKeySet(); + return t -> seen.add(keyExtractor.apply(t)); + } + + private Visa visaFromVisaClaimType(UserStatusInfo userStatusInfo, VisaClaimType type) { + VisaClaim claim = new VisaClaim(type.type(), type.asserted(), type.value(), type.source(), + type.by()); + Instant now = Instant.now(); + Long iat = now.toEpochMilli(); + Long exp = now.plusSeconds(3600).toEpochMilli(); + return new Visa(ISS, userStatusInfo.getUserSubjectId(), iat, exp, claim); + } +} diff --git a/src/main/resources/assets/api-docs.yaml b/src/main/resources/assets/api-docs.yaml index 8a01d804b9..96a04ff8b9 100644 --- a/src/main/resources/assets/api-docs.yaml +++ b/src/main/resources/assets/api-docs.yaml @@ -704,6 +704,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: diff --git a/src/main/resources/assets/paths/passport.yaml b/src/main/resources/assets/paths/passport.yaml new file mode 100644 index 0000000000..13e6f35480 --- /dev/null +++ b/src/main/resources/assets/paths/passport.yaml @@ -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. diff --git a/src/main/resources/assets/schemas/PassportClaim.yaml b/src/main/resources/assets/schemas/PassportClaim.yaml new file mode 100644 index 0000000000..3ae834bf84 --- /dev/null +++ b/src/main/resources/assets/schemas/PassportClaim.yaml @@ -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' \ No newline at end of file diff --git a/src/main/resources/assets/schemas/Visa.yaml b/src/main/resources/assets/schemas/Visa.yaml new file mode 100644 index 0000000000..c5ef642f5d --- /dev/null +++ b/src/main/resources/assets/schemas/Visa.yaml @@ -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' diff --git a/src/main/resources/assets/schemas/VisaClaim.yaml b/src/main/resources/assets/schemas/VisaClaim.yaml new file mode 100644 index 0000000000..2659bdf6cc --- /dev/null +++ b/src/main/resources/assets/schemas/VisaClaim.yaml @@ -0,0 +1,32 @@ +type: object +description: A GA4GH Visa Claim +properties: + type: + description: The type of claim as specified by the [GA4GH spec](https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#visa-type) + enum: + - AffiliationAndRole + - ControlledAccessGrants + - ResearcherStatus + - AcceptedTermsAndPolicies + - LinkedIdentities + asserted: + type: number + description: The Asserted timestamp + value: + type: string + description: A string value that conforms to the type as specified by the GA4GH spec + source: + type: string + description: A string source that conforms to the type as specified by the GA4GH spec + by: + description: The entity that is providing the authenticity of the claim + enum: + - dac + - self + - so + - system + conditions: + type: array + items: + $ref: './VisaCondition.yaml' + diff --git a/src/main/resources/assets/schemas/VisaCondition.yaml b/src/main/resources/assets/schemas/VisaCondition.yaml new file mode 100644 index 0000000000..b436c515fb --- /dev/null +++ b/src/main/resources/assets/schemas/VisaCondition.yaml @@ -0,0 +1,23 @@ +type: object +description: A condition of a Visa +properties: + type: + description: The type of claim as specified by the [GA4GH spec](https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md#visa-type) + enum: + - AffiliationAndRole + - ControlledAccessGrants + - AcceptedTermsAndPolicies + - LinkedIdentities + value: + type: string + description: A string value that conforms to the type as specified by the GA4GH spec + source: + type: string + description: A string source that conforms to the type as specified by the GA4GH spec + by: + description: The entity that is providing the authenticity of the claim + enum: + - const:dac + - const:self + - const:so + - const:system