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-access.git

commit bb5a9db151a3001b30f2c3e96ecf1007b864a138
Author: Keith Turner <ktur...@apache.org>
AuthorDate: Thu Sep 7 13:01:56 2023 -0400

    Initial commit of accumulo-access
    
    This is a work in progress to create a new stand alone library that offers
    the functionality behind ColumnVisibility and VisibilityEvaluator
    from Accumulo.
    
    This commit was created by taking a subset of the changes in draft PR
    https://github.com/apache/accumulo/pull/3715
    
    Co-authored-by: Dave Marion <dlmar...@apache.org>
---
 .gitignore                                         |  36 ++
 LICENSE                                            | 334 +++++++++++
 README.md                                          |  42 ++
 SPECIFICATION.md                                   |  66 +++
 pom.xml                                            |  70 +++
 .../apache/accumulo/access/AccessEvaluator.java    | 139 +++++
 .../accumulo/access/AccessEvaluatorImpl.java       | 284 +++++++++
 .../apache/accumulo/access/AccessExpression.java   | 114 ++++
 .../accumulo/access/AccessExpressionImpl.java      | 637 +++++++++++++++++++++
 .../org/apache/accumulo/access/Authorizations.java |  62 ++
 .../org/apache/accumulo/access/BytesWrapper.java   | 136 +++++
 .../accumulo/access/CachingAccessEvaluator.java    |  60 ++
 .../access/IllegalAccessExpressionException.java   |  34 ++
 .../accumulo/access/AccessEvaluatorTest.java       | 195 +++++++
 .../accumulo/access/AccessExpressionImplTest.java  | 112 ++++
 src/test/resources/testdata.json                   | 378 ++++++++++++
 16 files changed, 2699 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..55d7f58
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# Maven ignores
+/target/
+
+# IDE ignores
+/.settings/
+/.project
+/.classpath
+/.pydevproject
+/.idea
+/*.iml
+/*.ipr
+/*.iws
+/nbproject/
+/nbactions.xml
+/nb-configuration.xml
+/.vscode/
+/.factorypath
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..411ba86
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,334 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+**********
+
+# APACHE ACCUMULO SUBCOMPONENTS
+
+The Apache Accumulo project contains subcomponents with separate
+copyright notices and license terms. Your use of these is subject
+to the terms and conditions of the following licenses.
+
+## Software from the European Commission project OneLab
+
+Files:
+* core/src/main/java/org/apache/accumulo/core/bloomfilter/*
+
+    Copyright (c) 2005, European Commission project OneLab under contract 
034819
+    (http://www.one-lab.org)
+
+    All rights reserved.
+    Redistribution and use in source and binary forms, with or
+    without modification, are permitted provided that the following
+    conditions are met:
+     - Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+     - Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in
+       the documentation and/or other materials provided with the distribution.
+     - Neither the name of the University Catholique de Louvain - UCL
+       nor the names of its contributors may be used to endorse or
+       promote products derived from this software without specific prior
+       written permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+    POSSIBILITY OF SUCH DAMAGE.
+
+## JQuery 3.6.1 (https://jquery.com/)
+
+Files:
+* 
server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/jquery/*
+
+    jQuery JavaScript Library v3.6.1
+    https://jquery.com/
+
+    Includes Sizzle.js
+    https://sizzlejs.com/
+
+    Copyright JS Foundation and other contributors
+    Released under the MIT license
+    https://jquery.org/license
+
+    Date: 2022-08-26T17:52Z
+
+    Text of the MIT License:
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use,
+    copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+    OTHER DEALINGS IN THE SOFTWARE.
+
+## Flot 4.2.3 (https://github.com/flot/flot)
+
+Files:
+* 
server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/flot/*
+
+    Copyright (c) 2007-2014 IOLA and Ole Laursen
+
+    Available under the MIT License
+    (see 
server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/flot/LICENSE.txt)
+
+    Flot bundles additional works:
+
+    jquery.flot.pie.js
+        Flot plugin for rendering pie charts.
+
+        Copyright (c) 2007-2014 IOLA and Ole Laursen.
+        Licensed under the MIT license.
+
+        * Created by Brian Medendorp
+        * Updated with contributions from btburnett3, Anthony Aragues and Xavi 
Ivars
+
+    jquery.flot.resize.js
+    * Inline dependency:
+     * jQuery resize event - v1.1 - 3/14/2010
+     * http://benalman.com/projects/jquery-resize-plugin/
+     *
+     * Copyright (c) 2010 "Cowboy" Ben Alman
+     * Dual licensed under the MIT and GPL licenses.
+     * http://benalman.com/about/license/
+
+## Bootstrap v5.0.2 (https://getbootstrap.com/)
+
+Files:
+* 
server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/bootstrap/**/*
+
+    Copyright 2011-2021 The Bootstrap Authors 
(https://github.com/twbs/bootstrap/graphs/contributors)
+    Copyright 2011-2021 Twitter, Inc.
+    Licensed under the MIT license (see above)
+
+## DataTables 1.12.1 (https://datatables.net)
+
+Files:
+* 
server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/external/datatables/**/*
+
+    Copyright (c) 2008-2022 SpryMedia Ltd
+    Licensed under the MIT license (see above)
+
+**********
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..83e9e15
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+
+# accumulo-access
+Accumulo Access Control Library
+
+This project is a work in progress with the following goals.
+
+ * Create a standalone java library that offers the Accumulo visibility 
functionality
+ * Support the same syntax and semantics as ColumnVisibility and 
VisibilityEvaluator initially.  This will allow ColumnVisibility and 
VisibilityEvaluator to adapt to use this new library for their implementation.
+ * Have no dependencies for this new library
+ * Use no external types (like Hadoop types) in its API.
+ * Use semantic versioning.
+
+The following types constitute the public API of this library.  All other 
types are package private and are not part of the public API.
+
+  * 
[IllegalAccessExpressionException](src/main/java/org/apache/accumulo/access/IllegalAccessExpressionException.java).
+  * 
[AccessEvaluator](src/main/java/org/apache/accumulo/access/AccessEvaluator.java).
+  * 
[AccessExpression](src/main/java/org/apache/accumulo/access/AccessExpression.java).
+  * 
[Authorizations](src/main/java/org/apache/accumulo/access/Authorizations.java).
+
+For an example of using this library see the [unit 
test](src/test/java/org/apache/accumulo/access/AccessEvaluatorTest.java).
+
+See the [specification][SPECIFICATION.md] for details about access expressions.
diff --git a/SPECIFICATION.md b/SPECIFICATION.md
new file mode 100644
index 0000000..3e043f6
--- /dev/null
+++ b/SPECIFICATION.md
@@ -0,0 +1,66 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+
+# AccessExpression Specification
+
+This document specifies the format of an Apache Accumulo AccessExpression. An 
AccessExpression
+is an encoding of a boolean expression of the attributes that a subject is 
required to have to
+access a particular piece of data.
+
+## Syntax
+
+The formal definition of the AccessExpression UTF-8 string representation is 
provided by
+the following [ABNF][1]:
+
+```
+access-expression       = [expression] ; empty string is a valid access 
expression
+
+expression              =  and-expression / or-expression
+
+and-expression          =  and-expression and-operator and-expression
+and-expression          =/ lparen expression rparen
+and-expression          =/ access-token
+
+or-expression           =  or-expression or-operator or-expression
+or-expression           =/ lparen expression rparen
+or-expression           =/ access-token
+
+access-token            = 1*( ALPHA / DIGIT / "_" / "-" / "." / ":" / slash )
+access-token            =/ DQUOTE 1*(utf8-subset / escaped) DQUOTE
+
+utf8-subset             = %x20-21 / %x23-5B / %5D-7E / UVCHARBEYONDASCII ; 
utf8 minus '"' and '\'
+escaped                 = "\" DQUOTE / "\\"
+slash                   = "/"
+or-operator             = "|"
+and-operator            = "&"
+lparen                  = "("
+rparen                  = ")"
+```
+
+The definition of utf8 was borrowed from this [ietf document][2].  TODO that 
doc defines unicode and not utf8
+
+## Serialization
+
+An AccessExpression is a UTF-8 string. It can be serialized using a byte array 
as long as it
+can be deserialized back into the same UTF-8 string.
+
+[1]: https://www.rfc-editor.org/rfc/rfc5234
+[2]: 
https://datatracker.ietf.org/doc/html/draft-seantek-unicode-in-abnf-03#section-4.2
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..3095563
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache</groupId>
+    <artifactId>apache</artifactId>
+    <version>30</version>
+  </parent>
+
+  <groupId>org.apache.accumulo</groupId>
+  <artifactId>accumulo-access</artifactId>
+  <name>Apache Accumulo Access</name>
+  <version>1.0-SNAPSHOT</version>
+
+  <properties>
+    <maven.compiler.source>11</maven.compiler.source>
+    <maven.compiler.target>11</maven.compiler.target>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.github.spotbugs</groupId>
+      <artifactId>spotbugs-annotations</artifactId>
+      <version>4.7.3</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.10.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <version>5.9.2</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>net.revelc.code</groupId>
+       <artifactId>apilyzer-maven-plugin</artifactId>
+       <version>1.3.0</version>
+        <executions>
+          <execution>
+            <id>apilyzer</id>
+            <goals>
+              <goal>analyze</goal>
+            </goals>
+            <configuration>
+              <includes>
+                
<include>org[.]apache[.]accumulo[.]access[.]IllegalAccessExpressionException</include>
+                
<include>org[.]apache[.]accumulo[.]access[.]AccessExpression</include>
+                
<include>org[.]apache[.]accumulo[.]access[.]AccessEvaluator</include>
+                
<include>org[.]apache[.]accumulo[.]access[.]Authorizations</include>
+              </includes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/src/main/java/org/apache/accumulo/access/AccessEvaluator.java 
b/src/main/java/org/apache/accumulo/access/AccessEvaluator.java
new file mode 100644
index 0000000..0ec696b
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/AccessEvaluator.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * <p>
+ * An implementation of the Accumulo visibility standard as specified in this 
document (TODO write
+ * the document based on current Accumulo implementation and post somewhere).
+ *
+ * <p>
+ * Below is an example that should print false and then print true.
+ *
+ * <pre>
+ * {@code
+ * var evaluator = VisibilityArbiter.builder().authorizations("ALPHA", 
"OMEGA").build();
+ *
+ * System.out.println(evaluator.canAccess("ALPHA&BETA"));
+ * System.out.println(evaluator.canAccess("(ALPHA|BETA)&(OMEGA|EPSILON)"));
+ * }
+ * </pre>
+ *
+ *
+ * @since ???
+ */
+public interface AccessEvaluator {
+  /**
+   * @return true if the expression is visible using the authorizations 
supplied at creation, false
+   *         otherwise
+   * @throws IllegalArgumentException when the expression is not valid
+   */
+  boolean canAccess(String accessExpression) throws 
IllegalAccessExpressionException;
+
+  boolean canAccess(byte[] accessExpression) throws 
IllegalAccessExpressionException;
+
+  /**
+   * TODO documnet that may be more efficient
+   */
+  boolean canAccess(AccessExpression accessExpression) throws 
IllegalAccessExpressionException;
+
+  /**
+   * @since ???
+   */
+  interface Authorizer {
+    boolean isAuthorized(String auth);
+  }
+
+  interface AuthorizationsBuilder {
+
+    ExecutionBuilder authorizations(Authorizations authorizations);
+
+    /**
+     * Allows providing multiple sets of authorizations. Each expression will 
be evaluated
+     * independently against each set of authorizations and will only be 
deemed accessible if
+     * accessible for all. For example the following code would print false, 
true, and then false.
+     *
+     * <pre>
+     *     {@code
+     * Collection<Authorizations> authSets =
+     *     List.of(Authorizations.of("A", "B"), Authorizations.of("C", "D"));
+     * var evaluator = 
AccessEvaluator.builder().authorizations(authSets).build();
+     *
+     * System.out.println(evaluator.canAccess("A"));
+     * System.out.println(evaluator.canAccess("A|D"));
+     * System.out.println(evaluator.canAccess("A&D"));
+     *
+     * }
+     * </pre>
+     *
+     * <p>
+     * The following table shows how each expression in the example above will 
evaluate for each
+     * authorization set. In order to return true for {@code canAccess()} the 
expression must
+     * evaluate to true for each authorization set.
+     *
+     * <table>
+     * <caption>Evaluations</caption>
+     * <tr>
+     * <td></td>
+     * <td>[A,B]</td>
+     * <td>[C,D]</td>
+     * </tr>
+     * <tr>
+     * <td>A</td>
+     * <td>True</td>
+     * <td>False</td>
+     * </tr>
+     * <tr>
+     * <td>A|D</td>
+     * <td>True</td>
+     * <td>True</td>
+     * </tr>
+     * <tr>
+     * <td>A&amp;D</td>
+     * <td>False</td>
+     * <td>False</td>
+     * </tr>
+     *
+     * </table>
+     *
+     *
+     *
+     */
+    ExecutionBuilder authorizations(Collection<Authorizations> authorizations);
+
+    ExecutionBuilder authorizations(String... authorizations);
+
+    ExecutionBuilder authorizations(Authorizer authorizer);
+  }
+
+  interface ExecutionBuilder extends FinalBuilder {
+    ExecutionBuilder cacheSize(int cacheSize);
+  }
+
+  interface FinalBuilder {
+    AccessEvaluator build();
+  }
+
+  static AuthorizationsBuilder builder() {
+    return AccessEvaluatorImpl.builder();
+  }
+}
diff --git a/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java 
b/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java
new file mode 100644
index 0000000..ced77c4
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toSet;
+import static java.util.stream.Collectors.toUnmodifiableList;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+//this class is intentionally package private and should never be made public
+class AccessEvaluatorImpl implements AccessEvaluator {
+  private final Collection<Predicate<BytesWrapper>> authorizedPredicates;
+
+  private AccessEvaluatorImpl(Authorizer authorizationChecker) {
+    this.authorizedPredicates = List.of(auth -> 
authorizationChecker.isAuthorized(unescape(auth)));
+  }
+
+  public AccessEvaluatorImpl(Collection<List<byte[]>> authorizationSets) {
+    authorizedPredicates = authorizationSets.stream()
+        .map(authorizations -> authorizations.stream()
+            .map(auth -> AccessEvaluatorImpl.escape(auth, 
false)).map(BytesWrapper::new)
+            .collect(toSet()))
+        .map(escapedAuths -> (Predicate<BytesWrapper>) escapedAuths::contains)
+        .collect(Collectors.toList());
+  }
+
+  static String unescape(BytesWrapper 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 String(unescapedCopy, UTF_8);
+    } else {
+      return auth.toString();
+    }
+  }
+
+  /**
+   * Properly escapes an authorization string. The string can be quoted if 
desired.
+   *
+   * @param auth authorization string, as UTF-8 encoded bytes
+   * @param quote true to wrap escaped authorization in quotes
+   * @return escaped authorization string
+   */
+  static byte[] escape(byte[] auth, boolean quote) {
+    int escapeCount = 0;
+
+    for (byte value : auth) {
+      if (value == '"' || value == '\\') {
+        escapeCount++;
+      }
+    }
+
+    if (escapeCount > 0 || quote) {
+      byte[] escapedAuth = new byte[auth.length + escapeCount + (quote ? 2 : 
0)];
+      int index = quote ? 1 : 0;
+      for (byte b : auth) {
+        if (b == '"' || b == '\\') {
+          escapedAuth[index++] = '\\';
+        }
+        escapedAuth[index++] = b;
+      }
+
+      if (quote) {
+        escapedAuth[0] = '"';
+        escapedAuth[escapedAuth.length - 1] = '"';
+      }
+
+      auth = escapedAuth;
+    }
+    return auth;
+  }
+
+  @Override
+  public boolean canAccess(String expression) throws IllegalArgumentException {
+
+    return evaluate(new AccessExpressionImpl(expression));
+
+  }
+
+  @Override
+  public boolean canAccess(byte[] expression) throws IllegalArgumentException {
+
+    return evaluate(new AccessExpressionImpl(expression));
+
+  }
+
+  @Override
+  public boolean canAccess(AccessExpression expression) throws 
IllegalArgumentException {
+    if (expression instanceof AccessExpressionImpl) {
+
+      return evaluate((AccessExpressionImpl) expression);
+
+    } else {
+      return canAccess(expression.getExpression());
+    }
+  }
+
+  public boolean evaluate(AccessExpressionImpl visibility) throws 
IllegalAccessExpressionException {
+    // The VisibilityEvaluator computes a trie from the given Authorizations, 
that ColumnVisibility
+    // expressions can be evaluated against.
+    return authorizedPredicates.stream()
+        .allMatch(ap -> evaluate(ap, visibility.getExpressionBytes(), 
visibility.getParseTree()));
+  }
+
+  private static boolean evaluate(Predicate<BytesWrapper> authorizedPredicate,
+      final byte[] expression, final AccessExpressionImpl.Node root)
+      throws IllegalAccessExpressionException {
+    if (expression.length == 0) {
+      return true;
+    }
+    switch (root.type) {
+      case TERM:
+        return authorizedPredicate.test(root.getTerm(expression));
+      case AND:
+        if (root.children == null || root.children.size() < 2) {
+          throw new IllegalAccessExpressionException("AND has less than 2 
children",
+              root.getTerm(expression).toString(), root.start);
+        }
+        for (AccessExpressionImpl.Node child : root.children) {
+          if (!evaluate(authorizedPredicate, expression, child)) {
+            return false;
+          }
+        }
+        return true;
+      case OR:
+        if (root.children == null || root.children.size() < 2) {
+          throw new IllegalAccessExpressionException("OR has less than 2 
children",
+              root.getTerm(expression).toString(), root.start);
+        }
+        for (AccessExpressionImpl.Node child : root.children) {
+          if (evaluate(authorizedPredicate, expression, child)) {
+            return true;
+          }
+        }
+        return false;
+      default:
+        throw new IllegalAccessExpressionException("No such node type",
+            root.getTerm(expression).toString(), root.start);
+    }
+  }
+
+  private static class BuilderImpl
+      implements AuthorizationsBuilder, FinalBuilder, ExecutionBuilder {
+
+    private Authorizer authorizationsChecker;
+
+    private Collection<List<byte[]>> authorizationSets;
+    private int cacheSize = 0;
+
+    private void setAuthorizations(List<byte[]> auths) {
+      setAuthorizations(Collections.singletonList(auths));
+    }
+
+    private void setAuthorizations(Collection<List<byte[]>> authSets) {
+      if (authorizationsChecker != null) {
+        throw new IllegalStateException("Cannot set checker and 
authorizations");
+      }
+
+      for (List<byte[]> auths : authSets) {
+        for (byte[] auth : auths) {
+          if (auth.length == 0) {
+            throw new IllegalArgumentException("Empty authorization");
+          }
+        }
+      }
+      this.authorizationSets = authSets;
+    }
+
+    @Override
+    public ExecutionBuilder authorizations(Authorizations authorizations) {
+      setAuthorizations(authorizations.asSet().stream().map(auth -> 
auth.getBytes(UTF_8))
+          .collect(toUnmodifiableList()));
+      return this;
+    }
+
+    @Override
+    public ExecutionBuilder authorizations(Collection<Authorizations> 
authorizationSets) {
+      setAuthorizations(authorizationSets
+          .stream().map(authorizations -> authorizations.asSet().stream()
+              .map(auth -> auth.getBytes(UTF_8)).collect(toUnmodifiableList()))
+          .collect(Collectors.toList()));
+      return this;
+    }
+
+    @Override
+    public ExecutionBuilder authorizations(String... authorizations) {
+      setAuthorizations(Stream.of(authorizations).map(auth -> 
auth.getBytes(UTF_8))
+          .collect(toUnmodifiableList()));
+      return this;
+    }
+
+    @Override
+    public ExecutionBuilder authorizations(Authorizer authorizationChecker) {
+      if (authorizationSets != null) {
+        throw new IllegalStateException("Cannot set checker and 
authorizations");
+      }
+      this.authorizationsChecker = authorizationChecker;
+      return this;
+    }
+
+    @Override
+    public ExecutionBuilder cacheSize(int cacheSize) {
+      if (cacheSize < 0) {
+        throw new IllegalArgumentException();
+      }
+      this.cacheSize = cacheSize;
+      return this;
+    }
+
+    @Override
+    public AccessEvaluator build() {
+      if (authorizationSets != null ^ authorizationsChecker == null) {
+        throw new IllegalStateException();
+      }
+
+      AccessEvaluator accessEvaluator;
+      if (authorizationsChecker != null) {
+        accessEvaluator = new AccessEvaluatorImpl(authorizationsChecker);
+      } else {
+        accessEvaluator = new AccessEvaluatorImpl(authorizationSets);
+      }
+
+      if (cacheSize > 0) {
+        accessEvaluator = new CachingAccessEvaluator(accessEvaluator, 
cacheSize);
+      }
+      return accessEvaluator;
+    }
+
+  }
+
+  public static AuthorizationsBuilder builder() {
+    return new BuilderImpl();
+  }
+}
diff --git a/src/main/java/org/apache/accumulo/access/AccessExpression.java 
b/src/main/java/org/apache/accumulo/access/AccessExpression.java
new file mode 100644
index 0000000..2a5625f
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/AccessExpression.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+/**
+ * An opaque type that contains a parsed visibility expression. When this type 
is constructed with
+ * {@link #of(String)} and then used with {@link 
AccessEvaluator#canAccess(AccessExpression)} it can
+ * be more efficient and avoid reparsing the expression.
+ *
+ * <p>
+ * For reviewers : this type is similar to ColumnVisibility. This interface 
and impl have goal of
+ * being immutable which differs from column visibility. ColumnVisibility 
leaks internal
+ * implementation details in its public API, this type does not.
+ *
+ * TODO needs better javadoc.
+ *
+ * Below is an example of using this API.
+ *
+ * <pre>
+ *     {@code
+ * var auth1 = AccessExpression.quote("CAT");
+ * var auth2 = AccessExpression.quote("πŸ¦•");
+ * var auth3 = AccessExpression.quote("πŸ¦–");
+ * var visExp = AccessExpression
+ *     .of("(" + auth1 + "&" + auth3 + ")|(" + auth1 + "&" + auth2 + "&" + 
auth1 + ")");
+ * System.out.println(visExp.getExpression());
+ * System.out.println(visExp.normalize());
+ * System.out.println(visExp.getAuthorizations());
+ * }
+ * </pre>
+ *
+ * The above example will print the following.
+ *
+ * <pre>
+ * (CAT&amp;"πŸ¦–")|(CAT&amp;"πŸ¦•"&amp;CAT)
+ * ("πŸ¦•"&amp;CAT)|("πŸ¦–"&amp;CAT)
+ * [πŸ¦–, CAT, πŸ¦•]
+ * </pre>
+ *
+ * @since ???
+ */
+// TODO could name VisibilityLabel
+public interface AccessExpression {
+
+  /**
+   * @return the expression that was used to create this object.
+   */
+  String getExpression();
+
+  /**
+   * TODO give examples
+   *
+   * @return A normalized version of the visibility expression that removes 
duplicates and orders
+   *         the expression in a consistent way.
+   */
+  String normalize();
+
+  /**
+   * @return the unique authorizations that occur in the expression. For 
example, for the expression
+   *         {@code (A&B)|(A&C)|(A&D)} this method would return {@code 
[A,B,C,D]]}
+   */
+  Authorizations getAuthorizations();
+
+  static AccessExpression of(String expression) throws 
IllegalAccessExpressionException {
+    return new AccessExpressionImpl(expression);
+  }
+
+  // TODO document utf8 expectations
+  static AccessExpression of(byte[] expression) throws 
IllegalAccessExpressionException {
+    return new AccessExpressionImpl(expression);
+  }
+
+  /**
+   * @return an empty VisibilityExpression.
+   */
+  static AccessExpression of() {
+    return AccessExpressionImpl.EMPTY;
+  }
+
+  /**
+   * Authorizations occurring a visibility expression can only contain the 
characters TODO unless
+   * quoted. Use this method to quote authorizations that occur in a 
visibility expression. This
+   * method will only quote if its needed.
+   */
+  static byte[] quote(byte[] authorization) {
+    return AccessExpressionImpl.quote(authorization);
+  }
+
+  /**
+   * Authorizations occurring a visibility expression can only contain the 
characters TODO unless
+   * quoted. Use this method to quote authorizations that occur in a 
visibility expression. This
+   * method will only quote if its needed.
+   */
+  static String quote(String authorization) {
+    return AccessExpressionImpl.quote(authorization);
+  }
+
+}
diff --git a/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java 
b/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java
new file mode 100644
index 0000000..d21dd82
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java
@@ -0,0 +1,637 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Validate the column visibility is a valid expression and set the visibility 
for a Mutation. See
+ * {@link AccessExpressionImpl#AccessExpressionImpl(byte[])} for the 
definition of an expression.
+ *
+ * <p>
+ * The expression is a sequence of characters from the set [A-Za-z0-9_-.] 
along with the binary
+ * operators "&amp;" and "|" indicating that both operands are necessary, or 
the either is
+ * necessary. The following are valid expressions for visibility:
+ *
+ * <pre>
+ * A
+ * A|B
+ * (A|B)&amp;(C|D)
+ * orange|(red&amp;yellow)
+ * </pre>
+ *
+ * <p>
+ * The following are not valid expressions for visibility:
+ *
+ * <pre>
+ * A|B&amp;C
+ * A=B
+ * A|B|
+ * A&amp;|B
+ * ()
+ * )
+ * dog|!cat
+ * </pre>
+ *
+ * <p>
+ * In addition to the base set of visibilities, any character can be used in 
the expression if it is
+ * quoted. If the quoted term contains '&quot;' or '\', then escape the 
character with '\'. The
+ * {@link #quote(String)} method can be used to properly quote and escape 
terms automatically. The
+ * following is an example of a quoted term:
+ *
+ * <pre>
+ * &quot;A#C&quot; &amp; B
+ * </pre>
+ */
+class AccessExpressionImpl implements AccessExpression {
+
+  Node node = null;
+  private byte[] expression;
+
+  private final AtomicReference<String> expressionString = new 
AtomicReference<>(null);
+
+  @Override
+  public String getExpression() {
+    var expStr = expressionString.get();
+    if (expStr != null) {
+      return expStr;
+    }
+
+    return expressionString.updateAndGet(es -> es == null ? new 
String(expression, UTF_8) : es);
+  }
+
+  byte[] getExpressionBytes() {
+    return expression;
+  }
+
+  /**
+   * The node types in a parse tree for a visibility expression.
+   */
+  enum NodeType {
+    EMPTY, TERM, OR, AND,
+  }
+
+  /**
+   * All empty nodes are equal and represent the same value.
+   */
+  private static final Node EMPTY_NODE = new Node(NodeType.EMPTY, 0);
+
+  // must create this after creating EMPTY_NODE
+  static final AccessExpression EMPTY = new AccessExpressionImpl("");
+
+  /**
+   * A node in the parse tree for a visibility expression.
+   */
+  static class Node {
+    /**
+     * An empty list of nodes.
+     */
+    public static final List<Node> EMPTY = Collections.emptyList();
+    NodeType type;
+    int start;
+    int end;
+    List<Node> children = EMPTY;
+
+    public Node(NodeType type, int start) {
+      this.type = type;
+      this.start = start;
+      this.end = start + 1;
+    }
+
+    public Node(int start, int end) {
+      this.type = NodeType.TERM;
+      this.start = start;
+      this.end = end;
+    }
+
+    public void add(Node child) {
+      if (children == EMPTY) {
+        children = new ArrayList<>();
+      }
+
+      children.add(child);
+    }
+
+    public NodeType getType() {
+      return type;
+    }
+
+    public List<Node> getChildren() {
+      return children;
+    }
+
+    public BytesWrapper getTerm(byte[] expression) {
+      if (type != NodeType.TERM) {
+        throw new IllegalStateException();
+      }
+
+      if (expression[start] == '"') {
+        // its a quoted term
+        int qStart = start + 1;
+        int qEnd = end - 1;
+
+        return new BytesWrapper(expression, qStart, qEnd - qStart);
+      }
+      return new BytesWrapper(expression, start, end - start);
+    }
+  }
+
+  /**
+   * 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.
+   */
+  static class NodeComparator implements Comparator<Node>, Serializable {
+
+    private static final long serialVersionUID = 1L;
+    byte[] text;
+
+    /**
+     * Creates a new comparator.
+     *
+     * @param text expression string, encoded in UTF-8
+     */
+    public NodeComparator(byte[] text) {
+      this.text = text;
+    }
+
+    @Override
+    public int compare(Node a, Node b) {
+      int diff = a.type.ordinal() - b.type.ordinal();
+      if (diff != 0) {
+        return diff;
+      }
+      switch (a.type) {
+        case EMPTY:
+          return 0; // All empty nodes are the same
+        case TERM:
+          return Arrays.compare(text, a.start, a.end, text, b.start, b.end);
+        case OR:
+        case AND:
+          diff = a.children.size() - b.children.size();
+          if (diff != 0) {
+            return diff;
+          }
+          for (int i = 0; i < a.children.size(); i++) {
+            diff = compare(a.children.get(i), b.children.get(i));
+            if (diff != 0) {
+              return diff;
+            }
+          }
+      }
+      return 0;
+    }
+  }
+
+  /*
+   * Convenience method that delegates to normalize with a new NodeComparator 
constructed using the
+   * supplied expression.
+   */
+  private static Node normalize(Node root, byte[] expression) {
+    return normalize(root, new NodeComparator(expression));
+  }
+
+  // @formatter:off
+    /*
+     * Walks an expression's AST in order to:
+     *  1) roll up expressions with the same operant (`a&(b&c) becomes a&b&c`)
+     *  2) sort labels lexicographically (permutations of `a&b&c` are 
re-ordered to appear as `a&b&c`)
+     *  3) dedupes labels (`a&b&a` becomes `a&b`)
+     */
+    // @formatter:on
+  private static Node normalize(Node root, NodeComparator comparator) {
+    if (root.type != NodeType.TERM) {
+      TreeSet<Node> rolledUp = new TreeSet<>(comparator);
+      java.util.Iterator<Node> itr = root.children.iterator();
+      while (itr.hasNext()) {
+        Node c = normalize(itr.next(), comparator);
+        if (c.type == root.type) {
+          rolledUp.addAll(c.children);
+          itr.remove();
+        }
+      }
+      rolledUp.addAll(root.children);
+      root.children.clear();
+      root.children.addAll(rolledUp);
+
+      // need to promote a child if it's an only child
+      if (root.children.size() == 1) {
+        return root.children.get(0);
+      }
+    }
+
+    return root;
+  }
+
+  /*
+   * Walks an expression's AST and appends a string representation to a 
supplied StringBuilder. This
+   * method adds parens where necessary.
+   */
+  private 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));
+    } else {
+      String sep = "";
+      for (Node c : root.children) {
+        out.append(sep);
+        boolean parens = (c.type != NodeType.TERM && root.type != c.type);
+        if (parens) {
+          out.append("(");
+        }
+        stringify(c, expression, out);
+        if (parens) {
+          out.append(")");
+        }
+        sep = root.type == NodeType.AND ? "&" : "|";
+      }
+    }
+  }
+
+  @Override
+  public String normalize() {
+    Node normRoot = normalize(node, expression);
+    StringBuilder builder = new StringBuilder(expression.length);
+    stringify(normRoot, expression, builder);
+    return builder.toString();
+  }
+
+  @Override
+  public Authorizations getAuthorizations() {
+    HashSet<String> auths = new HashSet<>();
+    findAuths(node, expression, auths);
+    return Authorizations.of(auths);
+  }
+
+  private void findAuths(Node node, byte[] expression, HashSet<String> 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).toString());
+        break;
+      case EMPTY:
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown node type " + 
node.getType());
+    }
+  }
+
+  private static class ColumnVisibilityParser {
+    private int index = 0;
+    private int parens = 0;
+
+    public ColumnVisibilityParser() {}
+
+    Node parse(byte[] expression) {
+      if (expression.length > 0) {
+        Node node = parse_(expression);
+        if (node == null) {
+          throw new IllegalAccessExpressionException("operator or missing 
parens",
+              new String(expression, UTF_8), index - 1);
+        }
+        if (parens != 0) {
+          throw new IllegalAccessExpressionException("parenthesis mis-match",
+              new String(expression, UTF_8), index - 1);
+        }
+        return node;
+      }
+      return null;
+    }
+
+    Node processTerm(int start, int end, Node expr, byte[] expression) {
+      if (start != end) {
+        if (expr != null) {
+          throw new IllegalAccessExpressionException("expression needs | or &",
+              new String(expression, UTF_8), start);
+        }
+        return new Node(start, end);
+      }
+      if (expr == null) {
+        throw new IllegalAccessExpressionException("empty term", new 
String(expression, UTF_8),
+            start);
+      }
+      return expr;
+    }
+
+    Node parse_(byte[] expression) {
+      Node result = null;
+      Node expr = null;
+      int wholeTermStart = index;
+      int subtermStart = index;
+      boolean subtermComplete = false;
+
+      while (index < expression.length) {
+        switch (expression[index++]) {
+          case '&':
+            expr = processTerm(subtermStart, index - 1, expr, expression);
+            if (result != null) {
+              if (!result.type.equals(NodeType.AND)) {
+                throw new IllegalAccessExpressionException("cannot mix & and 
|",
+                    new String(expression, UTF_8), index - 1);
+              }
+            } else {
+              result = new Node(NodeType.AND, wholeTermStart);
+            }
+            result.add(expr);
+            expr = null;
+            subtermStart = index;
+            subtermComplete = false;
+            break;
+          case '|':
+            expr = processTerm(subtermStart, index - 1, expr, expression);
+            if (result != null) {
+              if (!result.type.equals(NodeType.OR)) {
+                throw new IllegalAccessExpressionException("cannot mix | and 
&",
+                    new String(expression, UTF_8), index - 1);
+              }
+            } else {
+              result = new Node(NodeType.OR, wholeTermStart);
+            }
+            result.add(expr);
+            expr = null;
+            subtermStart = index;
+            subtermComplete = false;
+            break;
+          case '(':
+            parens++;
+            if (subtermStart != index - 1 || expr != null) {
+              throw new IllegalAccessExpressionException("expression needs & 
or |",
+                  new String(expression, UTF_8), index - 1);
+            }
+            expr = parse_(expression);
+            subtermStart = index;
+            subtermComplete = false;
+            break;
+          case ')':
+            parens--;
+            Node child = processTerm(subtermStart, index - 1, expr, 
expression);
+            if (child == null && result == null) {
+              throw new IllegalAccessExpressionException("empty expression not 
allowed",
+                  new String(expression, UTF_8), index);
+            }
+            if (result == null) {
+              return child;
+            }
+            if (result.type == child.type) {
+              for (Node c : child.children) {
+                result.add(c);
+              }
+            } else {
+              result.add(child);
+            }
+            result.end = index - 1;
+            return result;
+          case '"':
+            if (subtermStart != index - 1) {
+              throw new IllegalAccessExpressionException("expression needs & 
or |",
+                  new String(expression, UTF_8), index - 1);
+            }
+
+            while (index < expression.length && expression[index] != '"') {
+              if (expression[index] == '\\') {
+                index++;
+                if (index == expression.length
+                    || (expression[index] != '\\' && expression[index] != 
'"')) {
+                  throw new IllegalAccessExpressionException("invalid escaping 
within quotes",
+                      new String(expression, UTF_8), index - 1);
+                }
+              }
+              index++;
+            }
+
+            if (index == expression.length) {
+              throw new IllegalAccessExpressionException("unclosed quote",
+                  new String(expression, UTF_8), subtermStart);
+            }
+
+            if (subtermStart + 1 == index) {
+              throw new IllegalAccessExpressionException("empty term",
+                  new String(expression, UTF_8), subtermStart);
+            }
+
+            index++;
+
+            subtermComplete = true;
+
+            break;
+          default:
+            if (subtermComplete) {
+              throw new IllegalAccessExpressionException("expression needs & 
or |",
+                  new String(expression, UTF_8), index - 1);
+            }
+
+            byte c = expression[index - 1];
+            if (!isValidAuthChar(c)) {
+              throw new IllegalAccessExpressionException("bad character (" + c 
+ ")",
+                  new String(expression, UTF_8), index - 1);
+            }
+        }
+      }
+      Node child = processTerm(subtermStart, index, expr, expression);
+      if (result != null) {
+        result.add(child);
+        result.end = index;
+      } else {
+        result = child;
+      }
+      if (result.type != NodeType.TERM) {
+        if (result.children.size() < 2) {
+          throw new IllegalAccessExpressionException("missing term", new 
String(expression, UTF_8),
+              index);
+        }
+      }
+      return result;
+    }
+  }
+
+  private void validate(byte[] expression) {
+    // TODO does not seem like null should be accepted
+    if (expression != null && expression.length > 0) {
+      ColumnVisibilityParser p = new ColumnVisibilityParser();
+      node = p.parse(expression);
+    } else {
+      node = EMPTY_NODE;
+    }
+    this.expression = expression;
+  }
+
+  /**
+   * Creates an empty visibility. Normally, elements with empty visibility can 
be seen by everyone.
+   * Though, one could change this behavior with filters.
+   *
+   * @see #AccessExpressionImpl(String)
+   */
+  AccessExpressionImpl() {
+    this(new byte[] {});
+  }
+
+  /**
+   * Creates a column visibility for a Mutation.
+   *
+   * @param expression An expression of the rights needed to see this 
mutation. The expression
+   *        syntax is defined at the class-level documentation
+   */
+  AccessExpressionImpl(String expression) {
+    this(expression.getBytes(UTF_8));
+    expressionString.set(expression);
+  }
+
+  /**
+   * Creates a column visibility for a Mutation from a string already encoded 
in UTF-8 bytes.
+   *
+   * @param expression visibility expression, encoded as UTF-8 bytes
+   * @see #AccessExpressionImpl(String)
+   */
+  AccessExpressionImpl(byte[] expression) {
+    // TODO copy bytes to make immutable?
+    validate(expression);
+  }
+
+  @Override
+  public String toString() {
+    return "[" + new String(expression, UTF_8) + "]";
+  }
+
+  /**
+   * See {@link #equals(AccessExpressionImpl)}
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof AccessExpressionImpl) {
+      return equals((AccessExpressionImpl) obj);
+    }
+    return false;
+  }
+
+  /**
+   * Compares two ColumnVisibilities for string equivalence, not as a 
meaningful comparison of terms
+   * and conditions.
+   *
+   * @param otherLe other column visibility
+   * @return true if this visibility equals the other via string comparison
+   */
+  boolean equals(AccessExpressionImpl otherLe) {
+    return Arrays.equals(expression, otherLe.expression);
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(expression);
+  }
+
+  /**
+   * Gets the parse tree for this column visibility.
+   *
+   * @return parse tree node
+   */
+  Node getParseTree() {
+    return node;
+  }
+
+  /**
+   * Properly quotes terms in a column visibility expression. If no quoting is 
needed, then nothing
+   * is done.
+   *
+   * <p>
+   * Examples of using quote :
+   *
+   * <pre>
+   * import static org.apache.accumulo.core.security.ColumnVisibility.quote;
+   *   .
+   *   .
+   *   .
+   * String s = quote(&quot;A#C&quot;) + &quot;&amp;&quot; + 
quote(&quot;FOO&quot;);
+   * ColumnVisibility cv = new ColumnVisibility(s);
+   * </pre>
+   *
+   * @param term term to quote
+   * @return quoted term (unquoted if unnecessary)
+   */
+  static String quote(String term) {
+    return new String(quote(term.getBytes(UTF_8)), UTF_8);
+  }
+
+  /**
+   * Properly quotes terms in a column visibility expression. If no quoting is 
needed, then nothing
+   * is done.
+   *
+   * @param term term to quote, encoded as UTF-8 bytes
+   * @return quoted term (unquoted if unnecessary), encoded as UTF-8 bytes
+   * @see #quote(String)
+   */
+  static byte[] quote(byte[] term) {
+    boolean needsQuote = false;
+
+    for (byte b : term) {
+      if (!isValidAuthChar(b)) {
+        needsQuote = true;
+        break;
+      }
+    }
+
+    if (!needsQuote) {
+      return term;
+    }
+
+    return AccessEvaluatorImpl.escape(term, true);
+  }
+
+  private static final boolean[] validAuthChars = new boolean[256];
+
+  static {
+    for (int i = 0; i < 256; i++) {
+      validAuthChars[i] = false;
+    }
+
+    for (int i = 'a'; i <= 'z'; i++) {
+      validAuthChars[i] = true;
+    }
+
+    for (int i = 'A'; i <= 'Z'; i++) {
+      validAuthChars[i] = true;
+    }
+
+    for (int i = '0'; i <= '9'; i++) {
+      validAuthChars[i] = true;
+    }
+
+    validAuthChars['_'] = true;
+    validAuthChars['-'] = true;
+    validAuthChars[':'] = true;
+    validAuthChars['.'] = true;
+    validAuthChars['/'] = true;
+  }
+
+  static final boolean isValidAuthChar(byte b) {
+    return validAuthChars[0xff & b];
+  }
+}
diff --git a/src/main/java/org/apache/accumulo/access/Authorizations.java 
b/src/main/java/org/apache/accumulo/access/Authorizations.java
new file mode 100644
index 0000000..a438c69
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/Authorizations.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ *
+ * @since ????
+ */
+public class Authorizations {
+  private final Set<String> authorizations;
+
+  private Authorizations(Set<String> authorizations) {
+    this.authorizations = Set.copyOf(authorizations);
+  }
+
+  public Set<String> asSet() {
+    return authorizations;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof Authorizations) {
+      var oa = (Authorizations) o;
+      return authorizations.equals(oa.authorizations);
+    }
+
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return authorizations.hashCode();
+  }
+
+  public static Authorizations of(String... authorizations) {
+    return new Authorizations(Set.of(authorizations));
+  }
+
+  public static Authorizations of(Collection<String> authorizations) {
+    return new Authorizations(Set.copyOf(authorizations));
+  }
+
+}
diff --git a/src/main/java/org/apache/accumulo/access/BytesWrapper.java 
b/src/main/java/org/apache/accumulo/access/BytesWrapper.java
new file mode 100644
index 0000000..598e4e2
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/BytesWrapper.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.Arrays;
+
+class BytesWrapper implements Comparable<BytesWrapper> {
+
+  protected byte[] data;
+  protected int offset;
+  protected int length;
+
+  /**
+   * Creates a new sequence. The given byte array is used directly as the 
backing array, so later
+   * changes made to the array reflect into the new sequence.
+   *
+   * @param data byte data
+   */
+  public BytesWrapper(byte[] data) {
+    this.data = data;
+    this.offset = 0;
+    this.length = data.length;
+  }
+
+  /**
+   * Creates a new sequence from a subsequence of the given byte array. The 
given byte array is used
+   * directly as the backing array, so later changes made to the (relevant 
portion of the) array
+   * reflect into the new sequence.
+   *
+   * @param data byte data
+   * @param offset starting offset in byte array (inclusive)
+   * @param length number of bytes to include in sequence
+   * @throws IllegalArgumentException if the offset or length are out of 
bounds for the given byte
+   *         array
+   */
+  public BytesWrapper(byte[] data, int offset, int length) {
+
+    if (offset < 0 || offset > data.length || length < 0 || (offset + length) 
> data.length) {
+      throw new IllegalArgumentException(" Bad offset and/or length 
data.length = " + data.length
+          + " offset = " + offset + " length = " + length);
+    }
+
+    this.data = data;
+    this.offset = offset;
+    this.length = length;
+
+  }
+
+  public byte byteAt(int i) {
+
+    if (i < 0) {
+      throw new IllegalArgumentException("i < 0, " + i);
+    }
+
+    if (i >= length) {
+      throw new IllegalArgumentException("i >= length, " + i + " >= " + 
length);
+    }
+
+    return data[offset + i];
+  }
+
+  public int length() {
+    return length;
+  }
+
+  public byte[] toArray() {
+    if (offset == 0 && length == data.length) {
+      return data;
+    }
+
+    byte[] copy = new byte[length];
+    System.arraycopy(data, offset, copy, 0, length);
+    return copy;
+  }
+
+  @Override
+  public int compareTo(BytesWrapper obs) {
+    return Arrays.compare(data, offset, offset + length(), obs.data, 
obs.offset,
+        obs.offset + obs.length());
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof BytesWrapper) {
+      BytesWrapper obs = (BytesWrapper) o;
+
+      if (this == o) {
+        return true;
+      }
+
+      if (length() != obs.length()) {
+        return false;
+      }
+
+      return compareTo(obs) == 0;
+    }
+
+    return false;
+
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = 1;
+
+    int end = offset + length();
+    for (int i = offset; i < end; i++) {
+      hash = (31 * hash) + data[i];
+    }
+
+    return hash;
+  }
+
+  @Override
+  public String toString() {
+    return new String(data, offset, length, UTF_8);
+  }
+}
diff --git 
a/src/main/java/org/apache/accumulo/access/CachingAccessEvaluator.java 
b/src/main/java/org/apache/accumulo/access/CachingAccessEvaluator.java
new file mode 100644
index 0000000..c82fd2c
--- /dev/null
+++ b/src/main/java/org/apache/accumulo/access/CachingAccessEvaluator.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+class CachingAccessEvaluator implements AccessEvaluator {
+
+  private final AccessEvaluator accessEvaluator;
+  private final LinkedHashMap<String,Boolean> cache;
+
+  CachingAccessEvaluator(AccessEvaluator accessEvaluator, int cacheSize) {
+    if (cacheSize <= 0) {
+      throw new IllegalArgumentException();
+    }
+    this.accessEvaluator = accessEvaluator;
+    this.cache = new LinkedHashMap<>(cacheSize, 0.75f, true) {
+      @Override
+      public boolean removeEldestEntry(Map.Entry<String,Boolean> entry) {
+        return size() > cacheSize;
+      }
+    };
+  }
+
+  @Override
+  public boolean canAccess(String expression) throws IllegalArgumentException {
+    return cache.computeIfAbsent(expression, accessEvaluator::canAccess);
+  }
+
+  @Override
+  public boolean canAccess(byte[] expression) throws IllegalArgumentException {
+    // TODO avoid converting to string, maybe create separate cache for byte 
arrays keys
+    return canAccess(new String(expression, UTF_8));
+  }
+
+  @Override
+  public boolean canAccess(AccessExpression expression) throws 
IllegalArgumentException {
+    return cache.computeIfAbsent(expression.getExpression(),
+        k -> accessEvaluator.canAccess(expression));
+  }
+}
diff --git 
a/src/main/java/org/apache/accumulo/access/IllegalAccessExpressionException.java
 
