Repository: DataBiosphere/jade-data-repo
Pull request #1924: [DT-1245][EnableInheritStewardFlight Steps 2 & 3] Lock Dataset Set dataset as parent in Sam for each child snapshot
Author: login: rjohanek
Created: 2025-03-05T20:27:27Z | Merged: 2025-03-10T18:05:01Z

__Jira ticket__: https://broadworkbench.atlassian.net/browse/DT-1245

## Addresses
This adds two steps to the flight that allows dataset custodians to inherit the steward role on their dataset's snapshots. 

The endpoint that calls this flight will not be functional until the rest of the tickets in this epic are complete.

## Summary of changes
Adds getResourceParent, setResourceParent, and deleteResourceParent to the IamProvider Interface. 
 - Implements them in SamIam by calling the Sam Resource Group API.

Creates a new step InheritStewardSetParentOnSnapshotsStep that sets the parent relationship on each snapshot made from a dataset to that dataset.

Adds the LockDataset and InheritStewardSetParentOnSnapshotsStep to the EnableInheritSteward Flight.

 
## Testing Strategy
Unit tests!

<!-- Reminder -->
<!-- Two CODEOWNERS will be automatically assigned to review this pull request. If you otherwise have two reviewers, you do not need to wait for their review. -->



Diff:
diff --git a/src/main/java/bio/terra/service/auth/iam/IamProviderInterface.java b/src/main/java/bio/terra/service/auth/iam/IamProviderInterface.java
index d1df1889d8..00a406fc87 100644
--- a/src/main/java/bio/terra/service/auth/iam/IamProviderInterface.java
+++ b/src/main/java/bio/terra/service/auth/iam/IamProviderInterface.java
@@ -13,6 +13,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
 import org.broadinstitute.dsde.workbench.client.sam.model.ManagedResourceGroupCoordinates;
 import org.broadinstitute.dsde.workbench.client.sam.model.UserIdInfo;
 
@@ -379,4 +380,50 @@ boolean getResourceTypeAdminPermission(
       throws InterruptedException;
 
   UserIdInfo getUserIds(String accessToken, String userEmail) throws InterruptedException;
+
+  /**
+   * Get the parent of a specified resource.
+   *
+   * @param accessToken String requesting user's access token
+   * @param childIamResourceType The IamResourceType of the child resource
+   * @param childId The UUID of the child resource
+   * @return The IamResourceType and UUID of the parent resource
+   * @throws InterruptedException throws if sam retry fails due to interruption
+   */
+  FullyQualifiedResourceId getResourceParent(
+      String accessToken, IamResourceType childIamResourceType, UUID childId)
+      throws InterruptedException;
+
+  /**
+   * Set the parent of a specified resource to the specified parent resource.
+   *
+   * @param accessToken String requesting user's access token
+   * @param childIamResourceType The IamResourceType of the child resource
+   * @param childId The UUID of the child resource
+   * @param parentIamResourceType The IamResourceType of the parent resource
+   * @param parentId The UUID of the parent resource
+   * @throws InterruptedException throws if sam retry fails due to interruption
+   */
+  void setResourceParent(
+      String accessToken,
+      IamResourceType childIamResourceType,
+      UUID childId,
+      IamResourceType parentIamResourceType,
+      UUID parentId)
+      throws InterruptedException;
+
+  /**
+   * Delete the parent of a specified resource.
+   *
+   * @param accessToken String requesting user's access token
+   * @param childIamResourceType The IamResourceType of the child resource
+   * @param childId The UUID of the child resource
+   * @throws InterruptedException throws if sam retry fails due to interruption
+   */
+  void deleteResourceParent(String accessToken, IamResourceType childIamResourceType, UUID childId)
+      throws InterruptedException;
+
+  List<FullyQualifiedResourceId> listResourceChildren(
+      String accessToken, IamResourceType parentIamResourceType, UUID parentId)
+      throws InterruptedException;
 }
diff --git a/src/main/java/bio/terra/service/auth/iam/IamService.java b/src/main/java/bio/terra/service/auth/iam/IamService.java
index b30aa82d48..846f374794 100644
--- a/src/main/java/bio/terra/service/auth/iam/IamService.java
+++ b/src/main/java/bio/terra/service/auth/iam/IamService.java
@@ -27,6 +27,7 @@
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.collections4.ListUtils;
 import org.apache.commons.collections4.map.PassiveExpiringMap;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
 import org.broadinstitute.dsde.workbench.client.sam.model.UserIdInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -578,4 +579,34 @@ public UserIdInfo getUserIds(String userEmail) {
     String tdrSaAccessToken = googleCredentialsService.getApplicationDefaultAccessToken(SCOPES);
     return callProvider(() -> iamProvider.getUserIds(tdrSaAccessToken, userEmail));
   }
+
+  public void setResourceParent(
+      String accessToken,
+      IamResourceType childIamResourceType,
+      UUID childId,
+      IamResourceType parentIamResourceType,
+      UUID parentId) {
+    callProvider(
+        () ->
+            iamProvider.setResourceParent(
+                accessToken, childIamResourceType, childId, parentIamResourceType, parentId));
+  }
+
+  public FullyQualifiedResourceId getResourceParent(
+      String accessToken, IamResourceType childIamResourceType, UUID childId) {
+    return callProvider(
+        () -> iamProvider.getResourceParent(accessToken, childIamResourceType, childId));
+  }
+
+  public void deleteResourceParent(
+      String accessToken, IamResourceType childIamResourceType, UUID childId) {
+    callProvider(
+        () -> iamProvider.deleteResourceParent(accessToken, childIamResourceType, childId));
+  }
+
+  public List<FullyQualifiedResourceId> listResourceChildren(
+      String accessToken, IamResourceType parentIamResourceType, UUID parentId) {
+    return callProvider(
+        () -> iamProvider.listResourceChildren(accessToken, parentIamResourceType, parentId));
+  }
 }
diff --git a/src/main/java/bio/terra/service/auth/iam/sam/SamIam.java b/src/main/java/bio/terra/service/auth/iam/sam/SamIam.java
index 3dc5d4c319..98995cfbc7 100644
--- a/src/main/java/bio/terra/service/auth/iam/sam/SamIam.java
+++ b/src/main/java/bio/terra/service/auth/iam/sam/SamIam.java
@@ -919,6 +919,84 @@ private UserIdInfo getUserIdsInner(String accessToken, String userEmail) throws
     return samApiService.usersApi(accessToken).getUserIds(userEmail);
   }
 
