Skip to content

Commit

Permalink
[WOR-1596] Add data-tracking policy (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
marctalbott authored Apr 5, 2024
1 parent 89e86a1 commit e7d90ce
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ public class Constants {
new PolicyName(TERRA_NAMESPACE, "region-constraint");
public static final PolicyName PROTECTED_DATA_POLICY_NAME =
new PolicyName(TERRA_NAMESPACE, "protected-data");

public static final PolicyName DATA_TRACKING_POLICY_NAME =
new PolicyName(TERRA_NAMESPACE, "data-tracking");
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
public enum KnownPolicy {
GROUP_CONSTRAINT(new PolicyGroupConstraint()),
REGION_CONSTRAINT(new PolicyRegionConstraint()),
DATA_TRACKING(new PolicyDataTrackingConstraint()),
PROTECTED_DATA(new PolicyLabel(Constants.PROTECTED_DATA_POLICY_NAME));

private final PolicyBase policy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package bio.terra.policy.service.policy;

import bio.terra.policy.common.model.Constants;
import bio.terra.policy.common.model.PolicyInput;
import bio.terra.policy.common.model.PolicyName;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class PolicyDataTrackingConstraint extends PolicyBase {
private static final String DATA_KEY = "dataType";

@Override
public PolicyName getPolicyName() {
return Constants.DATA_TRACKING_POLICY_NAME;
}

/**
* Combine of data types - there is no conflict case as data can fall under multiple categories
* (e.g. federally protected PHI). We simply create two Sets of data types from the
* comma-separated form, then mash them together, and make them back into comma-separated form.
*
* @param dependent policy input
* @param source policy input
* @return policy input
*/
@Override
protected PolicyInput performCombine(PolicyInput dependent, PolicyInput source) {
if (source == null) {
return dependent;
}

if (dependent == null) {
return source;
}

Set<String> dependentSet = dataToSet(dependent.getData(DATA_KEY));
Set<String> sourceSet = dataToSet(source.getData(DATA_KEY));
dependentSet.addAll(sourceSet);
Multimap<String, String> newData = ArrayListMultimap.create();
dependentSet.forEach(type -> newData.put(DATA_KEY, type));
return new PolicyInput(dependent.getPolicyName(), newData);
}

/**
* Remove data tracking
*
* @param target existing policy
* @param removePolicy policy to remove
* @return the target with dataTypes removed; null if no dataTypes left
*/
@Override
protected PolicyInput performRemove(PolicyInput target, PolicyInput removePolicy) {
Set<String> targetDataTypes = dataToSet(target.getData(DATA_KEY));
Set<String> removeDataTypes = dataToSet(removePolicy.getData(DATA_KEY));
targetDataTypes.removeAll(removeDataTypes);

if (targetDataTypes.isEmpty()) {
return null;
}

Multimap<String, String> newData = ArrayListMultimap.create();
targetDataTypes.forEach(dataType -> newData.put(DATA_KEY, dataType));
return new PolicyInput(Constants.DATA_TRACKING_POLICY_NAME, newData);
}

/**
* For dataTypes, the only thing we validate right now is that the key is correct.
*
* @param policyInput the input to validate
* @return
*/
@Override
protected boolean performIsValid(PolicyInput policyInput) {
var dataTypes = policyInput.getAdditionalData();
if (dataTypes.isEmpty()) {
return false;
}

for (var entry : dataTypes.entries()) {
if (!entry.getKey().equals(DATA_KEY)) {
return false;
}
}
return true;
}

@VisibleForTesting
Set<String> dataToSet(Collection<String> dataTypes) {
return new HashSet<>(dataTypes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
package bio.terra.policy.service.policy;

import static bio.terra.policy.service.policy.PolicyTestUtils.*;
import static bio.terra.policy.testutils.PaoTestUtil.DATA_TRACKING_CONSTRAINT;
import static bio.terra.policy.testutils.PaoTestUtil.DATA_TRACKING_KEY;
import static bio.terra.policy.testutils.PaoTestUtil.DATA_TYPE_NAME;
import static bio.terra.policy.testutils.PaoTestUtil.DATA_TYPE_NAME_ALT;
import static bio.terra.policy.testutils.PaoTestUtil.REGION_KEY;
import static bio.terra.policy.testutils.PaoTestUtil.TERRA_NAMESPACE;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import bio.terra.policy.common.model.PolicyInput;
import bio.terra.policy.testutils.TestUnitBase;
import com.google.common.collect.ArrayListMultimap;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;

public class PolicyDataTrackingConstraintTest extends TestUnitBase {
@Test
void dataTrackingConstraintTest_combineSameDataTypes() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

var dependentPolicy =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY, DATA_TYPE_NAME));
var sourcePolicy =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY, DATA_TYPE_NAME));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, sourcePolicy);

Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(1, dataTypeSet.size(), "Contains 1 dataType");
assertThat(dataTypeSet, contains(DATA_TYPE_NAME));
}

@Test
void dataTrackingConstraintTest_combineEmptySource() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

var dependentPolicy =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY, DATA_TYPE_NAME));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, null);

Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(1, dataTypeSet.size(), "Contains 1 dataType");
assertThat(dataTypeSet, contains(DATA_TYPE_NAME));
}

@Test
void dataTrackingConstraintTest_combineEmptyDestination() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

