| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.accumulo.access; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import java.io.Serializable; |
| import java.util.Arrays; |
| import java.util.function.Consumer; |
| import java.util.function.Predicate; |
| |
| /** |
| * This class offers the ability to operate on access expressions. |
| * |
| * <p> |
| * Below is an example of how to use this API. |
| * |
| * <pre> |
| * {@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)); |
| * |
| * } |
| * </pre> |
| * |
| * The above example will print the following. |
| * |
| * <pre> |
| * {@code |
| * (CAT&"🦖")|(CAT&"🦕") |
| * (CAT&"🦖")|(CAT&"🦕") |
| * CAT |
| * 🦖 |
| * CAT |
| * 🦕 |
| * type:OR child[0]:CAT&"🦖" child[1]:CAT&"🦕" |
| * } |
| * </pre> |
| * |
| * The following code will throw an {@link InvalidAccessExpressionException} because the expression |
| * is not valid. |
| * |
| * <pre> |
| * {@code |
| * AccessExpression.validate("A&B|C"); |
| * } |
| * </pre> |
| * |
| * <p> |
| * Instances of this class are thread-safe. |
| * |
| * <p> |
| * Note: The underlying implementation uses UTF-8 when converting between bytes and Strings. |
| * |
| * @see <a href="https://github.com/apache/accumulo-access">Accumulo Access Documentation</a> |
| * @since 1.0.0 |
| */ |
| public abstract class AccessExpression implements Serializable { |
| /* |
| * This is package private so that it can not be extended by classes outside of this package and |
| * create a mutable implementation. In this package all implementations that extends are |
| * immutable. |
| */ |
| AccessExpression() {} |
| |
| /** |
| * @return the expression that was used to create this object. |
| */ |
| public abstract String getExpression(); |
| |
| /** |
| * 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 |
| * 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. |
| */ |
| public abstract ParsedAccessExpression parse(); |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof AccessExpression) { |
| return ((AccessExpression) o).getExpression().equals(getExpression()); |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return getExpression().hashCode(); |
| } |
| |
| @Override |
| 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<Tokenizer.AuthorizationToken> 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. |
| * |
| * <p> |
| * 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. |
| * </p> |
| * |
| * @throws InvalidAccessExpressionException when the expression is not valid. |
| * @throws NullPointerException when any argument is null |
| */ |
| public static void findAuthorizations(String expression, Consumer<String> authorizationConsumer) |
| throws InvalidAccessExpressionException { |
| findAuthorizations(expression.getBytes(UTF_8), authorizationConsumer); |
| } |
| |
| /** |
| * @see #findAuthorizations(String, Consumer) |
| */ |
| public static void findAuthorizations(byte[] expression, Consumer<String> authorizationConsumer) |
| throws InvalidAccessExpressionException { |
| var bytesWrapper = ParserEvaluator.lookupWrappers.get(); |
| Predicate<Tokenizer.AuthorizationToken> atp = authToken -> { |
| bytesWrapper.set(authToken.data, authToken.start, authToken.len); |
| authorizationConsumer.accept(AccessEvaluatorImpl.unescape(bytesWrapper)); |
| return true; |
| }; |
| ParserEvaluator.parseAccessExpression(expression, atp, atp); |
| } |
| |
| /** |
| * Authorizations occurring in an access expression can only contain the characters listed in the |
| * <a href= |
| * "https://github.com/apache/accumulo-access/blob/main/SPECIFICATION.md">specification</a> 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 |
| * <a href= |
| * "https://github.com/apache/accumulo-access/blob/main/SPECIFICATION.md">specification</a> 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 an 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; |
| } |
| } |
| } |