Files
teleport/api/client/client_test.go
T
Alan Parra dd3a884abe Drop direct dependency on github.com/golang/protobuf (#16898)
Drop dependency on the (superseded) github.com/golang/protobuf and add a
depguard.
2022-09-30 18:19:44 +00:00

981 lines
28 KiB
Go

/*
Copyright 2021 Gravitational, Inc.
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 client
import (
"context"
"crypto/tls"
"fmt"
"net"
"testing"
"time"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// mockServer mocks an Auth Server.
type mockServer struct {
addr string
grpc *grpc.Server
*proto.UnimplementedAuthServiceServer
}
func newMockServer(addr string) *mockServer {
m := &mockServer{
addr: addr,
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
}
proto.RegisterAuthServiceServer(m.grpc, m)
return m
}
func (m *mockServer) Stop() {
m.grpc.Stop()
}
func (m *mockServer) Addr() string {
return m.addr
}
type ConfigOpt func(*Config)
func WithConfig(cfg Config) ConfigOpt {
return func(config *Config) {
*config = cfg
}
}
func (m *mockServer) NewClient(ctx context.Context, opts ...ConfigOpt) (*Client, error) {
cfg := Config{
Addrs: []string{m.addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
}
for _, opt := range opts {
opt(&cfg)
}
return New(ctx, cfg)
}
// startMockServer starts a new mock server. Parallel tests cannot use the same addr.
func startMockServer(t *testing.T) *mockServer {
l, err := net.Listen("tcp", "")
require.NoError(t, err)
return startMockServerWithListener(t, l)
}
// startMockServerWithListener starts a new mock server with the provided listener
func startMockServerWithListener(t *testing.T, l net.Listener) *mockServer {
srv := newMockServer(l.Addr().String())
t.Cleanup(srv.grpc.Stop)
go func() {
require.NoError(t, srv.grpc.Serve(l))
}()
return srv
}
func (m *mockServer) Ping(ctx context.Context, req *proto.PingRequest) (*proto.PingResponse, error) {
return &proto.PingResponse{}, nil
}
func (m *mockServer) ListResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
resources, err := testResources(req.ResourceType, req.Namespace)
if err != nil {
return nil, trail.ToGRPC(err)
}
resp := &proto.ListResourcesResponse{
Resources: make([]*proto.PaginatedResource, 0),
TotalCount: int32(len(resources)),
}
var (
takeResources = req.StartKey == ""
lastResourceName string
)
for _, resource := range resources {
if resource.GetName() == req.StartKey {
takeResources = true
continue
}
if !takeResources {
continue
}
var protoResource *proto.PaginatedResource
switch req.ResourceType {
case types.KindDatabaseServer:
database, ok := resource.(*types.DatabaseServerV3)
if !ok {
return nil, trace.Errorf("database server has invalid type %T", resource)
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_DatabaseServer{DatabaseServer: database}}
case types.KindAppServer:
app, ok := resource.(*types.AppServerV3)
if !ok {
return nil, trace.Errorf("application server has invalid type %T", resource)
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_AppServer{AppServer: app}}
case types.KindNode:
srv, ok := resource.(*types.ServerV2)
if !ok {
return nil, trace.Errorf("node has invalid type %T", resource)
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_Node{Node: srv}}
case types.KindKubeService:
srv, ok := resource.(*types.ServerV2)
if !ok {
return nil, trace.Errorf("kubernetes service has invalid type %T", resource)
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_KubeService{KubeService: srv}}
case types.KindKubeServer:
srv, ok := resource.(*types.KubernetesServerV3)
if !ok {
return nil, trace.Errorf("kubernetes server has invalid type %T", resource)
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_KubernetesServer{KubernetesServer: srv}}
case types.KindWindowsDesktop:
desktop, ok := resource.(*types.WindowsDesktopV3)
if !ok {
return nil, trace.Errorf("windows desktop has invalid type %T", resource)
}
protoResource = &proto.PaginatedResource{Resource: &proto.PaginatedResource_WindowsDesktop{WindowsDesktop: desktop}}
}
resp.Resources = append(resp.Resources, protoResource)
lastResourceName = resource.GetName()
if len(resp.Resources) == int(req.Limit) {
break
}
}
if len(resp.Resources) != len(resources) {
resp.NextKey = lastResourceName
}
return resp, nil
}
func (m *mockServer) AddMFADeviceSync(ctx context.Context, req *proto.AddMFADeviceSyncRequest) (*proto.AddMFADeviceSyncResponse, error) {
return nil, status.Error(codes.AlreadyExists, "Already Exists")
}
const fiveMBNode = "fiveMBNode"
func testResources(resourceType, namespace string) ([]types.ResourceWithLabels, error) {
var err error
size := 50
// Artificially make each node ~ 100KB to force
// ListResources to fail with chunks of >= 40.
labelSize := 100000
resources := make([]types.ResourceWithLabels, size)
switch resourceType {
case types.KindDatabaseServer:
for i := 0; i < size; i++ {
resources[i], err = types.NewDatabaseServerV3(types.Metadata{
Name: fmt.Sprintf("db-%d", i),
Labels: map[string]string{
"label": string(make([]byte, labelSize)),
},
}, types.DatabaseServerSpecV3{
Protocol: "",
URI: "localhost:5432",
Hostname: "localhost",
HostID: fmt.Sprintf("host-%d", i),
})
if err != nil {
return nil, trace.Wrap(err)
}
}
case types.KindAppServer:
for i := 0; i < size; i++ {
app, err := types.NewAppV3(types.Metadata{
Name: fmt.Sprintf("app-%d", i),
}, types.AppSpecV3{
URI: "localhost",
})
if err != nil {
return nil, trace.Wrap(err)
}
resources[i], err = types.NewAppServerV3(types.Metadata{
Name: fmt.Sprintf("app-%d", i),
Labels: map[string]string{
"label": string(make([]byte, labelSize)),
},
}, types.AppServerSpecV3{
HostID: fmt.Sprintf("host-%d", i),
App: app,
})
if err != nil {
return nil, trace.Wrap(err)
}
}
case types.KindNode:
for i := 0; i < size; i++ {
nodeLabelSize := labelSize
if namespace == fiveMBNode && i == 0 {
// Artificially make a node ~ 5MB to force
// ListNodes to fail regardless of chunk size.
nodeLabelSize = 5000000
}
var err error
resources[i], err = types.NewServerWithLabels(fmt.Sprintf("node-%d", i), types.KindNode, types.ServerSpecV2{},
map[string]string{
"label": string(make([]byte, nodeLabelSize)),
},
)
if err != nil {
return nil, trace.Wrap(err)
}
}
case types.KindKubeServer:
for i := 0; i < size; i++ {
var err error
name := fmt.Sprintf("kube-service-%d", i)
resources[i], err = types.NewKubernetesServerV3(types.Metadata{
Name: name,
Labels: map[string]string{"name": name},
},
types.KubernetesServerSpecV3{
Hostname: "test",
Cluster: &types.KubernetesClusterV3{
Metadata: types.Metadata{
Name: name,
Labels: map[string]string{"name": name},
},
},
},
)
if err != nil {
return nil, trace.Wrap(err)
}
}
case types.KindKubeService:
for i := 0; i < size; i++ {
var err error
name := fmt.Sprintf("kube-service-%d", i)
resources[i], err = types.NewServerWithLabels(name, types.KindKubeService, types.ServerSpecV2{
KubernetesClusters: []*types.KubernetesCluster{
{Name: name, StaticLabels: map[string]string{"name": name}},
},
}, map[string]string{
"label": string(make([]byte, labelSize)),
})
if err != nil {
return nil, trace.Wrap(err)
}
}
case types.KindWindowsDesktop:
for i := 0; i < size; i++ {
var err error
name := fmt.Sprintf("windows-desktop-%d", i)
resources[i], err = types.NewWindowsDesktopV3(
name,
map[string]string{"label": string(make([]byte, labelSize))},
types.WindowsDesktopSpecV3{
Addr: "_",
HostID: "_",
})
if err != nil {
return nil, trace.Wrap(err)
}
}
default:
return nil, trace.Errorf("unsupported resource type %s", resourceType)
}
return resources, nil
}
// mockInsecureCredentials mocks insecure Client credentials.
// it returns a nil tlsConfig which allows the client to run in insecure mode.
// TODO(Joerger) replace insecure credentials with proper TLS credentials.
type mockInsecureTLSCredentials struct{}
func (mc *mockInsecureTLSCredentials) Dialer(cfg Config) (ContextDialer, error) {
return nil, trace.NotImplemented("no dialer")
}
func (mc *mockInsecureTLSCredentials) TLSConfig() (*tls.Config, error) {
return nil, nil
}
func (mc *mockInsecureTLSCredentials) SSHClientConfig() (*ssh.ClientConfig, error) {
return nil, trace.NotImplemented("no ssh config")
}
func TestNew(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := startMockServer(t)
tests := []struct {
desc string
config Config
assertErr require.ErrorAssertionFunc
}{{
desc: "successfully dial tcp address.",
config: Config{
Addrs: []string{srv.Addr()},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
},
assertErr: require.NoError,
}, {
desc: "synchronously dial addr/cred pairs and succeed with the 1 good pair.",
config: Config{
Addrs: []string{"bad addr", srv.Addr(), "bad addr"},
Credentials: []Credentials{
&tlsConfigCreds{nil},
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
&tlsConfigCreds{nil},
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
},
assertErr: require.NoError,
}, {
desc: "fail to dial with a bad address.",
config: Config{
DialTimeout: time.Second,
Addrs: []string{"bad addr"},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
},
assertErr: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "all connection methods failed")
},
}, {
desc: "fail to dial with no address or dialer.",
config: Config{
DialTimeout: time.Second,
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
},
assertErr: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "no connection methods found, try providing Dialer or Addrs in config")
},
}}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
clt, err := srv.NewClient(ctx, WithConfig(tt.config))
tt.assertErr(t, err)
if err == nil {
t.Cleanup(func() { require.NoError(t, clt.Close()) })
// requests to the server should succeed.
_, err = clt.Ping(ctx)
require.NoError(t, err)
}
})
}
}
func TestNewDialBackground(t *testing.T) {
t.Parallel()
ctx := context.Background()
// get listener but don't serve it yet.
l, err := net.Listen("tcp", "")
require.NoError(t, err)
addr := l.Addr().String()
// Create client before the server is listening.
clt, err := New(ctx, Config{
DialInBackground: true,
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, clt.Close()) })
// requests to the server will result in a connection error.
cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
_, err = clt.Ping(cancelCtx)
require.Error(t, err)
// Start the server and wait for the client connection to be ready.
startMockServerWithListener(t, l)
require.NoError(t, clt.waitForConnectionReady(ctx))
// requests to the server should succeed.
_, err = clt.Ping(ctx)
require.NoError(t, err)
}
func TestWaitForConnectionReady(t *testing.T) {
t.Parallel()
ctx := context.Background()
l, err := net.Listen("tcp", "")
require.NoError(t, err)
addr := l.Addr().String()
// Create client before the server is listening.
clt, err := New(ctx, Config{
DialInBackground: true,
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, clt.Close()) })
// WaitForConnectionReady should return false once the
// context is canceled if the server isn't open to connections.
cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
require.Error(t, clt.waitForConnectionReady(cancelCtx))
// WaitForConnectionReady should return nil if the server is open to connections.
startMockServerWithListener(t, l)
require.NoError(t, clt.waitForConnectionReady(ctx))
// WaitForConnectionReady should return an error if the grpc connection is closed.
require.NoError(t, clt.Close())
require.Error(t, clt.waitForConnectionReady(ctx))
}
func TestListResources(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := startMockServer(t)
testCases := map[string]struct {
resourceType string
resourceStruct types.Resource
}{
"DatabaseServer": {
resourceType: types.KindDatabaseServer,
resourceStruct: &types.DatabaseServerV3{},
},
"ApplicationServer": {
resourceType: types.KindAppServer,
resourceStruct: &types.AppServerV3{},
},
"Node": {
resourceType: types.KindNode,
resourceStruct: &types.ServerV2{},
},
"KubeService": {
resourceType: types.KindKubeService,
resourceStruct: &types.ServerV2{},
},
"WindowsDesktop": {
resourceType: types.KindWindowsDesktop,
resourceStruct: &types.WindowsDesktopV3{},
},
}
// Create client
clt, err := srv.NewClient(ctx)
require.NoError(t, err)
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{
Namespace: defaults.Namespace,
Limit: 10,
ResourceType: test.resourceType,
})
require.NoError(t, err)
require.NotEmpty(t, resp.NextKey)
require.Len(t, resp.Resources, 10)
require.IsType(t, test.resourceStruct, resp.Resources[0])
// exceed the limit
_, err = clt.ListResources(ctx, proto.ListResourcesRequest{
Namespace: defaults.Namespace,
Limit: 50,
ResourceType: test.resourceType,
})
require.Error(t, err)
require.IsType(t, &trace.LimitExceededError{}, err.(*trace.TraceErr).OrigError())
})
}
// Test a list with total count returned.
resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindNode,
Limit: 10,
NeedTotalCount: true,
})
require.NoError(t, err)
require.Equal(t, 50, resp.TotalCount)
}
func TestGetResources(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := startMockServer(t)
// Create client
clt, err := srv.NewClient(ctx)
require.NoError(t, err)
testCases := map[string]struct {
resourceType string
}{
"DatabaseServer": {
resourceType: types.KindDatabaseServer,
},
"ApplicationServer": {
resourceType: types.KindAppServer,
},
"Node": {
resourceType: types.KindNode,
},
"KubeService": {
resourceType: types.KindKubeService,
},
"WindowsDesktop": {
resourceType: types.KindWindowsDesktop,
},
}
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
expectedResources, err := testResources(test.resourceType, defaults.Namespace)
require.NoError(t, err)
// Test listing everything at once errors with limit exceeded.
_, err = clt.ListResources(ctx, proto.ListResourcesRequest{
Namespace: defaults.Namespace,
Limit: int32(len(expectedResources)),
ResourceType: test.resourceType,
})
require.Error(t, err)
require.IsType(t, &trace.LimitExceededError{}, err.(*trace.TraceErr).OrigError())
// Test getting all resources by chunks to handle limit exceeded.
resources, err := GetResourcesWithFilters(ctx, clt, proto.ListResourcesRequest{
Namespace: defaults.Namespace,
ResourceType: test.resourceType,
})
require.NoError(t, err)
require.Len(t, resources, len(expectedResources))
require.Empty(t, cmp.Diff(expectedResources, resources))
})
}
}
type mockOIDCConnectorServer struct {
*mockServer
connectors map[string]*types.OIDCConnectorV3
}
func newMockOIDCConnectorServer() *mockOIDCConnectorServer {
m := &mockOIDCConnectorServer{
&mockServer{
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
},
make(map[string]*types.OIDCConnectorV3),
}
proto.RegisterAuthServiceServer(m.grpc, m)
return m
}
func startMockOIDCConnectorServer(t *testing.T) string {
l, err := net.Listen("tcp", "")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, l.Close()) })
go newMockOIDCConnectorServer().grpc.Serve(l)
return l.Addr().String()
}
func (m *mockOIDCConnectorServer) GetOIDCConnector(ctx context.Context, req *types.ResourceWithSecretsRequest) (*types.OIDCConnectorV3, error) {
conn, ok := m.connectors[req.Name]
if !ok {
return nil, trace.NotFound("not found")
}
return conn, nil
}
func (m *mockOIDCConnectorServer) GetOIDCConnectors(ctx context.Context, req *types.ResourcesWithSecretsRequest) (*types.OIDCConnectorV3List, error) {
var connectors []*types.OIDCConnectorV3
for _, conn := range m.connectors {
connectors = append(connectors, conn)
}
return &types.OIDCConnectorV3List{
OIDCConnectors: connectors,
}, nil
}
func (m *mockOIDCConnectorServer) UpsertOIDCConnector(ctx context.Context, oidcConnector *types.OIDCConnectorV3) (*emptypb.Empty, error) {
m.connectors[oidcConnector.Metadata.Name] = oidcConnector
return &emptypb.Empty{}, nil
}
// Test that client will perform properly with an old server
// DELETE IN 11.0.0
func TestSetOIDCRedirectURLBackwardsCompatibility(t *testing.T) {
ctx := context.Background()
addr := startMockOIDCConnectorServer(t)
// Create client
clt, err := New(ctx, Config{
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)
conn := &types.OIDCConnectorV3{
Metadata: types.Metadata{
Name: "one",
},
}
// Upsert should set "RedirectURL" on the provided connector if empty
conn.Spec.RedirectURLs = []string{"one.example.com"}
conn.Spec.RedirectURL = ""
err = clt.UpsertOIDCConnector(ctx, conn)
require.NoError(t, err)
require.Equal(t, 1, len(conn.GetRedirectURLs()))
require.Equal(t, conn.GetRedirectURLs()[0], conn.Spec.RedirectURL)
// GetOIDCConnector should set "RedirectURLs" on the received connector if empty
conn.Spec.RedirectURLs = []string{}
conn.Spec.RedirectURL = "one.example.com"
connResp, err := clt.GetOIDCConnector(ctx, conn.GetName(), false)
require.NoError(t, err)
require.Equal(t, 1, len(connResp.GetRedirectURLs()))
require.Equal(t, connResp.GetRedirectURLs()[0], "one.example.com")
// GetOIDCConnectors should set "RedirectURLs" on the received connectors if empty
conn.Spec.RedirectURLs = []string{}
conn.Spec.RedirectURL = "one.example.com"
connectorsResp, err := clt.GetOIDCConnectors(ctx, false)
require.NoError(t, err)
require.Equal(t, 1, len(connectorsResp))
require.Equal(t, 1, len(connectorsResp[0].GetRedirectURLs()))
require.Equal(t, "one.example.com", connectorsResp[0].GetRedirectURLs()[0])
}
type mockAccessRequestServer struct {
*mockServer
}
func (g *mockAccessRequestServer) GetAccessRequests(ctx context.Context, f *types.AccessRequestFilter) (*proto.AccessRequests, error) {
req, err := types.NewAccessRequest("foo", "bob", "admin")
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.AccessRequests{
AccessRequests: []*types.AccessRequestV3{req.(*types.AccessRequestV3)},
}, nil
}
// TestAccessRequestDowngrade tests that the client will downgrade to the non stream API for fetching access requests
// if the stream API is not available.
func TestAccessRequestDowngrade(t *testing.T) {
ctx := context.Background()
l, err := net.Listen("tcp", "")
require.NoError(t, err)
m := &mockAccessRequestServer{
&mockServer{
addr: l.Addr().String(),
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
},
}
proto.RegisterAuthServiceServer(m.grpc, m)
t.Cleanup(m.grpc.Stop)
remoteErr := make(chan error)
go func() {
remoteErr <- m.grpc.Serve(l)
}()
clt, err := m.NewClient(ctx)
require.NoError(t, err)
items, err := clt.GetAccessRequests(ctx, types.AccessRequestFilter{})
require.NoError(t, err)
require.Len(t, items, 1)
m.grpc.Stop()
require.NoError(t, <-remoteErr)
}
type mockRoleServer struct {
*mockServer
roles map[string]*types.RoleV5
}
func newMockRoleServer() *mockRoleServer {
m := &mockRoleServer{
&mockServer{
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
},
make(map[string]*types.RoleV5),
}
proto.RegisterAuthServiceServer(m.grpc, m)
return m
}
func startMockRoleServer(t *testing.T) string {
l, err := net.Listen("tcp", "")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, l.Close()) })
go newMockRoleServer().grpc.Serve(l)
return l.Addr().String()
}
func (m *mockRoleServer) GetRole(ctx context.Context, req *proto.GetRoleRequest) (*types.RoleV5, error) {
conn, ok := m.roles[req.Name]
if !ok {
return nil, trace.NotFound("not found")
}
return conn, nil
}
func (m *mockRoleServer) GetRoles(ctx context.Context, _ *emptypb.Empty) (*proto.GetRolesResponse, error) {
var connectors []*types.RoleV5
for _, conn := range m.roles {
connectors = append(connectors, conn)
}
return &proto.GetRolesResponse{
Roles: connectors,
}, nil
}
func (m *mockRoleServer) UpsertRole(ctx context.Context, role *types.RoleV5) (*emptypb.Empty, error) {
m.roles[role.Metadata.Name] = role
return &emptypb.Empty{}, nil
}
func (m *mockRoleServer) GetCurrentUserRoles(_ *emptypb.Empty, stream proto.AuthService_GetCurrentUserRolesServer) error {
for _, role := range m.roles {
if err := stream.Send(role); err != nil {
return trace.Wrap(err)
}
}
return nil
}
// Test that client will perform properly with an old server
// DELETE IN 13.0.0
func TestSetRoleRequireSessionMFABackwardsCompatibility(t *testing.T) {
ctx := context.Background()
addr := startMockRoleServer(t)
// Create client
clt, err := New(ctx, Config{
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)
role := &types.RoleV5{
Metadata: types.Metadata{
Name: "one",
},
}
t.Run("UpsertRole", func(t *testing.T) {
// UpsertRole should set "RequireSessionMFA" on the provided role if "RequireMFAType" is set
role.Spec.Options.RequireMFAType = types.RequireMFAType_SESSION
role.Spec.Options.RequireSessionMFA = false
err = clt.UpsertRole(ctx, role)
require.NoError(t, err)
require.True(t, role.GetOptions().RequireSessionMFA)
})
t.Run("GetRole", func(t *testing.T) {
// GetRole should set "RequireMFAType" on the received role if empty
role.Spec.Options.RequireMFAType = 0
role.Spec.Options.RequireSessionMFA = true
roleResp, err := clt.GetRole(ctx, role.GetName())
require.NoError(t, err)
require.Equal(t, types.RequireMFAType_SESSION, roleResp.GetOptions().RequireMFAType)
})
t.Run("GetRoles", func(t *testing.T) {
// GetRoles should set "RequireMFAType" on the received roles if empty
role.Spec.Options.RequireMFAType = 0
role.Spec.Options.RequireSessionMFA = true
rolesResp, err := clt.GetRoles(ctx)
require.NoError(t, err)
require.Len(t, rolesResp, 1)
require.Equal(t, types.RequireMFAType_SESSION, rolesResp[0].GetOptions().RequireMFAType)
})
t.Run("GetCurrentUserRoles", func(t *testing.T) {
// GetCurrentUserRoles should set "RequireMFAType" on the received roles if empty
role.Spec.Options.RequireMFAType = 0
role.Spec.Options.RequireSessionMFA = true
rolesResp, err := clt.GetCurrentUserRoles(ctx)
require.NoError(t, err)
require.Len(t, rolesResp, 1)
require.Equal(t, types.RequireMFAType_SESSION, rolesResp[0].GetOptions().RequireMFAType)
})
}
type mockAuthPreferenceServer struct {
*mockServer
pref *types.AuthPreferenceV2
}
func newMockAuthPreferenceServer() *mockAuthPreferenceServer {
m := &mockAuthPreferenceServer{
mockServer: &mockServer{
grpc: grpc.NewServer(),
UnimplementedAuthServiceServer: &proto.UnimplementedAuthServiceServer{},
},
}
proto.RegisterAuthServiceServer(m.grpc, m)
return m
}
func startMockAuthPreferenceServer(t *testing.T) string {
l, err := net.Listen("tcp", "")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, l.Close()) })
go newMockAuthPreferenceServer().grpc.Serve(l)
return l.Addr().String()
}
func (m *mockAuthPreferenceServer) GetAuthPreference(ctx context.Context, _ *emptypb.Empty) (*types.AuthPreferenceV2, error) {
if m.pref == nil {
return nil, trace.NotFound("not found")
}
return m.pref, nil
}
func (m *mockAuthPreferenceServer) SetAuthPreference(ctx context.Context, pref *types.AuthPreferenceV2) (*emptypb.Empty, error) {
m.pref = pref
return &emptypb.Empty{}, nil
}
// Test that client will perform properly with an old server
// DELETE IN 13.0.0
func TestSetAuthPreferenceRequireSessionMFABackwardsCompatibility(t *testing.T) {
ctx := context.Background()
addr := startMockAuthPreferenceServer(t)
// Create client
clt, err := New(ctx, Config{
Addrs: []string{addr},
Credentials: []Credentials{
&mockInsecureTLSCredentials{}, // TODO(Joerger) replace insecure credentials
},
DialOpts: []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()), // TODO(Joerger) remove insecure dial option
},
})
require.NoError(t, err)
pref := &types.AuthPreferenceV2{
Metadata: types.Metadata{
Name: "one",
},
}
t.Run("SetAuthPreference", func(t *testing.T) {
// SetAuthPreference should set "RequireSessionMFA" on the provided auth pref if "RequireMFAType" is set
pref.Spec.RequireMFAType = types.RequireMFAType_SESSION
pref.Spec.RequireSessionMFA = false
err = clt.SetAuthPreference(ctx, pref)
require.NoError(t, err)
require.True(t, pref.Spec.RequireSessionMFA)
})
t.Run("GetAuthPreference", func(t *testing.T) {
// GetAuthPreference should set "RequireMFAType" on the received auth pref if empty
pref.Spec.RequireMFAType = 0
pref.Spec.RequireSessionMFA = true
prefResp, err := clt.GetAuthPreference(ctx)
require.NoError(t, err)
require.Equal(t, types.RequireMFAType_SESSION, prefResp.GetRequireMFAType())
})
}