var sourcePolicy =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY, DATA_TYPE_NAME));

PolicyInput resultPolicy = dataTrackingConstraint.combine(null, sourcePolicy);
Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(1, dataTypeSet.size(), "Contains 1 dataType");
assertThat(dataTypeSet, contains(DATA_TYPE_NAME));
}

@Test
void dataTrackingConstraintTest_combineEmptySourceAndDestination() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

var dependentPolicy =
new PolicyInput(TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(REGION_KEY));
var sourcePolicy =
new PolicyInput(TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(REGION_KEY));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, sourcePolicy);

Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(0, dataTypeSet.size());
}

@Test
void dataTrackingConstraintTest_combineMismatchDataTypes() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

var dependentPolicy =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY, DATA_TYPE_NAME));
var sourcePolicy =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY, DATA_TYPE_NAME_ALT));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, sourcePolicy);
Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(2, dataTypeSet.size());
assertTrue(dataTypeSet.containsAll(Arrays.asList(DATA_TYPE_NAME, DATA_TYPE_NAME_ALT)));
}

@Test
void dataTrackingConstraintTest_combineMultipleDataTypes() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

String dataType2 = DATA_TYPE_NAME + "2";
String dataType3 = DATA_TYPE_NAME + "3";

Set<String> dataTypes = new HashSet<>(Arrays.asList(DATA_TYPE_NAME, dataType2, dataType3));

var dependentPolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));
var sourcePolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, sourcePolicy);
Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(dataTypes.size(), dataTypeSet.size());
assertTrue(dataTypeSet.containsAll(dataTypes));
}

@Test
void dataTrackingConstraintTest_combineWhenDestinationHasFewerDataTypes() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

String dataType2 = DATA_TYPE_NAME + "2";
String dataType3 = DATA_TYPE_NAME + "3";

Set<String> dataTypes = new HashSet<>(Arrays.asList(DATA_TYPE_NAME, dataType2));

var dependentPolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

dataTypes.add(dataType3);
var sourcePolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, sourcePolicy);
Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(dataTypes.size(), dataTypeSet.size());
assertTrue(dataTypeSet.containsAll(dataTypes));
}

@Test
void dataTrackingConstraintTest_combineWhenSourceHasFewerDataTypes() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

String dataType2 = DATA_TYPE_NAME + "2";
String dataType3 = DATA_TYPE_NAME + "3";

Set<String> dataTypes = new HashSet<>(Arrays.asList(DATA_TYPE_NAME, dataType2, dataType3));

var dependentPolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

dataTypes.remove(dataType3);
var sourcePolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

PolicyInput resultPolicy = dataTrackingConstraint.combine(dependentPolicy, sourcePolicy);
Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(3, dataTypeSet.size());
assertTrue(dataTypeSet.containsAll(dataTypes));
}

@Test
void dataTrackingConstraintTest_removeDataType() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

String dataType2 = DATA_TYPE_NAME + "2";
String dataType3 = DATA_TYPE_NAME + "3";

Set<String> dataTypes = new HashSet<>(Arrays.asList(DATA_TYPE_NAME));
var removePolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

dataTypes.add(dataType2);
dataTypes.add(dataType3);
var targetPolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

PolicyInput resultPolicy = dataTrackingConstraint.remove(targetPolicy, removePolicy);
Set<String> dataTypeSet =
dataTrackingConstraint.dataToSet(resultPolicy.getAdditionalData().get(DATA_TRACKING_KEY));

assertEquals(2, dataTypeSet.size());
assertTrue(dataTypeSet.containsAll(Arrays.asList(dataType2, dataType3)));
}

@Test
void dataTrackingConstraintTest_removeAllDataTypes() throws Exception {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

String dataType2 = DATA_TYPE_NAME + "2";
String dataType3 = DATA_TYPE_NAME + "3";

Set<String> dataTypes = new HashSet<>(Arrays.asList(DATA_TYPE_NAME, dataType2, dataType3));
var removePolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

var targetPolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

PolicyInput resultPolicy = dataTrackingConstraint.remove(targetPolicy, removePolicy);
assertNull(resultPolicy);
}

@Test
void dataTrackingConstraintTest_validation() {
var dataTrackingConstraint = new PolicyDataTrackingConstraint();

Set<String> dataTypes = new HashSet<>(Arrays.asList(DATA_TYPE_NAME));
var validPolicy =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));
var emptyPolicy =
new PolicyInput(TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, ArrayListMultimap.create());
var invalidKey =
new PolicyInput(
TERRA_NAMESPACE,
DATA_TRACKING_CONSTRAINT,
buildMultimap(DATA_TRACKING_KEY + "invalid", dataTypes));

dataTypes.add("invalid");
var invalidValue =
new PolicyInput(
TERRA_NAMESPACE, DATA_TRACKING_CONSTRAINT, buildMultimap(DATA_TRACKING_KEY, dataTypes));

assertTrue(dataTrackingConstraint.isValid(validPolicy));
assertFalse(dataTrackingConstraint.isValid(emptyPolicy));
assertFalse(dataTrackingConstraint.isValid(invalidKey));
// we don't currently validate the value
assertTrue(dataTrackingConstraint.isValid(invalidValue));
}
}
Loading

0 comments on commit e7d90ce

Please sign in to comment.