/* 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 auth import ( "context" "encoding/base32" "fmt" "net" "sort" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/metadata" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth/mocku2f" wanlib "github.com/gravitational/teleport/lib/auth/webauthn" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestMFADeviceManagement(t *testing.T) { ctx := context.Background() srv := newTestTLSServer(t) clock := srv.Clock().(clockwork.FakeClock) // Enable MFA support. authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ Type: constants.Local, SecondFactor: constants.SecondFactorOptional, Webauthn: &types.Webauthn{ RPID: "localhost", }, }) const webOrigin = "https://localhost" // matches RPID above require.NoError(t, err) err = srv.Auth().SetAuthPreference(ctx, authPref) require.NoError(t, err) // Create a fake user. user, _, err := CreateUserAndRole(srv.Auth(), "mfa-user", []string{"role"}) require.NoError(t, err) cl, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) // No MFA devices should exist for a new user. resp, err := cl.GetMFADevices(ctx, &proto.GetMFADevicesRequest{}) require.NoError(t, err) require.Empty(t, resp.Devices) // Add one device of each kind devs := addOneOfEachMFADevice(t, cl, clock, webOrigin) // Run AddMFADevice tests, including adding additional devices and failures. webKey2, err := mocku2f.Create() require.NoError(t, err) webKey2.PreferRPID = true const webDev2Name = "webauthn2" const pwdlessDevName = "pwdless" addTests := []struct { desc string opts mfaAddTestOpts }{ { desc: "fail TOTP auth challenge", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: "fail-dev", DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN, }, authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { require.NotNil(t, req.TOTP) // Respond to challenge using an unregistered TOTP device, // which should fail the auth challenge. badDev, err := totp.Generate(totp.GenerateOpts{Issuer: "Teleport", AccountName: user.GetName()}) require.NoError(t, err) code, err := totp.GenerateCode(badDev.Secret(), clock.Now()) require.NoError(t, err) return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{TOTP: &proto.TOTPResponse{ Code: code, }}} }, checkAuthErr: require.Error, }, }, { desc: "fail a TOTP registration challenge", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: "fail-dev", DeviceType: proto.DeviceType_DEVICE_TYPE_TOTP, }, authHandler: devs.totpAuthHandler, checkAuthErr: require.NoError, registerHandler: func(t *testing.T, req *proto.MFARegisterChallenge) *proto.MFARegisterResponse { totpRegisterChallenge := req.GetTOTP() require.NotEmpty(t, totpRegisterChallenge) require.Equal(t, totpRegisterChallenge.Algorithm, otp.AlgorithmSHA1.String()) // Use the wrong secret for registration, causing server // validation to fail. code, err := totp.GenerateCodeCustom(base32.StdEncoding.EncodeToString([]byte("wrong-secret")), clock.Now(), totp.ValidateOpts{ Period: uint(totpRegisterChallenge.PeriodSeconds), Digits: otp.Digits(totpRegisterChallenge.Digits), Algorithm: otp.AlgorithmSHA1, }) require.NoError(t, err) return &proto.MFARegisterResponse{ Response: &proto.MFARegisterResponse_TOTP{TOTP: &proto.TOTPRegisterResponse{ Code: code, }}, } }, checkRegisterErr: require.Error, }, }, { desc: "add a second webauthn device", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: webDev2Name, DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN, }, authHandler: devs.webAuthHandler, checkAuthErr: require.NoError, registerHandler: func(t *testing.T, challenge *proto.MFARegisterChallenge) *proto.MFARegisterResponse { ccr, err := webKey2.SignCredentialCreation(webOrigin, wanlib.CredentialCreationFromProto(challenge.GetWebauthn())) require.NoError(t, err) return &proto.MFARegisterResponse{ Response: &proto.MFARegisterResponse_Webauthn{ Webauthn: wanlib.CredentialCreationResponseToProto(ccr), }, } }, checkRegisterErr: require.NoError, }, }, { desc: "fail a webauthn auth challenge", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: "webauthn-1512000", DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN, }, authHandler: func(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { require.NotNil(t, challenge.WebauthnChallenge) // webauthn enabled // Sign challenge with an unknown device. key, err := mocku2f.Create() require.NoError(t, err) key.PreferRPID = true key.IgnoreAllowedCredentials = true resp, err := key.SignAssertion(webOrigin, wanlib.CredentialAssertionFromProto(challenge.WebauthnChallenge)) require.NoError(t, err) return &proto.MFAAuthenticateResponse{ Response: &proto.MFAAuthenticateResponse_Webauthn{ Webauthn: wanlib.CredentialAssertionResponseToProto(resp), }, } }, checkAuthErr: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.Equal(t, codes.PermissionDenied, status.Code(err)) }, }, }, { desc: "fail a webauthn registration challenge", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: "webauthn-1512000", DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN, }, authHandler: devs.webAuthHandler, checkAuthErr: require.NoError, registerHandler: func(t *testing.T, challenge *proto.MFARegisterChallenge) *proto.MFARegisterResponse { require.NotNil(t, challenge.GetWebauthn()) key, err := mocku2f.Create() require.NoError(t, err) key.PreferRPID = true ccr, err := key.SignCredentialCreation( "http://badorigin.com" /* origin */, wanlib.CredentialCreationFromProto(challenge.GetWebauthn())) require.NoError(t, err) return &proto.MFARegisterResponse{ Response: &proto.MFARegisterResponse_Webauthn{ Webauthn: wanlib.CredentialCreationResponseToProto(ccr), }, } }, checkRegisterErr: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) require.Equal(t, codes.InvalidArgument, status.Code(err)) }, }, }, { desc: "add passwordless device", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: pwdlessDevName, DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN, DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS, }, authHandler: devs.webAuthHandler, checkAuthErr: require.NoError, registerHandler: func(t *testing.T, challenge *proto.MFARegisterChallenge) *proto.MFARegisterResponse { require.NotNil(t, challenge.GetWebauthn(), "WebAuthn challenge cannot be nil") key, err := mocku2f.Create() require.NoError(t, err) key.PreferRPID = true key.SetPasswordless() ccr, err := key.SignCredentialCreation(webOrigin, wanlib.CredentialCreationFromProto(challenge.GetWebauthn())) require.NoError(t, err) return &proto.MFARegisterResponse{ Response: &proto.MFARegisterResponse_Webauthn{ Webauthn: wanlib.CredentialCreationResponseToProto(ccr), }, } }, checkRegisterErr: require.NoError, assertRegisteredDev: func(t *testing.T, dev *types.MFADevice) { // Do a few simple device checks - lib/auth/webauthn goes in depth. require.NotNil(t, dev.GetWebauthn(), "WebAuthnDevice cannot be nil") require.True(t, true, dev.GetWebauthn().ResidentKey, "ResidentKey should be set to true") }, }, }, } for _, tt := range addTests { t.Run(tt.desc, func(t *testing.T) { testAddMFADevice(ctx, t, cl, tt.opts) }) } // Check that all new devices are registered. resp, err = cl.GetMFADevices(ctx, &proto.GetMFADevicesRequest{}) require.NoError(t, err) deviceNames := make([]string, 0, len(resp.Devices)) deviceIDs := make(map[string]string) for _, dev := range resp.Devices { deviceNames = append(deviceNames, dev.GetName()) deviceIDs[dev.GetName()] = dev.Id } sort.Strings(deviceNames) require.Equal(t, deviceNames, []string{pwdlessDevName, devs.TOTPName, devs.WebName, webDev2Name}) // Delete several of the MFA devices. deleteTests := []struct { desc string opts mfaDeleteTestOpts }{ { desc: "fail to delete an unknown device", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: "unknown-dev", }, authHandler: devs.totpAuthHandler, checkErr: require.Error, }, }, { desc: "fail a TOTP auth challenge", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.TOTPName, }, authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { require.NotNil(t, req.TOTP) // Respond to challenge using an unregistered TOTP device, // which should fail the auth challenge. badDev, err := totp.Generate(totp.GenerateOpts{Issuer: "Teleport", AccountName: user.GetName()}) require.NoError(t, err) code, err := totp.GenerateCode(badDev.Secret(), clock.Now()) require.NoError(t, err) return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{TOTP: &proto.TOTPResponse{ Code: code, }}} }, checkErr: require.Error, }, }, { desc: "fail a webauthn auth challenge", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.WebName, }, authHandler: func(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { require.NotNil(t, challenge.WebauthnChallenge) // Sign challenge with an unknown device. key, err := mocku2f.Create() require.NoError(t, err) key.PreferRPID = true key.IgnoreAllowedCredentials = true resp, err := key.SignAssertion(webOrigin, wanlib.CredentialAssertionFromProto(challenge.WebauthnChallenge)) require.NoError(t, err) return &proto.MFAAuthenticateResponse{ Response: &proto.MFAAuthenticateResponse_Webauthn{ Webauthn: wanlib.CredentialAssertionResponseToProto(resp), }, } }, checkErr: require.Error, }, }, { desc: "delete TOTP device by name", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.TOTPName, }, authHandler: devs.totpAuthHandler, checkErr: require.NoError, }, }, { desc: "delete pwdless device by name", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: pwdlessDevName, }, authHandler: devs.webAuthHandler, checkErr: require.NoError, }, }, { desc: "delete webauthn device by name", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.WebName, }, authHandler: devs.webAuthHandler, checkErr: require.NoError, }, }, { desc: "delete webauthn device by ID", opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: deviceIDs[webDev2Name], }, authHandler: func(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { resp, err := webKey2.SignAssertion( webOrigin, wanlib.CredentialAssertionFromProto(challenge.WebauthnChallenge)) require.NoError(t, err) return &proto.MFAAuthenticateResponse{ Response: &proto.MFAAuthenticateResponse_Webauthn{ Webauthn: wanlib.CredentialAssertionResponseToProto(resp), }, } }, checkErr: require.NoError, }, }, } for _, tt := range deleteTests { t.Run(tt.desc, func(t *testing.T) { testDeleteMFADevice(ctx, t, cl, tt.opts) }) } // Check the remaining number of devices resp, err = cl.GetMFADevices(ctx, &proto.GetMFADevicesRequest{}) require.NoError(t, err) require.Empty(t, resp.Devices) } type mfaDevices struct { clock clockwork.Clock webOrigin string TOTPName, TOTPSecret string WebName string WebKey *mocku2f.Key } func (d *mfaDevices) totpAuthHandler(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { require.NotNil(t, challenge.TOTP) if c, ok := d.clock.(clockwork.FakeClock); ok { c.Advance(30 * time.Second) } code, err := totp.GenerateCode(d.TOTPSecret, d.clock.Now()) require.NoError(t, err) return &proto.MFAAuthenticateResponse{ Response: &proto.MFAAuthenticateResponse_TOTP{ TOTP: &proto.TOTPResponse{ Code: code, }, }, } } func (d *mfaDevices) webAuthHandler(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { require.NotNil(t, challenge.WebauthnChallenge) resp, err := d.WebKey.SignAssertion( d.webOrigin, wanlib.CredentialAssertionFromProto(challenge.WebauthnChallenge)) require.NoError(t, err) return &proto.MFAAuthenticateResponse{ Response: &proto.MFAAuthenticateResponse_Webauthn{ Webauthn: wanlib.CredentialAssertionResponseToProto(resp), }, } } func addOneOfEachMFADevice(t *testing.T, cl *Client, clock clockwork.Clock, origin string) mfaDevices { const totpName = "totp-dev" const webName = "webauthn-dev" devs := mfaDevices{ clock: clock, webOrigin: origin, TOTPName: totpName, WebName: webName, } var err error devs.WebKey, err = mocku2f.Create() require.NoError(t, err) devs.WebKey.PreferRPID = true // Add MFA devices of all kinds. ctx := context.Background() for _, test := range []struct { name string opts mfaAddTestOpts }{ { name: "TOTP device", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: totpName, DeviceType: proto.DeviceType_DEVICE_TYPE_TOTP, }, authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { // Empty for first device. return &proto.MFAAuthenticateResponse{} }, checkAuthErr: require.NoError, registerHandler: func(t *testing.T, challenge *proto.MFARegisterChallenge) *proto.MFARegisterResponse { require.NotEmpty(t, challenge.GetTOTP()) require.Equal(t, challenge.GetTOTP().Algorithm, otp.AlgorithmSHA1.String()) devs.TOTPSecret = challenge.GetTOTP().Secret code, err := totp.GenerateCodeCustom(devs.TOTPSecret, clock.Now(), totp.ValidateOpts{ Period: uint(challenge.GetTOTP().PeriodSeconds), Digits: otp.Digits(challenge.GetTOTP().Digits), Algorithm: otp.AlgorithmSHA1, }) require.NoError(t, err) return &proto.MFARegisterResponse{ Response: &proto.MFARegisterResponse_TOTP{ TOTP: &proto.TOTPRegisterResponse{ Code: code, }, }, } }, checkRegisterErr: require.NoError, assertRegisteredDev: func(t *testing.T, got *types.MFADevice) { want, err := services.NewTOTPDevice(totpName, devs.TOTPSecret, clock.Now()) want.Id = got.Id require.NoError(t, err) require.Empty(t, cmp.Diff(want, got)) }, }, }, { name: "Webauthn device", opts: mfaAddTestOpts{ initReq: &proto.AddMFADeviceRequestInit{ DeviceName: webName, DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN, }, authHandler: devs.totpAuthHandler, checkAuthErr: require.NoError, registerHandler: func(t *testing.T, challenge *proto.MFARegisterChallenge) *proto.MFARegisterResponse { require.NotNil(t, challenge.GetWebauthn()) ccr, err := devs.WebKey.SignCredentialCreation(origin, wanlib.CredentialCreationFromProto(challenge.GetWebauthn())) require.NoError(t, err) return &proto.MFARegisterResponse{ Response: &proto.MFARegisterResponse_Webauthn{ Webauthn: wanlib.CredentialCreationResponseToProto(ccr), }, } }, checkRegisterErr: require.NoError, assertRegisteredDev: func(t *testing.T, got *types.MFADevice) { // MFADevice device asserted in its entirety by lib/auth/webauthn // tests, a simple check suffices here. require.Equal(t, devs.WebKey.KeyHandle, got.GetWebauthn().CredentialId) }, }, }, } { t.Run(test.name, func(t *testing.T) { testAddMFADevice(ctx, t, cl, test.opts) }) } return devs } type mfaAddTestOpts struct { initReq *proto.AddMFADeviceRequestInit authHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse checkAuthErr require.ErrorAssertionFunc registerHandler func(*testing.T, *proto.MFARegisterChallenge) *proto.MFARegisterResponse checkRegisterErr require.ErrorAssertionFunc assertRegisteredDev func(*testing.T, *types.MFADevice) } func testAddMFADevice(ctx context.Context, t *testing.T, cl *Client, opts mfaAddTestOpts) { addStream, err := cl.AddMFADevice(ctx) require.NoError(t, err) err = addStream.Send(&proto.AddMFADeviceRequest{Request: &proto.AddMFADeviceRequest_Init{Init: opts.initReq}}) require.NoError(t, err) authChallenge, err := addStream.Recv() require.NoError(t, err) authResp := opts.authHandler(t, authChallenge.GetExistingMFAChallenge()) err = addStream.Send(&proto.AddMFADeviceRequest{Request: &proto.AddMFADeviceRequest_ExistingMFAResponse{ExistingMFAResponse: authResp}}) require.NoError(t, err) registerChallenge, err := addStream.Recv() opts.checkAuthErr(t, err) if err != nil { return } registerResp := opts.registerHandler(t, registerChallenge.GetNewMFARegisterChallenge()) err = addStream.Send(&proto.AddMFADeviceRequest{Request: &proto.AddMFADeviceRequest_NewMFARegisterResponse{NewMFARegisterResponse: registerResp}}) require.NoError(t, err) registerAck, err := addStream.Recv() opts.checkRegisterErr(t, err) if err != nil { return } if opts.assertRegisteredDev != nil { opts.assertRegisteredDev(t, registerAck.GetAck().GetDevice()) } require.NoError(t, addStream.CloseSend()) } type mfaDeleteTestOpts struct { initReq *proto.DeleteMFADeviceRequestInit authHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse checkErr require.ErrorAssertionFunc } func testDeleteMFADevice(ctx context.Context, t *testing.T, cl *Client, opts mfaDeleteTestOpts) { deleteStream, err := cl.DeleteMFADevice(ctx) require.NoError(t, err) err = deleteStream.Send(&proto.DeleteMFADeviceRequest{Request: &proto.DeleteMFADeviceRequest_Init{Init: opts.initReq}}) require.NoError(t, err) authChallenge, err := deleteStream.Recv() require.NoError(t, err) authResp := opts.authHandler(t, authChallenge.GetMFAChallenge()) err = deleteStream.Send(&proto.DeleteMFADeviceRequest{Request: &proto.DeleteMFADeviceRequest_MFAResponse{MFAResponse: authResp}}) require.NoError(t, err) deleteAck, err := deleteStream.Recv() opts.checkErr(t, err) if err != nil { return } require.Empty(t, cmp.Diff(deleteAck.GetAck(), &proto.DeleteMFADeviceResponseAck{})) require.NoError(t, deleteStream.CloseSend()) } func TestDeleteLastMFADevice(t *testing.T) { ctx := context.Background() srv := newTestTLSServer(t) // Enable MFA support. authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ Type: constants.Local, SecondFactor: constants.SecondFactorOptional, Webauthn: &types.Webauthn{ RPID: "localhost", }, }) const webOrigin = "https://localhost" // matches RPID above require.NoError(t, err) auth := srv.Auth() err = auth.SetAuthPreference(ctx, authPref) require.NoError(t, err) // Create a fake user. user, _, err := CreateUserAndRole(auth, "mfa-user", []string{"role"}) require.NoError(t, err) cl, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) // Add devices devs := addOneOfEachMFADevice(t, cl, srv.Clock(), webOrigin) tests := []struct { name string secondFactor constants.SecondFactorType opts mfaDeleteTestOpts }{ { name: "NOK sf=OTP trying to delete last OTP device", secondFactor: constants.SecondFactorOTP, opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.TOTPName, }, authHandler: devs.totpAuthHandler, checkErr: require.Error, }, }, { name: "NOK sf=Webauthn trying to delete last Webauthn device", secondFactor: constants.SecondFactorWebauthn, opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.WebName, }, authHandler: devs.webAuthHandler, checkErr: require.Error, }, }, { name: "OK delete OTP device", secondFactor: constants.SecondFactorOn, opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.TOTPName, }, authHandler: devs.totpAuthHandler, checkErr: require.NoError, }, }, { name: "NOK sf=on trying to delete last MFA device", secondFactor: constants.SecondFactorOn, opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.WebName, }, authHandler: devs.webAuthHandler, checkErr: require.Error, }, }, { name: "OK sf=optional delete last device (webauthn)", secondFactor: constants.SecondFactorOptional, opts: mfaDeleteTestOpts{ initReq: &proto.DeleteMFADeviceRequestInit{ DeviceName: devs.WebName, }, authHandler: devs.webAuthHandler, checkErr: require.NoError, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // Update second factor settings, if necessary. cap, err := auth.GetAuthPreference(ctx) require.NoError(t, err) if cap.GetSecondFactor() != test.secondFactor { cap.SetSecondFactor(test.secondFactor) require.NoError(t, auth.SetAuthPreference(ctx, cap)) } testDeleteMFADevice(ctx, t, cl, test.opts) }) } } func TestGenerateUserSingleUseCert(t *testing.T) { ctx := context.Background() srv := newTestTLSServer(t) clock := srv.Clock() authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ Type: constants.Local, SecondFactor: constants.SecondFactorOn, Webauthn: &types.Webauthn{ RPID: "localhost", }, }) const webOrigin = "https://localhost" // matches RPID above require.NoError(t, err) err = srv.Auth().SetAuthPreference(ctx, authPref) require.NoError(t, err) // Register an SSH node. node := &types.ServerV2{ Kind: types.KindKubeService, Version: types.V2, Metadata: types.Metadata{ Name: "node-a", }, Spec: types.ServerSpecV2{ Hostname: "node-a", }, } _, err = srv.Auth().UpsertNode(ctx, node) require.NoError(t, err) // Register a k8s cluster. k8sSrv := &types.ServerV2{ Kind: types.KindKubeService, Version: types.V2, Metadata: types.Metadata{ Name: "kube-a", }, Spec: types.ServerSpecV2{ KubernetesClusters: []*types.KubernetesCluster{{Name: "kube-a"}}, }, } _, err = srv.Auth().UpsertKubeServiceV2(ctx, k8sSrv) require.NoError(t, err) // Register a database. db, err := types.NewDatabaseServerV3(types.Metadata{ Name: "db-a", }, types.DatabaseServerSpecV3{ Protocol: "postgres", URI: "localhost", Hostname: "localhost", HostID: "localhost", }) require.NoError(t, err) _, err = srv.Auth().UpsertDatabaseServer(ctx, db) require.NoError(t, err) // Create a fake user. user, role, err := CreateUserAndRole(srv.Auth(), "mfa-user", []string{"role"}) require.NoError(t, err) // Make sure MFA is required for this user. roleOpt := role.GetOptions() roleOpt.RequireSessionMFA = true role.SetOptions(roleOpt) err = srv.Auth().UpsertRole(ctx, role) require.NoError(t, err) cl, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) // Register MFA devices for the fake user. registered := addOneOfEachMFADevice(t, cl, clock, webOrigin) // Fetch MFA device IDs. devs, err := srv.Auth().Identity.GetMFADevices(ctx, user.GetName(), false) require.NoError(t, err) var webDevID string for _, dev := range devs { if dev.GetWebauthn() != nil { webDevID = dev.Id break } } _, pub, err := srv.Auth().GenerateKeyPair("") require.NoError(t, err) tests := []struct { desc string opts generateUserSingleUseCertTestOpts }{ { desc: "ssh using webauthn", opts: generateUserSingleUseCertTestOpts{ initReq: &proto.UserCertsRequest{ PublicKey: pub, Username: user.GetName(), Expires: clock.Now().Add(teleport.UserSingleUseCertTTL), Usage: proto.UserCertsRequest_SSH, NodeName: "node-a", }, checkInitErr: require.NoError, authHandler: registered.webAuthHandler, checkAuthErr: require.NoError, validateCert: func(t *testing.T, c *proto.SingleUseUserCert) { crt := c.GetSSH() require.NotEmpty(t, crt) cert, err := sshutils.ParseCertificate(crt) require.NoError(t, err) require.Equal(t, cert.Extensions[teleport.CertExtensionMFAVerified], webDevID) require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionClientIP]).IsLoopback()) require.Equal(t, cert.ValidBefore, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix())) }, }, }, { desc: "ssh - adjusted expiry", opts: generateUserSingleUseCertTestOpts{ initReq: &proto.UserCertsRequest{ PublicKey: pub, Username: user.GetName(), // This expiry is longer than allowed, should be // automatically adjusted. Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL), Usage: proto.UserCertsRequest_SSH, NodeName: "node-a", }, checkInitErr: require.NoError, authHandler: registered.webAuthHandler, checkAuthErr: require.NoError, validateCert: func(t *testing.T, c *proto.SingleUseUserCert) { crt := c.GetSSH() require.NotEmpty(t, crt) cert, err := sshutils.ParseCertificate(crt) require.NoError(t, err) require.Equal(t, cert.Extensions[teleport.CertExtensionMFAVerified], webDevID) require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionClientIP]).IsLoopback()) require.Equal(t, cert.ValidBefore, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix())) }, }, }, { desc: "k8s", opts: generateUserSingleUseCertTestOpts{ initReq: &proto.UserCertsRequest{ PublicKey: pub, Username: user.GetName(), Expires: clock.Now().Add(teleport.UserSingleUseCertTTL), Usage: proto.UserCertsRequest_Kubernetes, KubernetesCluster: "kube-a", }, checkInitErr: require.NoError, authHandler: registered.webAuthHandler, checkAuthErr: require.NoError, validateCert: func(t *testing.T, c *proto.SingleUseUserCert) { crt := c.GetTLS() require.NotEmpty(t, crt) cert, err := tlsca.ParseCertificatePEM(crt) require.NoError(t, err) require.Equal(t, cert.NotAfter, clock.Now().Add(teleport.UserSingleUseCertTTL)) identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter) require.NoError(t, err) require.Equal(t, identity.MFAVerified, webDevID) require.True(t, net.ParseIP(identity.ClientIP).IsLoopback()) require.Equal(t, identity.Usage, []string{teleport.UsageKubeOnly}) require.Equal(t, identity.KubernetesCluster, "kube-a") }, }, }, { desc: "db", opts: generateUserSingleUseCertTestOpts{ initReq: &proto.UserCertsRequest{ PublicKey: pub, Username: user.GetName(), Expires: clock.Now().Add(teleport.UserSingleUseCertTTL), Usage: proto.UserCertsRequest_Database, RouteToDatabase: proto.RouteToDatabase{ ServiceName: "db-a", }, }, checkInitErr: require.NoError, authHandler: registered.webAuthHandler, checkAuthErr: require.NoError, validateCert: func(t *testing.T, c *proto.SingleUseUserCert) { crt := c.GetTLS() require.NotEmpty(t, crt) cert, err := tlsca.ParseCertificatePEM(crt) require.NoError(t, err) require.Equal(t, cert.NotAfter, clock.Now().Add(teleport.UserSingleUseCertTTL)) identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter) require.NoError(t, err) require.Equal(t, identity.MFAVerified, webDevID) require.True(t, net.ParseIP(identity.ClientIP).IsLoopback()) require.Equal(t, identity.Usage, []string{teleport.UsageDatabaseOnly}) require.Equal(t, identity.RouteToDatabase.ServiceName, "db-a") }, }, }, { desc: "fail - wrong usage", opts: generateUserSingleUseCertTestOpts{ initReq: &proto.UserCertsRequest{ PublicKey: pub, Username: user.GetName(), Expires: clock.Now().Add(teleport.UserSingleUseCertTTL), Usage: proto.UserCertsRequest_All, NodeName: "node-a", }, checkInitErr: require.Error, }, }, { desc: "fail - mfa challenge fail", opts: generateUserSingleUseCertTestOpts{ initReq: &proto.UserCertsRequest{ PublicKey: pub, Username: user.GetName(), Expires: clock.Now().Add(teleport.UserSingleUseCertTTL), Usage: proto.UserCertsRequest_SSH, NodeName: "node-a", }, checkInitErr: require.NoError, authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse { // Return no challenge response. return &proto.MFAAuthenticateResponse{} }, checkAuthErr: require.Error, }, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { testGenerateUserSingleUseCert(ctx, t, cl, tt.opts) }) } } type generateUserSingleUseCertTestOpts struct { initReq *proto.UserCertsRequest checkInitErr require.ErrorAssertionFunc authHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse checkAuthErr require.ErrorAssertionFunc validateCert func(*testing.T, *proto.SingleUseUserCert) } func testGenerateUserSingleUseCert(ctx context.Context, t *testing.T, cl *Client, opts generateUserSingleUseCertTestOpts) { stream, err := cl.GenerateUserSingleUseCerts(ctx) require.NoError(t, err) err = stream.Send(&proto.UserSingleUseCertsRequest{Request: &proto.UserSingleUseCertsRequest_Init{Init: opts.initReq}}) require.NoError(t, err) authChallenge, err := stream.Recv() opts.checkInitErr(t, err) if err != nil { return } authResp := opts.authHandler(t, authChallenge.GetMFAChallenge()) err = stream.Send(&proto.UserSingleUseCertsRequest{Request: &proto.UserSingleUseCertsRequest_MFAResponse{MFAResponse: authResp}}) require.NoError(t, err) certs, err := stream.Recv() opts.checkAuthErr(t, err) if err != nil { return } opts.validateCert(t, certs.GetCert()) require.NoError(t, stream.CloseSend()) } func TestIsMFARequired(t *testing.T) { ctx := context.Background() srv := newTestTLSServer(t) // Enable MFA support. authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ Type: constants.Local, SecondFactor: constants.SecondFactorOptional, Webauthn: &types.Webauthn{ RPID: "teleport", }, }) require.NoError(t, err) err = srv.Auth().SetAuthPreference(ctx, authPref) require.NoError(t, err) // Register an SSH node. node := &types.ServerV2{ Kind: types.KindKubeService, Version: types.V2, Metadata: types.Metadata{ Name: "node-a", }, Spec: types.ServerSpecV2{ Hostname: "node-a", }, } _, err = srv.Auth().UpsertNode(ctx, node) require.NoError(t, err) // Create a fake user. user, role, err := CreateUserAndRole(srv.Auth(), "no-mfa-user", []string{"role"}) require.NoError(t, err) for _, required := range []bool{true, false} { t.Run(fmt.Sprintf("required=%v", required), func(t *testing.T) { roleOpt := role.GetOptions() roleOpt.RequireSessionMFA = required role.SetOptions(roleOpt) err = srv.Auth().UpsertRole(ctx, role) require.NoError(t, err) cl, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) resp, err := cl.IsMFARequired(ctx, &proto.IsMFARequiredRequest{ Target: &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{ Login: user.GetName(), Node: "node-a", }}, }) require.NoError(t, err) require.Equal(t, resp.Required, required) }) } } func TestIsMFARequiredUnauthorized(t *testing.T) { ctx := context.Background() srv := newTestTLSServer(t) // Enable MFA support. authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ Type: constants.Local, SecondFactor: constants.SecondFactorOptional, Webauthn: &types.Webauthn{ RPID: "teleport", }, }) require.NoError(t, err) err = srv.Auth().SetAuthPreference(ctx, authPref) require.NoError(t, err) // Register an SSH node. node1 := &types.ServerV2{ Kind: types.KindNode, Version: types.V2, Metadata: types.Metadata{ Name: "node1", Namespace: apidefaults.Namespace, Labels: map[string]string{"a": "b"}, }, Spec: types.ServerSpecV2{ Hostname: "node1", Addr: "localhost:3022", }, } _, err = srv.Auth().UpsertNode(ctx, node1) require.NoError(t, err) // Register another SSH node with a duplicate hostname. node2 := &types.ServerV2{ Kind: types.KindNode, Version: types.V2, Metadata: types.Metadata{ Name: "node2", Namespace: apidefaults.Namespace, Labels: map[string]string{"a": "c"}, }, Spec: types.ServerSpecV2{ Hostname: "node1", Addr: "localhost:3022", }, } _, err = srv.Auth().UpsertNode(ctx, node2) require.NoError(t, err) user, role, err := CreateUserAndRole(srv.Auth(), "alice", []string{"alice"}) require.NoError(t, err) // Require MFA. roleOpt := role.GetOptions() roleOpt.RequireSessionMFA = true role.SetOptions(roleOpt) role.SetNodeLabels(types.Allow, map[string]apiutils.Strings{"a": []string{"c"}}) err = srv.Auth().UpsertRole(ctx, role) require.NoError(t, err) cl, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) // Call the endpoint for an authorized login. The user is only authorized // for the 2nd node, but should still be asked for MFA. resp, err := cl.IsMFARequired(ctx, &proto.IsMFARequiredRequest{ Target: &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{ Login: "alice", Node: "node1", }}, }) require.NoError(t, err) require.True(t, resp.Required) // Call the endpoint for an unauthorized login. resp, err = cl.IsMFARequired(ctx, &proto.IsMFARequiredRequest{ Target: &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{ Login: "bob", Node: "node1", }}, }) // When unauthorized, expect a silent `false`. require.NoError(t, err) require.False(t, resp.Required) } // TestRoleVersions tests that downgraded V4 roles are returned to older // clients, and V5 roles are returned to newer clients. func TestRoleVersions(t *testing.T) { srv := newTestTLSServer(t) role := &types.RoleV5{ Kind: types.KindRole, Version: types.V5, Metadata: types.Metadata{ Name: "test_role", }, Spec: types.RoleSpecV5{ Allow: types.RoleConditions{ Rules: []types.Rule{ types.NewRule(types.KindRole, services.RO()), types.NewRule(types.KindEvent, services.RW()), }, }, }, } user, err := CreateUser(srv.Auth(), "test_user", role) require.NoError(t, err) client, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) testCases := []struct { desc string clientVersion string disableMetadata bool expectedRoleVersion string assertErr require.ErrorAssertionFunc }{ { desc: "old", clientVersion: "7.1.1", expectedRoleVersion: "v4", assertErr: require.NoError, }, { desc: "new", clientVersion: "9.0.0", expectedRoleVersion: "v5", assertErr: require.NoError, }, { desc: "alpha", clientVersion: "7.2.4-alpha.0", expectedRoleVersion: "v4", assertErr: require.NoError, }, { desc: "greater than 10", clientVersion: "10.0.0-beta", expectedRoleVersion: "v5", assertErr: require.NoError, }, { desc: "empty version", clientVersion: "", assertErr: require.Error, }, { desc: "invalid version", clientVersion: "foo", assertErr: require.Error, }, { desc: "no version metadata", disableMetadata: true, expectedRoleVersion: "v4", assertErr: require.NoError, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { // setup client metadata ctx := context.Background() if tc.disableMetadata { ctx = context.WithValue(ctx, metadata.DisableInterceptors{}, struct{}{}) } else { ctx = metadata.AddMetadataToContext(ctx, map[string]string{ metadata.VersionKey: tc.clientVersion, }) } // test GetRole gotRole, err := client.GetRole(ctx, role.GetName()) tc.assertErr(t, err) if err == nil { require.Equal(t, tc.expectedRoleVersion, gotRole.GetVersion()) } // test GetRoles gotRoles, err := client.GetRoles(ctx) tc.assertErr(t, err) if err == nil { foundTestRole := false for _, gotRole := range gotRoles { if gotRole.GetName() == role.GetName() { require.Equal(t, tc.expectedRoleVersion, gotRole.GetVersion()) foundTestRole = true } } require.True(t, foundTestRole) } }) } } // testOriginDynamicStored tests setting a ResourceWithOrigin via the server // API always results in the resource being stored with OriginDynamic. func testOriginDynamicStored(t *testing.T, setWithOrigin func(*Client, string) error, getStored func(*Server) (types.ResourceWithOrigin, error)) { srv := newTestTLSServer(t) // Create a fake user. user, _, err := CreateUserAndRole(srv.Auth(), "configurer", []string{}) require.NoError(t, err) cl, err := srv.NewClient(TestUser(user.GetName())) require.NoError(t, err) for _, origin := range types.OriginValues { t.Run(fmt.Sprintf("setting with origin %q", origin), func(t *testing.T) { err := setWithOrigin(cl, origin) require.NoError(t, err) stored, err := getStored(srv.Auth()) require.NoError(t, err) require.Equal(t, stored.Origin(), types.OriginDynamic) }) } } func TestAuthPreferenceOriginDynamic(t *testing.T) { t.Parallel() ctx := context.Background() setWithOrigin := func(cl *Client, origin string) error { authPref := types.DefaultAuthPreference() authPref.SetOrigin(origin) return cl.SetAuthPreference(ctx, authPref) } getStored := func(asrv *Server) (types.ResourceWithOrigin, error) { return asrv.GetAuthPreference(ctx) } testOriginDynamicStored(t, setWithOrigin, getStored) } func TestClusterNetworkingConfigOriginDynamic(t *testing.T) { t.Parallel() ctx := context.Background() setWithOrigin := func(cl *Client, origin string) error { netConfig := types.DefaultClusterNetworkingConfig() netConfig.SetOrigin(origin) return cl.SetClusterNetworkingConfig(ctx, netConfig) } getStored := func(asrv *Server) (types.ResourceWithOrigin, error) { return asrv.GetClusterNetworkingConfig(ctx) } testOriginDynamicStored(t, setWithOrigin, getStored) } func TestSessionRecordingConfigOriginDynamic(t *testing.T) { t.Parallel() ctx := context.Background() setWithOrigin := func(cl *Client, origin string) error { recConfig := types.DefaultSessionRecordingConfig() recConfig.SetOrigin(origin) return cl.SetSessionRecordingConfig(ctx, recConfig) } getStored := func(asrv *Server) (types.ResourceWithOrigin, error) { return asrv.GetSessionRecordingConfig(ctx) } testOriginDynamicStored(t, setWithOrigin, getStored) } func TestGenerateHostCerts(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) priv, pub, err := clt.GenerateKeyPair("") require.NoError(t, err) pubTLS, err := PrivateKeyToPublicKeyTLS(priv) require.NoError(t, err) certs, err := clt.GenerateHostCerts(ctx, &proto.HostCertsRequest{ HostID: "Admin", Role: types.RoleAdmin, NodeName: "foo", // Ensure that 0.0.0.0 gets replaced with the RemoteAddr of the client AdditionalPrincipals: []string{"0.0.0.0"}, PublicSSHKey: pub, PublicTLSKey: pubTLS, }) require.NoError(t, err) require.NotNil(t, certs) } func TestNodesCRUD(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) // node1 and node2 will be added to default namespace node1, err := types.NewServerWithLabels("node1", types.KindNode, types.ServerSpecV2{}, nil) require.NoError(t, err) node2, err := types.NewServerWithLabels("node2", types.KindNode, types.ServerSpecV2{}, nil) require.NoError(t, err) t.Run("CreateNode", func(t *testing.T) { // Initially expect no nodes to be returned. nodes, err := clt.GetNodes(ctx, apidefaults.Namespace) require.NoError(t, err) require.Empty(t, nodes) // Create nodes. _, err = clt.UpsertNode(ctx, node1) require.NoError(t, err) _, err = clt.UpsertNode(ctx, node2) require.NoError(t, err) }) // Run NodeGetters in nested subtests to allow parallelization. t.Run("NodeGetters", func(t *testing.T) { t.Run("List Nodes", func(t *testing.T) { t.Parallel() // List nodes one at a time, last page should be empty. // First node. nodes, nextKey, err := clt.ListNodes(ctx, proto.ListNodesRequest{ Namespace: apidefaults.Namespace, Limit: 1, }) require.NoError(t, err) require.Len(t, nodes, 1) require.Empty(t, cmp.Diff([]types.Server{node1}, nodes, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) require.Equal(t, backend.NextPaginationKey(node1), nextKey) // Second node (last). nodes, nextKey, err = clt.ListNodes(ctx, proto.ListNodesRequest{ Namespace: apidefaults.Namespace, Limit: 1, StartKey: nextKey, }) require.NoError(t, err) require.Len(t, nodes, 1) require.Empty(t, cmp.Diff([]types.Server{node2}, nodes, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) require.Empty(t, nextKey) // ListNodes should not fail if namespace is empty _, _, err = clt.ListNodes(ctx, proto.ListNodesRequest{ Limit: 1, }) require.NoError(t, err) // ListNodes should fail if limit is nonpositive _, _, err = clt.ListNodes(ctx, proto.ListNodesRequest{ Namespace: apidefaults.Namespace, }) require.IsType(t, &trace.BadParameterError{}, err.(*trace.TraceErr).OrigError()) _, _, err = clt.ListNodes(ctx, proto.ListNodesRequest{ Namespace: apidefaults.Namespace, Limit: -1, }) require.IsType(t, &trace.BadParameterError{}, err.(*trace.TraceErr).OrigError()) }) t.Run("GetNodes", func(t *testing.T) { t.Parallel() // Get all nodes nodes, err := clt.GetNodes(ctx, apidefaults.Namespace) require.NoError(t, err) require.Len(t, nodes, 2) require.Empty(t, cmp.Diff([]types.Server{node1, node2}, nodes, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) // GetNodes should not fail if namespace is empty _, err = clt.GetNodes(ctx, "") require.NoError(t, err) }) t.Run("GetNode", func(t *testing.T) { t.Parallel() // Get Node node, err := clt.GetNode(ctx, apidefaults.Namespace, "node1") require.NoError(t, err) require.Empty(t, cmp.Diff(node1, node, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) // GetNode should fail if node name isn't provided _, err = clt.GetNode(ctx, apidefaults.Namespace, "") require.IsType(t, &trace.BadParameterError{}, err.(*trace.TraceErr).OrigError()) // GetNode should fail if namespace isn't provided _, err = clt.GetNode(ctx, "", "node1") require.IsType(t, &trace.BadParameterError{}, err.(*trace.TraceErr).OrigError()) }) }) t.Run("DeleteNode", func(t *testing.T) { // Make sure can't delete with empty namespace or name. err = clt.DeleteNode(ctx, apidefaults.Namespace, "") require.Error(t, err) require.IsType(t, trace.BadParameter(""), err) err = clt.DeleteNode(ctx, "", node1.GetName()) require.Error(t, err) require.IsType(t, trace.BadParameter(""), err) // Delete node. err = clt.DeleteNode(ctx, apidefaults.Namespace, node1.GetName()) require.NoError(t, err) // Expect node not found _, err := clt.GetNode(ctx, apidefaults.Namespace, "node1") require.IsType(t, trace.NotFound(""), err) }) t.Run("DeleteAllNodes", func(t *testing.T) { // Make sure can't delete with empty namespace. err = clt.DeleteAllNodes(ctx, "") require.Error(t, err) require.IsType(t, trace.BadParameter(""), err) // Delete nodes err = clt.DeleteAllNodes(ctx, apidefaults.Namespace) require.NoError(t, err) // Now expect no nodes to be returned. nodes, err := clt.GetNodes(ctx, apidefaults.Namespace) require.NoError(t, err) require.Empty(t, nodes) }) } func TestLocksCRUD(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) now := srv.Clock().Now() lock1, err := types.NewLock("lock1", types.LockSpecV2{ Target: types.LockTarget{ User: "user-A", }, Expires: &now, }) require.NoError(t, err) lock2, err := types.NewLock("lock2", types.LockSpecV2{ Target: types.LockTarget{ Node: "node", }, Message: "node compromised", }) require.NoError(t, err) t.Run("CreateLock", func(t *testing.T) { // Initially expect no locks to be returned. locks, err := clt.GetLocks(ctx, false) require.NoError(t, err) require.Empty(t, locks) // Create locks. err = clt.UpsertLock(ctx, lock1) require.NoError(t, err) err = clt.UpsertLock(ctx, lock2) require.NoError(t, err) }) // Run LockGetters in nested subtests to allow parallelization. t.Run("LockGetters", func(t *testing.T) { t.Run("GetLocks", func(t *testing.T) { t.Parallel() locks, err := clt.GetLocks(ctx, false) require.NoError(t, err) require.Len(t, locks, 2) require.Empty(t, cmp.Diff([]types.Lock{lock1, lock2}, locks, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) }) t.Run("GetLocks with targets", func(t *testing.T) { t.Parallel() // Match both locks with the targets. locks, err := clt.GetLocks(ctx, false, lock1.Target(), lock2.Target()) require.NoError(t, err) require.Len(t, locks, 2) require.Empty(t, cmp.Diff([]types.Lock{lock1, lock2}, locks, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) // Match only one of the locks. roleTarget := types.LockTarget{Role: "role-A"} locks, err = clt.GetLocks(ctx, false, lock1.Target(), roleTarget) require.NoError(t, err) require.Len(t, locks, 1) require.Empty(t, cmp.Diff([]types.Lock{lock1}, locks, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) // Match none of the locks. locks, err = clt.GetLocks(ctx, false, roleTarget) require.NoError(t, err) require.Empty(t, locks) }) t.Run("GetLock", func(t *testing.T) { t.Parallel() // Get one of the locks. lock, err := clt.GetLock(ctx, lock1.GetName()) require.NoError(t, err) require.Empty(t, cmp.Diff(lock1, lock, cmpopts.IgnoreFields(types.Metadata{}, "ID"))) // Attempt to get a nonexistent lock. _, err = clt.GetLock(ctx, "lock3") require.Error(t, err) require.True(t, trace.IsNotFound(err)) }) }) t.Run("UpsertLock", func(t *testing.T) { // Get one of the locks. lock, err := clt.GetLock(ctx, lock1.GetName()) require.NoError(t, err) require.Empty(t, lock.Message()) msg := "cluster maintenance" lock1.SetMessage(msg) err = clt.UpsertLock(ctx, lock1) require.NoError(t, err) lock, err = clt.GetLock(ctx, lock1.GetName()) require.NoError(t, err) require.Equal(t, msg, lock.Message()) }) t.Run("DeleteLock", func(t *testing.T) { // Delete lock. err = clt.DeleteLock(ctx, lock1.GetName()) require.NoError(t, err) // Expect lock not found. _, err := clt.GetLock(ctx, lock1.GetName()) require.Error(t, err) require.True(t, trace.IsNotFound(err)) }) } // TestApplicationServersCRUD tests application server operations. func TestApplicationServersCRUD(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) // Create a couple app servers. app1, err := types.NewAppV3(types.Metadata{Name: "app-1"}, types.AppSpecV3{URI: "localhost"}) require.NoError(t, err) server1, err := types.NewAppServerV3FromApp(app1, "server-1", "server-1") require.NoError(t, err) app2, err := types.NewAppV3(types.Metadata{Name: "app-2"}, types.AppSpecV3{URI: "localhost"}) require.NoError(t, err) server2, err := types.NewAppServerV3FromApp(app2, "server-2", "server-2") require.NoError(t, err) // Create a legacy app server. app3, err := types.NewAppV3(types.Metadata{Name: "app-3"}, types.AppSpecV3{URI: "localhost"}) require.NoError(t, err) server3Legacy, err := types.NewLegacyAppServer(app3, "server-3", "server-3") require.NoError(t, err) server3, err := types.NewAppServerV3FromApp(app3, "server-3", "server-3") require.NoError(t, err) // Initially we expect no app servers. out, err := clt.GetApplicationServers(ctx, apidefaults.Namespace) require.NoError(t, err) require.Equal(t, 0, len(out)) // Register all app servers. _, err = clt.UpsertApplicationServer(ctx, server1) require.NoError(t, err) _, err = clt.UpsertApplicationServer(ctx, server2) require.NoError(t, err) _, err = clt.UpsertAppServer(ctx, server3Legacy) require.NoError(t, err) // Fetch all app servers. out, err = clt.GetApplicationServers(ctx, apidefaults.Namespace) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.AppServer{server1, server2, server3}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Update an app server. server1.Metadata.Description = "description" _, err = clt.UpsertApplicationServer(ctx, server1) require.NoError(t, err) out, err = clt.GetApplicationServers(ctx, apidefaults.Namespace) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.AppServer{server1, server2, server3}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Delete an app server. err = clt.DeleteApplicationServer(ctx, server1.GetNamespace(), server1.GetHostID(), server1.GetName()) require.NoError(t, err) out, err = clt.GetApplicationServers(ctx, apidefaults.Namespace) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.AppServer{server2, server3}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Delete all app servers. err = clt.DeleteAllApplicationServers(ctx, apidefaults.Namespace) require.NoError(t, err) err = clt.DeleteAllAppServers(ctx, apidefaults.Namespace) require.NoError(t, err) out, err = clt.GetApplicationServers(ctx, apidefaults.Namespace) require.NoError(t, err) require.Equal(t, 0, len(out)) } // TestAppsCRUD tests application resource operations. func TestAppsCRUD(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) // Create a couple apps. app1, err := types.NewAppV3(types.Metadata{ Name: "app1", Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, }, types.AppSpecV3{ URI: "localhost1", }) require.NoError(t, err) app2, err := types.NewAppV3(types.Metadata{ Name: "app2", Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, }, types.AppSpecV3{ URI: "localhost2", }) require.NoError(t, err) // Initially we expect no apps. out, err := clt.GetApps(ctx) require.NoError(t, err) require.Equal(t, 0, len(out)) // Create both apps. err = clt.CreateApp(ctx, app1) require.NoError(t, err) err = clt.CreateApp(ctx, app2) require.NoError(t, err) // Fetch all apps. out, err = clt.GetApps(ctx) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.Application{app1, app2}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Fetch a specific app. app, err := clt.GetApp(ctx, app2.GetName()) require.NoError(t, err) require.Empty(t, cmp.Diff(app2, app, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Try to fetch an app that doesn't exist. _, err = clt.GetApp(ctx, "doesnotexist") require.IsType(t, trace.NotFound(""), err) // Try to create the same app. err = clt.CreateApp(ctx, app1) require.IsType(t, trace.AlreadyExists(""), err) // Update an app. app1.Metadata.Description = "description" err = clt.UpdateApp(ctx, app1) require.NoError(t, err) app, err = clt.GetApp(ctx, app1.GetName()) require.NoError(t, err) require.Empty(t, cmp.Diff(app1, app, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Delete an app. err = clt.DeleteApp(ctx, app1.GetName()) require.NoError(t, err) out, err = clt.GetApps(ctx) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.Application{app2}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Try to delete an app that doesn't exist. err = clt.DeleteApp(ctx, "doesnotexist") require.IsType(t, trace.NotFound(""), err) // Delete all apps. err = clt.DeleteAllApps(ctx) require.NoError(t, err) out, err = clt.GetApps(ctx) require.NoError(t, err) require.Len(t, out, 0) } // TestDatabasesCRUD tests database resource operations. func TestDatabasesCRUD(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) // Create a couple databases. db1, err := types.NewDatabaseV3(types.Metadata{ Name: "db1", Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, }, types.DatabaseSpecV3{ Protocol: defaults.ProtocolPostgres, URI: "localhost:5432", }) require.NoError(t, err) db2, err := types.NewDatabaseV3(types.Metadata{ Name: "db2", Labels: map[string]string{types.OriginLabel: types.OriginDynamic}, }, types.DatabaseSpecV3{ Protocol: defaults.ProtocolMySQL, URI: "localhost:3306", }) require.NoError(t, err) // Initially we expect no databases. out, err := clt.GetDatabases(ctx) require.NoError(t, err) require.Equal(t, 0, len(out)) // Create both databases. err = clt.CreateDatabase(ctx, db1) require.NoError(t, err) err = clt.CreateDatabase(ctx, db2) require.NoError(t, err) // Fetch all databases. out, err = clt.GetDatabases(ctx) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.Database{db1, db2}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Fetch a specific database. db, err := clt.GetDatabase(ctx, db2.GetName()) require.NoError(t, err) require.Empty(t, cmp.Diff(db2, db, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Try to fetch a database that doesn't exist. _, err = clt.GetDatabase(ctx, "doesnotexist") require.IsType(t, trace.NotFound(""), err) // Try to create the same database. err = clt.CreateDatabase(ctx, db1) require.IsType(t, trace.AlreadyExists(""), err) // Update a database. db1.Metadata.Description = "description" err = clt.UpdateDatabase(ctx, db1) require.NoError(t, err) db, err = clt.GetDatabase(ctx, db1.GetName()) require.NoError(t, err) require.Empty(t, cmp.Diff(db1, db, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Delete a database. err = clt.DeleteDatabase(ctx, db1.GetName()) require.NoError(t, err) out, err = clt.GetDatabases(ctx) require.NoError(t, err) require.Empty(t, cmp.Diff([]types.Database{db2}, out, cmpopts.IgnoreFields(types.Metadata{}, "ID"), )) // Try to delete a database that doesn't exist. err = clt.DeleteDatabase(ctx, "doesnotexist") require.IsType(t, trace.NotFound(""), err) // Delete all databases. err = clt.DeleteAllDatabases(ctx) require.NoError(t, err) out, err = clt.GetDatabases(ctx) require.NoError(t, err) require.Len(t, out, 0) } func TestListResources(t *testing.T) { t.Parallel() ctx := context.Background() srv := newTestTLSServer(t) clt, err := srv.NewClient(TestAdmin()) require.NoError(t, err) testCases := map[string]struct { resourceType string createResource func(name string, clt *Client) error }{ "DatabaseServers": { resourceType: types.KindDatabaseServer, createResource: func(name string, clt *Client) error { server, err := types.NewDatabaseServerV3(types.Metadata{ Name: name, }, types.DatabaseServerSpecV3{ Protocol: defaults.ProtocolPostgres, URI: "localhost:5432", Hostname: "localhost", HostID: uuid.New().String(), }) if err != nil { return err } _, err = clt.UpsertDatabaseServer(ctx, server) return err }, }, "ApplicationServers": { resourceType: types.KindAppServer, createResource: func(name string, clt *Client) error { app, err := types.NewAppV3(types.Metadata{ Name: name, }, types.AppSpecV3{ URI: "localhost", }) if err != nil { return err } server, err := types.NewAppServerV3(types.Metadata{ Name: name, }, types.AppServerSpecV3{ Hostname: "localhost", HostID: uuid.New().String(), App: app, }) if err != nil { return err } _, err = clt.UpsertApplicationServer(ctx, server) return err }, }, "KubeService": { resourceType: types.KindKubeService, createResource: func(name string, clt *Client) error { server, err := types.NewServer(name, types.KindKubeService, types.ServerSpecV2{ KubernetesClusters: []*types.KubernetesCluster{ {Name: name, StaticLabels: map[string]string{"name": name}}, }, }) if err != nil { return err } _, err = clt.UpsertKubeServiceV2(ctx, server) return err }, }, "Node": { resourceType: types.KindNode, createResource: func(name string, clt *Client) error { server, err := types.NewServer(name, types.KindNode, types.ServerSpecV2{}) if err != nil { return err } _, err = clt.UpsertNode(ctx, server) return err }, }, "WindowsDesktops": { resourceType: types.KindWindowsDesktop, createResource: func(name string, clt *Client) error { desktop, err := types.NewWindowsDesktopV3(name, nil, types.WindowsDesktopSpecV3{Addr: "_", HostID: "_"}) if err != nil { return err } return clt.UpsertWindowsDesktop(ctx, desktop) }, }, } for name, test := range testCases { name := name test := test t.Run(name, func(t *testing.T) { t.Parallel() resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{ ResourceType: test.resourceType, Namespace: apidefaults.Namespace, Limit: 100, }) require.NoError(t, err) require.Len(t, resp.Resources, 0) require.Empty(t, resp.NextKey) // create two resources err = test.createResource("foo", clt) require.NoError(t, err) err = test.createResource("bar", clt) require.NoError(t, err) resp, err = clt.ListResources(ctx, proto.ListResourcesRequest{ ResourceType: test.resourceType, Namespace: apidefaults.Namespace, Limit: 100, }) require.NoError(t, err) require.Len(t, resp.Resources, 2) require.Empty(t, resp.NextKey) require.Empty(t, resp.TotalCount) // Test types.KindKubernetesCluster if test.resourceType == types.KindKubeService { test.resourceType = types.KindKubernetesCluster resp, err = clt.ListResources(ctx, proto.ListResourcesRequest{ ResourceType: test.resourceType, Namespace: apidefaults.Namespace, Limit: 100, }) require.NoError(t, err) require.Len(t, resp.Resources, 2) require.Empty(t, resp.NextKey) require.Empty(t, resp.TotalCount) } // Test listing with NeedTotalCount flag. if test.resourceType != types.KindKubeService { resp, err = clt.ListResources(ctx, proto.ListResourcesRequest{ ResourceType: test.resourceType, Limit: 100, NeedTotalCount: true, }) require.NoError(t, err) require.Len(t, resp.Resources, 2) require.Empty(t, resp.NextKey) require.Equal(t, 2, resp.TotalCount) } }) } t.Run("InvalidResourceType", func(t *testing.T) { _, err := clt.ListResources(ctx, proto.ListResourcesRequest{ ResourceType: "", Namespace: apidefaults.Namespace, Limit: 100, }) require.Error(t, err) }) } func TestCustomRateLimiting(t *testing.T) { t.Parallel() ctx := context.Background() tests := []struct { name string burst int fn func(*Client) error }{ { name: "RPC ChangeUserAuthentication", fn: func(clt *Client) error { _, err := clt.ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{}) return err }, }, { name: "RPC CreateAuthenticateChallenge", burst: defaults.LimiterPasswordlessBurst, fn: func(clt *Client) error { _, err := clt.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{}) return err }, }, { name: "RPC GetAccountRecoveryToken", fn: func(clt *Client) error { _, err := clt.GetAccountRecoveryToken(ctx, &proto.GetAccountRecoveryTokenRequest{}) return err }, }, { name: "RPC StartAccountRecovery", fn: func(clt *Client) error { _, err := clt.StartAccountRecovery(ctx, &proto.StartAccountRecoveryRequest{}) return err }, }, { name: "RPC VerifyAccountRecovery", fn: func(clt *Client) error { _, err := clt.VerifyAccountRecovery(ctx, &proto.VerifyAccountRecoveryRequest{}) return err }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() // Create new instance per test case, to troubleshoot which test case // specifically failed, otherwise multiple cases can fail from running // cases in parallel. srv := newTestTLSServer(t) clt, err := srv.NewClient(TestNop()) require.NoError(t, err) var attempts int if test.burst == 0 { attempts = 10 // Good for most tests. } else { attempts = test.burst } for i := 0; i < attempts; i++ { err = test.fn(clt) require.False(t, trace.IsLimitExceeded(err), "got err = %v, want non-IsLimitExceeded", err) } err = test.fn(clt) require.True(t, trace.IsLimitExceeded(err), "got err = %v, want LimitExceeded", err) }) } }