blob: 5619701402aa21b7a429f1a22fb591bf1ae6f2b9 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.accumulo.access;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.apache.accumulo.access.ByteUtils.BACKSLASH;
import static org.apache.accumulo.access.ByteUtils.QUOTE;
import static org.apache.accumulo.access.ByteUtils.isQuoteOrSlash;
import static org.apache.accumulo.access.ByteUtils.isQuoteSymbol;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
//this class is intentionally package private and should never be made public
class AccessEvaluatorImpl implements AccessEvaluator {
private final Collection<Predicate<BytesWrapper>> authorizedPredicates;
private static final byte[] EMPTY = new byte[0];
private final ThreadLocal<BytesWrapper> lookupWrappers =
ThreadLocal.withInitial(() -> new BytesWrapper(EMPTY));
private final ThreadLocal<Tokenizer> tokenizers =
ThreadLocal.withInitial(() -> new Tokenizer(EMPTY));
private AccessEvaluatorImpl(Authorizer authorizationChecker) {
this.authorizedPredicates = List.of(auth -> authorizationChecker.isAuthorized(unescape(auth)));
}
public AccessEvaluatorImpl(Collection<List<byte[]>> authorizationSets) {
authorizedPredicates = authorizationSets.stream()
.map(authorizations -> authorizations.stream()
.map(auth -> AccessEvaluatorImpl.escape(auth, false)).map(BytesWrapper::new)
.collect(toSet()))
.map(escapedAuths -> (Predicate<BytesWrapper>) escapedAuths::contains)
.collect(Collectors.toUnmodifiableList());
}
static String unescape(BytesWrapper auth) {
int escapeCharCount = 0;
for (int i = 0; i < auth.length(); i++) {
byte b = auth.byteAt(i);
if (isQuoteOrSlash(b)) {
escapeCharCount++;
}
}
if (escapeCharCount > 0) {
if (escapeCharCount % 2 == 1) {
throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth);
}
byte[] unescapedCopy = new byte[auth.length() - escapeCharCount / 2];
int pos = 0;
for (int i = 0; i < auth.length(); i++) {
byte b = auth.byteAt(i);
if (b == BACKSLASH) {
i++;
b = auth.byteAt(i);
if (!isQuoteOrSlash(b)) {
throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth);
}
} else if (isQuoteSymbol(b)) {
// should only see quote after a slash
throw new IllegalArgumentException(
"Illegal character after slash in auth String : " + auth);
}
unescapedCopy[pos++] = b;
}
return new String(unescapedCopy, UTF_8);
} else {
return auth.toString();
}
}
/**
* Properly escapes an authorization string. The string can be quoted if desired.
*
* @param auth authorization string, as UTF-8 encoded bytes
* @param shouldQuote true to wrap escaped authorization in quotes
* @return escaped authorization string
*/
static byte[] escape(byte[] auth, boolean shouldQuote) {
int escapeCount = 0;
for (byte value : auth) {
if (isQuoteOrSlash(value)) {
escapeCount++;
}
}
if (escapeCount > 0 || shouldQuote) {
byte[] escapedAuth = new byte[auth.length + escapeCount + (shouldQuote ? 2 : 0)];
int index = shouldQuote ? 1 : 0;
for (byte b : auth) {
if (isQuoteOrSlash(b)) {
escapedAuth[index++] = BACKSLASH;
}
escapedAuth[index++] = b;
}
if (shouldQuote) {
escapedAuth[0] = QUOTE;
escapedAuth[escapedAuth.length - 1] = QUOTE;
}
auth = escapedAuth;
}
return auth;
}
@Override
public boolean canAccess(AccessExpression expression) {
return canAccess(expression.getExpression());
}
@Override
public boolean canAccess(String expression) throws IllegalAccessExpressionException {
return evaluate(expression.getBytes(UTF_8));
}
@Override
public boolean canAccess(byte[] expression) throws IllegalAccessExpressionException {
return evaluate(expression);
}
boolean evaluate(byte[] accessExpression) throws IllegalAccessExpressionException {
var bytesWrapper = lookupWrappers.get();
for (var auths : authorizedPredicates) {
var tokenizer = tokenizers.get();
tokenizer.reset(accessExpression);
Predicate<Tokenizer.AuthorizationToken> atp = authToken -> {
bytesWrapper.set(authToken.data, authToken.start, authToken.len);
return auths.test(bytesWrapper);
};
if (!ParserEvaluator.parseAccessExpression(tokenizer, atp, authToken -> true)) {
return false;
}
}
return true;
}
private static class BuilderImpl implements AuthorizationsBuilder, EvaluatorBuilder {
private Authorizer authorizationsChecker;
private Collection<List<byte[]>> authorizationSets;
private void setAuthorizations(List<byte[]> auths) {
setAuthorizations(Collections.singletonList(auths));
}
private void setAuthorizations(Collection<List<byte[]>> authSets) {
if (authorizationsChecker != null) {
throw new IllegalStateException("Cannot set checker and authorizations");
}
for (List<byte[]> auths : authSets) {
for (byte[] auth : auths) {
if (auth.length == 0) {
throw new IllegalArgumentException("Empty authorization");
}
}
}
this.authorizationSets = authSets;
}
@Override
public EvaluatorBuilder authorizations(Authorizations authorizations) {
setAuthorizations(authorizations.asSet().stream().map(auth -> auth.getBytes(UTF_8))
.collect(toUnmodifiableList()));
return this;
}
@Override
public EvaluatorBuilder authorizations(Collection<Authorizations> authorizationSets) {
setAuthorizations(authorizationSets
.stream().map(authorizations -> authorizations.asSet().stream()
.map(auth -> auth.getBytes(UTF_8)).collect(toUnmodifiableList()))
.collect(Collectors.toList()));
return this;
}
@Override
public EvaluatorBuilder authorizations(String... authorizations) {
setAuthorizations(Stream.of(authorizations).map(auth -> auth.getBytes(UTF_8))
.collect(toUnmodifiableList()));
return this;
}
@Override
public EvaluatorBuilder authorizations(Authorizer authorizationChecker) {
if (authorizationSets != null) {
throw new IllegalStateException("Cannot set checker and authorizations");
}
this.authorizationsChecker = authorizationChecker;
return this;
}
@Override
public AccessEvaluator build() {
if (authorizationSets != null ^ authorizationsChecker == null) {
throw new IllegalStateException(
"Exactly one of authorizationSets or authorizationsChecker must be set, not both or none.");
}
AccessEvaluator accessEvaluator;
if (authorizationsChecker != null) {
accessEvaluator = new AccessEvaluatorImpl(authorizationsChecker);
} else {
accessEvaluator = new AccessEvaluatorImpl(authorizationSets);
}
return accessEvaluator;
}
}
public static AuthorizationsBuilder builder() {
return new BuilderImpl();
}
}