This is an automated email from the ASF dual-hosted git repository. kturner pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/accumulo.git
The following commit(s) were added to refs/heads/main by this push: new 1e347f8482 updates column visibility to use accumulo access library (#3746) 1e347f8482 is described below commit 1e347f84828a597d97f698e276be11c12d37f41d Author: Keith Turner <ktur...@apache.org> AuthorDate: Mon Jul 8 14:38:36 2024 -0700 updates column visibility to use accumulo access library (#3746) Updates ColumnVisibility and VisibilityEvaluator to use the Accumulo Access control library : https://github.com/apache/accumulo-access Co-authored-by: Dave Marion <dlmar...@apache.org> --- assemble/pom.xml | 5 + core/pom.xml | 6 + .../summarizers/AuthorizationSummarizer.java | 36 +----- .../core/clientImpl/ConditionalWriterImpl.java | 29 ++--- .../data/constraints/VisibilityConstraint.java | 16 +-- .../core/iterators/user/TransformingIterator.java | 23 ++-- .../core/iterators/user/VisibilityFilter.java | 20 +-- .../iteratorsImpl/system/VisibilityFilter.java | 19 ++- .../accumulo/core/security/Authorizations.java | 20 +++ .../accumulo/core/security/ColumnVisibility.java | 85 ++++++++----- .../core/security/VisibilityEvaluator.java | 134 +++------------------ .../core/security/VisibilityParseException.java | 11 ++ .../accumulo/core/util/BadArgumentException.java | 7 ++ .../data/constraints/VisibilityConstraintTest.java | 13 +- .../core/security/ColumnVisibilityTest.java | 13 ++ .../core/security/VisibilityEvaluatorTest.java | 24 +--- pom.xml | 6 + 17 files changed, 206 insertions(+), 261 deletions(-) diff --git a/assemble/pom.xml b/assemble/pom.xml index 4e0bc83497..fbb5a9e698 100644 --- a/assemble/pom.xml +++ b/assemble/pom.xml @@ -181,6 +181,11 @@ <artifactId>jakarta.xml.bind-api</artifactId> <optional>true</optional> </dependency> + <dependency> + <groupId>org.apache.accumulo</groupId> + <artifactId>accumulo-access</artifactId> + <optional>true</optional> + </dependency> <dependency> <groupId>org.apache.accumulo</groupId> <artifactId>accumulo-compaction-coordinator</artifactId> diff --git a/core/pom.xml b/core/pom.xml index c064563a4e..ff2523748f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -81,6 +81,10 @@ <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-context</artifactId> </dependency> + <dependency> + <groupId>org.apache.accumulo</groupId> + <artifactId>accumulo-access</artifactId> + </dependency> <dependency> <groupId>org.apache.accumulo</groupId> <artifactId>accumulo-start</artifactId> @@ -272,6 +276,8 @@ <allow>javax[.]security[.]auth[.]DestroyFailedException</allow> <!-- allow questionable Hadoop exceptions for mapreduce --> <allow>org[.]apache[.]hadoop[.]mapred[.](FileAlreadyExistsException|InvalidJobConfException)</allow> + <!-- allow the following types from the visibility API --> + <allow>org[.]apache[.]accumulo[.]access[.].*</allow> </allows> </configuration> </execution> diff --git a/core/src/main/java/org/apache/accumulo/core/client/summary/summarizers/AuthorizationSummarizer.java b/core/src/main/java/org/apache/accumulo/core/client/summary/summarizers/AuthorizationSummarizer.java index 5660f665c3..77a92442e5 100644 --- a/core/src/main/java/org/apache/accumulo/core/client/summary/summarizers/AuthorizationSummarizer.java +++ b/core/src/main/java/org/apache/accumulo/core/client/summary/summarizers/AuthorizationSummarizer.java @@ -24,14 +24,13 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import org.apache.accumulo.access.AccessExpression; import org.apache.accumulo.core.client.admin.TableOperations; import org.apache.accumulo.core.client.summary.CountingSummarizer; import org.apache.accumulo.core.data.ArrayByteSequence; import org.apache.accumulo.core.data.ByteSequence; import org.apache.accumulo.core.data.Key; import org.apache.accumulo.core.data.Value; -import org.apache.accumulo.core.security.ColumnVisibility; -import org.apache.accumulo.core.security.ColumnVisibility.Node; /** * Counts unique authorizations in column visibility labels. Leverages super class to defend against @@ -82,7 +81,10 @@ public class AuthorizationSummarizer extends CountingSummarizer<ByteSequence> { if (vis.length() > 0) { Set<ByteSequence> auths = cache.get(vis); if (auths == null) { - auths = findAuths(vis); + auths = new HashSet<>(); + for (String auth : AccessExpression.of(vis.toArray()).getAuthorizations().asSet()) { + auths.add(new ArrayByteSequence(auth)); + } cache.put(new ArrayByteSequence(vis), auths); } @@ -91,33 +93,5 @@ public class AuthorizationSummarizer extends CountingSummarizer<ByteSequence> { } } } - - private Set<ByteSequence> findAuths(ByteSequence vis) { - HashSet<ByteSequence> auths = new HashSet<>(); - byte[] expression = vis.toArray(); - Node root = new ColumnVisibility(expression).getParseTree(); - - findAuths(root, expression, auths); - - return auths; - } - - private void findAuths(Node node, byte[] expression, HashSet<ByteSequence> auths) { - switch (node.getType()) { - case AND: - case OR: - for (Node child : node.getChildren()) { - findAuths(child, expression, auths); - } - break; - case TERM: - auths.add(node.getTerm(expression)); - break; - case EMPTY: - break; - default: - throw new IllegalArgumentException("Unknown node type " + node.getType()); - } - } } } diff --git a/core/src/main/java/org/apache/accumulo/core/clientImpl/ConditionalWriterImpl.java b/core/src/main/java/org/apache/accumulo/core/clientImpl/ConditionalWriterImpl.java index 3137a19f00..4ddcdae552 100644 --- a/core/src/main/java/org/apache/accumulo/core/clientImpl/ConditionalWriterImpl.java +++ b/core/src/main/java/org/apache/accumulo/core/clientImpl/ConditionalWriterImpl.java @@ -42,6 +42,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.IllegalAccessExpressionException; import org.apache.accumulo.core.Constants; import org.apache.accumulo.core.client.AccumuloException; import org.apache.accumulo.core.client.AccumuloSecurityException; @@ -69,13 +71,9 @@ import org.apache.accumulo.core.lock.ServiceLock; import org.apache.accumulo.core.rpc.ThriftUtil; import org.apache.accumulo.core.rpc.clients.ThriftClientTypes; import org.apache.accumulo.core.security.Authorizations; -import org.apache.accumulo.core.security.ColumnVisibility; -import org.apache.accumulo.core.security.VisibilityEvaluator; -import org.apache.accumulo.core.security.VisibilityParseException; import org.apache.accumulo.core.tabletingest.thrift.TabletIngestClientService; import org.apache.accumulo.core.tabletserver.thrift.NoSuchScanIDException; import org.apache.accumulo.core.trace.TraceUtil; -import org.apache.accumulo.core.util.BadArgumentException; import org.apache.accumulo.core.util.ByteBufferUtil; import org.apache.accumulo.core.util.threads.ThreadPools; import org.apache.accumulo.core.util.threads.Threads; @@ -97,14 +95,14 @@ class ConditionalWriterImpl implements ConditionalWriter { private static final int MAX_SLEEP = 30000; - private Authorizations auths; - private VisibilityEvaluator ve; - private Map<Text,Boolean> cache = Collections.synchronizedMap(new LRUMap<>(1000)); + private final Authorizations auths; + private final AccessEvaluator accessEvaluator; + private final Map<Text,Boolean> cache = Collections.synchronizedMap(new LRUMap<>(1000)); private final ClientContext context; - private TabletLocator locator; + private final TabletLocator locator; private final TableId tableId; private final String tableName; - private long timeout; + private final long timeout; private final Durability durability; private final String classLoaderContext; @@ -374,7 +372,7 @@ class ConditionalWriterImpl implements ConditionalWriter { ConditionalWriterConfig config) { this.context = context; this.auths = config.getAuthorizations(); - this.ve = new VisibilityEvaluator(config.getAuthorizations()); + this.accessEvaluator = AccessEvaluator.of(config.getAuthorizations().toAccessAuthorizations()); this.threadPool = context.threadPools().createScheduledExecutorService( config.getMaxWriteThreads(), this.getClass().getSimpleName()); this.locator = new SyncingTabletLocator(context, tableId); @@ -808,21 +806,24 @@ class ConditionalWriterImpl implements ConditionalWriter { } private boolean isVisible(ByteSequence cv) { - Text testVis = new Text(cv.toArray()); - if (testVis.getLength() == 0) { + + if (cv.length() == 0) { return true; } + byte[] arrayVis = cv.toArray(); + Text testVis = new Text(arrayVis); + Boolean b = cache.get(testVis); if (b != null) { return b; } try { - boolean bb = ve.evaluate(new ColumnVisibility(testVis)); + boolean bb = accessEvaluator.canAccess(arrayVis); cache.put(new Text(testVis), bb); return bb; - } catch (VisibilityParseException | BadArgumentException e) { + } catch (IllegalAccessExpressionException e) { return false; } } diff --git a/core/src/main/java/org/apache/accumulo/core/data/constraints/VisibilityConstraint.java b/core/src/main/java/org/apache/accumulo/core/data/constraints/VisibilityConstraint.java index 62531c5fa0..26f55f109a 100644 --- a/core/src/main/java/org/apache/accumulo/core/data/constraints/VisibilityConstraint.java +++ b/core/src/main/java/org/apache/accumulo/core/data/constraints/VisibilityConstraint.java @@ -24,12 +24,11 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.IllegalAccessExpressionException; +import org.apache.accumulo.core.data.ArrayByteSequence; import org.apache.accumulo.core.data.ColumnUpdate; import org.apache.accumulo.core.data.Mutation; -import org.apache.accumulo.core.security.ColumnVisibility; -import org.apache.accumulo.core.security.VisibilityEvaluator; -import org.apache.accumulo.core.security.VisibilityParseException; -import org.apache.accumulo.core.util.BadArgumentException; /** * A constraint that checks the visibility of columns against the actor's authorizations. Violation @@ -64,7 +63,7 @@ public class VisibilityConstraint implements Constraint { ok = new HashSet<>(); } - VisibilityEvaluator ve = null; + AccessEvaluator ve = null; for (ColumnUpdate update : updates) { @@ -78,14 +77,15 @@ public class VisibilityConstraint implements Constraint { try { if (ve == null) { - ve = new VisibilityEvaluator(env.getAuthorizationsContainer()); + var authContainer = env.getAuthorizationsContainer(); + ve = AccessEvaluator.of(auth -> authContainer.contains(new ArrayByteSequence(auth))); } - if (!ve.evaluate(new ColumnVisibility(cv))) { + if (!ve.canAccess(cv)) { return Collections.singletonList((short) 2); } - } catch (BadArgumentException | VisibilityParseException bae) { + } catch (IllegalAccessExpressionException iaee) { return Collections.singletonList((short) 1); } diff --git a/core/src/main/java/org/apache/accumulo/core/iterators/user/TransformingIterator.java b/core/src/main/java/org/apache/accumulo/core/iterators/user/TransformingIterator.java index be8d63330e..10dffb0083 100644 --- a/core/src/main/java/org/apache/accumulo/core/iterators/user/TransformingIterator.java +++ b/core/src/main/java/org/apache/accumulo/core/iterators/user/TransformingIterator.java @@ -30,6 +30,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.IllegalAccessExpressionException; import org.apache.accumulo.core.client.IteratorSetting; import org.apache.accumulo.core.conf.ConfigurationTypeHelper; import org.apache.accumulo.core.data.ByteSequence; @@ -43,10 +46,6 @@ import org.apache.accumulo.core.iterators.OptionDescriber; import org.apache.accumulo.core.iterators.SortedKeyValueIterator; import org.apache.accumulo.core.iterators.WrappingIterator; import org.apache.accumulo.core.security.Authorizations; -import org.apache.accumulo.core.security.ColumnVisibility; -import org.apache.accumulo.core.security.VisibilityEvaluator; -import org.apache.accumulo.core.security.VisibilityParseException; -import org.apache.accumulo.core.util.BadArgumentException; import org.apache.accumulo.core.util.Pair; import org.apache.commons.collections4.map.LRUMap; import org.apache.hadoop.io.Text; @@ -103,7 +102,7 @@ public abstract class TransformingIterator extends WrappingIterator implements O protected Collection<ByteSequence> seekColumnFamilies; protected boolean seekColumnFamiliesInclusive; - private VisibilityEvaluator ve = null; + private AccessEvaluator ve = null; private LRUMap<ByteSequence,Boolean> visibleCache = null; private LRUMap<ByteSequence,Boolean> parsedVisibilitiesCache = null; private long maxBufferSize; @@ -118,7 +117,7 @@ public abstract class TransformingIterator extends WrappingIterator implements O if (scanning) { String auths = options.get(AUTH_OPT); if (auths != null && !auths.isEmpty()) { - ve = new VisibilityEvaluator(new Authorizations(auths.getBytes(UTF_8))); + ve = AccessEvaluator.of(new Authorizations(auths.getBytes(UTF_8)).toAccessAuthorizations()); visibleCache = new LRUMap<>(100); } } @@ -409,13 +408,12 @@ public abstract class TransformingIterator extends WrappingIterator implements O // Ensure that the visibility (which could have been transformed) parses. Must always do this // check, even if visibility is not evaluated. ByteSequence visibility = key.getColumnVisibilityData(); - ColumnVisibility colVis = null; Boolean parsed = parsedVisibilitiesCache.get(visibility); if (parsed == null) { try { - colVis = new ColumnVisibility(visibility.toArray()); + AccessExpression.validate(visibility.toArray()); parsedVisibilitiesCache.put(visibility, Boolean.TRUE); - } catch (BadArgumentException e) { + } catch (IllegalAccessExpressionException e) { log.error("Parse error after transformation : {}", visibility); parsedVisibilitiesCache.put(visibility, Boolean.FALSE); if (scanning) { @@ -441,12 +439,9 @@ public abstract class TransformingIterator extends WrappingIterator implements O visible = visibleCache.get(visibility); if (visible == null) { try { - if (colVis == null) { - colVis = new ColumnVisibility(visibility.toArray()); - } - visible = ve.evaluate(colVis); + visible = ve.canAccess(visibility.toArray()); visibleCache.put(visibility, visible); - } catch (VisibilityParseException | BadArgumentException e) { + } catch (IllegalAccessExpressionException e) { log.error("Parse Error", e); visible = Boolean.FALSE; } 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 c91009bb6f..92fb75a72d 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 @@ -23,6 +23,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.util.Map; +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.IllegalAccessExpressionException; import org.apache.accumulo.core.client.IteratorSetting; import org.apache.accumulo.core.data.ByteSequence; import org.apache.accumulo.core.data.Key; @@ -32,10 +35,6 @@ import org.apache.accumulo.core.iterators.IteratorEnvironment; import org.apache.accumulo.core.iterators.OptionDescriber; import org.apache.accumulo.core.iterators.SortedKeyValueIterator; import org.apache.accumulo.core.security.Authorizations; -import org.apache.accumulo.core.security.ColumnVisibility; -import org.apache.accumulo.core.security.VisibilityEvaluator; -import org.apache.accumulo.core.security.VisibilityParseException; -import org.apache.accumulo.core.util.BadArgumentException; import org.apache.commons.collections4.map.LRUMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +44,7 @@ import org.slf4j.LoggerFactory; */ public class VisibilityFilter extends Filter implements OptionDescriber { - protected VisibilityEvaluator ve; + private AccessEvaluator accessEvaluator; protected Map<ByteSequence,Boolean> cache; private static final Logger log = LoggerFactory.getLogger(VisibilityFilter.class); @@ -66,7 +65,8 @@ public class VisibilityFilter extends Filter implements OptionDescriber { String auths = options.get(AUTHS); Authorizations authObj = auths == null || auths.isEmpty() ? new Authorizations() : new Authorizations(auths.getBytes(UTF_8)); - this.ve = new VisibilityEvaluator(authObj); + + this.accessEvaluator = AccessEvaluator.of(authObj.toAccessAuthorizations()); } this.cache = new LRUMap<>(1000); } @@ -80,10 +80,10 @@ public class VisibilityFilter extends Filter implements OptionDescriber { return b; } try { - new ColumnVisibility(testVis.toArray()); + AccessExpression.validate(testVis.toArray()); cache.put(testVis, true); return true; - } catch (BadArgumentException e) { + } catch (IllegalAccessExpressionException e) { cache.put(testVis, false); return false; } @@ -98,10 +98,10 @@ public class VisibilityFilter extends Filter implements OptionDescriber { } try { - boolean bb = ve.evaluate(new ColumnVisibility(testVis.toArray())); + boolean bb = accessEvaluator.canAccess(testVis.toArray()); cache.put(testVis, bb); return bb; - } catch (VisibilityParseException | BadArgumentException e) { + } catch (IllegalAccessExpressionException e) { log.error("Parse Error", e); return false; } diff --git a/core/src/main/java/org/apache/accumulo/core/iteratorsImpl/system/VisibilityFilter.java b/core/src/main/java/org/apache/accumulo/core/iteratorsImpl/system/VisibilityFilter.java index 0a0fd5f4aa..22acd782ea 100644 --- a/core/src/main/java/org/apache/accumulo/core/iteratorsImpl/system/VisibilityFilter.java +++ b/core/src/main/java/org/apache/accumulo/core/iteratorsImpl/system/VisibilityFilter.java @@ -18,6 +18,8 @@ */ package org.apache.accumulo.core.iteratorsImpl.system; +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.IllegalAccessExpressionException; import org.apache.accumulo.core.data.ArrayByteSequence; import org.apache.accumulo.core.data.ByteSequence; import org.apache.accumulo.core.data.Key; @@ -26,10 +28,6 @@ import org.apache.accumulo.core.iterators.IteratorEnvironment; import org.apache.accumulo.core.iterators.SortedKeyValueIterator; import org.apache.accumulo.core.iterators.SynchronizedServerFilter; import org.apache.accumulo.core.security.Authorizations; -import org.apache.accumulo.core.security.ColumnVisibility; -import org.apache.accumulo.core.security.VisibilityEvaluator; -import org.apache.accumulo.core.security.VisibilityParseException; -import org.apache.accumulo.core.util.BadArgumentException; import org.apache.commons.collections4.map.LRUMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +41,7 @@ import org.slf4j.LoggerFactory; * class. */ public class VisibilityFilter extends SynchronizedServerFilter { - protected VisibilityEvaluator ve; + protected AccessEvaluator ve; protected ByteSequence defaultVisibility; protected LRUMap<ByteSequence,Boolean> cache; protected Authorizations authorizations; @@ -53,7 +51,7 @@ public class VisibilityFilter extends SynchronizedServerFilter { private VisibilityFilter(SortedKeyValueIterator<Key,Value> iterator, Authorizations authorizations, byte[] defaultVisibility) { super(iterator); - this.ve = new VisibilityEvaluator(authorizations); + this.ve = AccessEvaluator.of(authorizations.toAccessAuthorizations()); this.authorizations = authorizations; this.defaultVisibility = new ArrayByteSequence(defaultVisibility); this.cache = new LRUMap<>(1000); @@ -80,14 +78,11 @@ public class VisibilityFilter extends SynchronizedServerFilter { } try { - boolean bb = ve.evaluate(new ColumnVisibility(testVis.toArray())); + boolean bb = ve.canAccess(testVis.toArray()); cache.put(testVis, bb); return bb; - } catch (VisibilityParseException e) { - log.error("VisibilityParseException with visibility of Key: {}", k, e); - return false; - } catch (BadArgumentException e) { - log.error("BadArgumentException with visibility of Key: {}", k, e); + } catch (IllegalAccessExpressionException e) { + log.error("IllegalAccessExpressionException with visibility of Key: {}", k, e); return false; } } diff --git a/core/src/main/java/org/apache/accumulo/core/security/Authorizations.java b/core/src/main/java/org/apache/accumulo/core/security/Authorizations.java index 1cfeebb8b9..363a0586f8 100644 --- a/core/src/main/java/org/apache/accumulo/core/security/Authorizations.java +++ b/core/src/main/java/org/apache/accumulo/core/security/Authorizations.java @@ -385,4 +385,24 @@ public class Authorizations implements Iterable<byte[]>, Serializable, Authoriza return sb.toString(); } + + private static final org.apache.accumulo.access.Authorizations EMPTY_ACCESS_AUTH = + org.apache.accumulo.access.Authorizations.of(Set.of()); + + /** + * Converts to an Accumulo Access Authorizations object. + * + * @since 3.1.0 + */ + public org.apache.accumulo.access.Authorizations toAccessAuthorizations() { + if (auths.isEmpty()) { + return EMPTY_ACCESS_AUTH; + } else { + Set<String> auths = new HashSet<>(authsList.size()); + for (var auth : authsList) { + auths.add(new String(auth, UTF_8)); + } + return org.apache.accumulo.access.Authorizations.of(auths); + } + } } diff --git a/core/src/main/java/org/apache/accumulo/core/security/ColumnVisibility.java b/core/src/main/java/org/apache/accumulo/core/security/ColumnVisibility.java index 4b8784be02..338cf1396c 100644 --- a/core/src/main/java/org/apache/accumulo/core/security/ColumnVisibility.java +++ b/core/src/main/java/org/apache/accumulo/core/security/ColumnVisibility.java @@ -27,7 +27,10 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.TreeSet; +import java.util.function.Supplier; +import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.IllegalAccessExpressionException; import org.apache.accumulo.core.data.ArrayByteSequence; import org.apache.accumulo.core.data.ByteSequence; import org.apache.accumulo.core.util.BadArgumentException; @@ -35,6 +38,8 @@ import org.apache.accumulo.core.util.TextUtil; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.WritableComparator; +import com.google.common.base.Suppliers; + /** * Validate the column visibility is a valid expression and set the visibility for a Mutation. See * {@link ColumnVisibility#ColumnVisibility(byte[])} for the definition of an expression. @@ -76,8 +81,10 @@ import org.apache.hadoop.io.WritableComparator; */ public class ColumnVisibility { - Node node = null; - private byte[] expression; + // This functionality is deprecated so its setup as a supplier so it is only computed if the + // deprecated functionality is called. + private final Supplier<Node> nodeSupplier; + private final byte[] expression; /** * Accessor for the underlying byte string. @@ -91,6 +98,7 @@ public class ColumnVisibility { /** * The node types in a parse tree for a visibility expression. */ + @Deprecated(since = "3.1.0") public enum NodeType { EMPTY, TERM, OR, AND, } @@ -103,6 +111,7 @@ public class ColumnVisibility { /** * A node in the parse tree for a visibility expression. */ + @Deprecated(since = "3.1.0") public static class Node { /** * An empty list of nodes. @@ -169,6 +178,7 @@ public class ColumnVisibility { * A node comparator. Nodes sort according to node type, terms sort lexicographically. AND and OR * nodes sort by number of children, or if the same by corresponding children. */ + @Deprecated(since = "3.1.0") public static class NodeComparator implements Comparator<Node>, Serializable { private static final long serialVersionUID = 1L; @@ -216,6 +226,7 @@ public class ColumnVisibility { * Convenience method that delegates to normalize with a new NodeComparator constructed using the * supplied expression. */ + @Deprecated(since = "3.1.0") public static Node normalize(Node root, byte[] expression) { return normalize(root, expression, new NodeComparator(expression)); } @@ -228,6 +239,7 @@ public class ColumnVisibility { * 3) dedupes labels (`a&b&a` becomes `a&b`) */ // @formatter:on + @Deprecated(since = "3.1.0") public static Node normalize(Node root, byte[] expression, NodeComparator comparator) { if (root.type != NodeType.TERM) { TreeSet<Node> rolledUp = new TreeSet<>(comparator); @@ -256,6 +268,7 @@ public class ColumnVisibility { * Walks an expression's AST and appends a string representation to a supplied StringBuilder. This * method adds parens where necessary. */ + @Deprecated(since = "3.1.0") public static void stringify(Node root, byte[] expression, StringBuilder out) { if (root.type == NodeType.TERM) { out.append(new String(expression, root.start, root.end - root.start, UTF_8)); @@ -282,13 +295,12 @@ public class ColumnVisibility { * * @return normalized expression in byte[] form */ + @Deprecated(since = "3.1.0") public byte[] flatten() { - Node normRoot = normalize(node, expression); - StringBuilder builder = new StringBuilder(expression.length); - stringify(normRoot, expression, builder); - return builder.toString().getBytes(UTF_8); + return AccessExpression.of(expression, true).getExpression().getBytes(UTF_8); } + @Deprecated private static class ColumnVisibilityParser { private int index = 0; private int parens = 0; @@ -455,16 +467,17 @@ public class ColumnVisibility { } } - private void validate(byte[] expression) { + private Node createNodeTree(byte[] expression) { if (expression != null && expression.length > 0) { ColumnVisibilityParser p = new ColumnVisibilityParser(); - node = p.parse(expression); + return p.parse(expression); } else { - node = EMPTY_NODE; + return EMPTY_NODE; } - this.expression = expression; } + private static final byte[] EMPTY_BYTES = new byte[0]; + /** * Creates an empty visibility. Normally, elements with empty visibility can be seen by everyone. * Though, one could change this behavior with filters. @@ -472,7 +485,8 @@ public class ColumnVisibility { * @see #ColumnVisibility(String) */ public ColumnVisibility() { - this(new byte[] {}); + expression = EMPTY_BYTES; + nodeSupplier = Suppliers.memoize(() -> createNodeTree(expression)); } /** @@ -496,13 +510,34 @@ public class ColumnVisibility { } /** - * Creates a column visibility for a Mutation from a string already encoded in UTF-8 bytes. + * Creates a column visibility for a Mutation from bytes already encoded in UTF-8. * * @param expression visibility expression, encoded as UTF-8 bytes * @see #ColumnVisibility(String) */ public ColumnVisibility(byte[] expression) { - validate(expression); + this.expression = expression; + try { + AccessExpression.validate(this.expression); + } catch (IllegalAccessExpressionException e) { + // This is thrown for compatability with the exception this class used to throw when it parsed + // exceptions itself. + throw new BadArgumentException(e); + } + nodeSupplier = Suppliers.memoize(() -> createNodeTree(this.expression)); + } + + /** + * Creates a column visibility for a Mutation from an AccessExpression. + * + * @param expression visibility expression, encoded as UTF-8 bytes + * @see #ColumnVisibility(String) + * @since 3.1.0 + */ + public ColumnVisibility(AccessExpression expression) { + // AccessExpression is a validated immutable object, so no need to re validate + this.expression = expression.getExpression().getBytes(UTF_8); + nodeSupplier = Suppliers.memoize(() -> createNodeTree(this.expression)); } @Override @@ -542,8 +577,9 @@ public class ColumnVisibility { * * @return parse tree node */ + @Deprecated(since = "3.1.0") public Node getParseTree() { - return node; + return nodeSupplier.get(); } /** @@ -564,9 +600,11 @@ public class ColumnVisibility { * * @param term term to quote * @return quoted term (unquoted if unnecessary) + * @deprecated use {@link AccessExpression#quote(String)} */ + @Deprecated(since = "3.1.0") public static String quote(String term) { - return new String(quote(term.getBytes(UTF_8)), UTF_8); + return AccessExpression.quote(term); } /** @@ -576,21 +614,10 @@ public class ColumnVisibility { * @param term term to quote, encoded as UTF-8 bytes * @return quoted term (unquoted if unnecessary), encoded as UTF-8 bytes * @see #quote(String) + * @deprecated use {@link AccessExpression#quote(byte[])} */ + @Deprecated(since = "3.1.0") public static byte[] quote(byte[] term) { - boolean needsQuote = false; - - for (byte b : term) { - if (!Authorizations.isValidAuthChar(b)) { - needsQuote = true; - break; - } - } - - if (!needsQuote) { - return term; - } - - return VisibilityEvaluator.escape(term, true); + return AccessExpression.quote(term); } } diff --git a/core/src/main/java/org/apache/accumulo/core/security/VisibilityEvaluator.java b/core/src/main/java/org/apache/accumulo/core/security/VisibilityEvaluator.java index dd07d64086..aa5d85f530 100644 --- a/core/src/main/java/org/apache/accumulo/core/security/VisibilityEvaluator.java +++ b/core/src/main/java/org/apache/accumulo/core/security/VisibilityEvaluator.java @@ -18,92 +18,18 @@ */ package org.apache.accumulo.core.security; -import java.util.ArrayList; - +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.IllegalAccessExpressionException; import org.apache.accumulo.core.data.ArrayByteSequence; -import org.apache.accumulo.core.data.ByteSequence; -import org.apache.accumulo.core.security.ColumnVisibility.Node; /** * A class which evaluates visibility expressions against a set of authorizations. + * + * @deprecated since 3.1.0 Use Accumulo Access library instead */ +@Deprecated(since = "3.1.0") public class VisibilityEvaluator { - private AuthorizationContainer auths; - - /** - * Authorizations in column visibility expression are in escaped form. Column visibility parsing - * does not unescape. This class wraps an AuthorizationContainer and unescapes auths before - * checking the wrapped container. - */ - private static class UnescapingAuthorizationContainer implements AuthorizationContainer { - - private AuthorizationContainer wrapped; - - UnescapingAuthorizationContainer(AuthorizationContainer wrapee) { - this.wrapped = wrapee; - } - - @Override - public boolean contains(ByteSequence auth) { - return wrapped.contains(unescape(auth)); - } - } - - static ByteSequence unescape(ByteSequence auth) { - int escapeCharCount = 0; - for (int i = 0; i < auth.length(); i++) { - byte b = auth.byteAt(i); - if (b == '"' || b == '\\') { - escapeCharCount++; - } - } - - if (escapeCharCount > 0) { - if (escapeCharCount % 2 == 1) { - throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); - } - - byte[] unescapedCopy = new byte[auth.length() - escapeCharCount / 2]; - int pos = 0; - for (int i = 0; i < auth.length(); i++) { - byte b = auth.byteAt(i); - if (b == '\\') { - i++; - b = auth.byteAt(i); - if (b != '"' && b != '\\') { - throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); - } - } else if (b == '"') { - // should only see quote after a slash - throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); - } - - unescapedCopy[pos++] = b; - } - - return new ArrayByteSequence(unescapedCopy); - } else { - return auth; - } - } - - /** - * Creates a new {@link Authorizations} object with escaped forms of the authorizations in the - * given object. - * - * @param auths original authorizations - * @return authorizations object with escaped authorization strings - * @see #escape(byte[], boolean) - */ - static Authorizations escape(Authorizations auths) { - ArrayList<byte[]> retAuths = new ArrayList<>(auths.getAuthorizations().size()); - - for (byte[] auth : auths.getAuthorizations()) { - retAuths.add(escape(auth, false)); - } - - return new Authorizations(retAuths); - } + private final AccessEvaluator accessEvaluator; /** * Properly escapes an authorization string. The string can be quoted if desired. @@ -147,7 +73,9 @@ public class VisibilityEvaluator { * @since 1.7.0 */ public VisibilityEvaluator(AuthorizationContainer authsContainer) { - this.auths = new UnescapingAuthorizationContainer(authsContainer); + // TODO need to look into efficiency and correctness of this + this.accessEvaluator = + AccessEvaluator.of(auth -> authsContainer.contains(new ArrayByteSequence(auth))); } /** @@ -157,7 +85,7 @@ public class VisibilityEvaluator { * @param authorizations authorizations object */ public VisibilityEvaluator(Authorizations authorizations) { - this.auths = escape(authorizations); + this.accessEvaluator = AccessEvaluator.of(authorizations.toAccessAuthorizations()); } /** @@ -171,42 +99,12 @@ public class VisibilityEvaluator { * subexpression is of an unknown type */ public boolean evaluate(ColumnVisibility visibility) throws VisibilityParseException { - // The VisibilityEvaluator computes a trie from the given Authorizations, that ColumnVisibility - // expressions can be evaluated against. - return evaluate(visibility.getExpression(), visibility.getParseTree()); - } - - private final boolean evaluate(final byte[] expression, final Node root) - throws VisibilityParseException { - if (expression.length == 0) { - return true; - } - switch (root.type) { - case TERM: - return auths.contains(root.getTerm(expression)); - case AND: - if (root.children == null || root.children.size() < 2) { - throw new VisibilityParseException("AND has less than 2 children", expression, - root.start); - } - for (Node child : root.children) { - if (!evaluate(expression, child)) { - return false; - } - } - return true; - case OR: - if (root.children == null || root.children.size() < 2) { - throw new VisibilityParseException("OR has less than 2 children", expression, root.start); - } - for (Node child : root.children) { - if (evaluate(expression, child)) { - return true; - } - } - return false; - default: - throw new VisibilityParseException("No such node type", expression, root.start); + try { + return accessEvaluator.canAccess(visibility.getExpression()); + } catch (IllegalAccessExpressionException e) { + // This is thrown for compatability with the exception this class used to evaluate expressions + // itself. + throw new VisibilityParseException(e); } } } diff --git a/core/src/main/java/org/apache/accumulo/core/security/VisibilityParseException.java b/core/src/main/java/org/apache/accumulo/core/security/VisibilityParseException.java index 297e575fb1..46dc634db8 100644 --- a/core/src/main/java/org/apache/accumulo/core/security/VisibilityParseException.java +++ b/core/src/main/java/org/apache/accumulo/core/security/VisibilityParseException.java @@ -22,6 +22,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.text.ParseException; +import org.apache.accumulo.access.IllegalAccessExpressionException; + /** * An exception thrown when a visibility string cannot be parsed. */ @@ -41,6 +43,15 @@ public class VisibilityParseException extends ParseException { this.visibility = new String(visibility, UTF_8); } + /** + * @since 3.1.0 + */ + public VisibilityParseException(IllegalAccessExpressionException e) { + // TODO need to look at output for this + super(e.getDescription(), e.getIndex()); + this.visibility = e.getPattern(); + } + @Override public String getMessage() { return super.getMessage() + " in string '" + visibility + "' at position " diff --git a/core/src/main/java/org/apache/accumulo/core/util/BadArgumentException.java b/core/src/main/java/org/apache/accumulo/core/util/BadArgumentException.java index ac9cafe1e4..88e1b6f3a6 100644 --- a/core/src/main/java/org/apache/accumulo/core/util/BadArgumentException.java +++ b/core/src/main/java/org/apache/accumulo/core/util/BadArgumentException.java @@ -20,10 +20,17 @@ package org.apache.accumulo.core.util; import java.util.regex.PatternSyntaxException; +import org.apache.accumulo.access.IllegalAccessExpressionException; + public final class BadArgumentException extends PatternSyntaxException { private static final long serialVersionUID = 1L; public BadArgumentException(String desc, String badarg, int index) { super(desc, badarg, index); } + + public BadArgumentException(IllegalAccessExpressionException e) { + super(e.getDescription(), e.getPattern(), e.getIndex()); + super.initCause(e); + } } diff --git a/core/src/test/java/org/apache/accumulo/core/data/constraints/VisibilityConstraintTest.java b/core/src/test/java/org/apache/accumulo/core/data/constraints/VisibilityConstraintTest.java index 70f10b9600..2f31877701 100644 --- a/core/src/test/java/org/apache/accumulo/core/data/constraints/VisibilityConstraintTest.java +++ b/core/src/test/java/org/apache/accumulo/core/data/constraints/VisibilityConstraintTest.java @@ -43,11 +43,13 @@ public class VisibilityConstraintTest { Environment env; Mutation mutation; - static final ColumnVisibility good = new ColumnVisibility("good"); - static final ColumnVisibility bad = new ColumnVisibility("bad"); + static final ColumnVisibility good = new ColumnVisibility("good|bad"); + static final ColumnVisibility bad = new ColumnVisibility("good&bad"); static final String D = "don't care"; + static final List<Short> ILLEGAL = Arrays.asList((short) 1); + static final List<Short> ENOAUTH = Arrays.asList((short) 2); @BeforeEach @@ -98,4 +100,11 @@ public class VisibilityConstraintTest { assertEquals(ENOAUTH, vc.check(env, mutation), "unauthorized"); } + @Test + public void testIllegalVisibility() { + mutation.put(D, D, good, D); + // set an illegal visibility string + mutation.at().family(D).qualifier(D).visibility("good&").put(D); + assertEquals(ILLEGAL, vc.check(env, mutation), "unauthorized"); + } } diff --git a/core/src/test/java/org/apache/accumulo/core/security/ColumnVisibilityTest.java b/core/src/test/java/org/apache/accumulo/core/security/ColumnVisibilityTest.java index e37af5b265..2723f3abe7 100644 --- a/core/src/test/java/org/apache/accumulo/core/security/ColumnVisibilityTest.java +++ b/core/src/test/java/org/apache/accumulo/core/security/ColumnVisibilityTest.java @@ -63,6 +63,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testEmptyFlatten() { // empty visibility is valid new ColumnVisibility().flatten(); @@ -87,6 +88,7 @@ public class ColumnVisibilityTest { shouldThrow("a*b"); } + @SuppressWarnings("deprecation") public void normalized(String... values) { for (int i = 0; i < values.length; i += 2) { ColumnVisibility cv = new ColumnVisibility(values[i].getBytes()); @@ -161,6 +163,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testToString() { ColumnVisibility cv = new ColumnVisibility(quote("a")); assertEquals("[a]", cv.toString()); @@ -171,6 +174,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testParseTree() { Node node = parse("(W)|(U&V)"); assertNode(node, NodeType.OR, 0, 9); @@ -179,12 +183,14 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testParseTreeWithNoChildren() { Node node = parse("ABC"); assertNode(node, NodeType.TERM, 0, 3); } @Test + @SuppressWarnings("deprecation") public void testParseTreeWithTwoChildren() { Node node = parse("ABC|DEF"); assertNode(node, NodeType.OR, 0, 7); @@ -193,6 +199,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testParseTreeWithParenthesesAndTwoChildren() { Node node = parse("(ABC|DEF)"); assertNode(node, NodeType.OR, 1, 8); @@ -201,6 +208,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testParseTreeWithParenthesizedChildren() { Node node = parse("ABC|(DEF&GHI)"); assertNode(node, NodeType.OR, 0, 13); @@ -211,6 +219,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testParseTreeWithMoreParentheses() { Node node = parse("(W)|(U&V)"); assertNode(node, NodeType.OR, 0, 9); @@ -221,6 +230,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testEmptyParseTreesAreEqual() { Comparator<Node> comparator = new NodeComparator(new byte[] {}); Node empty = new ColumnVisibility().getParseTree(); @@ -228,6 +238,7 @@ public class ColumnVisibilityTest { } @Test + @SuppressWarnings("deprecation") public void testParseTreesOrdering() { byte[] expression = "(b&c&d)|((a|m)&y&z)|(e&f)".getBytes(UTF_8); byte[] flattened = new ColumnVisibility(expression).flatten(); @@ -238,11 +249,13 @@ public class ColumnVisibilityTest { assertTrue(flat.indexOf('b') < flat.indexOf('a'), "shortest children sort first"); } + @SuppressWarnings("deprecation") private Node parse(String s) { ColumnVisibility v = new ColumnVisibility(s); return v.getParseTree(); } + @SuppressWarnings("deprecation") private void assertNode(Node node, NodeType nodeType, int start, int end) { assertEquals(node.type, nodeType); assertEquals(start, node.start); diff --git a/core/src/test/java/org/apache/accumulo/core/security/VisibilityEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/core/security/VisibilityEvaluatorTest.java index 637fb6877d..31ae8ea3ed 100644 --- a/core/src/test/java/org/apache/accumulo/core/security/VisibilityEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/core/security/VisibilityEvaluatorTest.java @@ -21,15 +21,12 @@ package org.apache.accumulo.core.security; import static org.apache.accumulo.core.security.ColumnVisibility.quote; 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; -import java.util.List; - -import org.apache.accumulo.core.data.ArrayByteSequence; import org.apache.accumulo.core.util.ByteArraySet; import org.junit.jupiter.api.Test; +@SuppressWarnings("deprecation") public class VisibilityEvaluatorTest { @Test @@ -100,25 +97,6 @@ public class VisibilityEvaluatorTest { assertEquals("\"五十\"", quote("五十")); } - @Test - public void testUnescape() { - assertEquals("a\"b", VisibilityEvaluator.unescape(new ArrayByteSequence("a\\\"b")).toString()); - assertEquals("a\\b", VisibilityEvaluator.unescape(new ArrayByteSequence("a\\\\b")).toString()); - assertEquals("a\\\"b", - VisibilityEvaluator.unescape(new ArrayByteSequence("a\\\\\\\"b")).toString()); - assertEquals("\\\"", - VisibilityEvaluator.unescape(new ArrayByteSequence("\\\\\\\"")).toString()); - assertEquals("a\\b\\c\\d", - VisibilityEvaluator.unescape(new ArrayByteSequence("a\\\\b\\\\c\\\\d")).toString()); - - final String message = "Expected failure to unescape invalid escape sequence"; - final var invalidEscapeSeqList = List.of(new ArrayByteSequence("a\\b"), - new ArrayByteSequence("a\\b\\c"), new ArrayByteSequence("a\"b\\")); - - invalidEscapeSeqList.forEach(seq -> assertThrows(IllegalArgumentException.class, - () -> VisibilityEvaluator.unescape(seq), message)); - } - @Test public void testNonAscii() throws VisibilityParseException { VisibilityEvaluator ct = new VisibilityEvaluator(new Authorizations("五", "六", "八", "九", "五十")); diff --git a/pom.xml b/pom.xml index 901e9db280..014424cdc9 100644 --- a/pom.xml +++ b/pom.xml @@ -145,6 +145,7 @@ <surefire.reuseForks>true</surefire.reuseForks> <unitTestMemSize>-Xmx1G</unitTestMemSize> <!-- dependency and plugin versions managed with properties --> + <version.accumulo-access>1.0.0-beta</version.accumulo-access> <version.auto-service>1.1.1</version.auto-service> <version.bouncycastle>1.78.1</version.bouncycastle> <version.curator>5.5.0</version.curator> @@ -315,6 +316,11 @@ <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> + <dependency> + <groupId>org.apache.accumulo</groupId> + <artifactId>accumulo-access</artifactId> + <version>${version.accumulo-access}</version> + </dependency> <dependency> <groupId>org.apache.accumulo</groupId> <artifactId>accumulo-compaction-coordinator</artifactId>