Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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,29 @@ INSERT INTO dac_rule_audit (action, dac_id, rule_id, user_id, action_date)
""")
List<DACAutomationRule> findAllDACAutomationRulesByDACId(@Bind("dacId") int dacId);

/**
* Every DAC-to-rule pairing that is currently enabled. A rule counts as enabled for a DAC when a
* dac_rule_settings row exists for that pairing and the rule itself is still AVAILABLE, so a
* retired rule is not reported as enabled — matching findAllDACAutomationRulesByDACId.
*
* <p>Deliberately keyed by DAC rather than by dataset — unlike {@code
* DatasetDAO.filterDatasetIdsByAutomationRuleType}, which takes a dataset id list. Indexing walks
* the entire dataset corpus, so a dataset-keyed query would mean an unbounded IN list; the result
* here is bounded by the number of DACs no matter how many datasets are being indexed.
*
* <p>Returns all rule types in one pass rather than taking a rule type argument, so indexing
* resolves every rule it decorates datasets with in a single query.
*/
@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,24 @@ 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 must be excluded here to
* match the abstention policy in {@code DataUseMatcherV5}.
*
* <p>Both the approval engine and dataset indexing gate on this before consulting a rule, so it
* lives here rather than in either caller — the two must not be able to disagree about which
* datasets automation can act on.
*/
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,39 @@ 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.
*
* <p>Split out so dataset indexing can report auto-approval eligibility with the same predicate
* the approval engine applies, rather than a client-side re-derivation that can drift from it.
* Rules that never auto-approve return false, matching their {@code compare}.
*/
default boolean datasetQualifies(Dataset dataset) {
return false;
}

/**
* Unlike {@code compare}, this is reached during indexing for datasets that may carry no data use
* at all, so the absence of one is a non-match rather than a failure.
*/
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