blob: ac7302fc38ade0f8a38eff5e87070774ec8c14ca [file] [log] [blame]
/*
* 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 com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
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.Locale;
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;
}
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. */
@AutoValue
public abstract static class AuthConfig {
public abstract ImmutableList<PolicyMatcher> policies();
public abstract Action action();
public static AuthConfig create(List<PolicyMatcher> policies, Action action) {
return new AutoValue_GrpcAuthorizationEngine_AuthConfig(
ImmutableList.copyOf(policies), 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.
*/
@AutoValue
public abstract static class PolicyMatcher implements Matcher {
public abstract String name();
public abstract OrMatcher permissions();
public abstract OrMatcher principals();
/** Constructs a matcher for one RBAC policy. */
public static PolicyMatcher create(String name, OrMatcher permissions, OrMatcher principals) {
return new AutoValue_GrpcAuthorizationEngine_PolicyMatcher(name, permissions, principals);
}
@Override
public boolean matches(EvaluateArgs args) {
return permissions().matches(args) && principals().matches(args);
}
}
@AutoValue
public abstract static class AuthenticatedMatcher implements Matcher {
@Nullable
public abstract Matchers.StringMatcher delegate();
/**
* Passing in null will match all authenticated user, i.e. SSL session is present.
* https://github.com/envoyproxy/envoy/blob/3975bf5dadb43421907bbc52df57c0e8539c9a06/api/envoy/config/rbac/v3/rbac.proto#L253
* */
public static AuthenticatedMatcher create(@Nullable Matchers.StringMatcher delegate) {
return new AutoValue_GrpcAuthorizationEngine_AuthenticatedMatcher(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;
}
}
@AutoValue
public abstract static class DestinationIpMatcher implements Matcher {
public abstract Matchers.CidrMatcher delegate();
public static DestinationIpMatcher create(Matchers.CidrMatcher delegate) {
return new AutoValue_GrpcAuthorizationEngine_DestinationIpMatcher(delegate);
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate().matches(args.getDestinationIp());
}
}
@AutoValue
public abstract static class SourceIpMatcher implements Matcher {
public abstract Matchers.CidrMatcher delegate();
public static SourceIpMatcher create(Matchers.CidrMatcher delegate) {
return new AutoValue_GrpcAuthorizationEngine_SourceIpMatcher(delegate);
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate().matches(args.getSourceIp());
}
}
@AutoValue
public abstract static class PathMatcher implements Matcher {
public abstract Matchers.StringMatcher delegate();
public static PathMatcher create(Matchers.StringMatcher delegate) {
return new AutoValue_GrpcAuthorizationEngine_PathMatcher(delegate);
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate().matches(args.getPath());
}
}
@AutoValue
public abstract static class AuthHeaderMatcher implements Matcher {
public abstract Matchers.HeaderMatcher delegate();
public static AuthHeaderMatcher create(Matchers.HeaderMatcher delegate) {
return new AutoValue_GrpcAuthorizationEngine_AuthHeaderMatcher(delegate);
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate().matches(args.getHeader(delegate().name()));
}
}
@AutoValue
public abstract static class DestinationPortMatcher implements Matcher {
public abstract int port();
public static DestinationPortMatcher create(int port) {
return new AutoValue_GrpcAuthorizationEngine_DestinationPortMatcher(port);
}
@Override
public boolean matches(EvaluateArgs args) {
return port() == args.getDestinationPort();
}
}
@AutoValue
public abstract static class DestinationPortRangeMatcher implements Matcher {
public abstract int start();
public abstract int end();
/** Start of the range is inclusive. End of the range is exclusive.*/
public static DestinationPortRangeMatcher create(int start, int end) {
return new AutoValue_GrpcAuthorizationEngine_DestinationPortRangeMatcher(start, end);
}
@Override
public boolean matches(EvaluateArgs args) {
int port = args.getDestinationPort();
return port >= start() && port < end();
}
}
@AutoValue
public abstract static class RequestedServerNameMatcher implements Matcher {
public abstract Matchers.StringMatcher delegate();
public static RequestedServerNameMatcher create(Matchers.StringMatcher delegate) {
return new AutoValue_GrpcAuthorizationEngine_RequestedServerNameMatcher(delegate);
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate().matches(args.getRequestedServerName());
}
}
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
*/
@Nullable
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) {
headerName = headerName.toLowerCase(Locale.ROOT);
if ("te".equals(headerName)) {
return null;
}
if (":authority".equals(headerName)) {
headerName = "host";
}
if ("host".equals(headerName)) {
return serverCall.getAuthority();
}
if (":path".equals(headerName)) {
return getPath();
}
if (":method".equals(headerName)) {
return "POST";
}
return deserializeHeader(headerName);
}
@Nullable
private String deserializeHeader(String headerName) {
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
Metadata.Key<byte[]> key;
try {
key = Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER);
} catch (IllegalArgumentException e) {
return null;
}
Iterable<byte[]> values = metadata.getAll(key);
if (values == null) {
return null;
}
List<String> encoded = new ArrayList<>();
for (byte[] v : values) {
encoded.add(BaseEncoding.base64().omitPadding().encode(v));
}
return Joiner.on(",").join(encoded);
}
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();
}
private String getRequestedServerName() {
return "";
}
}
public interface Matcher {
boolean matches(EvaluateArgs args);
}
@AutoValue
public abstract static class OrMatcher implements Matcher {
public abstract ImmutableList<? extends Matcher> anyMatch();
/** Matches when any of the matcher matches. */
public static OrMatcher create(List<? extends Matcher> matchers) {
checkNotNull(matchers, "matchers");
for (Matcher matcher : matchers) {
checkNotNull(matcher, "matcher");
}
return new AutoValue_GrpcAuthorizationEngine_OrMatcher(ImmutableList.copyOf(matchers));
}
public static OrMatcher create(Matcher...matchers) {
return OrMatcher.create(Arrays.asList(matchers));
}
@Override
public boolean matches(EvaluateArgs args) {
for (Matcher m : anyMatch()) {
if (m.matches(args)) {
return true;
}
}
return false;
}
}
@AutoValue
public abstract static class AndMatcher implements Matcher {
public abstract ImmutableList<? extends Matcher> allMatch();
/** Matches when all of the matchers match. */
public static AndMatcher create(List<? extends Matcher> matchers) {
checkNotNull(matchers, "matchers");
for (Matcher matcher : matchers) {
checkNotNull(matcher, "matcher");
}
return new AutoValue_GrpcAuthorizationEngine_AndMatcher(ImmutableList.copyOf(matchers));
}
public static AndMatcher create(Matcher...matchers) {
return AndMatcher.create(Arrays.asList(matchers));
}
@Override
public boolean matches(EvaluateArgs args) {
for (Matcher m : allMatch()) {
if (!m.matches(args)) {
return false;
}
}
return true;
}
}
/** Always true matcher.*/
@AutoValue
public abstract static class AlwaysTrueMatcher implements Matcher {
public static AlwaysTrueMatcher INSTANCE =
new AutoValue_GrpcAuthorizationEngine_AlwaysTrueMatcher();
@Override
public boolean matches(EvaluateArgs args) {
return true;
}
}
/** Negate matcher.*/
@AutoValue
public abstract static class InvertMatcher implements Matcher {
public abstract Matcher toInvertMatcher();
public static InvertMatcher create(Matcher matcher) {
return new AutoValue_GrpcAuthorizationEngine_InvertMatcher(matcher);
}
@Override
public boolean matches(EvaluateArgs args) {
return !toInvertMatcher().matches(args);
}
}
}