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:
Brian Joerger
2024-10-11 00:02:39 +00:00
committed by GitHub
parent b71f017244
commit dbe08e0430
12 changed files with 2856 additions and 2149 deletions
@@ -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
View File
@@ -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.
+175
View File
@@ -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)
}
})
}
}
+166
View File
@@ -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
}
+94
View File
@@ -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
View File
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
-1
View File
@@ -5045,7 +5045,6 @@ debug_service:
}
func TestSignatureAlgorithmSuite(t *testing.T) {
for desc, tc := range map[string]struct {
fips bool
hsm bool
+8
View File
@@ -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,
+1 -3
View File
@@ -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)
}
}