Skip to content

Commit

Permalink
Add tsh mfa device tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
Joerger committed Oct 15, 2024
1 parent 2e45321 commit e1e5663
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 15 deletions.
7 changes: 7 additions & 0 deletions api/types/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ type OIDCConnector interface {
GetClientRedirectSettings() *SSOClientRedirectSettings
// GetMFASettings returns the connector's MFA settings.
GetMFASettings() OIDCConnectorMFASettings
// SetMFASettings sets the connector's MFA settings.
SetMFASettings(s *OIDCConnectorMFASettings)
// IsMFAEnabled returns whether the connector has MFA enabled.
IsMFAEnabled() bool
// WithMFASettings returns the connector will some settings overwritten set from MFA settings.
Expand Down Expand Up @@ -515,6 +517,11 @@ func (o *OIDCConnectorV3) GetMFASettings() OIDCConnectorMFASettings {
return *o.Spec.MFASettings
}

// SetMFASettings sets the connector's MFA settings.
func (o *OIDCConnectorV3) SetMFASettings(s *OIDCConnectorMFASettings) {
o.Spec.MFASettings = s
}

// IsMFAEnabled returns whether the connector has MFA enabled.
func (o *OIDCConnectorV3) IsMFAEnabled() bool {
return o.GetMFASettings().Enabled
Expand Down
31 changes: 16 additions & 15 deletions tool/tsh/common/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,14 @@ func (c *mfaLSCommand) run(cf *CLIConf) error {
format := strings.ToLower(c.format)
switch format {
case teleport.Text, "":
printMFADevices(devs, c.verbose)
asciiTable := mfaDevicesASCIITable(devs, c.verbose)
fmt.Fprintln(cf.Stdout(), asciiTable)
case teleport.JSON, teleport.YAML:
out, err := serializeMFADevices(devs, format)
if err != nil {
return trace.Wrap(err)
}
fmt.Println(out)
fmt.Fprintln(cf.Stdout(), out)
default:
return trace.BadParameter("unsupported format %q", c.format)
}
Expand All @@ -165,7 +166,7 @@ func serializeMFADevices(devs []*types.MFADevice, format string) (string, error)
return string(out), trace.Wrap(err)
}

func printMFADevices(devs []*types.MFADevice, verbose bool) {
func mfaDevicesASCIITable(devs []*types.MFADevice, verbose bool) string {
if verbose {
t := asciitable.MakeTable([]string{"Name", "ID", "Type", "Added at", "Last used"})
for _, dev := range devs {
Expand All @@ -177,19 +178,19 @@ func printMFADevices(devs []*types.MFADevice, verbose bool) {
dev.LastUsed.Format(time.RFC1123),
})
}
fmt.Println(t.AsBuffer().String())
} else {
t := asciitable.MakeTable([]string{"Name", "Type", "Added at", "Last used"})
for _, dev := range devs {
t.AddRow([]string{
dev.GetName(),
dev.MFAType(),
dev.AddedAt.Format(time.RFC1123),
dev.LastUsed.Format(time.RFC1123),
})
}
fmt.Println(t.AsBuffer().String())
return t.AsBuffer().String()
}

t := asciitable.MakeTable([]string{"Name", "Type", "Added at", "Last used"})
for _, dev := range devs {
t.AddRow([]string{
dev.GetName(),
dev.MFAType(),
dev.AddedAt.Format(time.RFC1123),
dev.LastUsed.Format(time.RFC1123),
})
}
return t.AsBuffer().String()
}

type mfaAddCommand struct {
Expand Down
166 changes: 166 additions & 0 deletions tool/tsh/common/mfa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Teleport
* Copyright (C) 2024 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 common

import (
"bytes"
"context"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/tool/teleport/testenv"
)

func TestMFACommands_SSODevice(t *testing.T) {
ctx := context.Background()

authPref := types.DefaultAuthPreference()
authPref.SetSecondFactors(
types.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
types.SecondFactorType_SECOND_FACTOR_TYPE_SSO,
)
authPref.SetWebauthn(&types.Webauthn{
RPID: "localhost",
})

// Create an SSO user which can easily add MFA devices.
connector := mockConnector(t)
username := "[email protected]"
user, err := types.NewUser(username)
require.NoError(t, err)
user.SetRoles([]string{teleport.PresetAccessRoleName})
user.SetLogins(nil)
userCreatedAt := time.Now()
user.SetCreatedBy(types.CreatedBy{
Time: userCreatedAt,
Connector: &types.ConnectorRef{
ID: connector.GetName(),
Type: connector.GetKind(),
},
})

srv := testenv.MakeTestServer(t, testenv.WithAuthPreference(authPref), testenv.WithBootstrap(connector, user))
auth := srv.GetAuthServer()
proxyAddr, err := srv.ProxyWebAddr()
require.NoError(t, err)

tmpHomePath := t.TempDir()

mfaList := func(t *testing.T) string {
commandOutput := new(bytes.Buffer)
err = Run(ctx,
[]string{"mfa", "ls", "--format", teleport.JSON},
setHomePath(tmpHomePath),
setOverrideStdout(commandOutput),
)
require.NoError(t, err)
return string(commandOutput.Bytes())
}

// Prepare a WebAuthn device.
device, err := mocku2f.Create()
require.NoError(t, err)
webauthnLoginOpt := setupWebAuthnChallengeSolver(device, true /* success */)

mfaAddWebAuthn := func(t *testing.T) string {
commandOutput := new(bytes.Buffer)
err = Run(ctx,
[]string{"mfa", "add", "--name", "webauthn-device", "--type", "WEBAUTHN", "--allow-passwordless"},
setHomePath(tmpHomePath),
setOverrideStdout(commandOutput),
webauthnLoginOpt,
)
require.NoError(t, err)
return string(commandOutput.Bytes())
}

mfaRemove := func(t *testing.T, name string) string {
commandOutput := new(bytes.Buffer)
err = Run(ctx,
[]string{"mfa", "rm", name},
setHomePath(tmpHomePath),
setOverrideStdout(commandOutput),
)
require.NoError(t, err)
return string(commandOutput.Bytes())
}

// login.
err = Run(ctx,
[]string{"login", "-d", "--insecure", "--proxy", proxyAddr.String()},
setHomePath(tmpHomePath),
setMockSSOLogin(auth, user, connector.GetName()))
require.NoError(t, err)

// mfa ls should output no devices.
out := mfaList(t)
require.NoError(t, err)
require.Equal(t, "null\n", out)

// Add a webauthn device, it should show up in the list.
out = mfaAddWebAuthn(t)
require.Empty(t, out)

out = mfaList(t)
require.NoError(t, err)

expectJSON, err := serializeMFADevices([]*types.MFADevice{}, teleport.JSON)
require.Equal(t, expectJSON+"\n", out)

// Enable MFA in the SSO connector, we should now see the device.
connector.SetMFASettings(&types.OIDCConnectorMFASettings{
Enabled: true,
ClientId: "mfa-client",
})
_, err = auth.UpsertOIDCConnector(ctx, connector)
require.NoError(t, err)

// The SSO device should look like this, but we don't add it directly.
ssoDev, err := types.NewMFADevice(connector.GetDisplay(), connector.GetName(), userCreatedAt, &types.MFADevice_Sso{
Sso: &types.SSOMFADevice{
ConnectorId: connector.GetName(),
ConnectorType: connector.GetKind(),
},
})

out = mfaList(t)
require.NoError(t, err)

expectJSON, err = serializeMFADevices([]*types.MFADevice{ssoDev}, teleport.JSON)
require.Equal(t, expectJSON+"\n", out)

// The SSO MFA device cannot be deleted.
out = mfaRemove(t, connector.GetName())
require.Empty(t, out)

// Disabling MFA in the auth connector should remove the device for the user.
connector.SetMFASettings(&types.OIDCConnectorMFASettings{Enabled: false})
_, err = auth.UpsertOIDCConnector(ctx, connector)
require.NoError(t, err)

out = mfaList(t)
require.NoError(t, err)
require.Equal(t, "null\n", out)
}

0 comments on commit e1e5663

Please sign in to comment.