+  @Override
+  public FullyQualifiedResourceId getResourceParent(
+      String accessToken, IamResourceType childIamResourceType, UUID childId)
+      throws InterruptedException {
+    return SamRetry.retry(
+        configurationService,
+        () -> getResourceParentInner(accessToken, childIamResourceType, childId));
+  }
+
+  private FullyQualifiedResourceId getResourceParentInner(
+      String accessToken, IamResourceType childIamResourceType, UUID childId) throws ApiException {
+    ResourcesApi samResourceApi = samApiService.resourcesApi(accessToken);
+    return samResourceApi.getResourceParent(childIamResourceType.toString(), childId.toString());
+  }
+
+  @Override
+  public void setResourceParent(
+      String accessToken,
+      IamResourceType childIamResourceType,
+      UUID childId,
+      IamResourceType parentIamResourceType,
+      UUID parentId)
+      throws InterruptedException {
+    SamRetry.retry(
+        configurationService,
+        () ->
+            setResourceParentInner(
+                accessToken, childIamResourceType, childId, parentIamResourceType, parentId));
+  }
+
+  private void setResourceParentInner(
+      String accessToken,
+      IamResourceType childIamResourceType,
+      UUID childId,
+      IamResourceType parentIamResourceType,
+      UUID parentId)
+      throws ApiException {
+    ResourcesApi samResourceApi = samApiService.resourcesApi(accessToken);
+    samResourceApi.setResourceParent(
+        childIamResourceType.toString(),
+        childId.toString(),
+        new FullyQualifiedResourceId()
+            .resourceTypeName(parentIamResourceType.toString())
+            .resourceId(parentId.toString()));
+  }
+
+  @Override
+  public void deleteResourceParent(
+      String accessToken, IamResourceType childIamResourceType, UUID childId)
+      throws InterruptedException {
+    SamRetry.retry(
+        configurationService,
+        () -> deleteResourceParentInner(accessToken, childIamResourceType, childId));
+  }
+
+  private void deleteResourceParentInner(
+      String accessToken, IamResourceType childIamResourceType, UUID childId) throws ApiException {
+    ResourcesApi samResourceApi = samApiService.resourcesApi(accessToken);
+    samResourceApi.deleteResourceParent(childIamResourceType.toString(), childId.toString());
+  }
+
+  @Override
+  public List<FullyQualifiedResourceId> listResourceChildren(
+      String accessToken, IamResourceType parentIamResourceType, UUID parentId)
+      throws InterruptedException {
+    return SamRetry.retry(
+        configurationService,
+        () -> listResourceChildrenInner(accessToken, parentIamResourceType, parentId));
+  }
+
+  private List<FullyQualifiedResourceId> listResourceChildrenInner(
+      String accessToken, IamResourceType parentIamResourceType, UUID parentId)
+      throws ApiException {
+    ResourcesApi samResourceApi = samApiService.resourcesApi(accessToken);
+    return samResourceApi.listResourceChildren(
+        parentIamResourceType.toString(), parentId.toString());
+  }
+
   /**
    * Syncing a policy with SAM results in a Google group being created that is tied to that policy.
    * The response is an object with one key that is the policy group email and a value that is a
diff --git a/src/main/java/bio/terra/service/dataset/DatasetService.java b/src/main/java/bio/terra/service/dataset/DatasetService.java
index 1ce256a4e5..8e97af1c0d 100644
--- a/src/main/java/bio/terra/service/dataset/DatasetService.java
+++ b/src/main/java/bio/terra/service/dataset/DatasetService.java
@@ -43,6 +43,7 @@
 import bio.terra.service.dataset.exception.DatasetDataException;
 import bio.terra.service.dataset.exception.DatasetNotFoundException;
 import bio.terra.service.dataset.exception.IngestFailureException;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
 import bio.terra.service.dataset.flight.create.AddAssetSpecFlight;
 import bio.terra.service.dataset.flight.create.DatasetCreateFlight;
 import bio.terra.service.dataset.flight.datadelete.DatasetDataDeleteFlight;
@@ -768,8 +769,8 @@ public String enableInheritSteward(UUID datasetId, AuthenticatedUserRequest user
     return jobService
         .newJob(description, EnableInheritStewardFlight.class, null, userReq)
         .addParameter(JobMapKeys.IAM_RESOURCE_TYPE.getKeyName(), IamResourceType.DATASET)
-        .addParameter(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), datasetId)
         .addParameter(JobMapKeys.IAM_ACTION.getKeyName(), IamAction.SET_INHERIT_STEWARD)
+        .addParameter(DatasetWorkingMapKeys.DATASET_ID, datasetId)
         .addParameter(JobMapKeys.CUSTODIAN_EMAIL.getKeyName(), custodianEmail)
         .submit();
   }
diff --git a/src/main/java/bio/terra/service/dataset/flight/DatasetWorkingMapKeys.java b/src/main/java/bio/terra/service/dataset/flight/DatasetWorkingMapKeys.java
index 07fc37d595..b99a037c3e 100644
--- a/src/main/java/bio/terra/service/dataset/flight/DatasetWorkingMapKeys.java
+++ b/src/main/java/bio/terra/service/dataset/flight/DatasetWorkingMapKeys.java
@@ -17,6 +17,6 @@ private DatasetWorkingMapKeys() {}
   public static final String SERVICE_ACCOUNT_EMAIL = "serviceAccountEmail";
   public static final String SECURE_MONITORING_ENABLED = "secureMonitoringEnabled";
   public static final String SNAPSHOT_GOOGLE_PROJECT_IDS = "snapshotGoogleProjectIds";
-
   public static final String IS_SHARED_LOCK = "isSharedLock";
+  public static final String SNAPSHOT_IDS = "snapshotIds";
 }
diff --git a/src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
index 87e8359f92..5eb2f0396a 100644
--- a/src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
+++ b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
@@ -1,8 +1,15 @@
 package bio.terra.service.dataset.flight.inheritsteward;
 
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamService;
 import bio.terra.service.dataset.DatasetDao;
+import bio.terra.service.dataset.DatasetService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.service.dataset.flight.LockDatasetStep;
+import bio.terra.service.dataset.flight.UnlockDatasetStep;
 import bio.terra.service.job.JobMapKeys;
 import bio.terra.service.resourcemanagement.ResourceService;
+import bio.terra.service.snapshot.SnapshotDao;
 import bio.terra.service.snapshot.SnapshotService;
 import bio.terra.stairway.Flight;
 import bio.terra.stairway.FlightMap;
@@ -16,16 +23,25 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    SnapshotDao snapshotDao = appContext.getBean(SnapshotDao.class);
     ResourceService resourceService = appContext.getBean(ResourceService.class);
     SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
-    UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    UUID datasetId = inputParameters.get(DatasetWorkingMapKeys.DATASET_ID, UUID.class);
     String custodianEmail =
         inputParameters.get(JobMapKeys.CUSTODIAN_EMAIL.getKeyName(), String.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
-    addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
+    addStep(new SetInheritStewardFlagStep(datasetDao, datasetId, true));
+    addStep(new GetSnapshotIdsStep(snapshotDao, datasetId));
+    addStep(new SetParentOnSnapshotsStep(iamService, datasetId, userReq));
     addStep(new GetSnapshotGoogleProjectIdsStep(snapshotService, datasetId));
     addStep(new SetAuthBqJobUserStep(resourceService, custodianEmail, true));
+    addStep(new UnlockDatasetStep(datasetService, false));
   }
 }
diff --git a/src/main/java/bio/terra/service/dataset/flight/inheritsteward/GetSnapshotIdsStep.java b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/GetSnapshotIdsStep.java
new file mode 100644
index 0000000000..59b3421289
--- /dev/null
+++ b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/GetSnapshotIdsStep.java
@@ -0,0 +1,27 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.service.job.DefaultUndoStep;
+import bio.terra.service.snapshot.SnapshotDao;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class GetSnapshotIdsStep extends DefaultUndoStep {
+  private final SnapshotDao snapshotDao;
+  private final UUID datasetId;
+
+  public GetSnapshotIdsStep(SnapshotDao snapshotDao, UUID datasetId) {
+    this.snapshotDao = snapshotDao;
+    this.datasetId = datasetId;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshotIds = snapshotDao.getSnapshotIds(datasetId);
+    context.getWorkingMap().put(DatasetWorkingMapKeys.SNAPSHOT_IDS, snapshotIds);
+    return StepResult.getStepResultSuccess();
+  }
+}
diff --git a/src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetFlagStep.java b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetInheritStewardFlagStep.java
similarity index 93%
rename from src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetFlagStep.java
rename to src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetInheritStewardFlagStep.java
index aa6b08e8cb..b81ff892b1 100644
--- a/src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetFlagStep.java
+++ b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetInheritStewardFlagStep.java
@@ -6,12 +6,12 @@
 import bio.terra.stairway.StepResult;
 import java.util.UUID;
 
-public class InheritStewardSetFlagStep implements Step {
+public class SetInheritStewardFlagStep implements Step {
   private final DatasetDao datasetDao;
   private final UUID datasetId;
   private final boolean enableInheritSteward;
 
-  public InheritStewardSetFlagStep(
+  public SetInheritStewardFlagStep(
       DatasetDao datasetDao, UUID datasetId, boolean enableInheritSteward) {
     this.datasetDao = datasetDao;
     this.datasetId = datasetId;
diff --git a/src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java
new file mode 100644
index 0000000000..918a3318fa
--- /dev/null
+++ b/src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java
@@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
+
+public class SetParentOnSnapshotsStep implements Step {
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public SetParentOnSnapshotsStep(
+      IamService iamService, UUID datasetId, AuthenticatedUserRequest userReq) {
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    String accessToken = userReq.getToken();
+    List<UUID> snapshots =
+        context.getWorkingMap().get(DatasetWorkingMapKeys.SNAPSHOT_IDS, List.class);
+    Objects.requireNonNull(snapshots)
+        .forEach(
+            snapshotId ->
+                // do not catch and handle errors, if one occurs, fail the flight
+                // we are not checking if a parent already exists because
+                // we want to overwrite it no matter what it is
+                iamService.setResourceParent(
+                    accessToken,
+                    IamResourceType.DATASNAPSHOT,
+                    snapshotId,
+                    IamResourceType.DATASET,
+                    datasetId));
+    return StepResult.getStepResultSuccess();
+  }
+
+  @Override
+  public StepResult undoStep(FlightContext context) throws InterruptedException {
+    List<FullyQualifiedResourceId> children =
+        iamService.listResourceChildren(userReq.getToken(), IamResourceType.DATASET, datasetId);
+    children.stream()
+        .filter(
+            child ->
+                child
+                    .getResourceTypeName()
+                    .equalsIgnoreCase(IamResourceType.DATASNAPSHOT.getSamResourceName()))
+        .forEach(
+            child ->
+                iamService.deleteResourceParent(
+                    userReq.getToken(),
+                    IamResourceType.DATASNAPSHOT,
+                    UUID.fromString(child.getResourceId())));
+    return StepResult.getStepResultSuccess();
+  }
+}
diff --git a/src/main/java/bio/terra/service/snapshot/SnapshotDao.java b/src/main/java/bio/terra/service/snapshot/SnapshotDao.java
index ee33b5d5de..d50970e5d8 100644
--- a/src/main/java/bio/terra/service/snapshot/SnapshotDao.java
+++ b/src/main/java/bio/terra/service/snapshot/SnapshotDao.java
@@ -808,6 +808,18 @@ public List<String> getSnapshotGoogleProjectIds(UUID datasetId) {
     return jdbcTemplate.query(sql, Map.of("datasetId", datasetId), (rs, rowNum) -> rs.getString(1));
   }
 
+  public List<UUID> getSnapshotIds(UUID datasetId) {
+    String sql =
+        """
+            select snapshot.id
+            from snapshot_source,
+               snapshot
+            where dataset_id = :datasetId
+               and snapshot.id = snapshot_source.snapshot_id
+        """;
+    return jdbcTemplate.query(sql, Map.of("datasetId", datasetId), new UuidMapper("id"));
+  }
+
   private class SnapshotSummaryMapper implements RowMapper<SnapshotSummary> {
 
     public SnapshotSummary mapRow(ResultSet rs, int rowNum) throws SQLException {
diff --git a/src/test/java/bio/terra/service/auth/iam/IamServiceTest.java b/src/test/java/bio/terra/service/auth/iam/IamServiceTest.java
index 517966e253..3f05513740 100644
--- a/src/test/java/bio/terra/service/auth/iam/IamServiceTest.java
+++ b/src/test/java/bio/terra/service/auth/iam/IamServiceTest.java
@@ -28,6 +28,7 @@
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -305,4 +306,53 @@ void testRemoveEmailFromGroup() throws InterruptedException {
         .thenReturn(List.of());
     assertEquals(iamService.removeEmailFromGroup(groupName, policyName, email), List.of());
   }
+
+  @Test
+  void setResourceParent() throws InterruptedException {
+    UUID childId = UUID.randomUUID();
+    UUID parentId = UUID.randomUUID();
+    iamService.setResourceParent(
+        TEST_USER.getToken(),
+        IamResourceType.DATASNAPSHOT,
+        childId,
+        IamResourceType.DATASET,
+        parentId);
+    verify(iamProvider)
+        .setResourceParent(
+            TEST_USER.getToken(),
+            IamResourceType.DATASNAPSHOT,
+            childId,
+            IamResourceType.DATASET,
+            parentId);
+  }
+
+  @Test
+  void deleteResourceParent() throws InterruptedException {
+    UUID childId = UUID.randomUUID();
+    iamService.deleteResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId);
+    verify(iamProvider)
+        .deleteResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId);
+  }
+
+  @Test
+  void getResourceParent() throws InterruptedException {
+    UUID childId = UUID.randomUUID();
+    FullyQualifiedResourceId parent = new FullyQualifiedResourceId();
+    when(iamProvider.getResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId))
+        .thenReturn(parent);
+    assertEquals(
+        parent,
+        iamService.getResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId));
+  }
+
+  @Test
+  void listResourceChildren() throws InterruptedException {
+    UUID parentId = UUID.randomUUID();
+    List<FullyQualifiedResourceId> children = List.of(new FullyQualifiedResourceId());
+    when(iamProvider.listResourceChildren(TEST_USER.getToken(), IamResourceType.DATASET, parentId))
+        .thenReturn(children);
+    assertEquals(
+        children,
+        iamService.listResourceChildren(TEST_USER.getToken(), IamResourceType.DATASET, parentId));
+  }
 }
diff --git a/src/test/java/bio/terra/service/auth/iam/sam/SamIamTest.java b/src/test/java/bio/terra/service/auth/iam/sam/SamIamTest.java
index 2e46b14cc2..77dfc8f7f5 100644
--- a/src/test/java/bio/terra/service/auth/iam/sam/SamIamTest.java
+++ b/src/test/java/bio/terra/service/auth/iam/sam/SamIamTest.java
@@ -7,6 +7,7 @@
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -759,6 +760,130 @@ void testPatchAuthDomain() throws Exception {
           .patchAuthDomainV2(
               IamResourceType.DATASNAPSHOT.getSamResourceName(), snapshotId.toString(), authDomain);
     }
+
+    @Test
+    void setResourceParent() throws Exception {
+      UUID childId = UUID.randomUUID();
+      UUID parentId = UUID.randomUUID();
+      samIam.setResourceParent(
+          TEST_USER.getToken(),
+          IamResourceType.DATASNAPSHOT,
+          childId,
+          IamResourceType.DATASET,
+          parentId);
+      verify(samResourceApi)
+          .setResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(),
+              childId.toString(),
+              new FullyQualifiedResourceId()
+                  .resourceTypeName(IamResourceType.DATASET.getSamResourceName())
+                  .resourceId(parentId.toString()));
+    }
+
+    @Test
+    void setResourceParentThrows() throws Exception {
+      UUID childId = UUID.randomUUID();
+      UUID parentId = UUID.randomUUID();
+      String accessToken = TEST_USER.getToken();
+      ApiException samEx =
+          new ApiException(HttpStatusCodes.STATUS_CODE_NOT_FOUND, "Resource not found");
+      doThrow(samEx)
+          .when(samResourceApi)
+          .setResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(),
+              childId.toString(),
+              new FullyQualifiedResourceId()
+                  .resourceTypeName(IamResourceType.DATASET.getSamResourceName())
+                  .resourceId(parentId.toString()));
+      assertThrows(
+          IamNotFoundException.class,
+          () ->
+              samIam.setResourceParent(
+                  accessToken,
+                  IamResourceType.DATASNAPSHOT,
+                  childId,
+                  IamResourceType.DATASET,
+                  parentId));
+    }
+
+    @Test
+    void deleteResourceParent() throws Exception {
+      UUID childId = UUID.randomUUID();
+      samIam.deleteResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId);
+      verify(samResourceApi)
+          .deleteResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(), childId.toString());
+    }
+
+    @Test
+    void deleteResourceParentThrows() throws Exception {
+      UUID childId = UUID.randomUUID();
+      String accessToken = TEST_USER.getToken();
+      ApiException samEx =
+          new ApiException(HttpStatusCodes.STATUS_CODE_NOT_FOUND, "Resource not found");
+      doThrow(samEx)
+          .when(samResourceApi)
+          .deleteResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(), childId.toString());
+      assertThrows(
+          IamNotFoundException.class,
+          () -> samIam.deleteResourceParent(accessToken, IamResourceType.DATASNAPSHOT, childId));
+    }
+
+    @Test
+    void getResourceParent() throws Exception {
+      UUID childId = UUID.randomUUID();
+      FullyQualifiedResourceId parent =
+          new FullyQualifiedResourceId()
+              .resourceTypeName(IamResourceType.DATASET.getSamResourceName())
+              .resourceId(UUID.randomUUID().toString());
+      when(samResourceApi.getResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(), childId.toString()))
+          .thenReturn(parent);
+      assertThat(
+          samIam.getResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId),
+          is(parent));
+    }
+
+    @Test
+    void getResourceParentThrows() throws Exception {
+      UUID childId = UUID.randomUUID();
+      String accessToken = TEST_USER.getToken();
+      ApiException samEx =
+          new ApiException(HttpStatusCodes.STATUS_CODE_NOT_FOUND, "Resource parent not found");
+      doThrow(samEx)
+          .when(samResourceApi)
+          .getResourceParent(IamResourceType.DATASNAPSHOT.getSamResourceName(), childId.toString());
+      assertThrows(
+          IamNotFoundException.class,
+          () -> samIam.getResourceParent(accessToken, IamResourceType.DATASNAPSHOT, childId));
+    }
+
+    @Test
+    void listResourceChildren() throws Exception {
+      UUID parentId = UUID.randomUUID();
+      List<FullyQualifiedResourceId> children = List.of(new FullyQualifiedResourceId());
+      when(samResourceApi.listResourceChildren(
+              IamResourceType.DATASET.getSamResourceName(), parentId.toString()))
+          .thenReturn(children);
+      assertEquals(
+          children,
+          samIam.listResourceChildren(TEST_USER.getToken(), IamResourceType.DATASET, parentId));
+    }
+
+    @Test
+    void listResourceChildrenThrows() throws Exception {
+      UUID parentId = UUID.randomUUID();
+      String accessToken = TEST_USER.getToken();
+      ApiException samEx =
+          new ApiException(HttpStatusCodes.STATUS_CODE_NOT_FOUND, "Resource not found");
+      doThrow(samEx)
+          .when(samResourceApi)
+          .listResourceChildren(IamResourceType.DATASET.getSamResourceName(), parentId.toString());
+      assertThrows(
+          IamNotFoundException.class,
+          () -> samIam.listResourceChildren(accessToken, IamResourceType.DATASET, parentId));
+    }
   }
 
   @Nested
diff --git a/src/test/java/bio/terra/service/dataset/DatasetServiceUnitTest.java b/src/test/java/bio/terra/service/dataset/DatasetServiceUnitTest.java
index 49dc1792c7..328852c9eb 100644
--- a/src/test/java/bio/terra/service/dataset/DatasetServiceUnitTest.java
+++ b/src/test/java/bio/terra/service/dataset/DatasetServiceUnitTest.java
@@ -38,6 +38,7 @@
 import bio.terra.service.auth.iam.IamResourceType;
 import bio.terra.service.auth.iam.IamRole;
 import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
 import bio.terra.service.dataset.flight.inheritsteward.EnableInheritStewardFlight;
 import bio.terra.service.dataset.flight.unlock.DatasetUnlockFlight;
 import bio.terra.service.filedata.azure.AzureSynapsePdao;
@@ -407,8 +408,7 @@ void testEnableInheritSteward() {
     assertThat(
         flightMap.get(JobMapKeys.IAM_RESOURCE_TYPE.getKeyName(), IamResourceType.class),
         equalTo(IamResourceType.DATASET));
-    assertThat(
-        flightMap.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class), equalTo(DATASET_ID));
+    assertThat(flightMap.get(DatasetWorkingMapKeys.DATASET_ID, UUID.class), equalTo(DATASET_ID));
     assertThat(
         flightMap.get(JobMapKeys.IAM_ACTION.getKeyName(), IamAction.class),
         equalTo(IamAction.SET_INHERIT_STEWARD));
diff --git a/src/test/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlightTest.java b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlightTest.java
index a10067a382..4071d06408 100644
--- a/src/test/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlightTest.java
+++ b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlightTest.java
@@ -9,9 +9,19 @@
 
 import bio.terra.common.FlightTestUtils;
 import bio.terra.common.category.Unit;
+import bio.terra.common.fixtures.AuthenticationFixtures;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamAction;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
 import bio.terra.service.dataset.DatasetDao;
+import bio.terra.service.dataset.DatasetService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.service.dataset.flight.LockDatasetStep;
+import bio.terra.service.dataset.flight.UnlockDatasetStep;
 import bio.terra.service.job.JobMapKeys;
 import bio.terra.service.resourcemanagement.ResourceService;
+import bio.terra.service.snapshot.SnapshotDao;
 import bio.terra.service.snapshot.SnapshotService;
 import bio.terra.stairway.FlightMap;
 import java.util.UUID;
@@ -29,20 +39,31 @@ class EnableInheritStewardFlightTest {
 
   @Mock private ApplicationContext context;
   @Mock private DatasetDao datasetDao;
+  @Mock private SnapshotDao snapshotDao;
   @Mock private ResourceService resourceService;
   @Mock private SnapshotService snapshotService;
+  @Mock private DatasetService datasetService;
+  @Mock private IamService iamService;
   private final FlightMap inputParameters = new FlightMap();
 
   private static final String CUSTODIAN_EMAIL = "custodian email";
   private static final UUID DATASET_ID = UUID.randomUUID();
+  private static final AuthenticatedUserRequest TEST_USER =
+      AuthenticationFixtures.randomUserRequest();
 
   @BeforeEach
   void setUp() {
-    inputParameters.put(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), DATASET_ID);
+    inputParameters.put(JobMapKeys.IAM_RESOURCE_TYPE.getKeyName(), IamResourceType.DATASET);
+    inputParameters.put(DatasetWorkingMapKeys.DATASET_ID, DATASET_ID);
+    inputParameters.put(JobMapKeys.IAM_ACTION.getKeyName(), IamAction.SET_INHERIT_STEWARD);
+    inputParameters.put(JobMapKeys.AUTH_USER_INFO.getKeyName(), TEST_USER);
     inputParameters.put(JobMapKeys.CUSTODIAN_EMAIL.getKeyName(), CUSTODIAN_EMAIL);
     when(context.getBean(DatasetDao.class)).thenReturn(datasetDao);
+    when(context.getBean(SnapshotDao.class)).thenReturn(snapshotDao);
+    when(context.getBean(DatasetService.class)).thenReturn(datasetService);
     when(context.getBean(ResourceService.class)).thenReturn(resourceService);
     when(context.getBean(SnapshotService.class)).thenReturn(snapshotService);
+    when(context.getBean(IamService.class)).thenReturn(iamService);
   }
 
   @Test
@@ -52,16 +73,42 @@ void allSteps() {
     assertThat(
         steps,
         contains(
-            "InheritStewardSetFlagStep",
+            "LockDatasetStep",
+            "SetInheritStewardFlagStep",
+            "GetSnapshotIdsStep",
+            "SetParentOnSnapshotsStep",
             "GetSnapshotGoogleProjectIdsStep",
-            "SetAuthBqJobUserStep"));
+            "SetAuthBqJobUserStep",
+            "UnlockDatasetStep"));
   }
 
   @Test
-  void inheritStewardSetFlagStep() {
+  void lockDatasetStep() {
     try (var mockStep =
         mockConstruction(
-            InheritStewardSetFlagStep.class,
+            LockDatasetStep.class,
+            (mock, context) -> {
+              assertThat((DatasetService) context.arguments().get(0), equalTo(datasetService));
+              assertThat(
+                  "The correct datasetId is passed to the step",
+                  (UUID) context.arguments().get(1),
+                  equalTo(DATASET_ID));
+              assertThat(
+                  "The correct shared lock boolean flag is passed to the step",
+                  (boolean) context.arguments().get(2),
+                  equalTo(false));
+            })) {
+      //noinspection ResultOfObjectAllocationIgnored
+      new EnableInheritStewardFlight(inputParameters, context);
+      assertThat(mockStep.constructed(), hasSize(1));
+    }
+  }
+
+  @Test
+  void setFlagStep() {
+    try (var mockStep =
+        mockConstruction(
+            SetInheritStewardFlagStep.class,
             (mock, context) -> {
               assertThat((DatasetDao) context.arguments().get(0), equalTo(datasetDao));
               assertThat(
@@ -79,6 +126,43 @@ void inheritStewardSetFlagStep() {
     }
   }
 
+  @Test
+  void getSnapshotsStep() {
+    try (var mockStep =
+        mockConstruction(
+            GetSnapshotIdsStep.class,
+            (mock, context) -> {
+              assertThat((SnapshotDao) context.arguments().get(0), equalTo(snapshotDao));
+              assertThat(
+                  "The correct datasetId is passed to the step",
+                  (UUID) context.arguments().get(1),
+                  equalTo(DATASET_ID));
+            })) {
+      //noinspection ResultOfObjectAllocationIgnored
+      new EnableInheritStewardFlight(inputParameters, context);
+      assertThat(mockStep.constructed(), hasSize(1));
+    }
+  }
+
+  @Test
+  void setParentsStep() {
+    try (var mockStep =
+        mockConstruction(
+            SetParentOnSnapshotsStep.class,
+            (mock, context) -> {
+              assertThat((IamService) context.arguments().get(0), equalTo(iamService));
+              assertThat(
+                  "The correct datasetId is passed to the step",
+                  (UUID) context.arguments().get(1),
+                  equalTo(DATASET_ID));
+              assertThat((AuthenticatedUserRequest) context.arguments().get(2), equalTo(TEST_USER));
+            })) {
+      //noinspection ResultOfObjectAllocationIgnored
+      new EnableInheritStewardFlight(inputParameters, context);
+      assertThat(mockStep.constructed(), hasSize(1));
+    }
+  }
+
   @Test
   void getSnapshotGoogleProjectIdsStep() {
     try (var mockStep =
@@ -118,4 +202,22 @@ void setAuthBqJobUserStep() {
       assertThat(mockStep.constructed(), hasSize(1));
     }
   }
+
+  @Test
+  void unlockDatasetStep() {
+    try (var mockStep =
+        mockConstruction(
+            UnlockDatasetStep.class,
+            (mock, context) -> {
+              assertThat((DatasetService) context.arguments().get(0), equalTo(datasetService));
+              assertThat(
+                  "The correct shared lock boolean flag is passed to the step",
+                  (boolean) context.arguments().get(1),
+                  equalTo(false));
+            })) {
+      //noinspection ResultOfObjectAllocationIgnored
+      new EnableInheritStewardFlight(inputParameters, context);
+      assertThat(mockStep.constructed(), hasSize(1));
+    }
+  }
 }
diff --git a/src/test/java/bio/terra/service/dataset/flight/inheritsteward/GetSnapshotIdsStepTest.java b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/GetSnapshotIdsStepTest.java
new file mode 100644
index 0000000000..966a1019b2
--- /dev/null
+++ b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/GetSnapshotIdsStepTest.java
@@ -0,0 +1,45 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+import bio.terra.common.category.Unit;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.service.snapshot.SnapshotDao;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.FlightMap;
+import bio.terra.stairway.StepResult;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@Tag(Unit.TAG)
+class GetSnapshotIdsStepTest {
+
+  @Mock private SnapshotDao snapshotDao;
+  @Mock private FlightContext flightContext;
+  private static final UUID DATASET_ID = UUID.randomUUID();
+  private GetSnapshotIdsStep step;
+
+  @BeforeEach
+  void setUp() {
+    step = new GetSnapshotIdsStep(snapshotDao, DATASET_ID);
+  }
+
+  @Test
+  void doStep() throws Exception {
+    List<UUID> snapshotIds = Arrays.asList(UUID.randomUUID(), UUID.randomUUID());
+    FlightMap workingMap = new FlightMap();
+    when(flightContext.getWorkingMap()).thenReturn(workingMap);
+    when(snapshotDao.getSnapshotIds(DATASET_ID)).thenReturn(snapshotIds);
+    assertEquals(step.doStep(flightContext), StepResult.getStepResultSuccess());
+    assertEquals(workingMap.get(DatasetWorkingMapKeys.SNAPSHOT_IDS, List.class), snapshotIds);
+  }
+}
diff --git a/src/test/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetFlagStepTest.java b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/SetInheritStewardFlagStepTest.java
similarity index 89%
rename from src/test/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetFlagStepTest.java
rename to src/test/java/bio/terra/service/dataset/flight/inheritsteward/SetInheritStewardFlagStepTest.java
index ba1f407fd3..1c5dbfd3d7 100644
--- a/src/test/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetFlagStepTest.java
+++ b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/SetInheritStewardFlagStepTest.java
@@ -17,17 +17,17 @@
 
 @ExtendWith(MockitoExtension.class)
 @Tag(Unit.TAG)
-class InheritStewardSetFlagStepTest {
+class SetInheritStewardFlagStepTest {
 
   @Mock private DatasetDao datasetDao;
   private UUID datasetId;
-  private InheritStewardSetFlagStep step;
+  private SetInheritStewardFlagStep step;
   @Mock private FlightContext context;
 
   @BeforeEach
   void setUp() {
     datasetId = UUID.randomUUID();
-    step = new InheritStewardSetFlagStep(datasetDao, datasetId, true);
+    step = new SetInheritStewardFlagStep(datasetDao, datasetId, true);
   }
 
   @Test
diff --git a/src/test/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStepTest.java b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStepTest.java
new file mode 100644
index 0000000000..b05f63a380
--- /dev/null
+++ b/src/test/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStepTest.java
@@ -0,0 +1,82 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import bio.terra.common.category.Unit;
+import bio.terra.common.fixtures.AuthenticationFixtures;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.FlightMap;
+import bio.terra.stairway.StepResult;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@Tag(Unit.TAG)
+class SetParentOnSnapshotsStepTest {
+  @Mock private SnapshotService snapshotService;
+  @Mock private IamService iamService;
+  @Mock private FlightContext context;
+  private SetParentOnSnapshotsStep step;
+  private static final UUID DATASET_ID = UUID.randomUUID();
+  private static final AuthenticatedUserRequest TEST_USER =
+      AuthenticationFixtures.randomUserRequest();
+  private static final UUID SNAPSHOT_1 = UUID.randomUUID();
+  private static final UUID SNAPSHOT_2 = UUID.randomUUID();
+
+  @BeforeEach
+  void setUp() {
+    step = new SetParentOnSnapshotsStep(iamService, DATASET_ID, TEST_USER);
+  }
+
+  @Test
+  void doStep() throws InterruptedException {
+    List<UUID> snapshotIds = Arrays.asList(SNAPSHOT_1, SNAPSHOT_2);
+    FlightMap workingMap = new FlightMap();
+    workingMap.put(DatasetWorkingMapKeys.SNAPSHOT_IDS, snapshotIds);
+    when(context.getWorkingMap()).thenReturn(workingMap);
+    assertEquals(step.doStep(context), StepResult.getStepResultSuccess());
+    for (UUID snapshotId : snapshotIds) {
+      verify(iamService)
+          .setResourceParent(
+              TEST_USER.getToken(),
+              IamResourceType.DATASNAPSHOT,
+              snapshotId,
+              IamResourceType.DATASET,
+              DATASET_ID);
+    }
+  }
+
+  @Test
+  void undoStep() throws InterruptedException {
+    FullyQualifiedResourceId childSnapshot =
+        new FullyQualifiedResourceId()
+            .resourceId(SNAPSHOT_1.toString())
+            .resourceTypeName(IamResourceType.DATASNAPSHOT.name());
+    FullyQualifiedResourceId childDataset =
+        new FullyQualifiedResourceId()
+            .resourceId(UUID.randomUUID().toString())
+            .resourceTypeName(IamResourceType.DATASET.name());
+    when(iamService.listResourceChildren(TEST_USER.getToken(), IamResourceType.DATASET, DATASET_ID))
+        .thenReturn(List.of(childSnapshot, childDataset));
+    assertEquals(step.undoStep(context), StepResult.getStepResultSuccess());
+    verify(iamService)
+        .deleteResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, SNAPSHOT_1);
+    verifyNoMoreInteractions(iamService);
+  }
+}
diff --git a/src/test/java/bio/terra/service/snapshot/SnapshotDaoTest.java b/src/test/java/bio/terra/service/snapshot/SnapshotDaoTest.java
index d4d9286e61..9e7876465f 100644
--- a/src/test/java/bio/terra/service/snapshot/SnapshotDaoTest.java
+++ b/src/test/java/bio/terra/service/snapshot/SnapshotDaoTest.java
@@ -53,6 +53,7 @@
 import bio.terra.service.snapshot.exception.SnapshotNotFoundException;
 import bio.terra.service.snapshot.exception.SnapshotUpdateException;
 import com.fasterxml.jackson.core.type.TypeReference;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -136,6 +137,11 @@ private Snapshot createSnapshot(SnapshotRequestModel request) {
     return insertAndRetrieveSnapshot(snapshot);
   }
 
+  private Snapshot createSnapshot(SnapshotRequestModel request, Dataset sourceDataset) {
+    Snapshot snapshot = daoOperations.createSnapshotFromSnapshotRequest(request, sourceDataset);
+    return insertAndRetrieveSnapshot(snapshot);
+  }
+
   private Snapshot insertAndRetrieveSnapshot(Snapshot snapshot) {
     snapshotIds.add(snapshot.getId());
     return daoOperations.ingestSnapshot(snapshot);
@@ -1075,13 +1081,7 @@ void testRetrieveSnapshotNotFound() {
 
   @Test
   void getSnapshotGoogleProjectIds() {
-    String snapshotName = snapshotRequest.getName() + UUID.randomUUID();
-    List<Snapshot> snapshots =
-        IntStream.range(0, 3)
-            .mapToObj(i -> snapshotRequest.name(makeName(snapshotName, i)))
-            .map(this::createSnapshot)
-            .toList();
-
+    List<Snapshot> snapshots = makeSnapshots(dataset);
     assertThat(
         snapshotDao.getSnapshotGoogleProjectIds(datasetId),
         containsInAnyOrder(
@@ -1090,4 +1090,29 @@ void getSnapshotGoogleProjectIds() {
                 .map(GoogleProjectResource::getGoogleProjectId)
                 .toArray()));
   }
+
+  @Test
+  void getSnapshotIdsForDataset() throws IOException {
+    Dataset newDataset =
+        daoOperations.createDataset("snapshot-test-dataset-with-multi-columns.json");
+    List<Snapshot> snapshots = makeSnapshots(newDataset);
+    assertThat(
+        snapshotDao.getSnapshotIds(newDataset.getId()),
+        containsInAnyOrder(snapshots.stream().map(Snapshot::getId).toArray()));
+  }
+
+  private List<Snapshot> makeSnapshots(Dataset sourceDataset) {
+    String snapshotName = snapshotRequest.getName() + UUID.randomUUID();
+    return IntStream.range(0, 3)
+        .mapToObj(
+            i -> {
+              snapshotRequest
+                  .name(makeName(snapshotName, i))
+                  .profileId(sourceDataset.getDefaultProfileId());
+              snapshotRequest.getContents().get(0).datasetName(sourceDataset.getName());
+              return snapshotRequest;
+            })
+        .map(request -> createSnapshot(request, sourceDataset))
+        .toList();
+  }
 }


Reviews:
user: login: pshapiro4broad
body: Looks OK, just a few comments
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T16:16:46Z
user: login: pshapiro4broad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:30:45Z
user: login: snf2ye
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:20:44Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T19:02:38Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T19:28:28Z
user: login: pshapiro4broad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T20:23:46Z
user: login: pshapiro4broad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T20:25:33Z
user: login: s-rubenstein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T16:15:49Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T16:50:59Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T16:51:21Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T20:26:06Z
user: login: rjohanek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T20:39:28Z
user: login: s-rubenstein
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T20:59:34Z

Review comments:
diff_hunk: @@ -14,10 +19,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
     UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
     addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: pshapiro4broad
body: Wouldn't we want to lock before setting the flag on the dataset? I think normally this step is done before any changes are made, e.g., just after validation steps are run.
created_at: 2025-03-06T15:42:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class InheritStewardSetParentOnSnapshotsStep implements Step {
+  private final SnapshotService snapshotService;
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public InheritStewardSetParentOnSnapshotsStep(
+      SnapshotService snapshotService,
+      IamService iamService,
+      UUID datasetId,
+      AuthenticatedUserRequest userReq) {
+    this.snapshotService = snapshotService;
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStep.java
user: login: pshapiro4broad
body: If we think this will run on a large number of snapshots, we might want to have a separate step that retrieves the list of all snapshot IDs and stores them in the fight map, so we don't re-run the query if this step needs to be retried.

Also, I wonder if `enumerateSnapshotIdsForDataset()` is what we want here. This filters the results based on the user, so it might not return all snapshots. If this is an admin operation, maybe this should bypass Sam permissions and use a DAO call to get all the snapshots for a dataset.

created_at: 2025-03-06T15:48:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class InheritStewardSetParentOnSnapshotsStep implements Step {
+  private final SnapshotService snapshotService;
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public InheritStewardSetParentOnSnapshotsStep(
+      SnapshotService snapshotService,
+      IamService iamService,
+      UUID datasetId,
+      AuthenticatedUserRequest userReq) {
+    this.snapshotService = snapshotService;
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
+    snapshots.forEach(
+        snapshotId -> {
+          // do not catch and handle errors, if one occurs, fail the flight
+          // we are not checking if a parent already exists because
+          // we want to overwrite it no matter what it is
+          iamService.setResourceParent(
+              userReq.getToken(),
+              IamResourceType.DATASNAPSHOT,
+              snapshotId,
+              IamResourceType.DATASET,
+              datasetId);
+        });
+    return StepResult.getStepResultSuccess();
+  }
+
+  @Override
+  public StepResult undoStep(FlightContext context) throws InterruptedException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
+    snapshots.forEach(
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStep.java
user: login: pshapiro4broad
body: Another approach here is to call Sam's `listResourceChildren` on the dataset to get the snapshots that are actually registered as children of the dataset. That would avoid making the API call for ones that were never added as children.
created_at: 2025-03-06T15:51:50Z
author_association: MEMBER
diff_hunk: @@ -305,4 +306,45 @@ void testRemoveEmailFromGroup() throws InterruptedException {
         .thenReturn(List.of());
     assertEquals(iamService.removeEmailFromGroup(groupName, policyName, email), List.of());
   }
+
+  @Test
+  void setResourceParent() throws InterruptedException {
+    UUID childId = UUID.randomUUID();
+    UUID parentId = UUID.randomUUID();
+    iamService.setResourceParent(
+        TEST_USER.getToken(),
+        IamResourceType.DATASNAPSHOT,
+        childId,
+        IamResourceType.DATASET,
+        parentId);
+    verify(iamProvider)
+        .setResourceParent(
+            TEST_USER.getToken(),
+            IamResourceType.DATASNAPSHOT,
+            childId,
+            IamResourceType.DATASET,
+            parentId);
+  }
+
+  @Test
+  void deleteResourceParent() throws InterruptedException {
+    UUID childId = UUID.randomUUID();
+    iamService.deleteResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId);
+    verify(iamProvider)
+        .deleteResourceParent(TEST_USER.getToken(), IamResourceType.DATASNAPSHOT, childId);
+  }
+
+  @Test
+  void getResourceParent() throws InterruptedException {
+    UUID childId = UUID.randomUUID();
+    FullyQualifiedResourceId parent =
+        new FullyQualifiedResourceId()
+            .resourceTypeName(IamResourceType.DATASET.getSamResourceName())
+            .resourceId(UUID.randomUUID().toString());
path: src/test/java/bio/terra/service/auth/iam/IamServiceTest.java
user: login: pshapiro4broad
body: This is fine but it the contents of `parent` are unused in the code being tested so these lines have no effect on the test outcome. The only thing tested here is that the IamService API is returning the object that the IamProvider is returning.
```suggestion
    FullyQualifiedResourceId parent = new FullyQualifiedResourceId();
```
created_at: 2025-03-06T16:03:01Z
author_association: MEMBER
diff_hunk: @@ -759,6 +759,105 @@ void testPatchAuthDomain() throws Exception {
           .patchAuthDomainV2(
               IamResourceType.DATASNAPSHOT.getSamResourceName(), snapshotId.toString(), authDomain);
     }
+
+    @Test
+    void setResourceParent() throws ApiException, InterruptedException {
path: src/test/java/bio/terra/service/auth/iam/sam/SamIamTest.java
user: login: pshapiro4broad
body: To make it easier to maintain tests, I use `throws Exception`. Using specific exception types doesn't affect the test and ends up being a maintenance burden if the types ever change.
```suggestion
    void setResourceParent() throws Exception {
```
created_at: 2025-03-06T16:05:00Z
author_association: MEMBER
diff_hunk: @@ -759,6 +759,105 @@ void testPatchAuthDomain() throws Exception {
           .patchAuthDomainV2(
               IamResourceType.DATASNAPSHOT.getSamResourceName(), snapshotId.toString(), authDomain);
     }
+
+    @Test
+    void setResourceParent() throws ApiException, InterruptedException {
+      UUID childId = UUID.randomUUID();
+      UUID parentId = UUID.randomUUID();
+      samIam.setResourceParent(
+          TEST_USER.getToken(),
+          IamResourceType.DATASNAPSHOT,
+          childId,
+          IamResourceType.DATASET,
+          parentId);
+      verify(samResourceApi)
+          .setResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(),
+              childId.toString(),
+              new FullyQualifiedResourceId()
+                  .resourceTypeName(IamResourceType.DATASET.getSamResourceName())
+                  .resourceId(parentId.toString()));
+    }
+
+    @Test
+    void setResourceParentThrows() throws ApiException {
+      UUID childId = UUID.randomUUID();
+      UUID parentId = UUID.randomUUID();
+      ApiException samEx =
+          new ApiException(HttpStatusCodes.STATUS_CODE_NOT_FOUND, "Resource not found");
+      doThrow(samEx)
+          .when(samResourceApi)
+          .setResourceParent(
+              IamResourceType.DATASNAPSHOT.getSamResourceName(),
+              childId.toString(),
+              new FullyQualifiedResourceId()
+                  .resourceTypeName(IamResourceType.DATASET.getSamResourceName())
+                  .resourceId(parentId.toString()));
+      assertThrows(
+          IamNotFoundException.class,
+          () ->
+              samIam.setResourceParent(
+                  TEST_USER.getToken(),
path: src/test/java/bio/terra/service/auth/iam/sam/SamIamTest.java
user: login: pshapiro4broad
body: I'm sure you'll get a sonar warning about this, but the goal is that there's only one function call in the lambda passed to `assertThrows()`, so we know exactly what function is generating the exception. The fix is to assign `TEST_USER.getToken()` to a local variable outside the lambda.
created_at: 2025-03-06T16:07:22Z
author_association: MEMBER
diff_hunk: @@ -28,16 +34,37 @@ class EnableInheritStewardFlightTest {
 
   @Mock private ApplicationContext context;
   @Mock private DatasetDao datasetDao;
+  @Mock private DatasetService datasetService;
+  @Mock private SnapshotService snapshotService;
+  @Mock private IamService iamService;
   private FlightMap inputParameters;
   private static final UUID DATASET_ID = UUID.randomUUID();
+  private static final AuthenticatedUserRequest TEST_USER =
+      AuthenticationFixtures.randomUserRequest();
 
   @BeforeEach
   void setUp() {
     inputParameters = new FlightMap();
     inputParameters.put(JobMapKeys.IAM_RESOURCE_TYPE.getKeyName(), IamResourceType.DATASET);
     inputParameters.put(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), DATASET_ID);
     inputParameters.put(JobMapKeys.IAM_ACTION.getKeyName(), IamAction.SET_INHERIT_STEWARD);
+    inputParameters.put(JobMapKeys.AUTH_USER_INFO.getKeyName(), TEST_USER);
     when(context.getBean(DatasetDao.class)).thenReturn(datasetDao);
+    when(context.getBean(DatasetService.class)).thenReturn(datasetService);
+    when(context.getBean(SnapshotService.class)).thenReturn(snapshotService);
+    when(context.getBean(IamService.class)).thenReturn(iamService);
+  }
+
+  @Test
+  void testStepsIncluded() {
path: src/test/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlightTest.java
user: login: pshapiro4broad
body: I added my new steps to a single test but this is probably better, especially when we have a lot of steps.
created_at: 2025-03-06T16:12:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,78 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import bio.terra.common.category.Unit;
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.fixtures.AuthenticationFixtures;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.StepResult;
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+@Tag(Unit.TAG)
+class InheritStewardSetParentOnSnapshotsStepTest {
+  @Mock private SnapshotService snapshotService;
+  @Mock private IamService iamService;
+  @Mock private FlightContext context;
+  private InheritStewardSetParentOnSnapshotsStep step;
+  private static final UUID DATASET_ID = UUID.randomUUID();
+  private static final AuthenticatedUserRequest TEST_USER =
+      AuthenticationFixtures.randomUserRequest();
+  private static final UUID SNAPSHOT_1 = UUID.randomUUID();
+  private static final UUID SNAPSHOT_2 = UUID.randomUUID();
+
+  @BeforeEach
+  void setUp() {
+    step =
+        new InheritStewardSetParentOnSnapshotsStep(
+            snapshotService, iamService, DATASET_ID, TEST_USER);
+  }
+
+  @Test
+  void doStep() throws InterruptedException {
+    when(snapshotService.enumerateSnapshotIdsForDataset(DATASET_ID, TEST_USER))
+        .thenReturn(List.of(SNAPSHOT_1, SNAPSHOT_2));
+    assertEquals(step.doStep(context), StepResult.getStepResultSuccess());
+    verify(iamService)
+        .setResourceParent(
+            TEST_USER.getToken(),
+            IamResourceType.DATASNAPSHOT,
+            SNAPSHOT_1,
+            IamResourceType.DATASET,
+            DATASET_ID);
+    verify(iamService)
+        .setResourceParent(
+            TEST_USER.getToken(),
+            IamResourceType.DATASNAPSHOT,
+            SNAPSHOT_2,
+            IamResourceType.DATASET,
+            DATASET_ID);
path: src/test/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStepTest.java
user: login: pshapiro4broad
body: You can use a `for` loop here to avoid the code duplication
created_at: 2025-03-06T16:14:38Z
author_association: MEMBER
diff_hunk: @@ -14,10 +19,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
     UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
     addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: pshapiro4broad
body: Not sure why the work was split up this way, but I think it would be better to add the Unlock step at the same as as Lock is added.
created_at: 2025-03-06T16:16:37Z
author_association: MEMBER
diff_hunk: @@ -56,9 +83,45 @@ void testParametersForSetFlagStep() {
                   (boolean) context.arguments().get(2),
                   equalTo(true));
             })) {
-      var flight = new EnableInheritStewardFlight(inputParameters, context);
-      var steps = FlightTestUtils.getStepNames(flight);
-      assertThat(steps, contains("InheritStewardSetFlagStep"));
+      new EnableInheritStewardFlight(inputParameters, context);
+    }
+  }
+
+  @Test
+  void testParametersForLockDatasetStep() {
+    try (var mockedStep =
+        mockConstruction(
+            LockDatasetStep.class,
+            (mock, context) -> {
+              assertThat((DatasetService) context.arguments().get(0), equalTo(datasetService));
+              assertThat(
+                  "The correct datasetId is passed to the step",
+                  (UUID) context.arguments().get(1),
+                  equalTo(DATASET_ID));
+              assertThat(
+                  "The correct shared lock boolean flag is passed to the step",
+                  (boolean) context.arguments().get(2),
+                  equalTo(false));
+            })) {
+      new EnableInheritStewardFlight(inputParameters, context);
path: src/test/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlightTest.java
user: login: pshapiro4broad
body: One problem with this approach is that it doesn't validate that the constructor was called. A fix would be to assert on the number of objects constructed by the mock:
```
      assertThat(mockStep.constructed(), hasSize(1));
```

The remaining issue is that the result of `new EnableInheritStewardFlight()` is unused which will trigger a warning. This can be suppressed:
```
      //noinspection ResultOfObjectAllocationIgnored
      new EnableInheritStewardFlight(inputParameters, context);
```
or the test could extract the step from the flight
```
      var flight = new EnableInheritStewardFlight(inputParameters, context);
      var steps = FlightTestUtils.getStepNames(flight);
      assertThat(steps, hasItem("LockDatasetStep"));
```
but that's redundant with the first test
created_at: 2025-03-06T16:30:45Z
author_association: MEMBER
diff_hunk: @@ -14,10 +19,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
     UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
     addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: snf2ye
body: Hm, yeah, that works. Another approach I think we've taken in the past, especially for ingest flights, is to try to limit the amount of time we have resources locked. So, only locking around the steps that could create conflicts. But, we don't expect this flight to be run regularly/in contentious environments and the set flag step is quick anyway. So locking as the first step sound good. 
created_at: 2025-03-06T17:17:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,10 +19,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
     UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
     addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: snf2ye
body: Ha, that's a good idea. Otherwise they'll always be left in the lock state! 
created_at: 2025-03-06T17:18:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class InheritStewardSetParentOnSnapshotsStep implements Step {
+  private final SnapshotService snapshotService;
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public InheritStewardSetParentOnSnapshotsStep(
+      SnapshotService snapshotService,
+      IamService iamService,
+      UUID datasetId,
+      AuthenticatedUserRequest userReq) {
+    this.snapshotService = snapshotService;
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStep.java
user: login: rjohanek
body: okay, I will make a separate step for enumerating the snapshots. In terms of your other comment, if this is an admin operation, won't the user have permission to list all of the snapshots?
created_at: 2025-03-06T19:02:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class InheritStewardSetParentOnSnapshotsStep implements Step {
+  private final SnapshotService snapshotService;
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public InheritStewardSetParentOnSnapshotsStep(
+      SnapshotService snapshotService,
+      IamService iamService,
+      UUID datasetId,
+      AuthenticatedUserRequest userReq) {
+    this.snapshotService = snapshotService;
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
+    snapshots.forEach(
+        snapshotId -> {
+          // do not catch and handle errors, if one occurs, fail the flight
+          // we are not checking if a parent already exists because
+          // we want to overwrite it no matter what it is
+          iamService.setResourceParent(
+              userReq.getToken(),
+              IamResourceType.DATASNAPSHOT,
+              snapshotId,
+              IamResourceType.DATASET,
+              datasetId);
+        });
+    return StepResult.getStepResultSuccess();
+  }
+
+  @Override
+  public StepResult undoStep(FlightContext context) throws InterruptedException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
+    snapshots.forEach(
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStep.java
user: login: rjohanek
body: I like this idea, then we don't have to handle the 404 case. but then maybe we should check that its a snapshot and ignore children that aren't snapshots
created_at: 2025-03-06T19:28:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class InheritStewardSetParentOnSnapshotsStep implements Step {
+  private final SnapshotService snapshotService;
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public InheritStewardSetParentOnSnapshotsStep(
+      SnapshotService snapshotService,
+      IamService iamService,
+      UUID datasetId,
+      AuthenticatedUserRequest userReq) {
+    this.snapshotService = snapshotService;
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStep.java
user: login: pshapiro4broad
body: > In terms of your other comment, if this is an admin operation, won't the user have permission to list all of the snapshots?

I think you're right, but I would still bypass the auth check here. The thing I worry about is if the flag is turned on (or off) and not all snapshots are updated. I don't want to end up in a state where some have a parent set and others don't.
created_at: 2025-03-06T20:23:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.exception.NotFoundException;
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.snapshot.SnapshotService;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.UUID;
+
+public class InheritStewardSetParentOnSnapshotsStep implements Step {
+  private final SnapshotService snapshotService;
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public InheritStewardSetParentOnSnapshotsStep(
+      SnapshotService snapshotService,
+      IamService iamService,
+      UUID datasetId,
+      AuthenticatedUserRequest userReq) {
+    this.snapshotService = snapshotService;
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
+    snapshots.forEach(
+        snapshotId -> {
+          // do not catch and handle errors, if one occurs, fail the flight
+          // we are not checking if a parent already exists because
+          // we want to overwrite it no matter what it is
+          iamService.setResourceParent(
+              userReq.getToken(),
+              IamResourceType.DATASNAPSHOT,
+              snapshotId,
+              IamResourceType.DATASET,
+              datasetId);
+        });
+    return StepResult.getStepResultSuccess();
+  }
+
+  @Override
+  public StepResult undoStep(FlightContext context) throws InterruptedException {
+    List<UUID> snapshots = snapshotService.enumerateSnapshotIdsForDataset(datasetId, userReq);
+    snapshots.forEach(
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/InheritStewardSetParentOnSnapshotsStep.java
user: login: pshapiro4broad
body: > we should check that its a snapshot and ignore children that aren't snapshots

That's a good point. The `listResourceChildren` response does include `resourceTypeName` so we can filter the children.
created_at: 2025-03-06T20:25:33Z
author_association: MEMBER
diff_hunk: @@ -14,10 +21,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
-    UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    UUID datasetId = inputParameters.get(DatasetWorkingMapKeys.DATASET_ID, UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
-    addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
+    addStep(new SetInheritStewardFlagStep(datasetDao, datasetId, true));
+    addStep(new GetSnapshotIdsStep(snapshotService, datasetId, userReq));
+    addStep(new SetParentOnSnapshotsStep(iamService, datasetId, userReq));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: s-rubenstein
body: To test my own understanding, the reason `GetSnapshotIdsStep` is separate is we need the snapshots to set the bigquery resource acls?
created_at: 2025-03-07T16:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
+
+public class SetParentOnSnapshotsStep implements Step {
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public SetParentOnSnapshotsStep(
+      IamService iamService, UUID datasetId, AuthenticatedUserRequest userReq) {
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    String accessToken = userReq.getToken();
+    List<UUID> snapshots =
+        context.getWorkingMap().get(DatasetWorkingMapKeys.SNAPSHOT_IDS, List.class);
+    Objects.requireNonNull(snapshots)
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java
user: login: s-rubenstein
body: I'd love to think about abstracting these so that when we do `RemoveInheritSteward` work, we can reuse the methods? Do you think that is premature optimization or worth doing now?
created_at: 2025-03-07T16:15:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,10 +21,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
-    UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    UUID datasetId = inputParameters.get(DatasetWorkingMapKeys.DATASET_ID, UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
-    addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
+    addStep(new SetInheritStewardFlagStep(datasetDao, datasetId, true));
+    addStep(new GetSnapshotIdsStep(snapshotService, datasetId, userReq));
+    addStep(new SetParentOnSnapshotsStep(iamService, datasetId, userReq));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: rjohanek
body: so it's a good question, originally, I had getSnapshots and SetParent in the same step. This would work okay. Looking forward, for the BQ ACLs step, we may reuse them, but also I think that step may require more information such as the entire Snapshot Model so it may just make its own call. The motivation for moving this to a separate step comes from Phil's comment: "If we think this will run on a large number of snapshots, we might want to have a separate step that retrieves the list of all snapshot IDs and stores them in the fight map, so we don't re-run the query if this step needs to be retried." While this is an edge case, I agreed that it might be helpful to account for since it might be an expensive query and there might be a failure in the setParent part of the functionality.
created_at: 2025-03-07T16:50:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
+
+public class SetParentOnSnapshotsStep implements Step {
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public SetParentOnSnapshotsStep(
+      IamService iamService, UUID datasetId, AuthenticatedUserRequest userReq) {
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    String accessToken = userReq.getToken();
+    List<UUID> snapshots =
+        context.getWorkingMap().get(DatasetWorkingMapKeys.SNAPSHOT_IDS, List.class);
+    Objects.requireNonNull(snapshots)
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java
user: login: rjohanek
body: I think that is worth doing now!
created_at: 2025-03-07T16:51:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
+
+public class SetParentOnSnapshotsStep implements Step {
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public SetParentOnSnapshotsStep(
+      IamService iamService, UUID datasetId, AuthenticatedUserRequest userReq) {
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    String accessToken = userReq.getToken();
+    List<UUID> snapshots =
+        context.getWorkingMap().get(DatasetWorkingMapKeys.SNAPSHOT_IDS, List.class);
+    Objects.requireNonNull(snapshots)
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java
user: login: rjohanek
body: Maybe I'll just pull them out to helper methods in the service class
created_at: 2025-03-07T20:26:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package bio.terra.service.dataset.flight.inheritsteward;
+
+import bio.terra.common.iam.AuthenticatedUserRequest;
+import bio.terra.service.auth.iam.IamResourceType;
+import bio.terra.service.auth.iam.IamService;
+import bio.terra.service.dataset.flight.DatasetWorkingMapKeys;
+import bio.terra.stairway.FlightContext;
+import bio.terra.stairway.Step;
+import bio.terra.stairway.StepResult;
+import bio.terra.stairway.exception.RetryException;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import org.broadinstitute.dsde.workbench.client.sam.model.FullyQualifiedResourceId;
+
+public class SetParentOnSnapshotsStep implements Step {
+  private final IamService iamService;
+  private final UUID datasetId;
+  private final AuthenticatedUserRequest userReq;
+
+  public SetParentOnSnapshotsStep(
+      IamService iamService, UUID datasetId, AuthenticatedUserRequest userReq) {
+    this.iamService = iamService;
+    this.datasetId = datasetId;
+    this.userReq = userReq;
+  }
+
+  @Override
+  public StepResult doStep(FlightContext context) throws InterruptedException, RetryException {
+    String accessToken = userReq.getToken();
+    List<UUID> snapshots =
+        context.getWorkingMap().get(DatasetWorkingMapKeys.SNAPSHOT_IDS, List.class);
+    Objects.requireNonNull(snapshots)
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/SetParentOnSnapshotsStep.java
user: login: rjohanek
body: So, after coming up with an approach, I'm not sure if code sharing here is worth the extra computational cost since we will be dealing with thousands of snapshots. If we create a general add/remove parent for a list of snapshot ids method on the dataset service like I was thinking, this would require mapping the response of the listResourceChildren call (FullyQualifiedResourceId) to a UUID which means iterating through the list one additional time. What do you think?
created_at: 2025-03-07T20:39:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,10 +21,19 @@ public EnableInheritStewardFlight(FlightMap inputParameters, Object applicationC
     // Get the required DAOs and services to pass into the steps
     ApplicationContext appContext = (ApplicationContext) applicationContext;
     DatasetDao datasetDao = appContext.getBean(DatasetDao.class);
+    DatasetService datasetService = appContext.getBean(DatasetService.class);
+    SnapshotService snapshotService = appContext.getBean(SnapshotService.class);
+    IamService iamService = appContext.getBean(IamService.class);
 
     // Get the input parameters
-    UUID datasetId = inputParameters.get(JobMapKeys.IAM_RESOURCE_ID.getKeyName(), UUID.class);
+    UUID datasetId = inputParameters.get(DatasetWorkingMapKeys.DATASET_ID, UUID.class);
+    AuthenticatedUserRequest userReq =
+        inputParameters.get(JobMapKeys.AUTH_USER_INFO.getKeyName(), AuthenticatedUserRequest.class);
 
-    addStep(new InheritStewardSetFlagStep(datasetDao, datasetId, true));
+    addStep(new LockDatasetStep(datasetService, datasetId, false));
+    addStep(new SetInheritStewardFlagStep(datasetDao, datasetId, true));
+    addStep(new GetSnapshotIdsStep(snapshotService, datasetId, userReq));
+    addStep(new SetParentOnSnapshotsStep(iamService, datasetId, userReq));
path: src/main/java/bio/terra/service/dataset/flight/inheritsteward/EnableInheritStewardFlight.java
user: login: s-rubenstein
body: Got it. Seems totally reasonable to me.
created_at: 2025-03-07T20:59:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-10T16:49:54Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DataBiosphere_jade-data-repo&pullRequest=1924) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [8 New issues](https://sonarcloud.io/project/issues?id=DataBiosphere_jade-data-repo&pullRequest=1924&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DataBiosphere_jade-data-repo&pullRequest=1924&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DataBiosphere_jade-data-repo&pullRequest=1924&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [98.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_jade-data-repo&pullRequest=1924&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_jade-data-repo&pullRequest=1924&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DataBiosphere_jade-data-repo&pullRequest=1924)


author_association: NONE