This is an automated email from the ASF dual-hosted git repository. dlmarion 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&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&"π¦")|(CAT&"π¦"&CAT) + * ("π¦"&CAT)|("π¦"&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 "&" 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)&(C|D) + * orange|(red&yellow) + * </pre> + * + * <p> + * The following are not valid expressions for visibility: + * + * <pre> + * A|B&C + * A=B + * A|B| + * A&|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 '"' 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> + * "A#C" & 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("A#C") + "&" + quote("FOO"); + * 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