diff --git a/core/src/main/java/org/apache/accumulo/core/clientImpl/access/BytesAccess.java b/core/src/main/java/org/apache/accumulo/core/clientImpl/access/BytesAccess.java index 4c65621b0fc..21ce1698e05 100644 --- a/core/src/main/java/org/apache/accumulo/core/clientImpl/access/BytesAccess.java +++ b/core/src/main/java/org/apache/accumulo/core/clientImpl/access/BytesAccess.java @@ -20,6 +20,8 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -93,6 +95,19 @@ public boolean canAccess(byte[] expression) { } } + public static BytesEvaluator newEvaluator(Collection authsSet) { + Collection> convertedAuths = new ArrayList<>(); + for (Authorizations auths : authsSet) { + List bytesAuths = auths.getAuthorizations(); + Set stringAuths = new HashSet<>(bytesAuths.size()); + for (var auth : bytesAuths) { + stringAuths.add(new String(auth, ISO_8859_1)); + } + convertedAuths.add(stringAuths); + } + return new BytesEvaluator(ACCESS.newEvaluator(convertedAuths)); + } + public static BytesEvaluator newEvaluator(Authorizations auths) { List bytesAuths = auths.getAuthorizations(); Set stringAuths = new HashSet<>(bytesAuths.size()); diff --git a/core/src/main/java/org/apache/accumulo/core/iterators/user/VisibilityFilter.java b/core/src/main/java/org/apache/accumulo/core/iterators/user/VisibilityFilter.java index 92e5079ab65..a530920c4b2 100644 --- a/core/src/main/java/org/apache/accumulo/core/iterators/user/VisibilityFilter.java +++ b/core/src/main/java/org/apache/accumulo/core/iterators/user/VisibilityFilter.java @@ -21,7 +21,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.Map; +import java.util.Objects; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.core.client.IteratorSetting; @@ -39,6 +42,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Preconditions; + /** * A SortedKeyValueIterator that filters based on ColumnVisibility. */ @@ -50,7 +55,8 @@ public class VisibilityFilter extends Filter implements OptionDescriber { private static final Logger log = LoggerFactory.getLogger(VisibilityFilter.class); - private static final String AUTHS = "auths"; + private static final String NUM_AUTHS = "numAuths"; + private static final String AUTH_PREFIX = "auth_"; private static final String FILTER_INVALID_ONLY = "filterInvalid"; private boolean filterInvalid; @@ -63,11 +69,26 @@ public void init(SortedKeyValueIterator source, Map op this.filterInvalid = Boolean.parseBoolean(options.get(FILTER_INVALID_ONLY)); if (!filterInvalid) { - String auths = options.get(AUTHS); - Authorizations authObj = auths == null || auths.isEmpty() ? new Authorizations() - : new Authorizations(auths.getBytes(UTF_8)); - - this.accessEvaluator = BytesAccess.newEvaluator(authObj); + String numAuthsParameter = options.get(NUM_AUTHS); + Objects.requireNonNull(numAuthsParameter, "NUM_AUTHS option not set."); + int numAuths = Integer.parseInt(numAuthsParameter); + Preconditions.checkArgument(numAuths >= 0, NUM_AUTHS + " must be a positive integer"); + + Collection authSet = new ArrayList<>(); + if (numAuths == 0) { + authSet.add(new Authorizations()); + } else { + for (int idx = 0; idx < numAuths; idx++) { + String auths = options.get(AUTH_PREFIX + idx); + Authorizations authObj = auths == null || auths.isEmpty() ? new Authorizations() + : new Authorizations(auths.getBytes(UTF_8)); + authSet.add(authObj); + } + String auths = options.get(AUTH_PREFIX + numAuths); + Preconditions.checkArgument(auths == null, + "NUM_AUTHS is set incorrectly, should be at least: " + NUM_AUTHS + " = " + 1); + } + this.accessEvaluator = BytesAccess.newEvaluator(authSet); } this.cache = new LRUMap<>(1000); } @@ -136,14 +157,25 @@ public IteratorOptions describeOptions() { io.addNamedOption(FILTER_INVALID_ONLY, "if 'true', the iterator is instructed to ignore the authorizations and" + " only filter invalid visibility labels (default: false)"); - io.addNamedOption(AUTHS, - "the serialized set of authorizations to filter against (default: empty" - + " string, accepts only entries visible by all)"); + io.addNamedOption(NUM_AUTHS, + "The number of serialized authorizations to filter against (default 0)"); + io.addUnnamedOption(AUTH_PREFIX + + "N, where the value is a serialized set of authorizations. N must be between zero and NUM_AUTHS."); return io; } public static void setAuthorizations(IteratorSetting setting, Authorizations auths) { - setting.addOption(AUTHS, auths.serialize()); + setting.addOption(NUM_AUTHS, "1"); + setting.addOption(AUTH_PREFIX + 0, auths.serialize()); + } + + public static void setAuthorizations(IteratorSetting setting, Collection auths) { + setting.addOption(NUM_AUTHS, Integer.toString(auths.size())); + int idx = 0; + for (Authorizations auth : auths) { + setting.addOption(AUTH_PREFIX + idx, auth.serialize()); + idx++; + } } public static void filterInvalidLabelsOnly(IteratorSetting setting, boolean featureEnabled) { diff --git a/core/src/test/java/org/apache/accumulo/core/iterators/user/VisibilityFilterTest.java b/core/src/test/java/org/apache/accumulo/core/iterators/user/VisibilityFilterTest.java index 075e2bef330..f9db84de84b 100644 --- a/core/src/test/java/org/apache/accumulo/core/iterators/user/VisibilityFilterTest.java +++ b/core/src/test/java/org/apache/accumulo/core/iterators/user/VisibilityFilterTest.java @@ -24,7 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; @@ -155,6 +155,22 @@ public void testAllowAuthorizedLabelsOnly() throws IOException { verify(source, 1500, is.getOptions(), GOOD, GOOD, GOOD_VIS, 1000); } + @Test + public void testMulitAllowAuthorizedLabelsOnly() throws IOException { + IteratorSetting is = new IteratorSetting(1, VisibilityFilter.class); + VisibilityFilter.setAuthorizations(is, + List.of(new Authorizations("abc"), new Authorizations("def"))); + + TreeMap source = createSourceWithHiddenData(1, 2); + verify(source, 3, is.getOptions(), GOOD, GOOD, GOOD_VIS, 1); + + source = createSourceWithHiddenData(30, 500); + verify(source, 530, is.getOptions(), GOOD, GOOD, GOOD_VIS, 30); + + source = createSourceWithHiddenData(1000, 500); + verify(source, 1500, is.getOptions(), GOOD, GOOD, GOOD_VIS, 1000); + } + @Test public void testAllowUnauthorizedLabelsOnly() throws IOException { IteratorSetting is = new IteratorSetting(1, VisibilityFilter.class); @@ -171,6 +187,23 @@ public void testAllowUnauthorizedLabelsOnly() throws IOException { verify(source, 1500, is.getOptions(), BAD, BAD, HIDDEN_VIS, 500); } + @Test + public void testMultiAllowUnauthorizedLabelsOnly() throws IOException { + IteratorSetting is = new IteratorSetting(1, VisibilityFilter.class); + VisibilityFilter.setNegate(is, true); + VisibilityFilter.setAuthorizations(is, + List.of(new Authorizations("abc"), new Authorizations("def"))); + + TreeMap source = createSourceWithHiddenData(1, 2); + verify(source, 3, is.getOptions(), BAD, BAD, HIDDEN_VIS, 2); + + source = createSourceWithHiddenData(30, 500); + verify(source, 530, is.getOptions(), BAD, BAD, HIDDEN_VIS, 500); + + source = createSourceWithHiddenData(1000, 500); + verify(source, 1500, is.getOptions(), BAD, BAD, HIDDEN_VIS, 500); + } + @Test public void testNoLabels() throws IOException { IteratorSetting is = new IteratorSetting(1, VisibilityFilter.class); @@ -203,7 +236,7 @@ public void testFilterUnauthorizedAndBad() throws IOException { @Test public void testCommaSeparatedAuthorizations() throws IOException { - Map options = Collections.singletonMap("auths", "x,def,y"); + Map options = Map.of("numAuths", "1", "auth_0", "x,def,y"); TreeMap source = createSourceWithHiddenData(1, 2); verify(source, 3, options, GOOD, GOOD, GOOD_VIS, 1); @@ -218,7 +251,7 @@ public void testCommaSeparatedAuthorizations() throws IOException { @Test public void testSerializedAuthorizations() throws IOException { Map options = - Collections.singletonMap("auths", new Authorizations("x", "def", "y").serialize()); + Map.of("numAuths", "1", "auth_0", new Authorizations("x", "def", "y").serialize()); TreeMap source = createSourceWithHiddenData(1, 2); verify(source, 3, options, GOOD, GOOD, GOOD_VIS, 1); @@ -240,7 +273,7 @@ public void testStaticConfigurators() { Map opts = is.getOptions(); assertEquals("false", opts.get("filterInvalid")); assertEquals("true", opts.get("negate")); - assertEquals(new Authorizations("abc", "def").serialize(), opts.get("auths")); + assertEquals(new Authorizations("abc", "def").serialize(), opts.get("auth_0")); } @Test diff --git a/test/src/main/java/org/apache/accumulo/test/functional/VisibilityIT.java b/test/src/main/java/org/apache/accumulo/test/functional/VisibilityIT.java index 96cc472f8c7..c6bed4b1f79 100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/VisibilityIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/VisibilityIT.java @@ -37,12 +37,14 @@ import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.BatchScanner; import org.apache.accumulo.core.client.BatchWriter; +import org.apache.accumulo.core.client.IteratorSetting; import org.apache.accumulo.core.client.Scanner; import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.core.data.Key; import org.apache.accumulo.core.data.Mutation; import org.apache.accumulo.core.data.Range; import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.iterators.user.VisibilityFilter; import org.apache.accumulo.core.security.Authorizations; import org.apache.accumulo.core.security.ColumnVisibility; import org.apache.accumulo.core.util.ByteArraySet; @@ -92,6 +94,7 @@ public void run() throws Exception { insertData(c, table); queryData(c, table); + queryDataMultiAuth(c, table); deleteData(c, table); insertDefaultData(c, table2); @@ -222,6 +225,48 @@ private void queryData(AccumuloClient c, String tableName) throws Exception { queryData(c, tableName, nss("A", "B", "FOO", "L", "M", "Z"), nss(), expected); } + /** + * Configures Scanners with the users default authorizations, then it adds a + * MultiAuthVisibilityFilter with different sets of Authorizations + */ + private void queryDataMultiAuth(AccumuloClient c, String tableName) throws Exception { + + c.securityOperations().changeUserAuthorizations(getAdminPrincipal(), + new Authorizations("A", "B", "FOO", "L", "M", "Z")); + + Authorizations userAuths = c.securityOperations().getUserAuthorizations(c.whoami()); + + Set expectedUserAuths = + Set.of("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10", "v11", "v12", "v13"); + try (Scanner scanner = c.createScanner(tableName, userAuths); + BatchScanner bs = c.createBatchScanner(tableName, userAuths, 3)) { + verify(scanner.iterator(), expectedUserAuths.toArray(new String[] {})); + + bs.setRanges(Collections.singleton(new Range())); + verify(bs.iterator(), expectedUserAuths.toArray(new String[] {})); + } + + Authorizations entity1 = new Authorizations("A", "B", "FOO", "L", "M"); + Authorizations entity2 = new Authorizations("B", "FOO", "Z"); + // should only see entries with no column visibility, B and/or FOO + Set expectedAuths = Set.of("v1", "v3", "v11"); + + IteratorSetting is = new IteratorSetting(100, "userAuths", VisibilityFilter.class); + VisibilityFilter.setAuthorizations(is, Set.of(entity1, entity2)); + + try (Scanner scanner = c.createScanner(tableName, userAuths); + BatchScanner bs = c.createBatchScanner(tableName, userAuths, 3)) { + + scanner.addScanIterator(is); + verify(scanner.iterator(), expectedAuths.toArray(new String[] {})); + + bs.setRanges(Collections.singleton(new Range())); + bs.addScanIterator(is); + verify(bs.iterator(), expectedAuths.toArray(new String[] {})); + } + + } + private void queryData(AccumuloClient c, String tableName, Set allAuths, Set userAuths, Map,Set> expected) throws Exception {