From 5624e36db94eb397c75f9ac1deb75f022555d297 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 19 Dec 2025 18:55:47 +0000 Subject: [PATCH 01/33] WIP --- .../accumulo/access/AccessEvaluator.java | 4 +- .../accumulo/access/AccumuloAccess.java | 75 ++++++++ .../access/InvalidAuthorizationException.java | 28 +++ .../access/impl/AccumuloAccessImpl.java | 160 ++++++++++++++++++ .../accumulo/access/impl/BuilderImpl.java | 55 ++++++ src/test/java/example/AccessExample.java | 16 +- 6 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/apache/accumulo/access/AccumuloAccess.java create mode 100644 src/main/java/org/apache/accumulo/access/InvalidAuthorizationException.java create mode 100644 src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java create mode 100644 src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java diff --git a/src/main/java/org/apache/accumulo/access/AccessEvaluator.java b/src/main/java/org/apache/accumulo/access/AccessEvaluator.java index ea0fb2c..40ddc6a 100644 --- a/src/main/java/org/apache/accumulo/access/AccessEvaluator.java +++ b/src/main/java/org/apache/accumulo/access/AccessEvaluator.java @@ -21,6 +21,7 @@ import java.util.Collection; import org.apache.accumulo.access.impl.AccessEvaluatorImpl; +import org.apache.accumulo.access.impl.AccumuloAccessImpl; import org.apache.accumulo.access.impl.MultiAccessEvaluatorImpl; /** @@ -55,7 +56,8 @@ * @see Accumulo Access Documentation * @since 1.0.0 */ -public sealed interface AccessEvaluator permits AccessEvaluatorImpl, MultiAccessEvaluatorImpl { +public sealed interface AccessEvaluator permits AccessEvaluatorImpl, + AccumuloAccessImpl.ValidatingAccessEvaluator, MultiAccessEvaluatorImpl { /** * @param accessExpression for this parameter a valid access expression is expected. diff --git a/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/src/main/java/org/apache/accumulo/access/AccumuloAccess.java new file mode 100644 index 0000000..1d42308 --- /dev/null +++ b/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -0,0 +1,75 @@ +/* + * 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.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.apache.accumulo.access.impl.BuilderImpl; + +// TODO javadoc +// TODO remove all of the static entry points and use this instead +public interface AccumuloAccess { + + // TODO maybe move to top level + interface AuthorizationValidator extends Predicate {} + + interface Builder { + /** + * TODO document that users should make this as specific as possible in order to avoid creating + * unexpected expressions + * + * TODO document performance reasons for passing CharSequence (allows avoiding obj alloc) + * + */ + Builder authorizationValidator(AuthorizationValidator validator); + + AccumuloAccess build(); + } + + public static Builder builder() { + // TODO avoid object allocation when creating default + return new BuilderImpl(); + } + + AccessExpression newExpression(String expression); + + ParsedAccessExpression newParsedExpression(String expression); + + Authorizations newAuthorizations(); + + // TODO this could throw an exception now + Authorizations newAuthorizations(Set authorizations); + + void findAuthorizations(String expression, Consumer authorizationConsumer) + throws InvalidAccessExpressionException; + + String quote(String authorization); + + String unquote(String authorization); + + void validate(String expression) throws InvalidAccessExpressionException; + + AccessEvaluator newEvaluator(Authorizations authorizations); + + AccessEvaluator newEvaluator(AccessEvaluator.Authorizer authorizer); + + AccessEvaluator newEvaluator(Set authorizationSets); +} diff --git a/src/main/java/org/apache/accumulo/access/InvalidAuthorizationException.java b/src/main/java/org/apache/accumulo/access/InvalidAuthorizationException.java new file mode 100644 index 0000000..bd1ecca --- /dev/null +++ b/src/main/java/org/apache/accumulo/access/InvalidAuthorizationException.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * @since 1.0.0 + */ +public class InvalidAuthorizationException extends RuntimeException { + public InvalidAuthorizationException(String auth) { + super("authorization : '" + auth + "'"); + } +} diff --git a/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java new file mode 100644 index 0000000..6dd58d4 --- /dev/null +++ b/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -0,0 +1,160 @@ +/* + * 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.impl; + +import java.util.Set; +import java.util.function.Consumer; + +import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.InvalidAccessExpressionException; +import org.apache.accumulo.access.InvalidAuthorizationException; +import org.apache.accumulo.access.ParsedAccessExpression; + +public class AccumuloAccessImpl implements AccumuloAccess { + + private final AuthorizationValidator authValidator; + + private void validateAuthorization(CharSequence auth) { + if (!authValidator.test(auth)) { + throw new InvalidAuthorizationException(auth.toString()); + } + } + + public AccumuloAccessImpl(AuthorizationValidator authValidator) { + this.authValidator = authValidator; + } + + @Override + public AccessExpression newExpression(String expression) { + if (authValidator != null) { + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); + } + + return AccessExpression.of(expression); + } + + @Override + public ParsedAccessExpression newParsedExpression(String expression) { + if (authValidator != null) { + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); + } + + return AccessExpression.parse(expression); + } + + @Override + public Authorizations newAuthorizations() { + return Authorizations.of(); + } + + @Override + public Authorizations newAuthorizations(Set authorizations) { + if (authValidator != null) { + authorizations.forEach(this::validateAuthorization); + } + return Authorizations.of(authorizations); + } + + @Override + public void findAuthorizations(String expression, Consumer authorizationConsumer) + throws InvalidAccessExpressionException { + if (authValidator != null) { + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); + } + AccessExpression.findAuthorizations(expression, authorizationConsumer); + } + + @Override + public String quote(String authorization) { + validateAuthorization(authorization); + return AccessExpression.quote(authorization); + } + + @Override + public String unquote(String authorization) { + return ""; + } + + @Override + public void validate(String expression) throws InvalidAccessExpressionException { + if (authValidator != null) { + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); + } else { + AccessExpression.validate(expression); + } + } + + // TODO remove this class and push the authorization validation down into AccessEvaluatorImpl + public final class ValidatingAccessEvaluator implements AccessEvaluator { + + private final AccessEvaluator evaluator; + + private ValidatingAccessEvaluator(AccessEvaluator evaluator) { + this.evaluator = evaluator; + } + + @Override + public boolean canAccess(String accessExpression) throws InvalidAccessExpressionException { + if (authValidator != null) { + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(accessExpression, + AccumuloAccessImpl.this::validateAuthorization); + } + return evaluator.canAccess(accessExpression); + } + + @Override + public boolean canAccess(byte[] accessExpression) throws InvalidAccessExpressionException { + // this method would eventually go away in the super type when byte methods are removed + throw new UnsupportedOperationException(); + } + + @Override + public boolean canAccess(AccessExpression accessExpression) { + if (authValidator != null) { + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(accessExpression.getExpression(), + AccumuloAccessImpl.this::validateAuthorization); + } + return evaluator.canAccess(accessExpression); + } + } + + @Override + public AccessEvaluator newEvaluator(Authorizations authorizations) { + return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizations)); + } + + @Override + public AccessEvaluator newEvaluator(AccessEvaluator.Authorizer authorizer) { + return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizer)); + } + + @Override + public AccessEvaluator newEvaluator(Set authorizationSets) { + return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizationSets)); + } +} diff --git a/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java new file mode 100644 index 0000000..65c79a0 --- /dev/null +++ b/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -0,0 +1,55 @@ +/* + * 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.impl; + +import java.util.Objects; + +import org.apache.accumulo.access.AccumuloAccess; + +public class BuilderImpl implements AccumuloAccess.Builder { + + private AccumuloAccess.AuthorizationValidator validator; + + @Override + public AccumuloAccess.Builder + authorizationValidator(AccumuloAccess.AuthorizationValidator validator) { + this.validator = Objects.requireNonNull(validator); + return this; + } + + @Override + public AccumuloAccess build() { + + return new AccumuloAccessImpl(validator); + } + + public static void main(String[] args) { + var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { + for (int i = 0; i < auth.length(); i++) { + var c = auth.charAt(i); + if (Character.isISOControl(c) || Character.isWhitespace(c) || !Character.isDefined(c) + || c == '\uFFFD') { + return false; + } + } + return true; + }).build(); + var expression = accumuloAccess.newExpression("a|b"); + } +} diff --git a/src/test/java/example/AccessExample.java b/src/test/java/example/AccessExample.java index 5b4b2cc..8612c0a 100644 --- a/src/test/java/example/AccessExample.java +++ b/src/test/java/example/AccessExample.java @@ -26,7 +26,7 @@ import java.util.TreeMap; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.AccumuloAccess; public class AccessExample { @@ -57,8 +57,20 @@ void run(String... authorizations) { out.printf("Showing accessible records using authorizations: %s%n", Arrays.toString(authorizations)); + var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { + for (int i = 0; i < auth.length(); i++) { + var c = auth.charAt(i); + if (Character.isISOControl(c) || Character.isWhitespace(c) || !Character.isDefined(c) + || c == '\uFFFD') { + return false; + } + } + return true; + }).build(); + // Create an access evaluator using the provided authorizations - AccessEvaluator evaluator = AccessEvaluator.of(Authorizations.of(Set.of(authorizations))); + AccessEvaluator evaluator = + accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of(authorizations))); // Print each record whose access expression permits viewing using the provided authorizations getData().forEach((record, accessExpression) -> { From 1cf49e31340fb8207c8374a55236866eed3520eb Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 19 Dec 2025 21:45:40 +0000 Subject: [PATCH 02/33] WIP --- SPECIFICATION.md | 14 +++-- .../accumulo/access/AccumuloAccess.java | 4 -- .../access/AuthorizationValidator.java | 47 ++++++++++++++++ .../access/impl/AccumuloAccessImpl.java | 53 ++++++++----------- .../accumulo/access/impl/BuilderImpl.java | 9 ++-- src/test/java/example/AccessExample.java | 13 ++--- 6 files changed, 84 insertions(+), 56 deletions(-) create mode 100644 src/main/java/org/apache/accumulo/access/AuthorizationValidator.java diff --git a/SPECIFICATION.md b/SPECIFICATION.md index eae4e08..baaa304 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -49,14 +49,17 @@ and-expression = "&" (access-token / paren-expression) [and-expression or-expression = "|" (access-token / paren-expression) [or-expression] access-token = 1*( ALPHA / DIGIT / "_" / "-" / "." / ":" / slash ) -access-token =/ DQUOTE 1*(utf8-subset / escaped) DQUOTE +access-token =/ DQUOTE 1*(unicode-subset / escaped) DQUOTE -utf8-subset = %x20-21 / %x23-5B / %x5D-7E / unicode-beyond-ascii ; utf8 minus '"' and '\' -unicode-beyond-ascii = %x0080-D7FF / %xE000-10FFFF +unicode-subset = %x00-21 / %x23-5B / %x5D-7F / unicode-beyond-ascii ; unicode minus '"' and '\' escaped = "\" DQUOTE / "\\" slash = "/" ``` +Authorizations must be Unicode characters. Not all Unicode characters are human readable +(see [Unicode control characters][6]), implementations should provide a way to limit valid authorizations to human +readable characters. + ### Examples of Proper Expressions * `BLUE` @@ -73,8 +76,8 @@ slash = "/" ## 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. +An access expression or authorization must be a Unicode string. Serialization of an access expression or authorization +should use UTF-8. ## Evaluation @@ -140,3 +143,4 @@ within the Authorizations object, the token is unquoted, and the `\` character i [3]: https://en.wikipedia.org/wiki/Boolean_algebra [4]: https://en.wikipedia.org/wiki/Logical_conjunction [5]: https://en.wikipedia.org/wiki/Logical_disjunction +[6]: https://en.wikipedia.org/wiki/Unicode_control_characters diff --git a/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/src/main/java/org/apache/accumulo/access/AccumuloAccess.java index 1d42308..3096ede 100644 --- a/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -20,7 +20,6 @@ import java.util.Set; import java.util.function.Consumer; -import java.util.function.Predicate; import org.apache.accumulo.access.impl.BuilderImpl; @@ -28,9 +27,6 @@ // TODO remove all of the static entry points and use this instead public interface AccumuloAccess { - // TODO maybe move to top level - interface AuthorizationValidator extends Predicate {} - interface Builder { /** * TODO document that users should make this as specific as possible in order to avoid creating diff --git a/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java new file mode 100644 index 0000000..6fdd46c --- /dev/null +++ b/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -0,0 +1,47 @@ +/* + * 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.function.Predicate; + +// TODO maybe move to top level +public interface AuthorizationValidator extends Predicate { + // TODO document + AuthorizationValidator UNICODE = auth -> { + for (int i = 0; i < auth.length(); i++) { + if (!Character.isDefined(auth.charAt(i))) { + return false; + } + } + return true; + }; + + // TODO document + AuthorizationValidator READABLE = auth -> { + for (int i = 0; i < auth.length(); i++) { + var c = auth.charAt(i); + if (Character.isISOControl(c) || Character.isWhitespace(c) || !Character.isDefined(c) + || c == '\uFFFD') { + return false; + } + } + return true; + }; + +} diff --git a/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index 6dd58d4..b88f41a 100644 --- a/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -24,6 +24,7 @@ import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.InvalidAuthorizationException; @@ -45,20 +46,16 @@ public AccumuloAccessImpl(AuthorizationValidator authValidator) { @Override public AccessExpression newExpression(String expression) { - if (authValidator != null) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - } + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); return AccessExpression.of(expression); } @Override public ParsedAccessExpression newParsedExpression(String expression) { - if (authValidator != null) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - } + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); return AccessExpression.parse(expression); } @@ -70,19 +67,16 @@ public Authorizations newAuthorizations() { @Override public Authorizations newAuthorizations(Set authorizations) { - if (authValidator != null) { - authorizations.forEach(this::validateAuthorization); - } + authorizations.forEach(this::validateAuthorization); + return Authorizations.of(authorizations); } @Override public void findAuthorizations(String expression, Consumer authorizationConsumer) throws InvalidAccessExpressionException { - if (authValidator != null) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - } + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); AccessExpression.findAuthorizations(expression, authorizationConsumer); } @@ -94,17 +88,16 @@ public String quote(String authorization) { @Override public String unquote(String authorization) { - return ""; + var unquoted = AccessExpression.unquote(authorization); + validateAuthorization(unquoted); + return unquoted; } @Override public void validate(String expression) throws InvalidAccessExpressionException { - if (authValidator != null) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - } else { - AccessExpression.validate(expression); - } + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(expression, this::validateAuthorization); + AccessExpression.validate(expression); } // TODO remove this class and push the authorization validation down into AccessEvaluatorImpl @@ -118,11 +111,9 @@ private ValidatingAccessEvaluator(AccessEvaluator evaluator) { @Override public boolean canAccess(String accessExpression) throws InvalidAccessExpressionException { - if (authValidator != null) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(accessExpression, - AccumuloAccessImpl.this::validateAuthorization); - } + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(accessExpression, + AccumuloAccessImpl.this::validateAuthorization); return evaluator.canAccess(accessExpression); } @@ -134,11 +125,9 @@ public boolean canAccess(byte[] accessExpression) throws InvalidAccessExpression @Override public boolean canAccess(AccessExpression accessExpression) { - if (authValidator != null) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(accessExpression.getExpression(), - AccumuloAccessImpl.this::validateAuthorization); - } + // TODO push this down into the parsing code, this parses twice + AccessExpression.findAuthorizations(accessExpression.getExpression(), + AccumuloAccessImpl.this::validateAuthorization); return evaluator.canAccess(accessExpression); } } diff --git a/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java index 65c79a0..00ecc57 100644 --- a/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java +++ b/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -21,22 +21,21 @@ import java.util.Objects; import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.AuthorizationValidator; public class BuilderImpl implements AccumuloAccess.Builder { - private AccumuloAccess.AuthorizationValidator validator; + private AuthorizationValidator validator; @Override - public AccumuloAccess.Builder - authorizationValidator(AccumuloAccess.AuthorizationValidator validator) { + public AccumuloAccess.Builder authorizationValidator(AuthorizationValidator validator) { this.validator = Objects.requireNonNull(validator); return this; } @Override public AccumuloAccess build() { - - return new AccumuloAccessImpl(validator); + return new AccumuloAccessImpl(validator == null ? AuthorizationValidator.UNICODE : validator); } public static void main(String[] args) { diff --git a/src/test/java/example/AccessExample.java b/src/test/java/example/AccessExample.java index 8612c0a..dae9d59 100644 --- a/src/test/java/example/AccessExample.java +++ b/src/test/java/example/AccessExample.java @@ -27,6 +27,7 @@ import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.AuthorizationValidator; public class AccessExample { @@ -57,16 +58,8 @@ void run(String... authorizations) { out.printf("Showing accessible records using authorizations: %s%n", Arrays.toString(authorizations)); - var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { - for (int i = 0; i < auth.length(); i++) { - var c = auth.charAt(i); - if (Character.isISOControl(c) || Character.isWhitespace(c) || !Character.isDefined(c) - || c == '\uFFFD') { - return false; - } - } - return true; - }).build(); + var accumuloAccess = + AccumuloAccess.builder().authorizationValidator(AuthorizationValidator.READABLE).build(); // Create an access evaluator using the provided authorizations AccessEvaluator evaluator = From 91a0e3541e2ea8f020c78c7bbe6787e642765452 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 19 Dec 2025 22:38:24 +0000 Subject: [PATCH 03/33] WIP --- SPECIFICATION.md | 2 +- .../apache/accumulo/access/AccumuloAccess.java | 15 +++++++++------ .../accumulo/access/impl/AccumuloAccessImpl.java | 5 +++-- .../access/tests/AccessEvaluatorTest.java | 9 ++++++--- .../access/tests/AccessExpressionTest.java | 2 ++ 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index baaa304..9fb18b8 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -56,7 +56,7 @@ escaped = "\" DQUOTE / "\\" slash = "/" ``` -Authorizations must be Unicode characters. Not all Unicode characters are human readable +Authorizations must be Unicode characters. Not all Unicode characters are human readable (see [Unicode control characters][6]), implementations should provide a way to limit valid authorizations to human readable characters. diff --git a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java index 3096ede..2f89fae 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -18,6 +18,7 @@ */ package org.apache.accumulo.access; +import java.util.Collection; import java.util.Set; import java.util.function.Consumer; @@ -45,21 +46,23 @@ public static Builder builder() { return new BuilderImpl(); } - AccessExpression newExpression(String expression); + AccessExpression newExpression(String expression) + throws InvalidAccessExpressionException, InvalidAuthorizationException; - ParsedAccessExpression newParsedExpression(String expression); + ParsedAccessExpression newParsedExpression(String expression) + throws InvalidAccessExpressionException, InvalidAuthorizationException; Authorizations newAuthorizations(); // TODO this could throw an exception now - Authorizations newAuthorizations(Set authorizations); + Authorizations newAuthorizations(Set authorizations) throws InvalidAuthorizationException; void findAuthorizations(String expression, Consumer authorizationConsumer) throws InvalidAccessExpressionException; - String quote(String authorization); + String quote(String authorization) throws InvalidAuthorizationException; - String unquote(String authorization); + String unquote(String authorization) throws InvalidAuthorizationException; void validate(String expression) throws InvalidAccessExpressionException; @@ -67,5 +70,5 @@ void findAuthorizations(String expression, Consumer authorizationConsume AccessEvaluator newEvaluator(AccessEvaluator.Authorizer authorizer); - AccessEvaluator newEvaluator(Set authorizationSets); + AccessEvaluator newEvaluator(Collection authorizationSets); } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index b88f41a..f7eedf4 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -18,6 +18,7 @@ */ package org.apache.accumulo.access.impl; +import java.util.Collection; import java.util.Set; import java.util.function.Consumer; @@ -120,7 +121,7 @@ public boolean canAccess(String accessExpression) throws InvalidAccessExpression @Override public boolean canAccess(byte[] accessExpression) throws InvalidAccessExpressionException { // this method would eventually go away in the super type when byte methods are removed - throw new UnsupportedOperationException(); + return evaluator.canAccess(accessExpression); } @Override @@ -143,7 +144,7 @@ public AccessEvaluator newEvaluator(AccessEvaluator.Authorizer authorizer) { } @Override - public AccessEvaluator newEvaluator(Set authorizationSets) { + public AccessEvaluator newEvaluator(Collection authorizationSets) { return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizationSets)); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index 35a0d5c..969f173 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -36,6 +36,7 @@ import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.impl.AccessEvaluatorImpl; @@ -85,21 +86,23 @@ public void runTestCases() throws IOException { assertFalse(testData.isEmpty()); + var accumuloAccess = AccumuloAccess.builder().build(); + for (var testSet : testData) { System.out.println("runTestCases for " + testSet.description); AccessEvaluator evaluator; assertTrue(testSet.auths.length >= 1); if (testSet.auths.length == 1) { - evaluator = AccessEvaluator.of(Authorizations.of(Set.of(testSet.auths[0]))); + evaluator = accumuloAccess.newEvaluator(Authorizations.of(Set.of(testSet.auths[0]))); runTestCases(testSet, evaluator); Set auths = Stream.of(testSet.auths[0]).collect(Collectors.toSet()); - evaluator = AccessEvaluator.of(auths::contains); + evaluator = accumuloAccess.newEvaluator(auths::contains); runTestCases(testSet, evaluator); } else { var authSets = Stream.of(testSet.auths).map(a -> Authorizations.of(Set.of(a))) .collect(Collectors.toList()); - evaluator = AccessEvaluator.of(authSets); + evaluator = accumuloAccess.newEvaluator(authSets); runTestCases(testSet, evaluator); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java index c7f4c40..16edcf0 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java @@ -40,6 +40,7 @@ import org.apache.accumulo.access.AccessExpression; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.ParsedAccessExpression; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -151,6 +152,7 @@ public void testEqualsHashcode() { assertNotEquals(ae2.hashCode(), ae4.hashCode()); } + @Disabled @Test public void testSpecificationDocumentation() throws IOException, URISyntaxException { // verify AccessExpression.abnf matches what is documented in SPECIFICATION.md From 8da01cf33146e21874fe9ed00203a0d46057b9be Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Mon, 22 Dec 2025 23:14:43 +0000 Subject: [PATCH 04/33] WIP --- .../antlr/AccessExpressionAntlrBenchmark.java | 4 +- .../access/grammar/antlr/Antlr4Tests.java | 7 +- core/src/main/java/module-info.java | 1 + .../accumulo/access/AccessEvaluator.java | 89 +------- .../accumulo/access/AccessExpression.java | 193 +----------------- .../accumulo/access/AccumuloAccess.java | 145 ++++++++++++- .../access/AuthorizationValidator.java | 2 + .../accumulo/access/Authorizations.java | 72 +------ .../InvalidAccessExpressionException.java | 3 +- .../access/ParsedAccessExpression.java | 6 +- .../access/impl/AccessEvaluatorImpl.java | 103 ++++++---- .../access/impl/AccessExpressionImpl.java | 46 ++++- .../access/impl/AccumuloAccessImpl.java | 75 ++----- .../access/impl/AuthorizationsImpl.java | 72 +++++++ .../accumulo/access/impl/ByteUtils.java | 13 +- .../accumulo/access/impl/CharsWrapper.java | 109 ++++++++++ .../access/impl/MultiAccessEvaluatorImpl.java | 17 +- .../impl/ParsedAccessExpressionImpl.java | 54 +++-- .../accumulo/access/impl/ParserEvaluator.java | 50 ++++- .../accumulo/access/impl/Tokenizer.java | 45 ++-- .../access/tests/AccessEvaluatorTest.java | 114 +++++------ .../tests/AccessExpressionBenchmark.java | 27 ++- .../access/tests/AccessExpressionTest.java | 78 +++---- .../access/tests/AuthorizationTest.java | 17 +- .../tests/ParsedAccessExpressionTest.java | 18 +- .../access/examples/ParseExamples.java | 19 +- .../examples/test/ParseExamplesTest.java | 14 +- 27 files changed, 707 insertions(+), 686 deletions(-) create mode 100644 core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java create mode 100644 core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java index e32593a..2311ddd 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java @@ -29,7 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.impl.AuthorizationsImpl; import org.apache.accumulo.access.antlr.TestDataLoader; import org.apache.accumulo.access.antlr4.AccessExpressionAntlrEvaluator; import org.apache.accumulo.access.antlr4.AccessExpressionAntlrParser; @@ -89,7 +89,7 @@ public void loadData() throws IOException, URISyntaxException { et.expressions = new ArrayList<>(); et.evaluator = new AccessExpressionAntlrEvaluator(Stream.of(testDataSet.auths) - .map(a -> Authorizations.of(Set.of(a))).collect(Collectors.toList())); + .map(a -> AuthorizationsImpl.of(Set.of(a))).collect(Collectors.toList())); for (var tests : testDataSet.tests) { if (tests.expectedResult != TestDataLoader.ExpectedResult.ERROR) { diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java index 3ed6a9e..c1b33e3 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java @@ -42,6 +42,7 @@ import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.impl.AuthorizationsImpl; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.antlr.TestDataLoader; import org.apache.accumulo.access.antlr.TestDataLoader.ExpectedResult; @@ -122,7 +123,7 @@ public void testCompareWithAccessExpressionImplParsing() throws Exception { @Test public void testSimpleEvaluation() throws Exception { String accessExpression = "(one&two)|(foo&bar)"; - Authorizations auths = Authorizations.of(Set.of("four", "three", "one", "two")); + Authorizations auths = AuthorizationsImpl.of(Set.of("four", "three", "one", "two")); AccessExpressionAntlrEvaluator eval = new AccessExpressionAntlrEvaluator(List.of(auths)); assertTrue(eval.canAccess(accessExpression)); } @@ -130,7 +131,7 @@ public void testSimpleEvaluation() throws Exception { @Test public void testSimpleEvaluationFailure() throws Exception { String accessExpression = "(A&B&C)"; - Authorizations auths = Authorizations.of(Set.of("A", "C")); + Authorizations auths = AuthorizationsImpl.of(Set.of("A", "C")); AccessExpressionAntlrEvaluator eval = new AccessExpressionAntlrEvaluator(List.of(auths)); assertFalse(eval.canAccess(accessExpression)); } @@ -143,7 +144,7 @@ public void testCompareAntlrEvaluationAgainstAccessEvaluatorImpl() throws Except for (TestDataSet testSet : testData) { List authSets = Stream.of(testSet.auths) - .map(a -> Authorizations.of(Set.of(a))).collect(Collectors.toList()); + .map(a -> AuthorizationsImpl.of(Set.of(a))).collect(Collectors.toList()); AccessEvaluator evaluator = AccessEvaluator.of(authSets); AccessExpressionAntlrEvaluator antlr = new AccessExpressionAntlrEvaluator(authSets); diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 37addd4..d2a3a2e 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -18,4 +18,5 @@ */ module accumulo.access.core { exports org.apache.accumulo.access; + exports org.apache.accumulo.access.impl; } \ No newline at end of file diff --git a/core/src/main/java/org/apache/accumulo/access/AccessEvaluator.java b/core/src/main/java/org/apache/accumulo/access/AccessEvaluator.java index 40ddc6a..faa3472 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccessEvaluator.java +++ b/core/src/main/java/org/apache/accumulo/access/AccessEvaluator.java @@ -18,10 +18,7 @@ */ package org.apache.accumulo.access; -import java.util.Collection; - import org.apache.accumulo.access.impl.AccessEvaluatorImpl; -import org.apache.accumulo.access.impl.AccumuloAccessImpl; import org.apache.accumulo.access.impl.MultiAccessEvaluatorImpl; /** @@ -56,8 +53,7 @@ * @see Accumulo Access Documentation * @since 1.0.0 */ -public sealed interface AccessEvaluator permits AccessEvaluatorImpl, - AccumuloAccessImpl.ValidatingAccessEvaluator, MultiAccessEvaluatorImpl { +public sealed interface AccessEvaluator permits AccessEvaluatorImpl, MultiAccessEvaluatorImpl { /** * @param accessExpression for this parameter a valid access expression is expected. @@ -67,14 +63,6 @@ public sealed interface AccessEvaluator permits AccessEvaluatorImpl, */ boolean canAccess(String accessExpression) throws InvalidAccessExpressionException; - /** - * @param accessExpression for this parameter a valid access expression is expected. - * @return true if the expression is visible using the authorizations supplied at creation, false - * otherwise - * @throws InvalidAccessExpressionException when the expression is not valid - */ - boolean canAccess(byte[] accessExpression) throws InvalidAccessExpressionException; - /** * @param accessExpression previously validated access expression * @return true if the expression is visible using the authorizations supplied at creation, false @@ -82,81 +70,6 @@ public sealed interface AccessEvaluator permits AccessEvaluatorImpl, */ boolean canAccess(AccessExpression accessExpression); - /** - * Creates an AccessEvaluator from an Authorizations object - * - * @param authorizations auths to use in the AccessEvaluator - * @return AccessEvaluator object - */ - static AccessEvaluator of(Authorizations authorizations) { - return new AccessEvaluatorImpl(authorizations); - } - - /** - * Creates an AccessEvaluator from an Authorizer object - * - * @param authorizer authorizer to use in the AccessEvaluator - * @return AccessEvaluator object - */ - static AccessEvaluator of(Authorizer authorizer) { - return new AccessEvaluatorImpl(authorizer); - } - - /** - * 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. - * - *
-   *     {@code
-   * Collection authSets =
-   *     List.of(Authorizations.of("A", "B"), Authorizations.of("C", "D"));
-   * var evaluator = AccessEvaluator.of(authSets);
-   *
-   * System.out.println(evaluator.canAccess("A"));
-   * System.out.println(evaluator.canAccess("A|D"));
-   * System.out.println(evaluator.canAccess("A&D"));
-   *
-   * }
-   * 
- * - *

- * 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. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Evaluations
[A,B][C,D]
ATrueFalse
A|DTrueTrue
A&DFalseFalse
- * - * - * - */ - static AccessEvaluator of(Collection authorizationSets) { - return MultiAccessEvaluatorImpl.of(authorizationSets); - } - /** * An interface that is used to check if an authorization seen in an access expression is * authorized. diff --git a/core/src/main/java/org/apache/accumulo/access/AccessExpression.java b/core/src/main/java/org/apache/accumulo/access/AccessExpression.java index cf32670..e95142c 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccessExpression.java +++ b/core/src/main/java/org/apache/accumulo/access/AccessExpression.java @@ -18,20 +18,11 @@ */ package org.apache.accumulo.access; -import static java.nio.charset.StandardCharsets.UTF_8; - import java.io.Serializable; -import java.util.Arrays; -import java.util.function.Consumer; -import java.util.function.Predicate; -import org.apache.accumulo.access.impl.AccessEvaluatorImpl; import org.apache.accumulo.access.impl.AccessExpressionImpl; -import org.apache.accumulo.access.impl.BytesWrapper; -import org.apache.accumulo.access.impl.ParsedAccessExpressionImpl; -import org.apache.accumulo.access.impl.ParserEvaluator; -import org.apache.accumulo.access.impl.Tokenizer; +// TODO update or remove example code... maybe remove it because the project has example code that is tested /** * This class offers the ability to operate on access expressions. * @@ -115,10 +106,10 @@ protected AccessExpression() {} /** * Parses the access expression if it was never parsed before. If this access expression was - * created using {@link #parse(String)} or {@link #parse(byte[])} then it will have a parse from + * created using {@link AccumuloAccess#newParsedExpression(String)} then it will have a parse from * inception and this method will return itself. If the access expression was created using - * {@link #of(String)} or {@link #of(byte[])} then this method will create a parse tree the first - * time its called and remember it, returning the remembered parse tree on subsequent calls. + * {@link AccumuloAccess#newExpression(String)} then this method will create a parse tree the + * first time its called and remember it, returning the remembered parse tree on subsequent calls. */ public abstract ParsedAccessExpression parse(); @@ -140,180 +131,4 @@ public int hashCode() { public String toString() { return getExpression(); } - - /** - * Validates an access expression and returns an immutable AccessExpression object. If passing - * access expressions as arguments in code, consider using this type instead of a String. The - * advantage of passing this type over a String is that its known to be a valid expression. Also - * this type is much more informative than a String type. Conceptually this method calls - * {@link #validate(String)} and if that passes creates an immutable object that wraps the - * expression. - * - * @throws InvalidAccessExpressionException if the given expression is not valid - * @throws NullPointerException when the argument is null - */ - public static AccessExpression of(String expression) throws InvalidAccessExpressionException { - return new AccessExpressionImpl(expression); - } - - /** - * @see #of(String) - */ - public static AccessExpression of(byte[] expression) throws InvalidAccessExpressionException { - return new AccessExpressionImpl(expression); - } - - /** - * @return an empty AccessExpression that is immutable. - */ - public static AccessExpression of() { - return AccessExpressionImpl.EMPTY; - } - - /** - * @see #parse(String) - */ - public static ParsedAccessExpression parse(byte[] expression) - throws InvalidAccessExpressionException { - if (expression.length == 0) { - return ParsedAccessExpressionImpl.EMPTY; - } - - return ParsedAccessExpressionImpl.parseExpression(Arrays.copyOf(expression, expression.length)); - } - - /** - * Validates an access expression and returns an immutable object with a parse tree. Creating the - * parse tree is expensive relative to calling {@link #of(String)} or {@link #validate(String)}, - * so only use this method when the parse tree is always needed. If the code may only use the - * parse tree sometimes, then it may be best to call {@link #of(String)} to create the access - * expression and then call {@link AccessExpression#parse()} when needed. - * - * @throws NullPointerException when the argument is null - * @throws InvalidAccessExpressionException if the given expression is not valid - */ - public static ParsedAccessExpression parse(String expression) - throws InvalidAccessExpressionException { - if (expression.isEmpty()) { - return ParsedAccessExpressionImpl.EMPTY; - } - // Calling expression.getBytes(UTF8) will create a byte array that only this code has access to, - // so not need to copy that byte array. That is why parse(byte[]) is not called here because - // that would copy the array again. - return ParsedAccessExpressionImpl.parseExpression(expression.getBytes(UTF_8)); - } - - /** - * Quickly validates that an access expression is properly formed. - * - * @param expression a potential access expression that is expected to be encoded using UTF-8 - * @throws InvalidAccessExpressionException if the given expression is not valid - * @throws NullPointerException when the argument is null - */ - public static void validate(byte[] expression) throws InvalidAccessExpressionException { - if (expression.length > 0) { - Predicate atp = authToken -> true; - ParserEvaluator.parseAccessExpression(expression, atp, atp); - } // else empty expression is valid, avoid object allocation - } - - /** - * @see #validate(byte[]) - */ - public static void validate(String expression) throws InvalidAccessExpressionException { - if (!expression.isEmpty()) { - validate(expression.getBytes(UTF_8)); - } // else empty expression is valid, avoid object allocation - } - - /** - * Validates and access expression and finds all authorizations in it passing them to the - * authorizationConsumer. For example, for the expression {@code (A&B)|(A&C)|(A&D)}, this method - * would pass {@code A,B,A,C,A,D} to the consumer one at a time. The function will conceptually - * call {@link #unquote(String)} prior to passing an authorization to authorizationConsumer. - * - *

- * What this method does could also be accomplished by creating a parse tree using - * {@link AccessExpression#parse(String)} and then recursively walking the parse tree. The - * implementation of this method does not create a parse tree and is much faster. If a parse tree - * is already available, then it would likely be faster to use it rather than call this method. - *

- * - * @throws InvalidAccessExpressionException when the expression is not valid. - * @throws NullPointerException when any argument is null - */ - public static void findAuthorizations(String expression, Consumer authorizationConsumer) - throws InvalidAccessExpressionException { - findAuthorizations(expression.getBytes(UTF_8), authorizationConsumer); - } - - /** - * @see #findAuthorizations(String, Consumer) - */ - public static void findAuthorizations(byte[] expression, Consumer authorizationConsumer) - throws InvalidAccessExpressionException { - ParserEvaluator.findAuthorizations(expression, authorizationConsumer); - } - - /** - * Authorizations occurring in an access expression can only contain the characters listed in the - * specification unless - * quoted (surrounded by quotation marks). Use this method to quote authorizations that occur in - * an access expression. This method will only quote if it is needed. - * - * @throws NullPointerException when the argument is null - */ - public static byte[] quote(byte[] term) { - if (term.length == 0) { - throw new IllegalArgumentException("Empty strings are not legal authorizations."); - } - - boolean needsQuote = false; - - for (byte b : term) { - if (!Tokenizer.isValidAuthChar(b)) { - needsQuote = true; - break; - } - } - - if (!needsQuote) { - return term; - } - - return AccessEvaluatorImpl.escape(term, true); - } - - /** - * Authorizations occurring in an access expression can only contain the characters listed in the - * specification unless - * quoted (surrounded by quotation marks). Use this method to quote authorizations that occur in - * an access expression. This method will only quote if it is needed. - * - * @throws NullPointerException when the argument is null - */ - public static String quote(String term) { - return new String(quote(term.getBytes(UTF_8)), UTF_8); - } - - /** - * Reverses what {@link #quote(String)} does, so will unquote and unescape an authorization if - * needed. If the authorization is not quoted then it is returned as-is. - * - * @throws NullPointerException when the argument is null - */ - public static String unquote(String term) { - if (term.equals("\"\"") || term.isEmpty()) { - throw new IllegalArgumentException("Empty strings are not legal authorizations."); - } - - if (term.charAt(0) == '"' && term.charAt(term.length() - 1) == '"') { - term = term.substring(1, term.length() - 1); - return AccessEvaluatorImpl.unescape(new BytesWrapper(term.getBytes(UTF_8))); - } else { - return term; - } - } } diff --git a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java index 2f89fae..2d9455a 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -46,29 +46,168 @@ public static Builder builder() { return new BuilderImpl(); } + /** + * Validates an access expression and returns an immutable AccessExpression object. If passing + * access expressions as arguments in code, consider using this type instead of a String. The + * advantage of passing this type over a String is that its known to be a valid expression. Also, + * this type is much more informative than a String type. Conceptually this method calls + * {@link #validate(String)} and if that passes creates an immutable object that wraps the + * expression. + * + * @throws InvalidAccessExpressionException if the given expression is not valid + * @throws InvalidAuthorizationException when the expression contains an authorization that is not + * valid + * @throws NullPointerException when the argument is null + */ AccessExpression newExpression(String expression) throws InvalidAccessExpressionException, InvalidAuthorizationException; + /** + * Validates an access expression and returns an immutable object with a parse tree. Creating the + * parse tree is expensive relative to calling {@link # newExpression(String)} or + * {@link #validate(String)}, so only use this method when the parse tree is always needed. If the + * code may only use the parse tree sometimes, then it may be best to call + * {@link #newExpression(String)} to create the access expression and then call + * {@link AccessExpression#parse()} when needed. + * + * @throws NullPointerException when the argument is null + * @throws InvalidAuthorizationException when the expression contains an authorization that is not + * valid + * @throws InvalidAccessExpressionException if the given expression is not valid + */ ParsedAccessExpression newParsedExpression(String expression) throws InvalidAccessExpressionException, InvalidAuthorizationException; + /** + * @return a pre-allocated empty Authorizations object + */ Authorizations newAuthorizations(); - // TODO this could throw an exception now + /** + * Creates an Authorizations object from the set of input authorization strings. + * + * @param authorizations set of authorization strings + * @throws InvalidAuthorizationException when the expression contains an authorization that is not + * valid + * @return Authorizations object + */ Authorizations newAuthorizations(Set authorizations) throws InvalidAuthorizationException; + /** + * Validates an access expression and finds all authorizations in it passing them to the + * authorizationConsumer. For example, for the expression {@code (A&B)|(A&C)|(A&D)}, this method + * would pass {@code A,B,A,C,A,D} to the consumer one at a time. The function will conceptually + * call {@link #unquote(String)} prior to passing an authorization to authorizationConsumer. + * + *

+ * What this method does could also be accomplished by creating a parse tree using + * {@link AccessExpression#parse(String)} and then recursively walking the parse tree. The + * implementation of this method does not create a parse tree and is much faster. If a parse tree + * is already available, then it would likely be faster to use it rather than call this method. + *

+ * + * @throws InvalidAccessExpressionException when the expression is not valid. + * @throws InvalidAuthorizationException when the expression contains an authorization that is not + * valid + * @throws NullPointerException when any argument is null + */ void findAuthorizations(String expression, Consumer authorizationConsumer) - throws InvalidAccessExpressionException; + throws InvalidAccessExpressionException, InvalidAuthorizationException; + /** + * Authorizations occurring in an access expression can only contain the characters listed in the + * specification unless + * quoted (surrounded by quotation marks). Use this method to quote authorizations that occur in + * an access expression. This method will only quote if it is needed. + * + * @throws NullPointerException when the argument is null + */ String quote(String authorization) throws InvalidAuthorizationException; + /** + * Reverses what {@link #quote(String)} does, so will unquote and unescape an authorization if + * needed. If the authorization is not quoted then it is returned as-is. + * + * @throws NullPointerException when the argument is null + */ String unquote(String authorization) throws InvalidAuthorizationException; - void validate(String expression) throws InvalidAccessExpressionException; + /** + * Quickly validates that an access expression is properly formed. + * + * @param expression a potential access expression that + * @throws InvalidAccessExpressionException if the given expression is not valid + * @throws InvalidAuthorizationException if the expression contains an invalid authorization + * @throws NullPointerException when the argument is null + */ + void validate(String expression) + throws InvalidAccessExpressionException, InvalidAuthorizationException; + /** + * Creates an AccessEvaluator from an Authorizations object + * + * @param authorizations auths to use in the AccessEvaluator + * @return AccessEvaluator object + */ AccessEvaluator newEvaluator(Authorizations authorizations); + /** + * Creates an AccessEvaluator from an Authorizer + * + * @param authorizer authorizer to use in the AccessEvaluator + * @return AccessEvaluator object + */ AccessEvaluator newEvaluator(AccessEvaluator.Authorizer authorizer); + /** + * Creates an AccessEvaluator from 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. + * + *
+   *     {@code
+   * AccumuloAccess accumuloAccess = ...;
+   * Collection authSets =
+   *     List.of(Authorizations.of("A", "B"), Authorizations.of("C", "D"));
+   * var evaluator = accumuloAccess.newEvaluator(authSets);
+   *
+   * System.out.println(evaluator.canAccess("A"));
+   * System.out.println(evaluator.canAccess("A|D"));
+   * System.out.println(evaluator.canAccess("A&D"));
+   *
+   * }
+   * 
+ * + *

+ * 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. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Evaluations
[A,B][C,D]
ATrueFalse
A|DTrueTrue
A&DFalseFalse
+ */ AccessEvaluator newEvaluator(Collection authorizationSets); } diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java index a76d3ba..adcaebf 100644 --- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java +++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -20,6 +20,8 @@ import java.util.function.Predicate; +// TODO empty auth is not valid +// TODO pass in if known to be ascii? public interface AuthorizationValidator extends Predicate { // TODO document AuthorizationValidator UNICODE = auth -> { diff --git a/core/src/main/java/org/apache/accumulo/access/Authorizations.java b/core/src/main/java/org/apache/accumulo/access/Authorizations.java index 407b2eb..f3945d7 100644 --- a/core/src/main/java/org/apache/accumulo/access/Authorizations.java +++ b/core/src/main/java/org/apache/accumulo/access/Authorizations.java @@ -19,7 +19,6 @@ package org.apache.accumulo.access; import java.io.Serializable; -import java.util.Iterator; import java.util.Set; /** @@ -28,75 +27,8 @@ *

* Instances of this class are thread-safe. * - *

- * Note: The underlying implementation uses UTF-8 when converting between bytes and Strings. - * * @since 1.0.0 */ -public final class Authorizations implements Iterable, Serializable { - - private static final long serialVersionUID = 1L; - - private static final Authorizations EMPTY = new Authorizations(Set.of()); - - private final Set authorizations; - - private Authorizations(Set authorizations) { - this.authorizations = Set.copyOf(authorizations); - } - - /** - * Returns the set of authorization strings in this Authorization object - * - * @return immutable set of authorization strings - */ - public Set 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(); - } - - @Override - public String toString() { - return authorizations.toString(); - } - - /** - * @return a pre-allocated empty Authorizations object - */ - public static Authorizations of() { - return EMPTY; - } - - /** - * Creates an Authorizations object from the set of input authorization strings. - * - * @param authorizations set of authorization strings - * @return Authorizations object - */ - public static Authorizations of(Set authorizations) { - if (authorizations.isEmpty()) { - return EMPTY; - } else { - return new Authorizations(authorizations); - } - } - - @Override - public Iterator iterator() { - return authorizations.iterator(); - } +public interface Authorizations extends Iterable, Serializable { + Set asSet(); } diff --git a/core/src/main/java/org/apache/accumulo/access/InvalidAccessExpressionException.java b/core/src/main/java/org/apache/accumulo/access/InvalidAccessExpressionException.java index 0119682..324ad95 100644 --- a/core/src/main/java/org/apache/accumulo/access/InvalidAccessExpressionException.java +++ b/core/src/main/java/org/apache/accumulo/access/InvalidAccessExpressionException.java @@ -21,11 +21,10 @@ import java.util.regex.PatternSyntaxException; /** - * An exception that is thrown when an access expression is not correct. + * An exception that is thrown when an access expression is not valid. * * @since 1.0.0 */ -// TODO rename to illegal... public final class InvalidAccessExpressionException extends IllegalArgumentException { private static final long serialVersionUID = 1L; diff --git a/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java b/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java index 5928b1b..1ee8040 100644 --- a/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java +++ b/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java @@ -25,8 +25,8 @@ /** * Instances of this class are immutable and wrap a verified access expression and a parse tree for * the access expression. To create an instance of this class call - * {@link AccessExpression#parse(String)}. The Accumulo Access project has examples that show how to - * use the parse tree. + * {@link AccumuloAccess#newParsedExpression(String)}. The Accumulo Access project has examples that + * show how to use the parse tree. * * @since 1.0.0 */ @@ -52,7 +52,7 @@ public enum ExpressionType { /** * Indicates an access expression is a single authorization. For this type * {@link #getExpression()} will return the authorization in quoted and escaped form. Depending - * on the use case {@link #unquote(String)} may need to be called. + * on the use case {@link AccumuloAccess#unquote(String)} may need to be called. */ AUTHORIZATION, /** diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java index d5189c2..e34fb56 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java @@ -18,7 +18,6 @@ */ package org.apache.accumulo.access.impl; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.accumulo.access.impl.ByteUtils.BACKSLASH; import static org.apache.accumulo.access.impl.ByteUtils.QUOTE; import static org.apache.accumulo.access.impl.ByteUtils.isQuoteOrSlash; @@ -30,42 +29,56 @@ import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; +import org.apache.accumulo.access.InvalidAuthorizationException; public final class AccessEvaluatorImpl implements AccessEvaluator { - private final Predicate authorizedPredicate; + private final Predicate authorizedPredicate; + // TODO set + private final AuthorizationValidator authorizationValidator; /** * Create an AccessEvaluatorImpl using an Authorizer object */ - public AccessEvaluatorImpl(Authorizer authorizationChecker) { - this.authorizedPredicate = auth -> authorizationChecker.isAuthorized(unescape(auth)); + public AccessEvaluatorImpl(Authorizer authorizationChecker, + AuthorizationValidator authorizationValidator) { + this.authorizedPredicate = auth -> authorizationChecker.isAuthorized(auth.toString()); + this.authorizationValidator = authorizationValidator; } /** * Create an AccessEvaluatorImpl using a collection of authorizations */ - public AccessEvaluatorImpl(Authorizations authorizations) { + public AccessEvaluatorImpl(Authorizations authorizations, + AuthorizationValidator authorizationValidator) { var authsSet = authorizations.asSet(); - final Set authBytes = new HashSet<>(authsSet.size()); + final Set wrappedAuths = new HashSet<>(authsSet.size()); for (String authorization : authsSet) { - var auth = authorization.getBytes(UTF_8); - if (auth.length == 0) { + if (authorization.isEmpty()) { throw new IllegalArgumentException("Empty authorization"); } - authBytes.add(new BytesWrapper(AccessEvaluatorImpl.escape(auth, false))); + + wrappedAuths.add(new CharsWrapper(authorization)); } - authorizedPredicate = authBytes::contains; + this.authorizedPredicate = auth -> { + if (auth instanceof CharsWrapper) { + return wrappedAuths.contains(auth); + } else { + return wrappedAuths.contains(new CharsWrapper(auth)); + } + }; + this.authorizationValidator = authorizationValidator; } - public static String unescape(BytesWrapper auth) { + public static CharSequence unescape(CharSequence auth) { int escapeCharCount = 0; for (int i = 0; i < auth.length(); i++) { - byte b = auth.byteAt(i); - if (isQuoteOrSlash(b)) { + char c = auth.charAt(i); + if (isQuoteOrSlash(c)) { escapeCharCount++; } } @@ -75,28 +88,28 @@ public static String unescape(BytesWrapper auth) { throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); } - byte[] unescapedCopy = new byte[auth.length() - escapeCharCount / 2]; + char[] unescapedCopy = new char[auth.length() - escapeCharCount / 2]; int pos = 0; for (int i = 0; i < auth.length(); i++) { - byte b = auth.byteAt(i); - if (b == BACKSLASH) { + char c = auth.charAt(i); + if (c == BACKSLASH) { i++; - b = auth.byteAt(i); - if (!isQuoteOrSlash(b)) { + c = auth.charAt(i); + if (!isQuoteOrSlash(c)) { throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); } - } else if (isQuoteSymbol(b)) { + } else if (isQuoteSymbol(c)) { // should only see quote after a slash throw new IllegalArgumentException( "Illegal character after slash in auth String : " + auth); } - unescapedCopy[pos++] = b; + unescapedCopy[pos++] = c; } - return new String(unescapedCopy, UTF_8); + return new String(unescapedCopy); } else { - return auth.toString(); + return auth; } } @@ -107,23 +120,24 @@ public static String unescape(BytesWrapper auth) { * @param shouldQuote true to wrap escaped authorization in quotes * @return escaped authorization string */ - public static byte[] escape(byte[] auth, boolean shouldQuote) { + public static CharSequence escape(CharSequence auth, boolean shouldQuote) { int escapeCount = 0; - for (byte value : auth) { - if (isQuoteOrSlash(value)) { + for (int i = 0; i < auth.length(); i++) { + if (isQuoteOrSlash(auth.charAt(i))) { escapeCount++; } } if (escapeCount > 0 || shouldQuote) { - byte[] escapedAuth = new byte[auth.length + escapeCount + (shouldQuote ? 2 : 0)]; + char[] escapedAuth = new char[auth.length() + escapeCount + (shouldQuote ? 2 : 0)]; int index = shouldQuote ? 1 : 0; - for (byte b : auth) { - if (isQuoteOrSlash(b)) { + for (int i = 0; i < auth.length(); i++) { + char c = auth.charAt(i); + if (isQuoteOrSlash(c)) { escapedAuth[index++] = BACKSLASH; } - escapedAuth[index++] = b; + escapedAuth[index++] = c; } if (shouldQuote) { @@ -131,7 +145,7 @@ public static byte[] escape(byte[] auth, boolean shouldQuote) { escapedAuth[escapedAuth.length - 1] = QUOTE; } - auth = escapedAuth; + auth = new String(escapedAuth); } return auth; } @@ -143,20 +157,29 @@ public boolean canAccess(AccessExpression expression) { @Override public boolean canAccess(String expression) throws InvalidAccessExpressionException { - return evaluate(expression.getBytes(UTF_8)); - } - - @Override - public boolean canAccess(byte[] expression) throws InvalidAccessExpressionException { return evaluate(expression); } - boolean evaluate(byte[] accessExpression) throws InvalidAccessExpressionException { - var bytesWrapper = ParserEvaluator.lookupWrappers.get(); + boolean evaluate(String accessExpression) throws InvalidAccessExpressionException { + var charsWrapper = ParserEvaluator.lookupWrappers.get(); Predicate atp = authToken -> { - bytesWrapper.set(authToken.data, authToken.start, authToken.len); - return authorizedPredicate.test(bytesWrapper); + var authorization = ParserEvaluator.unescape(authToken, charsWrapper); + if (!authorizationValidator.test(authorization)) { + throw new InvalidAuthorizationException(authorization.toString()); + } + return authorizedPredicate.test(authorization); + }; + + // This is used once the expression is known to always be true or false. For this case only need + // to validate authorizations, do not need to look them up in a set. + Predicate shortCircuit = authToken -> { + var authorization = ParserEvaluator.unescape(authToken, charsWrapper); + if (!authorizationValidator.test(authorization)) { + throw new InvalidAuthorizationException(authorization.toString()); + } + return true; }; - return ParserEvaluator.parseAccessExpression(accessExpression, atp, authToken -> true); + + return ParserEvaluator.parseAccessExpression(accessExpression, atp, shortCircuit); } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java index 9f60e33..dcdad61 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java @@ -18,8 +18,6 @@ */ package org.apache.accumulo.access.impl; -import static java.nio.charset.StandardCharsets.UTF_8; - import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.access.AccessExpression; @@ -34,15 +32,9 @@ public final class AccessExpressionImpl extends AccessExpression { private final AtomicReference parseTreeRef = new AtomicReference<>(); public AccessExpressionImpl(String expression) { - validate(expression); this.expression = expression; } - public AccessExpressionImpl(byte[] expression) { - validate(expression); - this.expression = new String(expression, UTF_8); - } - @Override public String getExpression() { return expression; @@ -52,12 +44,48 @@ public String getExpression() { public ParsedAccessExpression parse() { ParsedAccessExpression parseTree = parseTreeRef.get(); if (parseTree == null) { + // This expression authorizations were already validated, so can pass a lambda that always + // returns true parseTreeRef.compareAndSet(null, - ParsedAccessExpressionImpl.parseExpression(expression.getBytes(UTF_8))); + ParsedAccessExpressionImpl.parseExpression(expression, auth -> true)); // must get() again in case another thread won w/ the compare and set, this ensures this // method always returns the exact same object parseTree = parseTreeRef.get(); } return parseTree; } + + public static CharSequence quote(CharSequence term) { + if (term.isEmpty()) { + throw new IllegalArgumentException("Empty strings are not legal authorizations."); + } + + boolean needsQuote = false; + + for (int i = 0; i < term.length(); i++) { + if (!Tokenizer.isValidAuthChar(term.charAt(i))) { + needsQuote = true; + break; + } + } + + if (!needsQuote) { + return term; + } + + return AccessEvaluatorImpl.escape(term, true); + } + + public static String unquote(String term) { + if (term.equals("\"\"") || term.isEmpty()) { + throw new IllegalArgumentException("Empty strings are not legal authorizations."); + } + + if (term.charAt(0) == '"' && term.charAt(term.length() - 1) == '"') { + term = term.substring(1, term.length() - 1); + return AccessEvaluatorImpl.unescape(term).toString(); + } else { + return term; + } + } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index f7eedf4..edd6a48 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -47,104 +47,69 @@ public AccumuloAccessImpl(AuthorizationValidator authValidator) { @Override public AccessExpression newExpression(String expression) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - - return AccessExpression.of(expression); + if (expression.isEmpty()) { + return AccessExpressionImpl.EMPTY; + } + validate(expression); + return new AccessExpressionImpl(expression); } @Override public ParsedAccessExpression newParsedExpression(String expression) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - - return AccessExpression.parse(expression); + return ParsedAccessExpressionImpl.parseExpression(expression, authValidator); } @Override public Authorizations newAuthorizations() { - return Authorizations.of(); + return AuthorizationsImpl.EMPTY; } @Override public Authorizations newAuthorizations(Set authorizations) { - authorizations.forEach(this::validateAuthorization); - - return Authorizations.of(authorizations); + if (authorizations.isEmpty()) { + return AuthorizationsImpl.EMPTY; + } else { + authorizations.forEach(this::validateAuthorization); + return new AuthorizationsImpl(authorizations); + } } @Override public void findAuthorizations(String expression, Consumer authorizationConsumer) throws InvalidAccessExpressionException { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - AccessExpression.findAuthorizations(expression, authorizationConsumer); + ParserEvaluator.findAuthorizations(expression, authorizationConsumer, authValidator); } @Override public String quote(String authorization) { validateAuthorization(authorization); - return AccessExpression.quote(authorization); + return AccessExpressionImpl.quote(authorization).toString(); } @Override public String unquote(String authorization) { - var unquoted = AccessExpression.unquote(authorization); + var unquoted = AccessExpressionImpl.unquote(authorization); validateAuthorization(unquoted); return unquoted; } @Override public void validate(String expression) throws InvalidAccessExpressionException { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(expression, this::validateAuthorization); - AccessExpression.validate(expression); - } - - // TODO remove this class and push the authorization validation down into AccessEvaluatorImpl - public final class ValidatingAccessEvaluator implements AccessEvaluator { - - private final AccessEvaluator evaluator; - - private ValidatingAccessEvaluator(AccessEvaluator evaluator) { - this.evaluator = evaluator; - } - - @Override - public boolean canAccess(String accessExpression) throws InvalidAccessExpressionException { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(accessExpression, - AccumuloAccessImpl.this::validateAuthorization); - return evaluator.canAccess(accessExpression); - } - - @Override - public boolean canAccess(byte[] accessExpression) throws InvalidAccessExpressionException { - // this method would eventually go away in the super type when byte methods are removed - return evaluator.canAccess(accessExpression); - } - - @Override - public boolean canAccess(AccessExpression accessExpression) { - // TODO push this down into the parsing code, this parses twice - AccessExpression.findAuthorizations(accessExpression.getExpression(), - AccumuloAccessImpl.this::validateAuthorization); - return evaluator.canAccess(accessExpression); - } + ParserEvaluator.validate(expression, authValidator); } @Override public AccessEvaluator newEvaluator(Authorizations authorizations) { - return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizations)); + return new AccessEvaluatorImpl(authorizations, authValidator); } @Override public AccessEvaluator newEvaluator(AccessEvaluator.Authorizer authorizer) { - return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizer)); + return new AccessEvaluatorImpl(authorizer, authValidator); } @Override public AccessEvaluator newEvaluator(Collection authorizationSets) { - return new ValidatingAccessEvaluator(AccessEvaluator.of(authorizationSets)); + return new MultiAccessEvaluatorImpl(authorizationSets, authValidator); } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java new file mode 100644 index 0000000..529d147 --- /dev/null +++ b/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java @@ -0,0 +1,72 @@ +/* + * 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.impl; + +import java.util.Iterator; +import java.util.Set; + +import org.apache.accumulo.access.Authorizations; + +public final class AuthorizationsImpl implements Authorizations { + + private static final long serialVersionUID = 1L; + + static final Authorizations EMPTY = new AuthorizationsImpl(Set.of()); + + private final Set authorizations; + + AuthorizationsImpl(Set authorizations) { + this.authorizations = Set.copyOf(authorizations); + } + + /** + * Returns the set of authorization strings in this Authorization object + * + * @return immutable set of authorization strings + */ + @Override + public Set asSet() { + return authorizations; + } + + @Override + public boolean equals(Object o) { + if (o instanceof AuthorizationsImpl) { + var oa = (AuthorizationsImpl) o; + return authorizations.equals(oa.authorizations); + } + + return false; + } + + @Override + public int hashCode() { + return authorizations.hashCode(); + } + + @Override + public String toString() { + return authorizations.toString(); + } + + @Override + public Iterator iterator() { + return authorizations.iterator(); + } +} diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java b/core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java index 8f3c209..b0ead04 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java @@ -22,6 +22,7 @@ * This class exists to avoid repeat conversions from byte to char as well as to provide helper * methods for comparing them. */ +// TODO rename final class ByteUtils { static final byte QUOTE = (byte) '"'; static final byte BACKSLASH = (byte) '\\'; @@ -32,27 +33,27 @@ private ByteUtils() { // private constructor to prevent instantiation } - static boolean isQuoteSymbol(byte b) { + static boolean isQuoteSymbol(char b) { return b == QUOTE; } - static boolean isBackslashSymbol(byte b) { + static boolean isBackslashSymbol(char b) { return b == BACKSLASH; } - static boolean isQuoteOrSlash(byte b) { + static boolean isQuoteOrSlash(char b) { return isQuoteSymbol(b) || isBackslashSymbol(b); } - static boolean isAndOperator(byte b) { + static boolean isAndOperator(char b) { return b == AND_OPERATOR; } - static boolean isOrOperator(byte b) { + static boolean isOrOperator(char b) { return b == OR_OPERATOR; } - static boolean isAndOrOperator(byte b) { + static boolean isAndOrOperator(char b) { return isAndOperator(b) || isOrOperator(b); } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java b/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java new file mode 100644 index 0000000..9f2eafa --- /dev/null +++ b/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java @@ -0,0 +1,109 @@ +/* + * 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.impl; + +public final class CharsWrapper implements CharSequence { + private CharSequence wrapped; + private int offset; + private int len; + + CharsWrapper(CharSequence wrapped) { + this.wrapped = wrapped; + this.offset = 0; + this.len = wrapped.length(); + } + + CharsWrapper(CharSequence wrapped, int offset, int len) { + // TODO bounds check + this.wrapped = wrapped; + this.offset = offset; + this.len = len; + } + + @Override + public int length() { + return len; + } + + @Override + public char charAt(int index) { + return wrapped.charAt(offset + index); + } + + @Override + public CharSequence subSequence(int start, int end) { + // TODO bounds check + return new CharsWrapper(wrapped, start + offset, end - start); + } + + @Override + public int hashCode() { + int hash = 1; + + int end = offset + length(); + for (int i = offset; i < end; i++) { + hash = (31 * hash) + wrapped.charAt(i); + } + + return hash; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CharsWrapper) { + CharsWrapper obs = (CharsWrapper) o; + + if (this == o) { + return true; + } + + if (length() != obs.length()) { + return false; + } + + int end = offset + len; + for (int i1 = offset, i2 = obs.offset; i1 < end; i1++, i2++) { + if (wrapped.charAt(i1) != obs.wrapped.charAt(i2)) { + return false; + } + } + + return true; + } + + return false; + } + + @Override + public String toString() { + char[] chars = new char[len]; + int end = offset + length(); + for (int i = offset; i < end; i++) { + chars[i - offset] = wrapped.charAt(i); + } + return new String(chars); + } + + public void set(CharSequence data, int start, int len) { + // TODO bounds check + this.wrapped = data; + this.offset = start; + this.len = len; + } +} diff --git a/core/src/main/java/org/apache/accumulo/access/impl/MultiAccessEvaluatorImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/MultiAccessEvaluatorImpl.java index 8c268f8..e2a905c 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/MultiAccessEvaluatorImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/MultiAccessEvaluatorImpl.java @@ -18,39 +18,30 @@ */ package org.apache.accumulo.access.impl; -import static java.nio.charset.StandardCharsets.UTF_8; - import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; public final class MultiAccessEvaluatorImpl implements AccessEvaluator { - public static AccessEvaluator of(Collection authorizationSets) { - return new MultiAccessEvaluatorImpl(authorizationSets); - } - private final List evaluators; - private MultiAccessEvaluatorImpl(Collection authorizationSets) { + MultiAccessEvaluatorImpl(Collection authorizationSets, + AuthorizationValidator validator) { evaluators = new ArrayList<>(authorizationSets.size()); for (Authorizations authorizations : authorizationSets) { - evaluators.add(new AccessEvaluatorImpl(authorizations)); + evaluators.add(new AccessEvaluatorImpl(authorizations, validator)); } } @Override public boolean canAccess(String accessExpression) throws InvalidAccessExpressionException { - return canAccess(accessExpression.getBytes(UTF_8)); - } - - @Override - public boolean canAccess(byte[] accessExpression) throws InvalidAccessExpressionException { for (AccessEvaluator evaluator : evaluators) { if (!evaluator.canAccess(accessExpression)) { return false; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java index 99f6464..cc9c930 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java @@ -18,7 +18,6 @@ */ package org.apache.accumulo.access.impl; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AND; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AUTHORIZATION; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.OR; @@ -30,13 +29,15 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import org.apache.accumulo.access.AuthorizationValidator; +import org.apache.accumulo.access.InvalidAuthorizationException; import org.apache.accumulo.access.ParsedAccessExpression; public final class ParsedAccessExpressionImpl extends ParsedAccessExpression { private static final long serialVersionUID = 1L; - private final byte[] expression; + private final String expression; private final int offset; private final int length; @@ -47,7 +48,7 @@ public final class ParsedAccessExpressionImpl extends ParsedAccessExpression { public static final ParsedAccessExpression EMPTY = new ParsedAccessExpressionImpl(); - private ParsedAccessExpressionImpl(byte operator, byte[] expression, int offset, int length, + private ParsedAccessExpressionImpl(char operator, String expression, int offset, int length, List children) { if (children.isEmpty()) { throw new IllegalArgumentException("Must have children with an operator"); @@ -67,9 +68,9 @@ private ParsedAccessExpressionImpl(byte operator, byte[] expression, int offset, this.children = List.copyOf(children); } - private ParsedAccessExpressionImpl(byte[] expression, int offset, int length) { + private ParsedAccessExpressionImpl(CharSequence expression, int offset, int length) { this.type = AUTHORIZATION; - this.expression = expression; + this.expression = expression.toString(); this.offset = offset; this.length = length; this.children = List.of(); @@ -79,7 +80,7 @@ private ParsedAccessExpressionImpl(byte[] expression, int offset, int length) { this.type = ExpressionType.EMPTY; this.offset = 0; this.length = 0; - this.expression = new byte[0]; + this.expression = ""; this.children = List.of(); } @@ -89,7 +90,7 @@ public String getExpression() { if (strExp != null) { return strExp; } - strExp = new String(expression, offset, length, UTF_8); + strExp = expression.substring(offset, length + offset); stringExpression.compareAndSet(null, strExp); return stringExpression.get(); } @@ -109,27 +110,29 @@ public List getChildren() { return children; } - public static ParsedAccessExpression parseExpression(byte[] expression) { - if (expression.length == 0) { + public static ParsedAccessExpression parseExpression(String expression, + AuthorizationValidator authorizationValidator) { + if (expression.isEmpty()) { return ParsedAccessExpressionImpl.EMPTY; } Tokenizer tokenizer = new Tokenizer(expression); - var parsed = ParsedAccessExpressionImpl.parseExpression(tokenizer, false); + var parsed = ParsedAccessExpressionImpl.parseExpression(tokenizer, authorizationValidator); if (tokenizer.hasNext()) { // not all input was read, so not a valid expression - tokenizer.error("Unexpected character '" + (char) tokenizer.peek() + "'"); + tokenizer.error("Unexpected character '" + tokenizer.peek() + "'"); } return parsed; } private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, - boolean wrappedWithParens) { + AuthorizationValidator authorizationValidator) { int beginOffset = tokenizer.curentOffset(); - ParsedAccessExpressionImpl node = parseParenExpressionOrAuthorization(tokenizer); + ParsedAccessExpressionImpl node = + parseParenExpressionOrAuthorization(tokenizer, authorizationValidator); if (tokenizer.hasNext()) { var operator = tokenizer.peek(); @@ -138,7 +141,8 @@ private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, nodes.add(node); do { tokenizer.advance(); - ParsedAccessExpression next = parseParenExpressionOrAuthorization(tokenizer); + ParsedAccessExpression next = + parseParenExpressionOrAuthorization(tokenizer, authorizationValidator); nodes.add(next); } while (tokenizer.hasNext() && tokenizer.peek() == operator); @@ -149,16 +153,16 @@ private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, int endOffset = tokenizer.curentOffset(); - node = new ParsedAccessExpressionImpl(operator, tokenizer.expression(), beginOffset, - endOffset - beginOffset, nodes); + node = new ParsedAccessExpressionImpl(operator, tokenizer.expression().toString(), + beginOffset, endOffset - beginOffset, nodes); } } return node; } - private static ParsedAccessExpressionImpl - parseParenExpressionOrAuthorization(Tokenizer tokenizer) { + private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(Tokenizer tokenizer, + AuthorizationValidator authorizationValidator) { if (!tokenizer.hasNext()) { tokenizer .error("Expected a '(' character or an authorization token instead saw end of input"); @@ -166,11 +170,23 @@ private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, if (tokenizer.peek() == ParserEvaluator.OPEN_PAREN) { tokenizer.advance(); - var node = parseExpression(tokenizer, true); + var node = parseExpression(tokenizer, authorizationValidator); tokenizer.next(ParserEvaluator.CLOSE_PAREN); return node; } else { var auth = tokenizer.nextAuthorization(true); + CharSequence unquotedAuth; + if (ByteUtils.isQuoteSymbol(auth.data.charAt(auth.start))) { + unquotedAuth = AccessExpressionImpl + .unquote(auth.data.subSequence(auth.start, auth.start + auth.len).toString()); + } else { + var wrapper = ParserEvaluator.lookupWrappers.get(); + wrapper.set(auth.data, auth.start, auth.len); + unquotedAuth = wrapper; + } + if (!authorizationValidator.test(unquotedAuth)) { + throw new InvalidAuthorizationException(unquotedAuth.toString()); + } return new ParsedAccessExpressionImpl(auth.data, auth.start, auth.len); } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java index 2df624c..94569f1 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java @@ -23,7 +23,9 @@ import java.util.function.Consumer; import java.util.function.Predicate; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.InvalidAccessExpressionException; +import org.apache.accumulo.access.InvalidAuthorizationException; /** * Code for parsing and evaluating an access expression at the same time. @@ -33,25 +35,53 @@ public final class ParserEvaluator { static final byte OPEN_PAREN = (byte) '('; static final byte CLOSE_PAREN = (byte) ')'; - private static final byte[] EMPTY = new byte[0]; - - static final ThreadLocal lookupWrappers = - ThreadLocal.withInitial(() -> new BytesWrapper(EMPTY)); + static final ThreadLocal lookupWrappers = + ThreadLocal.withInitial(() -> new CharsWrapper("", 0, 0)); private static final ThreadLocal tokenizers = - ThreadLocal.withInitial(() -> new Tokenizer(EMPTY)); + ThreadLocal.withInitial(() -> new Tokenizer("")); + + public static void validate(String expression, AuthorizationValidator authValidator) + throws InvalidAccessExpressionException { + if (expression.isEmpty()) { + return; + } + + var charsWrapper = ParserEvaluator.lookupWrappers.get(); + Predicate vp = authToken -> { + var authorizations = unescape(authToken, charsWrapper); + if (!authValidator.test(authorizations)) { + throw new InvalidAuthorizationException(authorizations.toString()); + } + return true; + }; + + ParserEvaluator.parseAccessExpression(expression, vp, vp); + } - public static void findAuthorizations(byte[] expression, Consumer authorizationConsumer) + public static void findAuthorizations(CharSequence expression, + Consumer authorizationConsumer, AuthorizationValidator authValidator) throws InvalidAccessExpressionException { - var bytesWrapper = ParserEvaluator.lookupWrappers.get(); + var charsWrapper = ParserEvaluator.lookupWrappers.get(); Predicate atp = authToken -> { - bytesWrapper.set(authToken.data, authToken.start, authToken.len); - authorizationConsumer.accept(AccessEvaluatorImpl.unescape(bytesWrapper)); + var authorizations = unescape(authToken, charsWrapper); + if (!authValidator.test(authorizations)) { + throw new InvalidAuthorizationException(authorizations.toString()); + } + authorizationConsumer.accept(authorizations.toString()); return true; }; ParserEvaluator.parseAccessExpression(expression, atp, atp); } - public static boolean parseAccessExpression(byte[] expression, + static CharSequence unescape(Tokenizer.AuthorizationToken token, CharsWrapper wrapper) { + wrapper.set(token.data, token.start, token.len); + if (token.hasEscapes) { + return AccessEvaluatorImpl.unescape(wrapper); + } + return wrapper; + } + + public static boolean parseAccessExpression(CharSequence expression, Predicate authorizedPredicate, Predicate shortCircuitPredicate) { var tokenizer = tokenizers.get(); diff --git a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java index da5b6ab..7a4e42e 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java @@ -18,7 +18,6 @@ */ package org.apache.accumulo.access.impl; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.accumulo.access.impl.ByteUtils.isBackslashSymbol; import static org.apache.accumulo.access.impl.ByteUtils.isQuoteOrSlash; import static org.apache.accumulo.access.impl.ByteUtils.isQuoteSymbol; @@ -47,34 +46,35 @@ public final class Tokenizer { "_-:./".chars().forEach(c -> validAuthChars[c] = true); } - public static boolean isValidAuthChar(byte b) { + public static boolean isValidAuthChar(char b) { return validAuthChars[0xff & b]; } - private byte[] expression; + private CharSequence expression; private int index; private final AuthorizationToken authorizationToken = new AuthorizationToken(); public static class AuthorizationToken { - public byte[] data; + public CharSequence data; public int start; public int len; + public boolean hasEscapes; } - Tokenizer(byte[] expression) { + Tokenizer(CharSequence expression) { this.expression = expression; authorizationToken.data = expression; } - void reset(byte[] expression) { + void reset(CharSequence expression) { this.expression = expression; authorizationToken.data = expression; this.index = 0; } boolean hasNext() { - return index < expression.length; + return index < expression.length(); } public void advance() { @@ -86,8 +86,9 @@ public void next(byte expected) { error("Expected '" + (char) expected + "' instead saw end of input"); } - if (expression[index] != expected) { - error("Expected '" + (char) expected + "' instead saw '" + (char) (expression[index]) + "'"); + if (expression.charAt(index) != expected) { + error("Expected '" + (char) expected + "' instead saw '" + (char) (expression.charAt(index)) + + "'"); } index++; } @@ -97,14 +98,14 @@ public void error(String msg) { } public void error(String msg, int idx) { - throw new InvalidAccessExpressionException(msg, new String(expression, UTF_8), idx); + throw new InvalidAccessExpressionException(msg, expression.toString(), idx); } - byte peek() { - return expression[index]; + char peek() { + return expression.charAt(index); } - byte[] expression() { + CharSequence expression() { return expression; } @@ -113,20 +114,22 @@ public int curentOffset() { } AuthorizationToken nextAuthorization(boolean includeQuotes) { - if (isQuoteSymbol(expression[index])) { + if (isQuoteSymbol(expression.charAt(index))) { int start = ++index; - while (index < expression.length && !isQuoteSymbol(expression[index])) { - if (isBackslashSymbol(expression[index])) { + boolean hasEscapes = false; + while (index < expression.length() && !isQuoteSymbol(expression.charAt(index))) { + if (isBackslashSymbol(expression.charAt(index))) { index++; - if (index == expression.length || !isQuoteOrSlash(expression[index])) { + if (index == expression.length() || !isQuoteOrSlash(expression.charAt(index))) { error("Invalid escaping within quotes", index - 1); } + hasEscapes = true; } index++; } - if (index == expression.length) { + if (index == expression.length()) { error("Unclosed quote", start - 1); } @@ -136,6 +139,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { authorizationToken.start = start; authorizationToken.len = index - start; + authorizationToken.hasEscapes = hasEscapes; if (includeQuotes) { authorizationToken.start--; @@ -146,13 +150,14 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { return authorizationToken; - } else if (isValidAuthChar(expression[index])) { + } else if (isValidAuthChar(expression.charAt(index))) { int start = index; - while (index < expression.length && isValidAuthChar(expression[index])) { + while (index < expression.length() && isValidAuthChar(expression.charAt(index))) { index++; } authorizationToken.start = start; authorizationToken.len = index - start; + authorizationToken.hasEscapes = false; return authorizationToken; } else { error( diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index 969f173..9e0a99a 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -19,8 +19,6 @@ package org.apache.accumulo.access.tests; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.accumulo.access.AccessExpression.quote; -import static org.apache.accumulo.access.AccessExpression.unquote; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -35,12 +33,9 @@ import java.util.stream.Stream; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.AccessExpression; import org.apache.accumulo.access.AccumuloAccess; -import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.impl.AccessEvaluatorImpl; -import org.apache.accumulo.access.impl.BytesWrapper; import org.junit.jupiter.api.Test; import com.google.gson.Gson; @@ -93,23 +88,24 @@ public void runTestCases() throws IOException { AccessEvaluator evaluator; assertTrue(testSet.auths.length >= 1); if (testSet.auths.length == 1) { - evaluator = accumuloAccess.newEvaluator(Authorizations.of(Set.of(testSet.auths[0]))); - runTestCases(testSet, evaluator); + evaluator = + accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of(testSet.auths[0]))); + runTestCases(accumuloAccess, testSet, evaluator); Set auths = Stream.of(testSet.auths[0]).collect(Collectors.toSet()); evaluator = accumuloAccess.newEvaluator(auths::contains); - runTestCases(testSet, evaluator); + runTestCases(accumuloAccess, testSet, evaluator); } else { - var authSets = Stream.of(testSet.auths).map(a -> Authorizations.of(Set.of(a))) - .collect(Collectors.toList()); + var authSets = Stream.of(testSet.auths) + .map(a -> accumuloAccess.newAuthorizations(Set.of(a))).collect(Collectors.toList()); evaluator = accumuloAccess.newEvaluator(authSets); - runTestCases(testSet, evaluator); + runTestCases(accumuloAccess, testSet, evaluator); } } - } - private static void runTestCases(TestDataSet testSet, AccessEvaluator evaluator) { + private static void runTestCases(AccumuloAccess accumuloAccess, TestDataSet testSet, + AccessEvaluator evaluator) { assertFalse(testSet.tests.isEmpty()); @@ -123,52 +119,42 @@ private static void runTestCases(TestDataSet testSet, AccessEvaluator evaluator) // exception if (tests.expectedResult == ExpectedResult.ACCESSIBLE || tests.expectedResult == ExpectedResult.INACCESSIBLE) { - AccessExpression.validate(expression); - AccessExpression.validate(expression.getBytes(UTF_8)); - assertEquals(expression, AccessExpression.of(expression).getExpression()); - assertEquals(expression, AccessExpression.of(expression.getBytes(UTF_8)).getExpression()); - // parsing an expression will strip uneeded outer parens - assertTrue(expression.contains(AccessExpression.parse(expression).getExpression())); - assertTrue(expression - .contains(AccessExpression.parse(expression.getBytes(UTF_8)).getExpression())); - AccessExpression.findAuthorizations(expression, auth -> {}); - AccessExpression.findAuthorizations(expression.getBytes(UTF_8), auth -> {}); + accumuloAccess.validate(expression); + assertEquals(expression, accumuloAccess.newExpression(expression).getExpression()); + // parsing an expression will strip unneeded outer parens + assertTrue( + expression.contains(accumuloAccess.newParsedExpression(expression).getExpression())); + accumuloAccess.findAuthorizations(expression, auth -> {}); } 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.parse(expression)), expression); - assertTrue(evaluator.canAccess(AccessExpression.parse(expression).getExpression()), + assertTrue(evaluator.canAccess(accumuloAccess.newExpression(expression)), expression); + assertTrue(evaluator.canAccess(accumuloAccess.newParsedExpression(expression)), + expression); + assertTrue( + evaluator.canAccess(accumuloAccess.newParsedExpression(expression).getExpression()), 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.parse(expression)), expression); - assertFalse(evaluator.canAccess(AccessExpression.parse(expression).getExpression()), + assertFalse(evaluator.canAccess(accumuloAccess.newExpression(expression)), expression); + assertFalse(evaluator.canAccess(accumuloAccess.newParsedExpression(expression)), + expression); + assertFalse( + evaluator.canAccess(accumuloAccess.newParsedExpression(expression).getExpression()), expression); break; case ERROR: assertThrows(InvalidAccessExpressionException.class, () -> evaluator.canAccess(expression), expression); assertThrows(InvalidAccessExpressionException.class, - () -> evaluator.canAccess(expression.getBytes(UTF_8)), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> AccessExpression.validate(expression), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> AccessExpression.validate(expression.getBytes(UTF_8)), expression); + () -> accumuloAccess.validate(expression), expression); assertThrows(InvalidAccessExpressionException.class, - () -> AccessExpression.of(expression), expression); + () -> accumuloAccess.newExpression(expression), expression); assertThrows(InvalidAccessExpressionException.class, - () -> AccessExpression.of(expression.getBytes(UTF_8)), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> AccessExpression.parse(expression), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> AccessExpression.parse(expression.getBytes(UTF_8)), expression); + () -> accumuloAccess.newParsedExpression(expression), expression); break; default: throw new IllegalArgumentException(); @@ -179,48 +165,52 @@ private static void runTestCases(TestDataSet testSet, AccessEvaluator evaluator) @Test public void testEmptyAuthorizations() { + var accumuloAccess = AccumuloAccess.builder().build(); + // TODO what part of the code throwing the exception? assertThrows(IllegalArgumentException.class, - () -> AccessEvaluator.of(Authorizations.of(Set.of("")))); + () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("")))); assertThrows(IllegalArgumentException.class, - () -> AccessEvaluator.of(Authorizations.of(Set.of("", "A")))); + () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("", "A")))); assertThrows(IllegalArgumentException.class, - () -> AccessEvaluator.of(Authorizations.of(Set.of("A", "")))); + () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("A", "")))); assertThrows(IllegalArgumentException.class, - () -> AccessEvaluator.of(Authorizations.of(Set.of("")))); + () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("")))); } @Test public void testSpecialChars() { + var accumuloAccess = AccumuloAccess.builder().build(); // special chars do not need quoting for (String qt : List.of("A_", "_", "A_C", "_C")) { - assertEquals(qt, quote(qt)); + assertEquals(qt, accumuloAccess.quote(qt)); for (char c : new char[] {'/', ':', '-', '.'}) { String qt2 = qt.replace('_', c); - assertEquals(qt2, quote(qt2)); + assertEquals(qt2, accumuloAccess.quote(qt2)); } } - assertEquals("a_b:c/d.e", quote("a_b:c/d.e")); + assertEquals("a_b:c/d.e", accumuloAccess.quote("a_b:c/d.e")); } @Test public void testQuote() { - assertEquals("\"A#C\"", quote("A#C")); - assertEquals("A#C", unquote(quote("A#C"))); - assertEquals("\"A\\\"C\"", quote("A\"C")); - assertEquals("A\"C", unquote(quote("A\"C"))); - assertEquals("\"A\\\"\\\\C\"", quote("A\"\\C")); - assertEquals("A\"\\C", unquote(quote("A\"\\C"))); - assertEquals("ACS", quote("ACS")); - assertEquals("ACS", unquote(quote("ACS"))); - assertEquals("\"九\"", quote("九")); - assertEquals("九", unquote(quote("九"))); - assertEquals("\"五十\"", quote("五十")); - assertEquals("五十", unquote(quote("五十"))); + var accumuloAccess = AccumuloAccess.builder().build(); + assertEquals("\"A#C\"", accumuloAccess.quote("A#C")); + assertEquals("A#C", accumuloAccess.unquote(accumuloAccess.quote("A#C"))); + assertEquals("\"A\\\"C\"", accumuloAccess.quote("A\"C")); + assertEquals("A\"C", accumuloAccess.unquote(accumuloAccess.quote("A\"C"))); + assertEquals("\"A\\\"\\\\C\"", accumuloAccess.quote("A\"\\C")); + assertEquals("A\"\\C", accumuloAccess.unquote(accumuloAccess.quote("A\"\\C"))); + assertEquals("ACS", accumuloAccess.quote("ACS")); + assertEquals("ACS", accumuloAccess.unquote(accumuloAccess.quote("ACS"))); + assertEquals("\"九\"", accumuloAccess.quote("九")); + assertEquals("九", accumuloAccess.unquote(accumuloAccess.quote("九"))); + assertEquals("\"五十\"", accumuloAccess.quote("五十")); + assertEquals("五十", accumuloAccess.unquote(accumuloAccess.quote("五十"))); } private static String unescape(String s) { - return AccessEvaluatorImpl.unescape(new BytesWrapper(s.getBytes(UTF_8))); + return AccessEvaluatorImpl.unescape(s).toString(); } @Test diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java index 64de076..28fd234 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java @@ -29,8 +29,7 @@ import java.util.stream.Stream; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.AccessExpression; -import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.core.security.ColumnVisibility; import org.apache.accumulo.core.security.VisibilityEvaluator; import org.apache.accumulo.core.security.VisibilityParseException; @@ -73,12 +72,14 @@ public static class VisibilityEvaluatorTests { public static class EvaluatorTests { AccessEvaluator evaluator; - List expressions; + List expressions; } @State(Scope.Benchmark) public static class BenchmarkState { + private AccumuloAccess accumuloAccess; + private ArrayList allTestExpressions; private ArrayList allTestExpressionsStr; @@ -89,6 +90,7 @@ public static class BenchmarkState { @Setup public void loadData() throws IOException { + accumuloAccess = AccumuloAccess.builder().build(); List testData = AccessEvaluatorTest.readTestData(); allTestExpressions = new ArrayList<>(); allTestExpressionsStr = new ArrayList<>(); @@ -119,11 +121,12 @@ public void loadData() throws IOException { et.expressions = new ArrayList<>(); if (testDataSet.auths.length == 1) { - et.evaluator = AccessEvaluator.of(Authorizations.of(Set.of(testDataSet.auths[0]))); + et.evaluator = accumuloAccess + .newEvaluator(accumuloAccess.newAuthorizations(Set.of(testDataSet.auths[0]))); } else { - var authSets = Stream.of(testDataSet.auths).map(a -> Authorizations.of(Set.of(a))) - .collect(Collectors.toList()); - et.evaluator = AccessEvaluator.of(authSets); + var authSets = Stream.of(testDataSet.auths) + .map(a -> accumuloAccess.newAuthorizations(Set.of(a))).collect(Collectors.toList()); + et.evaluator = accumuloAccess.newEvaluator(authSets); } for (var tests : testDataSet.tests) { @@ -132,7 +135,7 @@ public void loadData() throws IOException { allTestExpressionsStr.add(exp); byte[] byteExp = exp.getBytes(UTF_8); allTestExpressions.add(byteExp); - et.expressions.add(byteExp); + et.expressions.add(exp); vet.expressions.add(byteExp); vet.columnVisibilities.add(new ColumnVisibility(byteExp)); } @@ -167,8 +170,9 @@ List getVisibilityEvaluatorTests() { */ @Benchmark public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { + var accumuloAccess = state.accumuloAccess; for (byte[] accessExpression : state.getBytesExpressions()) { - AccessExpression.validate(accessExpression); + accumuloAccess.validate(new String(accessExpression, UTF_8)); } } @@ -177,8 +181,9 @@ public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { */ @Benchmark public void measureStringValidation(BenchmarkState state, Blackhole blackhole) { + var accumuloAccess = state.accumuloAccess; for (String accessExpression : state.getStringExpressions()) { - AccessExpression.validate(accessExpression); + accumuloAccess.validate(accessExpression); } } @@ -189,7 +194,7 @@ public void measureStringValidation(BenchmarkState state, Blackhole blackhole) { @Benchmark public void measureParseAndEvaluation(BenchmarkState state, Blackhole blackhole) { for (EvaluatorTests evaluatorTests : state.getEvaluatorTests()) { - for (byte[] expression : evaluatorTests.expressions) { + for (String expression : evaluatorTests.expressions) { blackhole.consume(evaluatorTests.evaluator.canAccess(expression)); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java index 16edcf0..1d82f9b 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java @@ -31,13 +31,13 @@ import java.io.InputStreamReader; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.ParsedAccessExpression; import org.junit.jupiter.api.Disabled; @@ -48,6 +48,7 @@ public class AccessExpressionTest { @Test public void testGetAuthorizations() { + var accumuloAccess = AccumuloAccess.builder().build(); // Test data pairs where the first entry of each pair is an expression to normalize and second // is the expected authorization in the expression var testData = new ArrayList>(); @@ -67,24 +68,19 @@ public void testGetAuthorizations() { var expression = testCase.get(0); var expected = testCase.get(1); HashSet found = new HashSet<>(); - AccessExpression.findAuthorizations(expression, found::add); + accumuloAccess.findAuthorizations(expression, found::add); var actual = found.stream().sorted().collect(Collectors.joining(",")); assertEquals(expected, actual); found.clear(); - AccessExpression.findAuthorizations(expression.getBytes(UTF_8), found::add); - actual = found.stream().sorted().collect(Collectors.joining(",")); - assertEquals(expected, actual); } } void checkError(String expression, String expected, int index) { - checkError(() -> AccessExpression.validate(expression), expected, index); - checkError(() -> AccessExpression.validate(expression.getBytes(UTF_8)), expected, index); - checkError(() -> AccessExpression.of(expression), expected, index); - checkError(() -> AccessExpression.of(expression.getBytes(UTF_8)), expected, index); - checkError(() -> AccessExpression.parse(expression), expected, index); - checkError(() -> AccessExpression.parse(expression.getBytes(UTF_8)), expected, index); + var accumuloAccess = AccumuloAccess.builder().build(); + checkError(() -> accumuloAccess.validate(expression), expected, index); + checkError(() -> accumuloAccess.newExpression(expression), expected, index); + checkError(() -> accumuloAccess.newParsedExpression(expression), expected, index); } void checkError(Executable executable, String expected, int index) { @@ -130,10 +126,11 @@ public void testErrorMessages() { @Test public void testEqualsHashcode() { - var ae1 = AccessExpression.of("A&B"); - var ae2 = AccessExpression.of("A&C"); - var ae3 = AccessExpression.of("A&B"); - var ae4 = AccessExpression.parse("A&B"); + var accumuloAccess = AccumuloAccess.builder().build(); + var ae1 = accumuloAccess.newExpression("A&B"); + var ae2 = accumuloAccess.newExpression("A&C"); + var ae3 = accumuloAccess.newExpression("A&B"); + var ae4 = accumuloAccess.newParsedExpression("A&B"); assertEquals(ae1, ae3); assertEquals(ae1, ae4); @@ -185,26 +182,23 @@ public void testSpecificationDocumentation() throws IOException, URISyntaxExcept @Test public void testEmpty() { + var accumuloAccess = AccumuloAccess.builder().build(); // do not expect empty expression to fail validation - AccessExpression.validate(new byte[0]); - AccessExpression.validate(""); - assertEquals("", AccessExpression.of(new byte[0]).getExpression()); - assertEquals("", AccessExpression.of("").getExpression()); - - for (var parsed : List.of(AccessExpression.parse(new byte[0]), AccessExpression.parse(""))) { - assertEquals("", parsed.getExpression()); - assertTrue(parsed.getChildren().isEmpty()); - assertEquals(ParsedAccessExpression.ExpressionType.EMPTY, parsed.getType()); - } + accumuloAccess.validate(""); + assertEquals("", accumuloAccess.newExpression("").getExpression()); + + var parsed = accumuloAccess.newParsedExpression(""); + assertEquals("", parsed.getExpression()); + assertTrue(parsed.getChildren().isEmpty()); + assertEquals(ParsedAccessExpression.ExpressionType.EMPTY, parsed.getType()); } @Test public void testImmutable() { - byte[] exp = "A&B&(C|D)".getBytes(UTF_8); - var exp1 = AccessExpression.of(exp); - var exp2 = AccessExpression.parse(exp); - Arrays.fill(exp, (byte) 0); - assertEquals("A&B&(C|D)", exp1.getExpression()); + var accumuloAccess = AccumuloAccess.builder().build(); + var exp = "A&B&(C|D)"; + var exp2 = accumuloAccess.newParsedExpression(exp); + assertEquals("A&B&(C|D)", exp2.getExpression()); assertEquals("A", exp2.getChildren().get(0).getExpression()); @@ -221,22 +215,14 @@ public void testImmutable() { @Test public void testNull() { - assertThrows(NullPointerException.class, () -> AccessExpression.parse((byte[]) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.parse((String) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.validate((byte[]) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.validate((String) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.of((byte[]) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.of((String) null)); - assertThrows(NullPointerException.class, - () -> AccessExpression.findAuthorizations((byte[]) null, auth -> {})); - assertThrows(NullPointerException.class, - () -> AccessExpression.findAuthorizations((String) null, auth -> {})); - assertThrows(NullPointerException.class, - () -> AccessExpression.findAuthorizations("A&B".getBytes(UTF_8), null)); + var accumuloAccess = AccumuloAccess.builder().build(); + assertThrows(NullPointerException.class, () -> accumuloAccess.newParsedExpression(null)); + assertThrows(NullPointerException.class, () -> accumuloAccess.validate(null)); + assertThrows(NullPointerException.class, () -> accumuloAccess.newExpression(null)); assertThrows(NullPointerException.class, - () -> AccessExpression.findAuthorizations("A&B", null)); - assertThrows(NullPointerException.class, () -> AccessExpression.quote((byte[]) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.quote((String) null)); - assertThrows(NullPointerException.class, () -> AccessExpression.unquote(null)); + () -> accumuloAccess.findAuthorizations(null, auth -> {})); + assertThrows(NullPointerException.class, () -> accumuloAccess.findAuthorizations("A&B", null)); + assertThrows(NullPointerException.class, () -> accumuloAccess.quote(null)); + assertThrows(NullPointerException.class, () -> accumuloAccess.unquote(null)); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 53d8164..2b3fa05 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -24,6 +24,7 @@ import java.util.Set; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; import org.junit.jupiter.api.Test; @@ -31,13 +32,14 @@ public class AuthorizationTest { @Test public void testEquality() { - Authorizations auths1 = Authorizations.of(Set.of("A", "B", "C")); - Authorizations auths2 = Authorizations.of(Set.of("A", "B", "C")); + var accumuloAccess = AccumuloAccess.builder().build(); + Authorizations auths1 = accumuloAccess.newAuthorizations(Set.of("A", "B", "C")); + Authorizations auths2 = accumuloAccess.newAuthorizations(Set.of("A", "B", "C")); assertEquals(auths1, auths2); assertEquals(auths1.hashCode(), auths2.hashCode()); - Authorizations auths3 = Authorizations.of(Set.of("D", "E", "F")); + Authorizations auths3 = accumuloAccess.newAuthorizations(Set.of("D", "E", "F")); assertNotEquals(auths1, auths3); assertNotEquals(auths1.hashCode(), auths3.hashCode()); @@ -45,11 +47,12 @@ public void testEquality() { @Test public void testEmpty() { + var accumuloAccess = AccumuloAccess.builder().build(); // check if new object is allocated - assertSame(Authorizations.of(), Authorizations.of()); + assertSame(accumuloAccess.newAuthorizations(), accumuloAccess.newAuthorizations()); // check if optimization is working - assertSame(Authorizations.of(), Authorizations.of(Set.of())); - assertEquals(Set.of(), Authorizations.of().asSet()); - assertSame(Set.of(), Authorizations.of().asSet()); + assertSame(accumuloAccess.newAuthorizations(), accumuloAccess.newAuthorizations(Set.of())); + assertEquals(Set.of(), accumuloAccess.newAuthorizations().asSet()); + assertSame(Set.of(), accumuloAccess.newAuthorizations().asSet()); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java index 3ef9c55..2acb5ab 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java @@ -18,7 +18,6 @@ */ package org.apache.accumulo.access.tests; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AND; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AUTHORIZATION; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.EMPTY; @@ -30,17 +29,17 @@ import java.util.List; -import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.ParsedAccessExpression; import org.junit.jupiter.api.Test; public class ParsedAccessExpressionTest { @Test public void testParsing() { + var accumuloAccess = AccumuloAccess.builder().build(); String expression = "(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|BLACK"; - for (var parsed : List.of(AccessExpression.parse(expression), - AccessExpression.parse(expression.getBytes(UTF_8)), AccessExpression.of(expression).parse(), - AccessExpression.of(expression.getBytes(UTF_8)).parse())) { + for (var parsed : List.of(accumuloAccess.newParsedExpression(expression), + accumuloAccess.newExpression(expression).parse())) { // verify root node verify("(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|BLACK", OR, 3, parsed); @@ -68,16 +67,15 @@ public void testParsing() { @Test public void testEmpty() { - var parsed = AccessExpression.parse(""); - verify("", EMPTY, 0, parsed); - parsed = AccessExpression.parse(new byte[0]); + var accumuloAccess = AccumuloAccess.builder().build(); + var parsed = accumuloAccess.newParsedExpression(""); verify("", EMPTY, 0, parsed); } @Test public void testParseTwice() { - for (var expression : List.of(AccessExpression.of("A&B"), - AccessExpression.of("A&B".getBytes(UTF_8)))) { + var accumuloAccess = AccumuloAccess.builder().build(); + for (var expression : List.of(accumuloAccess.newExpression("A&B"))) { var parsed = expression.parse(); assertNotSame(expression, parsed); assertEquals(expression.getExpression(), parsed.getExpression()); diff --git a/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java b/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java index d9cf134..b4da50f 100644 --- a/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java +++ b/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java @@ -18,15 +18,13 @@ */ package org.apache.accumulo.access.examples; -import static org.apache.accumulo.access.AccessExpression.quote; -import static org.apache.accumulo.access.AccessExpression.unquote; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AND; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AUTHORIZATION; import java.util.Map; import java.util.TreeSet; -import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.ParsedAccessExpression; import org.apache.accumulo.access.ParsedAccessExpression.ExpressionType; @@ -36,6 +34,8 @@ */ public class ParseExamples { + public static final AccumuloAccess ACCUMULO_ACCESS = AccumuloAccess.builder().build(); + private ParseExamples() {} /** @@ -47,10 +47,10 @@ public static void replaceAuthorizations(ParsedAccessExpression parsed, // If the term is quoted in the expression, the quotes will be preserved. Calling unquote() // will only unescape and unquote if the string is quoted, otherwise it returns the string as // is. - String auth = unquote(parsed.getExpression()); + String auth = ACCUMULO_ACCESS.unquote(parsed.getExpression()); // Must quote any authorization that needs it. Calling quote() will only quote and escape if // needed, otherwise it returns the string as is. - expressionBuilder.append(quote(replacements.getOrDefault(auth, auth))); + expressionBuilder.append(ACCUMULO_ACCESS.quote(replacements.getOrDefault(auth, auth))); } else { String operator = parsed.getType() == AND ? "&" : "|"; String sep = ""; @@ -103,7 +103,8 @@ public int compareTo(NormalizedExpression o) { if (cmp == 0) { if (type == AUTHORIZATION) { // sort based on the unquoted and unescaped form of the authorization - cmp = unquote(expression).compareTo(unquote(o.expression)); + cmp = + ACCUMULO_ACCESS.unquote(expression).compareTo(ACCUMULO_ACCESS.unquote(o.expression)); } else { cmp = expression.compareTo(o.expression); } @@ -170,8 +171,8 @@ public static NormalizedExpression normalize(ParsedAccessExpression parsed) { if (parsed.getType() == AUTHORIZATION) { // If the authorization is quoted and it does not need to be quoted then the following two // lines will remove the unnecessary quoting. - String unquoted = AccessExpression.unquote(parsed.getExpression()); - String quoted = AccessExpression.quote(unquoted); + String unquoted = ACCUMULO_ACCESS.unquote(parsed.getExpression()); + String quoted = ACCUMULO_ACCESS.quote(unquoted); return new NormalizedExpression(quoted, parsed.getType()); } else { // The tree set does the work of sorting and deduplicating sub expressions. @@ -217,7 +218,7 @@ public static void walk(String indent, ParsedAccessExpression parsed) { public static void main(String[] args) { - var parsed = AccessExpression.parse("((RED&GREEN)|(PINK&BLUE))"); + var parsed = ACCUMULO_ACCESS.newParsedExpression("((RED&GREEN)|(PINK&BLUE))"); System.out.printf("Operating on %s%n", parsed); diff --git a/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java b/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java index 6800a01..75b50ab 100644 --- a/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java +++ b/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java @@ -18,7 +18,7 @@ */ package org.apache.accumulo.access.examples.test; -import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.accumulo.access.examples.ParseExamples.ACCUMULO_ACCESS; import static org.apache.accumulo.access.examples.ParseExamples.replaceAuthorizations; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; -import org.apache.accumulo.access.AccessExpression; import org.apache.accumulo.access.examples.ParseExamples; import org.junit.jupiter.api.Test; @@ -90,11 +89,8 @@ public void testNormalize() { var expression = testCase.get(0); var expected = testCase.get(1); - var actual = ParseExamples.normalize(AccessExpression.parse(expression)).expression; - assertEquals(expected, actual); - - actual = - ParseExamples.normalize(AccessExpression.parse(expression.getBytes(UTF_8))).expression; + var actual = + ParseExamples.normalize(ACCUMULO_ACCESS.newParsedExpression(expression)).expression; assertEquals(expected, actual); } } @@ -102,13 +98,13 @@ public void testNormalize() { @Test public void testReplace() { // Test replacement code w/ quoting and escaping. - var parsed = AccessExpression.parse("((RED&\"ESC\\\\\")|(PINK&BLUE))"); + var parsed = ACCUMULO_ACCESS.newParsedExpression("((RED&\"ESC\\\\\")|(PINK&BLUE))"); StringBuilder expressionBuilder = new StringBuilder(); replaceAuthorizations(parsed, expressionBuilder, Map.of("ESC\\", "NEEDS+QUOTE")); assertEquals("(RED&\"NEEDS+QUOTE\")|(PINK&BLUE)", expressionBuilder.toString()); // Test replacing multiple - parsed = AccessExpression.parse("((RED&(GREEN|YELLOW))|(PINK&BLUE))"); + parsed = ACCUMULO_ACCESS.newParsedExpression("((RED&(GREEN|YELLOW))|(PINK&BLUE))"); expressionBuilder = new StringBuilder(); replaceAuthorizations(parsed, expressionBuilder, Map.of("RED", "ROUGE", "GREEN", "AQUA")); assertEquals("(ROUGE&(AQUA|YELLOW))|(PINK&BLUE)", expressionBuilder.toString()); From bc73738db2ca9869640b725f73707e8be84798b2 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Mon, 22 Dec 2025 23:36:21 +0000 Subject: [PATCH 05/33] WIP --- .../AccessExpressionAntlrEvaluator.java | 16 +++--- .../antlr/AccessExpressionAntlrBenchmark.java | 10 ++-- .../access/grammar/antlr/Antlr4Tests.java | 55 +++---------------- 3 files changed, 21 insertions(+), 60 deletions(-) diff --git a/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java b/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java index 0d1ba19..4097f89 100644 --- a/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java +++ b/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java @@ -18,6 +18,7 @@ */ package org.apache.accumulo.access.antlr4; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -26,6 +27,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.grammars.AccessExpressionParser.Access_expressionContext; @@ -35,8 +37,12 @@ import org.apache.accumulo.access.grammars.AccessExpressionParser.Or_expressionContext; import org.apache.accumulo.access.grammars.AccessExpressionParser.Or_operatorContext; +import static java.nio.charset.StandardCharsets.UTF_8; + public class AccessExpressionAntlrEvaluator { + public static final AccumuloAccess ACCUMULO_ACCESS = AccumuloAccess.builder().build(); + private class Entity { private Set authorizations; @@ -60,7 +66,7 @@ public AccessExpressionAntlrEvaluator(List authSets) { e.authorizations = new HashSet<>(entityAuths.size() * 2); a.asSet().stream().forEach(auth -> { e.authorizations.add(auth); - String quoted = AccessExpression.quote(auth); + String quoted = ACCUMULO_ACCESS.quote(auth); if (!quoted.startsWith("\"")) { quoted = '"' + quoted + '"'; } @@ -69,14 +75,6 @@ public AccessExpressionAntlrEvaluator(List authSets) { } } - public boolean canAccess(byte[] accessExpression) throws InvalidAccessExpressionException { - return canAccess(AccessExpression.of(accessExpression)); - } - - public boolean canAccess(AccessExpression accessExpression) { - return canAccess(accessExpression.getExpression()); - } - public boolean canAccess(String accessExpression) { if ("".equals(accessExpression)) { return true; diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java index 2311ddd..1beefb7 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java @@ -19,6 +19,7 @@ package org.apache.accumulo.access.grammar.antlr; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.accumulo.access.grammar.antlr.Antlr4Tests.ACCUMULO_ACCESS; import java.io.IOException; import java.net.URISyntaxException; @@ -29,7 +30,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.accumulo.access.impl.AuthorizationsImpl; import org.apache.accumulo.access.antlr.TestDataLoader; import org.apache.accumulo.access.antlr4.AccessExpressionAntlrEvaluator; import org.apache.accumulo.access.antlr4.AccessExpressionAntlrParser; @@ -64,7 +64,7 @@ public static class EvaluatorTests { List parsedExpressions; - List expressions; + List expressions; } @State(Scope.Benchmark) @@ -89,7 +89,7 @@ public void loadData() throws IOException, URISyntaxException { et.expressions = new ArrayList<>(); et.evaluator = new AccessExpressionAntlrEvaluator(Stream.of(testDataSet.auths) - .map(a -> AuthorizationsImpl.of(Set.of(a))).collect(Collectors.toList())); + .map(a -> ACCUMULO_ACCESS.newAuthorizations(Set.of(a))).collect(Collectors.toList())); for (var tests : testDataSet.tests) { if (tests.expectedResult != TestDataLoader.ExpectedResult.ERROR) { @@ -97,7 +97,7 @@ public void loadData() throws IOException, URISyntaxException { allTestExpressionsStr.add(exp); byte[] byteExp = exp.getBytes(UTF_8); allTestExpressions.add(byteExp); - et.expressions.add(byteExp); + et.expressions.add(exp); et.parsedExpressions.add(AccessExpressionAntlrParser.parseAccessExpression(exp)); } } @@ -160,7 +160,7 @@ public void measureEvaluation(BenchmarkState state, Blackhole blackhole) { @Benchmark public void measureEvaluationAndParsing(BenchmarkState state, Blackhole blackhole) { for (EvaluatorTests evaluatorTests : state.getEvaluatorTests()) { - for (byte[] expression : evaluatorTests.expressions) { + for (String expression : evaluatorTests.expressions) { blackhole.consume(evaluatorTests.evaluator.canAccess(expression)); } } diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java index c1b33e3..467bf55 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java @@ -18,7 +18,6 @@ */ package org.apache.accumulo.access.grammar.antlr; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -41,8 +40,8 @@ import org.antlr.v4.runtime.Recognizer; import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; -import org.apache.accumulo.access.impl.AuthorizationsImpl; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.antlr.TestDataLoader; import org.apache.accumulo.access.antlr.TestDataLoader.ExpectedResult; @@ -56,6 +55,8 @@ public class Antlr4Tests { + public static final AccumuloAccess ACCUMULO_ACCESS = AccumuloAccess.builder().build(); + private void testParse(String input) throws Exception { CodePointCharStream expression = CharStreams.fromString(input); final AtomicLong errors = new AtomicLong(0); @@ -109,10 +110,10 @@ public void testCompareWithAccessExpressionImplParsing() throws Exception { ExpectedResult result = test.expectedResult; for (String cv : test.expressions) { if (result == ExpectedResult.ERROR) { - assertThrows(InvalidAccessExpressionException.class, () -> AccessExpression.of(cv)); + assertThrows(InvalidAccessExpressionException.class, () -> ACCUMULO_ACCESS.newExpression(cv)); assertThrows(AssertionError.class, () -> testParse(cv)); } else { - AccessExpression.of(cv); + ACCUMULO_ACCESS.validate(cv); testParse(cv); } } @@ -123,7 +124,7 @@ public void testCompareWithAccessExpressionImplParsing() throws Exception { @Test public void testSimpleEvaluation() throws Exception { String accessExpression = "(one&two)|(foo&bar)"; - Authorizations auths = AuthorizationsImpl.of(Set.of("four", "three", "one", "two")); + Authorizations auths = ACCUMULO_ACCESS.newAuthorizations(Set.of("four", "three", "one", "two")); AccessExpressionAntlrEvaluator eval = new AccessExpressionAntlrEvaluator(List.of(auths)); assertTrue(eval.canAccess(accessExpression)); } @@ -131,7 +132,7 @@ public void testSimpleEvaluation() throws Exception { @Test public void testSimpleEvaluationFailure() throws Exception { String accessExpression = "(A&B&C)"; - Authorizations auths = AuthorizationsImpl.of(Set.of("A", "C")); + Authorizations auths = ACCUMULO_ACCESS.newAuthorizations(Set.of("A", "C")); AccessExpressionAntlrEvaluator eval = new AccessExpressionAntlrEvaluator(List.of(auths)); assertFalse(eval.canAccess(accessExpression)); } @@ -144,8 +145,8 @@ public void testCompareAntlrEvaluationAgainstAccessEvaluatorImpl() throws Except for (TestDataSet testSet : testData) { List authSets = Stream.of(testSet.auths) - .map(a -> AuthorizationsImpl.of(Set.of(a))).collect(Collectors.toList()); - AccessEvaluator evaluator = AccessEvaluator.of(authSets); + .map(a -> ACCUMULO_ACCESS.newAuthorizations(Set.of(a))).collect(Collectors.toList()); + AccessEvaluator evaluator = ACCUMULO_ACCESS.newEvaluator(authSets); AccessExpressionAntlrEvaluator antlr = new AccessExpressionAntlrEvaluator(authSets); for (TestExpressions test : testSet.tests) { @@ -153,58 +154,20 @@ public void testCompareAntlrEvaluationAgainstAccessEvaluatorImpl() throws Except switch (test.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.getBytes(UTF_8))), - expression); - assertEquals(expression, - AccessExpression.of(expression.getBytes(UTF_8)).getExpression()); - assertEquals(expression, AccessExpression.of(expression).getExpression()); - assertTrue(antlr.canAccess(expression), expression); - assertTrue(antlr.canAccess(expression.getBytes(UTF_8)), expression); - assertTrue(antlr.canAccess(AccessExpression.of(expression)), expression); - assertTrue(antlr.canAccess(AccessExpression.of(expression.getBytes(UTF_8))), - 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.getBytes(UTF_8))), - expression); - assertEquals(expression, - AccessExpression.of(expression.getBytes(UTF_8)).getExpression()); - assertEquals(expression, AccessExpression.of(expression).getExpression()); - assertFalse(antlr.canAccess(expression), expression); - assertFalse(antlr.canAccess(expression.getBytes(UTF_8)), expression); - assertFalse(antlr.canAccess(AccessExpression.of(expression)), expression); - assertFalse(antlr.canAccess(AccessExpression.of(expression.getBytes(UTF_8))), - expression); break; case ERROR: assertThrows(InvalidAccessExpressionException.class, () -> evaluator.canAccess(expression), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> evaluator.canAccess(expression.getBytes(UTF_8)), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> evaluator.canAccess(AccessExpression.of(expression)), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> evaluator.canAccess(AccessExpression.of(expression.getBytes(UTF_8))), - expression); assertThrows(InvalidAccessExpressionException.class, () -> antlr.canAccess(expression), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> antlr.canAccess(expression.getBytes(UTF_8)), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> antlr.canAccess(AccessExpression.of(expression)), expression); - assertThrows(InvalidAccessExpressionException.class, - () -> antlr.canAccess(AccessExpression.of(expression.getBytes(UTF_8))), - expression); break; default: throw new IllegalArgumentException(); From 0446fd866a4bab8299f33aa72342e8289039ed72 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 00:45:27 +0000 Subject: [PATCH 06/33] WIP --- .../access/tests/AuthorizationTest.java | 105 ++++ core/src/test/resources/testdata.json | 487 ++++++++++++++++++ 2 files changed, 592 insertions(+) create mode 100644 core/src/test/resources/testdata.json diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 2b3fa05..721c335 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -19,13 +19,18 @@ package org.apache.accumulo.access.tests; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Set; import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.InvalidAuthorizationException; import org.junit.jupiter.api.Test; public class AuthorizationTest { @@ -55,4 +60,104 @@ public void testEmpty() { assertEquals(Set.of(), accumuloAccess.newAuthorizations().asSet()); assertSame(Set.of(), accumuloAccess.newAuthorizations().asSet()); } + + @Test + public void testAuthorizationValidation() { + + // create an instance of accumulo access that expects all auths to start with a lower case + // letter followed by one or more lower case letters or digits. + var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { + if (auth.length() < 2) { + return false; + } + + char c = auth.charAt(0); + if (!Character.isLowerCase(c) || !Character.isLetter(c)) { + return false; + } + + for (int i = 1; i < auth.length(); i++) { + c = auth.charAt(i); + boolean valid = Character.isDigit(c) || (Character.isLetter(c) && Character.isLowerCase(c)); + if (!valid) { + return false; + } + } + + return true; + }).build(); + + runTest("ac", "a9", "dc", "9a", accumuloAccess); + } + + @Test + public void testNonUnicode() { + // test with a character that is not unicode + char c = '\u0378'; + assertFalse(Character.isDefined(c)); + var badAuth = new String(new char[] {'a', c}); + + var accumuloAccess = AccumuloAccess.builder().build(); + runTest("ac", "a9", "dc", badAuth, accumuloAccess); + } + + private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3, String badAuth, + AccumuloAccess accumuloAccess) { + List auths = List.of(goodAuth1, goodAuth2, badAuth, goodAuth3); + + var auths1 = accumuloAccess.newAuthorizations(Set.of(goodAuth1, goodAuth2)); + var auths2 = accumuloAccess.newAuthorizations(Set.of(goodAuth3, goodAuth2)); + + var evaluator = accumuloAccess.newEvaluator(auths1); + var multiEvaluator = accumuloAccess.newEvaluator(List.of(auths1, auths2)); + var evaluator2 = accumuloAccess.newEvaluator(auth -> auth.equals(goodAuth3)); + + for (int i = 0; i < 4; i++) { + var a1 = auths.get(i); + var a2 = auths.get((i + 1) % 4); + var a3 = auths.get((i + 2) % 4); + var a4 = auths.get((i + 3) % 4); + + // create the same expression with the invalid auth in different places + var exp = a1 + "|(" + a2 + "&" + a3 + ")|" + a4; + var exception = + assertThrows(InvalidAuthorizationException.class, () -> accumuloAccess.validate(exp)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = assertThrows(InvalidAuthorizationException.class, + () -> accumuloAccess.newExpression(exp)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = assertThrows(InvalidAuthorizationException.class, + () -> accumuloAccess.newParsedExpression(exp)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = assertThrows(InvalidAuthorizationException.class, () -> evaluator.canAccess(exp)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = + assertThrows(InvalidAuthorizationException.class, () -> multiEvaluator.canAccess(exp)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = + assertThrows(InvalidAuthorizationException.class, () -> evaluator2.canAccess(exp)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = assertThrows(InvalidAuthorizationException.class, + () -> accumuloAccess.findAuthorizations(exp, a -> {})); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = assertThrows(InvalidAuthorizationException.class, + () -> accumuloAccess.newAuthorizations(Set.of(a1, a2, a3, a4))); + assertTrue(exception.getMessage().contains(badAuth)); + } + + var exception = + assertThrows(InvalidAuthorizationException.class, () -> accumuloAccess.quote(badAuth)); + assertTrue(exception.getMessage().contains(badAuth)); + + exception = assertThrows(InvalidAuthorizationException.class, + () -> accumuloAccess.unquote('"' + badAuth + '"')); + assertTrue(exception.getMessage().contains(badAuth)); + } } diff --git a/core/src/test/resources/testdata.json b/core/src/test/resources/testdata.json new file mode 100644 index 0000000..3d94876 --- /dev/null +++ b/core/src/test/resources/testdata.json @@ -0,0 +1,487 @@ +[ + { + "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)", + "one&two&three&four", + "one|two|three|four", + "(one|five|six)&(two|seven|eight)&(three|eleven|nine|twenty)&four", + "(one&five&six)|(two&one&four)|(three&eleven&nine&twenty)|onehundred", + "(two&one&four)|(one&five&six)|(three&eleven&nine&twenty)|onehundred" + ] + }, + { + "expectedResult": "INACCESSIBLE", + "expressions": [ + "five", + "one&five", + "five&one", + "((one|foo)|bar)&goober", + "(one&five&six)|(two&seven&eight)|(three&eleven&nine&twenty)|onehundred", + "(one|five|six)&(two|seven|eight)&(three|eleven|nine|twenty)&onehundred", + "one&two&three&four&five", + "six|seven|eight|nine" + ] + } + ] + }, + { + "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", + "&(five)", + "|(five)", + "(five)&", + "five|", + "a|(b)&", + "(&five)", + "(five|)", + "one(five)", + "(five)one", + "(one)(two)", + "a|(b(c))", + "(", ")", + "(a&b", + "b|a)", + "A|B)", + "(A&B)|(C&D)&(E)", + "a|b&c", + "A&B&C|D", + "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", + "!", + "!|a", + "a|!", + "@", + "@|a", + "(@|a)", + "a|@", + "(a|@)", + "#", + "$", + "%", + "^", + "*", + "=", + "+", + "~", + "`", + "[", + "]", + "[A|Z]", + "{", + "{a|c}", + "}", + ",", + "<", + ">", + "?", + "&&", + "a&&b", + "a&b&&c", + "||", + "a||b", + "a|b||c", + "&|", + "|&", + "a|b&", + "(|a)", + "&a&b(a&b)", + "#A", + "#&", + "(A&B)D&C", + "A&B(D&C)" + ] + } + ] + }, + { + "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": "expressions with all possible chars not needing quotes", + "auths": [ + [ + "abcdefghijklmnopqrstuzwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + "/.-_:", + "Ab3/zy8" + ] + ], + "tests": [ + { + "expectedResult": "ACCESSIBLE", + "expressions": [ + "abcdefghijklmnopqrstuzwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + "/.-_:", + "Ab3/zy8", + "abcdefghijklmnopqrstuzwxyz&ABCDEFGHIJKLMNOPQRSTUVWXYZ&0123456789&/.-_:&Ab3/zy8", + "abcdefghijklmnopqrstuzwxyz|ABCDEFGHIJKLMNOPQRSTUVWXYZ|0123456789|/.-_:|Ab3/zy8", + "(abcdefghijklmnopqrstuzwxyz&ABCDEFGHIJKLMNOPQRSTUVWXYZ)|(0123456789&/.-_:&Ab3/zy8)", + "(abcdefghijklmnopqrstuzwxyz|ABCDEFGHIJKLMNOPQRSTUVWXYZ)&(0123456789|/.-_:|Ab3/zy8)" + ] + }, + { + "expectedResult": "INACCESSIBLE", + "expressions": [ + "abcdefghijklmnopqrstuzwxyz&abcdefghijklmnopqrstuzwxyz0123456789", + "ABCDEFGHIJKLMNOPQRSTUVWXYZAb3/zy8&0123456789", + "abcdefghijklmnopqrstuzwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + "Ab4/zy7|(z&Ab3/zy8)", + "/.-_:&999" + ] + } + ] + }, + { + "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)", + "\"A#C\"", + "(\"A#C\")" + ] + }, + { + "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 From 57d4b74080638b2aeff1a338278578c98b107343 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 00:49:18 +0000 Subject: [PATCH 07/33] WIP --- core/src/test/resources/testdata.json | 487 -------------------------- 1 file changed, 487 deletions(-) delete mode 100644 core/src/test/resources/testdata.json diff --git a/core/src/test/resources/testdata.json b/core/src/test/resources/testdata.json deleted file mode 100644 index 3d94876..0000000 --- a/core/src/test/resources/testdata.json +++ /dev/null @@ -1,487 +0,0 @@ -[ - { - "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)", - "one&two&three&four", - "one|two|three|four", - "(one|five|six)&(two|seven|eight)&(three|eleven|nine|twenty)&four", - "(one&five&six)|(two&one&four)|(three&eleven&nine&twenty)|onehundred", - "(two&one&four)|(one&five&six)|(three&eleven&nine&twenty)|onehundred" - ] - }, - { - "expectedResult": "INACCESSIBLE", - "expressions": [ - "five", - "one&five", - "five&one", - "((one|foo)|bar)&goober", - "(one&five&six)|(two&seven&eight)|(three&eleven&nine&twenty)|onehundred", - "(one|five|six)&(two|seven|eight)&(three|eleven|nine|twenty)&onehundred", - "one&two&three&four&five", - "six|seven|eight|nine" - ] - } - ] - }, - { - "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", - "&(five)", - "|(five)", - "(five)&", - "five|", - "a|(b)&", - "(&five)", - "(five|)", - "one(five)", - "(five)one", - "(one)(two)", - "a|(b(c))", - "(", ")", - "(a&b", - "b|a)", - "A|B)", - "(A&B)|(C&D)&(E)", - "a|b&c", - "A&B&C|D", - "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", - "!", - "!|a", - "a|!", - "@", - "@|a", - "(@|a)", - "a|@", - "(a|@)", - "#", - "$", - "%", - "^", - "*", - "=", - "+", - "~", - "`", - "[", - "]", - "[A|Z]", - "{", - "{a|c}", - "}", - ",", - "<", - ">", - "?", - "&&", - "a&&b", - "a&b&&c", - "||", - "a||b", - "a|b||c", - "&|", - "|&", - "a|b&", - "(|a)", - "&a&b(a&b)", - "#A", - "#&", - "(A&B)D&C", - "A&B(D&C)" - ] - } - ] - }, - { - "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": "expressions with all possible chars not needing quotes", - "auths": [ - [ - "abcdefghijklmnopqrstuzwxyz", - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "0123456789", - "/.-_:", - "Ab3/zy8" - ] - ], - "tests": [ - { - "expectedResult": "ACCESSIBLE", - "expressions": [ - "abcdefghijklmnopqrstuzwxyz", - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "0123456789", - "/.-_:", - "Ab3/zy8", - "abcdefghijklmnopqrstuzwxyz&ABCDEFGHIJKLMNOPQRSTUVWXYZ&0123456789&/.-_:&Ab3/zy8", - "abcdefghijklmnopqrstuzwxyz|ABCDEFGHIJKLMNOPQRSTUVWXYZ|0123456789|/.-_:|Ab3/zy8", - "(abcdefghijklmnopqrstuzwxyz&ABCDEFGHIJKLMNOPQRSTUVWXYZ)|(0123456789&/.-_:&Ab3/zy8)", - "(abcdefghijklmnopqrstuzwxyz|ABCDEFGHIJKLMNOPQRSTUVWXYZ)&(0123456789|/.-_:|Ab3/zy8)" - ] - }, - { - "expectedResult": "INACCESSIBLE", - "expressions": [ - "abcdefghijklmnopqrstuzwxyz&abcdefghijklmnopqrstuzwxyz0123456789", - "ABCDEFGHIJKLMNOPQRSTUVWXYZAb3/zy8&0123456789", - "abcdefghijklmnopqrstuzwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", - "Ab4/zy7|(z&Ab3/zy8)", - "/.-_:&999" - ] - } - ] - }, - { - "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)", - "\"A#C\"", - "(\"A#C\")" - ] - }, - { - "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 From 9ff4d774d759b661a8ef5151568c008204799dfb Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 00:51:28 +0000 Subject: [PATCH 08/33] WIP --- .../accumulo/access/impl/BytesWrapper.java | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 core/src/main/java/org/apache/accumulo/access/impl/BytesWrapper.java diff --git a/core/src/main/java/org/apache/accumulo/access/impl/BytesWrapper.java b/core/src/main/java/org/apache/accumulo/access/impl/BytesWrapper.java deleted file mode 100644 index e034f70..0000000 --- a/core/src/main/java/org/apache/accumulo/access/impl/BytesWrapper.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.impl; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.checkFromIndexSize; -import static java.util.Objects.checkIndex; - -import java.util.Arrays; - -public final class BytesWrapper implements Comparable { - - private byte[] data; - private int offset; - private 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) { - set(data, 0, data.length); - } - - public byte byteAt(int i) { - return data[offset + checkIndex(i, length)]; - } - - public int length() { - return length; - } - - @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); - } - - /* - * Wraps the given byte[] and captures the current offset and length. This method does *not* make - * a copy of the input buffer - */ - void set(byte[] data, int offset, int length) { - checkFromIndexSize(offset, length, data.length); - this.data = data; - this.offset = offset; - this.length = length; - } - -} From 77e44eac8fc3ba6b7b5b29e354460ef8e89251e8 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 18:29:04 +0000 Subject: [PATCH 09/33] WIP --- SPECIFICATION.md | 6 +- .../accumulo/access/AccumuloAccess.java | 34 +++++--- .../access/AuthorizationValidator.java | 54 ++++++++---- .../accumulo/access/impl/BuilderImpl.java | 3 +- .../access/tests/AuthorizationTest.java | 83 +++++++++++++++++++ .../access/examples/AccessExample.java | 4 +- 6 files changed, 151 insertions(+), 33 deletions(-) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 9fb18b8..6869625 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -56,9 +56,9 @@ escaped = "\" DQUOTE / "\\" slash = "/" ``` -Authorizations must be Unicode characters. Not all Unicode characters are human readable -(see [Unicode control characters][6]), implementations should provide a way to limit valid authorizations to human -readable characters. +Authorizations must be Unicode characters. Not all Unicode characters are human readable or even visible +(see [Unicode control characters][6]), implementations should provide a way to limit valid authorizations to +a subset of unicode characters (like human-readable characters). ### Examples of Proper Expressions diff --git a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java index 2d9455a..ea0c8ee 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -24,25 +24,36 @@ import org.apache.accumulo.access.impl.BuilderImpl; -// TODO javadoc -// TODO remove all of the static entry points and use this instead +/** + * The entry point into Accumulo Access to create access expressions, expression evaluators, and + * authorization sets. + * + * @see #builder() + * @since 1.0 + */ public interface AccumuloAccess { interface Builder { /** - * TODO document that users should make this as specific as possible in order to avoid creating - * unexpected expressions - * - * TODO document performance reasons for passing CharSequence (allows avoiding obj alloc) + * Provide a validator to accumulo access to narrow the set of valid authorizations for your + * specific use case. If one is not provided then + * {@link AuthorizationValidator#UNICODE_AND_NOT_ISO_CONTROL} will be used. * + *

+ * The provided validator is called very frequently within accumulo access and implementations + * that are slow will slow down accumulo access. */ Builder authorizationValidator(AuthorizationValidator validator); AccumuloAccess build(); } - public static Builder builder() { - // TODO avoid object allocation when creating default + /** + * Used to create an instance of AccumuloAccess. For efficiency, the recommend way to use this is + * to create a single instance and somehow make it available to an entire project for use. In + * addition to being efficient this ensures the entire project is using the same configuration. + */ + static Builder builder() { return new BuilderImpl(); } @@ -101,9 +112,10 @@ ParsedAccessExpression newParsedExpression(String expression) * *

* What this method does could also be accomplished by creating a parse tree using - * {@link AccessExpression#parse(String)} and then recursively walking the parse tree. The - * implementation of this method does not create a parse tree and is much faster. If a parse tree - * is already available, then it would likely be faster to use it rather than call this method. + * {@link AccumuloAccess#newParsedExpression(String)} and then recursively walking the parse tree. + * The implementation of this method does not create a parse tree and is much faster. If a parse + * tree is already available, then it would likely be faster to use it rather than call this + * method. *

* * @throws InvalidAccessExpressionException when the expression is not valid. diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java index adcaebf..8064d5f 100644 --- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java +++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -20,29 +20,51 @@ import java.util.function.Predicate; -// TODO empty auth is not valid -// TODO pass in if known to be ascii? +// TODO pass in if known to be an unquoted string? This means something is already known about the characters +/** + * Implementations validate authorizations for Accumulo Access. Creating implementations that are + * stricter for a given domain can help avoid expressions that contain unexpected and unused + * authorizations. + * + *

+ * A CharSequence is passed to this predicate for efficiency. It allows having a view into the + * larger expression at parse time without any memory allocations. It is not safe to keep a + * reference to the passed in char sequence as it is only stable while the predicate is called. If a + * reference needs to be kept for some side effect, then call {@code toString()} to allocate a copy. + * Avoiding calls to {@code toString()} will result in faster parsing. + *

+ */ public interface AuthorizationValidator extends Predicate { - // TODO document - AuthorizationValidator UNICODE = auth -> { - for (int i = 0; i < auth.length(); i++) { - if (!Character.isDefined(auth.charAt(i))) { - return false; - } - } - return true; - }; - // TODO document - AuthorizationValidator READABLE = auth -> { + /** + * This is the default validator for Accumulo Access. It does the following check of characters in + * an authorization. + * + *
+   *     {@code
+   *     AuthorizationValidator UNICODE_AND_NOT_ISO_CONTROL = auth -> {
+   *       for (int i = 0; i < auth.length(); i++) {
+   *         var c = auth.charAt(i);
+   *         if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c)) {
+   *           return false;
+   *         }
+   *       }
+   *       return true;
+   *     }
+   *     }
+   * 
+ * + * @see Character#isDefined(char) + * @see Character#isISOControl(char) + * @since 1.0.0 + */ + AuthorizationValidator UNICODE_AND_NOT_ISO_CONTROL = auth -> { for (int i = 0; i < auth.length(); i++) { var c = auth.charAt(i); - if (Character.isISOControl(c) || Character.isWhitespace(c) || !Character.isDefined(c) - || c == '\uFFFD') { + if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c)) { return false; } } return true; }; - } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java index 00ecc57..e0521d9 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -35,7 +35,8 @@ public AccumuloAccess.Builder authorizationValidator(AuthorizationValidator vali @Override public AccumuloAccess build() { - return new AccumuloAccessImpl(validator == null ? AuthorizationValidator.UNICODE : validator); + return new AccumuloAccessImpl( + validator == null ? AuthorizationValidator.UNICODE_AND_NOT_ISO_CONTROL : validator); } public static void main(String[] args) { diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 721c335..78a2717 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -31,6 +31,7 @@ import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAuthorizationException; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class AuthorizationTest { @@ -101,6 +102,88 @@ public void testNonUnicode() { runTest("ac", "a9", "dc", badAuth, accumuloAccess); } + @Test + public void testUnescaped() { + // This test ensures that auth passed to the authorization validator are unescaped, even if they + // are escaped in the expression + var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { + for (int i = 0; i < auth.length(); i++) { + Assertions.assertNotEquals('\\', auth.charAt(i)); + } + return true; + }).build(); + + var quoted = accumuloAccess.quote("ABC\"D"); + assertEquals('"' + "ABC\\\"D" + '"', quoted); + assertEquals("ABC\"D", accumuloAccess.unquote(quoted)); + + var auths1 = accumuloAccess.newAuthorizations(Set.of("ABC\"D", "DEF")); + var auths2 = accumuloAccess.newAuthorizations(Set.of("ABC\"D", "XYZ")); + var evaluator = accumuloAccess.newEvaluator(auths1); + + assertTrue(evaluator.canAccess(quoted + "|XYZ")); + assertTrue(evaluator.canAccess("(XYZ&RST)|" + quoted)); + + assertTrue(evaluator.canAccess(accumuloAccess.newExpression("(XYZ&RST)|" + quoted))); + assertTrue(evaluator.canAccess(accumuloAccess.newParsedExpression("(XYZ&RST)|" + quoted))); + assertTrue(evaluator.canAccess(accumuloAccess.newExpression("(XYZ&RST)|" + quoted).parse())); + + var multiEvaluator = accumuloAccess.newEvaluator(List.of(auths1, auths2)); + assertTrue(multiEvaluator.canAccess(quoted + "|XYZ")); + assertFalse(multiEvaluator.canAccess(quoted + "&DEF")); + assertTrue(evaluator.canAccess(quoted + "&DEF")); + + } + + @Test + public void testMultiCharCodepoint() { + // Some unicode characters span two UTF-16 chars, this test ensures its possible to handle + // those. + + String doubleChar = ""; + // find any valid code point that takes two chars + for (int i = 1 << 16; i < Integer.MAX_VALUE; i++) { + if (Character.isDefined(i)) { + var dc = Character.toChars(i); + if (dc.length == 2) { + System.out.printf("found %x\n", i); + doubleChar = new String(dc); + break; + } + } + } + + assertEquals(2, doubleChar.length()); + + // create an auth that uses doubleChar + var auth1 = "a" + doubleChar; + var auth2 = "abc"; + + // Ensure the defaults fail for this auth + var accumuloAccess = AccumuloAccess.builder().build(); + + var exp1 = accumuloAccess.quote(auth1) + "&" + auth2; + var exp2 = accumuloAccess.quote(auth1) + "|" + auth2; + + assertEquals('"' + auth1 + '"' + "&" + auth2, exp1); + assertEquals('"' + auth1 + '"' + "|" + auth2, exp2); + + var auths1 = accumuloAccess.newAuthorizations(Set.of(auth1, auth2)); + var evaluator1 = accumuloAccess.newEvaluator(auths1); + assertTrue(evaluator1.canAccess(exp1)); + assertTrue(evaluator1.canAccess(exp2)); + + var auths2 = accumuloAccess.newAuthorizations(Set.of(auth1)); + var evaluator2 = accumuloAccess.newEvaluator(auths2); + assertFalse(evaluator2.canAccess(exp1)); + assertTrue(evaluator2.canAccess(exp2)); + + var auths3 = accumuloAccess.newAuthorizations(Set.of(auth2)); + var evaluator3 = accumuloAccess.newEvaluator(auths3); + assertFalse(evaluator3.canAccess(exp1)); + assertTrue(evaluator3.canAccess(exp2)); + } + private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3, String badAuth, AccumuloAccess accumuloAccess) { List auths = List.of(goodAuth1, goodAuth2, badAuth, goodAuth3); diff --git a/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java b/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java index efe84aa..37c7a67 100644 --- a/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java +++ b/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java @@ -53,8 +53,8 @@ public void run(PrintStream out, String... authorizations) { out.printf("Showing accessible records using authorizations: %s%n", Arrays.toString(authorizations)); - var accumuloAccess = - AccumuloAccess.builder().authorizationValidator(AuthorizationValidator.READABLE).build(); + var accumuloAccess = AccumuloAccess.builder() + .authorizationValidator(AuthorizationValidator.UNICODE_AND_NOT_ISO_CONTROL).build(); // Create an access evaluator using the provided authorizations AccessEvaluator evaluator = From b27aba66a05a150ac6bf4785bd2c0e6d689fbc05 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 19:36:19 +0000 Subject: [PATCH 10/33] WIP --- .../accumulo/access/impl/Tokenizer.java | 5 +-- .../access/tests/AuthorizationTest.java | 43 +++++++++++++++++-- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java index 7a4e42e..0e23913 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java @@ -47,7 +47,7 @@ public final class Tokenizer { } public static boolean isValidAuthChar(char b) { - return validAuthChars[0xff & b]; + return validAuthChars[0xff & b] && b < 256; } private CharSequence expression; @@ -160,8 +160,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { authorizationToken.hasEscapes = false; return authorizationToken; } else { - error( - "Expected a '(' character or an authorization token instead saw '" + (char) peek() + "'"); + error("Expected a '(' character or an authorization token instead saw '" + peek() + "'"); return null; } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 78a2717..86a4eeb 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -30,7 +30,9 @@ import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.Authorizations; +import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.InvalidAuthorizationException; +import org.apache.accumulo.access.impl.Tokenizer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -96,12 +98,36 @@ public void testNonUnicode() { // test with a character that is not unicode char c = '\u0378'; assertFalse(Character.isDefined(c)); + assertFalse(Character.isISOControl(c)); var badAuth = new String(new char[] {'a', c}); var accumuloAccess = AccumuloAccess.builder().build(); runTest("ac", "a9", "dc", badAuth, accumuloAccess); } + @Test + public void testControlCharacters() { + char c = '\u000c'; + assertTrue(Character.isDefined(c)); + assertTrue(Character.isISOControl(c)); + + var accumuloAccess = AccumuloAccess.builder().build(); + var badAuth = new String(new char[] {'a', c}); + runTest("ac", "a9", "dc", badAuth, accumuloAccess); + } + + @Test + public void testAuthorizationCharacters() { + for (char c = 0; c < Character.MAX_VALUE; c++) { + boolean valid = (c >= 'a' && c <= 'z') | (c >= 'A' && c <= 'Z') | (c >= '0' && c <= '9') + | c == '_' | c == '-' | c == ':' | c == '.' | c == '/'; + // This code had a bug where it was only considering the lower 8 bits of the 16 bits in the + // char and that caused an obscure and easy to miss problem. So this test was written to cover + // all 64k chars. + assertEquals(valid, Tokenizer.isValidAuthChar(c)); + } + } + @Test public void testUnescaped() { // This test ensures that auth passed to the authorization validator are unescaped, even if they @@ -137,7 +163,7 @@ public void testUnescaped() { @Test public void testMultiCharCodepoint() { - // Some unicode characters span two UTF-16 chars, this test ensures its possible to handle + // Some unicode code points span two UTF-16 chars, this test ensures its possible to handle // those. String doubleChar = ""; @@ -146,7 +172,6 @@ public void testMultiCharCodepoint() { if (Character.isDefined(i)) { var dc = Character.toChars(i); if (dc.length == 2) { - System.out.printf("found %x\n", i); doubleChar = new String(dc); break; } @@ -159,7 +184,6 @@ public void testMultiCharCodepoint() { var auth1 = "a" + doubleChar; var auth2 = "abc"; - // Ensure the defaults fail for this auth var accumuloAccess = AccumuloAccess.builder().build(); var exp1 = accumuloAccess.quote(auth1) + "&" + auth2; @@ -202,7 +226,8 @@ private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3 var a4 = auths.get((i + 3) % 4); // create the same expression with the invalid auth in different places - var exp = a1 + "|(" + a2 + "&" + a3 + ")|" + a4; + var exp = + '"' + a1 + '"' + "|(" + '"' + a2 + '"' + "&" + '"' + a3 + '"' + ")|" + '"' + a4 + '"'; var exception = assertThrows(InvalidAuthorizationException.class, () -> accumuloAccess.validate(exp)); assertTrue(exception.getMessage().contains(badAuth)); @@ -233,6 +258,16 @@ private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3 exception = assertThrows(InvalidAuthorizationException.class, () -> accumuloAccess.newAuthorizations(Set.of(a1, a2, a3, a4))); assertTrue(exception.getMessage().contains(badAuth)); + + if (badAuth.chars().anyMatch(c -> !Tokenizer.isValidAuthChar((char) c))) { + // If the expression is created w/o quoting then the bad auth should just be seen as an + // invalid character in the expression and it should not even consult the authorization + // validation code. This should result in a different exception. + var exp2 = a1 + "|(" + a2 + "&" + a3 + ")|" + a4; + var exception2 = + assertThrows(InvalidAccessExpressionException.class, () -> evaluator.canAccess(exp2)); + assertTrue(exception2.getMessage().contains(badAuth)); + } } var exception = From bdce1e6affe13fb6993ab10e8743fd51c43cda48 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 20:08:18 +0000 Subject: [PATCH 11/33] remove unused code --- .../apache/accumulo/access/impl/BuilderImpl.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java index e0521d9..c10894f 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -38,18 +38,4 @@ public AccumuloAccess build() { return new AccumuloAccessImpl( validator == null ? AuthorizationValidator.UNICODE_AND_NOT_ISO_CONTROL : validator); } - - public static void main(String[] args) { - var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { - for (int i = 0; i < auth.length(); i++) { - var c = auth.charAt(i); - if (Character.isISOControl(c) || Character.isWhitespace(c) || !Character.isDefined(c) - || c == '\uFFFD') { - return false; - } - } - return true; - }).build(); - var expression = accumuloAccess.newExpression("a|b"); - } } From 48d96a7d37642e906850f3e9dded0dc9deba5b99 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 20:16:36 +0000 Subject: [PATCH 12/33] fix build --- .../main/java/org/apache/accumulo/access/AccumuloAccess.java | 2 +- .../org/apache/accumulo/access/tests/AuthorizationTest.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java index ea0c8ee..9f899a3 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -75,7 +75,7 @@ AccessExpression newExpression(String expression) /** * Validates an access expression and returns an immutable object with a parse tree. Creating the - * parse tree is expensive relative to calling {@link # newExpression(String)} or + * parse tree is expensive relative to calling {@link #newExpression(String)} or * {@link #validate(String)}, so only use this method when the parse tree is always needed. If the * code may only use the parse tree sometimes, then it may be best to call * {@link #newExpression(String)} to create the access expression and then call diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 86a4eeb..3e784bb 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -33,7 +33,6 @@ import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.InvalidAuthorizationException; import org.apache.accumulo.access.impl.Tokenizer; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class AuthorizationTest { @@ -134,7 +133,7 @@ public void testUnescaped() { // are escaped in the expression var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> { for (int i = 0; i < auth.length(); i++) { - Assertions.assertNotEquals('\\', auth.charAt(i)); + assertNotEquals('\\', auth.charAt(i)); } return true; }).build(); From 540dc9b4c12fc204ca9738a1fc1a74177f0266f5 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 20:41:15 +0000 Subject: [PATCH 13/33] remove unintended change to module --- core/src/main/java/module-info.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index d2a3a2e..37addd4 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -18,5 +18,4 @@ */ module accumulo.access.core { exports org.apache.accumulo.access; - exports org.apache.accumulo.access.impl; } \ No newline at end of file From 8ae95dbc2ac7d87268689b2294a4bf23e2510100 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 21:16:11 +0000 Subject: [PATCH 14/33] exclude replacement char by default --- .../org/apache/accumulo/access/AccumuloAccess.java | 4 ++-- .../accumulo/access/AuthorizationValidator.java | 14 ++++++++++---- .../apache/accumulo/access/impl/BuilderImpl.java | 3 +-- .../accumulo/access/examples/AccessExample.java | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java index 9f899a3..a77b9e2 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java @@ -36,8 +36,8 @@ public interface AccumuloAccess { interface Builder { /** * Provide a validator to accumulo access to narrow the set of valid authorizations for your - * specific use case. If one is not provided then - * {@link AuthorizationValidator#UNICODE_AND_NOT_ISO_CONTROL} will be used. + * specific use case. If one is not provided then {@link AuthorizationValidator#DEFAULT} will be + * used. * *

* The provided validator is called very frequently within accumulo access and implementations diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java index 8064d5f..1eba0a9 100644 --- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java +++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -42,10 +42,10 @@ public interface AuthorizationValidator extends Predicate { * *

    *     {@code
-   *     AuthorizationValidator UNICODE_AND_NOT_ISO_CONTROL = auth -> {
+   *     AuthorizationValidator DEFAULT = auth -> {
    *       for (int i = 0; i < auth.length(); i++) {
    *         var c = auth.charAt(i);
-   *         if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c)) {
+   *         if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c) || c == '\uFFFD') {
    *           return false;
    *         }
    *       }
@@ -54,14 +54,20 @@ public interface AuthorizationValidator extends Predicate {
    *     }
    * 
* + *

+ * The character U+FFFD is the Unicode replacement character and standard java libraries will + * insert this into strings when it has problem decoding UTF-8. Therefore, seeing this character + * likely means a java string was derived from corrupt or invalid UTF-8 data. This is why its + * considered invalid in an authorization by default. + * * @see Character#isDefined(char) * @see Character#isISOControl(char) * @since 1.0.0 */ - AuthorizationValidator UNICODE_AND_NOT_ISO_CONTROL = auth -> { + AuthorizationValidator DEFAULT = auth -> { for (int i = 0; i < auth.length(); i++) { var c = auth.charAt(i); - if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c)) { + if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c) || c == '\uFFFD') { return false; } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java index c10894f..2d3f41a 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -35,7 +35,6 @@ public AccumuloAccess.Builder authorizationValidator(AuthorizationValidator vali @Override public AccumuloAccess build() { - return new AccumuloAccessImpl( - validator == null ? AuthorizationValidator.UNICODE_AND_NOT_ISO_CONTROL : validator); + return new AccumuloAccessImpl(validator == null ? AuthorizationValidator.DEFAULT : validator); } } diff --git a/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java b/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java index 37c7a67..423afd1 100644 --- a/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java +++ b/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java @@ -53,8 +53,8 @@ public void run(PrintStream out, String... authorizations) { out.printf("Showing accessible records using authorizations: %s%n", Arrays.toString(authorizations)); - var accumuloAccess = AccumuloAccess.builder() - .authorizationValidator(AuthorizationValidator.UNICODE_AND_NOT_ISO_CONTROL).build(); + var accumuloAccess = + AccumuloAccess.builder().authorizationValidator(AuthorizationValidator.DEFAULT).build(); // Create an access evaluator using the provided authorizations AccessEvaluator evaluator = From a75d79ea08669d6ebe0e8fb35eb00474130009f8 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 21:19:36 +0000 Subject: [PATCH 15/33] adds test for replacement char --- .../accumulo/access/tests/AuthorizationTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 3e784bb..5d43914 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -18,6 +18,7 @@ */ package org.apache.accumulo.access.tests; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -115,6 +116,16 @@ public void testControlCharacters() { runTest("ac", "a9", "dc", badAuth, accumuloAccess); } + @Test + public void testReplacementCharacter() { + char c = '\uFFFD'; + assertEquals(c + "", UTF_8.newDecoder().replacement()); + + var accumuloAccess = AccumuloAccess.builder().build(); + var badAuth = new String(new char[] {'a', c}); + runTest("ac", "a9", "dc", badAuth, accumuloAccess); + } + @Test public void testAuthorizationCharacters() { for (char c = 0; c < Character.MAX_VALUE; c++) { From 61900987f3526dcc175132a64bee732b73fa1dfa Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 23 Dec 2025 21:31:34 +0000 Subject: [PATCH 16/33] pass information about quoting for optimization --- .../access/AuthorizationValidator.java | 38 ++++++++++++++++--- .../access/impl/AccessEvaluatorImpl.java | 4 +- .../access/impl/AccessExpressionImpl.java | 2 +- .../access/impl/AccumuloAccessImpl.java | 14 ++++--- .../impl/ParsedAccessExpressionImpl.java | 5 ++- .../accumulo/access/impl/ParserEvaluator.java | 4 +- .../accumulo/access/impl/Tokenizer.java | 5 +++ .../access/tests/AuthorizationTest.java | 10 ++++- 8 files changed, 64 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java index 1eba0a9..94b4d64 100644 --- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java +++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -18,15 +18,20 @@ */ package org.apache.accumulo.access; -import java.util.function.Predicate; +import java.util.function.BiPredicate; -// TODO pass in if known to be an unquoted string? This means something is already known about the characters /** * Implementations validate authorizations for Accumulo Access. Creating implementations that are * stricter for a given domain can help avoid expressions that contain unexpected and unused * authorizations. * *

+ * When an authorization is quoted and/or escaped in access expression that is undone before is + * passed to this predicate. Conceptually it is like {@link AccumuloAccess#unquote(String)} is + * called prior to being passed to this predicate. If the authorization was quoted that information + * is passed along is it may be useful for optimizations. + * + *

* A CharSequence is passed to this predicate for efficiency. It allows having a view into the * larger expression at parse time without any memory allocations. It is not safe to keep a * reference to the passed in char sequence as it is only stable while the predicate is called. If a @@ -34,7 +39,21 @@ * Avoiding calls to {@code toString()} will result in faster parsing. *

*/ -public interface AuthorizationValidator extends Predicate { +public interface AuthorizationValidator + extends BiPredicate { + + enum AuthorizationQuoting { + /** + * Denotes that an authorization seen in a valid access expression was quoted. This may mean the + * expression has extra characters not seen in an unquoted authorization. + */ + QUOTED, + /** + * Denotes that an authorization seen in a valid access expression was unquoted. This means the + * expression only contains the characters allowed in an unquoted authorization. + */ + UNQUOTED + } /** * This is the default validator for Accumulo Access. It does the following check of characters in @@ -42,7 +61,10 @@ public interface AuthorizationValidator extends Predicate { * *
    *     {@code
-   *     AuthorizationValidator DEFAULT = auth -> {
+   *     AuthorizationValidator DEFAULT = (auth, quoting) -> {
+   *       if(quoting == AuthorizationQuoting.UNQUOTED) {
+   *         return true;
+   *       }
    *       for (int i = 0; i < auth.length(); i++) {
    *         var c = auth.charAt(i);
    *         if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c) || c == '\uFFFD') {
@@ -64,7 +86,13 @@ public interface AuthorizationValidator extends Predicate {
    * @see Character#isISOControl(char)
    * @since 1.0.0
    */
-  AuthorizationValidator DEFAULT = auth -> {
+  AuthorizationValidator DEFAULT = (auth, quoting) -> {
+    if (quoting == AuthorizationQuoting.UNQUOTED) {
+      // If a string in a valid access expression is unquoted and then its already known to only
+      // contain a small set of ASCII chars and no further validation is needed.
+      return true;
+    }
+
     for (int i = 0; i < auth.length(); i++) {
       var c = auth.charAt(i);
       if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c) || c == '\uFFFD') {
diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java
index e34fb56..ee97549 100644
--- a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java
+++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java
@@ -164,7 +164,7 @@ boolean evaluate(String accessExpression) throws InvalidAccessExpressionExceptio
     var charsWrapper = ParserEvaluator.lookupWrappers.get();
     Predicate atp = authToken -> {
       var authorization = ParserEvaluator.unescape(authToken, charsWrapper);
-      if (!authorizationValidator.test(authorization)) {
+      if (!authorizationValidator.test(authorization, authToken.quoting)) {
         throw new InvalidAuthorizationException(authorization.toString());
       }
       return authorizedPredicate.test(authorization);
@@ -174,7 +174,7 @@ boolean evaluate(String accessExpression) throws InvalidAccessExpressionExceptio
     // to validate authorizations, do not need to look them up in a set.
     Predicate shortCircuit = authToken -> {
       var authorization = ParserEvaluator.unescape(authToken, charsWrapper);
-      if (!authorizationValidator.test(authorization)) {
+      if (!authorizationValidator.test(authorization, authToken.quoting)) {
         throw new InvalidAuthorizationException(authorization.toString());
       }
       return true;
diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java
index dcdad61..603a528 100644
--- a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java
+++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java
@@ -47,7 +47,7 @@ public ParsedAccessExpression parse() {
       // This expression authorizations were already validated, so can pass a lambda that always
       // returns true
       parseTreeRef.compareAndSet(null,
-          ParsedAccessExpressionImpl.parseExpression(expression, auth -> true));
+          ParsedAccessExpressionImpl.parseExpression(expression, (auth, quoting) -> true));
       // must get() again in case another thread won w/ the compare and set, this ensures this
       // method always returns the exact same object
       parseTree = parseTreeRef.get();
diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java
index edd6a48..3fd2caf 100644
--- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java
+++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java
@@ -18,6 +18,8 @@
  */
 package org.apache.accumulo.access.impl;
 
+import static org.apache.accumulo.access.AuthorizationValidator.AuthorizationQuoting.QUOTED;
+
 import java.util.Collection;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -35,8 +37,9 @@ public class AccumuloAccessImpl implements AccumuloAccess {
 
   private final AuthorizationValidator authValidator;
 
-  private void validateAuthorization(CharSequence auth) {
-    if (!authValidator.test(auth)) {
+  private void validateAuthorization(CharSequence auth,
+      AuthorizationValidator.AuthorizationQuoting quoting) {
+    if (!authValidator.test(auth, quoting)) {
       throw new InvalidAuthorizationException(auth.toString());
     }
   }
@@ -69,7 +72,8 @@ public Authorizations newAuthorizations(Set authorizations) {
     if (authorizations.isEmpty()) {
       return AuthorizationsImpl.EMPTY;
     } else {
-      authorizations.forEach(this::validateAuthorization);
+      // not sure if the auth needs quoting or not, so assume it does
+      authorizations.forEach(auth -> validateAuthorization(auth, QUOTED));
       return new AuthorizationsImpl(authorizations);
     }
   }
@@ -82,14 +86,14 @@ public void findAuthorizations(String expression, Consumer authorization
 
   @Override
   public String quote(String authorization) {
-    validateAuthorization(authorization);
+    validateAuthorization(authorization, QUOTED);
     return AccessExpressionImpl.quote(authorization).toString();
   }
 
   @Override
   public String unquote(String authorization) {
     var unquoted = AccessExpressionImpl.unquote(authorization);
-    validateAuthorization(unquoted);
+    validateAuthorization(unquoted, QUOTED);
     return unquoted;
   }
 
diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java
index cc9c930..b29148d 100644
--- a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java
+++ b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java
@@ -176,15 +176,18 @@ private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(To
     } else {
       var auth = tokenizer.nextAuthorization(true);
       CharSequence unquotedAuth;
+      AuthorizationValidator.AuthorizationQuoting quoting;
       if (ByteUtils.isQuoteSymbol(auth.data.charAt(auth.start))) {
         unquotedAuth = AccessExpressionImpl
             .unquote(auth.data.subSequence(auth.start, auth.start + auth.len).toString());
+        quoting = AuthorizationValidator.AuthorizationQuoting.QUOTED;
       } else {
         var wrapper = ParserEvaluator.lookupWrappers.get();
         wrapper.set(auth.data, auth.start, auth.len);
         unquotedAuth = wrapper;
+        quoting = AuthorizationValidator.AuthorizationQuoting.UNQUOTED;
       }
-      if (!authorizationValidator.test(unquotedAuth)) {
+      if (!authorizationValidator.test(unquotedAuth, quoting)) {
         throw new InvalidAuthorizationException(unquotedAuth.toString());
       }
       return new ParsedAccessExpressionImpl(auth.data, auth.start, auth.len);
diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java
index 94569f1..efc8371 100644
--- a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java
+++ b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java
@@ -49,7 +49,7 @@ public static void validate(String expression, AuthorizationValidator authValida
     var charsWrapper = ParserEvaluator.lookupWrappers.get();
     Predicate vp = authToken -> {
       var authorizations = unescape(authToken, charsWrapper);
-      if (!authValidator.test(authorizations)) {
+      if (!authValidator.test(authorizations, authToken.quoting)) {
         throw new InvalidAuthorizationException(authorizations.toString());
       }
       return true;
@@ -64,7 +64,7 @@ public static void findAuthorizations(CharSequence expression,
     var charsWrapper = ParserEvaluator.lookupWrappers.get();
     Predicate atp = authToken -> {
       var authorizations = unescape(authToken, charsWrapper);
-      if (!authValidator.test(authorizations)) {
+      if (!authValidator.test(authorizations, authToken.quoting)) {
         throw new InvalidAuthorizationException(authorizations.toString());
       }
       authorizationConsumer.accept(authorizations.toString());
diff --git a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java
index 0e23913..baf1164 100644
--- a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java
+++ b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java
@@ -24,6 +24,7 @@
 
 import java.util.stream.IntStream;
 
+import org.apache.accumulo.access.AuthorizationValidator;
 import org.apache.accumulo.access.InvalidAccessExpressionException;
 
 /**
@@ -60,6 +61,8 @@ public static class AuthorizationToken {
     public int start;
     public int len;
     public boolean hasEscapes;
+    public AuthorizationValidator.AuthorizationQuoting quoting;
+
   }
 
   Tokenizer(CharSequence expression) {
@@ -140,6 +143,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) {
       authorizationToken.start = start;
       authorizationToken.len = index - start;
       authorizationToken.hasEscapes = hasEscapes;
+      authorizationToken.quoting = AuthorizationValidator.AuthorizationQuoting.QUOTED;
 
       if (includeQuotes) {
         authorizationToken.start--;
@@ -158,6 +162,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) {
       authorizationToken.start = start;
       authorizationToken.len = index - start;
       authorizationToken.hasEscapes = false;
+      authorizationToken.quoting = AuthorizationValidator.AuthorizationQuoting.UNQUOTED;
       return authorizationToken;
     } else {
       error("Expected a '(' character or an authorization token instead saw '" + peek() + "'");
diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java
index 5d43914..8181959 100644
--- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java
+++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java
@@ -69,7 +69,7 @@ public void testAuthorizationValidation() {
 
     // create an instance of accumulo access that expects all auths to start with a lower case
     // letter followed by one or more lower case letters or digits.
-    var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> {
+    var accumuloAccess = AccumuloAccess.builder().authorizationValidator((auth, quoting) -> {
       if (auth.length() < 2) {
         return false;
       }
@@ -142,7 +142,7 @@ public void testAuthorizationCharacters() {
   public void testUnescaped() {
     // This test ensures that auth passed to the authorization validator are unescaped, even if they
     // are escaped in the expression
-    var accumuloAccess = AccumuloAccess.builder().authorizationValidator(auth -> {
+    var accumuloAccess = AccumuloAccess.builder().authorizationValidator((auth, quoting) -> {
       for (int i = 0; i < auth.length(); i++) {
         assertNotEquals('\\', auth.charAt(i));
       }
@@ -277,6 +277,12 @@ private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3
         var exception2 =
             assertThrows(InvalidAccessExpressionException.class, () -> evaluator.canAccess(exp2));
         assertTrue(exception2.getMessage().contains(badAuth));
+      } else {
+        // The bad auth does not need quoting, but should still see an invalid auth exception
+        var exp2 = a1 + "|(" + a2 + "&" + a3 + ")|" + a4;
+        var exception2 =
+            assertThrows(InvalidAuthorizationException.class, () -> evaluator.canAccess(exp2));
+        assertTrue(exception2.getMessage().contains(badAuth));
       }
     }
 

From 9b7d82718f309ad8b899b7f3eaa30952414de36d Mon Sep 17 00:00:00 2001
From: Keith Turner 
Date: Tue, 23 Dec 2025 22:00:56 +0000
Subject: [PATCH 17/33] fix comment

---
 .../java/org/apache/accumulo/access/AuthorizationValidator.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
index 94b4d64..850e9a7 100644
--- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
+++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
@@ -88,7 +88,7 @@ enum AuthorizationQuoting {
    */
   AuthorizationValidator DEFAULT = (auth, quoting) -> {
     if (quoting == AuthorizationQuoting.UNQUOTED) {
-      // If a string in a valid access expression is unquoted and then its already known to only
+      // If an authorization in a valid access expression is unquoted then its already known to only
       // contain a small set of ASCII chars and no further validation is needed.
       return true;
     }

From dd2ef2dfb0124b96960df3de82ee6ae94c7fb07d Mon Sep 17 00:00:00 2001
From: Keith Turner 
Date: Tue, 23 Dec 2025 22:54:26 +0000
Subject: [PATCH 18/33] fix comment

---
 .../org/apache/accumulo/access/AuthorizationValidator.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
index 850e9a7..53b1ac9 100644
--- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
+++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
@@ -88,8 +88,8 @@ enum AuthorizationQuoting {
    */
   AuthorizationValidator DEFAULT = (auth, quoting) -> {
     if (quoting == AuthorizationQuoting.UNQUOTED) {
-      // If an authorization in a valid access expression is unquoted then its already known to only
-      // contain a small set of ASCII chars and no further validation is needed.
+      // If an authorization in a valid access expression and is unquoted then its already known to
+      // only contain a small set of ASCII chars and no further validation is needed.
       return true;
     }
 

From ba917f38cb79023dcef7ae826b418c1b42f6defb Mon Sep 17 00:00:00 2001
From: Keith Turner 
Date: Tue, 6 Jan 2026 18:02:27 +0000
Subject: [PATCH 19/33] doc update

---
 .../apache/accumulo/access/AuthorizationValidator.java   | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
index 53b1ac9..1b815b6 100644
--- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
+++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java
@@ -38,10 +38,15 @@
  * reference needs to be kept for some side effect, then call {@code toString()} to allocate a copy.
  * Avoiding calls to {@code toString()} will result in faster parsing.
  * 

+ * + * @since 1.0.0 */ public interface AuthorizationValidator extends BiPredicate { + /** + * @since 1.0.0 + */ enum AuthorizationQuoting { /** * Denotes that an authorization seen in a valid access expression was quoted. This may mean the @@ -88,8 +93,8 @@ enum AuthorizationQuoting { */ AuthorizationValidator DEFAULT = (auth, quoting) -> { if (quoting == AuthorizationQuoting.UNQUOTED) { - // If an authorization in a valid access expression and is unquoted then its already known to - // only contain a small set of ASCII chars and no further validation is needed. + // If an authorization is in a valid access expression and is unquoted then its already known + // to only contain a small set of ASCII chars and no further validation is needed. return true; } From 22af52dae335f8ba9d9b94cb7273252df6556cd3 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 6 Jan 2026 21:51:07 +0000 Subject: [PATCH 20/33] optimize by avoiding String.charAt() --- .../access/impl/AccessEvaluatorImpl.java | 4 +- .../access/impl/AccessExpressionImpl.java | 4 +- .../access/impl/AccumuloAccessImpl.java | 2 +- .../accumulo/access/impl/CharsWrapper.java | 39 ++++++-------- .../impl/ParsedAccessExpressionImpl.java | 53 ++++++++----------- .../accumulo/access/impl/ParserEvaluator.java | 40 +++++++++----- .../accumulo/access/impl/Tokenizer.java | 50 ++++++++--------- .../tests/AccessExpressionBenchmark.java | 6 +-- src/build/ci/spotbugs-exclude.xml | 5 ++ 9 files changed, 101 insertions(+), 102 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java index ee97549..01b9cac 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java @@ -61,14 +61,14 @@ public AccessEvaluatorImpl(Authorizations authorizations, throw new IllegalArgumentException("Empty authorization"); } - wrappedAuths.add(new CharsWrapper(authorization)); + wrappedAuths.add(new CharsWrapper(authorization.toCharArray())); } this.authorizedPredicate = auth -> { if (auth instanceof CharsWrapper) { return wrappedAuths.contains(auth); } else { - return wrappedAuths.contains(new CharsWrapper(auth)); + return wrappedAuths.contains(new CharsWrapper(auth.toString().toCharArray())); } }; this.authorizationValidator = authorizationValidator; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java index 603a528..7a944b2 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java @@ -76,13 +76,13 @@ public static CharSequence quote(CharSequence term) { return AccessEvaluatorImpl.escape(term, true); } - public static String unquote(String term) { + public static CharSequence unquote(CharSequence term) { if (term.equals("\"\"") || term.isEmpty()) { throw new IllegalArgumentException("Empty strings are not legal authorizations."); } if (term.charAt(0) == '"' && term.charAt(term.length() - 1) == '"') { - term = term.substring(1, term.length() - 1); + term = term.subSequence(1, term.length() - 1); return AccessEvaluatorImpl.unescape(term).toString(); } else { return term; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index 3fd2caf..60752cf 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -94,7 +94,7 @@ public String quote(String authorization) { public String unquote(String authorization) { var unquoted = AccessExpressionImpl.unquote(authorization); validateAuthorization(unquoted, QUOTED); - return unquoted; + return unquoted.toString(); } @Override diff --git a/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java b/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java index 9f2eafa..4085aff 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/CharsWrapper.java @@ -18,19 +18,21 @@ */ package org.apache.accumulo.access.impl; +import java.util.Arrays; +import java.util.Objects; + public final class CharsWrapper implements CharSequence { - private CharSequence wrapped; + private char[] wrapped; private int offset; private int len; - CharsWrapper(CharSequence wrapped) { + CharsWrapper(char[] wrapped) { this.wrapped = wrapped; this.offset = 0; - this.len = wrapped.length(); + this.len = this.wrapped.length; } - CharsWrapper(CharSequence wrapped, int offset, int len) { - // TODO bounds check + private CharsWrapper(char[] wrapped, int offset, int len) { this.wrapped = wrapped; this.offset = offset; this.len = len; @@ -43,12 +45,13 @@ public int length() { @Override public char charAt(int index) { - return wrapped.charAt(offset + index); + Objects.checkIndex(index, len); + return wrapped[offset + index]; } @Override public CharSequence subSequence(int start, int end) { - // TODO bounds check + Objects.checkFromToIndex(start, end, len); return new CharsWrapper(wrapped, start + offset, end - start); } @@ -58,7 +61,7 @@ public int hashCode() { int end = offset + length(); for (int i = offset; i < end; i++) { - hash = (31 * hash) + wrapped.charAt(i); + hash = (31 * hash) + wrapped[i]; } return hash; @@ -77,14 +80,8 @@ public boolean equals(Object o) { return false; } - int end = offset + len; - for (int i1 = offset, i2 = obs.offset; i1 < end; i1++, i2++) { - if (wrapped.charAt(i1) != obs.wrapped.charAt(i2)) { - return false; - } - } - - return true; + return Arrays.equals(wrapped, offset, offset + len, obs.wrapped, obs.offset, + obs.offset + obs.len); } return false; @@ -92,16 +89,10 @@ public boolean equals(Object o) { @Override public String toString() { - char[] chars = new char[len]; - int end = offset + length(); - for (int i = offset; i < end; i++) { - chars[i - offset] = wrapped.charAt(i); - } - return new String(chars); + return new String(wrapped, offset, len); } - public void set(CharSequence data, int start, int len) { - // TODO bounds check + public void set(char[] data, int start, int len) { this.wrapped = data; this.offset = start; this.len = len; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java index b29148d..2a56d10 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.InvalidAuthorizationException; @@ -44,8 +43,6 @@ public final class ParsedAccessExpressionImpl extends ParsedAccessExpression { private final ExpressionType type; private final List children; - private final AtomicReference stringExpression = new AtomicReference<>(null); - public static final ParsedAccessExpression EMPTY = new ParsedAccessExpressionImpl(); private ParsedAccessExpressionImpl(char operator, String expression, int offset, int length, @@ -68,31 +65,25 @@ private ParsedAccessExpressionImpl(char operator, String expression, int offset, this.children = List.copyOf(children); } - private ParsedAccessExpressionImpl(CharSequence expression, int offset, int length) { + private ParsedAccessExpressionImpl(String expression) { this.type = AUTHORIZATION; - this.expression = expression.toString(); - this.offset = offset; - this.length = length; + this.expression = expression; + this.offset = 0; + this.length = expression.length(); this.children = List.of(); } ParsedAccessExpressionImpl() { this.type = ExpressionType.EMPTY; + this.expression = ""; this.offset = 0; this.length = 0; - this.expression = ""; this.children = List.of(); } @Override public String getExpression() { - String strExp = stringExpression.get(); - if (strExp != null) { - return strExp; - } - strExp = expression.substring(offset, length + offset); - stringExpression.compareAndSet(null, strExp); - return stringExpression.get(); + return expression.substring(offset, length + offset); } @Override @@ -116,8 +107,9 @@ public static ParsedAccessExpression parseExpression(String expression, return ParsedAccessExpressionImpl.EMPTY; } - Tokenizer tokenizer = new Tokenizer(expression); - var parsed = ParsedAccessExpressionImpl.parseExpression(tokenizer, authorizationValidator); + Tokenizer tokenizer = ParserEvaluator.getPerThreadTokenizer(expression); + var parsed = + ParsedAccessExpressionImpl.parseExpression(tokenizer, expression, authorizationValidator); if (tokenizer.hasNext()) { // not all input was read, so not a valid expression @@ -128,11 +120,11 @@ public static ParsedAccessExpression parseExpression(String expression, } private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, - AuthorizationValidator authorizationValidator) { + String wholeExpression, AuthorizationValidator authorizationValidator) { int beginOffset = tokenizer.curentOffset(); ParsedAccessExpressionImpl node = - parseParenExpressionOrAuthorization(tokenizer, authorizationValidator); + parseParenExpressionOrAuthorization(tokenizer, wholeExpression, authorizationValidator); if (tokenizer.hasNext()) { var operator = tokenizer.peek(); @@ -141,8 +133,8 @@ private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, nodes.add(node); do { tokenizer.advance(); - ParsedAccessExpression next = - parseParenExpressionOrAuthorization(tokenizer, authorizationValidator); + ParsedAccessExpression next = parseParenExpressionOrAuthorization(tokenizer, + wholeExpression, authorizationValidator); nodes.add(next); } while (tokenizer.hasNext() && tokenizer.peek() == operator); @@ -153,8 +145,8 @@ private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, int endOffset = tokenizer.curentOffset(); - node = new ParsedAccessExpressionImpl(operator, tokenizer.expression().toString(), - beginOffset, endOffset - beginOffset, nodes); + node = new ParsedAccessExpressionImpl(operator, wholeExpression, beginOffset, + endOffset - beginOffset, nodes); } } @@ -162,7 +154,7 @@ private static ParsedAccessExpressionImpl parseExpression(Tokenizer tokenizer, } private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(Tokenizer tokenizer, - AuthorizationValidator authorizationValidator) { + String wholeExpression, AuthorizationValidator authorizationValidator) { if (!tokenizer.hasNext()) { tokenizer .error("Expected a '(' character or an authorization token instead saw end of input"); @@ -170,27 +162,26 @@ private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(To if (tokenizer.peek() == ParserEvaluator.OPEN_PAREN) { tokenizer.advance(); - var node = parseExpression(tokenizer, authorizationValidator); + var node = parseExpression(tokenizer, wholeExpression, authorizationValidator); tokenizer.next(ParserEvaluator.CLOSE_PAREN); return node; } else { var auth = tokenizer.nextAuthorization(true); CharSequence unquotedAuth; AuthorizationValidator.AuthorizationQuoting quoting; - if (ByteUtils.isQuoteSymbol(auth.data.charAt(auth.start))) { - unquotedAuth = AccessExpressionImpl - .unquote(auth.data.subSequence(auth.start, auth.start + auth.len).toString()); + var wrapper = ParserEvaluator.lookupWrappers.get(); + wrapper.set(auth.data, auth.start, auth.len); + if (ByteUtils.isQuoteSymbol(wrapper.charAt(0))) { + unquotedAuth = AccessExpressionImpl.unquote(wrapper); quoting = AuthorizationValidator.AuthorizationQuoting.QUOTED; } else { - var wrapper = ParserEvaluator.lookupWrappers.get(); - wrapper.set(auth.data, auth.start, auth.len); unquotedAuth = wrapper; quoting = AuthorizationValidator.AuthorizationQuoting.UNQUOTED; } if (!authorizationValidator.test(unquotedAuth, quoting)) { throw new InvalidAuthorizationException(unquotedAuth.toString()); } - return new ParsedAccessExpressionImpl(auth.data, auth.start, auth.len); + return new ParsedAccessExpressionImpl(new String(auth.data, auth.start, auth.len)); } } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java index efc8371..9a18247 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java @@ -32,13 +32,31 @@ */ public final class ParserEvaluator { - static final byte OPEN_PAREN = (byte) '('; - static final byte CLOSE_PAREN = (byte) ')'; + static final char OPEN_PAREN = '('; + static final char CLOSE_PAREN = ')'; static final ThreadLocal lookupWrappers = - ThreadLocal.withInitial(() -> new CharsWrapper("", 0, 0)); - private static final ThreadLocal tokenizers = - ThreadLocal.withInitial(() -> new Tokenizer("")); + ThreadLocal.withInitial(() -> new CharsWrapper(new char[0])); + static final ThreadLocal tokenizers = + ThreadLocal.withInitial(() -> new Tokenizer(new char[0])); + static final ThreadLocal expressionArrays = ThreadLocal.withInitial(() -> new char[128]); + + static Tokenizer getPerThreadTokenizer(String expression) { + var tokenizer = tokenizers.get(); + var array = expressionArrays.get(); + if (array.length < expression.length()) { + int newLen = array.length; + while (newLen < expression.length()) { + newLen = Math.multiplyExact(newLen, 2); + } + array = new char[newLen]; + expressionArrays.set(array); + } + expression.getChars(0, expression.length(), array, 0); + tokenizer.reset(array, expression.length()); + + return tokenizer; + } public static void validate(String expression, AuthorizationValidator authValidator) throws InvalidAccessExpressionException { @@ -58,9 +76,8 @@ public static void validate(String expression, AuthorizationValidator authValida ParserEvaluator.parseAccessExpression(expression, vp, vp); } - public static void findAuthorizations(CharSequence expression, - Consumer authorizationConsumer, AuthorizationValidator authValidator) - throws InvalidAccessExpressionException { + public static void findAuthorizations(String expression, Consumer authorizationConsumer, + AuthorizationValidator authValidator) throws InvalidAccessExpressionException { var charsWrapper = ParserEvaluator.lookupWrappers.get(); Predicate atp = authToken -> { var authorizations = unescape(authToken, charsWrapper); @@ -81,11 +98,10 @@ static CharSequence unescape(Tokenizer.AuthorizationToken token, CharsWrapper wr return wrapper; } - public static boolean parseAccessExpression(CharSequence expression, + public static boolean parseAccessExpression(String expression, Predicate authorizedPredicate, Predicate shortCircuitPredicate) { - var tokenizer = tokenizers.get(); - tokenizer.reset(expression); + var tokenizer = getPerThreadTokenizer(expression); return parseAccessExpression(tokenizer, authorizedPredicate, shortCircuitPredicate); } @@ -101,7 +117,7 @@ private static boolean parseAccessExpression(Tokenizer tokenizer, if (tokenizer.hasNext()) { // not all input was read, so not a valid expression - tokenizer.error("Unexpected character '" + (char) tokenizer.peek() + "'"); + tokenizer.error("Unexpected character '" + tokenizer.peek() + "'"); } return node; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java index baf1164..2ad6de2 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java @@ -51,13 +51,14 @@ public static boolean isValidAuthChar(char b) { return validAuthChars[0xff & b] && b < 256; } - private CharSequence expression; + private char[] expression; + private int len; private int index; private final AuthorizationToken authorizationToken = new AuthorizationToken(); public static class AuthorizationToken { - public CharSequence data; + public char[] data; public int start; public int len; public boolean hasEscapes; @@ -65,33 +66,32 @@ public static class AuthorizationToken { } - Tokenizer(CharSequence expression) { - this.expression = expression; - authorizationToken.data = expression; + Tokenizer(char[] expression) { + reset(expression, expression.length); } - void reset(CharSequence expression) { + void reset(char[] expression, int len) { this.expression = expression; - authorizationToken.data = expression; this.index = 0; + this.len = len; + this.authorizationToken.data = expression; } boolean hasNext() { - return index < expression.length(); + return index < len; } public void advance() { index++; } - public void next(byte expected) { + public void next(char expected) { if (!hasNext()) { - error("Expected '" + (char) expected + "' instead saw end of input"); + error("Expected '" + expected + "' instead saw end of input"); } - if (expression.charAt(index) != expected) { - error("Expected '" + (char) expected + "' instead saw '" + (char) (expression.charAt(index)) - + "'"); + if (expression[index] != expected) { + error("Expected '" + expected + "' instead saw '" + (expression[index]) + "'"); } index++; } @@ -101,15 +101,11 @@ public void error(String msg) { } public void error(String msg, int idx) { - throw new InvalidAccessExpressionException(msg, expression.toString(), idx); + throw new InvalidAccessExpressionException(msg, new String(expression, 0, len), idx); } char peek() { - return expression.charAt(index); - } - - CharSequence expression() { - return expression; + return expression[index]; } public int curentOffset() { @@ -117,14 +113,14 @@ public int curentOffset() { } AuthorizationToken nextAuthorization(boolean includeQuotes) { - if (isQuoteSymbol(expression.charAt(index))) { + if (isQuoteSymbol(expression[index])) { int start = ++index; boolean hasEscapes = false; - while (index < expression.length() && !isQuoteSymbol(expression.charAt(index))) { - if (isBackslashSymbol(expression.charAt(index))) { + while (index < len && !isQuoteSymbol(expression[index])) { + if (isBackslashSymbol(expression[index])) { index++; - if (index == expression.length() || !isQuoteOrSlash(expression.charAt(index))) { + if (index == len || !isQuoteOrSlash(expression[index])) { error("Invalid escaping within quotes", index - 1); } hasEscapes = true; @@ -132,7 +128,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { index++; } - if (index == expression.length()) { + if (index == len) { error("Unclosed quote", start - 1); } @@ -154,11 +150,11 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { return authorizationToken; - } else if (isValidAuthChar(expression.charAt(index))) { + } else if (isValidAuthChar(expression[index])) { int start = index; - while (index < expression.length() && isValidAuthChar(expression.charAt(index))) { + do { index++; - } + } while (index < len && isValidAuthChar(expression[index])); authorizationToken.start = start; authorizationToken.len = index - start; authorizationToken.hasEscapes = false; diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java index 28fd234..26e7147 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java @@ -168,7 +168,7 @@ List getVisibilityEvaluatorTests() { /** * Measures the time it takes to parse an expression stored in byte[] and produce a parse tree. */ - @Benchmark + // @Benchmark public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { var accumuloAccess = state.accumuloAccess; for (byte[] accessExpression : state.getBytesExpressions()) { @@ -205,7 +205,7 @@ public void measureParseAndEvaluation(BenchmarkState state, Blackhole blackhole) * * @throws VisibilityParseException error parsing expression with legacy code */ - @Benchmark + // @Benchmark public void measureLegacyEvaluationOnly(BenchmarkState state, Blackhole blackhole) throws VisibilityParseException { for (VisibilityEvaluatorTests evaluatorTests : state.getVisibilityEvaluatorTests()) { @@ -223,7 +223,7 @@ public void measureLegacyEvaluationOnly(BenchmarkState state, Blackhole blackhol * * @throws VisibilityParseException error parsing expression with legacy code */ - @Benchmark + // @Benchmark public void measureLegacyParseAndEvaluation(BenchmarkState state, Blackhole blackhole) throws VisibilityParseException { for (VisibilityEvaluatorTests evaluatorTests : state.getVisibilityEvaluatorTests()) { diff --git a/src/build/ci/spotbugs-exclude.xml b/src/build/ci/spotbugs-exclude.xml index c7e7650..797c675 100644 --- a/src/build/ci/spotbugs-exclude.xml +++ b/src/build/ci/spotbugs-exclude.xml @@ -23,4 +23,9 @@ + + + + + From 12770d5bf46d7933d593b7e1eea0990326595b50 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Tue, 6 Jan 2026 22:13:30 +0000 Subject: [PATCH 21/33] revert benchmark change --- .../accumulo/access/tests/AccessExpressionBenchmark.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java index 26e7147..28fd234 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java @@ -168,7 +168,7 @@ List getVisibilityEvaluatorTests() { /** * Measures the time it takes to parse an expression stored in byte[] and produce a parse tree. */ - // @Benchmark + @Benchmark public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { var accumuloAccess = state.accumuloAccess; for (byte[] accessExpression : state.getBytesExpressions()) { @@ -205,7 +205,7 @@ public void measureParseAndEvaluation(BenchmarkState state, Blackhole blackhole) * * @throws VisibilityParseException error parsing expression with legacy code */ - // @Benchmark + @Benchmark public void measureLegacyEvaluationOnly(BenchmarkState state, Blackhole blackhole) throws VisibilityParseException { for (VisibilityEvaluatorTests evaluatorTests : state.getVisibilityEvaluatorTests()) { @@ -223,7 +223,7 @@ public void measureLegacyEvaluationOnly(BenchmarkState state, Blackhole blackhol * * @throws VisibilityParseException error parsing expression with legacy code */ - // @Benchmark + @Benchmark public void measureLegacyParseAndEvaluation(BenchmarkState state, Blackhole blackhole) throws VisibilityParseException { for (VisibilityEvaluatorTests evaluatorTests : state.getVisibilityEvaluatorTests()) { From 778c9e483adb83c1e3eefa3cb824456f0b1dd2f6 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Thu, 8 Jan 2026 23:06:39 +0000 Subject: [PATCH 22/33] rename AccumuloAccess to Access --- .../AccessExpressionAntlrEvaluator.java | 11 +-- .../antlr/AccessExpressionAntlrBenchmark.java | 4 +- .../access/grammar/antlr/Antlr4Tests.java | 17 +++-- .../{AccumuloAccess.java => Access.java} | 11 ++- .../accumulo/access/AccessExpression.java | 6 +- .../access/AuthorizationValidator.java | 6 +- .../access/ParsedAccessExpression.java | 6 +- .../access/impl/AccumuloAccessImpl.java | 4 +- .../accumulo/access/impl/BuilderImpl.java | 8 +-- .../access/tests/AccessEvaluatorTest.java | 67 +++++++++---------- .../tests/AccessExpressionBenchmark.java | 22 +++--- .../access/tests/AccessExpressionTest.java | 53 +++++++-------- .../access/tests/AuthorizationTest.java | 56 ++++++++-------- .../tests/ParsedAccessExpressionTest.java | 16 ++--- .../access/examples/AccessExample.java | 7 +- .../access/examples/ParseExamples.java | 17 +++-- .../examples/test/ParseExamplesTest.java | 9 ++- 17 files changed, 154 insertions(+), 166 deletions(-) rename core/src/main/java/org/apache/accumulo/access/{AccumuloAccess.java => Access.java} (95%) diff --git a/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java b/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java index 4097f89..d9bd39e 100644 --- a/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java +++ b/antlr4-example/src/main/java/org/apache/accumulo/access/antlr4/AccessExpressionAntlrEvaluator.java @@ -18,7 +18,6 @@ */ package org.apache.accumulo.access.antlr4; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -26,10 +25,8 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; -import org.apache.accumulo.access.AccessExpression; -import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.Authorizations; -import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.grammars.AccessExpressionParser.Access_expressionContext; import org.apache.accumulo.access.grammars.AccessExpressionParser.Access_tokenContext; import org.apache.accumulo.access.grammars.AccessExpressionParser.And_expressionContext; @@ -37,11 +34,9 @@ import org.apache.accumulo.access.grammars.AccessExpressionParser.Or_expressionContext; import org.apache.accumulo.access.grammars.AccessExpressionParser.Or_operatorContext; -import static java.nio.charset.StandardCharsets.UTF_8; - public class AccessExpressionAntlrEvaluator { - public static final AccumuloAccess ACCUMULO_ACCESS = AccumuloAccess.builder().build(); + public static final Access ACCESS = Access.builder().build(); private class Entity { @@ -66,7 +61,7 @@ public AccessExpressionAntlrEvaluator(List authSets) { e.authorizations = new HashSet<>(entityAuths.size() * 2); a.asSet().stream().forEach(auth -> { e.authorizations.add(auth); - String quoted = ACCUMULO_ACCESS.quote(auth); + String quoted = ACCESS.quote(auth); if (!quoted.startsWith("\"")) { quoted = '"' + quoted + '"'; } diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java index 1beefb7..acf4905 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/AccessExpressionAntlrBenchmark.java @@ -19,7 +19,7 @@ package org.apache.accumulo.access.grammar.antlr; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.accumulo.access.grammar.antlr.Antlr4Tests.ACCUMULO_ACCESS; +import static org.apache.accumulo.access.grammar.antlr.Antlr4Tests.ACCESS; import java.io.IOException; import java.net.URISyntaxException; @@ -89,7 +89,7 @@ public void loadData() throws IOException, URISyntaxException { et.expressions = new ArrayList<>(); et.evaluator = new AccessExpressionAntlrEvaluator(Stream.of(testDataSet.auths) - .map(a -> ACCUMULO_ACCESS.newAuthorizations(Set.of(a))).collect(Collectors.toList())); + .map(a -> ACCESS.newAuthorizations(Set.of(a))).collect(Collectors.toList())); for (var tests : testDataSet.tests) { if (tests.expectedResult != TestDataLoader.ExpectedResult.ERROR) { diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java index 467bf55..269ca54 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java @@ -39,8 +39,7 @@ import org.antlr.v4.runtime.RecognitionException; import org.antlr.v4.runtime.Recognizer; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.AccessExpression; -import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.antlr.TestDataLoader; @@ -55,7 +54,7 @@ public class Antlr4Tests { - public static final AccumuloAccess ACCUMULO_ACCESS = AccumuloAccess.builder().build(); + public static final Access ACCESS = Access.builder().build(); private void testParse(String input) throws Exception { CodePointCharStream expression = CharStreams.fromString(input); @@ -110,10 +109,10 @@ public void testCompareWithAccessExpressionImplParsing() throws Exception { ExpectedResult result = test.expectedResult; for (String cv : test.expressions) { if (result == ExpectedResult.ERROR) { - assertThrows(InvalidAccessExpressionException.class, () -> ACCUMULO_ACCESS.newExpression(cv)); + assertThrows(InvalidAccessExpressionException.class, () -> ACCESS.newExpression(cv)); assertThrows(AssertionError.class, () -> testParse(cv)); } else { - ACCUMULO_ACCESS.validate(cv); + ACCESS.validate(cv); testParse(cv); } } @@ -124,7 +123,7 @@ public void testCompareWithAccessExpressionImplParsing() throws Exception { @Test public void testSimpleEvaluation() throws Exception { String accessExpression = "(one&two)|(foo&bar)"; - Authorizations auths = ACCUMULO_ACCESS.newAuthorizations(Set.of("four", "three", "one", "two")); + Authorizations auths = ACCESS.newAuthorizations(Set.of("four", "three", "one", "two")); AccessExpressionAntlrEvaluator eval = new AccessExpressionAntlrEvaluator(List.of(auths)); assertTrue(eval.canAccess(accessExpression)); } @@ -132,7 +131,7 @@ public void testSimpleEvaluation() throws Exception { @Test public void testSimpleEvaluationFailure() throws Exception { String accessExpression = "(A&B&C)"; - Authorizations auths = ACCUMULO_ACCESS.newAuthorizations(Set.of("A", "C")); + Authorizations auths = ACCESS.newAuthorizations(Set.of("A", "C")); AccessExpressionAntlrEvaluator eval = new AccessExpressionAntlrEvaluator(List.of(auths)); assertFalse(eval.canAccess(accessExpression)); } @@ -145,8 +144,8 @@ public void testCompareAntlrEvaluationAgainstAccessEvaluatorImpl() throws Except for (TestDataSet testSet : testData) { List authSets = Stream.of(testSet.auths) - .map(a -> ACCUMULO_ACCESS.newAuthorizations(Set.of(a))).collect(Collectors.toList()); - AccessEvaluator evaluator = ACCUMULO_ACCESS.newEvaluator(authSets); + .map(a -> ACCESS.newAuthorizations(Set.of(a))).collect(Collectors.toList()); + AccessEvaluator evaluator = ACCESS.newEvaluator(authSets); AccessExpressionAntlrEvaluator antlr = new AccessExpressionAntlrEvaluator(authSets); for (TestExpressions test : testSet.tests) { diff --git a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java b/core/src/main/java/org/apache/accumulo/access/Access.java similarity index 95% rename from core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java rename to core/src/main/java/org/apache/accumulo/access/Access.java index a77b9e2..ff4d0c6 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccumuloAccess.java +++ b/core/src/main/java/org/apache/accumulo/access/Access.java @@ -31,7 +31,7 @@ * @see #builder() * @since 1.0 */ -public interface AccumuloAccess { +public interface Access { interface Builder { /** @@ -45,7 +45,7 @@ interface Builder { */ Builder authorizationValidator(AuthorizationValidator validator); - AccumuloAccess build(); + Access build(); } /** @@ -112,10 +112,9 @@ ParsedAccessExpression newParsedExpression(String expression) * *

* What this method does could also be accomplished by creating a parse tree using - * {@link AccumuloAccess#newParsedExpression(String)} and then recursively walking the parse tree. - * The implementation of this method does not create a parse tree and is much faster. If a parse - * tree is already available, then it would likely be faster to use it rather than call this - * method. + * {@link Access#newParsedExpression(String)} and then recursively walking the parse tree. The + * implementation of this method does not create a parse tree and is much faster. If a parse tree + * is already available, then it would likely be faster to use it rather than call this method. *

* * @throws InvalidAccessExpressionException when the expression is not valid. diff --git a/core/src/main/java/org/apache/accumulo/access/AccessExpression.java b/core/src/main/java/org/apache/accumulo/access/AccessExpression.java index e95142c..dfc7ab4 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccessExpression.java +++ b/core/src/main/java/org/apache/accumulo/access/AccessExpression.java @@ -106,10 +106,10 @@ protected AccessExpression() {} /** * Parses the access expression if it was never parsed before. If this access expression was - * created using {@link AccumuloAccess#newParsedExpression(String)} then it will have a parse from + * created using {@link Access#newParsedExpression(String)} then it will have a parse from * inception and this method will return itself. If the access expression was created using - * {@link AccumuloAccess#newExpression(String)} then this method will create a parse tree the - * first time its called and remember it, returning the remembered parse tree on subsequent calls. + * {@link Access#newExpression(String)} then this method will create a parse tree the first time + * its called and remember it, returning the remembered parse tree on subsequent calls. */ public abstract ParsedAccessExpression parse(); diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java index 1b815b6..0e18a7c 100644 --- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java +++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -27,9 +27,9 @@ * *

* When an authorization is quoted and/or escaped in access expression that is undone before is - * passed to this predicate. Conceptually it is like {@link AccumuloAccess#unquote(String)} is - * called prior to being passed to this predicate. If the authorization was quoted that information - * is passed along is it may be useful for optimizations. + * passed to this predicate. Conceptually it is like {@link Access#unquote(String)} is called prior + * to being passed to this predicate. If the authorization was quoted that information is passed + * along is it may be useful for optimizations. * *

* A CharSequence is passed to this predicate for efficiency. It allows having a view into the diff --git a/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java b/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java index 1ee8040..498df67 100644 --- a/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java +++ b/core/src/main/java/org/apache/accumulo/access/ParsedAccessExpression.java @@ -25,8 +25,8 @@ /** * Instances of this class are immutable and wrap a verified access expression and a parse tree for * the access expression. To create an instance of this class call - * {@link AccumuloAccess#newParsedExpression(String)}. The Accumulo Access project has examples that - * show how to use the parse tree. + * {@link Access#newParsedExpression(String)}. The Accumulo Access project has examples that show + * how to use the parse tree. * * @since 1.0.0 */ @@ -52,7 +52,7 @@ public enum ExpressionType { /** * Indicates an access expression is a single authorization. For this type * {@link #getExpression()} will return the authorization in quoted and escaped form. Depending - * on the use case {@link AccumuloAccess#unquote(String)} may need to be called. + * on the use case {@link Access#unquote(String)} may need to be called. */ AUTHORIZATION, /** diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index 60752cf..87317c8 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -24,16 +24,16 @@ import java.util.Set; import java.util.function.Consumer; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessEvaluator; import org.apache.accumulo.access.AccessExpression; -import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.InvalidAuthorizationException; import org.apache.accumulo.access.ParsedAccessExpression; -public class AccumuloAccessImpl implements AccumuloAccess { +public class AccumuloAccessImpl implements Access { private final AuthorizationValidator authValidator; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java index 2d3f41a..7ac6d4f 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -20,21 +20,21 @@ import java.util.Objects; -import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AuthorizationValidator; -public class BuilderImpl implements AccumuloAccess.Builder { +public class BuilderImpl implements Access.Builder { private AuthorizationValidator validator; @Override - public AccumuloAccess.Builder authorizationValidator(AuthorizationValidator validator) { + public Access.Builder authorizationValidator(AuthorizationValidator validator) { this.validator = Objects.requireNonNull(validator); return this; } @Override - public AccumuloAccess build() { + public Access build() { return new AccumuloAccessImpl(validator == null ? AuthorizationValidator.DEFAULT : validator); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index 9e0a99a..c46e165 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -32,8 +32,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.impl.AccessEvaluatorImpl; import org.junit.jupiter.api.Test; @@ -81,30 +81,29 @@ public void runTestCases() throws IOException { assertFalse(testData.isEmpty()); - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); for (var testSet : testData) { System.out.println("runTestCases for " + testSet.description); AccessEvaluator evaluator; assertTrue(testSet.auths.length >= 1); if (testSet.auths.length == 1) { - evaluator = - accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of(testSet.auths[0]))); - runTestCases(accumuloAccess, testSet, evaluator); + evaluator = access.newEvaluator(access.newAuthorizations(Set.of(testSet.auths[0]))); + runTestCases(access, testSet, evaluator); Set auths = Stream.of(testSet.auths[0]).collect(Collectors.toSet()); - evaluator = accumuloAccess.newEvaluator(auths::contains); - runTestCases(accumuloAccess, testSet, evaluator); + evaluator = access.newEvaluator(auths::contains); + runTestCases(access, testSet, evaluator); } else { - var authSets = Stream.of(testSet.auths) - .map(a -> accumuloAccess.newAuthorizations(Set.of(a))).collect(Collectors.toList()); - evaluator = accumuloAccess.newEvaluator(authSets); - runTestCases(accumuloAccess, testSet, evaluator); + var authSets = Stream.of(testSet.auths).map(a -> access.newAuthorizations(Set.of(a))) + .collect(Collectors.toList()); + evaluator = access.newEvaluator(authSets); + runTestCases(access, testSet, evaluator); } } } - private static void runTestCases(AccumuloAccess accumuloAccess, TestDataSet testSet, + private static void runTestCases(Access accumuloAccess, TestDataSet testSet, AccessEvaluator evaluator) { assertFalse(testSet.tests.isEmpty()); @@ -165,48 +164,48 @@ private static void runTestCases(AccumuloAccess accumuloAccess, TestDataSet test @Test public void testEmptyAuthorizations() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); // TODO what part of the code throwing the exception? assertThrows(IllegalArgumentException.class, - () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("")))); + () -> access.newEvaluator(access.newAuthorizations(Set.of("")))); assertThrows(IllegalArgumentException.class, - () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("", "A")))); + () -> access.newEvaluator(access.newAuthorizations(Set.of("", "A")))); assertThrows(IllegalArgumentException.class, - () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("A", "")))); + () -> access.newEvaluator(access.newAuthorizations(Set.of("A", "")))); assertThrows(IllegalArgumentException.class, - () -> accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of("")))); + () -> access.newEvaluator(access.newAuthorizations(Set.of("")))); } @Test public void testSpecialChars() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); // special chars do not need quoting for (String qt : List.of("A_", "_", "A_C", "_C")) { - assertEquals(qt, accumuloAccess.quote(qt)); + assertEquals(qt, access.quote(qt)); for (char c : new char[] {'/', ':', '-', '.'}) { String qt2 = qt.replace('_', c); - assertEquals(qt2, accumuloAccess.quote(qt2)); + assertEquals(qt2, access.quote(qt2)); } } - assertEquals("a_b:c/d.e", accumuloAccess.quote("a_b:c/d.e")); + assertEquals("a_b:c/d.e", access.quote("a_b:c/d.e")); } @Test public void testQuote() { - var accumuloAccess = AccumuloAccess.builder().build(); - assertEquals("\"A#C\"", accumuloAccess.quote("A#C")); - assertEquals("A#C", accumuloAccess.unquote(accumuloAccess.quote("A#C"))); - assertEquals("\"A\\\"C\"", accumuloAccess.quote("A\"C")); - assertEquals("A\"C", accumuloAccess.unquote(accumuloAccess.quote("A\"C"))); - assertEquals("\"A\\\"\\\\C\"", accumuloAccess.quote("A\"\\C")); - assertEquals("A\"\\C", accumuloAccess.unquote(accumuloAccess.quote("A\"\\C"))); - assertEquals("ACS", accumuloAccess.quote("ACS")); - assertEquals("ACS", accumuloAccess.unquote(accumuloAccess.quote("ACS"))); - assertEquals("\"九\"", accumuloAccess.quote("九")); - assertEquals("九", accumuloAccess.unquote(accumuloAccess.quote("九"))); - assertEquals("\"五十\"", accumuloAccess.quote("五十")); - assertEquals("五十", accumuloAccess.unquote(accumuloAccess.quote("五十"))); + var access = Access.builder().build(); + assertEquals("\"A#C\"", access.quote("A#C")); + assertEquals("A#C", access.unquote(access.quote("A#C"))); + assertEquals("\"A\\\"C\"", access.quote("A\"C")); + assertEquals("A\"C", access.unquote(access.quote("A\"C"))); + assertEquals("\"A\\\"\\\\C\"", access.quote("A\"\\C")); + assertEquals("A\"\\C", access.unquote(access.quote("A\"\\C"))); + assertEquals("ACS", access.quote("ACS")); + assertEquals("ACS", access.unquote(access.quote("ACS"))); + assertEquals("\"九\"", access.quote("九")); + assertEquals("九", access.unquote(access.quote("九"))); + assertEquals("\"五十\"", access.quote("五十")); + assertEquals("五十", access.unquote(access.quote("五十"))); } private static String unescape(String s) { diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java index 905a201..746ba8c 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java @@ -28,8 +28,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.core.security.ColumnVisibility; import org.apache.accumulo.core.security.VisibilityEvaluator; import org.apache.accumulo.core.security.VisibilityParseException; @@ -78,7 +78,7 @@ public static class EvaluatorTests { @State(Scope.Benchmark) public static class BenchmarkState { - private AccumuloAccess accumuloAccess; + private Access access; private ArrayList allTestExpressions; @@ -90,7 +90,7 @@ public static class BenchmarkState { @Setup public void loadData() throws IOException { - accumuloAccess = AccumuloAccess.builder().build(); + access = Access.builder().build(); List testData = AccessEvaluatorTest.readTestData(); allTestExpressions = new ArrayList<>(); allTestExpressionsStr = new ArrayList<>(); @@ -121,12 +121,12 @@ public void loadData() throws IOException { et.expressions = new ArrayList<>(); if (testDataSet.auths.length == 1) { - et.evaluator = accumuloAccess - .newEvaluator(accumuloAccess.newAuthorizations(Set.of(testDataSet.auths[0]))); + et.evaluator = + access.newEvaluator(access.newAuthorizations(Set.of(testDataSet.auths[0]))); } else { - var authSets = Stream.of(testDataSet.auths) - .map(a -> accumuloAccess.newAuthorizations(Set.of(a))).collect(Collectors.toList()); - et.evaluator = accumuloAccess.newEvaluator(authSets); + var authSets = Stream.of(testDataSet.auths).map(a -> access.newAuthorizations(Set.of(a))) + .collect(Collectors.toList()); + et.evaluator = access.newEvaluator(authSets); } for (var tests : testDataSet.tests) { @@ -170,7 +170,7 @@ List getVisibilityEvaluatorTests() { */ @Benchmark public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { - var accumuloAccess = state.accumuloAccess; + var accumuloAccess = state.access; for (byte[] accessExpression : state.getBytesExpressions()) { accumuloAccess.validate(new String(accessExpression, UTF_8)); } @@ -181,7 +181,7 @@ public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { */ @Benchmark public void measureStringValidation(BenchmarkState state, Blackhole blackhole) { - var accumuloAccess = state.accumuloAccess; + var accumuloAccess = state.access; for (String accessExpression : state.getStringExpressions()) { accumuloAccess.validate(accessExpression); } @@ -193,7 +193,7 @@ public void measureStringValidation(BenchmarkState state, Blackhole blackhole) { */ @Benchmark public void measureCreateParseTree(BenchmarkState state, Blackhole blackhole) { - var accumuloAccess = state.accumuloAccess; + var accumuloAccess = state.access; for (String accessExpression : state.getStringExpressions()) { blackhole.consume(accumuloAccess.newParsedExpression(accessExpression)); } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java index 1d82f9b..41c46bd 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java @@ -36,8 +36,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessExpression; -import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.ParsedAccessExpression; import org.junit.jupiter.api.Disabled; @@ -48,7 +48,7 @@ public class AccessExpressionTest { @Test public void testGetAuthorizations() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); // Test data pairs where the first entry of each pair is an expression to normalize and second // is the expected authorization in the expression var testData = new ArrayList>(); @@ -68,7 +68,7 @@ public void testGetAuthorizations() { var expression = testCase.get(0); var expected = testCase.get(1); HashSet found = new HashSet<>(); - accumuloAccess.findAuthorizations(expression, found::add); + access.findAuthorizations(expression, found::add); var actual = found.stream().sorted().collect(Collectors.joining(",")); assertEquals(expected, actual); found.clear(); @@ -77,10 +77,10 @@ public void testGetAuthorizations() { } void checkError(String expression, String expected, int index) { - var accumuloAccess = AccumuloAccess.builder().build(); - checkError(() -> accumuloAccess.validate(expression), expected, index); - checkError(() -> accumuloAccess.newExpression(expression), expected, index); - checkError(() -> accumuloAccess.newParsedExpression(expression), expected, index); + var access = Access.builder().build(); + checkError(() -> access.validate(expression), expected, index); + checkError(() -> access.newExpression(expression), expected, index); + checkError(() -> access.newParsedExpression(expression), expected, index); } void checkError(Executable executable, String expected, int index) { @@ -126,11 +126,11 @@ public void testErrorMessages() { @Test public void testEqualsHashcode() { - var accumuloAccess = AccumuloAccess.builder().build(); - var ae1 = accumuloAccess.newExpression("A&B"); - var ae2 = accumuloAccess.newExpression("A&C"); - var ae3 = accumuloAccess.newExpression("A&B"); - var ae4 = accumuloAccess.newParsedExpression("A&B"); + var access = Access.builder().build(); + var ae1 = access.newExpression("A&B"); + var ae2 = access.newExpression("A&C"); + var ae3 = access.newExpression("A&B"); + var ae4 = access.newParsedExpression("A&B"); assertEquals(ae1, ae3); assertEquals(ae1, ae4); @@ -182,12 +182,12 @@ public void testSpecificationDocumentation() throws IOException, URISyntaxExcept @Test public void testEmpty() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); // do not expect empty expression to fail validation - accumuloAccess.validate(""); - assertEquals("", accumuloAccess.newExpression("").getExpression()); + access.validate(""); + assertEquals("", access.newExpression("").getExpression()); - var parsed = accumuloAccess.newParsedExpression(""); + var parsed = access.newParsedExpression(""); assertEquals("", parsed.getExpression()); assertTrue(parsed.getChildren().isEmpty()); assertEquals(ParsedAccessExpression.ExpressionType.EMPTY, parsed.getType()); @@ -195,9 +195,9 @@ public void testEmpty() { @Test public void testImmutable() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); var exp = "A&B&(C|D)"; - var exp2 = accumuloAccess.newParsedExpression(exp); + var exp2 = access.newParsedExpression(exp); assertEquals("A&B&(C|D)", exp2.getExpression()); @@ -215,14 +215,13 @@ public void testImmutable() { @Test public void testNull() { - var accumuloAccess = AccumuloAccess.builder().build(); - assertThrows(NullPointerException.class, () -> accumuloAccess.newParsedExpression(null)); - assertThrows(NullPointerException.class, () -> accumuloAccess.validate(null)); - assertThrows(NullPointerException.class, () -> accumuloAccess.newExpression(null)); - assertThrows(NullPointerException.class, - () -> accumuloAccess.findAuthorizations(null, auth -> {})); - assertThrows(NullPointerException.class, () -> accumuloAccess.findAuthorizations("A&B", null)); - assertThrows(NullPointerException.class, () -> accumuloAccess.quote(null)); - assertThrows(NullPointerException.class, () -> accumuloAccess.unquote(null)); + var access = Access.builder().build(); + assertThrows(NullPointerException.class, () -> access.newParsedExpression(null)); + assertThrows(NullPointerException.class, () -> access.validate(null)); + assertThrows(NullPointerException.class, () -> access.newExpression(null)); + assertThrows(NullPointerException.class, () -> access.findAuthorizations(null, auth -> {})); + assertThrows(NullPointerException.class, () -> access.findAuthorizations("A&B", null)); + assertThrows(NullPointerException.class, () -> access.quote(null)); + assertThrows(NullPointerException.class, () -> access.unquote(null)); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 8181959..e8b9895 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.Set; -import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.InvalidAuthorizationException; @@ -40,14 +40,14 @@ public class AuthorizationTest { @Test public void testEquality() { - var accumuloAccess = AccumuloAccess.builder().build(); - Authorizations auths1 = accumuloAccess.newAuthorizations(Set.of("A", "B", "C")); - Authorizations auths2 = accumuloAccess.newAuthorizations(Set.of("A", "B", "C")); + var access = Access.builder().build(); + Authorizations auths1 = access.newAuthorizations(Set.of("A", "B", "C")); + Authorizations auths2 = access.newAuthorizations(Set.of("A", "B", "C")); assertEquals(auths1, auths2); assertEquals(auths1.hashCode(), auths2.hashCode()); - Authorizations auths3 = accumuloAccess.newAuthorizations(Set.of("D", "E", "F")); + Authorizations auths3 = access.newAuthorizations(Set.of("D", "E", "F")); assertNotEquals(auths1, auths3); assertNotEquals(auths1.hashCode(), auths3.hashCode()); @@ -55,13 +55,13 @@ public void testEquality() { @Test public void testEmpty() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); // check if new object is allocated - assertSame(accumuloAccess.newAuthorizations(), accumuloAccess.newAuthorizations()); + assertSame(access.newAuthorizations(), access.newAuthorizations()); // check if optimization is working - assertSame(accumuloAccess.newAuthorizations(), accumuloAccess.newAuthorizations(Set.of())); - assertEquals(Set.of(), accumuloAccess.newAuthorizations().asSet()); - assertSame(Set.of(), accumuloAccess.newAuthorizations().asSet()); + assertSame(access.newAuthorizations(), access.newAuthorizations(Set.of())); + assertEquals(Set.of(), access.newAuthorizations().asSet()); + assertSame(Set.of(), access.newAuthorizations().asSet()); } @Test @@ -69,7 +69,7 @@ public void testAuthorizationValidation() { // create an instance of accumulo access that expects all auths to start with a lower case // letter followed by one or more lower case letters or digits. - var accumuloAccess = AccumuloAccess.builder().authorizationValidator((auth, quoting) -> { + var accumuloAccess = Access.builder().authorizationValidator((auth, quoting) -> { if (auth.length() < 2) { return false; } @@ -101,8 +101,8 @@ public void testNonUnicode() { assertFalse(Character.isISOControl(c)); var badAuth = new String(new char[] {'a', c}); - var accumuloAccess = AccumuloAccess.builder().build(); - runTest("ac", "a9", "dc", badAuth, accumuloAccess); + var access = Access.builder().build(); + runTest("ac", "a9", "dc", badAuth, access); } @Test @@ -111,9 +111,9 @@ public void testControlCharacters() { assertTrue(Character.isDefined(c)); assertTrue(Character.isISOControl(c)); - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); var badAuth = new String(new char[] {'a', c}); - runTest("ac", "a9", "dc", badAuth, accumuloAccess); + runTest("ac", "a9", "dc", badAuth, access); } @Test @@ -121,9 +121,9 @@ public void testReplacementCharacter() { char c = '\uFFFD'; assertEquals(c + "", UTF_8.newDecoder().replacement()); - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); var badAuth = new String(new char[] {'a', c}); - runTest("ac", "a9", "dc", badAuth, accumuloAccess); + runTest("ac", "a9", "dc", badAuth, access); } @Test @@ -142,7 +142,7 @@ public void testAuthorizationCharacters() { public void testUnescaped() { // This test ensures that auth passed to the authorization validator are unescaped, even if they // are escaped in the expression - var accumuloAccess = AccumuloAccess.builder().authorizationValidator((auth, quoting) -> { + var accumuloAccess = Access.builder().authorizationValidator((auth, quoting) -> { for (int i = 0; i < auth.length(); i++) { assertNotEquals('\\', auth.charAt(i)); } @@ -194,32 +194,32 @@ public void testMultiCharCodepoint() { var auth1 = "a" + doubleChar; var auth2 = "abc"; - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); - var exp1 = accumuloAccess.quote(auth1) + "&" + auth2; - var exp2 = accumuloAccess.quote(auth1) + "|" + auth2; + var exp1 = access.quote(auth1) + "&" + auth2; + var exp2 = access.quote(auth1) + "|" + auth2; assertEquals('"' + auth1 + '"' + "&" + auth2, exp1); assertEquals('"' + auth1 + '"' + "|" + auth2, exp2); - var auths1 = accumuloAccess.newAuthorizations(Set.of(auth1, auth2)); - var evaluator1 = accumuloAccess.newEvaluator(auths1); + var auths1 = access.newAuthorizations(Set.of(auth1, auth2)); + var evaluator1 = access.newEvaluator(auths1); assertTrue(evaluator1.canAccess(exp1)); assertTrue(evaluator1.canAccess(exp2)); - var auths2 = accumuloAccess.newAuthorizations(Set.of(auth1)); - var evaluator2 = accumuloAccess.newEvaluator(auths2); + var auths2 = access.newAuthorizations(Set.of(auth1)); + var evaluator2 = access.newEvaluator(auths2); assertFalse(evaluator2.canAccess(exp1)); assertTrue(evaluator2.canAccess(exp2)); - var auths3 = accumuloAccess.newAuthorizations(Set.of(auth2)); - var evaluator3 = accumuloAccess.newEvaluator(auths3); + var auths3 = access.newAuthorizations(Set.of(auth2)); + var evaluator3 = access.newEvaluator(auths3); assertFalse(evaluator3.canAccess(exp1)); assertTrue(evaluator3.canAccess(exp2)); } private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3, String badAuth, - AccumuloAccess accumuloAccess) { + Access accumuloAccess) { List auths = List.of(goodAuth1, goodAuth2, badAuth, goodAuth3); var auths1 = accumuloAccess.newAuthorizations(Set.of(goodAuth1, goodAuth2)); diff --git a/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java index 2acb5ab..9c2213d 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java @@ -29,17 +29,17 @@ import java.util.List; -import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.ParsedAccessExpression; import org.junit.jupiter.api.Test; public class ParsedAccessExpressionTest { @Test public void testParsing() { - var accumuloAccess = AccumuloAccess.builder().build(); + var access = Access.builder().build(); String expression = "(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|BLACK"; - for (var parsed : List.of(accumuloAccess.newParsedExpression(expression), - accumuloAccess.newExpression(expression).parse())) { + for (var parsed : List.of(access.newParsedExpression(expression), + access.newExpression(expression).parse())) { // verify root node verify("(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|BLACK", OR, 3, parsed); @@ -67,15 +67,15 @@ public void testParsing() { @Test public void testEmpty() { - var accumuloAccess = AccumuloAccess.builder().build(); - var parsed = accumuloAccess.newParsedExpression(""); + var access = Access.builder().build(); + var parsed = access.newParsedExpression(""); verify("", EMPTY, 0, parsed); } @Test public void testParseTwice() { - var accumuloAccess = AccumuloAccess.builder().build(); - for (var expression : List.of(accumuloAccess.newExpression("A&B"))) { + var access = Access.builder().build(); + for (var expression : List.of(access.newExpression("A&B"))) { var parsed = expression.parse(); assertNotSame(expression, parsed); assertEquals(expression.getExpression(), parsed.getExpression()); diff --git a/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java b/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java index 423afd1..b9fdd6d 100644 --- a/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java +++ b/examples/src/main/java/org/apache/accumulo/access/examples/AccessExample.java @@ -25,8 +25,8 @@ import java.util.Set; import java.util.TreeMap; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessEvaluator; -import org.apache.accumulo.access.AccumuloAccess; import org.apache.accumulo.access.AuthorizationValidator; public class AccessExample { @@ -53,12 +53,11 @@ public void run(PrintStream out, String... authorizations) { out.printf("Showing accessible records using authorizations: %s%n", Arrays.toString(authorizations)); - var accumuloAccess = - AccumuloAccess.builder().authorizationValidator(AuthorizationValidator.DEFAULT).build(); + var access = Access.builder().authorizationValidator(AuthorizationValidator.DEFAULT).build(); // Create an access evaluator using the provided authorizations AccessEvaluator evaluator = - accumuloAccess.newEvaluator(accumuloAccess.newAuthorizations(Set.of(authorizations))); + access.newEvaluator(access.newAuthorizations(Set.of(authorizations))); // Print each record whose access expression permits viewing using the provided authorizations getData().forEach((record, accessExpression) -> { diff --git a/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java b/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java index b4da50f..4aff924 100644 --- a/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java +++ b/examples/src/main/java/org/apache/accumulo/access/examples/ParseExamples.java @@ -24,7 +24,7 @@ import java.util.Map; import java.util.TreeSet; -import org.apache.accumulo.access.AccumuloAccess; +import org.apache.accumulo.access.Access; import org.apache.accumulo.access.ParsedAccessExpression; import org.apache.accumulo.access.ParsedAccessExpression.ExpressionType; @@ -34,7 +34,7 @@ */ public class ParseExamples { - public static final AccumuloAccess ACCUMULO_ACCESS = AccumuloAccess.builder().build(); + public static final Access ACCESS = Access.builder().build(); private ParseExamples() {} @@ -47,10 +47,10 @@ public static void replaceAuthorizations(ParsedAccessExpression parsed, // If the term is quoted in the expression, the quotes will be preserved. Calling unquote() // will only unescape and unquote if the string is quoted, otherwise it returns the string as // is. - String auth = ACCUMULO_ACCESS.unquote(parsed.getExpression()); + String auth = ACCESS.unquote(parsed.getExpression()); // Must quote any authorization that needs it. Calling quote() will only quote and escape if // needed, otherwise it returns the string as is. - expressionBuilder.append(ACCUMULO_ACCESS.quote(replacements.getOrDefault(auth, auth))); + expressionBuilder.append(ACCESS.quote(replacements.getOrDefault(auth, auth))); } else { String operator = parsed.getType() == AND ? "&" : "|"; String sep = ""; @@ -103,8 +103,7 @@ public int compareTo(NormalizedExpression o) { if (cmp == 0) { if (type == AUTHORIZATION) { // sort based on the unquoted and unescaped form of the authorization - cmp = - ACCUMULO_ACCESS.unquote(expression).compareTo(ACCUMULO_ACCESS.unquote(o.expression)); + cmp = ACCESS.unquote(expression).compareTo(ACCESS.unquote(o.expression)); } else { cmp = expression.compareTo(o.expression); } @@ -171,8 +170,8 @@ public static NormalizedExpression normalize(ParsedAccessExpression parsed) { if (parsed.getType() == AUTHORIZATION) { // If the authorization is quoted and it does not need to be quoted then the following two // lines will remove the unnecessary quoting. - String unquoted = ACCUMULO_ACCESS.unquote(parsed.getExpression()); - String quoted = ACCUMULO_ACCESS.quote(unquoted); + String unquoted = ACCESS.unquote(parsed.getExpression()); + String quoted = ACCESS.quote(unquoted); return new NormalizedExpression(quoted, parsed.getType()); } else { // The tree set does the work of sorting and deduplicating sub expressions. @@ -218,7 +217,7 @@ public static void walk(String indent, ParsedAccessExpression parsed) { public static void main(String[] args) { - var parsed = ACCUMULO_ACCESS.newParsedExpression("((RED&GREEN)|(PINK&BLUE))"); + var parsed = ACCESS.newParsedExpression("((RED&GREEN)|(PINK&BLUE))"); System.out.printf("Operating on %s%n", parsed); diff --git a/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java b/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java index 75b50ab..c34d045 100644 --- a/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java +++ b/examples/src/test/java/org/apache/accumulo/access/examples/test/ParseExamplesTest.java @@ -18,7 +18,7 @@ */ package org.apache.accumulo.access.examples.test; -import static org.apache.accumulo.access.examples.ParseExamples.ACCUMULO_ACCESS; +import static org.apache.accumulo.access.examples.ParseExamples.ACCESS; import static org.apache.accumulo.access.examples.ParseExamples.replaceAuthorizations; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -89,8 +89,7 @@ public void testNormalize() { var expression = testCase.get(0); var expected = testCase.get(1); - var actual = - ParseExamples.normalize(ACCUMULO_ACCESS.newParsedExpression(expression)).expression; + var actual = ParseExamples.normalize(ACCESS.newParsedExpression(expression)).expression; assertEquals(expected, actual); } } @@ -98,13 +97,13 @@ public void testNormalize() { @Test public void testReplace() { // Test replacement code w/ quoting and escaping. - var parsed = ACCUMULO_ACCESS.newParsedExpression("((RED&\"ESC\\\\\")|(PINK&BLUE))"); + var parsed = ACCESS.newParsedExpression("((RED&\"ESC\\\\\")|(PINK&BLUE))"); StringBuilder expressionBuilder = new StringBuilder(); replaceAuthorizations(parsed, expressionBuilder, Map.of("ESC\\", "NEEDS+QUOTE")); assertEquals("(RED&\"NEEDS+QUOTE\")|(PINK&BLUE)", expressionBuilder.toString()); // Test replacing multiple - parsed = ACCUMULO_ACCESS.newParsedExpression("((RED&(GREEN|YELLOW))|(PINK&BLUE))"); + parsed = ACCESS.newParsedExpression("((RED&(GREEN|YELLOW))|(PINK&BLUE))"); expressionBuilder = new StringBuilder(); replaceAuthorizations(parsed, expressionBuilder, Map.of("RED", "ROUGE", "GREEN", "AQUA")); assertEquals("(ROUGE&(AQUA|YELLOW))|(PINK&BLUE)", expressionBuilder.toString()); From 7a375938e4ea914b9f5c50ac0bd6de47589d2eda Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Thu, 8 Jan 2026 23:55:38 +0000 Subject: [PATCH 23/33] rename validate to validateExpression --- .../access/grammar/antlr/Antlr4Tests.java | 2 +- .../org/apache/accumulo/access/Access.java | 30 +++++++++---------- .../access/impl/AccumuloAccessImpl.java | 4 +-- .../access/tests/AccessEvaluatorTest.java | 4 +-- .../tests/AccessExpressionBenchmark.java | 4 +-- .../access/tests/AccessExpressionTest.java | 6 ++-- .../access/tests/AuthorizationTest.java | 4 +-- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java index 269ca54..6d74196 100644 --- a/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java +++ b/antlr4-example/src/test/java/org/apache/accumulo/access/grammar/antlr/Antlr4Tests.java @@ -112,7 +112,7 @@ public void testCompareWithAccessExpressionImplParsing() throws Exception { assertThrows(InvalidAccessExpressionException.class, () -> ACCESS.newExpression(cv)); assertThrows(AssertionError.class, () -> testParse(cv)); } else { - ACCESS.validate(cv); + ACCESS.validateExpression(cv); testParse(cv); } } diff --git a/core/src/main/java/org/apache/accumulo/access/Access.java b/core/src/main/java/org/apache/accumulo/access/Access.java index ff4d0c6..191f485 100644 --- a/core/src/main/java/org/apache/accumulo/access/Access.java +++ b/core/src/main/java/org/apache/accumulo/access/Access.java @@ -62,8 +62,8 @@ static Builder builder() { * access expressions as arguments in code, consider using this type instead of a String. The * advantage of passing this type over a String is that its known to be a valid expression. Also, * this type is much more informative than a String type. Conceptually this method calls - * {@link #validate(String)} and if that passes creates an immutable object that wraps the - * expression. + * {@link #validateExpression(String)} and if that passes creates an immutable object that wraps + * the expression. * * @throws InvalidAccessExpressionException if the given expression is not valid * @throws InvalidAuthorizationException when the expression contains an authorization that is not @@ -73,11 +73,22 @@ static Builder builder() { AccessExpression newExpression(String expression) throws InvalidAccessExpressionException, InvalidAuthorizationException; + /** + * Quickly validates that an access expression is properly formed. + * + * @param expression a potential access expression that + * @throws InvalidAccessExpressionException if the given expression is not valid + * @throws InvalidAuthorizationException if the expression contains an invalid authorization + * @throws NullPointerException when the argument is null + */ + void validateExpression(String expression) + throws InvalidAccessExpressionException, InvalidAuthorizationException; + /** * Validates an access expression and returns an immutable object with a parse tree. Creating the * parse tree is expensive relative to calling {@link #newExpression(String)} or - * {@link #validate(String)}, so only use this method when the parse tree is always needed. If the - * code may only use the parse tree sometimes, then it may be best to call + * {@link #validateExpression(String)}, so only use this method when the parse tree is always + * needed. If the code may only use the parse tree sometimes, then it may be best to call * {@link #newExpression(String)} to create the access expression and then call * {@link AccessExpression#parse()} when needed. * @@ -144,17 +155,6 @@ void findAuthorizations(String expression, Consumer authorizationConsume */ String unquote(String authorization) throws InvalidAuthorizationException; - /** - * Quickly validates that an access expression is properly formed. - * - * @param expression a potential access expression that - * @throws InvalidAccessExpressionException if the given expression is not valid - * @throws InvalidAuthorizationException if the expression contains an invalid authorization - * @throws NullPointerException when the argument is null - */ - void validate(String expression) - throws InvalidAccessExpressionException, InvalidAuthorizationException; - /** * Creates an AccessEvaluator from an Authorizations object * diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index 87317c8..6d71f3c 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -53,7 +53,7 @@ public AccessExpression newExpression(String expression) { if (expression.isEmpty()) { return AccessExpressionImpl.EMPTY; } - validate(expression); + validateExpression(expression); return new AccessExpressionImpl(expression); } @@ -98,7 +98,7 @@ public String unquote(String authorization) { } @Override - public void validate(String expression) throws InvalidAccessExpressionException { + public void validateExpression(String expression) throws InvalidAccessExpressionException { ParserEvaluator.validate(expression, authValidator); } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index c46e165..9c1bb83 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -118,7 +118,7 @@ private static void runTestCases(Access accumuloAccess, TestDataSet testSet, // exception if (tests.expectedResult == ExpectedResult.ACCESSIBLE || tests.expectedResult == ExpectedResult.INACCESSIBLE) { - accumuloAccess.validate(expression); + accumuloAccess.validateExpression(expression); assertEquals(expression, accumuloAccess.newExpression(expression).getExpression()); // parsing an expression will strip unneeded outer parens assertTrue( @@ -149,7 +149,7 @@ private static void runTestCases(Access accumuloAccess, TestDataSet testSet, assertThrows(InvalidAccessExpressionException.class, () -> evaluator.canAccess(expression), expression); assertThrows(InvalidAccessExpressionException.class, - () -> accumuloAccess.validate(expression), expression); + () -> accumuloAccess.validateExpression(expression), expression); assertThrows(InvalidAccessExpressionException.class, () -> accumuloAccess.newExpression(expression), expression); assertThrows(InvalidAccessExpressionException.class, diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java index 746ba8c..7d0bfbd 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java @@ -172,7 +172,7 @@ List getVisibilityEvaluatorTests() { public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { var accumuloAccess = state.access; for (byte[] accessExpression : state.getBytesExpressions()) { - accumuloAccess.validate(new String(accessExpression, UTF_8)); + accumuloAccess.validateExpression(new String(accessExpression, UTF_8)); } } @@ -183,7 +183,7 @@ public void measureBytesValidation(BenchmarkState state, Blackhole blackhole) { public void measureStringValidation(BenchmarkState state, Blackhole blackhole) { var accumuloAccess = state.access; for (String accessExpression : state.getStringExpressions()) { - accumuloAccess.validate(accessExpression); + accumuloAccess.validateExpression(accessExpression); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java index 41c46bd..c4e0394 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java @@ -78,7 +78,7 @@ public void testGetAuthorizations() { void checkError(String expression, String expected, int index) { var access = Access.builder().build(); - checkError(() -> access.validate(expression), expected, index); + checkError(() -> access.validateExpression(expression), expected, index); checkError(() -> access.newExpression(expression), expected, index); checkError(() -> access.newParsedExpression(expression), expected, index); } @@ -184,7 +184,7 @@ public void testSpecificationDocumentation() throws IOException, URISyntaxExcept public void testEmpty() { var access = Access.builder().build(); // do not expect empty expression to fail validation - access.validate(""); + access.validateExpression(""); assertEquals("", access.newExpression("").getExpression()); var parsed = access.newParsedExpression(""); @@ -217,7 +217,7 @@ public void testImmutable() { public void testNull() { var access = Access.builder().build(); assertThrows(NullPointerException.class, () -> access.newParsedExpression(null)); - assertThrows(NullPointerException.class, () -> access.validate(null)); + assertThrows(NullPointerException.class, () -> access.validateExpression(null)); assertThrows(NullPointerException.class, () -> access.newExpression(null)); assertThrows(NullPointerException.class, () -> access.findAuthorizations(null, auth -> {})); assertThrows(NullPointerException.class, () -> access.findAuthorizations("A&B", null)); diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index e8b9895..52c54a6 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -238,8 +238,8 @@ private static void runTest(String goodAuth1, String goodAuth2, String goodAuth3 // create the same expression with the invalid auth in different places var exp = '"' + a1 + '"' + "|(" + '"' + a2 + '"' + "&" + '"' + a3 + '"' + ")|" + '"' + a4 + '"'; - var exception = - assertThrows(InvalidAuthorizationException.class, () -> accumuloAccess.validate(exp)); + var exception = assertThrows(InvalidAuthorizationException.class, + () -> accumuloAccess.validateExpression(exp)); assertTrue(exception.getMessage().contains(badAuth)); exception = assertThrows(InvalidAuthorizationException.class, From 8331f6313fd6e8aa12cceb3d1b326743ac72316c Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 00:05:19 +0000 Subject: [PATCH 24/33] remove newAuthorizations() --- core/src/main/java/org/apache/accumulo/access/Access.java | 5 ----- .../apache/accumulo/access/impl/AccumuloAccessImpl.java | 5 ----- .../apache/accumulo/access/tests/AuthorizationTest.java | 8 +++----- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/Access.java b/core/src/main/java/org/apache/accumulo/access/Access.java index 191f485..4f35f23 100644 --- a/core/src/main/java/org/apache/accumulo/access/Access.java +++ b/core/src/main/java/org/apache/accumulo/access/Access.java @@ -100,11 +100,6 @@ void validateExpression(String expression) ParsedAccessExpression newParsedExpression(String expression) throws InvalidAccessExpressionException, InvalidAuthorizationException; - /** - * @return a pre-allocated empty Authorizations object - */ - Authorizations newAuthorizations(); - /** * Creates an Authorizations object from the set of input authorization strings. * diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java index 6d71f3c..a5acdf9 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java @@ -62,11 +62,6 @@ public ParsedAccessExpression newParsedExpression(String expression) { return ParsedAccessExpressionImpl.parseExpression(expression, authValidator); } - @Override - public Authorizations newAuthorizations() { - return AuthorizationsImpl.EMPTY; - } - @Override public Authorizations newAuthorizations(Set authorizations) { if (authorizations.isEmpty()) { diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java index 52c54a6..b869465 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AuthorizationTest.java @@ -57,11 +57,9 @@ public void testEquality() { public void testEmpty() { var access = Access.builder().build(); // check if new object is allocated - assertSame(access.newAuthorizations(), access.newAuthorizations()); - // check if optimization is working - assertSame(access.newAuthorizations(), access.newAuthorizations(Set.of())); - assertEquals(Set.of(), access.newAuthorizations().asSet()); - assertSame(Set.of(), access.newAuthorizations().asSet()); + assertSame(access.newAuthorizations(Set.of()), access.newAuthorizations(Set.of())); + assertEquals(Set.of(), access.newAuthorizations(Set.of()).asSet()); + assertSame(Set.of(), access.newAuthorizations(Set.of()).asSet()); } @Test From 4134d651c3fb20591ad928923c9dfc9caa784c7a Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 00:06:17 +0000 Subject: [PATCH 25/33] renamed AccumuloAccessImpl to AccessImpl --- .../access/impl/{AccumuloAccessImpl.java => AccessImpl.java} | 4 ++-- .../java/org/apache/accumulo/access/impl/BuilderImpl.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename core/src/main/java/org/apache/accumulo/access/impl/{AccumuloAccessImpl.java => AccessImpl.java} (97%) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java similarity index 97% rename from core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java rename to core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java index a5acdf9..ba5a8cb 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccumuloAccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java @@ -33,7 +33,7 @@ import org.apache.accumulo.access.InvalidAuthorizationException; import org.apache.accumulo.access.ParsedAccessExpression; -public class AccumuloAccessImpl implements Access { +public class AccessImpl implements Access { private final AuthorizationValidator authValidator; @@ -44,7 +44,7 @@ private void validateAuthorization(CharSequence auth, } } - public AccumuloAccessImpl(AuthorizationValidator authValidator) { + public AccessImpl(AuthorizationValidator authValidator) { this.authValidator = authValidator; } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java index 7ac6d4f..7e8fc88 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/BuilderImpl.java @@ -35,6 +35,6 @@ public Access.Builder authorizationValidator(AuthorizationValidator validator) { @Override public Access build() { - return new AccumuloAccessImpl(validator == null ? AuthorizationValidator.DEFAULT : validator); + return new AccessImpl(validator == null ? AuthorizationValidator.DEFAULT : validator); } } From f8492ab2035d9c373a768c53de0f2bd3e93c0d3b Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 19:20:35 +0000 Subject: [PATCH 26/33] improve API names --- .../access/AuthorizationValidator.java | 42 +++++++++++-------- .../accumulo/access/impl/AccessImpl.java | 11 +++-- .../impl/ParsedAccessExpressionImpl.java | 6 +-- .../accumulo/access/impl/Tokenizer.java | 6 +-- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java index 0e18a7c..7baa907 100644 --- a/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java +++ b/core/src/main/java/org/apache/accumulo/access/AuthorizationValidator.java @@ -18,6 +18,7 @@ */ package org.apache.accumulo.access; +import java.nio.charset.CharsetDecoder; import java.util.function.BiPredicate; /** @@ -29,7 +30,7 @@ * When an authorization is quoted and/or escaped in access expression that is undone before is * passed to this predicate. Conceptually it is like {@link Access#unquote(String)} is called prior * to being passed to this predicate. If the authorization was quoted that information is passed - * along is it may be useful for optimizations. + * along as it may be useful for optimizations. * *

* A CharSequence is passed to this predicate for efficiency. It allows having a view into the @@ -42,22 +43,22 @@ * @since 1.0.0 */ public interface AuthorizationValidator - extends BiPredicate { + extends BiPredicate { /** * @since 1.0.0 */ - enum AuthorizationQuoting { + enum AuthorizationCharacters { /** - * Denotes that an authorization seen in a valid access expression was quoted. This may mean the - * expression has extra characters not seen in an unquoted authorization. + * This authorization could potentially contain any java character. */ - QUOTED, + ANY, /** - * Denotes that an authorization seen in a valid access expression was unquoted. This means the - * expression only contains the characters allowed in an unquoted authorization. + * Authorization only contains the characters + * + *

{@code [0-9a-zA-Z_-.:/] }
*/ - UNQUOTED + BASIC } /** @@ -66,17 +67,20 @@ enum AuthorizationQuoting { * *
    *     {@code
-   *     AuthorizationValidator DEFAULT = (auth, quoting) -> {
-   *       if(quoting == AuthorizationQuoting.UNQUOTED) {
+   *     AuthorizationValidator DEFAULT = (auth, authChars) -> {
+   *       if (authChars == AuthorizationCharacters.BASIC) {
+   *         // The authorization is already known to only contain a small set of ASCII chars and no
+   *         // further validation is needed.
    *         return true;
    *       }
+   *
+   *       // Unsure what characters are present, so must validate them all.
    *       for (int i = 0; i < auth.length(); i++) {
    *         var c = auth.charAt(i);
-   *         if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c) || c == '\uFFFD') {
+   *         if (!Character.isDefined(c) || Character.isISOControl(c) || c == '\uFFFD') {
    *           return false;
    *         }
    *       }
-   *       return true;
    *     }
    *     }
    * 
@@ -89,18 +93,20 @@ enum AuthorizationQuoting { * * @see Character#isDefined(char) * @see Character#isISOControl(char) + * @see CharsetDecoder#replacement() * @since 1.0.0 */ - AuthorizationValidator DEFAULT = (auth, quoting) -> { - if (quoting == AuthorizationQuoting.UNQUOTED) { - // If an authorization is in a valid access expression and is unquoted then its already known - // to only contain a small set of ASCII chars and no further validation is needed. + AuthorizationValidator DEFAULT = (auth, authChars) -> { + if (authChars == AuthorizationCharacters.BASIC) { + // The authorization is already known to only contain a small set of ASCII chars and no + // further validation is needed. return true; } + // Unsure what characters are present, so must validate them all. for (int i = 0; i < auth.length(); i++) { var c = auth.charAt(i); - if (!Character.isDefined(auth.charAt(i)) || Character.isISOControl(c) || c == '\uFFFD') { + if (!Character.isDefined(c) || Character.isISOControl(c) || c == '\uFFFD') { return false; } } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java index ba5a8cb..bec9b4b 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java @@ -18,7 +18,7 @@ */ package org.apache.accumulo.access.impl; -import static org.apache.accumulo.access.AuthorizationValidator.AuthorizationQuoting.QUOTED; +import static org.apache.accumulo.access.AuthorizationValidator.AuthorizationCharacters.ANY; import java.util.Collection; import java.util.Set; @@ -38,7 +38,7 @@ public class AccessImpl implements Access { private final AuthorizationValidator authValidator; private void validateAuthorization(CharSequence auth, - AuthorizationValidator.AuthorizationQuoting quoting) { + AuthorizationValidator.AuthorizationCharacters quoting) { if (!authValidator.test(auth, quoting)) { throw new InvalidAuthorizationException(auth.toString()); } @@ -67,8 +67,7 @@ public Authorizations newAuthorizations(Set authorizations) { if (authorizations.isEmpty()) { return AuthorizationsImpl.EMPTY; } else { - // not sure if the auth needs quoting or not, so assume it does - authorizations.forEach(auth -> validateAuthorization(auth, QUOTED)); + authorizations.forEach(auth -> validateAuthorization(auth, ANY)); return new AuthorizationsImpl(authorizations); } } @@ -81,14 +80,14 @@ public void findAuthorizations(String expression, Consumer authorization @Override public String quote(String authorization) { - validateAuthorization(authorization, QUOTED); + validateAuthorization(authorization, ANY); return AccessExpressionImpl.quote(authorization).toString(); } @Override public String unquote(String authorization) { var unquoted = AccessExpressionImpl.unquote(authorization); - validateAuthorization(unquoted, QUOTED); + validateAuthorization(unquoted, ANY); return unquoted.toString(); } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java index 2a56d10..2645c88 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java @@ -168,15 +168,15 @@ private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(To } else { var auth = tokenizer.nextAuthorization(true); CharSequence unquotedAuth; - AuthorizationValidator.AuthorizationQuoting quoting; + AuthorizationValidator.AuthorizationCharacters quoting; var wrapper = ParserEvaluator.lookupWrappers.get(); wrapper.set(auth.data, auth.start, auth.len); if (ByteUtils.isQuoteSymbol(wrapper.charAt(0))) { unquotedAuth = AccessExpressionImpl.unquote(wrapper); - quoting = AuthorizationValidator.AuthorizationQuoting.QUOTED; + quoting = AuthorizationValidator.AuthorizationCharacters.ANY; } else { unquotedAuth = wrapper; - quoting = AuthorizationValidator.AuthorizationQuoting.UNQUOTED; + quoting = AuthorizationValidator.AuthorizationCharacters.BASIC; } if (!authorizationValidator.test(unquotedAuth, quoting)) { throw new InvalidAuthorizationException(unquotedAuth.toString()); diff --git a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java index 2ad6de2..01e6c7c 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java @@ -62,7 +62,7 @@ public static class AuthorizationToken { public int start; public int len; public boolean hasEscapes; - public AuthorizationValidator.AuthorizationQuoting quoting; + public AuthorizationValidator.AuthorizationCharacters quoting; } @@ -139,7 +139,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { authorizationToken.start = start; authorizationToken.len = index - start; authorizationToken.hasEscapes = hasEscapes; - authorizationToken.quoting = AuthorizationValidator.AuthorizationQuoting.QUOTED; + authorizationToken.quoting = AuthorizationValidator.AuthorizationCharacters.ANY; if (includeQuotes) { authorizationToken.start--; @@ -158,7 +158,7 @@ AuthorizationToken nextAuthorization(boolean includeQuotes) { authorizationToken.start = start; authorizationToken.len = index - start; authorizationToken.hasEscapes = false; - authorizationToken.quoting = AuthorizationValidator.AuthorizationQuoting.UNQUOTED; + authorizationToken.quoting = AuthorizationValidator.AuthorizationCharacters.BASIC; return authorizationToken; } else { error("Expected a '(' character or an authorization token instead saw '" + peek() + "'"); From e69ecf9e89916b04c741f6f72376b24053c4ab91 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 22:19:04 +0000 Subject: [PATCH 27/33] improve performance and test --- .../access/impl/AccessExpressionImpl.java | 2 +- .../impl/ParsedAccessExpressionImpl.java | 39 ++++++++++++------- .../access/tests/AccessEvaluatorTest.java | 37 ++++++++++++++++++ .../tests/AccessExpressionBenchmark.java | 11 ++++-- .../access/tests/AccessExpressionTest.java | 11 +++++- .../tests/ParsedAccessExpressionTest.java | 30 +++++++++++--- 6 files changed, 105 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java index 7a944b2..da611b6 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessExpressionImpl.java @@ -83,7 +83,7 @@ public static CharSequence unquote(CharSequence term) { if (term.charAt(0) == '"' && term.charAt(term.length() - 1) == '"') { term = term.subSequence(1, term.length() - 1); - return AccessEvaluatorImpl.unescape(term).toString(); + return AccessEvaluatorImpl.unescape(term); } else { return term; } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java index 2645c88..16309d8 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.InvalidAuthorizationException; @@ -36,7 +37,8 @@ public final class ParsedAccessExpressionImpl extends ParsedAccessExpression { private static final long serialVersionUID = 1L; - private final String expression; + private final String wholeExpression; + private final AtomicReference expression = new AtomicReference<>(null); private final int offset; private final int length; @@ -45,7 +47,7 @@ public final class ParsedAccessExpressionImpl extends ParsedAccessExpression { public static final ParsedAccessExpression EMPTY = new ParsedAccessExpressionImpl(); - private ParsedAccessExpressionImpl(char operator, String expression, int offset, int length, + private ParsedAccessExpressionImpl(char operator, String wholeExpression, int offset, int length, List children) { if (children.isEmpty()) { throw new IllegalArgumentException("Must have children with an operator"); @@ -59,23 +61,23 @@ private ParsedAccessExpressionImpl(char operator, String expression, int offset, this.type = OR; } - this.expression = expression; + this.wholeExpression = wholeExpression; this.offset = offset; this.length = length; this.children = List.copyOf(children); } - private ParsedAccessExpressionImpl(String expression) { + private ParsedAccessExpressionImpl(String wholeExpression, int offset, int length) { this.type = AUTHORIZATION; - this.expression = expression; - this.offset = 0; - this.length = expression.length(); + this.wholeExpression = wholeExpression; + this.offset = offset; + this.length = length; this.children = List.of(); } ParsedAccessExpressionImpl() { this.type = ExpressionType.EMPTY; - this.expression = ""; + this.wholeExpression = ""; this.offset = 0; this.length = 0; this.children = List.of(); @@ -83,7 +85,13 @@ private ParsedAccessExpressionImpl(String expression) { @Override public String getExpression() { - return expression.substring(offset, length + offset); + String strExp = expression.get(); + if (strExp != null) { + return strExp; + } + strExp = wholeExpression.substring(offset, length + offset); + expression.compareAndSet(null, strExp); + return expression.get(); } @Override @@ -170,18 +178,23 @@ private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(To CharSequence unquotedAuth; AuthorizationValidator.AuthorizationCharacters quoting; var wrapper = ParserEvaluator.lookupWrappers.get(); - wrapper.set(auth.data, auth.start, auth.len); - if (ByteUtils.isQuoteSymbol(wrapper.charAt(0))) { - unquotedAuth = AccessExpressionImpl.unquote(wrapper); + if (ByteUtils.isQuoteSymbol(auth.data[auth.start])) { + wrapper.set(auth.data, auth.start + 1, auth.len - 2); + if (auth.hasEscapes) { + unquotedAuth = AccessEvaluatorImpl.unescape(wrapper); + } else { + unquotedAuth = wrapper; + } quoting = AuthorizationValidator.AuthorizationCharacters.ANY; } else { + wrapper.set(auth.data, auth.start, auth.len); unquotedAuth = wrapper; quoting = AuthorizationValidator.AuthorizationCharacters.BASIC; } if (!authorizationValidator.test(unquotedAuth, quoting)) { throw new InvalidAuthorizationException(unquotedAuth.toString()); } - return new ParsedAccessExpressionImpl(new String(auth.data, auth.start, auth.len)); + return new ParsedAccessExpressionImpl(wholeExpression, auth.start, auth.len); } } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index 9c1bb83..95489cb 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -34,6 +35,7 @@ import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessEvaluator; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.impl.AccessEvaluatorImpl; import org.junit.jupiter.api.Test; @@ -227,5 +229,40 @@ public void testUnescape() { .forEach(seq -> assertThrows(IllegalArgumentException.class, () -> unescape(seq), message)); } + @Test + public void testAuthValidation() { + // This test ensures that unquoted and unescaped auths are passed to the auth validator. + HashSet seenAuths = new HashSet<>(); + AuthorizationValidator authorizationValidator = (auth, authChars) -> { + seenAuths.add(auth.toString()); + return AuthorizationValidator.DEFAULT.test(auth, authChars); + }; + var access = Access.builder().authorizationValidator(authorizationValidator).build(); + var qa1 = access.quote("A"); + var qa2 = access.quote("B/C"); + var qa3 = access.quote("D\\E"); + + assertEquals(Set.of("A", "B/C", "D\\E"), seenAuths); + seenAuths.clear(); + + assertEquals("A", access.unquote(qa1)); + assertEquals("B/C", access.unquote(qa2)); + assertEquals("D\\E", access.unquote(qa3)); + + assertEquals(Set.of("A", "B/C", "D\\E"), seenAuths); + seenAuths.clear(); + + var eval = access.newEvaluator(access.newAuthorizations(Set.of("A"))); + assertFalse(eval.canAccess(qa1 + "&" + qa2 + "&" + qa3)); + assertEquals(Set.of("A", "B/C", "D\\E"), seenAuths); + seenAuths.clear(); + + eval = access.newEvaluator(a -> a.equals("A")); + assertFalse(eval.canAccess(qa1 + "&" + qa2 + "&" + qa3)); + assertEquals(Set.of("A", "B/C", "D\\E"), seenAuths); + seenAuths.clear(); + + } + // TODO need to copy all test from Accumulo } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java index 7d0bfbd..a5eb3b4 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionBenchmark.java @@ -255,10 +255,13 @@ public static void main(String[] args) throws RunnerException, IOException { System.out.println("Number of Expressions: " + numExpressions); - Options opt = new OptionsBuilder().include(AccessExpressionBenchmark.class.getSimpleName()) - .mode(Mode.Throughput).operationsPerInvocation(numExpressions) - .timeUnit(TimeUnit.MICROSECONDS).warmupTime(TimeValue.seconds(5)).warmupIterations(3) - .measurementIterations(4).forks(3).build(); + var include = System.getenv().getOrDefault("ACCESS_BENCHMARK", + AccessExpressionBenchmark.class.getSimpleName()); + + Options opt = new OptionsBuilder().include(include).mode(Mode.Throughput) + .operationsPerInvocation(numExpressions).timeUnit(TimeUnit.MICROSECONDS) + .warmupTime(TimeValue.seconds(5)).warmupIterations(3).measurementIterations(4).forks(3) + .build(); new Runner(opt).run(); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java index c4e0394..5fa1f1e 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java @@ -38,6 +38,7 @@ import org.apache.accumulo.access.Access; import org.apache.accumulo.access.AccessExpression; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.ParsedAccessExpression; import org.junit.jupiter.api.Disabled; @@ -48,7 +49,12 @@ public class AccessExpressionTest { @Test public void testGetAuthorizations() { - var access = Access.builder().build(); + HashSet seenAuths = new HashSet<>(); + AuthorizationValidator authorizationValidator = (auth, authChars) -> { + seenAuths.add(auth.toString()); + return AuthorizationValidator.DEFAULT.test(auth, authChars); + }; + var access = Access.builder().authorizationValidator(authorizationValidator).build(); // Test data pairs where the first entry of each pair is an expression to normalize and second // is the expected authorization in the expression var testData = new ArrayList>(); @@ -72,6 +78,9 @@ public void testGetAuthorizations() { var actual = found.stream().sorted().collect(Collectors.joining(",")); assertEquals(expected, actual); found.clear(); + actual = seenAuths.stream().sorted().collect(Collectors.joining(",")); + assertEquals(expected, actual); + seenAuths.clear(); } } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java index 9c2213d..0acbdce 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/ParsedAccessExpressionTest.java @@ -27,26 +27,44 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.apache.accumulo.access.Access; +import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.ParsedAccessExpression; import org.junit.jupiter.api.Test; public class ParsedAccessExpressionTest { @Test public void testParsing() { - var access = Access.builder().build(); - String expression = "(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|BLACK"; - for (var parsed : List.of(access.newParsedExpression(expression), - access.newExpression(expression).parse())) { + HashSet seenAuths = new HashSet<>(); + AuthorizationValidator authorizationValidator = (auth, authChars) -> { + seenAuths.add(auth.toString()); + return AuthorizationValidator.DEFAULT.test(auth, authChars); + }; + var access = Access.builder().authorizationValidator(authorizationValidator).build(); + String expression = + "(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|\"BLACK\\\\RED\""; + + var pae1 = access.newParsedExpression(expression); + // check that unescaped and unquoted auths are passed in to the auth validator + assertEquals(Set.of("BLUE", "RED", "PINK", "YELLOW", "GREEN/GREY", "BLACK\\RED"), seenAuths); + seenAuths.clear(); + var pae2 = access.newExpression(expression).parse(); + // check that unescaped and unquoted auths are passed in to the auth validator + assertEquals(Set.of("BLUE", "RED", "PINK", "YELLOW", "GREEN/GREY", "BLACK\\RED"), seenAuths); + + for (var parsed : List.of(pae1, pae2)) { // verify root node - verify("(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|BLACK", OR, 3, parsed); + verify("(BLUE&(RED|PINK|YELLOW))|((YELLOW|\"GREEN/GREY\")&(RED|BLUE))|\"BLACK\\\\RED\"", OR, + 3, parsed); // verify all nodes at level 1 in the tree verify("BLUE&(RED|PINK|YELLOW)", AND, 2, parsed, 0); verify("(YELLOW|\"GREEN/GREY\")&(RED|BLUE)", AND, 2, parsed, 1); - verify("BLACK", AUTHORIZATION, 0, parsed, 2); + verify("\"BLACK\\\\RED\"", AUTHORIZATION, 0, parsed, 2); // verify all nodes at level 2 in the tree verify("BLUE", AUTHORIZATION, 0, parsed, 0, 0); From 6752d657d5a4e926784150575779e04b73edb8f5 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 22:37:00 +0000 Subject: [PATCH 28/33] add null checks and refactor code --- .../access/impl/AccessEvaluatorImpl.java | 15 ++------ .../accumulo/access/impl/AccessImpl.java | 3 +- .../accumulo/access/impl/ParserEvaluator.java | 34 +++++++++---------- .../access/tests/AccessEvaluatorTest.java | 6 ++++ 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java index 01b9cac..359d07e 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java @@ -32,7 +32,6 @@ import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.Authorizations; import org.apache.accumulo.access.InvalidAccessExpressionException; -import org.apache.accumulo.access.InvalidAuthorizationException; public final class AccessEvaluatorImpl implements AccessEvaluator { @@ -162,21 +161,13 @@ public boolean canAccess(String expression) throws InvalidAccessExpressionExcept boolean evaluate(String accessExpression) throws InvalidAccessExpressionException { var charsWrapper = ParserEvaluator.lookupWrappers.get(); - Predicate atp = authToken -> { - var authorization = ParserEvaluator.unescape(authToken, charsWrapper); - if (!authorizationValidator.test(authorization, authToken.quoting)) { - throw new InvalidAuthorizationException(authorization.toString()); - } - return authorizedPredicate.test(authorization); - }; + Predicate atp = authToken -> authorizedPredicate + .test(ParserEvaluator.validateAuth(authorizationValidator, authToken, charsWrapper)); // This is used once the expression is known to always be true or false. For this case only need // to validate authorizations, do not need to look them up in a set. Predicate shortCircuit = authToken -> { - var authorization = ParserEvaluator.unescape(authToken, charsWrapper); - if (!authorizationValidator.test(authorization, authToken.quoting)) { - throw new InvalidAuthorizationException(authorization.toString()); - } + ParserEvaluator.validateAuth(authorizationValidator, authToken, charsWrapper); return true; }; diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java index bec9b4b..834c0d8 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java @@ -21,6 +21,7 @@ import static org.apache.accumulo.access.AuthorizationValidator.AuthorizationCharacters.ANY; import java.util.Collection; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -45,7 +46,7 @@ private void validateAuthorization(CharSequence auth, } public AccessImpl(AuthorizationValidator authValidator) { - this.authValidator = authValidator; + this.authValidator = Objects.requireNonNull(authValidator); } @Override diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java index 9a18247..c7f4487 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java @@ -58,6 +58,21 @@ static Tokenizer getPerThreadTokenizer(String expression) { return tokenizer; } + static CharSequence validateAuth(AuthorizationValidator authValidator, + Tokenizer.AuthorizationToken authToken, CharsWrapper charsWrapper) { + charsWrapper.set(authToken.data, authToken.start, authToken.len); + CharSequence authorizations; + if (authToken.hasEscapes) { + authorizations = AccessEvaluatorImpl.unescape(charsWrapper); + } else { + authorizations = charsWrapper; + } + if (!authValidator.test(authorizations, authToken.quoting)) { + throw new InvalidAuthorizationException(authorizations.toString()); + } + return authorizations; + } + public static void validate(String expression, AuthorizationValidator authValidator) throws InvalidAccessExpressionException { if (expression.isEmpty()) { @@ -66,10 +81,7 @@ public static void validate(String expression, AuthorizationValidator authValida var charsWrapper = ParserEvaluator.lookupWrappers.get(); Predicate vp = authToken -> { - var authorizations = unescape(authToken, charsWrapper); - if (!authValidator.test(authorizations, authToken.quoting)) { - throw new InvalidAuthorizationException(authorizations.toString()); - } + validateAuth(authValidator, authToken, charsWrapper); return true; }; @@ -80,24 +92,12 @@ public static void findAuthorizations(String expression, Consumer author AuthorizationValidator authValidator) throws InvalidAccessExpressionException { var charsWrapper = ParserEvaluator.lookupWrappers.get(); Predicate atp = authToken -> { - var authorizations = unescape(authToken, charsWrapper); - if (!authValidator.test(authorizations, authToken.quoting)) { - throw new InvalidAuthorizationException(authorizations.toString()); - } - authorizationConsumer.accept(authorizations.toString()); + authorizationConsumer.accept(validateAuth(authValidator, authToken, charsWrapper).toString()); return true; }; ParserEvaluator.parseAccessExpression(expression, atp, atp); } - static CharSequence unescape(Tokenizer.AuthorizationToken token, CharsWrapper wrapper) { - wrapper.set(token.data, token.start, token.len); - if (token.hasEscapes) { - return AccessEvaluatorImpl.unescape(wrapper); - } - return wrapper; - } - public static boolean parseAccessExpression(String expression, Predicate authorizedPredicate, Predicate shortCircuitPredicate) { diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index 95489cb..110fb29 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -229,6 +229,12 @@ public void testUnescape() { .forEach(seq -> assertThrows(IllegalArgumentException.class, () -> unescape(seq), message)); } + @Test + public void testNullAuthValidator() { + assertThrows(NullPointerException.class, + () -> Access.builder().authorizationValidator(null).build()); + } + @Test public void testAuthValidation() { // This test ensures that unquoted and unescaped auths are passed to the auth validator. From 55a0c263f3bc6d97bfa97cc6ed92d19e1f658806 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 22:57:22 +0000 Subject: [PATCH 29/33] use record and to todo --- .../access/impl/AccessEvaluatorImpl.java | 9 +++--- .../access/impl/AuthorizationsImpl.java | 28 +++---------------- .../impl/{ByteUtils.java => CharUtils.java} | 12 ++++---- .../impl/ParsedAccessExpressionImpl.java | 8 +++--- .../accumulo/access/impl/ParserEvaluator.java | 10 +++---- .../accumulo/access/impl/Tokenizer.java | 6 ++-- 6 files changed, 26 insertions(+), 47 deletions(-) rename core/src/main/java/org/apache/accumulo/access/impl/{ByteUtils.java => CharUtils.java} (87%) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java index 359d07e..6f34a69 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessEvaluatorImpl.java @@ -18,10 +18,10 @@ */ package org.apache.accumulo.access.impl; -import static org.apache.accumulo.access.impl.ByteUtils.BACKSLASH; -import static org.apache.accumulo.access.impl.ByteUtils.QUOTE; -import static org.apache.accumulo.access.impl.ByteUtils.isQuoteOrSlash; -import static org.apache.accumulo.access.impl.ByteUtils.isQuoteSymbol; +import static org.apache.accumulo.access.impl.CharUtils.BACKSLASH; +import static org.apache.accumulo.access.impl.CharUtils.QUOTE; +import static org.apache.accumulo.access.impl.CharUtils.isQuoteOrSlash; +import static org.apache.accumulo.access.impl.CharUtils.isQuoteSymbol; import java.util.HashSet; import java.util.Set; @@ -36,7 +36,6 @@ public final class AccessEvaluatorImpl implements AccessEvaluator { private final Predicate authorizedPredicate; - // TODO set private final AuthorizationValidator authorizationValidator; /** diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java index 529d147..6ad1d46 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AuthorizationsImpl.java @@ -18,20 +18,20 @@ */ package org.apache.accumulo.access.impl; +import java.io.Serial; import java.util.Iterator; import java.util.Set; import org.apache.accumulo.access.Authorizations; -public final class AuthorizationsImpl implements Authorizations { +public record AuthorizationsImpl(Set authorizations) implements Authorizations { + @Serial private static final long serialVersionUID = 1L; static final Authorizations EMPTY = new AuthorizationsImpl(Set.of()); - private final Set authorizations; - - AuthorizationsImpl(Set authorizations) { + public AuthorizationsImpl(Set authorizations) { this.authorizations = Set.copyOf(authorizations); } @@ -45,26 +45,6 @@ public Set asSet() { return authorizations; } - @Override - public boolean equals(Object o) { - if (o instanceof AuthorizationsImpl) { - var oa = (AuthorizationsImpl) o; - return authorizations.equals(oa.authorizations); - } - - return false; - } - - @Override - public int hashCode() { - return authorizations.hashCode(); - } - - @Override - public String toString() { - return authorizations.toString(); - } - @Override public Iterator iterator() { return authorizations.iterator(); diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java b/core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java similarity index 87% rename from core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java rename to core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java index b0ead04..d2a5f17 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ByteUtils.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java @@ -23,13 +23,13 @@ * methods for comparing them. */ // TODO rename -final class ByteUtils { - static final byte QUOTE = (byte) '"'; - static final byte BACKSLASH = (byte) '\\'; - static final byte AND_OPERATOR = (byte) '&'; - static final byte OR_OPERATOR = (byte) '|'; +final class CharUtils { + static final char QUOTE = '"'; + static final char BACKSLASH = '\\'; + static final char AND_OPERATOR = '&'; + static final char OR_OPERATOR = '|'; - private ByteUtils() { + private CharUtils() { // private constructor to prevent instantiation } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java index 16309d8..4f97f32 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParsedAccessExpressionImpl.java @@ -21,9 +21,9 @@ import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AND; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.AUTHORIZATION; import static org.apache.accumulo.access.ParsedAccessExpression.ExpressionType.OR; -import static org.apache.accumulo.access.impl.ByteUtils.AND_OPERATOR; -import static org.apache.accumulo.access.impl.ByteUtils.OR_OPERATOR; -import static org.apache.accumulo.access.impl.ByteUtils.isAndOrOperator; +import static org.apache.accumulo.access.impl.CharUtils.AND_OPERATOR; +import static org.apache.accumulo.access.impl.CharUtils.OR_OPERATOR; +import static org.apache.accumulo.access.impl.CharUtils.isAndOrOperator; import java.util.ArrayList; import java.util.List; @@ -178,7 +178,7 @@ private static ParsedAccessExpressionImpl parseParenExpressionOrAuthorization(To CharSequence unquotedAuth; AuthorizationValidator.AuthorizationCharacters quoting; var wrapper = ParserEvaluator.lookupWrappers.get(); - if (ByteUtils.isQuoteSymbol(auth.data[auth.start])) { + if (CharUtils.isQuoteSymbol(auth.data[auth.start])) { wrapper.set(auth.data, auth.start + 1, auth.len - 2); if (auth.hasEscapes) { unquotedAuth = AccessEvaluatorImpl.unescape(wrapper); diff --git a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java index c7f4487..557d89a 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/ParserEvaluator.java @@ -18,7 +18,7 @@ */ package org.apache.accumulo.access.impl; -import static org.apache.accumulo.access.impl.ByteUtils.isAndOrOperator; +import static org.apache.accumulo.access.impl.CharUtils.isAndOrOperator; import java.util.function.Consumer; import java.util.function.Predicate; @@ -132,13 +132,13 @@ private static boolean parseExpression(Tokenizer tokenizer, if (tokenizer.hasNext()) { var operator = tokenizer.peek(); - if (operator == ByteUtils.AND_OPERATOR) { + if (operator == CharUtils.AND_OPERATOR) { result = parseAndExpression(result, tokenizer, authorizedPredicate, shortCircuitPredicate); if (tokenizer.hasNext() && isAndOrOperator(tokenizer.peek())) { // A case of mixed operators, lets give a clear error message tokenizer.error("Cannot mix '|' and '&'"); } - } else if (operator == ByteUtils.OR_OPERATOR) { + } else if (operator == CharUtils.OR_OPERATOR) { result = parseOrExpression(result, tokenizer, authorizedPredicate, shortCircuitPredicate); if (tokenizer.hasNext() && isAndOrOperator(tokenizer.peek())) { // A case of mixed operators, lets give a clear error message @@ -163,7 +163,7 @@ private static boolean parseAndExpression(boolean result, Tokenizer tokenizer, var nextResult = parseParenExpressionOrAuthorization(tokenizer, authorizedPredicate, shortCircuitPredicate); result &= nextResult; - } while (tokenizer.hasNext() && tokenizer.peek() == ByteUtils.AND_OPERATOR); + } while (tokenizer.hasNext() && tokenizer.peek() == CharUtils.AND_OPERATOR); return result; } @@ -180,7 +180,7 @@ private static boolean parseOrExpression(boolean result, Tokenizer tokenizer, var nextResult = parseParenExpressionOrAuthorization(tokenizer, authorizedPredicate, shortCircuitPredicate); result |= nextResult; - } while (tokenizer.hasNext() && tokenizer.peek() == ByteUtils.OR_OPERATOR); + } while (tokenizer.hasNext() && tokenizer.peek() == CharUtils.OR_OPERATOR); return result; } diff --git a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java index 01e6c7c..6b46f35 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/Tokenizer.java @@ -18,9 +18,9 @@ */ package org.apache.accumulo.access.impl; -import static org.apache.accumulo.access.impl.ByteUtils.isBackslashSymbol; -import static org.apache.accumulo.access.impl.ByteUtils.isQuoteOrSlash; -import static org.apache.accumulo.access.impl.ByteUtils.isQuoteSymbol; +import static org.apache.accumulo.access.impl.CharUtils.isBackslashSymbol; +import static org.apache.accumulo.access.impl.CharUtils.isQuoteOrSlash; +import static org.apache.accumulo.access.impl.CharUtils.isQuoteSymbol; import java.util.stream.IntStream; From 0ccaa8a588ac9701a4be8d00a75ff74d73ef4fd4 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 22:58:16 +0000 Subject: [PATCH 30/33] remove todo --- .../src/main/java/org/apache/accumulo/access/impl/CharUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java b/core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java index d2a5f17..696fbb1 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/CharUtils.java @@ -22,7 +22,6 @@ * This class exists to avoid repeat conversions from byte to char as well as to provide helper * methods for comparing them. */ -// TODO rename final class CharUtils { static final char QUOTE = '"'; static final char BACKSLASH = '\\'; From f7b0aa9a8f8a97844f592667e029f73abedf8ae0 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 23:31:44 +0000 Subject: [PATCH 31/33] remove todo --- .../accumulo/access/AccessExpression.java | 67 +------------------ 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/AccessExpression.java b/core/src/main/java/org/apache/accumulo/access/AccessExpression.java index dfc7ab4..7c7de37 100644 --- a/core/src/main/java/org/apache/accumulo/access/AccessExpression.java +++ b/core/src/main/java/org/apache/accumulo/access/AccessExpression.java @@ -22,74 +22,9 @@ import org.apache.accumulo.access.impl.AccessExpressionImpl; -// TODO update or remove example code... maybe remove it because the project has example code that is tested /** - * This class offers the ability to operate on access expressions. + * An immutable wrapper for a validated access expression. * - *

- * Below is an example of how to use this API. - * - *

- * {@code
- * // The following authorization does not need quoting
- * // so the return value is the same as the input.
- * var auth1 = AccessExpression.quote("CAT");
- *
- * // The following two authorizations need quoting and the return values will be quoted.
- * var auth2 = AccessExpression.quote("πŸ¦•");
- * var auth3 = AccessExpression.quote("πŸ¦–");
- *
- * // Create an AccessExpression using auth1, auth2, and auth3
- * var exp = "(" + auth1 + "&" + auth3 + ")|(" + auth1 + "&" + auth2 + ")";
- *
- * // Validate the expression w/o creating an object
- * AccessExpression.validate(exp);
- * System.out.println(exp);
- *
- * // Validate the expression and create an immutable AccessExpression object.  This object can be passed around in code and other code knows it valid and does not need to revalidate.
- * AccessExpression accessExpression = AccessExpression.of(exp);
- * System.out.println(accessExpression);
- *
- * // Print the authorization in the expression
- * AccessExpression.findAuthorizations(exp, System.out::println);
- *
- * // Create an AccessExpression with a parse tree.  Creating this is more expensive than calling AccessExpression.of(), so it should only be used if the parse tree is needed.
- * ParsedAccessExpression parsed = AccessExpression.parse(exp);
- * System.out.println("type:"+parsed.getType()+" child[0]:"+parsed.getChildren().get(0)+" child[1]:"+  child[1]:"+parsed.getChildren().get(1));
- *
- * }
- * 
- * - * The above example will print the following. - * - *
- * {@code
- * (CAT&"πŸ¦–")|(CAT&"πŸ¦•")
- * (CAT&"πŸ¦–")|(CAT&"πŸ¦•")
- * CAT
- * πŸ¦–
- * CAT
- * πŸ¦•
- * type:OR child[0]:CAT&"πŸ¦–" child[1]:CAT&"πŸ¦•"
- * }
- * 
- * - * The following code will throw an {@link InvalidAccessExpressionException} because the expression - * is not valid. - * - *
- * {@code
- * AccessExpression.validate("A&B|C");
- * }
- * 
- * - *

- * Instances of this class are thread-safe. - * - *

- * Note: The underlying implementation uses UTF-8 when converting between bytes and Strings. - * - * @see Accumulo Access Documentation * @since 1.0.0 */ public sealed abstract class AccessExpression implements Serializable From 346e3226bc939e67d558c7249408381b7d150d40 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Fri, 9 Jan 2026 23:41:04 +0000 Subject: [PATCH 32/33] do todo --- .../apache/accumulo/access/impl/AccessImpl.java | 3 +++ .../access/tests/AccessEvaluatorTest.java | 15 ++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java index 834c0d8..d8b1d57 100644 --- a/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java +++ b/core/src/main/java/org/apache/accumulo/access/impl/AccessImpl.java @@ -40,6 +40,9 @@ public class AccessImpl implements Access { private void validateAuthorization(CharSequence auth, AuthorizationValidator.AuthorizationCharacters quoting) { + if (auth.isEmpty()) { + throw new IllegalArgumentException("Empty string is not a valid authorization"); + } if (!authValidator.test(auth, quoting)) { throw new InvalidAuthorizationException(auth.toString()); } diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java index 110fb29..b72b2a0 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessEvaluatorTest.java @@ -167,15 +167,10 @@ private static void runTestCases(Access accumuloAccess, TestDataSet testSet, @Test public void testEmptyAuthorizations() { var access = Access.builder().build(); - // TODO what part of the code throwing the exception? - assertThrows(IllegalArgumentException.class, - () -> access.newEvaluator(access.newAuthorizations(Set.of("")))); - assertThrows(IllegalArgumentException.class, - () -> access.newEvaluator(access.newAuthorizations(Set.of("", "A")))); - assertThrows(IllegalArgumentException.class, - () -> access.newEvaluator(access.newAuthorizations(Set.of("A", "")))); - assertThrows(IllegalArgumentException.class, - () -> access.newEvaluator(access.newAuthorizations(Set.of("")))); + assertThrows(IllegalArgumentException.class, () -> access.newAuthorizations(Set.of(""))); + assertThrows(IllegalArgumentException.class, () -> access.newAuthorizations(Set.of("", "A"))); + assertThrows(IllegalArgumentException.class, () -> access.newAuthorizations(Set.of("A", ""))); + assertThrows(IllegalArgumentException.class, () -> access.newAuthorizations(Set.of(""))); } @Test @@ -208,6 +203,8 @@ public void testQuote() { assertEquals("九", access.unquote(access.quote("九"))); assertEquals("\"五十\"", access.quote("五十")); assertEquals("五十", access.unquote(access.quote("五十"))); + assertThrows(IllegalArgumentException.class, () -> access.quote("")); + assertThrows(IllegalArgumentException.class, () -> access.unquote("")); } private static String unescape(String s) { From 77072042ac2a2e5ef710dd9ecbd43689aab854a6 Mon Sep 17 00:00:00 2001 From: Keith Turner Date: Mon, 12 Jan 2026 23:31:22 +0000 Subject: [PATCH 33/33] fix specification test --- .../accumulo/access/specification/AccessExpression.abnf | 5 ++--- .../apache/accumulo/access/tests/AccessExpressionTest.java | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/src/main/resources/org/apache/accumulo/access/specification/AccessExpression.abnf b/core/src/main/resources/org/apache/accumulo/access/specification/AccessExpression.abnf index 30f45b3..f2aae57 100644 --- a/core/src/main/resources/org/apache/accumulo/access/specification/AccessExpression.abnf +++ b/core/src/main/resources/org/apache/accumulo/access/specification/AccessExpression.abnf @@ -28,9 +28,8 @@ and-expression = "&" (access-token / paren-expression) [and-expression or-expression = "|" (access-token / paren-expression) [or-expression] access-token = 1*( ALPHA / DIGIT / "_" / "-" / "." / ":" / slash ) -access-token =/ DQUOTE 1*(utf8-subset / escaped) DQUOTE +access-token =/ DQUOTE 1*(unicode-subset / escaped) DQUOTE -utf8-subset = %x20-21 / %x23-5B / %x5D-7E / unicode-beyond-ascii ; utf8 minus '"' and '\' -unicode-beyond-ascii = %x0080-D7FF / %xE000-10FFFF +unicode-subset = %x00-21 / %x23-5B / %x5D-7F / unicode-beyond-ascii ; unicode minus '"' and '\' escaped = "\" DQUOTE / "\\" slash = "/" diff --git a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java index 5fa1f1e..55abca3 100644 --- a/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java +++ b/core/src/test/java/org/apache/accumulo/access/tests/AccessExpressionTest.java @@ -41,7 +41,6 @@ import org.apache.accumulo.access.AuthorizationValidator; import org.apache.accumulo.access.InvalidAccessExpressionException; import org.apache.accumulo.access.ParsedAccessExpression; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -158,7 +157,6 @@ public void testEqualsHashcode() { assertNotEquals(ae2.hashCode(), ae4.hashCode()); } - @Disabled @Test public void testSpecificationDocumentation() throws IOException, URISyntaxException { // verify AccessExpression.abnf matches what is documented in SPECIFICATION.md