Files
teleport/lib/auth/grpcserver_test.go
T
Michael Wilson 8f628084d5 Introduce the Okta service. (#22899)
The Okta service and client have been introduced. This will allow for the
maintenance of Okta objects via our gRPC server and auth client. Some
additional improvements have been made so that the local okta service now
better reflects the gRPC definition.

Additionally, the gRPC authorization mechanism has been lifted out of
`lib/auth/grpcserver.go` and moved into a common authorizer. Finally, the
ability to check access to verbs has been lifted out of the `pluginsv1`
service into that same authorizer. This will hopefully provide a good base
for separate gRPC services in the future.
2023-03-14 16:25:22 +00:00

3676 lines
110 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 auth
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base32"
"encoding/pem"
"fmt"
"net"
"net/http"
"net/http/httptest"
"sort"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"github.com/jonboulle/clockwork"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
otlpcommonv1 "go.opentelemetry.io/proto/otlp/common/v1"
otlpresourcev1 "go.opentelemetry.io/proto/otlp/resource/v1"
otlptracev1 "go.opentelemetry.io/proto/otlp/trace/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/emptypb"
"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/observability/tracing"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/installers"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/testauthority"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
)
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"}, nil)
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
}
deleted := deleteAck.GetAck().GetDevice()
require.NotNil(t, deleted, "deleted device in ack message is nil")
require.NotEmpty(t, deleted.Id, "deleted device.Id in ack message is empty")
require.NotEmpty(t, deleted.GetName(), "deleted device.Name in ack message is empty")
// opts.initReq.DeviceName can be either the device name or ID, so check if
// either matches the deleted device.
wantName := []string{
deleted.Id,
deleted.GetName(),
}
require.Contains(t, wantName, opts.initReq.DeviceName)
require.NoError(t, deleteStream.CloseSend())
}
func TestDeleteLastMFADevice(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
// Enable MFA support.
authSpec := &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
}
authPref, err := types.NewAuthPreference(*authSpec)
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"}, nil)
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 {
authSpec.SecondFactor = test.secondFactor
newCAP, err := types.NewAuthPreference(*authSpec)
require.NoError(t, err)
require.NoError(t, auth.SetAuthPreference(ctx, newCAP))
}
testDeleteMFADevice(ctx, t, cl, test.opts)
})
}
}
func TestCreateAppSession_deviceExtensions(t *testing.T) {
ctx := context.Background()
testServer := newTestTLSServer(t)
authServer := testServer.Auth()
// Create an user for testing.
user, _, err := CreateUserAndRole(authServer, "llama", []string{"llama"}, nil)
require.NoError(t, err, "CreateUserAndRole failed")
// Register an application.
app, err := types.NewAppV3(
types.Metadata{
Name: "llamaapp",
}, types.AppSpecV3{
URI: "http://localhost:8080",
PublicAddr: "llamaapp.example.com",
})
require.NoError(t, err, "NewAppV3 failed")
appServer, err := types.NewAppServerV3FromApp(app, "host", uuid.New().String())
require.NoError(t, err, "NewAppServerV3FromApp failed")
_, err = authServer.UpsertApplicationServer(ctx, appServer)
require.NoError(t, err, "UpsertApplicationServer failed")
wantExtensions := &tlsca.DeviceExtensions{
DeviceID: "device1",
AssetTag: "assettag1",
CredentialID: "credentialid1",
}
tests := []struct {
name string
modifyUser func(u *TestIdentity)
assertCert func(t *testing.T, cert *x509.Certificate)
}{
{
name: "no device extensions",
// Absence of errors is enough here, this is mostly to make sure the base
// scenario works.
},
{
name: "user with device extensions",
modifyUser: func(u *TestIdentity) {
lu := u.I.(authz.LocalUser)
lu.Identity.DeviceExtensions = *wantExtensions
u.I = lu
},
assertCert: func(t *testing.T, cert *x509.Certificate) {
gotIdentity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err, "FromSubject failed")
if diff := cmp.Diff(*wantExtensions, gotIdentity.DeviceExtensions, protocmp.Transform()); diff != "" {
t.Errorf("DeviceExtensions mismatch (-want +got)\n%s", diff)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
u := TestUser(user.GetName())
if test.modifyUser != nil {
test.modifyUser(&u)
}
userClient, err := testServer.NewClient(u)
require.NoError(t, err, "NewClient failed")
session, err := userClient.CreateAppSession(ctx, types.CreateAppSessionRequest{
Username: user.GetName(),
PublicAddr: app.GetPublicAddr(),
ClusterName: testServer.ClusterName(),
})
require.NoError(t, err, "CreateAppSession failed")
block, _ := pem.Decode(session.GetTLSCert())
require.NotNil(t, block, "Decode failed")
gotCert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err, "ParserCertificate failed")
if test.assertCert != nil {
test.assertCert(t, gotCert)
}
})
}
}
func TestGenerateUserCerts_deviceExtensions(t *testing.T) {
ctx := context.Background()
testServer := newTestTLSServer(t)
// Create an user for testing.
user, _, err := CreateUserAndRole(testServer.Auth(), "llama", []string{"llama"}, nil)
require.NoError(t, err, "CreateUserAndRole failed")
_, pub, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err, "GenerateKeyPair failed")
wantExtensions := &tlsca.DeviceExtensions{
DeviceID: "device1",
AssetTag: "assettag1",
CredentialID: "credentialid1",
}
tests := []struct {
name string
modifyUser func(u *TestIdentity)
assertCert func(t *testing.T, cert *x509.Certificate)
}{
{
name: "no device extensions",
// Absence of errors is enough here, this is mostly to make sure the base
// scenario works.
},
{
name: "user with device extensions",
modifyUser: func(u *TestIdentity) {
lu := u.I.(authz.LocalUser)
lu.Identity.DeviceExtensions = *wantExtensions
u.I = lu
},
assertCert: func(t *testing.T, cert *x509.Certificate) {
gotIdentity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err, "FromSubject failed")
if diff := cmp.Diff(*wantExtensions, gotIdentity.DeviceExtensions, protocmp.Transform()); diff != "" {
t.Errorf("DeviceExtensions mismatch (-want +got)\n%s", diff)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
u := TestUser(user.GetName())
if test.modifyUser != nil {
test.modifyUser(&u)
}
userClient, err := testServer.NewClient(u)
require.NoError(t, err, "NewClient failed")
resp, err := userClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
Expires: testServer.Clock().Now().Add(1 * time.Hour),
})
require.NoError(t, err, "GenerateUserCerts failed")
block, _ := pem.Decode(resp.TLS)
require.NotNil(t, block, "Decode failed")
gotCert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err, "ParserCertificate failed")
if test.assertCert != nil {
test.assertCert(t, gotCert)
}
})
}
}
func TestGenerateUserCerts_deviceAuthz(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestBuildType: modules.BuildEnterprise, // required for Device Trust.
})
testServer := newTestTLSServer(t)
ctx := context.Background()
clock := testServer.Clock()
clusterName := testServer.ClusterName()
authServer := testServer.Auth()
// Create a user for testing.
user, _, err := CreateUserAndRole(testServer.Auth(), "llama", []string{"llama"}, nil)
require.NoError(t, err, "CreateUserAndRole failed")
username := user.GetName()
// Create clients with and without device extensions.
clientWithoutDevice, err := testServer.NewClient(TestUser(username))
require.NoError(t, err, "NewClient failed")
clientWithDevice, err := testServer.NewClient(
TestUserWithDeviceExtensions(username, tlsca.DeviceExtensions{
DeviceID: "deviceid1",
AssetTag: "assettag1",
CredentialID: "credentialid1",
}))
require.NoError(t, err, "NewClient failed")
// updateAuthPref is a helper used throughout the test.
updateAuthPref := func(t *testing.T, modify func(ap types.AuthPreference)) {
authPref, err := authServer.GetAuthPreference(ctx)
require.NoError(t, err, "GetAuthPreference failed")
modify(authPref)
require.NoError(t,
authServer.SetAuthPreference(ctx, authPref),
"SetAuthPreference failed")
}
// Register MFA devices for the user.
// Required to issue certificates with MFA.
const rpID = "localhost"
const origin = "https://" + rpID + ":3080" // matches RPID.
updateAuthPref(t, func(authPref types.AuthPreference) {
authPref.SetSecondFactor(constants.SecondFactorOptional)
authPref.SetWebauthn(&types.Webauthn{
RPID: "localhost",
})
})
mfaDevices := addOneOfEachMFADevice(t, clientWithoutDevice, clock, origin)
// Create a public key for UserCertsRequest.
_, pub, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err, "GenerateKeyPair failed")
expires := clock.Now().Add(1 * time.Hour)
sshReq := proto.UserCertsRequest{
PublicKey: pub,
Username: username,
Expires: expires,
RouteToCluster: clusterName,
NodeName: "mynode",
Usage: proto.UserCertsRequest_SSH,
}
appReq := proto.UserCertsRequest{
PublicKey: pub,
Username: username,
Expires: expires,
RouteToCluster: clusterName,
Usage: proto.UserCertsRequest_App,
RouteToApp: proto.RouteToApp{
Name: "hello",
SessionID: "mysessionid",
PublicAddr: "hello.cluster.dev",
ClusterName: clusterName,
},
}
winReq := proto.UserCertsRequest{
PublicKey: pub,
Username: username,
Expires: expires,
RouteToCluster: clusterName,
Usage: proto.UserCertsRequest_WindowsDesktop,
RouteToWindowsDesktop: proto.RouteToWindowsDesktop{
WindowsDesktop: "mydesktop",
Login: username,
},
}
assertSuccess := func(t *testing.T, err error) {
assert.NoError(t, err, "GenerateUserCerts error mismatch")
}
assertAccessDenied := func(t *testing.T, err error) {
assert.True(t, trace.IsAccessDenied(err), "GenerateUserCerts error mismatch, got=%v (%T), want trace.AccessDeniedError", err, err)
}
// generateCertsMFA is used to run the MFA-aware, streaming certificate
// issuance ceremony.
generateCertsMFA := func(t *testing.T, client *Client, req proto.UserCertsRequest) (cert *proto.SingleUseUserCert, err error) {
defer func() {
// Translate gRPC to trace errors, as our clients do.
err = trail.FromGRPC(err)
}()
stream, err := client.GenerateUserSingleUseCerts(ctx)
if err != nil {
return nil, err
}
if err := stream.Send(&proto.UserSingleUseCertsRequest{
Request: &proto.UserSingleUseCertsRequest_Init{
Init: &req,
},
}); err != nil {
return nil, err
}
resp, err := stream.Recv()
if err != nil {
return nil, err
}
mfaResp := mfaDevices.webAuthHandler(t, resp.GetMFAChallenge())
if err := stream.Send(&proto.UserSingleUseCertsRequest{
Request: &proto.UserSingleUseCertsRequest_MFAResponse{
MFAResponse: mfaResp,
},
}); err != nil {
return nil, err
}
resp, err = stream.Recv()
if err != nil {
return nil, err
}
cert = resp.GetCert()
if cert == nil {
return nil, fmt.Errorf("unexpected response, cert is nil: %v", resp)
}
return cert, nil
}
tests := []struct {
name string
clusterDeviceMode string
client *Client
req proto.UserCertsRequest
skipGenerateUserCertsRPC bool // aka non-MFA issuance.
skipSingleUseCertsRPC bool // aka MFA/streaming issuance.
assertErr func(t *testing.T, err error)
}{
{
name: "mode=optional without extensions",
clusterDeviceMode: constants.DeviceTrustModeOptional,
client: clientWithoutDevice,
req: sshReq,
assertErr: assertSuccess,
},
{
name: "mode=optional with extensions",
clusterDeviceMode: constants.DeviceTrustModeOptional,
client: clientWithDevice,
req: sshReq,
assertErr: assertSuccess,
},
{
name: "nok: mode=required without extensions",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithoutDevice,
req: sshReq,
assertErr: assertAccessDenied,
},
{
name: "mode=required with extensions",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithDevice,
req: sshReq,
assertErr: assertSuccess,
},
{
name: "mode=required ignores App Access requests (non-MFA)",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithoutDevice,
req: appReq,
skipSingleUseCertsRPC: true,
assertErr: assertSuccess,
},
{
// Tracked here because, if this changes, then the scenario should be the
// same as the one above.
name: "GenerateUserSingleUseCerts does not allow App usage",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithoutDevice,
req: appReq,
skipGenerateUserCertsRPC: true,
assertErr: func(t *testing.T, err error) {
assert.ErrorContains(t, err, "app access certificates", "GenerateUserSingleUseCerts expected to fail for usage=App")
},
},
{
name: "mode=required ignores Desktop Access requests",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithoutDevice,
req: winReq,
assertErr: assertSuccess,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
updateAuthPref(t, func(ap types.AuthPreference) {
ap.SetDeviceTrust(&types.DeviceTrust{
Mode: test.clusterDeviceMode,
})
})
// Test the unary, non-MFA endpoint.
if !test.skipGenerateUserCertsRPC {
t.Run("GenerateUserCerts", func(t *testing.T) {
_, err := test.client.GenerateUserCerts(ctx, test.req)
test.assertErr(t, err)
})
}
// Test the streaming, MFA-aware endpoint.
if !test.skipSingleUseCertsRPC {
t.Run("GenerateUserSingleUseCerts", func(t *testing.T) {
_, err := generateCertsMFA(t, test.client, test.req)
test.assertErr(t, err)
})
}
})
}
}
func TestGenerateUserSingleUseCert(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
clock := srv.Clock()
userCertTTL := 12 * time.Hour
userCertExpires := clock.Now().Add(userCertTTL)
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.KindNode,
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)
kube, err := types.NewKubernetesClusterV3(types.Metadata{
Name: "kube-a",
}, types.KubernetesClusterSpecV3{})
require.NoError(t, err)
kubeServer, err := types.NewKubernetesServerV3FromCluster(kube, "kube-a", "kube-a")
require.NoError(t, err)
_, err = srv.Auth().UpsertKubernetesServer(ctx, kubeServer)
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"}, nil)
require.NoError(t, err)
// Make sure MFA is required for this user.
roleOpt := role.GetOptions()
roleOpt.RequireMFAType = types.RequireMFAType_SESSION
role.SetOptions(roleOpt)
err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
testUser := TestUser(user.GetName())
testUser.TTL = userCertTTL
cl, err := srv.NewClient(testUser)
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().Services.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 := testauthority.New().GenerateKeyPair()
require.NoError(t, err)
// Used for device trust tests.
wantDeviceExtensions := tlsca.DeviceExtensions{
DeviceID: "device-id1",
AssetTag: "device-assettag1",
CredentialID: "device-credentialid1",
}
tests := []struct {
desc string
newClient func() (*Client, error) // optional, makes a new client for the test.
opts generateUserSingleUseCertTestOpts
}{
{
desc: "ssh using webauthn",
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, webDevID, cert.Extensions[teleport.CertExtensionMFAVerified])
require.Equal(t, userCertExpires.Format(time.RFC3339), cert.Extensions[teleport.CertExtensionPreviousIdentityExpires])
require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionLoginIP]).IsLoopback())
require.Equal(t, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()), cert.ValidBefore)
},
},
},
{
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, webDevID, cert.Extensions[teleport.CertExtensionMFAVerified])
require.Equal(t, userCertExpires.Format(time.RFC3339), cert.Extensions[teleport.CertExtensionPreviousIdentityExpires])
require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionLoginIP]).IsLoopback())
require.Equal(t, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()), cert.ValidBefore)
},
},
},
{
desc: "k8s",
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_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, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageKubeOnly}, identity.Usage)
require.Equal(t, "kube-a", identity.KubernetesCluster)
},
},
},
{
desc: "db",
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_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, clock.Now().Add(teleport.UserSingleUseCertTTL), cert.NotAfter)
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageDatabaseOnly}, identity.Usage)
require.Equal(t, identity.RouteToDatabase.ServiceName, "db-a")
},
},
},
{
desc: "db with ttl limit disabled",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
// This expiry should *not* be adjusted to single user cert TTL,
// since ttl limiting is disabled when requester is a local proxy tunnel.
// It *should* be adjusted to the user cert ttl though.
Expires: clock.Now().Add(1000 * time.Hour),
Usage: proto.UserCertsRequest_Database,
RouteToDatabase: proto.RouteToDatabase{
ServiceName: "db-a",
},
RequesterName: proto.UserCertsRequest_TSH_DB_LOCAL_PROXY_TUNNEL,
},
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, userCertExpires, cert.NotAfter)
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageDatabaseOnly}, identity.Usage)
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,
},
},
{
desc: "device extensions copied SSH cert",
newClient: func() (*Client, error) {
u := TestUser(user.GetName())
u.TTL = 1 * time.Hour
// Add device extensions to the fake user's identity.
localUser := u.I.(authz.LocalUser)
localUser.Identity.DeviceExtensions = wantDeviceExtensions
u.I = localUser
return srv.NewClient(u)
},
opts: generateUserSingleUseCertTestOpts{
// Same as SSH options. Nothing special here.
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) {
// SSH certificate.
sshRaw := c.GetSSH()
require.NotEmpty(t, sshRaw, "Got empty single-use SSH certificate")
sshCert, err := sshutils.ParseCertificate(sshRaw)
require.NoError(t, err, "ParseCertificate failed")
gotSSH := tlsca.DeviceExtensions{
DeviceID: sshCert.Extensions[teleport.CertExtensionDeviceID],
AssetTag: sshCert.Extensions[teleport.CertExtensionDeviceAssetTag],
CredentialID: sshCert.Extensions[teleport.CertExtensionDeviceCredentialID],
}
if diff := cmp.Diff(wantDeviceExtensions, gotSSH, protocmp.Transform()); diff != "" {
t.Errorf("SSH DeviceExtensions mismatch (-want +got)\n%s", diff)
}
},
},
},
{
desc: "device extensions copied TLS cert",
newClient: func() (*Client, error) {
u := TestUser(user.GetName())
u.TTL = 1 * time.Hour
// Add device extensions to the fake user's identity.
localUser := u.I.(authz.LocalUser)
localUser.Identity.DeviceExtensions = wantDeviceExtensions
u.I = localUser
return srv.NewClient(u)
},
opts: generateUserSingleUseCertTestOpts{
// Same as Database options. Nothing special here.
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) {
// TLS certificate.
tlsRaw := c.GetTLS()
require.NotEmpty(t, tlsRaw, "Got empty single-use TLS certificate")
block, _ := pem.Decode(tlsRaw)
require.NotNil(t, block, "Decode failed (TLS PEM)")
tlsCert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err, "ParseCertificate failed")
singleUseIdentity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
require.NoError(t, err, "FromSubject failed")
gotTLS := singleUseIdentity.DeviceExtensions
if diff := cmp.Diff(wantDeviceExtensions, gotTLS, protocmp.Transform()); diff != "" {
t.Errorf("TLS DeviceExtensions mismatch (-want +got)\n%s", diff)
}
},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
testClient := cl
if tt.newClient != nil {
var err error
testClient, err = tt.newClient()
require.NoError(t, err, "newClient failed")
}
testGenerateUserSingleUseCert(ctx, t, testClient, 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())
}
var requireMFATypes = []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
types.RequireMFAType_HARDWARE_KEY_TOUCH,
}
func TestIsMFARequired(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildEnterprise})
ctx := context.Background()
srv := newTestTLSServer(t)
// Register an SSH node.
node := &types.ServerV2{
Kind: types.KindNode,
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{"no-mfa-user"}, nil)
require.NoError(t, err)
for _, authPrefRequireMFAType := range requireMFATypes {
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
RequireMFAType: authPrefRequireMFAType,
Webauthn: &types.Webauthn{
RPID: "teleport",
},
})
require.NoError(t, err)
err = srv.Auth().SetAuthPreference(ctx, authPref)
require.NoError(t, err)
for _, roleRequireMFAType := range requireMFATypes {
// If role or auth pref have "hardware_key_touch", expect not required.
expectRequired := !(roleRequireMFAType == types.RequireMFAType_HARDWARE_KEY_TOUCH || authPrefRequireMFAType == types.RequireMFAType_HARDWARE_KEY_TOUCH)
// Otherwise, if auth pref or role require session MFA, expect required.
expectRequired = expectRequired && (roleRequireMFAType.IsSessionMFARequired() || authPrefRequireMFAType.IsSessionMFARequired())
t.Run(fmt.Sprintf("authPref=%v/role=%v/expect=%v", authPrefRequireMFAType, roleRequireMFAType, expectRequired), func(t *testing.T) {
roleOpt := role.GetOptions()
roleOpt.RequireMFAType = roleRequireMFAType
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, expectRequired, resp.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"}, nil)
require.NoError(t, err)
// Require MFA.
roleOpt := role.GetOptions()
roleOpt.RequireMFAType = types.RequireMFAType_SESSION
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 V6 roles are returned to older
// clients, and V6 roles are returned to newer clients.
func TestRoleVersions(t *testing.T) {
srv := newTestTLSServer(t)
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V6,
Metadata: types.Metadata{
Name: "test_role",
},
Spec: types.RoleSpecV6{
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: "11.1.1",
expectedRoleVersion: types.V5,
assertErr: require.NoError,
},
{
desc: "old v9",
clientVersion: "9.0.0",
expectedRoleVersion: types.V5,
assertErr: require.NoError,
},
{
desc: "alpha",
clientVersion: "11.2.4-alpha.0",
expectedRoleVersion: types.V5,
assertErr: require.NoError,
},
{
desc: "greater than 12",
clientVersion: "12.0.0-beta",
expectedRoleVersion: types.V6,
assertErr: require.NoError,
},
{
desc: "12",
clientVersion: "12.0.0",
expectedRoleVersion: types.V6,
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: types.V5,
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{}, nil)
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 := testauthority.New().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)
}
// TestInstanceCertAndControlStream attempts to generate an instance cert via the
// assertion API and use it to handle an inventory ping via the control stream.
func TestInstanceCertAndControlStream(t *testing.T) {
const assertionID = "test-assertion"
const serverID = "test-server"
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv := newTestTLSServer(t)
roles := []types.SystemRole{
types.RoleNode,
types.RoleAuth,
types.RoleProxy,
}
clt, err := srv.NewClient(TestServerID(types.RoleNode, serverID))
require.NoError(t, err)
defer clt.Close()
priv, pub, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err)
pubTLS, err := PrivateKeyToPublicKeyTLS(priv)
require.NoError(t, err)
req := proto.HostCertsRequest{
HostID: serverID,
Role: types.RoleInstance,
PublicSSHKey: pub,
PublicTLSKey: pubTLS,
SystemRoles: roles,
// assertion ID is omitted initially to test
// the failure case
}
// request should fail since clt only holds RoleNode
_, err = clt.GenerateHostCerts(ctx, &req)
require.True(t, trace.IsAccessDenied(err))
// perform assertions
for _, role := range roles {
func() {
clt, err := srv.NewClient(TestServerID(role, serverID))
require.NoError(t, err)
defer clt.Close()
err = clt.UnstableAssertSystemRole(ctx, proto.UnstableSystemRoleAssertion{
ServerID: serverID,
AssertionID: assertionID,
SystemRole: role,
})
require.NoError(t, err)
}()
}
// set assertion ID
req.UnstableSystemRoleAssertionID = assertionID
// assertion should allow us to generate certs
certs, err := clt.GenerateHostCerts(ctx, &req)
require.NoError(t, err)
// make an instance client
instanceCert, err := tls.X509KeyPair(certs.TLS, priv)
require.NoError(t, err)
instanceClt := srv.NewClientWithCert(instanceCert)
// instance cert can self-renew without assertions
req.UnstableSystemRoleAssertionID = ""
_, err = instanceClt.GenerateHostCerts(ctx, &req)
require.NoError(t, err)
stream, err := instanceClt.InventoryControlStream(ctx)
require.NoError(t, err)
defer stream.Close()
err = stream.Send(ctx, proto.UpstreamInventoryHello{
ServerID: serverID,
Version: teleport.Version,
Services: roles,
})
require.NoError(t, err)
select {
case msg := <-stream.Recv():
_, ok := msg.(proto.DownstreamInventoryHello)
require.True(t, ok)
case <-time.After(time.Second * 5):
t.Fatalf("timeout waiting for downstream hello")
}
// fire off a ping in the background
pingErr := make(chan error, 1)
go func() {
defer close(pingErr)
// get an admin client so that we can test pings
clt, err := srv.NewClient(TestAdmin())
if err != nil {
pingErr <- err
return
}
defer clt.Close()
_, err = clt.PingInventory(ctx, proto.InventoryPingRequest{
ServerID: serverID,
})
pingErr <- err
}()
// wait for the ping
select {
case msg := <-stream.Recv():
ping, ok := msg.(proto.DownstreamInventoryPing)
require.True(t, ok)
err = stream.Send(ctx, proto.UpstreamInventoryPong{
ID: ping.ID,
})
require.NoError(t, err)
case <-time.After(time.Second * 5):
t.Fatalf("timeout waiting for downstream ping")
}
// ensure that bg ping routine was successful
require.NoError(t, <-pingErr)
}
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("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)
lock1.SetCreatedBy(string(types.RoleAdmin))
lock1.SetCreatedAt(now)
lock2, err := types.NewLock("lock2", types.LockSpecV2{
Target: types.LockTarget{
Node: "node",
},
Message: "node compromised",
})
require.NoError(t, err)
lock2.SetCreatedBy(string(types.RoleAdmin))
lock2.SetCreatedAt(now)
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)
app3, err := types.NewAppV3(types.Metadata{Name: "app-3"},
types.AppSpecV3{URI: "localhost"})
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.UpsertApplicationServer(ctx, server3)
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)
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)
}
// TestDatabaseServicesCRUD tests DatabaseService resource operations.
func TestDatabaseServicesCRUD(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
clt, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
// Create two DatabaseServices.
db1, err := types.NewDatabaseServiceV1(types.Metadata{
Name: "db1",
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
}, types.DatabaseServiceSpecV1{
ResourceMatchers: []*types.DatabaseResourceMatcher{
{
Labels: &types.Labels{
"env": []string{"prod"},
},
},
},
})
require.NoError(t, err)
db2, err := types.NewDatabaseServiceV1(types.Metadata{
Name: "db2",
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
}, types.DatabaseServiceSpecV1{
ResourceMatchers: []*types.DatabaseResourceMatcher{
{
Labels: &types.Labels{
"env": []string{"stg"},
},
},
},
})
require.NoError(t, err)
// Initially we expect no DatabaseServices.
listServicesResp, err := clt.ListResources(ctx,
proto.ListResourcesRequest{
ResourceType: types.KindDatabaseService,
Limit: apidefaults.DefaultChunkSize,
},
)
require.NoError(t, err)
out, err := types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
require.NoError(t, err)
require.Empty(t, out)
// Create both DatabaseServices.
_, err = clt.UpsertDatabaseService(ctx, db1)
require.NoError(t, err)
_, err = clt.UpsertDatabaseService(ctx, db2)
require.NoError(t, err)
// Fetch all DatabaseServices.
listServicesResp, err = clt.ListResources(ctx,
proto.ListResourcesRequest{
ResourceType: types.KindDatabaseService,
Limit: apidefaults.DefaultChunkSize,
},
)
require.NoError(t, err)
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
require.NoError(t, err)
require.Empty(t, cmp.Diff([]types.DatabaseService{db1, db2}, out,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Update a DatabaseService.
db1.Spec.ResourceMatchers[0] = &types.DatabaseResourceMatcher{
Labels: &types.Labels{
"env": []string{"notprod"},
},
}
_, err = clt.UpsertDatabaseService(ctx, db1)
require.NoError(t, err)
listServicesResp, err = clt.ListResources(ctx,
proto.ListResourcesRequest{
ResourceType: types.KindDatabaseService,
Limit: apidefaults.DefaultChunkSize,
},
)
require.NoError(t, err)
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
require.NoError(t, err)
require.Empty(t, cmp.Diff([]types.DatabaseService{db1, db2}, out,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Delete a DatabaseService.
err = clt.DeleteDatabaseService(ctx, db1.GetName())
require.NoError(t, err)
listServicesResp, err = clt.ListResources(ctx,
proto.ListResourcesRequest{
ResourceType: types.KindDatabaseService,
Limit: apidefaults.DefaultChunkSize,
},
)
require.NoError(t, err)
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
require.NoError(t, err)
require.Empty(t, cmp.Diff([]types.DatabaseService{db2}, out,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Try to delete a DatabaseService that doesn't exist.
err = clt.DeleteDatabaseService(ctx, "doesnotexist")
require.IsType(t, trace.NotFound(""), err)
// Delete all DatabaseServices.
err = clt.DeleteAllDatabaseServices(ctx)
require.NoError(t, err)
listServicesResp, err = clt.ListResources(ctx,
proto.ListResourcesRequest{
ResourceType: types.KindDatabaseService,
Limit: apidefaults.DefaultChunkSize,
},
)
require.NoError(t, err)
out, err = types.ResourcesWithLabels(listServicesResp.Resources).AsDatabaseServices()
require.NoError(t, err)
require.Empty(t, out)
}
// TestSAMLIdPServiceProvidersCRUD tests SAMLIdPServiceProviders resource operations.
func TestSAMLIdPServiceProvidersCRUD(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
clt, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
// Create two SAML IdP service providers.
sp1, err := types.NewSAMLIdPServiceProvider(
types.Metadata{
Name: "sp1",
},
types.SAMLIdPServiceProviderSpecV1{
EntityDescriptor: newEntityDescriptor("sp1"),
EntityID: "sp1",
})
require.NoError(t, err)
sp2, err := types.NewSAMLIdPServiceProvider(
types.Metadata{
Name: "sp2",
},
types.SAMLIdPServiceProviderSpecV1{
EntityDescriptor: newEntityDescriptor("sp2"),
EntityID: "sp2",
})
require.NoError(t, err)
// Initially we expect no service providers.
listResp, nextKey, err := clt.ListSAMLIdPServiceProviders(ctx, 200, "")
require.NoError(t, err)
require.Empty(t, nextKey)
require.Empty(t, listResp)
// Create both service providers
err = clt.CreateSAMLIdPServiceProvider(ctx, sp1)
require.NoError(t, err)
err = clt.CreateSAMLIdPServiceProvider(ctx, sp2)
require.NoError(t, err)
// Fetch all service providers
listResp, nextKey, err = clt.ListSAMLIdPServiceProviders(ctx, 200, "")
require.NoError(t, err)
require.Empty(t, nextKey)
require.Empty(t, cmp.Diff([]types.SAMLIdPServiceProvider{sp1, sp2}, listResp,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Update a service provider.
sp1.SetEntityDescriptor(newEntityDescriptor("updated-sp1"))
sp1.SetEntityID("updated-sp1")
err = clt.UpdateSAMLIdPServiceProvider(ctx, sp1)
require.NoError(t, err)
listResp, nextKey, err = clt.ListSAMLIdPServiceProviders(ctx, 200, "")
require.NoError(t, err)
require.Empty(t, nextKey)
require.Empty(t, cmp.Diff([]types.SAMLIdPServiceProvider{sp1, sp2}, listResp,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Delete a service provider.
err = clt.DeleteSAMLIdPServiceProvider(ctx, sp1.GetName())
require.NoError(t, err)
listResp, nextKey, err = clt.ListSAMLIdPServiceProviders(ctx, 200, "")
require.NoError(t, err)
require.Empty(t, nextKey)
require.Empty(t, cmp.Diff([]types.SAMLIdPServiceProvider{sp2}, listResp,
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
))
// Try to delete a service provider that doesn't exist.
err = clt.DeleteSAMLIdPServiceProvider(ctx, "doesnotexist")
require.True(t, trace.IsNotFound(err))
// Delete all service providers.
err = clt.DeleteAllSAMLIdPServiceProviders(ctx)
require.NoError(t, err)
listResp, nextKey, err = clt.ListSAMLIdPServiceProviders(ctx, 200, "")
require.NoError(t, err)
require.Empty(t, nextKey)
require.Empty(t, listResp)
}
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
},
},
"KubeServer": {
resourceType: types.KindKubeServer,
createResource: func(name string, clt *Client) error {
kube, err := types.NewKubernetesClusterV3(
types.Metadata{
Name: name,
Labels: map[string]string{"name": name},
},
types.KubernetesClusterSpecV3{},
)
if err != nil {
return err
}
kubeServer, err := types.NewKubernetesServerV3FromCluster(kube, "_", "_")
if err != nil {
return err
}
_, err = clt.UpsertKubernetesServer(ctx, kubeServer)
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)
// ListResources should also work when called on auth directly
resp, err = srv.Auth().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.KindKubeServer {
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.Equal(t, 2, resp.TotalCount)
} else {
// Test listing with NeedTotalCount flag.
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)
})
}
}
type mockAuthorizer struct {
ctx *authz.Context
err error
}
func (a mockAuthorizer) Authorize(context.Context) (*authz.Context, error) {
return a.ctx, a.err
}
type mockTraceClient struct {
err error
spans []*otlptracev1.ResourceSpans
}
func (m mockTraceClient) Start(ctx context.Context) error {
return nil
}
func (m mockTraceClient) Stop(ctx context.Context) error {
return nil
}
func (m *mockTraceClient) UploadTraces(ctx context.Context, protoSpans []*otlptracev1.ResourceSpans) error {
m.spans = protoSpans
return m.err
}
func TestExport(t *testing.T) {
t.Parallel()
uploadErr := trace.AccessDenied("failed to upload")
const user = "user"
validateResource := func(forwardedFor string, resourceSpan *otlptracev1.ResourceSpans) {
var forwarded []string
for _, attribute := range resourceSpan.Resource.Attributes {
if attribute.Key == forwardedTag {
forwarded = append(forwarded, attribute.Value.GetStringValue())
}
}
require.Len(t, forwarded, 1)
for _, scopeSpan := range resourceSpan.ScopeSpans {
for _, span := range scopeSpan.Spans {
for _, attribute := range span.Attributes {
if attribute.Key == forwardedTag {
forwarded = append(forwarded, attribute.Value.GetStringValue())
}
}
}
}
require.Len(t, forwarded, 2)
for _, value := range forwarded {
require.Equal(t, forwardedFor, value)
}
}
validateTaggedSpans := func(forwardedFor string) require.ValueAssertionFunc {
return func(t require.TestingT, i interface{}, i2 ...interface{}) {
require.NotEmpty(t, i)
resourceSpans, ok := i.([]*otlptracev1.ResourceSpans)
require.True(t, ok)
for _, resourceSpan := range resourceSpans {
if resourceSpan.Resource != nil {
validateResource(forwardedFor, resourceSpan)
return
}
for _, scopeSpan := range resourceSpan.ScopeSpans {
for _, span := range scopeSpan.Spans {
var foundForwardedTag bool
for _, attribute := range span.Attributes {
if attribute.Key == forwardedTag {
require.False(t, foundForwardedTag)
foundForwardedTag = true
require.Equal(t, forwardedFor, attribute.Value.GetStringValue())
}
}
require.True(t, foundForwardedTag)
}
}
}
}
}
testSpans := []*otlptracev1.ResourceSpans{
{
Resource: &otlpresourcev1.Resource{
Attributes: []*otlpcommonv1.KeyValue{
{
Key: "test",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_IntValue{
IntValue: 1,
},
},
},
{
Key: "key",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_StringValue{
StringValue: user,
},
},
},
},
},
ScopeSpans: []*otlptracev1.ScopeSpans{
{
Spans: []*otlptracev1.Span{
{
Name: "with-attributes",
Attributes: []*otlpcommonv1.KeyValue{
{
Key: "test",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_IntValue{
IntValue: 1,
},
},
},
{
Key: "key",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_DoubleValue{
DoubleValue: 5.0,
},
},
},
},
},
{
Name: "with-tag",
Attributes: []*otlpcommonv1.KeyValue{{Key: forwardedTag, Value: &otlpcommonv1.AnyValue{Value: &otlpcommonv1.AnyValue_StringValue{StringValue: "test"}}}},
},
{
Name: "no-attributes",
},
},
},
},
},
{
ScopeSpans: []*otlptracev1.ScopeSpans{
{
Spans: []*otlptracev1.Span{
{
Name: "more-with-attributes",
Attributes: []*otlpcommonv1.KeyValue{
{
Key: "test2",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_IntValue{
IntValue: 11,
},
},
},
{
Key: "key2",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_DoubleValue{
DoubleValue: 15.0,
},
},
},
},
},
{
Name: "already-tagged",
Attributes: []*otlpcommonv1.KeyValue{
{
Key: forwardedTag,
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_StringValue{
StringValue: user,
},
},
},
{
Key: "key2",
Value: &otlpcommonv1.AnyValue{
Value: &otlpcommonv1.AnyValue_DoubleValue{
DoubleValue: 15.0,
},
},
},
},
},
},
},
},
},
}
cases := []struct {
name string
identity TestIdentity
errAssertion require.ErrorAssertionFunc
uploadedAssertion require.ValueAssertionFunc
spans []*otlptracev1.ResourceSpans
authorizer authz.Authorizer
mockTraceClient mockTraceClient
}{
{
name: "error when unauthorized",
identity: TestNop(),
errAssertion: require.Error,
uploadedAssertion: require.Empty,
spans: make([]*otlptracev1.ResourceSpans, 1),
authorizer: &mockAuthorizer{err: trace.AccessDenied("unauthorized")},
},
{
name: "nop for empty spans",
identity: TestBuiltin(types.RoleNode),
errAssertion: require.NoError,
uploadedAssertion: require.Empty,
},
{
name: "failure to forward spans",
identity: TestBuiltin(types.RoleNode),
errAssertion: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.ErrorIs(t, trail.FromGRPC(trace.Unwrap(err)), uploadErr)
},
uploadedAssertion: func(t require.TestingT, i interface{}, i2 ...interface{}) {
require.NotNil(t, i)
require.Len(t, i, 1)
},
spans: make([]*otlptracev1.ResourceSpans, 1),
mockTraceClient: mockTraceClient{err: uploadErr},
},
{
name: "forwarded spans get tagged for system roles",
identity: TestBuiltin(types.RoleProxy),
errAssertion: require.NoError,
spans: testSpans,
uploadedAssertion: validateTaggedSpans(fmt.Sprintf("%s.localhost:%s", types.RoleProxy, types.RoleProxy)),
},
{
name: "forwarded spans get tagged for users",
identity: TestUser(user),
errAssertion: require.NoError,
spans: testSpans,
uploadedAssertion: validateTaggedSpans(user),
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
as, err := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
Clock: clockwork.NewFakeClock(),
TraceClient: &tt.mockTraceClient,
})
require.NoError(t, err)
srv, err := as.NewTestTLSServer()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, srv.Close()) })
// Create a fake user.
_, _, err = CreateUserAndRole(srv.Auth(), user, []string{"role"}, nil)
require.NoError(t, err)
// Setup the server
if tt.authorizer != nil {
srv.TLSServer.grpcServer.Authorizer = tt.authorizer
require.NoError(t, err)
}
// Get a client for the test identity
clt, err := srv.NewClient(tt.identity)
require.NoError(t, err)
// create a tracing client and forward some traces
traceClt := tracing.NewClient(clt.APIClient.GetConnection())
t.Cleanup(func() { require.NoError(t, traceClt.Close()) })
require.NoError(t, traceClt.Start(ctx))
tt.errAssertion(t, traceClt.UploadTraces(ctx, tt.spans))
tt.uploadedAssertion(t, tt.mockTraceClient.spans)
})
}
}
// TestGRPCServer_CreateToken tests the handler of the deprecated CreateToken
// RPC.
func TestGRPCServer_CreateToken(t *testing.T) {
ctx := context.Background()
server := newTestTLSServer(t)
// Allow us to directly invoke the deprecated gRPC methods with
// authentication.
user := TestAdmin()
ctx = authz.ContextWithUser(ctx, user.I)
// Test default expiry is applied.
t.Run("undefined-expiry", func(t *testing.T) {
tokenName := "undefined-expiry"
roles := types.SystemRoles{types.RoleNode}
token, err := types.NewProvisionToken(
tokenName,
roles,
time.Time{},
)
require.NoError(t, err)
_, err = server.TLSServer.grpcServer.CreateToken(
ctx, token.(*types.ProvisionTokenV2),
)
require.NoError(t, err)
token, err = server.TLSServer.grpcServer.GetToken(
ctx, &types.ResourceRequest{Name: tokenName},
)
require.NoError(t, err)
require.Equal(t, tokenName, token.GetName())
ttl := token.Expiry().Sub(server.Clock().Now())
defaultTTL := defaults.ProvisioningTokenTTL
require.LessOrEqual(t, ttl, defaultTTL)
require.GreaterOrEqual(t, ttl, defaultTTL-(time.Second*10))
})
// Test set expiry is applied.
t.Run("set-expiry", func(t *testing.T) {
tokenName := "set-expiry"
roles := types.SystemRoles{types.RoleNode}
ttl := time.Hour * 24
token, err := types.NewProvisionToken(
tokenName,
roles,
server.Clock().Now().Add(ttl),
)
require.NoError(t, err)
_, err = server.TLSServer.grpcServer.CreateToken(
ctx, token.(*types.ProvisionTokenV2),
)
require.NoError(t, err)
token, err = server.TLSServer.grpcServer.GetToken(
ctx, &types.ResourceRequest{Name: tokenName},
)
require.NoError(t, err)
require.Equal(t, tokenName, token.GetName())
actualTTL := token.Expiry().Sub(server.Clock().Now())
require.LessOrEqual(t, actualTTL, ttl)
require.GreaterOrEqual(t, actualTTL, ttl-(time.Second*10))
})
// Test expiry in past is changed to default
t.Run("past-expiry", func(t *testing.T) {
tokenName := "past-expiry"
roles := types.SystemRoles{types.RoleNode}
token, err := types.NewProvisionToken(
tokenName,
roles,
server.Clock().Now().Add(-1*time.Hour),
)
require.NoError(t, err)
_, err = server.TLSServer.grpcServer.CreateToken(
ctx, token.(*types.ProvisionTokenV2),
)
require.NoError(t, err)
token, err = server.TLSServer.grpcServer.GetToken(
ctx, &types.ResourceRequest{Name: tokenName},
)
require.NoError(t, err)
require.Equal(t, tokenName, token.GetName())
ttl := token.Expiry().Sub(server.Clock().Now())
defaultTTL := defaults.ProvisioningTokenTTL
require.LessOrEqual(t, ttl, defaultTTL)
require.GreaterOrEqual(t, ttl, defaultTTL-(time.Second*10))
})
}
// TestGRPCServer_UpsertToken tests the handler of the deprecated CreateToken
// RPC.
func TestGRPCServer_UpsertToken(t *testing.T) {
ctx := context.Background()
server := newTestTLSServer(t)
// Allow us to directly invoke the deprecated gRPC methods with
// authentication.
user := TestAdmin()
ctx = authz.ContextWithUser(ctx, user.I)
// Test default expiry is applied.
t.Run("undefined-expiry", func(t *testing.T) {
tokenName := "undefined-expiry"
roles := types.SystemRoles{types.RoleNode}
token, err := types.NewProvisionToken(
tokenName,
roles,
time.Time{},
)
require.NoError(t, err)
_, err = server.TLSServer.grpcServer.UpsertToken(
ctx, token.(*types.ProvisionTokenV2),
)
require.NoError(t, err)
token, err = server.TLSServer.grpcServer.GetToken(
ctx, &types.ResourceRequest{Name: tokenName},
)
require.NoError(t, err)
require.Equal(t, tokenName, token.GetName())
ttl := token.Expiry().Sub(server.Clock().Now())
defaultTTL := defaults.ProvisioningTokenTTL
require.LessOrEqual(t, ttl, defaultTTL)
require.GreaterOrEqual(t, ttl, defaultTTL-(time.Second*10))
})
// Test set expiry is applied.
t.Run("set-expiry", func(t *testing.T) {
tokenName := "set-expiry"
roles := types.SystemRoles{types.RoleNode}
ttl := time.Hour * 24
token, err := types.NewProvisionToken(
tokenName,
roles,
server.Clock().Now().Add(ttl),
)
require.NoError(t, err)
_, err = server.TLSServer.grpcServer.UpsertToken(
ctx, token.(*types.ProvisionTokenV2),
)
require.NoError(t, err)
token, err = server.TLSServer.grpcServer.GetToken(
ctx, &types.ResourceRequest{Name: tokenName},
)
require.NoError(t, err)
require.Equal(t, tokenName, token.GetName())
actualTTL := token.Expiry().Sub(server.Clock().Now())
require.LessOrEqual(t, actualTTL, ttl)
require.GreaterOrEqual(t, actualTTL, ttl-(time.Second*10))
})
// Test expiry in past is changed to default
t.Run("past-expiry", func(t *testing.T) {
tokenName := "past-expiry"
roles := types.SystemRoles{types.RoleNode}
token, err := types.NewProvisionToken(
tokenName,
roles,
server.Clock().Now().Add(-1*time.Hour),
)
require.NoError(t, err)
_, err = server.TLSServer.grpcServer.UpsertToken(
ctx, token.(*types.ProvisionTokenV2),
)
require.NoError(t, err)
token, err = server.TLSServer.grpcServer.GetToken(
ctx, &types.ResourceRequest{Name: tokenName},
)
require.NoError(t, err)
require.Equal(t, tokenName, token.GetName())
ttl := token.Expiry().Sub(server.Clock().Now())
defaultTTL := defaults.ProvisioningTokenTTL
require.LessOrEqual(t, ttl, defaultTTL)
require.GreaterOrEqual(t, ttl, defaultTTL-(time.Second*10))
})
}
// TestSAMLValidation tests that SAML validation does not perform an HTTP
// request if the calling user does not have permissions to create or update
// a SAML connector.
func TestSAMLValidation(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestFeatures: modules.Features{SAML: true},
})
// minimal entity_descriptor to pass validation. not actually valid
const minimalEntityDescriptor = `
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://example.com">
<md:IDPSSODescriptor>
<md:SingleSignOnService Location="http://example.com" />
</md:IDPSSODescriptor>
</md:EntityDescriptor>`
allowSAMLUpsert := types.RoleConditions{
Rules: []types.Rule{{
Resources: []string{types.KindSAML},
Verbs: []string{types.VerbCreate, types.VerbUpdate},
}},
}
testCases := []struct {
desc string
allow types.RoleConditions
entityDescriptor string
entityServerCalled bool
assertErr func(error) bool
}{
{
desc: "access denied",
allow: types.RoleConditions{},
entityServerCalled: false,
assertErr: trace.IsAccessDenied,
},
{
desc: "validation failure",
allow: allowSAMLUpsert,
entityDescriptor: "", // validation fails with no issuer
entityServerCalled: true,
assertErr: trace.IsBadParameter,
},
{
desc: "access permitted",
allow: allowSAMLUpsert,
entityDescriptor: minimalEntityDescriptor,
entityServerCalled: true,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server := newTestTLSServer(t)
// Create an http server to serve the entity descriptor url
entityServerCalled := false
entityServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
entityServerCalled = true
_, err := w.Write([]byte(tc.entityDescriptor))
require.NoError(t, err)
}))
role, err := CreateRole(ctx, server.Auth(), "test_role", types.RoleSpecV6{Allow: tc.allow})
require.NoError(t, err)
user, err := CreateUser(server.Auth(), "test_user", role)
require.NoError(t, err)
connector, err := types.NewSAMLConnector("test_connector", types.SAMLConnectorSpecV2{
AssertionConsumerService: "http://localhost:65535/acs", // not called
EntityDescriptorURL: entityServer.URL,
AttributesToRoles: []types.AttributeMapping{
// not used. can be any name, value but role must exist
{Name: "groups", Value: "admin", Roles: []string{role.GetName()}},
},
})
require.NoError(t, err)
client, err := server.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
err = client.UpsertSAMLConnector(ctx, connector)
if tc.assertErr != nil {
require.Error(t, err)
require.True(t, tc.assertErr(err), "UpsertSAMLConnector error type mismatch. got: %T", trace.Unwrap(err))
} else {
require.NoError(t, err)
}
if tc.entityServerCalled {
require.True(t, entityServerCalled, "entity_descriptor_url was not called")
} else {
require.False(t, entityServerCalled, "entity_descriptor_url was called")
}
})
}
}
func newEntityDescriptor(entityID string) string {
return fmt.Sprintf(testEntityDescriptor, entityID)
}
// A test entity descriptor from https://sptest.iamshowcase.com/testsp_metadata.xml.
const testEntityDescriptor = `
<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="%s" validUntil="2025-12-09T09:13:31.006Z">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sptest.iamshowcase.com/acs" index="0" isDefault="true"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
`
func TestGRPCServer_GetInstallers(t *testing.T) {
t.Parallel()
ctx := context.Background()
server := newTestTLSServer(t)
grpc := server.TLSServer.grpcServer
user := TestAdmin()
ctx = authz.ContextWithUser(ctx, user.I)
tests := []struct {
name string
inputInstallers map[string]string
expectedInstallers map[string]string
}{
{
name: "default installers only",
expectedInstallers: map[string]string{
installers.InstallerScriptName: installers.DefaultInstaller.GetScript(),
installers.InstallerScriptNameAgentless: installers.DefaultAgentlessInstaller.GetScript(),
},
},
{
name: "default and custom installers",
inputInstallers: map[string]string{
"my-custom-installer": "echo test",
},
expectedInstallers: map[string]string{
"my-custom-installer": "echo test",
installers.InstallerScriptName: installers.DefaultInstaller.GetScript(),
installers.InstallerScriptNameAgentless: installers.DefaultAgentlessInstaller.GetScript(),
},
},
{
name: "override default installer",
inputInstallers: map[string]string{
installers.InstallerScriptName: "echo test",
},
expectedInstallers: map[string]string{
installers.InstallerScriptName: "echo test",
installers.InstallerScriptNameAgentless: installers.DefaultAgentlessInstaller.GetScript(),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
_, err := grpc.DeleteAllInstallers(ctx, &emptypb.Empty{})
require.NoError(t, err)
})
for name, script := range tc.inputInstallers {
installer, err := types.NewInstallerV1(name, script)
require.NoError(t, err)
_, err = grpc.SetInstaller(ctx, installer)
require.NoError(t, err)
}
outputInstallerList, err := grpc.GetInstallers(ctx, &emptypb.Empty{})
require.NoError(t, err)
outputInstallers := make(map[string]string, len(tc.expectedInstallers))
for _, installer := range outputInstallerList.Installers {
outputInstallers[installer.GetName()] = installer.GetScript()
}
require.Equal(t, tc.expectedInstallers, outputInstallers)
})
}
}