diff --git a/go/flags/endtoend/vtgate.txt b/go/flags/endtoend/vtgate.txt index 0761740d33c..946b10a39b2 100644 --- a/go/flags/endtoend/vtgate.txt +++ b/go/flags/endtoend/vtgate.txt @@ -34,6 +34,7 @@ Usage of vtgate: --gate_query_cache_size int gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a cache. This config controls the expected amount of unique entries in the cache. (default 5000) --gateway_initial_tablet_timeout duration At startup, the tabletGateway will wait up to this duration to get at least one tablet per keyspace/shard/tablet type (default 30s) --grpc-use-effective-groups If set, and SSL is not used, will set the immediate caller's security groups from the effective caller id's groups. + --grpc-use-static-authentication-callerid If set, will set the immediate caller id to the username authenticated by the static auth plugin. --grpc_auth_mode string Which auth plugin implementation to use (eg: static) --grpc_auth_mtls_allowed_substrings string List of substrings of at least one of the client certificate names (separated by colon). --grpc_auth_static_client_creds string When using grpc_static_auth in the server, this file provides the credentials to use to authenticate with server. diff --git a/go/test/endtoend/vtgate/grpc_server_acls/acls_test.go b/go/test/endtoend/vtgate/grpc_server_acls/acls_test.go new file mode 100644 index 00000000000..a63ca12a201 --- /dev/null +++ b/go/test/endtoend/vtgate/grpc_server_acls/acls_test.go @@ -0,0 +1,204 @@ +/* +Copyright 2023 The Vitess 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 grpc_server_acls + +import ( + "context" + "flag" + "fmt" + "os" + "path" + "testing" + + "vitess.io/vitess/go/vt/callerid" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + "vitess.io/vitess/go/test/endtoend/cluster" + "vitess.io/vitess/go/vt/grpcclient" + "vitess.io/vitess/go/vt/vtgate/grpcvtgateconn" + "vitess.io/vitess/go/vt/vtgate/vtgateconn" +) + +var ( + clusterInstance *cluster.LocalProcessCluster + vtgateGrpcAddress string + hostname = "localhost" + keyspaceName = "ks" + cell = "zone1" + sqlSchema = ` + create table test_table ( + id bigint, + val varchar(128), + primary key(id) + ) Engine=InnoDB; +` + grpcServerAuthStaticJSON = ` + [ + { + "Username": "some_other_user", + "Password": "test_password" + }, + { + "Username": "another_unrelated_user", + "Password": "test_password" + } + ] +` + tableACLJSON = ` + { + "table_groups": [ + { + "name": "default", + "table_names_or_prefixes": ["%"], + "readers": ["user_with_access"], + "writers": ["user_with_access"], + "admins": ["user_with_access"] + } + ] + } +` +) + +func TestMain(m *testing.M) { + + defer cluster.PanicHandler(nil) + flag.Parse() + + exitcode := func() int { + clusterInstance = cluster.NewCluster(cell, hostname) + defer clusterInstance.Teardown() + + // Start topo server + if err := clusterInstance.StartTopo(); err != nil { + return 1 + } + + // Directory for authn / authz config files + authDirectory := path.Join(clusterInstance.TmpDirectory, "auth") + if err := os.Mkdir(authDirectory, 0700); err != nil { + return 1 + } + + // Create grpc_server_auth_static.json file + grpcServerAuthStaticPath := path.Join(authDirectory, "grpc_server_auth_static.json") + if err := createFile(grpcServerAuthStaticPath, grpcServerAuthStaticJSON); err != nil { + return 1 + } + + // Create table_acl.json file + tableACLPath := path.Join(authDirectory, "table_acl.json") + if err := createFile(tableACLPath, tableACLJSON); err != nil { + return 1 + } + + // Configure vtgate to use static auth + clusterInstance.VtGateExtraArgs = []string{ + "--grpc_auth_mode", "static", + "--grpc_auth_static_password_file", grpcServerAuthStaticPath, + "--grpc_use_effective_callerid", + "--grpc-use-static-authentication-callerid", + } + + // Configure vttablet to use table ACL + clusterInstance.VtTabletExtraArgs = []string{ + "--enforce-tableacl-config", + "--queryserver-config-strict-table-acl", + "--table-acl-config", tableACLPath, + } + + // Start keyspace + keyspace := &cluster.Keyspace{ + Name: keyspaceName, + SchemaSQL: sqlSchema, + } + if err := clusterInstance.StartUnshardedKeyspace(*keyspace, 1, false); err != nil { + return 1 + } + + // Start vtgate + if err := clusterInstance.StartVtgate(); err != nil { + clusterInstance.VtgateProcess = cluster.VtgateProcess{} + return 1 + } + vtgateGrpcAddress = fmt.Sprintf("%s:%d", clusterInstance.Hostname, clusterInstance.VtgateGrpcPort) + + return m.Run() + }() + os.Exit(exitcode) +} + +// TestEffectiveCallerIDWithAccess verifies that an authenticated gRPC static user with an effectiveCallerID that has ACL access can execute queries +func TestEffectiveCallerIDWithAccess(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + vtgateConn, err := dialVTGate(ctx, t, "some_other_user", "test_password") + if err != nil { + t.Fatal(err) + } + defer vtgateConn.Close() + + session := vtgateConn.Session(keyspaceName+"@primary", nil) + query := "SELECT id FROM test_table" + ctx = callerid.NewContext(ctx, callerid.NewEffectiveCallerID("user_with_access", "", ""), nil) + _, err = session.Execute(ctx, query, nil) + assert.NoError(t, err) +} + +// TestEffectiveCallerIDWithNoAccess verifies that an authenticated gRPC static user without an effectiveCallerID that has ACL access cannot execute queries +func TestEffectiveCallerIDWithNoAccess(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + vtgateConn, err := dialVTGate(ctx, t, "another_unrelated_user", "test_password") + if err != nil { + t.Fatal(err) + } + defer vtgateConn.Close() + + session := vtgateConn.Session(keyspaceName+"@primary", nil) + query := "SELECT id FROM test_table" + ctx = callerid.NewContext(ctx, callerid.NewEffectiveCallerID("user_no_access", "", ""), nil) + _, err = session.Execute(ctx, query, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "Select command denied to user") + assert.Contains(t, err.Error(), "for table 'test_table' (ACL check error)") +} + +func dialVTGate(ctx context.Context, t *testing.T, username string, password string) (*vtgateconn.VTGateConn, error) { + clientCreds := &grpcclient.StaticAuthClientCreds{Username: username, Password: password} + creds := grpc.WithPerRPCCredentials(clientCreds) + dialerFunc := grpcvtgateconn.DialWithOpts(ctx, creds) + dialerName := t.Name() + vtgateconn.RegisterDialer(dialerName, dialerFunc) + return vtgateconn.DialProtocol(ctx, dialerName, vtgateGrpcAddress) +} + +func createFile(path string, contents string) error { + f, err := os.Create(path) + if err != nil { + return err + } + _, err = f.WriteString(contents) + if err != nil { + return err + } + return f.Close() +} diff --git a/go/test/endtoend/vtgate/grpc_server_auth_static/main_test.go b/go/test/endtoend/vtgate/grpc_server_auth_static/main_test.go index 510f969ce9a..c00f4b3f2c1 100644 --- a/go/test/endtoend/vtgate/grpc_server_auth_static/main_test.go +++ b/go/test/endtoend/vtgate/grpc_server_auth_static/main_test.go @@ -109,6 +109,7 @@ func TestMain(m *testing.M) { clusterInstance.VtGateExtraArgs = []string{ "--grpc_auth_mode", "static", "--grpc_auth_static_password_file", grpcServerAuthStaticPath, + "--grpc-use-static-authentication-callerid", } // Configure vttablet to use table ACL diff --git a/go/vt/vtgate/grpcvtgateservice/server.go b/go/vt/vtgate/grpcvtgateservice/server.go index d012786d6eb..0ebe829ac4d 100644 --- a/go/vt/vtgate/grpcvtgateservice/server.go +++ b/go/vt/vtgate/grpcvtgateservice/server.go @@ -46,13 +46,15 @@ const ( ) var ( - useEffective bool - useEffectiveGroups bool + useEffective bool + useEffectiveGroups bool + useStaticAuthenticationIdentity bool ) func registerFlags(fs *pflag.FlagSet) { fs.BoolVar(&useEffective, "grpc_use_effective_callerid", false, "If set, and SSL is not used, will set the immediate caller id from the effective caller id's principal.") fs.BoolVar(&useEffectiveGroups, "grpc-use-effective-groups", false, "If set, and SSL is not used, will set the immediate caller's security groups from the effective caller id's groups.") + fs.BoolVar(&useStaticAuthenticationIdentity, "grpc-use-static-authentication-callerid", false, "If set, will set the immediate caller id to the username authenticated by the static auth plugin.") } func init() { @@ -94,23 +96,35 @@ func immediateCallerIDFromCert(ctx context.Context) (string, []string) { return cert.Subject.CommonName, cert.DNSNames } -func immediateCallerID(ctx context.Context) (string, []string) { +// immediateCallerIdFromStaticAuthentication extracts the username of the current +// static authentication context and returns that to the caller. +func immediateCallerIdFromStaticAuthentication(ctx context.Context) (string, []string) { if immediate := servenv.StaticAuthUsernameFromContext(ctx); immediate != "" { return immediate, nil } - return immediateCallerIDFromCert(ctx) + + return "", nil } // withCallerIDContext creates a context that extracts what we need // from the incoming call and can be forwarded for use when talking to vttablet. func withCallerIDContext(ctx context.Context, effectiveCallerID *vtrpcpb.CallerID) context.Context { - immediate, securityGroups := immediateCallerID(ctx) + // The client cert common name (if using mTLS) + immediate, securityGroups := immediateCallerIDFromCert(ctx) + + // The effective caller id (if --grpc_use_effective_callerid=true) if immediate == "" && useEffective && effectiveCallerID != nil { immediate = effectiveCallerID.Principal if useEffectiveGroups && len(effectiveCallerID.Groups) > 0 { securityGroups = effectiveCallerID.Groups } } + + // The static auth username (if --grpc-use-static-authentication-callerid=true) + if immediate == "" && useStaticAuthenticationIdentity { + immediate, securityGroups = immediateCallerIdFromStaticAuthentication(ctx) + } + if immediate == "" { immediate = unsecureClient } diff --git a/test/config.json b/test/config.json index d5757645b6c..511e632e4ad 100644 --- a/test/config.json +++ b/test/config.json @@ -882,6 +882,15 @@ "RetryMax": 1, "Tags": [] }, + "vtgate_grpc_server_acls": { + "File": "unused.go", + "Args": ["vitess.io/vitess/go/test/endtoend/vtgate/grpc_server_acls"], + "Command": [], + "Manual": false, + "Shard": "vtgate_general_heavy", + "RetryMax": 1, + "Tags": [] + }, "topo_zk2": { "File": "unused.go", "Args": ["vitess.io/vitess/go/test/endtoend/topotest/zk2", "--topo-flavor=zk2"],