b/src/main/java/org/apache/accumulo/access/IllegalAccessExpressionException.java
new file mode 100644
index 0000000..77424b8
--- /dev/null
+++ 
b/src/main/java/org/apache/accumulo/access/IllegalAccessExpressionException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * TODO document
+ *
+ * @since ???
+ */
+public final class IllegalAccessExpressionException extends 
PatternSyntaxException {
+  private static final long serialVersionUID = 1L;
+
+  public IllegalAccessExpressionException(String desc, String badarg, int 
index) {
+    super(desc, badarg, index);
+  }
+}
diff --git a/src/test/java/org/apache/accumulo/access/AccessEvaluatorTest.java 
b/src/test/java/org/apache/accumulo/access/AccessEvaluatorTest.java
new file mode 100644
index 0000000..f49af8d
--- /dev/null
+++ b/src/test/java/org/apache/accumulo/access/AccessEvaluatorTest.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.accumulo.access.AccessExpression.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.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+public class AccessEvaluatorTest {
+
+  enum ExpectedResult {
+    ACCESSIBLE, INACCESSIBLE, ERROR
+  }
+
+  public static class TestExpressions {
+    ExpectedResult expectedResult;
+    String[] expressions;
+  }
+
+  public static class TestDataSet {
+    String description;
+
+    String[][] auths;
+
+    List<TestExpressions> tests;
+
+  }
+
+  private List<TestDataSet> readTestData() throws IOException {
+    try (var input = 
getClass().getClassLoader().getResourceAsStream("testdata.json")) {
+      if (input == null) {
+        throw new IllegalStateException("could not find resource : 
testdata.json");
+      }
+      var json = new String(input.readAllBytes(), UTF_8);
+
+      Type listType = new TypeToken<ArrayList<TestDataSet>>() {}.getType();
+      return new Gson().fromJson(json, listType);
+    }
+  }
+
+  @SuppressFBWarnings(value = {"UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"},
+      justification = "Field is written by Gson")
+  @Test
+  public void runTestCases() throws IOException {
+    List<TestDataSet> testData = readTestData();
+
+    assertFalse(testData.isEmpty());
+
+    for (var testSet : testData) {
+      AccessEvaluator evaluator;
+      assertTrue(testSet.auths.length >= 1);
+      if (testSet.auths.length == 1) {
+        evaluator = 
AccessEvaluator.builder().authorizations(testSet.auths[0]).build();
+        runTestCases(testSet, evaluator);
+
+        evaluator = 
AccessEvaluator.builder().authorizations(testSet.auths[0]).cacheSize(1).build();
+        runTestCases(testSet, evaluator);
+
+        evaluator =
+            
AccessEvaluator.builder().authorizations(testSet.auths[0]).cacheSize(10).build();
+        runTestCases(testSet, evaluator);
+
+        Set<String> auths = 
Stream.of(testSet.auths[0]).collect(Collectors.toSet());
+        evaluator = 
AccessEvaluator.builder().authorizations(auths::contains).build();
+        runTestCases(testSet, evaluator);
+      } else {
+        var authSets =
+            
Stream.of(testSet.auths).map(Authorizations::of).collect(Collectors.toList());
+        evaluator = AccessEvaluator.builder().authorizations(authSets).build();
+        runTestCases(testSet, evaluator);
+      }
+    }
+
+  }
+
+  @SuppressFBWarnings(value = {"UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"},
+      justification = "Field is written by Gson")
+  private static void runTestCases(TestDataSet testSet, AccessEvaluator 
evaluator) {
+
+    assertFalse(testSet.tests.isEmpty());
+
+    for (var tests : testSet.tests) {
+
+      assertTrue(tests.expressions.length > 0);
+
+      for (var expression : tests.expressions) {
+        switch (tests.expectedResult) {
+          case ACCESSIBLE:
+            assertTrue(evaluator.canAccess(expression), expression);
+            assertTrue(evaluator.canAccess(expression.getBytes(UTF_8)), 
expression);
+            assertTrue(evaluator.canAccess(AccessExpression.of(expression)), 
expression);
+            
assertTrue(evaluator.canAccess(AccessExpression.of(expression).normalize()),
+                expression);
+            break;
+          case INACCESSIBLE:
+            assertFalse(evaluator.canAccess(expression), expression);
+            assertFalse(evaluator.canAccess(expression.getBytes(UTF_8)), 
expression);
+            assertFalse(evaluator.canAccess(AccessExpression.of(expression)), 
expression);
+            
assertFalse(evaluator.canAccess(AccessExpression.of(expression).normalize()),
+                expression);
+            break;
+          case ERROR:
+            assertThrows(IllegalAccessExpressionException.class,
+                () -> evaluator.canAccess(expression), expression);
+            assertThrows(IllegalAccessExpressionException.class,
+                () -> evaluator.canAccess(expression.getBytes(UTF_8)), 
expression);
+            assertThrows(IllegalAccessExpressionException.class,
+                () -> evaluator.canAccess(AccessExpression.of(expression)), 
expression);
+            break;
+          default:
+            throw new IllegalArgumentException();
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testSpecialChars() {
+    // special chars do not need quoting
+    for (String qt : List.of("A_", "_", "A_C", "_C")) {
+      assertEquals(qt, quote(qt));
+      for (char c : new char[] {'/', ':', '-', '.'}) {
+        String qt2 = qt.replace('_', c);
+        assertEquals(qt2, quote(qt2));
+      }
+    }
+
+    assertEquals("a_b:c/d.e", quote("a_b:c/d.e"));
+  }
+
+  @Test
+  public void testQuote() {
+    assertEquals("\"A#C\"", quote("A#C"));
+    assertEquals("\"A\\\"C\"", quote("A\"C"));
+    assertEquals("\"A\\\"\\\\C\"", quote("A\"\\C"));
+    assertEquals("ACS", quote("ACS"));
+    assertEquals("\"九\"", quote("九"));
+    assertEquals("\"五十\"", quote("五十"));
+  }
+
+  private static String unescape(String s) {
+    return AccessEvaluatorImpl.unescape(new BytesWrapper(s.getBytes(UTF_8)));
+  }
+
+  @Test
+  public void testUnescape() {
+    assertEquals("a\"b", unescape("a\\\"b"));
+    assertEquals("a\\b", unescape("a\\\\b"));
+    assertEquals("a\\\"b", unescape("a\\\\\\\"b"));
+    assertEquals("\\\"", unescape("\\\\\\\""));
+    assertEquals("a\\b\\c\\d", unescape("a\\\\b\\\\c\\\\d"));
+
+    final String message = "Expected failure to unescape invalid escape 
sequence";
+    final var invalidEscapeSeqList = List.of("a\\b", "a\\b\\c", "a\"b\\");
+
+    invalidEscapeSeqList
+        .forEach(seq -> assertThrows(IllegalArgumentException.class, () -> 
unescape(seq), message));
+  }
+
+  // TODO need to copy all test from Accumulo
+}
diff --git 
a/src/test/java/org/apache/accumulo/access/AccessExpressionImplTest.java 
b/src/test/java/org/apache/accumulo/access/AccessExpressionImplTest.java
new file mode 100644
index 0000000..2175155
--- /dev/null
+++ b/src/test/java/org/apache/accumulo/access/AccessExpressionImplTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.access;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Comparator;
+
+import org.apache.accumulo.access.AccessExpressionImpl.Node;
+import org.apache.accumulo.access.AccessExpressionImpl.NodeComparator;
+import org.apache.accumulo.access.AccessExpressionImpl.NodeType;
+import org.junit.jupiter.api.Test;
+
+public class AccessExpressionImplTest {
+
+  @Test
+  public void testParseTree() {
+    Node node = parse("(W)|(U&V)");
+    assertNode(node, NodeType.OR, 0, 9);
+    assertNode(node.getChildren().get(0), NodeType.TERM, 1, 2);
+    assertNode(node.getChildren().get(1), NodeType.AND, 5, 8);
+  }
+
+  @Test
+  public void testParseTreeWithNoChildren() {
+    Node node = parse("ABC");
+    assertNode(node, NodeType.TERM, 0, 3);
+  }
+
+  @Test
+  public void testParseTreeWithTwoChildren() {
+    Node node = parse("ABC|DEF");
+    assertNode(node, NodeType.OR, 0, 7);
+    assertNode(node.getChildren().get(0), NodeType.TERM, 0, 3);
+    assertNode(node.getChildren().get(1), NodeType.TERM, 4, 7);
+  }
+
+  @Test
+  public void testParseTreeWithParenthesesAndTwoChildren() {
+    Node node = parse("(ABC|DEF)");
+    assertNode(node, NodeType.OR, 1, 8);
+    assertNode(node.getChildren().get(0), NodeType.TERM, 1, 4);
+    assertNode(node.getChildren().get(1), NodeType.TERM, 5, 8);
+  }
+
+  @Test
+  public void testParseTreeWithParenthesizedChildren() {
+    Node node = parse("ABC|(DEF&GHI)");
+    assertNode(node, NodeType.OR, 0, 13);
+    assertNode(node.getChildren().get(0), NodeType.TERM, 0, 3);
+    assertNode(node.getChildren().get(1), NodeType.AND, 5, 12);
+    assertNode(node.getChildren().get(1).children.get(0), NodeType.TERM, 5, 8);
+    assertNode(node.getChildren().get(1).children.get(1), NodeType.TERM, 9, 
12);
+  }
+
+  @Test
+  public void testParseTreeWithMoreParentheses() {
+    Node node = parse("(W)|(U&V)");
+    assertNode(node, NodeType.OR, 0, 9);
+    assertNode(node.getChildren().get(0), NodeType.TERM, 1, 2);
+    assertNode(node.getChildren().get(1), NodeType.AND, 5, 8);
+    assertNode(node.getChildren().get(1).children.get(0), NodeType.TERM, 5, 6);
+    assertNode(node.getChildren().get(1).children.get(1), NodeType.TERM, 7, 8);
+  }
+
+  @Test
+  public void testEmptyParseTreesAreEqual() {
+    Comparator<Node> comparator = new NodeComparator(new byte[] {});
+    Node empty = new AccessExpressionImpl().getParseTree();
+    assertEquals(0, comparator.compare(empty, parse("")));
+  }
+
+  @Test
+  public void testParseTreesOrdering() {
+    byte[] expression = "(b&c&d)|((a|m)&y&z)|(e&f)".getBytes(UTF_8);
+    byte[] flattened = new 
AccessExpressionImpl(expression).normalize().getBytes(UTF_8);
+
+    // Convert to String for indexOf convenience
+    String flat = new String(flattened, UTF_8);
+    assertTrue(flat.indexOf('e') < flat.indexOf('|'), "shortest expressions 
sort first");
+    assertTrue(flat.indexOf('b') < flat.indexOf('a'), "shortest children sort 
first");
+  }
+
+  private Node parse(String s) {
+    AccessExpressionImpl v = new AccessExpressionImpl(s);
+    return v.getParseTree();
+  }
+
+  private void assertNode(Node node, NodeType nodeType, int start, int end) {
+    assertEquals(node.type, nodeType);
+    assertEquals(start, node.start);
+    assertEquals(end, node.end);
+  }
+}
diff --git a/src/test/resources/testdata.json b/src/test/resources/testdata.json
new file mode 100644
index 0000000..2367b31
--- /dev/null
+++ b/src/test/resources/testdata.json
@@ -0,0 +1,378 @@
+[
+  {
+    "description": "basic expressions",
+    "auths": [
+      [
+        "one",
+        "two",
+        "three",
+        "four"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "one",
+          "one|five",
+          "five|one",
+          "(one)",
+          "(one&two)|(foo&bar)",
+          "(one|foo)&three",
+          "one|foo|bar",
+          "(one|foo)|bar",
+          "((one|foo)|bar)&two",
+          "",
+          "one&two",
+          "foor|four",
+          "(one&two)|(foo&bar)"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "five",
+          "one&five",
+          "five&one",
+          "((one|foo)|bar)&goober"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "basic expressions with repeats",
+    "auths": [
+      [
+        "A1",
+        "Z9"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "A1",
+          "Z9",
+          "A1|G2",
+          "G2|A1",
+          "Z9|G2",
+          "G2|A1",
+          "G2|A1",
+          "Z9|A1",
+          "A1|Z9",
+          "Z9|A1",
+          "(A1|G2)&(Z9|G5)",
+          "Z9|A1",
+          "(A1|G2)&(Z9|G5)"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "Z8",
+          "A2",
+          "A2|Z8",
+          "A1&Z8",
+          "Z8&A1"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "incorrect expressions",
+    "auths": [
+      [
+        "A1",
+        "Z9"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ERROR",
+        "expressions": [
+          "()",
+          "()|()",
+          "()&()",
+          "&",
+          "|",
+          "(&)",
+          "(|)",
+          "A|",
+          "|A",
+          "A&",
+          "&A",
+          "A|(B|)",
+          "A|(|B)",
+          "A|(B&)",
+          "A|(&B)",
+          "((A)",
+          "(A",
+          "A)",
+          "((A)",
+          ")",
+          "))",
+          "A|B)",
+          "(A|B))",
+          "A&B)",
+          "(A&B))",
+          "A&)",
+          "A|)",
+          "(&A",
+          "(|B",
+          "A$B",
+          "(A|(B&()))",
+          "A|B&C",
+          "A&B|C",
+          "(A&B|C)|(C&Z)",
+          "(A&B|C)&(C&Z)",
+          "(A&B|C)|(D|C&Z)",
+          "(A&B|C)&(D|C&Z)",
+          "\"",
+          "\"\\c\"",
+          "\"\\\"",
+          "\"\"\"",
+          "\"\"\"&A"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "incorrect empty quoted expressions",
+    "auths": [
+      [
+        "A1",
+        "Z9"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ERROR",
+        "expressions": [
+          "\"\"",
+          "\"\"|A",
+          "A|\"\"",
+          "\"\"&A",
+          "A&\"\"",
+          "A&(\"\"|B)",
+          "(\"\")"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "expressions with non alpha numeric characters",
+    "auths": [
+      [
+        "a_b",
+        "a-c",
+        "a/d",
+        "a:e",
+        "a.f",
+        "a_b-c/d:e.f"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "a_b",
+          "\"a_b\"",
+          "a-c",
+          "\"a-c\"",
+          "a/d",
+          "\"a/d\"",
+          "a:e",
+          "\"a:e\"",
+          "a.f",
+          "\"a.f\"",
+          "a_b|a_z",
+          "a-z|a-c",
+          "a/d|a/z",
+          "a:e|a:z",
+          "a.z|a.f",
+          "a_b&a-c&a/d&a:e&a.f",
+          "(a-z|a-c)&(a/d|a/z)",
+          "a_b-c/d:e.f",
+          "a_b-c/d:e.f&a/d"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "a_c",
+          "b_b",
+          "a-b",
+          "a/c",
+          "a:f",
+          "a.e",
+          "a_b&a_z",
+          "a_b&a-b&a/d&a:e&a.f",
+          "a_b-c/d:e.z",
+          "a_b-c/d:e.f&a/c"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "expressions with non alpha numeric characters",
+    "auths": [
+      [
+        "_",
+        "-",
+        "/",
+        ":",
+        "."
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "_",
+          "\"_\"",
+          "-",
+          "/",
+          ":",
+          ".",
+          "_&-",
+          "_&(a|:)"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "A&_",
+          "A",
+          "/&A",
+          "B|(_&C)"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "non ascii expressions",
+    "auths": [
+      [
+        "δΊ”",
+        "ε…­",
+        "ε…«",
+        "九",
+        "五十"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "\"δΊ”\"|\"ε››\"",
+          "\"δΊ”\"&(\"ε››\"|\"九\")",
+          "\"δΊ”\"&(\"ε››\"|\"五十\")"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "\"δΊ”\"&\"ε››\"",
+          "\"δΊ”\"&(\"ε››\"|\"δΈ‰\")",
+          "\"δΊ”\"&(\"ε››\"|\"δΈ‰\")"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "multiple authorization sets",
+    "auths": [
+      [
+        "A",
+        "B"
+      ],
+      [
+        "C",
+        "D"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "",
+          "B|C",
+          "(A&B)|(C&D)",
+          "(A&B)|(C)",
+          "(A&B)|C",
+          "(A|C)&(B|D)"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "A",
+          "A&B",
+          "C&D",
+          "A&C",
+          "B&C",
+          "A&B&C&D",
+          "(A&C)|(B&D)"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "test auths needing quoting",
+    "auths": [
+      [
+        "A#C",
+        "A\"C",
+        "A\\C",
+        "AC"
+      ]
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          "\"A#C\"|\"A?C\"",
+          "\"A\\\"C\"&\"A\\\\C\"",
+          "(\"A\\\"C\"|B)&(\"A#C\"|D)"
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "\"A#C\"&B"
+        ]
+      }
+    ]
+  },
+  {
+    "description": "no authorizations",
+    "auths": [
+      []
+    ],
+    "tests": [
+      {
+        "expectedResult": "ACCESSIBLE",
+        "expressions": [
+          ""
+        ]
+      },
+      {
+        "expectedResult": "INACCESSIBLE",
+        "expressions": [
+          "A",
+          "A&B",
+          "A|B",
+          "AB&(CD|E)"
+        ]
+      },
+      {
+        "expectedResult": "ERROR",
+        "expressions": [
+          "()",
+          " ",
+          "\n"
+        ]
+      }
+    ]
+  }
+]
\ No newline at end of file


Reply via email to