Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
85d178f
[DT-3888] Surface SO authorization model on indexed datasets
kevinmarete Aug 11, 2026
340633e
[DT-3888] Address review: no-DAC fallback, exception logging, OpenAPI…
kevinmarete Aug 12, 2026
9a33245
[DT-3888] Address review: clarify Optional semantics, drop duplicate …
kevinmarete Aug 12, 2026
f23283f
[DT-3888] Address review: drop dead enum scaffolding, cast the bind p…
kevinmarete Aug 12, 2026
e5e7535
[DT-3888] Keep the dataset lookup inside the best-effort reindex try
kevinmarete Aug 12, 2026
e73f961
[DT-3888] Narrow toDatasetTerm overload, document the AVAILABLE filter
kevinmarete Aug 12, 2026
d2fbfda
[DT-3888] Extract the dataset half of each automation rule
kevinmarete Aug 12, 2026
c6c95ee
[DT-3888] Resolve every DAC rule in one query, surface instant approval
kevinmarete Aug 12, 2026
941f8c4
[DT-3888] Reindex all datasets on any rule toggle, off the request th…
kevinmarete Aug 12, 2026
c1b6443
[DT-3888] Release the reindex guard when scheduling is rejected
kevinmarete Aug 12, 2026
50545d4
[DT-3888] Drop the test-only toDatasetTerm overload, guard the reinde…
kevinmarete Aug 13, 2026
d65e59c
[DT-3888] Wait for the follow-up reindex pass instead of a fixed window
kevinmarete Aug 13, 2026
0b4e3ac
[DT-3888] Release the reindex guard in a finally rather than catching…
kevinmarete Aug 13, 2026
6d44739
[DT-3888] Ignore incomplete dac_rule_settings rows when resolving ena…
kevinmarete Aug 13, 2026
0ac8ad2
[DT-3888] Scope the toggle reindex to the DAC, queue toggles FIFO
kevinmarete Aug 13, 2026
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 @@ -655,8 +655,13 @@ private VoteService providesVoteService(
@Provides
@Singleton
private DACAutomationRuleService providesRuleService(
Jdbi jdbi, VoteServiceDAO voteServiceDAO, VoteService voteService) {
return new DACAutomationRuleService(jdbi, voteServiceDAO, voteService);
Jdbi jdbi,
VoteServiceDAO voteServiceDAO,
VoteService voteService,
ElasticSearchService elasticSearchService,
ExecutorService executorService) {
return new DACAutomationRuleService(
jdbi, voteServiceDAO, voteService, elasticSearchService, executorService);
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import java.util.List;
import org.broadinstitute.consent.http.db.mapper.DACAutomationRuleAuditMapper;
import org.broadinstitute.consent.http.db.mapper.DACAutomationRuleMapper;
import org.broadinstitute.consent.http.db.mapper.DACRuleAssignmentMapper;
import org.broadinstitute.consent.http.rules.DACAutomationRule;
import org.broadinstitute.consent.http.rules.DACAutomationRuleAudit;
import org.broadinstitute.consent.http.rules.DACRuleAssignment;
import org.broadinstitute.consent.http.rules.RuleAuditAction;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
Expand Down Expand Up @@ -155,6 +157,21 @@ INSERT INTO dac_rule_audit (action, dac_id, rule_id, user_id, action_date)
""")
List<DACAutomationRule> findAllDACAutomationRulesByDACId(@Bind("dacId") int dacId);

/**
* Every enabled DAC-to-rule pairing, in one pass. Keyed by DAC rather than by dataset — unlike
* {@code DatasetDAO.filterDatasetIdsByAutomationRuleType} — so indexing the whole corpus does not
* need an unbounded IN list.
*/
@RegisterRowMapper(DACRuleAssignmentMapper.class)
@SqlQuery(
"""
SELECT DISTINCT settings.dac_id, rules.rule
FROM dac_rule_settings settings
INNER JOIN dac_automation_rules rules ON rules.id = settings.rule_id
WHERE rules.state = 'AVAILABLE'
""")
List<DACRuleAssignment> findEnabledRuleAssignments();

@RegisterRowMapper(DACAutomationRuleAuditMapper.class)
@SqlQuery(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.broadinstitute.consent.http.db.mapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import org.broadinstitute.consent.http.rules.DACAutomationRuleType;
import org.broadinstitute.consent.http.rules.DACRuleAssignment;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;

public class DACRuleAssignmentMapper implements RowMapper<DACRuleAssignment> {

@Override
public DACRuleAssignment map(ResultSet rs, StatementContext ctx) throws SQLException {
return new DACRuleAssignment(
rs.getInt("dac_id"), DACAutomationRuleType.valueOf(rs.getString("rule")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.broadinstitute.consent.http.enumeration;

import com.google.gson.annotations.SerializedName;

/**
* Which Signing Official authorization model applies to a dataset, derived from whether the
* dataset's DAC has the REQUIRE_SO_DAR_APPROVAL automation rule enabled. Surfaced on indexed
* datasets so clients do not have to resolve DAC rules themselves.
*
* <p>The wire values are pinned with {@link SerializedName} so renaming a constant cannot silently
* change the published contract.
*/
public enum SoApprovalModel {
/** The SO named in each access request must approve that request before the DAC reviews it. */
@SerializedName("PER_REQUEST")
PER_REQUEST,

/** The SO authorizes researchers in advance; no per-request SO approval is needed. */
@SerializedName("PRE_AUTHORIZED")
PRE_AUTHORIZED
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ public final class DataUsePrimaryClassifier {

private DataUsePrimaryClassifier() {}

/**
* Whether a Data Use has the single-primary shape DAC automation supports. {@code Shape.SINGLE}
* also covers an Other-only primary category, which is non-canonical and excluded here to match
* the abstention policy in {@code DataUseMatcherV5}. Shared by the approval engine and dataset
* indexing, which both gate on it before consulting a rule.
*/
public static boolean hasCanonicalSinglePrimary(DataUse dataUse) {
if (dataUse == null) {
return false;
}
DataUsePrimaryClassification classification = classify(dataUse);
return classification.shape() == DataUsePrimaryClassification.Shape.SINGLE
&& !classification.categories().contains(DataUsePrimaryCategory.OTHER);
}

public static DataUsePrimaryClassification classify(DataUse dataUse) {
return classify(
dataUse.getGeneralUse(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.broadinstitute.consent.http.models.elastic_search;

import java.util.Map;
import org.broadinstitute.consent.http.enumeration.SoApprovalModel;
import org.broadinstitute.consent.http.models.ontology.DataUseSummary;

public class DatasetTerm {
Expand All @@ -24,6 +25,8 @@ public class DatasetTerm {
private UserTerm submitter;
private UserTerm updateUser;
private DacTerm dac;
private SoApprovalModel soApprovalModel;
private Boolean instantApprovalEligible;
private Boolean hasInstitutionCertification;
private Map<String, Object> data;

Expand Down Expand Up @@ -171,6 +174,22 @@ public void setDac(DacTerm dac) {
this.dac = dac;
}

public SoApprovalModel getSoApprovalModel() {
return soApprovalModel;
}

public void setSoApprovalModel(SoApprovalModel soApprovalModel) {
this.soApprovalModel = soApprovalModel;
}

public Boolean getInstantApprovalEligible() {
return instantApprovalEligible;
}

public void setInstantApprovalEligible(Boolean instantApprovalEligible) {
this.instantApprovalEligible = instantApprovalEligible;
}

public Boolean getHasInstitutionCertification() {
return hasInstitutionCertification;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.broadinstitute.consent.http.rules;

/** A single DAC-to-rule pairing: this DAC currently has this automation rule enabled. */
public record DACRuleAssignment(Integer dacId, DACAutomationRuleType ruleType) {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
public class GeneralResearchUseV1 implements RuleImplementationInterface {

public boolean compare(Dataset dataset, DataAccessRequest dataAccessRequest) {
return Boolean.TRUE.equals(dataset.getDataUse().getGeneralUse())
&& hasNoModifiers(dataset.getDataUse())
&& requestIsOnlyHMB(dataAccessRequest.getData());
return datasetQualifies(dataset) && requestIsOnlyHMB(dataAccessRequest.getData());
}

@Override
public boolean datasetQualifies(Dataset dataset) {
return datasetIsUnmodifiedGeneralUse(dataset);
}

public DACAutomationRuleType getRuleType() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public DACAutomationRuleType getRuleType() {

@Override
public boolean compare(Dataset dataset, DataAccessRequest dataAccessRequest) {
return Boolean.TRUE.equals(dataset.getDataUse().getGeneralUse())
&& hasNoModifiers(dataset.getDataUse())
&& requestHasDiseases(dataAccessRequest.getData());
return datasetQualifies(dataset) && requestHasDiseases(dataAccessRequest.getData());
}

@Override
public boolean datasetQualifies(Dataset dataset) {
return datasetIsUnmodifiedGeneralUse(dataset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public DACAutomationRuleType getRuleType() {

@Override
public boolean compare(Dataset dataset, DataAccessRequest dataAccessRequest) {
return Boolean.TRUE.equals(dataset.getDataUse().getHmbResearch())
&& hasNoModifiers(dataset.getDataUse())
&& requestIsOnlyHMB(dataAccessRequest.getData());
return datasetQualifies(dataset) && requestIsOnlyHMB(dataAccessRequest.getData());
}

@Override
public boolean datasetQualifies(Dataset dataset) {
return datasetIsUnmodifiedHmbResearch(dataset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public DACAutomationRuleType getRuleType() {

@Override
public boolean compare(Dataset dataset, DataAccessRequest dataAccessRequest) {
return Boolean.TRUE.equals(dataset.getDataUse().getHmbResearch())
&& hasNoModifiers(dataset.getDataUse())
&& requestHasDiseases(dataAccessRequest.getData());
return datasetQualifies(dataset) && requestHasDiseases(dataAccessRequest.getData());
}

@Override
public boolean datasetQualifies(Dataset dataset) {
return datasetIsUnmodifiedHmbResearch(dataset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ public interface RuleImplementationInterface {

boolean compare(Dataset dataset, DataAccessRequest dataAccessRequest);

/**
* Whether the dataset's own data use qualifies it for automatic approval under this rule,
* independent of any request. {@link #compare} layers the request-side conditions on top, and
* dataset indexing applies this half alone. Rules that never auto-approve return false.
*/
default boolean datasetQualifies(Dataset dataset) {
return false;
}

/** Reached during indexing for datasets that may carry no data use, so absence is a non-match. */
default boolean datasetIsUnmodifiedGeneralUse(Dataset dataset) {
DataUse dataUse = dataset.getDataUse();
return dataUse != null
&& Boolean.TRUE.equals(dataUse.getGeneralUse())
&& hasNoModifiers(dataUse);
}

/**
* @see #datasetIsUnmodifiedGeneralUse
*/
default boolean datasetIsUnmodifiedHmbResearch(Dataset dataset) {
DataUse dataUse = dataset.getDataUse();
return dataUse != null
&& Boolean.TRUE.equals(dataUse.getHmbResearch())
&& hasNoModifiers(dataUse);
}

default boolean hasNoModifiers(DataUse data) {
if (Boolean.TRUE.equals(data.getCollaboratorRequired())) {
return false;
Expand Down
Loading
Loading