xds, rbac: implement rbac engine (#8168)

This commit is contained in:
yifeizhuang 2021-06-08 14:45:11 -07:00 committed by GitHub
parent d4c31ffad4
commit b7f3fddc76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1252 additions and 201 deletions

View File

@ -66,15 +66,15 @@ import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.Filter.NamedFilterConfig;
import io.grpc.xds.LoadStatsManager2.ClusterDropStats;
import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats;
import io.grpc.xds.Matchers.FractionMatcher;
import io.grpc.xds.Matchers.HeaderMatcher;
import io.grpc.xds.Matchers.PathMatcher;
import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteAction;
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.internal.Matchers.FractionMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;

View File

@ -1,171 +0,0 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.auto.value.AutoValue;
import com.google.re2j.Pattern;
import javax.annotation.Nullable;
/** A group of request matchers. */
final class Matchers {
private Matchers() {}
/** Matcher for HTTP request path. */
@AutoValue
abstract static class PathMatcher {
// Exact full path to be matched.
@Nullable
abstract String path();
// Path prefix to be matched.
@Nullable
abstract String prefix();
// Regular expression pattern of the path to be matched.
@Nullable
abstract Pattern regEx();
// Whether case sensitivity is taken into account for matching.
// Only valid for full path matching or prefix matching.
abstract boolean caseSensitive();
static PathMatcher fromPath(String path, boolean caseSensitive) {
checkNotNull(path, "path");
return PathMatcher.create(path, null, null, caseSensitive);
}
static PathMatcher fromPrefix(String prefix, boolean caseSensitive) {
checkNotNull(prefix, "prefix");
return PathMatcher.create(null, prefix, null, caseSensitive);
}
static PathMatcher fromRegEx(Pattern regEx) {
checkNotNull(regEx, "regEx");
return PathMatcher.create(null, null, regEx, false /* doesn't matter */);
}
private static PathMatcher create(@Nullable String path, @Nullable String prefix,
@Nullable Pattern regEx, boolean caseSensitive) {
return new AutoValue_Matchers_PathMatcher(path, prefix, regEx, caseSensitive);
}
}
/** Matcher for HTTP request headers. */
@AutoValue
abstract static class HeaderMatcher {
// Name of the header to be matched.
abstract String name();
// Matches exact header value.
@Nullable
abstract String exactValue();
// Matches header value with the regular expression pattern.
@Nullable
abstract Pattern safeRegEx();
// Matches header value an integer value in the range.
@Nullable
abstract Range range();
// Matches header presence.
@Nullable
abstract Boolean present();
// Matches header value with the prefix.
@Nullable
abstract String prefix();
// Matches header value with the suffix.
@Nullable
abstract String suffix();
// Whether the matching semantics is inverted. E.g., present && !inverted -> !present
abstract boolean inverted();
static HeaderMatcher forExactValue(String name, String exactValue, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(exactValue, "exactValue");
return HeaderMatcher.create(name, exactValue, null, null, null, null, null, inverted);
}
static HeaderMatcher forSafeRegEx(String name, Pattern safeRegEx, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(safeRegEx, "safeRegEx");
return HeaderMatcher.create(name, null, safeRegEx, null, null, null, null, inverted);
}
static HeaderMatcher forRange(String name, Range range, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(range, "range");
return HeaderMatcher.create(name, null, null, range, null, null, null, inverted);
}
static HeaderMatcher forPresent(String name, boolean present, boolean inverted) {
checkNotNull(name, "name");
return HeaderMatcher.create(name, null, null, null, present, null, null, inverted);
}
static HeaderMatcher forPrefix(String name, String prefix, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(prefix, "prefix");
return HeaderMatcher.create(name, null, null, null, null, prefix, null, inverted);
}
static HeaderMatcher forSuffix(String name, String suffix, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(suffix, "suffix");
return HeaderMatcher.create(name, null, null, null, null, null, suffix, inverted);
}
private static HeaderMatcher create(String name, @Nullable String exactValue,
@Nullable Pattern safeRegEx, @Nullable Range range,
@Nullable Boolean present, @Nullable String prefix,
@Nullable String suffix, boolean inverted) {
checkNotNull(name, "name");
return new AutoValue_Matchers_HeaderMatcher(name, exactValue, safeRegEx, range, present,
prefix, suffix, inverted);
}
/** Represents an integer range. */
@AutoValue
abstract static class Range {
abstract long start();
abstract long end();
static Range create(long start, long end) {
return new AutoValue_Matchers_HeaderMatcher_Range(start, end);
}
}
}
/** Represents a fractional value. */
@AutoValue
abstract static class FractionMatcher {
abstract int numerator();
abstract int denominator();
static FractionMatcher create(int numerator, int denominator) {
return new AutoValue_Matchers_FractionMatcher(numerator, denominator);
}
}
}

View File

@ -25,9 +25,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.re2j.Pattern;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.Matchers.FractionMatcher;
import io.grpc.xds.Matchers.HeaderMatcher;
import io.grpc.xds.Matchers.PathMatcher;
import io.grpc.xds.internal.Matchers.FractionMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -90,6 +89,47 @@ abstract class VirtualHost {
return new AutoValue_VirtualHost_Route_RouteMatch(pathMatcher,
ImmutableList.copyOf(headerMatchers), fractionMatcher);
}
/** Matcher for HTTP request path. */
@AutoValue
abstract static class PathMatcher {
// Exact full path to be matched.
@Nullable
abstract String path();
// Path prefix to be matched.
@Nullable
abstract String prefix();
// Regular expression pattern of the path to be matched.
@Nullable
abstract Pattern regEx();
// Whether case sensitivity is taken into account for matching.
// Only valid for full path matching or prefix matching.
abstract boolean caseSensitive();
static PathMatcher fromPath(String path, boolean caseSensitive) {
checkNotNull(path, "path");
return create(path, null, null, caseSensitive);
}
static PathMatcher fromPrefix(String prefix, boolean caseSensitive) {
checkNotNull(prefix, "prefix");
return create(null, prefix, null, caseSensitive);
}
static PathMatcher fromRegEx(Pattern regEx) {
checkNotNull(regEx, "regEx");
return create(null, null, regEx, false /* doesn't matter */);
}
private static PathMatcher create(@Nullable String path, @Nullable String prefix,
@Nullable Pattern regEx, boolean caseSensitive) {
return new AutoValue_VirtualHost_Route_RouteMatch_PathMatcher(path, prefix, regEx,
caseSensitive);
}
}
}
@AutoValue

