mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
Add second_factors (#47233)
* Add proto. * Add decoding logic for SecondFactorType. * Update auth preference methods to use and prefer SecondFactors. * Add fileconf and warning logs. * Fix tests. * Address comments. * Address comments. * Validate SecondFactor; Disallow SecondFactor and SecondFactors to both be set. * Address comments. * Treat second factor SSO as SecondFactor=on; Prevent local user lockout when SSO is the only enabled MFA method; Ensure SecondFactors=[] is disallowed. * Upate terraform schema, docs, crds. * Address comments. * Address comments. * Fix lint, fix test.
This commit is contained in:
@@ -2097,6 +2097,20 @@ message AuthPreferenceSpecV2 {
|
||||
// SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster.
|
||||
// The current default value is "legacy". This field is not yet fully supported.
|
||||
SignatureAlgorithmSuite signature_algorithm_suite = 20;
|
||||
|
||||
// SecondFactors is a list of supported second factor types.
|
||||
repeated SecondFactorType SecondFactors = 21 [(gogoproto.jsontag) = "second_factors,omitempty"];
|
||||
}
|
||||
|
||||
// SecondFactorType is a type of second factor.
|
||||
enum SecondFactorType {
|
||||
SECOND_FACTOR_TYPE_UNSPECIFIED = 0;
|
||||
// SECOND_FACTOR_TYPE_OTP is OTP second factor.
|
||||
SECOND_FACTOR_TYPE_OTP = 1;
|
||||
// SECOND_FACTOR_TYPE_WEBAUTHN is WebAuthn second factor.
|
||||
SECOND_FACTOR_TYPE_WEBAUTHN = 2;
|
||||
// SECOND_FACTOR_TYPE_SSO is SSO second factor.
|
||||
SECOND_FACTOR_TYPE_SSO = 3;
|
||||
}
|
||||
|
||||
// U2F defines settings for U2F device.
|
||||
|
||||
+107
-80
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -74,18 +75,26 @@ type AuthPreference interface {
|
||||
GetSecondFactor() constants.SecondFactorType
|
||||
// SetSecondFactor sets the type of second factor.
|
||||
SetSecondFactor(constants.SecondFactorType)
|
||||
// GetSecondFactors gets a list of supported second factors.
|
||||
GetSecondFactors() []SecondFactorType
|
||||
// SetSecondFactors sets the list of supported second factors.
|
||||
SetSecondFactors(...SecondFactorType)
|
||||
// GetPreferredLocalMFA returns a server-side hint for clients to pick an MFA
|
||||
// method when various options are available.
|
||||
// It is empty if there is nothing to suggest.
|
||||
GetPreferredLocalMFA() constants.SecondFactorType
|
||||
// IsSecondFactorEnforced checks if second factor is enforced
|
||||
// (not disabled or set to optional).
|
||||
// IsSecondFactorEnabled checks if second factor is enabled.
|
||||
IsSecondFactorEnabled() bool
|
||||
// IsSecondFactorEnforced checks if second factor is enforced.
|
||||
IsSecondFactorEnforced() bool
|
||||
// IsSecondFactorTOTPAllowed checks if users are allowed to register TOTP devices.
|
||||
// IsSecondFactorLocalAllowed checks if a local second factor method is enabled (webauthn, totp).
|
||||
IsSecondFactorLocalAllowed() bool
|
||||
// IsSecondFactorTOTPAllowed checks if users can use TOTP as an MFA method.
|
||||
IsSecondFactorTOTPAllowed() bool
|
||||
// IsSecondFactorWebauthnAllowed checks if users are allowed to register
|
||||
// Webauthn devices.
|
||||
// IsSecondFactorWebauthnAllowed checks if users can use WebAuthn as an MFA method.
|
||||
IsSecondFactorWebauthnAllowed() bool
|
||||
// IsSecondFactorSSOAllowed checks if users can use SSO as an MFA method.
|
||||
IsSecondFactorSSOAllowed() bool
|
||||
// IsAdminActionMFAEnforced checks if admin action MFA is enforced.
|
||||
IsAdminActionMFAEnforced() bool
|
||||
|
||||
@@ -314,62 +323,87 @@ func (c *AuthPreferenceV2) SetType(s string) {
|
||||
|
||||
// GetSecondFactor returns the type of second factor.
|
||||
func (c *AuthPreferenceV2) GetSecondFactor() constants.SecondFactorType {
|
||||
// SecondFactors takes priority if set.
|
||||
if len(c.Spec.SecondFactors) > 0 {
|
||||
return legacySecondFactorFromSecondFactors(c.Spec.SecondFactors)
|
||||
}
|
||||
|
||||
return c.Spec.SecondFactor
|
||||
}
|
||||
|
||||
// SetSecondFactor sets the type of second factor.
|
||||
func (c *AuthPreferenceV2) SetSecondFactor(s constants.SecondFactorType) {
|
||||
c.Spec.SecondFactor = s
|
||||
|
||||
// Unset SecondFactors, only one can be set at a time.
|
||||
c.Spec.SecondFactors = nil
|
||||
}
|
||||
|
||||
// GetSecondFactors gets a list of supported second factors.
|
||||
func (c *AuthPreferenceV2) GetSecondFactors() []SecondFactorType {
|
||||
if len(c.Spec.SecondFactors) > 0 {
|
||||
return c.Spec.SecondFactors
|
||||
}
|
||||
|
||||
// If SecondFactors isn't set, try to convert the old SecondFactor field.
|
||||
return secondFactorsFromLegacySecondFactor(c.Spec.SecondFactor)
|
||||
}
|
||||
|
||||
// SetSecondFactors sets the list of supported second factors.
|
||||
func (c *AuthPreferenceV2) SetSecondFactors(sfs ...SecondFactorType) {
|
||||
c.Spec.SecondFactors = sfs
|
||||
|
||||
// Unset SecondFactor, only one can be set at a time.
|
||||
c.Spec.SecondFactor = ""
|
||||
}
|
||||
|
||||
// GetPreferredLocalMFA returns a server-side hint for clients to pick an MFA
|
||||
// method when various options are available.
|
||||
// It is empty if there is nothing to suggest.
|
||||
func (c *AuthPreferenceV2) GetPreferredLocalMFA() constants.SecondFactorType {
|
||||
switch sf := c.GetSecondFactor(); sf {
|
||||
case constants.SecondFactorOff:
|
||||
return "" // Nothing to suggest.
|
||||
case constants.SecondFactorOTP, constants.SecondFactorWebauthn:
|
||||
return sf // Single method.
|
||||
case constants.SecondFactorOn, constants.SecondFactorOptional:
|
||||
// In order of preference:
|
||||
// 1. WebAuthn (public-key based)
|
||||
// 2. OTP
|
||||
if _, err := c.GetWebauthn(); err == nil {
|
||||
return constants.SecondFactorWebauthn
|
||||
}
|
||||
return constants.SecondFactorOTP
|
||||
default:
|
||||
slog.WarnContext(context.Background(), "Found unknown second_factor setting", "second_factor", sf)
|
||||
return "" // Unsure, say nothing.
|
||||
if c.IsSecondFactorWebauthnAllowed() {
|
||||
return secondFactorTypeWebauthnString
|
||||
}
|
||||
|
||||
if c.IsSecondFactorTOTPAllowed() {
|
||||
return secondFactorTypeOTPString
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsSecondFactorEnforced checks if second factor is enforced (not disabled or set to optional).
|
||||
// IsSecondFactorEnforced checks if second factor is enabled.
|
||||
func (c *AuthPreferenceV2) IsSecondFactorEnabled() bool {
|
||||
// TODO(Joerger): outside of tests, second factor should always be enabled.
|
||||
// All calls should be removed and the old off/optional second factors removed.
|
||||
return len(c.GetSecondFactors()) > 0
|
||||
}
|
||||
|
||||
// IsSecondFactorEnforced checks if second factor is enforced.
|
||||
func (c *AuthPreferenceV2) IsSecondFactorEnforced() bool {
|
||||
return c.Spec.SecondFactor != constants.SecondFactorOff && c.Spec.SecondFactor != constants.SecondFactorOptional
|
||||
// TODO(Joerger): outside of tests, second factor should always be enforced.
|
||||
// All calls should be removed and the old off/optional second factors removed.
|
||||
return len(c.GetSecondFactors()) > 0 && c.Spec.SecondFactor != constants.SecondFactorOptional
|
||||
}
|
||||
|
||||
// IsSecondFactorTOTPAllowed checks if users are allowed to register TOTP devices.
|
||||
// IsSecondFactorLocalAllowed checks if a local second factor method is enabled.
|
||||
func (c *AuthPreferenceV2) IsSecondFactorLocalAllowed() bool {
|
||||
return c.IsSecondFactorTOTPAllowed() || c.IsSecondFactorWebauthnAllowed()
|
||||
}
|
||||
|
||||
// IsSecondFactorTOTPAllowed checks if users can use TOTP as an MFA method.
|
||||
func (c *AuthPreferenceV2) IsSecondFactorTOTPAllowed() bool {
|
||||
return c.Spec.SecondFactor == constants.SecondFactorOTP ||
|
||||
c.Spec.SecondFactor == constants.SecondFactorOptional ||
|
||||
c.Spec.SecondFactor == constants.SecondFactorOn
|
||||
return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_OTP)
|
||||
}
|
||||
|
||||
// IsSecondFactorWebauthnAllowed checks if users are allowed to register
|
||||
// Webauthn devices.
|
||||
// IsSecondFactorWebauthnAllowed checks if users can use WebAuthn as an MFA method.
|
||||
func (c *AuthPreferenceV2) IsSecondFactorWebauthnAllowed() bool {
|
||||
// Is Webauthn configured and enabled?
|
||||
switch _, err := c.GetWebauthn(); {
|
||||
case trace.IsNotFound(err): // OK, expected to happen in some cases.
|
||||
return false
|
||||
case err != nil:
|
||||
slog.WarnContext(context.Background(), "Got unexpected error when reading Webauthn config", "error", err)
|
||||
return false
|
||||
}
|
||||
return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN)
|
||||
}
|
||||
|
||||
// Are second factor settings in accordance?
|
||||
return c.Spec.SecondFactor == constants.SecondFactorWebauthn ||
|
||||
c.Spec.SecondFactor == constants.SecondFactorOptional ||
|
||||
c.Spec.SecondFactor == constants.SecondFactorOn
|
||||
// IsSecondFactorSSOAllowed checks if users can use SSO as an MFA method.
|
||||
func (c *AuthPreferenceV2) IsSecondFactorSSOAllowed() bool {
|
||||
return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_SSO)
|
||||
}
|
||||
|
||||
// IsAdminActionMFAEnforced checks if admin action MFA is enforced.
|
||||
@@ -657,9 +691,6 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
|
||||
if c.Spec.Type == "" {
|
||||
c.Spec.Type = constants.Local
|
||||
}
|
||||
if c.Spec.SecondFactor == "" {
|
||||
c.Spec.SecondFactor = constants.SecondFactorOTP
|
||||
}
|
||||
if c.Spec.AllowLocalAuth == nil {
|
||||
c.Spec.AllowLocalAuth = NewBoolOption(true)
|
||||
}
|
||||
@@ -686,20 +717,32 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
|
||||
return trace.BadParameter("authentication type %q not supported", c.Spec.Type)
|
||||
}
|
||||
|
||||
if c.Spec.SecondFactor == constants.SecondFactorU2F {
|
||||
// Validate SecondFactor and SecondFactors.
|
||||
if c.Spec.SecondFactor != "" && len(c.Spec.SecondFactors) > 0 {
|
||||
return trace.BadParameter("must set either SecondFactor or SecondFactors, not both")
|
||||
}
|
||||
|
||||
switch c.Spec.SecondFactor {
|
||||
case constants.SecondFactorOff, constants.SecondFactorOTP, constants.SecondFactorWebauthn, constants.SecondFactorOn, constants.SecondFactorOptional:
|
||||
case constants.SecondFactorU2F:
|
||||
const deprecationMessage = `` +
|
||||
`Second Factor "u2f" is deprecated and marked for removal, using "webauthn" instead. ` +
|
||||
`Please update your configuration to use WebAuthn. ` +
|
||||
`Refer to https://goteleport.com/docs/access-controls/guides/webauthn/`
|
||||
slog.WarnContext(context.Background(), deprecationMessage)
|
||||
c.Spec.SecondFactor = constants.SecondFactorWebauthn
|
||||
case "":
|
||||
// default to OTP if SecondFactors is also not set.
|
||||
if len(c.Spec.SecondFactors) == 0 {
|
||||
c.Spec.SecondFactor = constants.SecondFactorOTP
|
||||
}
|
||||
default:
|
||||
return trace.BadParameter("second factor type %q not supported", c.Spec.SecondFactor)
|
||||
}
|
||||
|
||||
// Make sure second factor makes sense.
|
||||
sf := c.Spec.SecondFactor
|
||||
switch sf {
|
||||
case constants.SecondFactorOff, constants.SecondFactorOTP:
|
||||
case constants.SecondFactorWebauthn:
|
||||
// Validate expected fields for webauthn.
|
||||
hasWebauthn := c.IsSecondFactorWebauthnAllowed()
|
||||
if hasWebauthn {
|
||||
// If U2F is present validate it, we can derive Webauthn from it.
|
||||
if c.Spec.U2F != nil {
|
||||
if err := c.Spec.U2F.Check(); err != nil {
|
||||
@@ -709,45 +752,21 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
|
||||
// Not a problem, try to derive from U2F.
|
||||
c.Spec.Webauthn = &Webauthn{}
|
||||
}
|
||||
}
|
||||
if c.Spec.Webauthn == nil {
|
||||
return trace.BadParameter("missing required webauthn configuration for second factor type %q", sf)
|
||||
}
|
||||
if err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
case constants.SecondFactorOn, constants.SecondFactorOptional:
|
||||
// The following scenarios are allowed for "on" and "optional":
|
||||
// - Webauthn is configured (preferred)
|
||||
// - U2F is configured, Webauthn derived from it (U2F-compat mode)
|
||||
|
||||
if c.Spec.U2F == nil && c.Spec.Webauthn == nil {
|
||||
return trace.BadParameter("missing required webauthn configuration for second factor type %q", sf)
|
||||
}
|
||||
|
||||
// Is U2F configured?
|
||||
if c.Spec.U2F != nil {
|
||||
if err := c.Spec.U2F.Check(); err != nil {
|
||||
if err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if c.Spec.Webauthn == nil {
|
||||
// Not a problem, try to derive from U2F.
|
||||
c.Spec.Webauthn = &Webauthn{}
|
||||
}
|
||||
}
|
||||
|
||||
// Is Webauthn valid? At this point we should always have a config.
|
||||
if c.Spec.Webauthn == nil {
|
||||
return trace.BadParameter("missing required webauthn configuration")
|
||||
}
|
||||
|
||||
if err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
default:
|
||||
return trace.BadParameter("second factor type %q not supported", c.Spec.SecondFactor)
|
||||
}
|
||||
|
||||
// Set/validate AllowPasswordless. We need Webauthn first to do this properly.
|
||||
hasWebauthn := sf == constants.SecondFactorWebauthn ||
|
||||
sf == constants.SecondFactorOn ||
|
||||
sf == constants.SecondFactorOptional
|
||||
switch {
|
||||
case c.Spec.AllowPasswordless == nil:
|
||||
c.Spec.AllowPasswordless = NewBoolOption(hasWebauthn)
|
||||
@@ -763,6 +782,14 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
|
||||
return trace.BadParameter("missing required Webauthn configuration for headless=true")
|
||||
}
|
||||
|
||||
// Prevent local lockout by disabling local second factor methods.
|
||||
if c.GetAllowLocalAuth() && c.IsSecondFactorEnforced() && !c.IsSecondFactorLocalAllowed() {
|
||||
if c.IsSecondFactorSSOAllowed() {
|
||||
trace.BadParameter("missing a local second factor method for local users (otp, webauthn), either add a local second factor method or disable local auth")
|
||||
}
|
||||
return trace.BadParameter("missing a local second factor method for local users (otp, webauthn)")
|
||||
}
|
||||
|
||||
// Validate connector name for type=local.
|
||||
if c.Spec.Type == constants.Local {
|
||||
switch connectorName := c.Spec.ConnectorName; connectorName {
|
||||
@@ -836,7 +863,7 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
|
||||
|
||||
// String represents a human readable version of authentication settings.
|
||||
func (c *AuthPreferenceV2) String() string {
|
||||
return fmt.Sprintf("AuthPreference(Type=%q,SecondFactor=%q)", c.Spec.Type, c.Spec.SecondFactor)
|
||||
return fmt.Sprintf("AuthPreference(Type=%q,SecondFactor=%q)", c.Spec.Type, c.GetSecondFactor())
|
||||
}
|
||||
|
||||
// Clone returns a copy of the AuthPreference resource.
|
||||
|
||||
@@ -19,7 +19,11 @@ package types
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
)
|
||||
|
||||
// TestMarshalUnmarshalRequireMFAType tests encoding/decoding of the RequireMFAType.
|
||||
@@ -64,3 +68,174 @@ func TestEncodeDecodeRequireMFAType(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuthPreference_secondFactors(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
spec AuthPreferenceSpecV2
|
||||
assertErr require.ErrorAssertionFunc
|
||||
assertAuthPref func(t *testing.T, authPref AuthPreference)
|
||||
}{
|
||||
{
|
||||
name: "OK default to OTP",
|
||||
spec: AuthPreferenceSpecV2{},
|
||||
assertAuthPref: func(t *testing.T, authPref AuthPreference) {
|
||||
assert.Equal(t, []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP}, authPref.GetSecondFactors())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK OTP default settings",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
},
|
||||
},
|
||||
assertAuthPref: func(t *testing.T, authPref AuthPreference) {
|
||||
assert.False(t, authPref.GetAllowPasswordless())
|
||||
assert.False(t, authPref.GetAllowHeadless())
|
||||
assert.True(t, authPref.GetAllowLocalAuth())
|
||||
assert.False(t, authPref.IsAdminActionMFAEnforced())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK WebAuthn default settings",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
|
||||
},
|
||||
Webauthn: &Webauthn{
|
||||
RPID: "localhost",
|
||||
},
|
||||
},
|
||||
assertAuthPref: func(t *testing.T, authPref AuthPreference) {
|
||||
assert.True(t, authPref.GetAllowPasswordless())
|
||||
assert.True(t, authPref.GetAllowHeadless())
|
||||
assert.True(t, authPref.GetAllowLocalAuth())
|
||||
assert.True(t, authPref.IsAdminActionMFAEnforced())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK SSO default settings",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_SSO,
|
||||
},
|
||||
AllowLocalAuth: NewBoolOption(false),
|
||||
},
|
||||
assertAuthPref: func(t *testing.T, authPref AuthPreference) {
|
||||
assert.False(t, authPref.GetAllowPasswordless())
|
||||
assert.False(t, authPref.GetAllowHeadless())
|
||||
assert.False(t, authPref.GetAllowLocalAuth())
|
||||
assert.True(t, authPref.IsAdminActionMFAEnforced())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK all second factors",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_SSO,
|
||||
},
|
||||
Webauthn: &Webauthn{
|
||||
RPID: "localhost",
|
||||
},
|
||||
},
|
||||
assertAuthPref: func(t *testing.T, authPref AuthPreference) {
|
||||
assert.True(t, authPref.GetAllowPasswordless())
|
||||
assert.True(t, authPref.GetAllowHeadless())
|
||||
assert.True(t, authPref.GetAllowLocalAuth())
|
||||
// enabling OTP disables admin mfa.
|
||||
assert.False(t, authPref.IsAdminActionMFAEnforced())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK U2F config provided",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
|
||||
},
|
||||
U2F: &U2F{
|
||||
AppID: "https://localhost",
|
||||
},
|
||||
},
|
||||
assertAuthPref: func(t *testing.T, authPref AuthPreference) {
|
||||
w, err := authPref.GetWebauthn()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &Webauthn{RPID: "localhost"}, w)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NOK SecondFactor and SecondFactors both set",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactor: constants.SecondFactorWebauthn,
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
|
||||
},
|
||||
},
|
||||
assertErr: func(t require.TestingT, err error, vals ...interface{}) {
|
||||
assert.ErrorAs(t, err, new(*trace.BadParameterError))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NOK WebAuthn config missing",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
|
||||
},
|
||||
},
|
||||
assertErr: func(t require.TestingT, err error, vals ...interface{}) {
|
||||
assert.ErrorAs(t, err, new(*trace.BadParameterError))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NOK prevent passwordless without WebAuthn",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
},
|
||||
AllowPasswordless: NewBoolOption(true),
|
||||
},
|
||||
assertErr: func(t require.TestingT, err error, vals ...interface{}) {
|
||||
assert.ErrorAs(t, err, new(*trace.BadParameterError))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NOK prevent headless without WebAuthn",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
},
|
||||
AllowHeadless: NewBoolOption(true),
|
||||
},
|
||||
assertErr: func(t require.TestingT, err error, vals ...interface{}) {
|
||||
assert.ErrorAs(t, err, new(*trace.BadParameterError))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NOK prevent local lockout with second factor SSO",
|
||||
spec: AuthPreferenceSpecV2{
|
||||
SecondFactors: []SecondFactorType{
|
||||
SecondFactorType_SECOND_FACTOR_TYPE_SSO,
|
||||
},
|
||||
AllowLocalAuth: NewBoolOption(true),
|
||||
},
|
||||
assertErr: func(t require.TestingT, err error, vals ...interface{}) {
|
||||
assert.ErrorAs(t, err, new(*trace.BadParameterError))
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authPref, err := NewAuthPreference(tt.spec)
|
||||
if tt.assertErr != nil {
|
||||
tt.assertErr(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.assertAuthPref != nil {
|
||||
tt.assertAuthPref(t, authPref)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright 2024 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
)
|
||||
|
||||
// legacySecondFactorFromSecondFactors returns a suitable legacy second factor for the given list of second factors.
|
||||
func legacySecondFactorFromSecondFactors(secondFactors []SecondFactorType) constants.SecondFactorType {
|
||||
hasOTP := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_OTP)
|
||||
hasWebAuthn := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN)
|
||||
|
||||
switch {
|
||||
case hasOTP && hasWebAuthn:
|
||||
return constants.SecondFactorOn
|
||||
case hasWebAuthn:
|
||||
return constants.SecondFactorWebauthn
|
||||
case hasOTP:
|
||||
return constants.SecondFactorOTP
|
||||
default:
|
||||
return constants.SecondFactorOff
|
||||
}
|
||||
}
|
||||
|
||||
// secondFactorsFromLegacySecondFactor returns the list of SecondFactorTypes supported by the given second factor type.
|
||||
func secondFactorsFromLegacySecondFactor(sf constants.SecondFactorType) []SecondFactorType {
|
||||
switch sf {
|
||||
case constants.SecondFactorOff:
|
||||
return nil
|
||||
case constants.SecondFactorOptional, constants.SecondFactorOn:
|
||||
return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN, SecondFactorType_SECOND_FACTOR_TYPE_OTP}
|
||||
case constants.SecondFactorOTP:
|
||||
return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP}
|
||||
case constants.SecondFactorWebauthn:
|
||||
return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON marshals SecondFactorType to string.
|
||||
func (s *SecondFactorType) MarshalYAML() (interface{}, error) {
|
||||
val, err := s.encode()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML supports parsing SecondFactorType from string.
|
||||
func (s *SecondFactorType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var val interface{}
|
||||
err := unmarshal(&val)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = s.decode(val)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// MarshalJSON marshals SecondFactorType to string.
|
||||
func (s *SecondFactorType) MarshalJSON() ([]byte, error) {
|
||||
val, err := s.encode()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
out, err := json.Marshal(val)
|
||||
return out, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports parsing SecondFactorType from string.
|
||||
func (s *SecondFactorType) UnmarshalJSON(data []byte) error {
|
||||
var val interface{}
|
||||
err := json.Unmarshal(data, &val)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = s.decode(val)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
const (
|
||||
// secondFactorTypeOTPString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_OTP
|
||||
secondFactorTypeOTPString = "otp"
|
||||
// secondFactorTypeWebauthnString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN
|
||||
secondFactorTypeWebauthnString = "webauthn"
|
||||
// secondFactorTypeSSOString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_SSO
|
||||
secondFactorTypeSSOString = "sso"
|
||||
)
|
||||
|
||||
func (s *SecondFactorType) encode() (string, error) {
|
||||
switch *s {
|
||||
case SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED:
|
||||
return "", nil
|
||||
case SecondFactorType_SECOND_FACTOR_TYPE_OTP:
|
||||
return secondFactorTypeOTPString, nil
|
||||
case SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN:
|
||||
return secondFactorTypeWebauthnString, nil
|
||||
case SecondFactorType_SECOND_FACTOR_TYPE_SSO:
|
||||
return secondFactorTypeSSOString, nil
|
||||
default:
|
||||
return "", trace.BadParameter("invalid SecondFactorType value %v", *s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SecondFactorType) decode(val any) error {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
switch v {
|
||||
case secondFactorTypeOTPString:
|
||||
*s = SecondFactorType_SECOND_FACTOR_TYPE_OTP
|
||||
case secondFactorTypeWebauthnString:
|
||||
*s = SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN
|
||||
case secondFactorTypeSSOString:
|
||||
*s = SecondFactorType_SECOND_FACTOR_TYPE_SSO
|
||||
case "":
|
||||
*s = SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
|
||||
default:
|
||||
return trace.BadParameter("invalid SecondFactorType value %v", val)
|
||||
}
|
||||
case int32:
|
||||
return trace.Wrap(s.setFromEnum(v))
|
||||
case int64:
|
||||
return trace.Wrap(s.setFromEnum(int32(v)))
|
||||
case int:
|
||||
return trace.Wrap(s.setFromEnum(int32(v)))
|
||||
case float64:
|
||||
return trace.Wrap(s.setFromEnum(int32(v)))
|
||||
case float32:
|
||||
return trace.Wrap(s.setFromEnum(int32(v)))
|
||||
default:
|
||||
return trace.BadParameter("invalid SecondFactorType type %T", val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setFromEnum sets the value from enum value as int32.
|
||||
func (s *SecondFactorType) setFromEnum(val int32) error {
|
||||
if _, ok := SecondFactorType_name[val]; !ok {
|
||||
return trace.BadParameter("invalid SecondFactorType enum %v", val)
|
||||
}
|
||||
*s = SecondFactorType(val)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2022 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
)
|
||||
|
||||
func TestEncodeDecodeSecondFactorType(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
secondFactorType SecondFactorType
|
||||
encoded string
|
||||
}{
|
||||
{
|
||||
secondFactorType: SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
encoded: secondFactorTypeOTPString,
|
||||
}, {
|
||||
secondFactorType: SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN,
|
||||
encoded: secondFactorTypeWebauthnString,
|
||||
}, {
|
||||
secondFactorType: SecondFactorType_SECOND_FACTOR_TYPE_SSO,
|
||||
encoded: secondFactorTypeSSOString,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.secondFactorType.String(), func(t *testing.T) {
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
encoded, err := tt.secondFactorType.encode()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.encoded, encoded)
|
||||
})
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
var decoded SecondFactorType
|
||||
err := decoded.decode(tt.encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.secondFactorType, decoded)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySecondFactorsFromLegacySecondFactor(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
sf constants.SecondFactorType
|
||||
sfs []SecondFactorType
|
||||
}{
|
||||
{
|
||||
sf: "",
|
||||
sfs: nil,
|
||||
},
|
||||
{
|
||||
sf: constants.SecondFactorOff,
|
||||
sfs: nil,
|
||||
},
|
||||
{
|
||||
sf: constants.SecondFactorOptional,
|
||||
sfs: []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN, SecondFactorType_SECOND_FACTOR_TYPE_OTP},
|
||||
},
|
||||
{
|
||||
sf: constants.SecondFactorOn,
|
||||
sfs: []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN, SecondFactorType_SECOND_FACTOR_TYPE_OTP},
|
||||
},
|
||||
{
|
||||
sf: constants.SecondFactorOTP,
|
||||
sfs: []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP},
|
||||
},
|
||||
{
|
||||
sf: constants.SecondFactorWebauthn,
|
||||
sfs: []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN},
|
||||
},
|
||||
} {
|
||||
t.Run(string(tt.sf), func(t *testing.T) {
|
||||
assert.Equal(t, tt.sfs, secondFactorsFromLegacySecondFactor(tt.sf))
|
||||
})
|
||||
}
|
||||
}
|
||||
+2204
-2065
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,7 @@ Optional:
|
||||
- `okta` (Attributes) Okta is a set of options related to the Okta service in Teleport. Requires Teleport Enterprise. (see [below for nested schema](#nested-schema-for-specokta))
|
||||
- `require_session_mfa` (Number) RequireMFAType is the type of MFA requirement enforced for this cluster. 0 is "OFF", 1 is "SESSION", 2 is "SESSION_AND_HARDWARE_KEY", 3 is "HARDWARE_KEY_TOUCH", 4 is "HARDWARE_KEY_PIN", 5 is "HARDWARE_KEY_TOUCH_AND_PIN".
|
||||
- `second_factor` (String) SecondFactor is the type of mult-factor.
|
||||
- `second_factors` (List of Number) SecondFactors is a list of supported second factor types.
|
||||
- `signature_algorithm_suite` (Number) SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. The current default value is "legacy". This field is not yet fully supported.
|
||||
- `type` (String) Type is the type of authentication.
|
||||
- `u2f` (Attributes) U2F are the settings for the U2F device. (see [below for nested schema](#nested-schema-for-specu2f))
|
||||
|
||||
@@ -59,6 +59,7 @@ Optional:
|
||||
- `okta` (Attributes) Okta is a set of options related to the Okta service in Teleport. Requires Teleport Enterprise. (see [below for nested schema](#nested-schema-for-specokta))
|
||||
- `require_session_mfa` (Number) RequireMFAType is the type of MFA requirement enforced for this cluster. 0 is "OFF", 1 is "SESSION", 2 is "SESSION_AND_HARDWARE_KEY", 3 is "HARDWARE_KEY_TOUCH", 4 is "HARDWARE_KEY_PIN", 5 is "HARDWARE_KEY_TOUCH_AND_PIN".
|
||||
- `second_factor` (String) SecondFactor is the type of mult-factor.
|
||||
- `second_factors` (List of Number) SecondFactors is a list of supported second factor types.
|
||||
- `signature_algorithm_suite` (Number) SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. The current default value is "legacy". This field is not yet fully supported.
|
||||
- `type` (String) Type is the type of authentication.
|
||||
- `u2f` (Attributes) U2F are the settings for the U2F device. (see [below for nested schema](#nested-schema-for-specu2f))
|
||||
|
||||
@@ -1399,6 +1399,11 @@ func GenSchemaAuthPreferenceV2(ctx context.Context) (github_com_hashicorp_terraf
|
||||
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()},
|
||||
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
|
||||
},
|
||||
"second_factors": {
|
||||
Description: "SecondFactors is a list of supported second factor types.",
|
||||
Optional: true,
|
||||
Type: github_com_hashicorp_terraform_plugin_framework_types.ListType{ElemType: github_com_hashicorp_terraform_plugin_framework_types.Int64Type},
|
||||
},
|
||||
"signature_algorithm_suite": {
|
||||
Description: "SignatureAlgorithmSuite is the configured signature algorithm suite for the cluster. The current default value is \"legacy\". This field is not yet fully supported.",
|
||||
Optional: true,
|
||||
@@ -14096,6 +14101,33 @@ func CopyAuthPreferenceV2FromTerraform(_ context.Context, tf github_com_hashicor
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
a, ok := tf.Attrs["second_factors"]
|
||||
if !ok {
|
||||
diags.Append(attrReadMissingDiag{"AuthPreferenceV2.Spec.SecondFactors"})
|
||||
} else {
|
||||
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.List)
|
||||
if !ok {
|
||||
diags.Append(attrReadConversionFailureDiag{"AuthPreferenceV2.Spec.SecondFactors", "github.com/hashicorp/terraform-plugin-framework/types.List"})
|
||||
} else {
|
||||
obj.SecondFactors = make([]github_com_gravitational_teleport_api_types.SecondFactorType, len(v.Elems))
|
||||
if !v.Null && !v.Unknown {
|
||||
for k, a := range v.Elems {
|
||||
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Int64)
|
||||
if !ok {
|
||||
diags.Append(attrReadConversionFailureDiag{"AuthPreferenceV2.Spec.SecondFactors", "github_com_hashicorp_terraform_plugin_framework_types.Int64"})
|
||||
} else {
|
||||
var t github_com_gravitational_teleport_api_types.SecondFactorType
|
||||
if !v.Null && !v.Unknown {
|
||||
t = github_com_gravitational_teleport_api_types.SecondFactorType(v.Value)
|
||||
}
|
||||
obj.SecondFactors[k] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15292,6 +15324,59 @@ func CopyAuthPreferenceV2ToTerraform(ctx context.Context, obj *github_com_gravit
|
||||
tf.Attrs["signature_algorithm_suite"] = v
|
||||
}
|
||||
}
|
||||
{
|
||||
a, ok := tf.AttrTypes["second_factors"]
|
||||
if !ok {
|
||||
diags.Append(attrWriteMissingDiag{"AuthPreferenceV2.Spec.SecondFactors"})
|
||||
} else {
|
||||
o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ListType)
|
||||
if !ok {
|
||||
diags.Append(attrWriteConversionFailureDiag{"AuthPreferenceV2.Spec.SecondFactors", "github.com/hashicorp/terraform-plugin-framework/types.ListType"})
|
||||
} else {
|
||||
c, ok := tf.Attrs["second_factors"].(github_com_hashicorp_terraform_plugin_framework_types.List)
|
||||
if !ok {
|
||||
c = github_com_hashicorp_terraform_plugin_framework_types.List{
|
||||
|
||||
ElemType: o.ElemType,
|
||||
Elems: make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.SecondFactors)),
|
||||
Null: true,
|
||||
}
|
||||
} else {
|
||||
if c.Elems == nil {
|
||||
c.Elems = make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.SecondFactors))
|
||||
}
|
||||
}
|
||||
if obj.SecondFactors != nil {
|
||||
t := o.ElemType
|
||||
if len(obj.SecondFactors) != len(c.Elems) {
|
||||
c.Elems = make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.SecondFactors))
|
||||
}
|
||||
for k, a := range obj.SecondFactors {
|
||||
v, ok := tf.Attrs["second_factors"].(github_com_hashicorp_terraform_plugin_framework_types.Int64)
|
||||
if !ok {
|
||||
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
|
||||
if err != nil {
|
||||
diags.Append(attrWriteGeneralError{"AuthPreferenceV2.Spec.SecondFactors", err})
|
||||
}
|
||||
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.Int64)
|
||||
if !ok {
|
||||
diags.Append(attrWriteConversionFailureDiag{"AuthPreferenceV2.Spec.SecondFactors", "github.com/hashicorp/terraform-plugin-framework/types.Int64"})
|
||||
}
|
||||
v.Null = int64(a) == 0
|
||||
}
|
||||
v.Value = int64(a)
|
||||
v.Unknown = false
|
||||
c.Elems[k] = v
|
||||
}
|
||||
if len(obj.SecondFactors) > 0 {
|
||||
c.Null = false
|
||||
}
|
||||
}
|
||||
c.Unknown = false
|
||||
tf.Attrs["second_factors"] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v.Unknown = false
|
||||
tf.Attrs["spec"] = v
|
||||
|
||||
@@ -5045,7 +5045,6 @@ debug_service:
|
||||
}
|
||||
|
||||
func TestSignatureAlgorithmSuite(t *testing.T) {
|
||||
|
||||
for desc, tc := range map[string]struct {
|
||||
fips bool
|
||||
hsm bool
|
||||
|
||||
@@ -1002,6 +1002,7 @@ func (t StaticToken) Parse() ([]types.ProvisionTokenV1, error) {
|
||||
type AuthenticationConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
SecondFactor constants.SecondFactorType `yaml:"second_factor,omitempty"`
|
||||
SecondFactors []types.SecondFactorType `yaml:"second_factors,omitempty"`
|
||||
ConnectorName string `yaml:"connector_name,omitempty"`
|
||||
U2F *UniversalSecondFactor `yaml:"u2f,omitempty"`
|
||||
Webauthn *Webauthn `yaml:"webauthn,omitempty"`
|
||||
@@ -1090,9 +1091,16 @@ func (a *AuthenticationConfig) Parse() (types.AuthPreference, error) {
|
||||
default:
|
||||
}
|
||||
|
||||
if a.SecondFactor != "" && a.SecondFactors != nil {
|
||||
log.Warn(`` +
|
||||
`second_factor and second_factors are both set. second_factors will take precedence. ` +
|
||||
`second_factor should be unset to remove this warning.`)
|
||||
}
|
||||
|
||||
return types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
|
||||
Type: a.Type,
|
||||
SecondFactor: a.SecondFactor,
|
||||
SecondFactors: a.SecondFactors,
|
||||
ConnectorName: a.ConnectorName,
|
||||
U2F: u,
|
||||
Webauthn: w,
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/types/accesslist"
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
@@ -325,8 +324,7 @@ func ValidateResource(res types.Resource) error {
|
||||
if GetModules().Features().Cloud || !IsInsecureTestMode() {
|
||||
switch r := res.(type) {
|
||||
case types.AuthPreference:
|
||||
switch r.GetSecondFactor() {
|
||||
case constants.SecondFactorOff, constants.SecondFactorOptional:
|
||||
if !r.IsSecondFactorEnforced() {
|
||||
return trace.Wrap(ErrCannotDisableSecondFactor)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user