Files
teleport/lib/auth/grpcserver_test.go
T
rosstimothy 3c3b0b9c0c Sanitize SSH server hostnames (#48988)
* Sanitize SSH server hostnames

Prevents any invalid and malicious hostnames, but replacing them with
known valid data already associated with the host. This was chosen
instead of rejecting to persist the server resource in an attempt to
continue providing access to the host in order to remedy the invalid
hostname.

Any servers that represent a Teleport ssh_service with an invalid
hostname will be replaced by the host UUID. Any static OpenSSH servers
will have invalid hostnames replaced with the address. This will continue
to allow the hosts to be dialable. In order to make these hosts
discoverable, the invalid hostname will be set in the
"teleport.internal/invalid-hostname" label.

Updates https://github.com/gravitational/teleport-private/issues/1676.

* add and use internal update node method

* add test coverage for UpdateNode
2024-11-15 19:04:45 +00:00

4766 lines
150 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth
import (
"context"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"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"
prom_client_model "github.com/prometheus/client_model/go"
"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/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"
clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/observability/tracing"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/installers"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/entitlements"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/testauthority"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/defaults"
dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/srv/server/installer"
"github.com/gravitational/teleport/lib/tlsca"
)
func TestMFADeviceManagement(t *testing.T) {
testServer := newTestTLSServer(t)
authServer := testServer.Auth()
clock := testServer.Clock().(clockwork.FakeClock)
ctx := context.Background()
// Enable MFA support.
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactors: []types.SecondFactorType{
types.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
types.SecondFactorType_SECOND_FACTOR_TYPE_SSO,
},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
const webOrigin = "https://localhost" // matches RPID above
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
// Create a fake user.
user, _, err := CreateUserAndRole(authServer, "mfa-user", []string{"role"}, nil)
require.NoError(t, err)
userClient, err := testServer.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
// No MFA devices should exist for a new user.
resp, err := userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
require.NoError(t, err)
require.Empty(t, resp.Devices)
// Add one device of each kind
devs := addOneOfEachMFADevice(t, userClient, clock, webOrigin)
// Run scenarios beyond adding one of each device, both happy 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{
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{
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{
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, wantypes.CredentialCreationFromProto(challenge.GetWebauthn()))
require.NoError(t, err)
return &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_Webauthn{
Webauthn: wantypes.CredentialCreationResponseToProto(ccr),
},
}
},
checkRegisterErr: require.NoError,
},
},
{
desc: "fail a webauthn auth challenge",
opts: mfaAddTestOpts{
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, wantypes.CredentialAssertionFromProto(challenge.WebauthnChallenge))
require.NoError(t, err)
return &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_Webauthn{
Webauthn: wantypes.CredentialAssertionResponseToProto(resp),
},
}
},
checkAuthErr: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err))
},
},
},
{
desc: "fail a webauthn registration challenge",
opts: mfaAddTestOpts{
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 */, wantypes.CredentialCreationFromProto(challenge.GetWebauthn()))
require.NoError(t, err)
return &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_Webauthn{
Webauthn: wantypes.CredentialCreationResponseToProto(ccr),
},
}
},
checkRegisterErr: func(t require.TestingT, err error, i ...interface{}) {
require.Error(t, err)
require.True(t, trace.IsBadParameter(err))
},
},
},
{
desc: "add passwordless device",
opts: mfaAddTestOpts{
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, wantypes.CredentialCreationFromProto(challenge.GetWebauthn()))
require.NoError(t, err)
return &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_Webauthn{
Webauthn: wantypes.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 _, test := range addTests {
t.Run(test.desc, func(t *testing.T) {
testAddMFADevice(ctx, t, userClient, test.opts)
})
}
// Register an extra device to test allow deletion of other devices and test that
// the last device cannot be deleted.
const lastDeviceName = "lastDevice"
lastDevice, err := RegisterTestDevice(ctx, userClient, lastDeviceName, proto.DeviceType_DEVICE_TYPE_WEBAUTHN, devs.WebDev)
require.NoError(t, err, "RegisterTestDevice failed")
// Also add a password so we can testing add last non-passkey MFA device. Testing the
// deletion of the last passkey is handled in TestDeletingLastPasswordlessDevice below.
err = authServer.UpsertPassword(user.GetName(), []byte("living on the edge"))
require.NoError(t, err, "UpsertPassword")
// Since this device won't be deleted, we can use it to solve webauthn
// challenges throughout the tests below.
lastDeviceWebAuthnHandler := func(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.NotNil(t, challenge.WebauthnChallenge, "nil Webauthn challenge")
mfaResp, err := lastDevice.SolveAuthn(challenge)
require.NoError(t, err, "SolveAuthn")
return mfaResp
}
// Check that all new devices are registered.
resp, err = userClient.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, []string{lastDeviceName, pwdlessDevName, devs.TOTPName, devs.WebName, webDev2Name}, deviceNames)
// Delete several of the MFA devices.
deleteTests := []struct {
desc string
opts mfaDeleteTestOpts
}{
{
desc: "fail to delete an unknown device",
opts: mfaDeleteTestOpts{
deviceName: "unknown-dev",
authHandler: lastDeviceWebAuthnHandler,
checkErr: require.Error,
},
},
{
desc: "fail a TOTP auth challenge",
opts: mfaDeleteTestOpts{
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{
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, wantypes.CredentialAssertionFromProto(challenge.WebauthnChallenge))
require.NoError(t, err)
return &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_Webauthn{
Webauthn: wantypes.CredentialAssertionResponseToProto(resp),
},
}
},
checkErr: require.Error,
},
},
{
desc: "delete TOTP device by name",
opts: mfaDeleteTestOpts{
deviceName: devs.TOTPName,
authHandler: lastDeviceWebAuthnHandler,
checkErr: require.NoError,
},
},
{
desc: "delete webauthn device by name",
opts: mfaDeleteTestOpts{
deviceName: devs.WebName,
authHandler: lastDeviceWebAuthnHandler,
checkErr: require.NoError,
},
},
{
desc: "delete passwordless device by name",
opts: mfaDeleteTestOpts{
deviceName: pwdlessDevName,
authHandler: lastDeviceWebAuthnHandler,
checkErr: require.NoError,
},
},
{
desc: "delete webauthn device by ID",
opts: mfaDeleteTestOpts{
deviceName: deviceIDs[webDev2Name],
authHandler: lastDeviceWebAuthnHandler,
checkErr: require.NoError,
},
},
{
desc: "fail to delete last device",
opts: mfaDeleteTestOpts{
deviceName: lastDeviceName,
authHandler: lastDeviceWebAuthnHandler,
checkErr: func(t require.TestingT, err error, _ ...any) {
require.ErrorContains(t,
err,
"cannot delete the last MFA device for this user",
"Unexpected error deleting last MFA device",
)
},
},
},
}
for _, test := range deleteTests {
t.Run(test.desc, func(t *testing.T) {
testDeleteMFADevice(ctx, t, userClient, test.opts)
})
}
// Check no remaining devices, apart from the additional device that we can't delete.
resp, err = userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
require.NoError(t, err)
require.Equal(t, lastDeviceName, resp.Devices[0].GetName())
}
func TestMFADeviceManagement_SSO(t *testing.T) {
testServer := newTestTLSServer(t)
authServer := testServer.Auth()
ctx := context.Background()
// Enable MFA support.
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactors: []types.SecondFactorType{
types.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
types.SecondFactorType_SECOND_FACTOR_TYPE_SSO,
},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
// Create a fake user.
user, _, err := CreateUserAndRole(authServer, "mfa-user", []string{"role"}, nil)
require.NoError(t, err)
userClient, err := testServer.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
// Create an auth connector.
samlConnector, err := types.NewSAMLConnector("saml", types.SAMLConnectorSpecV2{
AssertionConsumerService: "http://localhost:65535/acs", // not called
Issuer: "test",
SSO: "https://localhost:65535/sso", // not called
AttributesToRoles: []types.AttributeMapping{
// not used. can be any name, value but role must exist
{Name: "groups", Value: "admin", Roles: user.GetRoles()},
},
})
require.NoError(t, err)
_, err = authServer.UpsertSAMLConnector(ctx, samlConnector)
require.NoError(t, err)
// Convert the user to an SSO user for this auth connector.
userCreatedAt := authServer.clock.Now()
user.SetCreatedBy(types.CreatedBy{
Time: userCreatedAt,
Connector: &types.ConnectorRef{
ID: samlConnector.GetKind(),
Type: samlConnector.GetName(),
},
})
_, err = authServer.UpsertUser(ctx, user)
require.NoError(t, err)
// No MFA devices should exist for the user.
resp, err := userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
require.NoError(t, err)
require.Empty(t, resp.Devices)
// prepare a passwordless device.
passkeyName := "passkey"
passkey, err := RegisterTestDevice(ctx, userClient, passkeyName, proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil, WithPasswordless())
require.NoError(t, err, "RegisterTestDevice")
passkeyWebAuthnHandler := func(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.NotNil(t, challenge.WebauthnChallenge, "nil Webauthn challenge")
mfaResp, err := passkey.SolveAuthn(challenge)
require.NoError(t, err, "SolveAuthn")
return mfaResp
}
resp, err = userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
require.NoError(t, err)
require.Len(t, resp.Devices, 1)
webauthnDevice := resp.Devices[0]
// Update the auth connector to enable SSO MFA.
samlConnector.SetMFASettings(&types.SAMLConnectorMFASettings{
Enabled: true,
Issuer: "test",
Sso: "https://localhost:65535/sso", // not called
})
require.NoError(t, err)
_, err = authServer.UpsertSAMLConnector(ctx, samlConnector)
require.NoError(t, err)
// Ephemeral sso device should show up in the list now. It can't be deleted.
resp, err = userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
assert.NoError(t, err)
assert.Len(t, resp.Devices, 2)
expectSSODev, err := types.NewMFADevice(samlConnector.GetDisplay(), samlConnector.GetName(), userCreatedAt, &types.MFADevice_Sso{
Sso: &types.SSOMFADevice{
ConnectorId: samlConnector.GetName(),
ConnectorType: samlConnector.GetKind(),
DisplayName: samlConnector.GetDisplay(),
},
})
require.NoError(t, err)
assert.Equal(t, []*types.MFADevice{webauthnDevice, expectSSODev}, resp.Devices)
testDeleteMFADevice(ctx, t, userClient, mfaDeleteTestOpts{
deviceName: "saml",
authHandler: passkeyWebAuthnHandler,
checkErr: func(t require.TestingT, err error, _ ...interface{}) {
assert.ErrorAs(t, err, new(*trace.BadParameterError))
assert.ErrorContains(t, err, "cannot delete ephemeral SSO MFA device")
}},
)
// Last non-SSO, passwordless device can be deleted now.
testDeleteMFADevice(ctx, t, userClient, mfaDeleteTestOpts{
deviceName: passkeyName,
authHandler: passkeyWebAuthnHandler,
checkErr: require.NoError,
})
}
func TestDeletingLastPasswordlessDevice(t *testing.T) {
testServer := newTestTLSServer(t)
authServer := testServer.Auth()
clock := testServer.Clock().(clockwork.FakeClock)
ctx := context.Background()
tests := []struct {
name string
setup func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice)
checkErr require.ErrorAssertionFunc
}{
{
name: "NOK no other MFA device",
setup: func(*testing.T, string, *authclient.Client, *TestDevice) {},
checkErr: func(t require.TestingT, err error, _ ...any) {
require.ErrorContains(t,
err,
"cannot delete the last MFA device for this user",
"Unexpected error deleting last passwordless device",
)
},
},
{
name: "OK extra passwordless device",
setup: func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice) {
_, err := RegisterTestDevice(ctx, userClient, "another-passkey", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, pwdlessDev, WithPasswordless())
require.NoError(t, err, "RegisterTestDevice failed")
},
checkErr: require.NoError,
},
{
name: "OK password set with other WebAuthn device",
setup: func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice) {
err := authServer.UpsertPassword(username, []byte("living on the edge"))
require.NoError(t, err, "UpsertPassword")
_, err = RegisterTestDevice(
ctx, userClient, "another-dev", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, pwdlessDev)
require.NoError(t, err, "RegisterTestDevice")
},
checkErr: require.NoError,
},
{
name: "OK password set with other TOTP device",
setup: func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice) {
err := authServer.UpsertPassword(username, []byte("living on the edge"))
require.NoError(t, err, "UpsertPassword")
_, err = RegisterTestDevice(
ctx, userClient, "another-dev", proto.DeviceType_DEVICE_TYPE_TOTP, pwdlessDev, WithTestDeviceClock(clock))
require.NoError(t, err, "RegisterTestDevice")
},
checkErr: require.NoError,
},
{
name: "OK SSO user with other device",
setup: func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice) {
user, err := authServer.GetUser(ctx, username, false)
require.NoError(t, err, "GetUser")
user.SetCreatedBy(types.CreatedBy{
Connector: &types.ConnectorRef{},
})
_, err = authServer.UpsertUser(ctx, user)
require.NoError(t, err, "UpsertUser")
_, err = RegisterTestDevice(
ctx, userClient, "another-dev", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, pwdlessDev)
require.NoError(t, err, "RegisterTestDevice")
},
checkErr: require.NoError,
},
{
name: "NOK password set but no other MFAs",
setup: func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice) {
err := authServer.UpsertPassword(username, []byte("living on the edge"))
require.NoError(t, err, "UpsertPassword")
},
checkErr: func(t require.TestingT, err error, _ ...any) {
require.ErrorContains(t,
err,
"cannot delete the last MFA device for this user",
"Unexpected error deleting last passwordless device",
)
},
},
{
name: "NOK other MFAs, but no password set",
setup: func(t *testing.T, username string, userClient *authclient.Client, pwdlessDev *TestDevice) {
_, err := RegisterTestDevice(
ctx, userClient, "another-dev", proto.DeviceType_DEVICE_TYPE_TOTP, pwdlessDev, WithTestDeviceClock(clock))
require.NoError(t, err, "RegisterTestDevice")
},
checkErr: func(t require.TestingT, err error, _ ...any) {
require.ErrorContains(t,
err,
"cannot delete last passwordless credential for user",
"Unexpected error deleting last passwordless device",
)
},
},
{
name: "NOK other MFAs, but no password set, passwordless is off",
setup: func(t *testing.T, _ string, userClient *authclient.Client, pwdlessDev *TestDevice) {
// Register a non-passwordless device without adding a password.
_, err := RegisterTestDevice(ctx, userClient, "another-dev", proto.DeviceType_DEVICE_TYPE_TOTP, pwdlessDev, WithTestDeviceClock(clock))
require.NoError(t, err, "RegisterTestDevice")
authPref, err := authServer.GetAuthPreference(ctx)
require.NoError(t, err, "GetAuthPreference")
// Turn off passwordless authentication.
authPref.SetAllowPasswordless(false)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err, "UpsertAuthPreference")
},
checkErr: func(t require.TestingT, err error, _ ...any) {
require.ErrorContains(t,
err,
"cannot delete last passwordless credential for user",
"Unexpected error deleting last passwordless device",
)
},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Enable MFA support.
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactors: []types.SecondFactorType{
types.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
// Create a fake user.
username := fmt.Sprintf("mfa-user-%d", i)
user, _, err := CreateUserAndRole(authServer, username, []string{"role"}, nil)
require.NoError(t, err)
userClient, err := testServer.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
// No MFA devices should exist for a new user.
resp, err := userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
require.NoError(t, err)
require.Empty(t, resp.Devices)
// Add the passwordless device to be deleted.
pwdlessDevName := "pwdless-dev"
pwdlessDev, err := RegisterTestDevice(
ctx, userClient, pwdlessDevName, proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil, WithPasswordless())
require.NoError(t, err)
// Case-specific setup.
test.setup(t, username, userClient, pwdlessDev)
// Delete the last passwordless device.
testDeleteMFADevice(ctx, t, userClient, mfaDeleteTestOpts{
deviceName: pwdlessDevName,
authHandler: func(t *testing.T, ch *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.NotNil(t, ch.WebauthnChallenge, "nil Webauthn challenge")
mfaResp, err := pwdlessDev.SolveAuthn(ch)
require.NoError(t, err, "SolveAuthn")
return mfaResp
},
checkErr: test.checkErr,
})
})
}
}
type mfaDevices struct {
clock clockwork.Clock
webOrigin string
TOTPName string
TOTPDev *TestDevice
WebName string
WebDev *TestDevice
}
func (d *mfaDevices) totpAuthHandler(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.NotNil(t, challenge.TOTP, "nil TOTP challenge")
if c, ok := d.clock.(clockwork.FakeClock); ok {
c.Advance(30 * time.Second)
}
mfaResp, err := d.TOTPDev.SolveAuthn(challenge)
require.NoError(t, err, "SolveAuthn")
return mfaResp
}
func (d *mfaDevices) webAuthHandler(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.NotNil(t, challenge.WebauthnChallenge, "nil Webauthn challenge")
mfaResp, err := d.WebDev.SolveAuthn(challenge)
require.NoError(t, err, "SolveAuthn")
return mfaResp
}
func addOneOfEachMFADevice(t *testing.T, userClient *authclient.Client, clock clockwork.Clock, origin string) mfaDevices {
const totpName = "totp-dev"
const webName = "webauthn-dev"
ctx := context.Background()
totpDev, err := RegisterTestDevice(
ctx, userClient, totpName, proto.DeviceType_DEVICE_TYPE_TOTP, nil /* authenticator */, WithTestDeviceClock(clock))
require.NoError(t, err, "RegisterTestDevice(totp)")
webDev, err := RegisterTestDevice(
ctx, userClient, webName, proto.DeviceType_DEVICE_TYPE_WEBAUTHN, totpDev /* authenticator */)
require.NoError(t, err, "RegisterTestDevice(totp)")
return mfaDevices{
clock: clock,
webOrigin: origin,
TOTPName: totpName,
WebName: webName,
TOTPDev: totpDev,
WebDev: webDev,
}
}
type mfaAddTestOpts struct {
deviceName string
deviceType proto.DeviceType
deviceUsage proto.DeviceUsage
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, authClient *authclient.Client, opts mfaAddTestOpts) {
authChal, err := authClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
},
})
require.NoError(t, err, "CreateAuthenticateChallenge")
authnSolved := opts.authHandler(t, authChal)
registerChal, err := authClient.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
ExistingMFAResponse: authnSolved,
DeviceType: opts.deviceType,
DeviceUsage: opts.deviceUsage,
})
opts.checkAuthErr(t, err)
if err != nil {
return
}
registerSolved := opts.registerHandler(t, registerChal)
addResp, err := authClient.AddMFADeviceSync(ctx, &proto.AddMFADeviceSyncRequest{
NewDeviceName: opts.deviceName,
NewMFAResponse: registerSolved,
DeviceUsage: opts.deviceUsage,
})
opts.checkRegisterErr(t, err)
switch {
case err != nil:
return
case opts.assertRegisteredDev != nil:
opts.assertRegisteredDev(t, addResp.Device)
}
}
type mfaDeleteTestOpts struct {
deviceName string
authHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse
checkErr require.ErrorAssertionFunc
}
func testDeleteMFADevice(ctx context.Context, t *testing.T, authClient *authclient.Client, opts mfaDeleteTestOpts) {
// Issue and solve authn challenge.
authnChal, err := authClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
},
})
require.NoError(t, err, "CreateAuthenticateChallenge")
authnSolved := opts.authHandler(t, authnChal)
// Attempt deletion.
opts.checkErr(t,
authClient.DeleteMFADeviceSync(ctx, &proto.DeleteMFADeviceSyncRequest{
DeviceName: opts.deviceName,
ExistingMFAResponse: authnSolved,
}))
}
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, &proto.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")
key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err, "GenerateKeyWithAlgorithm failed")
publicKeyPEM, err := keys.MarshalPublicKey(key.Public())
require.NoError(t, err, "MarshalPublicKey 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{
TLSPublicKey: publicKeyPEM,
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.
TestFeatures: modules.Features{
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.App: {Enabled: true},
},
},
})
testServer := newTestTLSServer(t)
ctx := context.Background()
clock := testServer.Clock()
clusterName := testServer.ClusterName()
authServer := testServer.Auth()
// Create a user for testing.
user, role, err := CreateUserAndRole(testServer.Auth(), "llama", []string{"llama"}, nil)
require.NoError(t, err, "CreateUserAndRole failed")
username := user.GetName()
// Make sure MFA is required for this user.
roleOpt := role.GetOptions()
roleOpt.RequireMFAType = types.RequireMFAType_SESSION
role.SetOptions(roleOpt)
_, err = authServer.UpsertRole(ctx, role)
require.NoError(t, err)
// Register an SSH node.
node := &types.ServerV2{
Kind: types.KindNode,
Version: types.V2,
Metadata: types.Metadata{
Name: "mynode",
},
Spec: types.ServerSpecV2{
Hostname: "node-a",
},
}
_, err = authServer.UpsertNode(ctx, node)
require.NoError(t, err)
// 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)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err, "UpsertAuthPreference 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.SetSecondFactors(
types.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
)
authPref.SetWebauthn(&types.Webauthn{
RPID: "localhost",
})
})
mfaDevices := addOneOfEachMFADevice(t, clientWithoutDevice, clock, origin)
mfaPromptFunc := mfa.PromptFunc(func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) {
return mfaDevices.WebDev.SolveAuthn(chal)
})
promptConstructor := func(...mfa.PromptOpt) mfa.Prompt { return mfaPromptFunc }
clientWithoutDevice.SetMFAPromptConstructor(promptConstructor)
clientWithDevice.SetMFAPromptConstructor(promptConstructor)
// Create public keys for UserCertsRequest.
_, sshPub, _, tlsPub := newSSHAndTLSKeyPairs(t)
expires := clock.Now().Add(1 * time.Hour)
sshReq := proto.UserCertsRequest{
SSHPublicKey: sshPub,
Username: username,
Expires: expires,
RouteToCluster: clusterName,
NodeName: "mynode",
Usage: proto.UserCertsRequest_SSH,
SSHLogin: "llama",
}
appReq := proto.UserCertsRequest{
TLSPublicKey: tlsPub,
Username: username,
Expires: expires,
RouteToCluster: clusterName,
Usage: proto.UserCertsRequest_App,
RouteToApp: proto.RouteToApp{
Name: "hello",
PublicAddr: "hello.cluster.dev",
ClusterName: clusterName,
},
}
winReq := proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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 generate single-use, MFA-enabled certificates.
generateCertsMFA := func(t *testing.T, client *authclient.Client, req proto.UserCertsRequest) (cert *proto.Certs, err error) {
defer func() {
// Translate gRPC to trace errors, as our clients do.
err = trail.FromGRPC(err)
}()
authnChal, err := client.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION,
},
})
if err != nil {
return nil, err
}
req.MFAResponse = mfaDevices.webAuthHandler(t, authnChal)
req.Purpose = proto.UserCertsRequest_CERT_PURPOSE_SINGLE_USE_CERTS
return client.GenerateUserCerts(ctx, req)
}
tests := []struct {
name string
clusterDeviceMode string
client *authclient.Client
req proto.UserCertsRequest
skipLoginCerts bool // aka non-MFA issuance.
skipSingleUseCerts 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,
skipSingleUseCerts: true,
assertErr: assertSuccess,
},
{
name: "mode=required ignores App Access requests",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithoutDevice,
req: appReq,
skipLoginCerts: true,
assertErr: assertSuccess,
},
{
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,
})
})
if !test.skipLoginCerts {
t.Run("login certs", func(t *testing.T) {
_, err := test.client.GenerateUserCerts(ctx, test.req)
test.assertErr(t, err)
})
}
if !test.skipSingleUseCerts {
t.Run("single-use certs", func(t *testing.T) {
_, err := generateCertsMFA(t, test.client, test.req)
test.assertErr(t, err)
})
}
})
}
}
// Test that device trust is required for a user registering their first MFA device.
func TestRegisterFirstDevice_deviceAuthz(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestBuildType: modules.BuildEnterprise, // required for Device Trust.
})
testServer := newTestTLSServer(t)
ctx := context.Background()
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)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err, "UpsertAuthPreference failed")
}
// Enable webauthn
updateAuthPref(t, func(authPref types.AuthPreference) {
authPref.SetSecondFactors(
types.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
)
authPref.SetWebauthn(&types.Webauthn{
RPID: "localhost",
})
})
assertSuccess := func(t *testing.T, err error) {
assert.NoError(t, err)
}
assertAccessDenied := func(t *testing.T, err error) {
assert.True(t, trace.IsAccessDenied(err), "expected access denied error but got %v", err)
assert.ErrorContains(t, err, dtauthz.ErrTrustedDeviceRequired.Error())
}
tests := []struct {
name string
clusterDeviceMode string
client *authclient.Client
skipLoginCerts bool // aka non-MFA issuance.
skipSingleUseCerts bool // aka MFA/streaming issuance.
assertErr func(t *testing.T, err error)
}{
{
name: "mode=optional without extensions",
clusterDeviceMode: constants.DeviceTrustModeOptional,
client: clientWithoutDevice,
assertErr: assertSuccess,
},
{
name: "mode=optional with extensions",
clusterDeviceMode: constants.DeviceTrustModeOptional,
client: clientWithDevice,
assertErr: assertSuccess,
},
{
name: "nok: mode=required without extensions",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithoutDevice,
assertErr: assertAccessDenied,
},
{
name: "mode=required with extensions",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: clientWithDevice,
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,
})
})
t.Run("CreatePrivilegeTokenRequest", func(t *testing.T) {
_, err := test.client.CreatePrivilegeToken(ctx, &proto.CreatePrivilegeTokenRequest{})
test.assertErr(t, err)
})
t.Run("CreateRegisterChallenge", func(t *testing.T) {
_, err := test.client.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_MFA,
})
test.assertErr(t, err)
})
})
}
}
func mustCreateDatabase(t *testing.T, name, protocol, uri string) *types.DatabaseV3 {
database, err := types.NewDatabaseV3(
types.Metadata{
Name: name,
},
types.DatabaseSpecV3{
Protocol: protocol,
URI: uri,
},
)
require.NoError(t, err)
return database
}
func TestGenerateUserCerts_singleUseCerts(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestBuildType: modules.BuildEnterprise, // required for IP pinning.
TestFeatures: modules.GetModules().Features(),
})
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().UpsertAuthPreference(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{
Database: mustCreateDatabase(t, "db-a", "postgres", "localhost"),
Hostname: "localhost",
HostID: "localhost",
})
require.NoError(t, err)
_, err = srv.Auth().UpsertDatabaseServer(ctx, db)
require.NoError(t, err)
desktop, err := types.NewWindowsDesktopV3("desktop", nil, types.WindowsDesktopSpecV3{
Addr: "localhost",
HostID: "test",
})
require.NoError(t, err)
require.NoError(t, srv.Auth().CreateWindowsDesktop(ctx, desktop))
app, err := types.NewAppV3(
types.Metadata{
Name: "app-a",
},
types.AppSpecV3{
URI: "localhost",
},
)
require.NoError(t, err)
appServer, err := types.NewAppServerV3FromApp(app, "app-a", "app-a")
require.NoError(t, err)
_, err = srv.Auth().UpsertApplicationServer(ctx, appServer)
require.NoError(t, err)
leaf, err := types.NewRemoteCluster("leaf")
require.NoError(t, err)
// create remote cluster
_, err = srv.Auth().CreateRemoteCluster(ctx, leaf)
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.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetDatabaseNames(types.Allow, []string{types.Wildcard})
role.SetWindowsLogins(types.Allow, []string{"role"})
role.SetWindowsDesktopLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
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)
// Adding MFA devices advances fake clock by 1 minute, here we return it back.
fakeClock, ok := clock.(clockwork.FakeClock)
require.True(t, ok)
fakeClock.Advance(-60 * time.Second)
// 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
}
}
_, sshPub, _, tlsPub := newSSHAndTLSKeyPairs(t)
// Used for device trust tests.
wantDeviceExtensions := tlsca.DeviceExtensions{
DeviceID: "device-id1",
AssetTag: "device-assettag1",
CredentialID: "device-credentialid1",
}
tests := []struct {
desc string
newClient func() (*authclient.Client, error) // optional, makes a new client for the test.
opts generateUserSingleUseCertsTestOpts
}{
{
desc: "ssh using webauthn",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
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",
SSHLogin: "role",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
sshCertBytes := c.SSH
require.NotEmpty(t, sshCertBytes)
cert, err := sshutils.ParseCertificate(sshCertBytes)
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: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
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",
SSHLogin: "role",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.SSH
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: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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",
Database: "db-a",
},
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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, "db-a", identity.RouteToDatabase.ServiceName)
},
},
},
{
desc: "app",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_App,
RouteToApp: proto.RouteToApp{
Name: "app-a",
},
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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.UsageAppsOnly}, identity.Usage)
require.Equal(t, "app-a", identity.RouteToApp.Name)
// session ID should be set to a random ID, corresponding to an app session.
require.NotZero(t, identity.RouteToApp.SessionID)
},
},
},
{
desc: "db with ttl limit disabled",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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,
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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, "db-a", identity.RouteToDatabase.ServiceName)
},
},
},
{
desc: "kube with ttl limit disabled",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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.
// It *should* be adjusted to the user cert ttl though.
Expires: clock.Now().Add(1000 * time.Hour),
Usage: proto.UserCertsRequest_Kubernetes,
KubernetesCluster: "kube-a",
RequesterName: proto.UserCertsRequest_TSH_KUBE_LOCAL_PROXY,
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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.UsageKubeOnly}, identity.Usage)
require.Equal(t, "kube-a", identity.KubernetesCluster)
},
},
},
{
desc: "app with ttl limit disabled",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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.
// It *should* be adjusted to the user cert ttl though.
Expires: clock.Now().Add(1000 * time.Hour),
Usage: proto.UserCertsRequest_App,
RouteToApp: proto.RouteToApp{
Name: "app-a",
},
RequesterName: proto.UserCertsRequest_TSH_APP_LOCAL_PROXY,
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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.UsageAppsOnly}, identity.Usage)
require.Equal(t, "app-a", identity.RouteToApp.Name)
// session ID should be set to a random ID, corresponding to an app session.
require.NotZero(t, identity.RouteToApp.SessionID)
},
},
},
{
desc: "desktops",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_WindowsDesktop,
RouteToWindowsDesktop: proto.RouteToWindowsDesktop{
WindowsDesktop: "desktop",
Login: "role",
},
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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.UsageWindowsDesktopOnly}, identity.Usage)
},
},
},
{
desc: "fail - wrong usage",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
TLSPublicKey: tlsPub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_All,
NodeName: "node-a",
},
verifyErr: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorContains(t, err, "all purposes")
},
},
},
{
desc: "fail - mfa challenge fail",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
TLSPublicKey: tlsPub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
SSHLogin: "role",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
// Return no challenge response.
return &proto.MFAAuthenticateResponse{}
},
verifyErr: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorContains(t, err, "unknown or missing MFAAuthenticateResponse")
},
},
},
{
desc: "device extensions copied SSH cert",
newClient: func() (*authclient.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: generateUserSingleUseCertsTestOpts{
// Same as SSH options. Nothing special here.
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
SSHLogin: "role",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
// SSH certificate.
sshRaw := c.SSH
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() (*authclient.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: generateUserSingleUseCertsTestOpts{
// Same as Database options. Nothing special here.
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_Database,
RouteToDatabase: proto.RouteToDatabase{
ServiceName: "db-a",
},
},
authnHandler: registered.webAuthHandler,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
// TLS certificate.
tlsRaw := c.TLS
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)
}
},
},
},
{
desc: "mfa unspecified when no SSHLogin provided",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
TLSPublicKey: tlsPub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authnHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
// Return no challenge response.
return &proto.MFAAuthenticateResponse{}
},
verifyErr: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorContains(t, err, "unknown or missing MFAAuthenticateResponse")
},
},
},
{
desc: "k8s in leaf cluster",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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-b",
RouteToCluster: "leaf",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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-b", identity.KubernetesCluster)
},
},
},
{
desc: "db in leaf cluster",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
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-b",
Database: "db-b",
},
RouteToCluster: "leaf",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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, "db-b", identity.RouteToDatabase.ServiceName)
},
},
},
{
desc: "app in leaf cluster",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
TLSPublicKey: tlsPub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_App,
RouteToApp: proto.RouteToApp{
Name: "app-b",
},
RouteToCluster: "leaf",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
crt := c.TLS
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.UsageAppsOnly}, identity.Usage)
require.Equal(t, "app-b", identity.RouteToApp.Name)
// session ID should be set to a random ID, corresponding to an app session.
require.NotZero(t, identity.RouteToApp.SessionID)
},
},
},
{
desc: "ssh in leaf node",
opts: generateUserSingleUseCertsTestOpts{
initReq: &proto.UserCertsRequest{
SSHPublicKey: sshPub,
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-b",
SSHLogin: "role",
RouteToCluster: "leaf",
},
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authnHandler: registered.webAuthHandler,
verifyErr: require.NoError,
verifyCert: func(t *testing.T, c *proto.Certs) {
sshCertBytes := c.SSH
require.NotEmpty(t, sshCertBytes)
cert, err := sshutils.ParseCertificate(sshCertBytes)
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)
},
},
},
}
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")
}
testGenerateUserSingleUseCerts(ctx, t, testClient, tt.opts)
})
}
}
type generateUserSingleUseCertsTestOpts struct {
initReq *proto.UserCertsRequest
authnHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse
mfaRequiredHandler func(*testing.T, proto.MFARequired)
verifyErr require.ErrorAssertionFunc
verifyCert func(*testing.T, *proto.Certs)
}
func testGenerateUserSingleUseCerts(ctx context.Context, t *testing.T, cl *authclient.Client, opts generateUserSingleUseCertsTestOpts) {
authnChal, err := cl.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION,
},
})
require.NoError(t, err, "CreateAuthenticateChallenge")
req := opts.initReq
req.Purpose = proto.UserCertsRequest_CERT_PURPOSE_SINGLE_USE_CERTS
if opts.authnHandler != nil {
req.MFAResponse = opts.authnHandler(t, authnChal)
}
certs, err := cl.GenerateUserCerts(ctx, *req)
opts.verifyErr(t, err)
if err != nil {
return
}
opts.verifyCert(t, certs)
}
var requireMFATypes = []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
types.RequireMFAType_HARDWARE_KEY_TOUCH,
types.RequireMFAType_HARDWARE_KEY_PIN,
types.RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN,
}
func TestIsMFARequired(t *testing.T) {
testModules := &modules.TestModules{
TestBuildType: modules.BuildEnterprise,
MockAttestationData: &keys.AttestationData{},
}
modules.SetTestModules(t, testModules)
ctx := context.Background()
srv := newTestTLSServer(t)
// Register an SSH node.
node := &types.ServerV2{
Kind: types.KindNode,
Version: types.V2,
Metadata: types.Metadata{
Name: uuid.NewString(),
},
Spec: types.ServerSpecV2{
Hostname: "node-a",
},
}
_, err := srv.Auth().UpsertNode(ctx, node)
require.NoError(t, err)
for _, authPrefRequireMFAType := range requireMFATypes {
t.Run(fmt.Sprintf("authPref=%v", authPrefRequireMFAType.String()), func(t *testing.T) {
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
RequireMFAType: authPrefRequireMFAType,
Webauthn: &types.Webauthn{
RPID: "teleport",
},
})
require.NoError(t, err)
authPref, err = srv.Auth().UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
for _, roleRequireMFAType := range requireMFATypes {
roleRequireMFAType := roleRequireMFAType
t.Run(fmt.Sprintf("role=%v", roleRequireMFAType.String()), func(t *testing.T) {
user, err := types.NewUser(roleRequireMFAType.String())
require.NoError(t, err)
role := services.RoleForUser(user)
roleOpt := role.GetOptions()
roleOpt.RequireMFAType = roleRequireMFAType
role.SetOptions(roleOpt)
role.SetLogins(types.Allow, []string{user.GetName()})
role, err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
user.AddRole(role.GetName())
user, err = srv.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
mfaVerifiedByHardwareKey := role.GetPrivateKeyPolicy().MFAVerified() || authPref.GetPrivateKeyPolicy().MFAVerified()
if mfaVerifiedByHardwareKey {
// Set attestated key policy to the most restrictive hardware key MFA is required.
testModules.MockAttestationData.PrivateKeyPolicy = keys.PrivateKeyPolicyHardwareKeyTouchAndPIN
} else {
testModules.MockAttestationData.PrivateKeyPolicy = keys.PrivateKeyPolicyHardwareKey
}
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)
// If auth pref or role require session MFA, and MFA is not already
// verified according to private key policy, expect MFA required.
wantRequired := (role.GetOptions().RequireMFAType.IsSessionMFARequired() || authPref.GetRequireMFAType().IsSessionMFARequired()) && !mfaVerifiedByHardwareKey
var wantMFARequired proto.MFARequired
if wantRequired {
wantMFARequired = proto.MFARequired_MFA_REQUIRED_YES
} else {
wantMFARequired = proto.MFARequired_MFA_REQUIRED_NO
}
assert.Equal(t, wantRequired, resp.Required, "Required mismatch")
assert.Equal(t, wantMFARequired, resp.MFARequired, "IsMFARequired mismatch")
})
}
})
}
}
func TestIsMFARequired_unauthorized(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().UpsertAuthPreference(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]utils.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, "IsMFARequired")
assert.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, resp.MFARequired, "MFARequired mismatch")
assert.True(t, resp.Required, "Required mismatch")
// 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",
}},
})
require.NoError(t, err, "IsMFARequired silent failure wanted")
assert.Equal(t, proto.MFARequired_MFA_REQUIRED_NO, resp.MFARequired, "MFARequired mismatch")
assert.False(t, resp.Required, "Required mismatch")
}
func TestIsMFARequired_nodeMatch(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildEnterprise})
ctx := context.Background()
srv := newTestTLSServer(t)
// Register an SSH node.
node, err := types.NewServerWithLabels(uuid.NewString(), types.KindNode, types.ServerSpecV2{
Hostname: "node-a",
Addr: "127.0.0.1:3022",
PublicAddrs: []string{"node.example.com:3022", "localhost:3022"},
}, map[string]string{"foo": "bar"})
require.NoError(t, err)
_, err = srv.Auth().UpsertNode(ctx, node)
require.NoError(t, err)
// Create a fake user with per session mfa required for all nodes.
role, err := CreateRole(ctx, srv.Auth(), "mfa-user", types.RoleSpecV6{
Options: types.RoleOptions{
RequireMFAType: types.RequireMFAType_SESSION,
},
Allow: types.RoleConditions{
Logins: []string{"mfa-user"},
NodeLabels: types.Labels{types.Wildcard: utils.Strings{types.Wildcard}},
},
})
require.NoError(t, err)
user, err := CreateUser(ctx, srv.Auth(), "mfa-user", role)
require.NoError(t, err)
cl, err := srv.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
for _, tc := range []struct {
desc string
// IsMFARequired only expects a host name or ip without the port.
node string
want proto.MFARequired
}{
{
desc: "OK uuid match",
node: node.GetName(),
want: proto.MFARequired_MFA_REQUIRED_YES,
},
{
desc: "OK host name match",
node: node.GetHostname(),
want: proto.MFARequired_MFA_REQUIRED_YES,
},
{
desc: "OK addr match",
node: node.GetAddr(),
want: proto.MFARequired_MFA_REQUIRED_YES,
},
{
desc: "OK public addr 1 match",
node: "node.example.com",
want: proto.MFARequired_MFA_REQUIRED_YES,
},
{
desc: "OK public addr 2 match",
node: "localhost",
want: proto.MFARequired_MFA_REQUIRED_YES,
},
{
desc: "NOK label match",
node: "foo",
want: proto.MFARequired_MFA_REQUIRED_NO,
},
{
desc: "NOK unknown ip",
node: "1.2.3.4",
want: proto.MFARequired_MFA_REQUIRED_NO,
},
{
desc: "NOK unknown addr",
node: "unknown.example.com",
want: proto.MFARequired_MFA_REQUIRED_NO,
},
} {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
resp, err := cl.IsMFARequired(ctx, &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{
Login: user.GetName(),
Node: tc.node,
}},
})
require.NoError(t, err, "IsMFARequired")
assert.Equal(t, tc.want, resp.MFARequired, "MFARequired mismatch")
assert.Equal(t, MFARequiredToBool(tc.want), resp.Required, "Required mismatch")
})
}
}
func TestIsMFARequired_App(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
appA, err := types.NewAppV3(types.Metadata{
Name: "app-a",
Labels: map[string]string{
"env": "dev",
},
}, types.AppSpecV3{
URI: "app-a",
PublicAddr: "appa.example.com",
})
require.NoError(t, err)
appAServer, err := types.NewAppServerV3FromApp(appA, "host", "host-id")
require.NoError(t, err)
_, err = srv.Auth().UpsertApplicationServer(ctx, appAServer)
require.NoError(t, err)
accessDevApp, err := types.NewRole("access-a", types.RoleSpecV6{
Allow: types.RoleConditions{
AppLabels: types.Labels{
"env": []string{"dev"},
},
},
})
require.NoError(t, err)
_, err = srv.Auth().UpsertRole(ctx, accessDevApp)
require.NoError(t, err)
accessDevAppMFA, err := types.NewRole("access-a-mfa", types.RoleSpecV6{
Options: types.RoleOptions{
RequireMFAType: types.RequireMFAType_SESSION,
},
Allow: types.RoleConditions{
AppLabels: types.Labels{
"env": []string{"dev"},
},
},
})
require.NoError(t, err)
_, err = srv.Auth().UpsertRole(ctx, accessDevAppMFA)
require.NoError(t, err)
accessStagingAppMFA, err := types.NewRole("access-b-mfa", types.RoleSpecV6{
Options: types.RoleOptions{
RequireMFAType: types.RequireMFAType_SESSION,
},
Allow: types.RoleConditions{
AppLabels: types.Labels{
"env": []string{"staging"},
},
},
})
require.NoError(t, err)
_, err = srv.Auth().UpsertRole(ctx, accessStagingAppMFA)
require.NoError(t, err)
for _, tc := range []struct {
name string
authPrefMFAType types.RequireMFAType
roles []string
expectMFARequired proto.MFARequired
}{
{
name: "mfa not required",
authPrefMFAType: types.RequireMFAType_OFF,
roles: []string{accessDevApp.GetName()},
expectMFARequired: proto.MFARequired_MFA_REQUIRED_NO,
}, {
name: "mfa required by role",
authPrefMFAType: types.RequireMFAType_OFF,
roles: []string{accessDevAppMFA.GetName()},
expectMFARequired: proto.MFARequired_MFA_REQUIRED_YES,
}, {
name: "mfa required by cap",
authPrefMFAType: types.RequireMFAType_SESSION,
roles: []string{accessDevApp.GetName()},
expectMFARequired: proto.MFARequired_MFA_REQUIRED_YES,
}, {
name: "mfa not required from unmatched role role",
authPrefMFAType: types.RequireMFAType_OFF,
roles: []string{accessDevApp.GetName(), accessStagingAppMFA.GetName()},
expectMFARequired: proto.MFARequired_MFA_REQUIRED_NO,
},
} {
// Update auth pref mfa type
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
RequireMFAType: tc.authPrefMFAType,
Webauthn: &types.Webauthn{
RPID: "teleport",
},
})
require.NoError(t, err)
_, err = srv.Auth().UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
// Create a user with test case roles.
user, err := types.NewUser("alice")
require.NoError(t, err)
user.SetRoles(tc.roles)
user, err = srv.Auth().UpsertUser(ctx, user)
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_App{App: &proto.RouteToApp{
Name: "app-a",
}},
})
require.NoError(t, err)
assert.Equal(t, tc.expectMFARequired, resp.MFARequired, "IsMFARequired mismatch")
assert.Equal(t, tc.expectMFARequired == proto.MFARequired_MFA_REQUIRED_YES, resp.Required, "Required mismatch")
}
}
// 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(*authclient.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, types.OriginDynamic, stored.Origin())
})
}
}
func TestAuthPreferenceOriginDynamic(t *testing.T) {
t.Parallel()
ctx := context.Background()
setWithOrigin := func(cl *authclient.Client, origin string) error {
authPref := types.DefaultAuthPreference()
authPref.SetOrigin(origin)
_, err := cl.UpsertAuthPreference(ctx, authPref)
return err
}
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 *authclient.Client, origin string) error {
netConfig := types.DefaultClusterNetworkingConfig()
netConfig.SetOrigin(origin)
_, err := cl.UpsertClusterNetworkingConfig(ctx, netConfig)
return trace.Wrap(err)
}
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 *authclient.Client, origin string) error {
recConfig := types.DefaultSessionRecordingConfig()
recConfig.SetOrigin(origin)
_, err := cl.UpsertSessionRecordingConfig(ctx, recConfig)
return err
}
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)
}
func TestGenerateDatabaseCerts(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
clt, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
// Generate CSR once for speed sake.
priv, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.RSA2048)
require.NoError(t, err)
csr, err := tlsca.GenerateCertificateRequestPEM(pkix.Name{CommonName: "test"}, priv)
require.NoError(t, err)
certs, err := clt.GenerateDatabaseCert(ctx, &proto.DatabaseCertRequest{CSR: csr})
require.NoError(t, err)
require.NotNil(t, certs)
certs, err = clt.GenerateDatabaseCert(ctx, &proto.DatabaseCertRequest{CSR: csr, RequesterName: proto.DatabaseCertRequest_TCTL})
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.AssertSystemRole(ctx, proto.SystemRoleAssertion{
ServerID: serverID,
AssertionID: assertionID,
SystemRole: role,
})
require.NoError(t, err)
}()
}
// set assertion ID
req.SystemRoleAssertionID = 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.SystemRoleAssertionID = ""
_, 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 TestGetSSHTargets(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
clt, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
upper, err := types.NewServerWithLabels(uuid.New().String(), types.KindNode, types.ServerSpecV2{
Hostname: "Foo",
UseTunnel: true,
}, nil)
require.NoError(t, err)
lower, err := types.NewServerWithLabels(uuid.New().String(), types.KindNode, types.ServerSpecV2{
Hostname: "foo",
UseTunnel: true,
}, nil)
require.NoError(t, err)
other, err := types.NewServerWithLabels(uuid.New().String(), types.KindNode, types.ServerSpecV2{
Hostname: "bar",
UseTunnel: true,
}, nil)
require.NoError(t, err)
for _, node := range []types.Server{upper, lower, other} {
_, err = clt.UpsertNode(ctx, node)
require.NoError(t, err)
}
rsp, err := clt.GetSSHTargets(ctx, &proto.GetSSHTargetsRequest{
Host: "foo",
Port: "0",
})
require.NoError(t, err)
require.Len(t, rsp.Servers, 1)
require.Equal(t, "foo", rsp.Servers[0].GetHostname())
cnc := types.DefaultClusterNetworkingConfig()
cnc.SetCaseInsensitiveRouting(true)
_, err = clt.UpsertClusterNetworkingConfig(ctx, cnc)
require.NoError(t, err)
rsp, err = clt.GetSSHTargets(ctx, &proto.GetSSHTargetsRequest{
Host: "foo",
Port: "0",
})
require.NoError(t, err)
require.Len(t, rsp.Servers, 2)
require.ElementsMatch(t, []string{rsp.Servers[0].GetHostname(), rsp.Servers[1].GetHostname()}, []string{"foo", "Foo"})
}
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{Hostname: "node1"}, nil)
require.NoError(t, err)
node2, err := types.NewServerWithLabels("node2", types.KindNode, types.ServerSpecV2{Hostname: "node2"}, 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{}, "Revision")))
// 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{}, "Revision")))
// GetNode should fail if node name isn't provided
_, err = clt.GetNode(ctx, apidefaults.Namespace, "")
require.True(t, trace.IsBadParameter(err), "trace.IsBadParameter failed: err=%v (%T)", err, trace.Unwrap(err))
// GetNode should fail if namespace isn't provided
_, err = clt.GetNode(ctx, "", "node1")
require.True(t, trace.IsBadParameter(err), "trace.IsBadParameter failed: err=%v (%T)", err, trace.Unwrap(err))
})
})
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{}, "Revision")))
})
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{}, "Revision")))
// 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{}, "Revision")))
// 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{}, "Revision")))
// 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.Empty(t, 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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.Empty(t, 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.OriginOkta}, // This should be overwritten
}, types.AppSpecV3{
URI: "localhost2",
})
require.NoError(t, err)
// Initially we expect no apps.
out, err := clt.GetApps(ctx)
require.NoError(t, err)
require.Empty(t, 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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.Empty(t, out)
}
// TestAppServersCRUD tests application server resource operations.
func TestAppServersCRUD(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
// Create an app server, expected origin dynamic.
clt, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
app1, err := types.NewAppV3(types.Metadata{
Name: "app-dynamic",
}, types.AppSpecV3{
URI: "localhost1",
})
require.NoError(t, err)
appServer1, err := types.NewAppServerV3FromApp(app1, "app-dynamic", "hostID")
require.NoError(t, err)
_, err = clt.UpsertApplicationServer(ctx, appServer1)
require.NoError(t, err)
resources, err := clt.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindAppServer,
Limit: apidefaults.DefaultChunkSize,
})
require.NoError(t, err)
require.Len(t, resources.Resources, 1)
appServer := resources.Resources[0].(types.AppServer)
require.Empty(t, cmp.Diff(appServer, appServer1,
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
))
require.NoError(t, clt.DeleteApplicationServer(ctx, apidefaults.Namespace, "hostID", appServer1.GetName()))
resources, err = clt.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindAppServer,
Limit: apidefaults.DefaultChunkSize,
})
require.NoError(t, err)
require.Empty(t, resources.Resources)
// Try to create app servers with Okta labels as a non-Okta role.
app2, err := types.NewAppV3(types.Metadata{
Name: "app-okta",
Labels: map[string]string{types.OriginLabel: types.OriginOkta},
}, types.AppSpecV3{
URI: "localhost1",
})
require.NoError(t, err)
appServer2, err := types.NewAppServerV3FromApp(app2, "app-okta", "hostID")
require.NoError(t, err)
_, err = clt.UpsertApplicationServer(ctx, appServer2)
require.ErrorIs(t, err, trace.BadParameter("only the Okta role can create app servers and apps with an Okta origin"))
delete(app2.Metadata.Labels, types.OriginLabel)
appServer2.SetOrigin(types.OriginOkta)
_, err = clt.UpsertApplicationServer(ctx, appServer2)
require.ErrorIs(t, err, trace.BadParameter("only the Okta role can create app servers and apps with an Okta origin"))
// Create an app server with Okta labels using the Okta role.
clt, err = srv.NewClient(TestBuiltin(types.RoleOkta))
require.NoError(t, err)
app2.SetOrigin(types.OriginOkta)
appServer2.SetOrigin(types.OriginOkta)
_, err = clt.UpsertApplicationServer(ctx, appServer2)
require.NoError(t, err)
resources, err = clt.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindAppServer,
Limit: apidefaults.DefaultChunkSize,
})
require.NoError(t, err)
require.Len(t, resources.Resources, 1)
appServer2.SetOrigin(types.OriginOkta)
app2.SetOrigin(types.OriginOkta)
appServer = resources.Resources[0].(types.AppServer)
require.Empty(t, cmp.Diff(appServer, appServer2,
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
))
require.NoError(t, clt.DeleteApplicationServer(ctx, apidefaults.Namespace, "hostID", appServer2.GetName()))
resources, err = clt.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindAppServer,
Limit: apidefaults.DefaultChunkSize,
})
require.NoError(t, err)
require.Empty(t, resources.Resources)
}
// 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.Empty(t, 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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.Empty(t, out)
}
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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)
}
func TestServerInfoCRUD(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv := newTestTLSServer(t)
clt, err := srv.NewClient(TestAdmin())
require.NoError(t, err)
serverInfo1, err := types.NewServerInfo(types.Metadata{
Name: "serverInfo1",
}, types.ServerInfoSpecV1{})
require.NoError(t, err)
serverInfo1.SetSubKind(types.SubKindCloudInfo)
serverInfo2, err := types.NewServerInfo(types.Metadata{
Name: "serverInfo2",
}, types.ServerInfoSpecV1{})
require.NoError(t, err)
serverInfo2.SetSubKind(types.SubKindCloudInfo)
createServerInfos := func(t *testing.T) {
// Initially expect no server info to be returned.
serverInfos, err := stream.Collect(clt.GetServerInfos(ctx))
require.NoError(t, err)
require.Empty(t, serverInfos)
// Create server info.
require.NoError(t, clt.UpsertServerInfo(ctx, serverInfo1))
require.NoError(t, clt.UpsertServerInfo(ctx, serverInfo2))
}
deleteAllServerInfos := func(t *testing.T) {
// Delete server infos.
require.NoError(t, clt.DeleteAllServerInfos(ctx))
// Expect no server infos to be returned.
serverInfos, err := stream.Collect(clt.GetServerInfos(ctx))
require.NoError(t, err)
require.Empty(t, serverInfos)
}
requireResourcesEqual := func(t *testing.T, expected, actual interface{}) {
require.Empty(t, cmp.Diff(expected, actual, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
}
t.Run("ServerInfoGetters", func(t *testing.T) {
createServerInfos(t)
t.Cleanup(func() { deleteAllServerInfos(t) })
t.Run("GetServerInfos", func(t *testing.T) {
t.Parallel()
// Get all server infos.
serverInfos, err := stream.Collect(clt.GetServerInfos(ctx))
require.NoError(t, err)
require.Len(t, serverInfos, 2)
requireResourcesEqual(t, []types.ServerInfo{serverInfo1, serverInfo2}, serverInfos)
})
t.Run("GetServerInfo", func(t *testing.T) {
t.Parallel()
// Get server info.
si, err := clt.GetServerInfo(ctx, serverInfo1.GetName())
require.NoError(t, err)
requireResourcesEqual(t, serverInfo1, si)
// GetServerInfo should fail if name isn't provided.
_, err = clt.GetServerInfo(ctx, "")
require.Error(t, err)
require.True(t, trace.IsBadParameter(err))
})
})
t.Run("DeleteServerInfo", func(t *testing.T) {
createServerInfos(t)
t.Cleanup(func() { deleteAllServerInfos(t) })
// DeleteServerInfo should fail if name isn't provided.
err := clt.DeleteServerInfo(ctx, "")
require.Error(t, err)
require.True(t, trace.IsBadParameter(err))
// Delete server info.
err = clt.DeleteServerInfo(ctx, serverInfo1.GetName())
require.NoError(t, err)
// Expect server info not found.
_, err = clt.GetServerInfo(ctx, serverInfo1.GetName())
require.Error(t, err)
require.True(t, trace.IsNotFound(err))
// Expect other server info still exists.
si, err := clt.GetServerInfo(ctx, serverInfo2.GetName())
require.NoError(t, err)
requireResourcesEqual(t, serverInfo2, si)
})
}
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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{}, "Revision"),
))
// 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 *authclient.Client) error
}{
"DatabaseServers": {
resourceType: types.KindDatabaseServer,
createResource: func(name string, clt *authclient.Client) error {
server, err := types.NewDatabaseServerV3(types.Metadata{
Name: name,
}, types.DatabaseServerSpecV3{
Database: mustCreateDatabase(t, name, defaults.ProtocolPostgres, "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 *authclient.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 *authclient.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 *authclient.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 *authclient.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.Empty(t, resp.Resources)
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(*authclient.Client) error
}{
{
name: "RPC ChangeUserAuthentication",
fn: func(clt *authclient.Client) error {
_, err := clt.ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{})
return err
},
},
{
name: "RPC CreateAuthenticateChallenge",
burst: defaults.LimiterBurst,
fn: func(clt *authclient.Client) error {
_, err := clt.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{})
return err
},
},
{
name: "RPC GetAccountRecoveryToken",
fn: func(clt *authclient.Client) error {
_, err := clt.GetAccountRecoveryToken(ctx, &proto.GetAccountRecoveryTokenRequest{})
return err
},
},
{
name: "RPC StartAccountRecovery",
fn: func(clt *authclient.Client) error {
_, err := clt.StartAccountRecovery(ctx, &proto.StartAccountRecoveryRequest{})
return err
},
},
{
name: "RPC VerifyAccountRecovery",
fn: func(clt *authclient.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)
})
}
}
// 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{
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.SAML: {Enabled: 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(ctx, 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{
types.DefaultInstallerScriptName: installer.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",
types.DefaultInstallerScriptName: installer.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)
})
}
}
func TestUpsertApplicationServerOrigin(t *testing.T) {
t.Parallel()
parentCtx := context.Background()
server := newTestTLSServer(t)
admin := TestAdmin()
client, err := server.NewClient(admin)
require.NoError(t, err)
// Dynamic origin should work for admin role.
app, err := types.NewAppV3(types.Metadata{
Name: "app1",
Labels: map[string]string{types.OriginLabel: types.OriginDynamic},
}, types.AppSpecV3{
URI: "localhost1",
})
require.NoError(t, err)
appServer, err := types.NewAppServerV3FromApp(app, "localhost", "123456")
require.NoError(t, err)
ctx := authz.ContextWithUser(parentCtx, admin.I)
_, err = client.UpsertApplicationServer(ctx, appServer)
require.NoError(t, err)
// Okta origin should not work for admin role.
app.SetOrigin(types.OriginOkta)
appServer, err = types.NewAppServerV3FromApp(app, "localhost", "123456")
require.NoError(t, err)
ctx = authz.ContextWithUser(parentCtx, admin.I)
_, err = client.UpsertApplicationServer(ctx, appServer)
require.True(t, trace.IsBadParameter(err), "trace.IsBadParameter failed: err=%v (%T)", err, trace.Unwrap(err))
require.ErrorContains(t, err, "only the Okta role can create app servers and apps with an Okta origin")
// Okta origin should not work with instance and node roles.
client, err = server.NewClient(TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleInstance,
AdditionalSystemRoles: []types.SystemRole{
types.RoleNode,
},
Username: server.ClusterName(),
},
})
require.NoError(t, err)
ctx = authz.ContextWithUser(parentCtx, admin.I)
_, err = client.UpsertApplicationServer(ctx, appServer)
require.True(t, trace.IsBadParameter(err), "trace.IsBadParameter failed: err=%v (%T)", err, trace.Unwrap(err))
require.ErrorContains(t, err, "only the Okta role can create app servers and apps with an Okta origin")
// Okta origin should work with Okta role in role field.
node := TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleOkta,
AdditionalSystemRoles: []types.SystemRole{
types.RoleNode,
},
Username: server.ClusterName(),
},
}
client, err = server.NewClient(node)
require.NoError(t, err)
ctx = authz.ContextWithUser(parentCtx, node.I)
_, err = client.UpsertApplicationServer(ctx, appServer)
require.NoError(t, err)
// Okta origin should work with Okta role in additional system roles.
node = TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleInstance,
AdditionalSystemRoles: []types.SystemRole{
types.RoleNode,
types.RoleOkta,
},
Username: server.ClusterName(),
},
}
client, err = server.NewClient(node)
require.NoError(t, err)
ctx = authz.ContextWithUser(parentCtx, node.I)
_, err = client.UpsertApplicationServer(ctx, appServer)
require.NoError(t, err)
}
func TestGetAccessGraphConfig(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestFeatures: modules.Features{
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.Policy: {Enabled: true},
},
},
})
server := newTestTLSServer(t,
withAccessGraphConfig(AccessGraphConfig{
Enabled: true,
CA: []byte("ca"),
Address: "addr",
}),
)
user, _, err := CreateUserAndRole(server.Auth(), "test", []string{"role"}, nil)
require.NoError(t, err)
positiveResponse := &clusterconfigpb.AccessGraphConfig{
Enabled: true,
Ca: []byte("ca"),
Address: "addr",
SecretsScanConfig: &clusterconfigpb.AccessGraphSecretsScanConfiguration{},
}
tests := []struct {
desc string
identity authz.IdentityGetter
assertErr require.ErrorAssertionFunc
expected *clusterconfigpb.AccessGraphConfig
}{
{
desc: "users can't pull the access graph config",
identity: authz.LocalUser{
Username: user.GetName(),
},
assertErr: require.Error,
},
{
desc: "proxy can pull access graph config",
identity: authz.BuiltinRole{
Role: types.RoleProxy,
Username: server.ClusterName(),
},
assertErr: require.NoError,
expected: positiveResponse,
},
{
desc: "discovery can pull access graph config",
identity: authz.BuiltinRole{
Role: types.RoleDiscovery,
Username: server.ClusterName(),
},
assertErr: require.NoError,
expected: positiveResponse,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
client, err := server.NewClient(TestIdentity{
I: test.identity,
})
require.NoError(t, err)
rsp, err := client.GetClusterAccessGraphConfig(context.Background())
test.assertErr(t, err)
require.Empty(t, cmp.Diff(test.expected, rsp, protocmp.Transform()))
})
}
}
func TestCreateAuditStreamLimit(t *testing.T) {
const N = 5
t.Setenv("TELEPORT_UNSTABLE_CREATEAUDITSTREAM_INFLIGHT_LIMIT", fmt.Sprintf("%d", N))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := newTestTLSServer(t)
clt, err := server.NewClient(TestServerID(types.RoleNode, uuid.NewString()))
require.NoError(t, err)
// HACK(espadolini): we're piggybacking on the prometheus counter which
// can't change while this test is running (we set an envvar, so we can't be
// running in parallel with other tests) but it's still pretty awful, and
// it'd be much better to actually check that the streams were accepted by
// the server; unfortunately, the CreateAuditStream stream doesn't actually
// send anything back unless there's a real upload going on, and the test
// server uses a discard emitter which never ends up sending anything
getAcceptedTotal := func() int {
var m prom_client_model.Metric
require.NoError(t, createAuditStreamAcceptedTotalMetric.Write(&m))
return int(m.Counter.GetValue())
}
currentAcceptedTotal := getAcceptedTotal()
for i := 0; i < N; i++ {
stream, err := clt.CreateAuditStream(ctx, session.NewID())
require.NoError(t, err)
t.Cleanup(func() { stream.Close(ctx) })
}
require.EventuallyWithT(t, func(t *assert.CollectT) {
assert.EqualValues(t, currentAcceptedTotal+N, getAcceptedTotal())
}, time.Second, 100*time.Millisecond)
ac := proto.NewAuthServiceClient(clt.APIClient.GetConnection())
stream, err := ac.CreateAuditStream(ctx)
require.NoError(t, err)
_, err = stream.Recv()
require.ErrorAs(t, err, new(*trace.ConnectionProblemError))
}