View File

@ -30,12 +30,12 @@ import io.grpc.internal.SharedResourceHolder;
import io.grpc.xds.EnvoyServerProtoData.CidrRange;
import io.grpc.xds.EnvoyServerProtoData.FilterChain;
import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch;
import io.grpc.xds.internal.Matchers.CidrMatcher;
import io.grpc.xds.internal.sds.SslContextProviderSupplier;
import io.netty.channel.Channel;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.math.BigInteger;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@ -338,20 +338,8 @@ public final class XdsClientWrapperForServerSds {
return filtered;
}
private static boolean isCidrMatching(byte[] cidrBytes, byte[] addressBytes, int prefixLen) {
BigInteger cidrInt = new BigInteger(cidrBytes);
BigInteger addrInt = new BigInteger(addressBytes);
int shiftAmount = 8 * cidrBytes.length - prefixLen;
cidrInt = cidrInt.shiftRight(shiftAmount);
addrInt = addrInt.shiftRight(shiftAmount);
return cidrInt.equals(addrInt);
}
private static int getMatchingPrefixLength(
FilterChainMatch filterChainMatch, InetAddress address, boolean forDestination) {
byte[] addressBytes = address.getAddress();
boolean isIPv6 = address instanceof Inet6Address;
List<CidrRange> cidrRanges =
forDestination
@ -366,10 +354,9 @@ public final class XdsClientWrapperForServerSds {
InetAddress cidrAddr = cidrRange.getAddressPrefix();
boolean cidrIsIpv6 = cidrAddr instanceof Inet6Address;
if (isIPv6 == cidrIsIpv6) {
byte[] cidrBytes = cidrAddr.getAddress();
int prefixLen = cidrRange.getPrefixLen();
if (isCidrMatching(cidrBytes, addressBytes, prefixLen)
&& prefixLen > matchingPrefixLength) {
CidrMatcher matcher = CidrMatcher.create(cidrAddr, prefixLen);
if (matcher.matches(address) && prefixLen > matchingPrefixLength) {
matchingPrefixLength = prefixLen;
}
}

View File

@ -46,15 +46,13 @@ import io.grpc.internal.ObjectPool;
import io.grpc.xds.Filter.ClientInterceptorBuilder;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.Filter.NamedFilterConfig;
import io.grpc.xds.Matchers.FractionMatcher;
import io.grpc.xds.Matchers.HeaderMatcher;
import io.grpc.xds.Matchers.PathMatcher;
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteAction;
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.LdsResourceWatcher;
import io.grpc.xds.XdsClient.LdsUpdate;
import io.grpc.xds.XdsClient.RdsResourceWatcher;
@ -62,6 +60,8 @@ import io.grpc.xds.XdsClient.RdsUpdate;
import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory;
import io.grpc.xds.internal.Matchers.FractionMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -577,6 +577,7 @@ final class XdsNameResolver extends NameResolver {
return pathMatcher.regEx().matches(fullMethodName);
}
// TODO(zivy): consider reuse Matchers.HeaderMatcher.matches()
private static boolean matchHeader(HeaderMatcher headerMatcher, @Nullable String value) {
if (headerMatcher.present() != null) {
return (value == null) == headerMatcher.present().equals(headerMatcher.inverted());

View File

@ -0,0 +1,299 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.auto.value.AutoValue;
import com.google.re2j.Pattern;
import java.math.BigInteger;
import java.net.InetAddress;
import javax.annotation.Nullable;
/**
* Provides a group of request matchers. A matcher evaluates an input and tells whether certain
* argument in the input matches a predefined matching pattern.
*/
public final class Matchers {
/** Matcher for HTTP request headers. */
@AutoValue
public abstract static class HeaderMatcher {
// Name of the header to be matched.
public abstract String name();
// Matches exact header value.
@Nullable
public abstract String exactValue();
// Matches header value with the regular expression pattern.
@Nullable
public abstract Pattern safeRegEx();
// Matches header value an integer value in the range.
@Nullable
public abstract Range range();
// Matches header presence.
@Nullable
public abstract Boolean present();
// Matches header value with the prefix.
@Nullable
public abstract String prefix();
// Matches header value with the suffix.
@Nullable
public abstract String suffix();
// Whether the matching semantics is inverted. E.g., present && !inverted -> !present
public abstract boolean inverted();
/** The request header value should exactly match the specified value. */
public static HeaderMatcher forExactValue(String name, String exactValue, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(exactValue, "exactValue");
return HeaderMatcher.create(name, exactValue, null, null, null, null, null, inverted);
}
/** The request header value should match the regular expression pattern. */
public static HeaderMatcher forSafeRegEx(String name, Pattern safeRegEx, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(safeRegEx, "safeRegEx");
return HeaderMatcher.create(name, null, safeRegEx, null, null, null, null, inverted);
}
/** The request header value should be within the range. */
public static HeaderMatcher forRange(String name, Range range, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(range, "range");
return HeaderMatcher.create(name, null, null, range, null, null, null, inverted);
}
/** The request header value should exist. */
public static HeaderMatcher forPresent(String name, boolean present, boolean inverted) {
checkNotNull(name, "name");
return HeaderMatcher.create(name, null, null, null, present, null, null, inverted);
}
/** The request header value should have this prefix. */
public static HeaderMatcher forPrefix(String name, String prefix, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(prefix, "prefix");
return HeaderMatcher.create(name, null, null, null, null, prefix, null, inverted);
}
/** The request header value should have this suffix. */
public static HeaderMatcher forSuffix(String name, String suffix, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(suffix, "suffix");
return HeaderMatcher.create(name, null, null, null, null, null, suffix, inverted);
}
private static HeaderMatcher create(String name, @Nullable String exactValue,
@Nullable Pattern safeRegEx, @Nullable Range range,
@Nullable Boolean present, @Nullable String prefix,
@Nullable String suffix, boolean inverted) {
checkNotNull(name, "name");
return new AutoValue_Matchers_HeaderMatcher(name, exactValue, safeRegEx, range, present,
prefix, suffix, inverted);
}
/** Returns the matching result. */
public boolean matches(@Nullable String value) {
if (present() != null) {
return (value == null) == present().equals(inverted());
}
// FIXME(zivy@): invert result for null value.
// https://github.com/envoyproxy/envoy/blob/0fae6970ddaf93f024908ba304bbd2b34e997a51/source/common/http/header_utility.cc#L130
if (value == null) {
return false;
}
boolean baseMatch;
if (exactValue() != null) {
baseMatch = exactValue().equals(value);
} else if (safeRegEx() != null) {
baseMatch = safeRegEx().matches(value);
} else if (range() != null) {
long numValue;
try {
numValue = Long.parseLong(value);
baseMatch = numValue >= range().start()
&& numValue <= range().end();
} catch (NumberFormatException ignored) {
baseMatch = false;
}
} else if (prefix() != null) {
baseMatch = value.startsWith(prefix());
} else {
baseMatch = value.endsWith(suffix());
}
return baseMatch != inverted();
}
/** Represents an integer range. */
@AutoValue
public abstract static class Range {
public abstract long start();
public abstract long end();
public static Range create(long start, long end) {
return new AutoValue_Matchers_HeaderMatcher_Range(start, end);
}
}
}
/** Represents a fractional value. */
@AutoValue
public abstract static class FractionMatcher {
public abstract int numerator();
public abstract int denominator();
public static FractionMatcher create(int numerator, int denominator) {
return new AutoValue_Matchers_FractionMatcher(numerator, denominator);
}
}
/** Represents various ways to match a string .*/
@AutoValue
public abstract static class StringMatcher {
@Nullable
abstract String exact();
// The input string has this prefix.
@Nullable
abstract String prefix();
// The input string has this suffix.
@Nullable
abstract String suffix();
// The input string matches the regular expression.
@Nullable
abstract Pattern regEx();
// The input string has this substring.
@Nullable
abstract String contains();
// If true, exact/prefix/suffix matching should be case insensitive.
abstract boolean ignoreCase();
/** The input string should exactly matches the specified string. */
public static StringMatcher forExact(String exact, boolean ignoreCase) {
checkNotNull(exact, "exact");
return StringMatcher.create(exact, null, null, null, null,
ignoreCase);
}
/** The input string should have the prefix. */
public static StringMatcher forPrefix(String prefix, boolean ignoreCase) {
checkNotNull(prefix, "prefix");
return StringMatcher.create(null, prefix, null, null, null,
ignoreCase);
}
/** The input string should have the suffix. */
public static StringMatcher forSuffix(String suffix, boolean ignoreCase) {
checkNotNull(suffix, "suffix");
return StringMatcher.create(null, null, suffix, null, null,
ignoreCase);
}
/** The input string should match this pattern. */
public static StringMatcher forSafeRegEx(Pattern regEx) {
checkNotNull(regEx, "regEx");
return StringMatcher.create(null, null, null, regEx, null,
false/* doesn't matter */);
}
/** The input string should contain this substring. */
public static StringMatcher forContains(String contains) {
checkNotNull(contains, "contains");
return StringMatcher.create(null, null, null, null, contains,
false/* doesn't matter */);
}
/** Returns the matching result for this string. */
public boolean matches(String args) {
if (args == null) {
return false;
}
if (exact() != null) {
return ignoreCase()
? exact().equalsIgnoreCase(args)
: exact().equals(args);
} else if (prefix() != null) {
return ignoreCase()
? args.toLowerCase().startsWith(prefix().toLowerCase())
: args.startsWith(prefix());
} else if (suffix() != null) {
return ignoreCase()
? args.toLowerCase().endsWith(suffix().toLowerCase())
: args.endsWith(suffix());
} else if (contains() != null) {
return args.contains(contains());
}
return regEx().matches(args);
}
private static StringMatcher create(@Nullable String exact, @Nullable String prefix,
@Nullable String suffix, @Nullable Pattern regEx, @Nullable String contains,
boolean ignoreCase) {
return new AutoValue_Matchers_StringMatcher(exact, prefix, suffix, regEx, contains,
ignoreCase);
}
}
/** Matcher to evaluate whether an IPv4 or IPv6 address is within a CIDR range. */
@AutoValue
public abstract static class CidrMatcher {
abstract InetAddress addressPrefix();
abstract int prefixLen();
/** Returns matching result for this address. */
public boolean matches(InetAddress address) {
if (address == null) {
return false;
}
byte[] cidr = addressPrefix().getAddress();
byte[] addr = address.getAddress();
if (addr.length != cidr.length) {
return false;
}
BigInteger cidrInt = new BigInteger(cidr);
BigInteger addrInt = new BigInteger(addr);
int shiftAmount = 8 * cidr.length - prefixLen();
cidrInt = cidrInt.shiftRight(shiftAmount);
addrInt = addrInt.shiftRight(shiftAmount);
return cidrInt.equals(addrInt);
}
/** Constructs a CidrMatcher with this prefix and prefix length.
* Do not provide string addressPrefix constructor to avoid IO exception handling.
* */
public static CidrMatcher create(InetAddress addressPrefix, int prefixLen) {
return new AutoValue_Matchers_CidrMatcher(addressPrefix, prefixLen);
}
}
}

View File

@ -0,0 +1,415 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds.internal.rbac.engine;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import io.grpc.Grpc;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.xds.internal.Matchers;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
/**
* Implementation of gRPC server access control based on envoy RBAC protocol:
* https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto
*
* <p>One GrpcAuthorizationEngine is initialized with one action type and a list of policies.
* Policies are examined sequentially in order in an any match fashion, and the first matched policy
* will be returned. If not matched at all, the opposite action type is returned as a result.
*/
public final class GrpcAuthorizationEngine {
private static final Logger log = Logger.getLogger(GrpcAuthorizationEngine.class.getName());
private final AuthConfig authConfig;
/** Instantiated with envoy policyMatcher configuration. */
public GrpcAuthorizationEngine(AuthConfig authConfig) {
this.authConfig = authConfig;
}
/** Return the auth decision for the request argument against the policies. */
public AuthDecision evaluate(Metadata metadata, ServerCall<?,?> serverCall) {
checkNotNull(metadata, "metadata");
checkNotNull(serverCall, "serverCall");
String firstMatch = null;
EvaluateArgs args = new EvaluateArgs(metadata, serverCall);
for (PolicyMatcher policyMatcher : authConfig.policies) {
if (policyMatcher.matches(args)) {
firstMatch = policyMatcher.name;
break;
}
}
Action decisionType = Action.DENY;
if (Action.DENY.equals(authConfig.action) == (firstMatch == null)) {
decisionType = Action.ALLOW;
}
log.log(Level.FINER, "RBAC decision: {0}, policy match: {1}.",
new Object[]{decisionType, firstMatch});
return AuthDecision.create(decisionType, firstMatch);
}
public enum Action {
ALLOW,
DENY,
}
/**
* An authorization decision provides information about the decision type and the policy name
* identifier based on the authorization engine evaluation. */
@AutoValue
public abstract static class AuthDecision {
public abstract Action decision();
@Nullable
public abstract String matchingPolicyName();
static AuthDecision create(Action decisionType, @Nullable String matchingPolicy) {
return new AutoValue_GrpcAuthorizationEngine_AuthDecision(decisionType, matchingPolicy);
}
}
/** Represents authorization config policy that the engine will evaluate against. */
public static final class AuthConfig {
private final List<PolicyMatcher> policies;
private final Action action;
public AuthConfig(List<PolicyMatcher> policies, Action action) {
this.policies = Collections.unmodifiableList(new ArrayList<>(policies));
this.action = action;
}
}
/**
* Implements a top level {@link Matcher} for a single RBAC policy configuration per envoy
* protocol:
* https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#config-rbac-v3-policy.
*
* <p>Currently we only support matching some of the request fields. Those unsupported fields are
* considered not match until we stop ignoring them.
*/
public static final class PolicyMatcher implements Matcher {
private final OrMatcher permissions;
private final OrMatcher principals;
private final String name;
/** Constructs a matcher for one RBAC policy. */
public PolicyMatcher(String name, OrMatcher permissions, OrMatcher principals) {
this.name = name;
this.permissions = permissions;
this.principals = principals;
}
@Override
public boolean matches(EvaluateArgs args) {
return permissions.matches(args) && principals.matches(args);
}
}
public static final class AuthenticatedMatcher implements Matcher {
private final Matchers.StringMatcher delegate;
/**
* Passing in null will match all authenticated user, i.e. SSL session is present.
* https://github.com/envoyproxy/envoy/blob/main/api/envoy/config/rbac/v3/rbac.proto#L240
* */
public AuthenticatedMatcher(@Nullable Matchers.StringMatcher delegate) {
this.delegate = delegate;
}
@Override
public boolean matches(EvaluateArgs args) {
Collection<String> principalNames = args.getPrincipalNames();
log.log(Level.FINER, "Matching principal names: {0}", new Object[]{principalNames});
// Null means unauthenticated connection.
if (principalNames == null) {
return false;
}
// Connection is authenticated, so returns match when delegated string matcher is not present.
if (delegate == null) {
return true;
}
for (String name : principalNames) {
if (delegate.matches(name)) {
return true;
}
}
return false;
}
}
public static final class DestinationIpMatcher implements Matcher {
private final Matchers.CidrMatcher delegate;
public DestinationIpMatcher(Matchers.CidrMatcher delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate.matches(args.getDestinationIp());
}
}
public static final class SourceIpMatcher implements Matcher {
private final Matchers.CidrMatcher delegate;
public SourceIpMatcher(Matchers.CidrMatcher delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate.matches(args.getSourceIp());
}
}
public static final class PathMatcher implements Matcher {
private final Matchers.StringMatcher delegate;
public PathMatcher(Matchers.StringMatcher delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate.matches(args.getPath());
}
}
public static final class HeaderMatcher implements Matcher {
private final Matchers.HeaderMatcher delegate;
public HeaderMatcher(Matchers.HeaderMatcher delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate.matches(args.getHeader(delegate.name()));
}
}
public static final class DestinationPortMatcher implements Matcher {
private final int port;
public DestinationPortMatcher(int port) {
this.port = port;
}
@Override
public boolean matches(EvaluateArgs args) {
return port == args.getDestinationPort();
}
}
private static final class EvaluateArgs {
private final Metadata metadata;
private final ServerCall<?,?> serverCall;
// https://github.com/envoyproxy/envoy/blob/63619d578e1abe0c1725ea28ba02f361466662e1/api/envoy/config/rbac/v3/rbac.proto#L238-L240
private static final int URI_SAN = 6;
private static final int DNS_SAN = 2;
private EvaluateArgs(Metadata metadata, ServerCall<?,?> serverCall) {
this.metadata = metadata;
this.serverCall = serverCall;
}
private String getPath() {
return "/" + serverCall.getMethodDescriptor().getFullMethodName();
}
/**
* Returns null for unauthenticated connection.
* Returns empty string collection if no valid certificate and no
* principal names we are interested in.
* https://github.com/envoyproxy/envoy/blob/0fae6970ddaf93f024908ba304bbd2b34e997a51/envoy/ssl/connection.h#L70
*/
private Collection<String> getPrincipalNames() {
SSLSession sslSession = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION);
if (sslSession == null) {
return null;
}
try {
Certificate[] certs = sslSession.getPeerCertificates();
if (certs == null || certs.length < 1) {
return Collections.singleton("");
}
X509Certificate cert = (X509Certificate)certs[0];
if (cert == null) {
return Collections.singleton("");
}
Collection<List<?>> names = cert.getSubjectAlternativeNames();
List<String> principalNames = new ArrayList<>();
if (names != null) {
for (List<?> name : names) {
if (URI_SAN == (Integer) name.get(0)) {
principalNames.add((String) name.get(1));
}
}
if (!principalNames.isEmpty()) {
return Collections.unmodifiableCollection(principalNames);
}
for (List<?> name : names) {
if (DNS_SAN == (Integer) name.get(0)) {
principalNames.add((String) name.get(1));
}
}
if (!principalNames.isEmpty()) {
return Collections.unmodifiableCollection(principalNames);
}
}
if (cert.getSubjectDN() == null || cert.getSubjectDN().getName() == null) {
return Collections.singleton("");
}
return Collections.singleton(cert.getSubjectDN().getName());
} catch (SSLPeerUnverifiedException | CertificateParsingException ex) {
log.log(Level.FINE, "Unexpected getPrincipalNames error.", ex);
return Collections.singleton("");
}
}
@Nullable
private String getHeader(String headerName) {
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
return null;
}
Metadata.Key<String> key;
try {
key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
} catch (IllegalArgumentException e) {
return null;
}
Iterable<String> values = metadata.getAll(key);
return values == null ? null : Joiner.on(",").join(values);
}
private InetAddress getDestinationIp() {
SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
return addr == null ? null : ((InetSocketAddress) addr).getAddress();
}
private InetAddress getSourceIp() {
SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
return addr == null ? null : ((InetSocketAddress) addr).getAddress();
}
private int getDestinationPort() {
SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
return addr == null ? -1 : ((InetSocketAddress) addr).getPort();
}
}
interface Matcher {
boolean matches(EvaluateArgs args);
}
public static final class OrMatcher implements Matcher {
private final List<? extends Matcher> anyMatch;
/** Matches when any of the matcher matches. */
public OrMatcher(List<? extends Matcher> matchers) {
checkNotNull(matchers, "matchers");
for (Matcher matcher : matchers) {
checkNotNull(matcher, "matcher");
}
this.anyMatch = Collections.unmodifiableList(new ArrayList<>(matchers));
}
public static OrMatcher create(Matcher...matchers) {
return new OrMatcher(Arrays.asList(matchers));
}
@Override
public boolean matches(EvaluateArgs args) {
for (Matcher m : anyMatch) {
if (m.matches(args)) {
return true;
}
}
return false;
}
}
public static final class AndMatcher implements Matcher {
private final List<? extends Matcher> allMatch;
/** Matches when all of the matchers match. */
public AndMatcher(List<? extends Matcher> matchers) {
checkNotNull(matchers, "matchers");
for (Matcher matcher : matchers) {
checkNotNull(matcher, "matcher");
}
this.allMatch = Collections.unmodifiableList(new ArrayList<>(matchers));
}
public static AndMatcher create(Matcher...matchers) {
return new AndMatcher(Arrays.asList(matchers));
}
@Override
public boolean matches(EvaluateArgs args) {
for (Matcher m : allMatch) {
if (!m.matches(args)) {
return false;
}
}
return true;
}
}
/** Always true matcher.*/
public static final class AlwaysTrueMatcher implements Matcher {
static AlwaysTrueMatcher INSTANCE = new AlwaysTrueMatcher();
@Override
public boolean matches(EvaluateArgs args) {
return true;
}
}
/** Negate matcher.*/
public static final class InvertMatcher implements Matcher {
private final Matcher toInvertMatcher;
public InvertMatcher(Matcher matcher) {
this.toInvertMatcher = checkNotNull(matcher, "matcher");
}
@Override
public boolean matches(EvaluateArgs args) {
return !toInvertMatcher.matches(args);
}
}
}

View File

@ -73,15 +73,15 @@ import io.grpc.xds.Endpoints.LbEndpoint;
import io.grpc.xds.Endpoints.LocalityLbEndpoints;
import io.grpc.xds.FaultConfig.FaultAbort;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.Matchers.FractionMatcher;
import io.grpc.xds.Matchers.HeaderMatcher;
import io.grpc.xds.Matchers.PathMatcher;
import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteAction;
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.CdsUpdate;
import io.grpc.xds.internal.Matchers.FractionMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;

View File

@ -67,14 +67,14 @@ import io.grpc.xds.FaultConfig.FaultAbort;
import io.grpc.xds.FaultConfig.FaultDelay;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.Filter.NamedFilterConfig;
import io.grpc.xds.Matchers.HeaderMatcher;
import io.grpc.xds.Matchers.PathMatcher;
import io.grpc.xds.VirtualHost.Route;
import io.grpc.xds.VirtualHost.Route.RouteAction;
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;

View File

@ -0,0 +1,176 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds.internal;
import static com.google.common.truth.Truth.assertThat;
import com.google.re2j.Pattern;
import io.grpc.xds.internal.Matchers.CidrMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher;
import io.grpc.xds.internal.Matchers.HeaderMatcher.Range;
import io.grpc.xds.internal.Matchers.StringMatcher;
import java.net.InetAddress;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(JUnit4.class)
public class MatcherTest {
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@Test
public void ipMatcher_Ipv4() throws Exception {
CidrMatcher matcher = CidrMatcher.create(InetAddress.getByName("10.10.24.10"), 20);
assertThat(matcher.matches(InetAddress.getByName("::0"))).isFalse();
assertThat(matcher.matches(InetAddress.getByName("10.10.20.0"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("10.10.16.0"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("10.10.24.10"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("10.10.31.0"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("10.10.17.0"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("10.32.20.0"))).isFalse();
assertThat(matcher.matches(InetAddress.getByName("10.10.40.0"))).isFalse();
matcher = CidrMatcher.create(InetAddress.getByName("0.0.0.0"), 20);
assertThat(matcher.matches(InetAddress.getByName("10.32.20.0"))).isFalse();
assertThat(matcher.matches(InetAddress.getByName("0.0.31.0"))).isFalse();
assertThat(matcher.matches(InetAddress.getByName("0.0.15.0"))).isTrue();
assertThat(matcher.matches(null)).isFalse();
}
@Test
public void ipMatcher_Ipv6() throws Exception {
CidrMatcher matcher = CidrMatcher.create(InetAddress.getByName("2012:00fe:d808::"), 36);
assertThat(matcher.matches(InetAddress.getByName("0.0.0.0"))).isFalse();
assertThat(matcher.matches(InetAddress.getByName("2012:00fe:d000::0"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("2012:00fe:d808::"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("2012:00fe:da81:0909:0008:4018:e930:b019")))
.isTrue();
assertThat(matcher.matches(InetAddress.getByName("2013:00fe:d000::0"))).isFalse();
}
@Test
public void stringMatcher() {
StringMatcher matcher = StringMatcher.forExact("essence", false);
assertThat(matcher.matches("elite")).isFalse();
assertThat(matcher.matches("ess")).isFalse();
assertThat(matcher.matches("")).isFalse();
assertThat(matcher.matches("essential")).isFalse();
assertThat(matcher.matches("Essence")).isFalse();
assertThat(matcher.matches("essence")).isTrue();
assertThat(matcher.matches((String)null)).isFalse();
matcher = StringMatcher.forExact("essence", true);
assertThat(matcher.matches("Essence")).isTrue();
assertThat(matcher.matches("essence")).isTrue();
matcher = StringMatcher.forExact("", true);
assertThat(matcher.matches("essence")).isFalse();
assertThat(matcher.matches("")).isTrue();
matcher = StringMatcher.forPrefix("Ess", false);
assertThat(matcher.matches("elite")).isFalse();
assertThat(matcher.matches("ess")).isFalse();
assertThat(matcher.matches("")).isFalse();
assertThat(matcher.matches("e")).isFalse();
assertThat(matcher.matches("essential")).isFalse();
assertThat(matcher.matches("Essence")).isTrue();
assertThat(matcher.matches("essence")).isFalse();
assertThat(matcher.matches((String)null)).isFalse();
matcher = StringMatcher.forPrefix("Ess", true);
assertThat(matcher.matches("esSEncE")).isTrue();
assertThat(matcher.matches("ess")).isTrue();
assertThat(matcher.matches("ES")).isFalse();
matcher = StringMatcher.forPrefix("", false);
assertThat(matcher.matches("elite")).isTrue();
matcher = StringMatcher.forSuffix("ess", false);
assertThat(matcher.matches("elite")).isFalse();
assertThat(matcher.matches("es")).isFalse();
assertThat(matcher.matches("")).isFalse();
assertThat(matcher.matches("ess")).isTrue();
assertThat(matcher.matches("Excess")).isTrue();
assertThat(matcher.matches("ExcesS")).isFalse();
assertThat(matcher.matches((String)null)).isFalse();
matcher = StringMatcher.forSuffix("ess", true);
assertThat(matcher.matches("esSEncESs")).isTrue();
assertThat(matcher.matches("ess")).isTrue();
matcher = StringMatcher.forSuffix("", true);
assertThat(matcher.matches("")).isTrue();
assertThat(matcher.matches("any")).isTrue();
matcher = StringMatcher.forContains("ess");
assertThat(matcher.matches("elite")).isFalse();
assertThat(matcher.matches("es")).isFalse();
assertThat(matcher.matches("")).isFalse();
assertThat(matcher.matches("essence")).isTrue();
assertThat(matcher.matches("eSs")).isFalse();
assertThat(matcher.matches("ExcesS")).isFalse();
assertThat(matcher.matches((String)null)).isFalse();
matcher = StringMatcher.forSafeRegEx(Pattern.compile("^es*.*"));
assertThat(matcher.matches("essence")).isTrue();
assertThat(matcher.matches("")).isFalse();
}
@Test
public void headerMatcher() {
HeaderMatcher matcher = HeaderMatcher.forExactValue("version", "v1", false);
assertThat(matcher.matches("v1")).isTrue();
assertThat(matcher.matches("v2")).isFalse();
matcher = HeaderMatcher.forExactValue("version", "v1", true);
assertThat(matcher.matches("v1")).isFalse();
assertThat(matcher.matches( "v2")).isTrue();
matcher = HeaderMatcher.forPresent("version", true, false);
assertThat(matcher.matches("any")).isTrue();
assertThat(matcher.matches(null)).isFalse();
matcher = HeaderMatcher.forPresent("version", true, true);
assertThat(matcher.matches("version")).isFalse();
matcher = HeaderMatcher.forPresent("version", false, true);
assertThat(matcher.matches("tag")).isTrue();
matcher = HeaderMatcher.forPresent("version", false, false);
assertThat(matcher.matches("tag")).isFalse();
matcher = HeaderMatcher.forPrefix("version", "v2", false);
assertThat(matcher.matches("v22")).isTrue();
matcher = HeaderMatcher.forPrefix("version", "v2", true);
assertThat(matcher.matches("v22")).isFalse();
matcher = HeaderMatcher.forSuffix("version", "v1", false);
assertThat(matcher.matches("xv1")).isTrue();
assertThat(matcher.matches("v1x")).isFalse();
matcher = HeaderMatcher.forSuffix("version", "v2", true);
assertThat(matcher.matches("xv1")).isTrue();
assertThat(matcher.matches("1v2")).isFalse();
matcher = HeaderMatcher.forSafeRegEx("version", Pattern.compile("v2.*"), false);
assertThat(matcher.matches("v2..")).isTrue();
assertThat(matcher.matches("v1")).isFalse();
matcher = HeaderMatcher.forSafeRegEx("version", Pattern.compile("v1\\..*"), true);
assertThat(matcher.matches("v1.43")).isFalse();
assertThat(matcher.matches("v2")).isTrue();
matcher = HeaderMatcher.forRange("version", Range.create(8080L, 8090L), false);
assertThat(matcher.matches("8080")).isTrue();
assertThat(matcher.matches("1")).isFalse();
matcher = HeaderMatcher.forRange("version", Range.create(8080L, 8090L), true);
assertThat(matcher.matches("1")).isTrue();
assertThat(matcher.matches("8080")).isFalse();
}
}

View File

@ -0,0 +1,304 @@
/*
* Copyright 2021 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds.internal.rbac.engine;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import io.grpc.Attributes;
import io.grpc.Grpc;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.ServerCall;
import io.grpc.internal.testing.TestUtils;
import io.grpc.testing.TestMethodDescriptors;
import io.grpc.xds.internal.Matchers;
import io.grpc.xds.internal.Matchers.CidrMatcher;
import io.grpc.xds.internal.Matchers.StringMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.Action;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AndMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthenticatedMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationIpMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.HeaderMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.InvertMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PathMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PolicyMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.SourceIpMatcher;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(JUnit4.class)
public class GrpcAuthorizationEngineTest {
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
private static final String POLICY_NAME = "policy-name";
private static final String HEADER_KEY = "header-key";
private static final String HEADER_VALUE = "header-val";
private static final String IP_ADDR1 = "10.10.10.0";
private static final String IP_ADDR2 = "68.36.0.19";
private static final int PORT = 100;
private static final String PATH = "/auth/engine";
private static final StringMatcher STRING_MATCHER = StringMatcher.forExact("/" + PATH, false);
private static final Metadata HEADER = metadata(HEADER_KEY, HEADER_VALUE);
@Mock
private ServerCall<Void,Void> serverCall;
@Mock
private SSLSession sslSession;
@Before
public void setUp() throws Exception {
X509Certificate[] certs = {TestUtils.loadX509Cert("server1.pem")};
when(sslSession.getPeerCertificates()).thenReturn(certs);
Attributes attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(IP_ADDR2, PORT))
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress(IP_ADDR1, PORT))
.set(Grpc.TRANSPORT_ATTR_SSL_SESSION, sslSession)
.build();
when(serverCall.getAttributes()).thenReturn(attributes);
when(serverCall.getMethodDescriptor()).thenReturn(method().build());
}
@Test
public void ipMatcher() throws Exception {
CidrMatcher ip1 = CidrMatcher.create(InetAddress.getByName(IP_ADDR1), 24);
DestinationIpMatcher destIpMatcher = new DestinationIpMatcher(ip1);
CidrMatcher ip2 = CidrMatcher.create(InetAddress.getByName(IP_ADDR2), 24);
SourceIpMatcher sourceIpMatcher = new SourceIpMatcher(ip2);
DestinationPortMatcher portMatcher = new DestinationPortMatcher(PORT);
OrMatcher permission = OrMatcher.create(AndMatcher.create(portMatcher, destIpMatcher));
OrMatcher principal = OrMatcher.create(sourceIpMatcher);
PolicyMatcher policyMatcher = new PolicyMatcher(POLICY_NAME, permission, principal);
GrpcAuthorizationEngine engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.ALLOW));
AuthDecision decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.ALLOW);
assertThat(decision.matchingPolicyName()).isEqualTo(POLICY_NAME);
Attributes attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(IP_ADDR2, PORT))
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress(IP_ADDR1, 2))
.build();
when(serverCall.getAttributes()).thenReturn(attributes);
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
assertThat(decision.matchingPolicyName()).isEqualTo(null);
attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, null)
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("1.1.1.1", PORT))
.build();
when(serverCall.getAttributes()).thenReturn(attributes);
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
assertThat(decision.matchingPolicyName()).isEqualTo(null);
engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.DENY));
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.ALLOW);
assertThat(decision.matchingPolicyName()).isEqualTo(null);
}
@Test
public void headerMatcher() {
HeaderMatcher headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY, HEADER_VALUE, false));
OrMatcher principal = OrMatcher.create(headerMatcher);
OrMatcher permission = OrMatcher.create(
new InvertMatcher(new DestinationPortMatcher(PORT + 1)));
PolicyMatcher policyMatcher = new PolicyMatcher(POLICY_NAME, permission, principal);
GrpcAuthorizationEngine engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.ALLOW));
AuthDecision decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.ALLOW);
assertThat(decision.matchingPolicyName()).isEqualTo(POLICY_NAME);
HEADER.put(Metadata.Key.of(HEADER_KEY, Metadata.ASCII_STRING_MARSHALLER), HEADER_VALUE);
headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY, HEADER_VALUE + "," + HEADER_VALUE, false));
principal = OrMatcher.create(headerMatcher);
policyMatcher = new PolicyMatcher(POLICY_NAME,
OrMatcher.create(AlwaysTrueMatcher.INSTANCE), principal);
engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.ALLOW));
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.ALLOW);
headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY + Metadata.BINARY_HEADER_SUFFIX, HEADER_VALUE, false));
principal = OrMatcher.create(headerMatcher);
policyMatcher = new PolicyMatcher(POLICY_NAME,
OrMatcher.create(AlwaysTrueMatcher.INSTANCE), principal);
engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.ALLOW));
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
}
@Test
public void pathMatcher() {
PathMatcher pathMatcher = new PathMatcher(STRING_MATCHER);
OrMatcher permission = OrMatcher.create(AlwaysTrueMatcher.INSTANCE);
OrMatcher principal = OrMatcher.create(pathMatcher);
PolicyMatcher policyMatcher = new PolicyMatcher(POLICY_NAME, permission, principal);
GrpcAuthorizationEngine engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.DENY));
AuthDecision decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
assertThat(decision.matchingPolicyName()).isEqualTo(POLICY_NAME);
}
@Test
public void authenticatedMatcher() throws Exception {
AuthenticatedMatcher authMatcher = new AuthenticatedMatcher(
StringMatcher.forExact("*.test.google.fr", false));
PathMatcher pathMatcher = new PathMatcher(STRING_MATCHER);
OrMatcher permission = OrMatcher.create(authMatcher);
OrMatcher principal = OrMatcher.create(pathMatcher);
PolicyMatcher policyMatcher = new PolicyMatcher(POLICY_NAME, permission, principal);
GrpcAuthorizationEngine engine = new GrpcAuthorizationEngine(
new AuthConfig(Collections.singletonList(policyMatcher), Action.ALLOW));
AuthDecision decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.ALLOW);
assertThat(decision.matchingPolicyName()).isEqualTo(POLICY_NAME);
X509Certificate[] certs = {TestUtils.loadX509Cert("badserver.pem")};
when(sslSession.getPeerCertificates()).thenReturn(certs);
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
assertThat(decision.matchingPolicyName()).isEqualTo(null);
X509Certificate mockCert = mock(X509Certificate.class);
when(sslSession.getPeerCertificates()).thenReturn(new X509Certificate[]{mockCert});
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.DENY);
when(mockCert.getSubjectDN()).thenReturn(mock(Principal.class));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.DENY);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(2, "*.test.google.fr")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.ALLOW);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(6, "*.test.google.fr")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.ALLOW);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(10, "*.test.google.fr")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.DENY);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(2, "google.com"), Arrays.asList(6, "*.test.google.fr")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.ALLOW);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(6, "*.test.google.fr"), Arrays.asList(2, "google.com")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.ALLOW);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(2, "*.test.google.fr"), Arrays.asList(6, "google.com")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.DENY);
when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.<List<?>>asList(
Arrays.asList(2, "*.test.google.fr"), Arrays.asList(6, "google.com"),
Arrays.asList(6, "*.test.google.fr")));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.ALLOW);
// match any authenticated connection if StringMatcher not set in AuthenticatedMatcher
permission = OrMatcher.create(new AuthenticatedMatcher(null));
policyMatcher = new PolicyMatcher(POLICY_NAME, permission, principal);
when(mockCert.getSubjectAlternativeNames()).thenReturn(
Arrays.<List<?>>asList(Arrays.asList(6, "random")));
engine = new GrpcAuthorizationEngine(new AuthConfig(Collections.singletonList(policyMatcher),
Action.ALLOW));
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.ALLOW);
// not match any unauthenticated connection
Attributes attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(IP_ADDR2, PORT))
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress(IP_ADDR1, PORT))
.build();
when(serverCall.getAttributes()).thenReturn(attributes);
assertThat(engine.evaluate(HEADER, serverCall).decision()).isEqualTo(Action.DENY);
doThrow(new SSLPeerUnverifiedException("bad")).when(sslSession).getPeerCertificates();
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
assertThat(decision.matchingPolicyName()).isEqualTo(null);
}
@Test
public void multiplePolicies() throws Exception {
AuthenticatedMatcher authMatcher = new AuthenticatedMatcher(
StringMatcher.forSuffix("TEST.google.fr", true));
PathMatcher pathMatcher = new PathMatcher(STRING_MATCHER);
OrMatcher principal = OrMatcher.create(AndMatcher.create(authMatcher, pathMatcher));
OrMatcher permission = OrMatcher.create(AndMatcher.create(pathMatcher,
new InvertMatcher(new DestinationPortMatcher(PORT + 1))));
PolicyMatcher policyMatcher1 = new PolicyMatcher(POLICY_NAME, permission, principal);
HeaderMatcher headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY, HEADER_VALUE + 1, false));
authMatcher = new AuthenticatedMatcher(
StringMatcher.forContains("TEST.google.fr"));
principal = OrMatcher.create(headerMatcher, authMatcher);
CidrMatcher ip1 = CidrMatcher.create(InetAddress.getByName(IP_ADDR1), 24);
DestinationIpMatcher destIpMatcher = new DestinationIpMatcher(ip1);
permission = OrMatcher.create(destIpMatcher, pathMatcher);
PolicyMatcher policyMatcher2 = new PolicyMatcher(POLICY_NAME + "-2", permission, principal);
GrpcAuthorizationEngine engine = new GrpcAuthorizationEngine(
new AuthConfig(ImmutableList.of(policyMatcher1, policyMatcher2), Action.DENY));
AuthDecision decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.DENY);
assertThat(decision.matchingPolicyName()).isEqualTo(POLICY_NAME);
}
private MethodDescriptor.Builder<Void, Void> method() {
return MethodDescriptor.<Void,Void>newBuilder()
.setType(MethodType.BIDI_STREAMING)
.setFullMethodName(PATH)
.setRequestMarshaller(TestMethodDescriptors.voidMarshaller())
.setResponseMarshaller(TestMethodDescriptors.voidMarshaller());
}
private static Metadata metadata(String key, String value) {
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value);
return metadata;
}
}