Skip to content

Commit e469f0d

Browse files
authored
xds: Add env var protection for RBAC HTTP Filter (#4765)
* xds: Add env var protection for RBAC HTTP Filter
1 parent 567da6b commit e469f0d

File tree

10 files changed

+322
-7
lines changed

10 files changed

+322
-7
lines changed

internal/xds/env/env.go

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
clientSideSecuritySupportEnv = "GRPC_XDS_EXPERIMENTAL_SECURITY_SUPPORT"
4444
aggregateAndDNSSupportEnv = "GRPC_XDS_EXPERIMENTAL_ENABLE_AGGREGATE_AND_LOGICAL_DNS_CLUSTER"
4545
retrySupportEnv = "GRPC_XDS_EXPERIMENTAL_ENABLE_RETRY"
46+
rbacSupportEnv = "GRPC_XDS_EXPERIMENTAL_ENABLE_RBAC"
4647

4748
c2pResolverSupportEnv = "GRPC_EXPERIMENTAL_GOOGLE_C2P_RESOLVER"
4849
c2pResolverTestOnlyTrafficDirectorURIEnv = "GRPC_TEST_ONLY_GOOGLE_C2P_RESOLVER_TRAFFIC_DIRECTOR_URI"
@@ -82,6 +83,9 @@ var (
8283
// RetrySupport indicates whether xDS retry is enabled.
8384
RetrySupport = !strings.EqualFold(os.Getenv(retrySupportEnv), "false")
8485

86+
// RBACSupport indicates whether xDS configured RBAC HTTP Filter is enabled.
87+
RBACSupport = strings.EqualFold(os.Getenv(rbacSupportEnv), "true")
88+
8589
// C2PResolverSupport indicates whether support for C2P resolver is enabled.
8690
// This can be enabled by setting the environment variable
8791
// "GRPC_EXPERIMENTAL_GOOGLE_C2P_RESOLVER" to "true".

xds/internal/httpfilter/httpfilter.go

+5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ func Register(b Filter) {
9494
}
9595
}
9696

97+
// UnregisterForTesting unregisters the HTTP Filter for testing purposes.
98+
func UnregisterForTesting(typeURL string) {
99+
delete(m, typeURL)
100+
}
101+
97102
// Get returns the HTTPFilter registered with typeURL.
98103
//
99104
// If no filter is register with typeURL, nil will be returned.

xds/internal/httpfilter/rbac/rbac.go

+19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/golang/protobuf/proto"
2929
"github.com/golang/protobuf/ptypes"
3030
"google.golang.org/grpc/internal/resolver"
31+
"google.golang.org/grpc/internal/xds/env"
3132
"google.golang.org/grpc/internal/xds/rbac"
3233
"google.golang.org/grpc/xds/internal/httpfilter"
3334
"google.golang.org/protobuf/types/known/anypb"
@@ -37,9 +38,27 @@ import (
3738
)
3839

3940
func init() {
41+
if env.RBACSupport {
42+
httpfilter.Register(builder{})
43+
}
44+
}
45+
46+
// RegisterForTesting registers the RBAC HTTP Filter for testing purposes, regardless
47+
// of the RBAC environment variable. This is needed because there is no way to set the RBAC
48+
// environment variable to true in a test before init() in this package is run.
49+
func RegisterForTesting() {
4050
httpfilter.Register(builder{})
4151
}
4252

53+
// UnregisterForTesting unregisters the RBAC HTTP Filter for testing purposes. This is needed because
54+
// there is no way to unregister the HTTP Filter after registering it solely for testing purposes using
55+
// rbac.RegisterForTesting()
56+
func UnregisterForTesting() {
57+
for _, typeURL := range builder.TypeURLs(builder{}) {
58+
httpfilter.UnregisterForTesting(typeURL)
59+
}
60+
}
61+
4362
type builder struct {
4463
}
4564

xds/internal/server/listener_wrapper.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
internalbackoff "google.golang.org/grpc/internal/backoff"
3636
internalgrpclog "google.golang.org/grpc/internal/grpclog"
3737
"google.golang.org/grpc/internal/grpcsync"
38+
"google.golang.org/grpc/internal/xds/env"
3839
"google.golang.org/grpc/xds/internal/xdsclient"
3940
"google.golang.org/grpc/xds/internal/xdsclient/bootstrap"
4041
)
@@ -272,6 +273,9 @@ func (l *listenerWrapper) Accept() (net.Conn, error) {
272273
conn.Close()
273274
continue
274275
}
276+
if !env.RBACSupport {
277+
return &connWrapper{Conn: conn, filterChain: fc, parent: l}, nil
278+
}
275279
var rc xdsclient.RouteConfigUpdate
276280
if fc.InlineRouteConfig != nil {
277281
rc = *fc.InlineRouteConfig
@@ -410,8 +414,10 @@ func (l *listenerWrapper) handleLDSUpdate(update ldsUpdateWithError) {
410414
// Server's state to ServingModeNotServing. That prevents new connections
411415
// from being accepted, whereas here we simply want the clients to reconnect
412416
// to get the updated configuration.
413-
if l.drainCallback != nil {
414-
l.drainCallback(l.Listener.Addr())
417+
if env.RBACSupport {
418+
if l.drainCallback != nil {
419+
l.drainCallback(l.Listener.Addr())
420+
}
415421
}
416422
l.rdsHandler.updateRouteNamesToWatch(ilc.FilterChains.RouteConfigNames)
417423
// If there are no dynamic RDS Configurations still needed to be received

xds/internal/server/listener_wrapper_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
3535
"google.golang.org/grpc/internal/grpctest"
3636
"google.golang.org/grpc/internal/testutils"
37+
"google.golang.org/grpc/internal/xds/env"
3738
_ "google.golang.org/grpc/xds/internal/httpfilter/router"
3839
"google.golang.org/grpc/xds/internal/testutils/e2e"
3940
"google.golang.org/grpc/xds/internal/testutils/fakeclient"
@@ -325,6 +326,11 @@ func (s) TestNewListenerWrapper(t *testing.T) {
325326
// the update from the rds handler should it move the server to
326327
// ServingModeServing.
327328
func (s) TestNewListenerWrapperWithRouteUpdate(t *testing.T) {
329+
oldRBAC := env.RBACSupport
330+
env.RBACSupport = true
331+
defer func() {
332+
env.RBACSupport = oldRBAC
333+
}()
328334
_, readyCh, xdsC, _, cleanup := newListenerWrapper(t)
329335
defer cleanup()
330336

xds/internal/test/xds_server_integration_test.go

+225-1
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ import (
3434
"google.golang.org/grpc/codes"
3535
"google.golang.org/grpc/credentials/insecure"
3636
"google.golang.org/grpc/internal/testutils"
37+
"google.golang.org/grpc/internal/xds/env"
3738
"google.golang.org/grpc/status"
3839
"google.golang.org/grpc/xds"
39-
_ "google.golang.org/grpc/xds/internal/httpfilter/rbac"
40+
"google.golang.org/grpc/xds/internal/httpfilter/rbac"
4041
"google.golang.org/grpc/xds/internal/testutils/e2e"
4142

4243
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
@@ -374,6 +375,11 @@ func (s) TestServerSideXDS_SecurityConfigChange(t *testing.T) {
374375
// (NonForwardingAction), and the RPC's matching those routes should proceed as
375376
// normal.
376377
func (s) TestServerSideXDS_RouteConfiguration(t *testing.T) {
378+
oldRBAC := env.RBACSupport
379+
env.RBACSupport = true
380+
defer func() {
381+
env.RBACSupport = oldRBAC
382+
}()
377383
managementServer, nodeID, bootstrapContents, resolver, cleanup1 := setupManagementServer(t)
378384
defer cleanup1()
379385

@@ -711,6 +717,13 @@ func serverListenerWithRBACHTTPFilters(host string, port uint32, rbacCfg *rpb.RB
711717
// as normal and certain RPC's are denied by the RBAC HTTP Filter which gets
712718
// called by hooked xds interceptors.
713719
func (s) TestRBACHTTPFilter(t *testing.T) {
720+
oldRBAC := env.RBACSupport
721+
env.RBACSupport = true
722+
defer func() {
723+
env.RBACSupport = oldRBAC
724+
}()
725+
rbac.RegisterForTesting()
726+
defer rbac.UnregisterForTesting()
714727
tests := []struct {
715728
name string
716729
rbacCfg *rpb.RBAC
@@ -823,7 +836,218 @@ func (s) TestRBACHTTPFilter(t *testing.T) {
823836
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantStatusUnaryCall {
824837
t.Fatalf("UnaryCall() returned err with status: %v, wantStatusUnaryCall: %v", err, test.wantStatusUnaryCall)
825838
}
839+
840+
// Toggle the RBAC Env variable off, this should disable RBAC and allow any RPC"s through (will not go through
841+
// routing or processed by HTTP Filters and thus will never get denied by RBAC).
842+
env.RBACSupport = false
843+
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.OK {
844+
t.Fatalf("EmptyCall() returned err with status: %v, once RBAC is disabled all RPC's should proceed as normal", status.Code(err))
845+
}
846+
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.OK {
847+
t.Fatalf("UnaryCall() returned err with status: %v, once RBAC is disabled all RPC's should proceed as normal", status.Code(err))
848+
}
849+
// Toggle RBAC back on for next iterations.
850+
env.RBACSupport = true
826851
}()
827852
})
828853
}
829854
}
855+
856+
// serverListenerWithBadRouteConfiguration returns an xds Listener resource with
857+
// a Route Configuration that will never successfully match in order to test
858+
// RBAC Environment variable being toggled on and off.
859+
func serverListenerWithBadRouteConfiguration(host string, port uint32) *v3listenerpb.Listener {
860+
return &v3listenerpb.Listener{
861+
Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port)))),
862+
Address: &v3corepb.Address{
863+
Address: &v3corepb.Address_SocketAddress{
864+
SocketAddress: &v3corepb.SocketAddress{
865+
Address: host,
866+
PortSpecifier: &v3corepb.SocketAddress_PortValue{
867+
PortValue: port,
868+
},
869+
},
870+
},
871+
},
872+
FilterChains: []*v3listenerpb.FilterChain{
873+
{
874+
Name: "v4-wildcard",
875+
FilterChainMatch: &v3listenerpb.FilterChainMatch{
876+
PrefixRanges: []*v3corepb.CidrRange{
877+
{
878+
AddressPrefix: "0.0.0.0",
879+
PrefixLen: &wrapperspb.UInt32Value{
880+
Value: uint32(0),
881+
},
882+
},
883+
},
884+
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
885+
SourcePrefixRanges: []*v3corepb.CidrRange{
886+
{
887+
AddressPrefix: "0.0.0.0",
888+
PrefixLen: &wrapperspb.UInt32Value{
889+
Value: uint32(0),
890+
},
891+
},
892+
},
893+
},
894+
Filters: []*v3listenerpb.Filter{
895+
{
896+
Name: "filter-1",
897+
ConfigType: &v3listenerpb.Filter_TypedConfig{
898+
TypedConfig: testutils.MarshalAny(&v3httppb.HttpConnectionManager{
899+
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
900+
RouteConfig: &v3routepb.RouteConfiguration{
901+
Name: "routeName",
902+
VirtualHosts: []*v3routepb.VirtualHost{{
903+
// Incoming RPC's will try and match to Virtual Hosts based on their :authority header.
904+
// Thus, incoming RPC's will never match to a Virtual Host (server side requires matching
905+
// to a VH/Route of type Non Forwarding Action to proceed normally), and all incoming RPC's
906+
// with this route configuration will be denied.
907+
Domains: []string{"will-never-match"},
908+
Routes: []*v3routepb.Route{{
909+
Match: &v3routepb.RouteMatch{
910+
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
911+
},
912+
Action: &v3routepb.Route_NonForwardingAction{},
913+
}}}}},
914+
},
915+
HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
916+
}),
917+
},
918+
},
919+
},
920+
},
921+
{
922+
Name: "v6-wildcard",
923+
FilterChainMatch: &v3listenerpb.FilterChainMatch{
924+
PrefixRanges: []*v3corepb.CidrRange{
925+
{
926+
AddressPrefix: "::",
927+
PrefixLen: &wrapperspb.UInt32Value{
928+
Value: uint32(0),
929+
},
930+
},
931+
},
932+
SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
933+
SourcePrefixRanges: []*v3corepb.CidrRange{
934+
{
935+
AddressPrefix: "::",
936+
PrefixLen: &wrapperspb.UInt32Value{
937+
Value: uint32(0),
938+
},
939+
},
940+
},
941+
},
942+
Filters: []*v3listenerpb.Filter{
943+
{
944+
Name: "filter-1",
945+
ConfigType: &v3listenerpb.Filter_TypedConfig{
946+
TypedConfig: testutils.MarshalAny(&v3httppb.HttpConnectionManager{
947+
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
948+
RouteConfig: &v3routepb.RouteConfiguration{
949+
Name: "routeName",
950+
VirtualHosts: []*v3routepb.VirtualHost{{
951+
// Incoming RPC's will try and match to Virtual Hosts based on their :authority header.
952+
// Thus, incoming RPC's will never match to a Virtual Host (server side requires matching
953+
// to a VH/Route of type Non Forwarding Action to proceed normally), and all incoming RPC's
954+
// with this route configuration will be denied.
955+
Domains: []string{"will-never-match"},
956+
Routes: []*v3routepb.Route{{
957+
Match: &v3routepb.RouteMatch{
958+
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
959+
},
960+
Action: &v3routepb.Route_NonForwardingAction{},
961+
}}}}},
962+
},
963+
HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
964+
}),
965+
},
966+
},
967+
},
968+
},
969+
},
970+
}
971+
}
972+
973+
// TestRBACToggledOffThenToggledOnWithBadRouteConfiguration tests a scenario
974+
// where the server gets a listener configuration with a route table that is
975+
// garbage, with incoming RPC's never matching to a VH/Route of type Non
976+
// Forwarding Action, thus never proceeding as normal. In the default scenario
977+
// (RBAC Env Var turned off, thus all logic related to Route Configuration
978+
// protected), the RPC's should simply proceed as normal due to ignoring the
979+
// route configuration. Once toggling the route configuration on, the RPC's
980+
// should all fail after updating the Server.
981+
func (s) TestRBACToggledOffThenToggledOnWithBadRouteConfiguration(t *testing.T) {
982+
managementServer, nodeID, bootstrapContents, resolver, cleanup1 := setupManagementServer(t)
983+
defer cleanup1()
984+
985+
lis, cleanup2 := setupGRPCServer(t, bootstrapContents)
986+
defer cleanup2()
987+
988+
host, port, err := hostPortFromListener(lis)
989+
if err != nil {
990+
t.Fatalf("failed to retrieve host and port of server: %v", err)
991+
}
992+
const serviceName = "my-service-fallback"
993+
994+
// The inbound listener needs a route table that will never match on a VH,
995+
// and thus shouldn't allow incoming RPC's to proceed.
996+
resources := e2e.DefaultClientResources(e2e.ResourceParams{
997+
DialTarget: serviceName,
998+
NodeID: nodeID,
999+
Host: host,
1000+
Port: port,
1001+
SecLevel: e2e.SecurityLevelNone,
1002+
})
1003+
// This bad route configuration shouldn't affect incoming RPC's from
1004+
// proceeding as normal, as the configuration shouldn't be parsed due to the
1005+
// RBAC Environment variable not being set to true.
1006+
inboundLis := serverListenerWithBadRouteConfiguration(host, port)
1007+
resources.Listeners = append(resources.Listeners, inboundLis)
1008+
1009+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
1010+
defer cancel()
1011+
// Setup the management server with client and server-side resources.
1012+
if err := managementServer.Update(ctx, resources); err != nil {
1013+
t.Fatal(err)
1014+
}
1015+
1016+
cc, err := grpc.DialContext(ctx, fmt.Sprintf("xds:///%s", serviceName), grpc.WithInsecure(), grpc.WithResolvers(resolver))
1017+
if err != nil {
1018+
t.Fatalf("failed to dial local test server: %v", err)
1019+
}
1020+
defer cc.Close()
1021+
1022+
client := testpb.NewTestServiceClient(cc)
1023+
1024+
// The default setting of RBAC being disabled should allow any RPC's to
1025+
// proceed as normal.
1026+
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.OK {
1027+
t.Fatalf("EmptyCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err))
1028+
}
1029+
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.OK {
1030+
t.Fatalf("UnaryCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err))
1031+
}
1032+
1033+
// After toggling RBAC support on, all the RPC's should get denied with
1034+
// status code Unavailable due to not matching to a route of type Non
1035+
// Forwarding Action (Route Table not configured properly).
1036+
oldRBAC := env.RBACSupport
1037+
env.RBACSupport = true
1038+
defer func() {
1039+
env.RBACSupport = oldRBAC
1040+
}()
1041+
// Update the server with the same configuration, this is blocking on server
1042+
// side so no raciness here.
1043+
if err := managementServer.Update(ctx, resources); err != nil {
1044+
t.Fatal(err)
1045+
}
1046+
1047+
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable {
1048+
t.Fatalf("EmptyCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err))
1049+
}
1050+
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.Unavailable {
1051+
t.Fatalf("UnaryCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err))
1052+
}
1053+
}

xds/internal/xdsclient/filter_chain.go

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/golang/protobuf/proto"
3030
"github.com/golang/protobuf/ptypes"
3131
"google.golang.org/grpc/internal/resolver"
32+
"google.golang.org/grpc/internal/xds/env"
3233
"google.golang.org/grpc/xds/internal/httpfilter"
3334
"google.golang.org/grpc/xds/internal/version"
3435
)
@@ -598,6 +599,9 @@ func processNetworkFilters(filters []*v3listenerpb.Filter) (*FilterChain, error)
598599
// TODO: Implement terminal filter logic, as per A36.
599600
filterChain.HTTPFilters = filters
600601
seenHCM = true
602+
if !env.RBACSupport {
603+
continue
604+
}
601605
switch hcm.RouteSpecifier.(type) {
602606
case *v3httppb.HttpConnectionManager_Rds:
603607
if hcm.GetRds().GetConfigSource().GetAds() == nil {

0 commit comments

Comments
 (0)