mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-16 07:44:31 +08:00
This adds an rpc UpsertKubeServiceV2 to replace UpsertKubeService. Currently, kubernetes service does not have a keepalive heartbeat unlike app, db, and windows service. This brings functionality in line with the others. This would allow for future use of the keepalive to track connected agents via prometheus metrics.
1828 lines
54 KiB
Go
1828 lines
54 KiB
Go
/*
|
|
Copyright 2015-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 auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"fmt"
|
|
mathrand "math/rand"
|
|
"os"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
apidefaults "github.com/gravitational/teleport/api/defaults"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
authority "github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/auth/u2f"
|
|
"github.com/gravitational/teleport/lib/backend"
|
|
"github.com/gravitational/teleport/lib/backend/lite"
|
|
"github.com/gravitational/teleport/lib/backend/memory"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"github.com/gravitational/teleport/lib/fixtures"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/services/local"
|
|
"github.com/gravitational/teleport/lib/services/suite"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/coreos/go-oidc/jose"
|
|
"github.com/google/uuid"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type testPack struct {
|
|
bk backend.Backend
|
|
clusterName types.ClusterName
|
|
a *Server
|
|
mockEmitter *events.MockEmitter
|
|
}
|
|
|
|
func newTestPack(ctx context.Context, dataDir string) (testPack, error) {
|
|
var (
|
|
p testPack
|
|
err error
|
|
)
|
|
p.bk, err = lite.NewWithConfig(ctx, lite.Config{Path: dataDir})
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
p.clusterName, err = services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
|
|
ClusterName: "test.localhost",
|
|
})
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
authConfig := &InitConfig{
|
|
Backend: p.bk,
|
|
ClusterName: p.clusterName,
|
|
Authority: authority.New(),
|
|
SkipPeriodicOperations: true,
|
|
}
|
|
p.a, err = NewServer(authConfig)
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
// set lock watcher
|
|
lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{
|
|
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
|
Component: teleport.ComponentAuth,
|
|
Client: p.a,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
p.a.SetLockWatcher(lockWatcher)
|
|
|
|
// set cluster name
|
|
err = p.a.SetClusterName(p.clusterName)
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
// set static tokens
|
|
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{},
|
|
})
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
err = p.a.SetStaticTokens(staticTokens)
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOff,
|
|
})
|
|
if err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.SetAuthPreference(ctx, authPreference); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.SetClusterAuditConfig(ctx, types.DefaultClusterAuditConfig()); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.SetClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig()); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.SetSessionRecordingConfig(ctx, types.DefaultSessionRecordingConfig()); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
if err := p.a.UpsertCertAuthority(suite.NewTestCA(types.UserCA, p.clusterName.GetClusterName())); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.UpsertCertAuthority(suite.NewTestCA(types.HostCA, p.clusterName.GetClusterName())); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
if err := p.a.UpsertNamespace(types.DefaultNamespace()); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
p.mockEmitter = &events.MockEmitter{}
|
|
p.a.emitter = p.mockEmitter
|
|
return p, nil
|
|
}
|
|
|
|
func newAuthSuite(t *testing.T) *testPack {
|
|
s, err := newTestPack(context.Background(), t.TempDir())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if s.bk != nil {
|
|
s.bk.Close()
|
|
}
|
|
})
|
|
|
|
return &s
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
utils.InitLoggerForTests()
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestSessions(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
|
|
user := "user1"
|
|
pass := []byte("abc123")
|
|
|
|
_, err := s.a.AuthenticateWebUser(AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, _, err = CreateUserAndRole(s.a, user, []string{user})
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
ws, err := s.a.AuthenticateWebUser(AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ws)
|
|
|
|
out, err := s.a.GetWebSessionInfo(ctx, user, ws.GetName())
|
|
require.NoError(t, err)
|
|
ws.SetPriv(nil)
|
|
require.Equal(t, ws, out)
|
|
|
|
err = s.a.WebSessions().Delete(ctx, types.DeleteWebSessionRequest{
|
|
User: user,
|
|
SessionID: ws.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = s.a.GetWebSession(ctx, types.GetWebSessionRequest{
|
|
User: user,
|
|
SessionID: ws.GetName(),
|
|
})
|
|
require.True(t, trace.IsNotFound(err), "%#v", err)
|
|
}
|
|
|
|
func TestAuthenticateSSHUser(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Register the leaf cluster.
|
|
leaf, err := types.NewRemoteCluster("leaf.localhost")
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.a.CreateRemoteCluster(leaf))
|
|
|
|
user := "user1"
|
|
pass := []byte("abc123")
|
|
|
|
// Try to login as an unknown user.
|
|
_, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// Create the user.
|
|
_, role, err := CreateUserAndRole(s.a, user, []string{user})
|
|
require.NoError(t, err)
|
|
err = s.a.UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
// Give the role some k8s principals too.
|
|
role.SetKubeUsers(types.Allow, []string{user})
|
|
role.SetKubeGroups(types.Allow, []string{"system:masters"})
|
|
err = s.a.UpsertRole(ctx, role)
|
|
require.NoError(t, err)
|
|
|
|
kg := testauthority.New()
|
|
_, pub, err := kg.GetNewKeyPairFromPool()
|
|
require.NoError(t, err)
|
|
|
|
// Login to the root cluster.
|
|
resp, err := s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.Username, user)
|
|
// Verify the public key and principals in SSH cert.
|
|
inSSHPub, _, _, _, err := ssh.ParseAuthorizedKey(pub)
|
|
require.NoError(t, err)
|
|
gotSSHCert, err := sshutils.ParseCertificate(resp.Cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, gotSSHCert.Key, inSSHPub)
|
|
require.Equal(t, gotSSHCert.ValidPrincipals, []string{user})
|
|
// Verify the public key and Subject in TLS cert.
|
|
inCryptoPub := inSSHPub.(ssh.CryptoPublicKey).CryptoPublicKey()
|
|
gotTLSCert, err := tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, gotTLSCert.PublicKey, inCryptoPub)
|
|
wantID := tlsca.Identity{
|
|
Username: user,
|
|
Groups: []string{role.GetName()},
|
|
Principals: []string{user},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
gotID, err := tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Login to the leaf cluster.
|
|
resp, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: "leaf.localhost",
|
|
KubernetesCluster: "leaf-kube-cluster",
|
|
})
|
|
require.NoError(t, err)
|
|
// Verify the TLS cert has the correct RouteToCluster set.
|
|
gotTLSCert, err = tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
wantID = tlsca.Identity{
|
|
Username: user,
|
|
Groups: []string{role.GetName()},
|
|
Principals: []string{user},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
// It's OK to use a non-existent kube cluster for leaf teleport
|
|
// clusters. The leaf is responsible for validating those.
|
|
KubernetesCluster: "leaf-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: "leaf.localhost",
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
gotID, err = tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Register a kubernetes cluster to verify the defaulting logic in TLS cert
|
|
// generation.
|
|
_, err = s.a.UpsertKubeServiceV2(ctx, &types.ServerV2{
|
|
Metadata: types.Metadata{Name: "kube-service"},
|
|
Kind: types.KindKubeService,
|
|
Version: types.V2,
|
|
Spec: types.ServerSpecV2{
|
|
KubernetesClusters: []*types.KubernetesCluster{{Name: "root-kube-cluster"}},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Login specifying a valid kube cluster. It should appear in the TLS cert.
|
|
resp, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
KubernetesCluster: "root-kube-cluster",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.Username, user)
|
|
gotTLSCert, err = tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
wantID = tlsca.Identity{
|
|
Username: user,
|
|
Groups: []string{role.GetName()},
|
|
Principals: []string{user},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
gotID, err = tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Login without specifying kube cluster. A registered one should be picked
|
|
// automatically.
|
|
resp, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
// Intentionally empty, auth server should default to a registered
|
|
// kubernetes cluster.
|
|
KubernetesCluster: "",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.Username, user)
|
|
gotTLSCert, err = tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
wantID = tlsca.Identity{
|
|
Username: user,
|
|
Groups: []string{role.GetName()},
|
|
Principals: []string{user},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
gotID, err = tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Register a kubernetes cluster to verify the defaulting logic in TLS cert
|
|
// generation.
|
|
err = s.a.UpsertKubeService(ctx, &types.ServerV2{
|
|
Metadata: types.Metadata{Name: "kube-service"},
|
|
Kind: types.KindKubeService,
|
|
Version: types.V2,
|
|
Spec: types.ServerSpecV2{
|
|
KubernetesClusters: []*types.KubernetesCluster{{Name: "root-kube-cluster"}},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Login specifying a valid kube cluster. It should appear in the TLS cert.
|
|
resp, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
KubernetesCluster: "root-kube-cluster",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.Username, user)
|
|
gotTLSCert, err = tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
wantID = tlsca.Identity{
|
|
Username: user,
|
|
Groups: []string{role.GetName()},
|
|
Principals: []string{user},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
gotID, err = tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Login without specifying kube cluster. A registered one should be picked
|
|
// automatically.
|
|
resp, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
// Intentionally empty, auth server should default to a registered
|
|
// kubernetes cluster.
|
|
KubernetesCluster: "",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.Username, user)
|
|
gotTLSCert, err = tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
wantID = tlsca.Identity{
|
|
Username: user,
|
|
Groups: []string{role.GetName()},
|
|
Principals: []string{user},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
gotID, err = tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Login specifying an invalid kube cluster. This should fail.
|
|
_, err = s.a.AuthenticateSSHUser(AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
},
|
|
PublicKey: pub,
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
KubernetesCluster: "invalid-kube-cluster",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestUserLock(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
username := "user1"
|
|
pass := []byte("abc123")
|
|
|
|
_, err := s.a.AuthenticateWebUser(AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, _, err = CreateUserAndRole(s.a, username, []string{username})
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertPassword(username, pass)
|
|
require.NoError(t, err)
|
|
|
|
// successful log in
|
|
ws, err := s.a.AuthenticateWebUser(AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ws)
|
|
|
|
fakeClock := clockwork.NewFakeClock()
|
|
s.a.SetClock(fakeClock)
|
|
|
|
for i := 0; i <= defaults.MaxLoginAttempts; i++ {
|
|
_, err = s.a.AuthenticateWebUser(AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: []byte("wrong pass")},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
user, err := s.a.Identity.GetUser(username, false)
|
|
require.NoError(t, err)
|
|
require.True(t, user.GetStatus().IsLocked)
|
|
|
|
// advance time and make sure we can login again
|
|
fakeClock.Advance(defaults.AccountLockInterval + time.Second)
|
|
|
|
_, err = s.a.AuthenticateWebUser(AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTokensCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
|
|
// before we do anything, we should have 0 tokens
|
|
btokens, err := s.a.GetTokens(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, btokens, 0)
|
|
|
|
// generate persistent token
|
|
tok, err := s.a.GenerateToken(ctx, GenerateTokenRequest{Roles: types.SystemRoles{types.RoleNode}})
|
|
require.NoError(t, err)
|
|
require.Len(t, tok, 2*TokenLenBytes)
|
|
tokens, err := s.a.GetTokens(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, tokens, 1)
|
|
require.Equal(t, tokens[0].GetName(), tok)
|
|
|
|
roles, _, err := s.a.ValidateToken(ctx, tok)
|
|
require.NoError(t, err)
|
|
require.True(t, roles.Include(types.RoleNode))
|
|
require.False(t, roles.Include(types.RoleProxy))
|
|
|
|
// generate predefined token
|
|
customToken := "custom-token"
|
|
tok, err = s.a.GenerateToken(ctx, GenerateTokenRequest{Roles: types.SystemRoles{types.RoleNode}, Token: customToken})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tok, customToken)
|
|
|
|
roles, _, err = s.a.ValidateToken(ctx, tok)
|
|
require.NoError(t, err)
|
|
require.True(t, roles.Include(types.RoleNode))
|
|
require.False(t, roles.Include(types.RoleProxy))
|
|
|
|
err = s.a.DeleteToken(ctx, customToken)
|
|
require.NoError(t, err)
|
|
|
|
// lets use static tokens now
|
|
roles = types.SystemRoles{types.RoleProxy}
|
|
st, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{{
|
|
Token: "static-token-value",
|
|
Roles: roles,
|
|
Expires: time.Unix(0, 0).UTC(),
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.SetStaticTokens(st)
|
|
require.NoError(t, err)
|
|
|
|
r, _, err := s.a.ValidateToken(ctx, "static-token-value")
|
|
require.NoError(t, err)
|
|
require.Equal(t, r, roles)
|
|
|
|
// List tokens (should see 2: one static, one regular)
|
|
tokens, err = s.a.GetTokens(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, tokens, 2)
|
|
}
|
|
|
|
func TestBadTokens(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
// empty
|
|
_, _, err := s.a.ValidateToken(ctx, "")
|
|
require.Error(t, err)
|
|
|
|
// garbage
|
|
_, _, err = s.a.ValidateToken(ctx, "bla bla")
|
|
require.Error(t, err)
|
|
|
|
// tampered
|
|
tok, err := s.a.GenerateToken(ctx, GenerateTokenRequest{Roles: types.SystemRoles{types.RoleAuth}})
|
|
require.NoError(t, err)
|
|
|
|
tampered := string(tok[0]+1) + tok[1:]
|
|
_, _, err = s.a.ValidateToken(ctx, tampered)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestGenerateTokenEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
// test trusted cluster token emit
|
|
_, err := s.a.GenerateToken(ctx, GenerateTokenRequest{Roles: types.SystemRoles{types.RoleTrustedCluster}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.TrustedClusterTokenCreateEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test emit with multiple roles
|
|
_, err = s.a.GenerateToken(ctx, GenerateTokenRequest{Roles: types.SystemRoles{
|
|
types.RoleNode,
|
|
types.RoleTrustedCluster,
|
|
types.RoleAuth,
|
|
}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.TrustedClusterTokenCreateEvent)
|
|
}
|
|
|
|
func TestValidateACRValues(t *testing.T) {
|
|
s := newAuthSuite(t)
|
|
|
|
tests := []struct {
|
|
comment string
|
|
inIDToken string
|
|
inACRValue string
|
|
inACRProvider string
|
|
outIsValid require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
"0 - default, acr values match",
|
|
`
|
|
{
|
|
"acr": "foo",
|
|
"aud": "00000000-0000-0000-0000-000000000000",
|
|
"exp": 1111111111
|
|
}
|
|
`,
|
|
"foo",
|
|
"",
|
|
require.NoError,
|
|
},
|
|
{
|
|
"1 - default, acr values do not match",
|
|
`
|
|
{
|
|
"acr": "foo",
|
|
"aud": "00000000-0000-0000-0000-000000000000",
|
|
"exp": 1111111111
|
|
}
|
|
`,
|
|
"bar",
|
|
"",
|
|
require.Error,
|
|
},
|
|
{
|
|
"2 - netiq, acr values match",
|
|
`
|
|
{
|
|
"acr": {
|
|
"values": [
|
|
"foo/bar/baz"
|
|
]
|
|
},
|
|
"aud": "00000000-0000-0000-0000-000000000000",
|
|
"exp": 1111111111
|
|
}
|
|
`,
|
|
"foo/bar/baz",
|
|
"netiq",
|
|
require.NoError,
|
|
},
|
|
{
|
|
"3 - netiq, invalid format",
|
|
`
|
|
{
|
|
"acr": {
|
|
"values": "foo/bar/baz"
|
|
},
|
|
"aud": "00000000-0000-0000-0000-000000000000",
|
|
"exp": 1111111111
|
|
}
|
|
`,
|
|
"foo/bar/baz",
|
|
"netiq",
|
|
require.Error,
|
|
},
|
|
{
|
|
"4 - netiq, invalid value",
|
|
`
|
|
{
|
|
"acr": {
|
|
"values": [
|
|
"foo/bar/baz/qux"
|
|
]
|
|
},
|
|
"aud": "00000000-0000-0000-0000-000000000000",
|
|
"exp": 1111111111
|
|
}
|
|
`,
|
|
"foo/bar/baz",
|
|
"netiq",
|
|
require.Error,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.comment, func(t *testing.T) {
|
|
t.Parallel()
|
|
var claims jose.Claims
|
|
err := json.Unmarshal([]byte(tt.inIDToken), &claims)
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.validateACRValues(tt.inACRValue, tt.inACRProvider, claims)
|
|
tt.outIsValid(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateConfig(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
cn, err := s.a.GetClusterName()
|
|
require.NoError(t, err)
|
|
require.Equal(t, cn.GetClusterName(), s.clusterName.GetClusterName())
|
|
st, err := s.a.GetStaticTokens()
|
|
require.NoError(t, err)
|
|
require.Equal(t, st.GetStaticTokens(), []types.ProvisionToken{})
|
|
|
|
// try and set cluster name, this should fail because you can only set the
|
|
// cluster name once
|
|
clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
|
|
ClusterName: "foo.localhost",
|
|
})
|
|
require.NoError(t, err)
|
|
// use same backend but start a new auth server with different config.
|
|
authConfig := &InitConfig{
|
|
ClusterName: clusterName,
|
|
Backend: s.bk,
|
|
Authority: authority.New(),
|
|
SkipPeriodicOperations: true,
|
|
}
|
|
authServer, err := NewServer(authConfig)
|
|
require.NoError(t, err)
|
|
|
|
err = authServer.SetClusterName(clusterName)
|
|
require.Error(t, err)
|
|
// try and set static tokens, this should be successful because the last
|
|
// one to upsert tokens wins
|
|
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{{
|
|
Token: "bar",
|
|
Roles: types.SystemRoles{types.SystemRole("baz")},
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
err = authServer.SetStaticTokens(staticTokens)
|
|
require.NoError(t, err)
|
|
|
|
// check first auth server and make sure it returns the correct values
|
|
// (original cluster name, new static tokens)
|
|
cn, err = s.a.GetClusterName()
|
|
require.NoError(t, err)
|
|
require.Equal(t, cn.GetClusterName(), s.clusterName.GetClusterName())
|
|
st, err = s.a.GetStaticTokens()
|
|
require.NoError(t, err)
|
|
require.Equal(t, st.GetStaticTokens(), types.ProvisionTokensFromV1([]types.ProvisionTokenV1{{
|
|
Token: "bar",
|
|
Roles: types.SystemRoles{types.SystemRole("baz")},
|
|
}}))
|
|
|
|
// check second auth server and make sure it also has the correct values
|
|
// new static tokens
|
|
st, err = authServer.GetStaticTokens()
|
|
require.NoError(t, err)
|
|
require.Equal(t, st.GetStaticTokens(), types.ProvisionTokensFromV1([]types.ProvisionTokenV1{{
|
|
Token: "bar",
|
|
Roles: types.SystemRoles{types.SystemRole("baz")},
|
|
}}))
|
|
}
|
|
|
|
func TestCreateAndUpdateUserEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
user, err := types.NewUser("some-user")
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
// test create uesr, happy path
|
|
user.SetCreatedBy(types.CreatedBy{
|
|
User: types.UserRef{Name: "some-auth-user"},
|
|
})
|
|
err = s.a.CreateUser(ctx, user)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.UserCreateEvent)
|
|
require.Equal(t, s.mockEmitter.LastEvent().(*apievents.UserCreate).User, "some-auth-user")
|
|
s.mockEmitter.Reset()
|
|
|
|
// test create user with existing user
|
|
err = s.a.CreateUser(ctx, user)
|
|
require.True(t, trace.IsAlreadyExists(err))
|
|
require.Nil(t, s.mockEmitter.LastEvent())
|
|
|
|
// test createdBy gets set to default
|
|
user2, err := types.NewUser("some-other-user")
|
|
require.NoError(t, err)
|
|
err = s.a.CreateUser(ctx, user2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().(*apievents.UserCreate).User, teleport.UserSystem)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test update on non-existent user
|
|
user3, err := types.NewUser("non-existent-user")
|
|
require.NoError(t, err)
|
|
err = s.a.UpdateUser(ctx, user3)
|
|
require.True(t, trace.IsNotFound(err))
|
|
require.Nil(t, s.mockEmitter.LastEvent())
|
|
|
|
// test update user
|
|
err = s.a.UpdateUser(ctx, user)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.UserUpdatedEvent)
|
|
require.Equal(t, s.mockEmitter.LastEvent().(*apievents.UserCreate).User, teleport.UserSystem)
|
|
}
|
|
|
|
func TestTrustedClusterCRUDEventEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
s.a.emitter = s.mockEmitter
|
|
|
|
// set up existing cluster to bypass switch cases that
|
|
// makes a network request when creating new clusters
|
|
tc, err := types.NewTrustedCluster("test", types.TrustedClusterSpecV2{
|
|
Enabled: true,
|
|
Roles: []string{"a"},
|
|
ReverseTunnelAddress: "b",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = s.a.Presence.UpsertTrustedCluster(ctx, tc)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.a.UpsertCertAuthority(suite.NewTestCA(types.UserCA, "test")))
|
|
require.NoError(t, s.a.UpsertCertAuthority(suite.NewTestCA(types.HostCA, "test")))
|
|
|
|
err = s.a.createReverseTunnel(tc)
|
|
require.NoError(t, err)
|
|
|
|
// test create event for switch case: when tc exists but enabled is false
|
|
tc.SetEnabled(false)
|
|
|
|
_, err = s.a.UpsertTrustedCluster(ctx, tc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.TrustedClusterCreateEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test create event for switch case: when tc exists but enabled is true
|
|
tc.SetEnabled(true)
|
|
|
|
_, err = s.a.UpsertTrustedCluster(ctx, tc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.TrustedClusterCreateEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test delete event
|
|
err = s.a.DeleteTrustedCluster(ctx, "test")
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.TrustedClusterDeleteEvent)
|
|
}
|
|
|
|
func TestGithubConnectorCRUDEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
// test github create event
|
|
github, err := types.NewGithubConnector("test", types.GithubConnectorSpecV3{})
|
|
require.NoError(t, err)
|
|
err = s.a.upsertGithubConnector(ctx, github)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.GithubConnectorCreatedEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test github update event
|
|
err = s.a.upsertGithubConnector(ctx, github)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.GithubConnectorCreatedEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test github delete event
|
|
err = s.a.deleteGithubConnector(ctx, "test")
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.GithubConnectorDeletedEvent)
|
|
}
|
|
|
|
func TestOIDCConnectorCRUDEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
// test oidc create event
|
|
oidc, err := types.NewOIDCConnector("test", types.OIDCConnectorSpecV3{ClientID: "a"})
|
|
require.NoError(t, err)
|
|
err = s.a.UpsertOIDCConnector(ctx, oidc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.OIDCConnectorCreatedEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test oidc update event
|
|
err = s.a.UpsertOIDCConnector(ctx, oidc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.OIDCConnectorCreatedEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test oidc delete event
|
|
err = s.a.DeleteOIDCConnector(ctx, "test")
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.OIDCConnectorDeletedEvent)
|
|
}
|
|
|
|
func TestSAMLConnectorCRUDEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
// generate a certificate that makes ParseCertificatePEM happy, copied from ca_test.go
|
|
ca, err := tlsca.FromKeys([]byte(fixtures.TLSCACertPEM), []byte(fixtures.TLSCAKeyPEM))
|
|
require.NoError(t, err)
|
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, constants.RSAKeySize)
|
|
require.NoError(t, err)
|
|
|
|
testClock := clockwork.NewFakeClock()
|
|
certBytes, err := ca.GenerateCertificate(tlsca.CertificateRequest{
|
|
Clock: testClock,
|
|
PublicKey: privateKey.Public(),
|
|
Subject: pkix.Name{CommonName: "test"},
|
|
NotAfter: testClock.Now().Add(time.Hour),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// test saml create
|
|
saml, err := types.NewSAMLConnector("test", types.SAMLConnectorSpecV2{
|
|
AssertionConsumerService: "a",
|
|
Issuer: "b",
|
|
SSO: "c",
|
|
Cert: string(certBytes),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertSAMLConnector(ctx, saml)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.SAMLConnectorCreatedEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test saml update event
|
|
err = s.a.UpsertSAMLConnector(ctx, saml)
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.SAMLConnectorCreatedEvent)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test saml delete event
|
|
err = s.a.DeleteSAMLConnector(ctx, "test")
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.SAMLConnectorDeletedEvent)
|
|
}
|
|
|
|
func TestU2FSignChallengeCompat(t *testing.T) {
|
|
// Test that the new U2F challenge encoding format is backwards-compatible
|
|
// with older clients and servers.
|
|
//
|
|
// New format is U2FAuthenticateChallenge as JSON.
|
|
// Old format was u2f.AuthenticateChallenge as JSON.
|
|
t.Run("old client, new server", func(t *testing.T) {
|
|
t.Parallel()
|
|
newChallenge := &MFAAuthenticateChallenge{
|
|
AuthenticateChallenge: &u2f.AuthenticateChallenge{
|
|
Challenge: "c1",
|
|
},
|
|
U2FChallenges: []u2f.AuthenticateChallenge{
|
|
{Challenge: "c1"},
|
|
{Challenge: "c2"},
|
|
{Challenge: "c3"},
|
|
},
|
|
}
|
|
wire, err := json.Marshal(newChallenge)
|
|
require.NoError(t, err)
|
|
|
|
var oldChallenge u2f.AuthenticateChallenge
|
|
err = json.Unmarshal(wire, &oldChallenge)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, cmp.Diff(oldChallenge, *newChallenge.AuthenticateChallenge))
|
|
})
|
|
t.Run("new client, old server", func(t *testing.T) {
|
|
t.Parallel()
|
|
oldChallenge := &u2f.AuthenticateChallenge{
|
|
Challenge: "c1",
|
|
}
|
|
wire, err := json.Marshal(oldChallenge)
|
|
require.NoError(t, err)
|
|
|
|
var newChallenge MFAAuthenticateChallenge
|
|
err = json.Unmarshal(wire, &newChallenge)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, cmp.Diff(newChallenge, MFAAuthenticateChallenge{AuthenticateChallenge: oldChallenge}))
|
|
})
|
|
}
|
|
|
|
func TestEmitSSOLoginFailureEvent(t *testing.T) {
|
|
mockE := &events.MockEmitter{}
|
|
|
|
emitSSOLoginFailureEvent(context.Background(), mockE, "test", trace.BadParameter("some error"))
|
|
|
|
require.Equal(t, mockE.LastEvent(), &apievents.UserLogin{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.UserLoginEvent,
|
|
Code: events.UserSSOLoginFailureCode,
|
|
},
|
|
Method: "test",
|
|
Status: apievents.Status{
|
|
Success: false,
|
|
Error: "some error",
|
|
UserMessage: "some error",
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestGenerateUserCertWithLocks(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
user, role, err := CreateUserAndRole(p.a, "test-user", []string{})
|
|
require.NoError(t, err)
|
|
mfaID := "test-mfa-id"
|
|
requestID := "test-access-request"
|
|
keygen := testauthority.New()
|
|
_, pub, err := keygen.GetNewKeyPairFromPool()
|
|
require.NoError(t, err)
|
|
certReq := certRequest{
|
|
user: user,
|
|
checker: services.NewRoleSet(role),
|
|
mfaVerified: mfaID,
|
|
publicKey: pub,
|
|
activeRequests: services.RequestIDs{AccessRequests: []string{requestID}},
|
|
}
|
|
_, err = p.a.generateUserCert(certReq)
|
|
require.NoError(t, err)
|
|
|
|
testTargets := append(
|
|
[]types.LockTarget{{User: user.GetName()}, {MFADevice: mfaID}, {AccessRequest: requestID}},
|
|
services.RolesToLockTargets(user.GetRoles())...,
|
|
)
|
|
for _, target := range testTargets {
|
|
t.Run(fmt.Sprintf("lock targeting %v", target), func(t *testing.T) {
|
|
lockWatch, err := p.a.lockWatcher.Subscribe(ctx, target)
|
|
require.NoError(t, err)
|
|
defer lockWatch.Close()
|
|
lock, err := types.NewLock("test-lock", types.LockSpecV2{Target: target})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, p.a.UpsertLock(ctx, lock))
|
|
select {
|
|
case event := <-lockWatch.Events():
|
|
require.Equal(t, types.OpPut, event.Type)
|
|
require.Empty(t, resourceDiff(event.Resource, lock))
|
|
case <-lockWatch.Done():
|
|
t.Fatal("Watcher has unexpectedly exited.")
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timeout waiting for lock update.")
|
|
}
|
|
_, err = p.a.generateUserCert(certReq)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, services.LockInForceAccessDenied(lock).Error())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateHostCertWithLocks(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
hostID := uuid.New().String()
|
|
keygen := testauthority.New()
|
|
_, pub, err := keygen.GetNewKeyPairFromPool()
|
|
require.NoError(t, err)
|
|
_, err = p.a.GenerateHostCert(pub, hostID, "test-node", []string{},
|
|
p.clusterName.GetClusterName(), types.RoleNode, time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
target := types.LockTarget{Node: hostID}
|
|
lockWatch, err := p.a.lockWatcher.Subscribe(ctx, target)
|
|
require.NoError(t, err)
|
|
defer lockWatch.Close()
|
|
lock, err := types.NewLock("test-lock", types.LockSpecV2{Target: target})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, p.a.UpsertLock(ctx, lock))
|
|
select {
|
|
case event := <-lockWatch.Events():
|
|
require.Equal(t, types.OpPut, event.Type)
|
|
require.Empty(t, resourceDiff(event.Resource, lock))
|
|
case <-lockWatch.Done():
|
|
t.Fatal("Watcher has unexpectedly exited.")
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timeout waiting for lock update.")
|
|
}
|
|
_, err = p.a.GenerateHostCert(pub, hostID, "test-node", []string{}, p.clusterName.GetClusterName(), types.RoleNode, time.Minute)
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, services.LockInForceAccessDenied(lock).Error())
|
|
|
|
// Locks targeting nodes should not apply to other system roles.
|
|
_, err = p.a.GenerateHostCert(pub, hostID, "test-proxy", []string{}, p.clusterName.GetClusterName(), types.RoleProxy, time.Minute)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestNewWebSession(t *testing.T) {
|
|
t.Parallel()
|
|
p, err := newTestPack(context.Background(), t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// Set a web idle timeout.
|
|
duration := time.Duration(5) * time.Minute
|
|
cfg := types.DefaultClusterNetworkingConfig()
|
|
cfg.SetWebIdleTimeout(duration)
|
|
p.a.SetClusterNetworkingConfig(context.Background(), cfg)
|
|
|
|
// Create a user.
|
|
user, _, err := CreateUserAndRole(p.a, "test-user", []string{"test-role"})
|
|
require.NoError(t, err)
|
|
|
|
// Create a new web session.
|
|
req := types.NewWebSessionRequest{
|
|
User: user.GetName(),
|
|
Roles: user.GetRoles(),
|
|
Traits: user.GetTraits(),
|
|
LoginTime: p.a.clock.Now().UTC(),
|
|
SessionTTL: apidefaults.CertDuration,
|
|
}
|
|
bearerTokenTTL := utils.MinTTL(req.SessionTTL, BearerTokenTTL)
|
|
|
|
ws, err := p.a.NewWebSession(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, user.GetName(), ws.GetUser())
|
|
require.Equal(t, duration, ws.GetIdleTimeout())
|
|
require.Equal(t, req.LoginTime, ws.GetLoginTime())
|
|
require.Equal(t, req.LoginTime.UTC().Add(req.SessionTTL), ws.GetExpiryTime())
|
|
require.Equal(t, req.LoginTime.UTC().Add(bearerTokenTTL), ws.GetBearerTokenExpiryTime())
|
|
require.NotEmpty(t, ws.GetBearerToken())
|
|
require.NotEmpty(t, ws.GetPriv())
|
|
require.NotEmpty(t, ws.GetPub())
|
|
require.NotEmpty(t, ws.GetTLSCert())
|
|
}
|
|
|
|
func TestDeleteMFADeviceSync(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t)
|
|
ctx := context.Background()
|
|
mockEmitter := &events.MockEmitter{}
|
|
srv.Auth().emitter = mockEmitter
|
|
|
|
username := "llama@goteleport.com"
|
|
_, _, err := CreateUserAndRole(srv.Auth(), username, []string{username})
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOn,
|
|
U2F: &types.U2F{
|
|
AppID: "https://localhost",
|
|
Facets: []string{"https://localhost"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
clt, err := srv.NewClient(TestUser(username))
|
|
require.NoError(t, err)
|
|
|
|
// Insert dummy devices.
|
|
webDev1, err := RegisterTestDevice(ctx, clt, "web-1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
|
|
require.NoError(t, err)
|
|
webDev2, err := RegisterTestDevice(ctx, clt, "web-2", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, webDev1)
|
|
require.NoError(t, err)
|
|
totpDev1, err := RegisterTestDevice(ctx, clt, "otp-1", proto.DeviceType_DEVICE_TYPE_TOTP, webDev1, WithTestDeviceClock(srv.Clock()))
|
|
require.NoError(t, err)
|
|
totpDev2, err := RegisterTestDevice(ctx, clt, "otp-2", proto.DeviceType_DEVICE_TYPE_TOTP, webDev1, WithTestDeviceClock(srv.Clock()))
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
deviceToDelete string
|
|
tokenReq CreateUserTokenRequest
|
|
}{
|
|
{
|
|
name: "recovery approved token",
|
|
deviceToDelete: webDev1.MFA.GetName(),
|
|
tokenReq: CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeRecoveryApproved,
|
|
},
|
|
},
|
|
{
|
|
name: "privilege token",
|
|
deviceToDelete: totpDev1.MFA.GetName(),
|
|
tokenReq: CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypePrivilege,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
token, err := srv.Auth().newUserToken(tc.tokenReq)
|
|
require.NoError(t, err)
|
|
_, err = srv.Auth().Identity.CreateUserToken(ctx, token)
|
|
require.NoError(t, err)
|
|
|
|
// Delete the TOTP device.
|
|
err = srv.Auth().DeleteMFADeviceSync(ctx, &proto.DeleteMFADeviceSyncRequest{
|
|
TokenID: token.GetName(),
|
|
DeviceName: tc.deviceToDelete,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
// Check it's been deleted.
|
|
devs, err := srv.Auth().Identity.GetMFADevices(ctx, username, false)
|
|
require.NoError(t, err)
|
|
compareDevices(t, devs, webDev2.MFA, totpDev2.MFA)
|
|
|
|
// Test last events emitted.
|
|
event := mockEmitter.LastEvent()
|
|
require.Equal(t, events.MFADeviceDeleteEvent, event.GetType())
|
|
require.Equal(t, events.MFADeviceDeleteEventCode, event.GetCode())
|
|
require.Equal(t, event.(*apievents.MFADeviceDelete).UserMetadata.User, username)
|
|
}
|
|
|
|
func TestDeleteMFADeviceSync_WithErrors(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t)
|
|
ctx := context.Background()
|
|
|
|
username := "llama@goteleport.com"
|
|
_, _, err := CreateUserAndRole(srv.Auth(), username, []string{username})
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional,
|
|
U2F: &types.U2F{
|
|
AppID: "https://localhost",
|
|
Facets: []string{"https://localhost"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
clt, err := srv.NewClient(TestUser(username))
|
|
require.NoError(t, err)
|
|
|
|
// Insert a device.
|
|
const devName = "otp"
|
|
_, err = RegisterTestDevice(ctx, clt, devName, proto.DeviceType_DEVICE_TYPE_TOTP, nil /* authenticator */, WithTestDeviceClock(srv.Clock()))
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
deviceName string
|
|
tokenRequest *CreateUserTokenRequest
|
|
assertErrType func(error) bool
|
|
}{
|
|
{
|
|
name: "token not found",
|
|
deviceName: devName,
|
|
assertErrType: trace.IsAccessDenied,
|
|
},
|
|
{
|
|
name: "invalid token type",
|
|
deviceName: devName,
|
|
tokenRequest: &CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: "unknown-token-type",
|
|
},
|
|
assertErrType: trace.IsAccessDenied,
|
|
},
|
|
{
|
|
name: "device not found",
|
|
deviceName: "does-not-exist",
|
|
tokenRequest: &CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeRecoveryApproved,
|
|
},
|
|
assertErrType: trace.IsNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tokenID := "test-token-not-found"
|
|
|
|
if tc.tokenRequest != nil {
|
|
token, err := srv.Auth().newUserToken(*tc.tokenRequest)
|
|
require.NoError(t, err)
|
|
_, err = srv.Auth().Identity.CreateUserToken(context.Background(), token)
|
|
require.NoError(t, err)
|
|
|
|
tokenID = token.GetName()
|
|
}
|
|
|
|
err = srv.Auth().DeleteMFADeviceSync(ctx, &proto.DeleteMFADeviceSyncRequest{
|
|
TokenID: tokenID,
|
|
DeviceName: tc.deviceName,
|
|
})
|
|
require.True(t, tc.assertErrType(err))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeleteMFADeviceSync_LastDevice tests for preventing deletion of last device
|
|
// when second factor is required.
|
|
func TestDeleteMFADeviceSync_LastDevice(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t)
|
|
ctx := context.Background()
|
|
|
|
u2fAuthSetting := &types.U2F{
|
|
AppID: "https://localhost",
|
|
Facets: []string{"https://localhost"},
|
|
}
|
|
|
|
newTOTPForUser := func(user string) *types.MFADevice {
|
|
clt, err := srv.NewClient(TestUser(user))
|
|
require.NoError(t, err)
|
|
dev, err := RegisterTestDevice(ctx, clt, "otp", proto.DeviceType_DEVICE_TYPE_TOTP, nil /* authenticator */, WithTestDeviceClock(srv.Clock()))
|
|
require.NoError(t, err)
|
|
return dev.MFA
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
wantErr bool
|
|
setAuthPreference func()
|
|
createDevice func(user string) *types.MFADevice
|
|
}{
|
|
{
|
|
name: "with second factor optional",
|
|
setAuthPreference: func() {
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional,
|
|
U2F: u2fAuthSetting,
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
},
|
|
createDevice: newTOTPForUser,
|
|
},
|
|
{
|
|
name: "with second factor otp",
|
|
wantErr: true,
|
|
setAuthPreference: func() {
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOTP,
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
},
|
|
createDevice: newTOTPForUser,
|
|
},
|
|
{
|
|
name: "with second factor webauthn",
|
|
wantErr: true,
|
|
setAuthPreference: func() {
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorWebauthn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "localhost",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
},
|
|
createDevice: func(user string) *types.MFADevice {
|
|
clt, err := srv.NewClient(TestUser(user))
|
|
require.NoError(t, err)
|
|
dev, err := RegisterTestDevice(ctx, clt, "web", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
|
|
require.NoError(t, err)
|
|
return dev.MFA
|
|
},
|
|
},
|
|
{
|
|
name: "with second factor on",
|
|
wantErr: true,
|
|
setAuthPreference: func() {
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOn,
|
|
U2F: u2fAuthSetting,
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
},
|
|
createDevice: newTOTPForUser,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create a user with no MFA device.
|
|
username := fmt.Sprintf("llama%v@goteleport.com", mathrand.Int())
|
|
_, _, err := CreateUserAndRole(srv.Auth(), username, []string{username})
|
|
require.NoError(t, err)
|
|
|
|
// Set auth preference.
|
|
tc.setAuthPreference()
|
|
|
|
// Insert a MFA device.
|
|
dev := tc.createDevice(username)
|
|
|
|
// Acquire an approved token.
|
|
token, err := srv.Auth().newUserToken(CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeRecoveryApproved,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = srv.Auth().Identity.CreateUserToken(context.Background(), token)
|
|
require.NoError(t, err)
|
|
|
|
// Delete the device.
|
|
err = srv.Auth().DeleteMFADeviceSync(ctx, &proto.DeleteMFADeviceSyncRequest{
|
|
TokenID: token.GetName(),
|
|
DeviceName: dev.GetName(),
|
|
})
|
|
|
|
switch {
|
|
case tc.wantErr:
|
|
require.Error(t, err)
|
|
// Check it hasn't been deleted.
|
|
res, err := srv.Auth().GetMFADevices(ctx, &proto.GetMFADevicesRequest{
|
|
TokenID: token.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.GetDevices(), 1)
|
|
default:
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddMFADeviceSync(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t)
|
|
ctx := context.Background()
|
|
mockEmitter := &events.MockEmitter{}
|
|
srv.Auth().emitter = mockEmitter
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOn,
|
|
U2F: &types.U2F{
|
|
AppID: "https://localhost",
|
|
Facets: []string{"https://localhost"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
u, err := createUserWithSecondFactors(srv)
|
|
require.NoError(t, err)
|
|
|
|
clt, err := srv.NewClient(TestUser(u.username))
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
deviceName string
|
|
wantErr bool
|
|
getReq func(string) *proto.AddMFADeviceSyncRequest
|
|
}{
|
|
{
|
|
name: "invalid token type",
|
|
wantErr: true,
|
|
getReq: func(deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
// Obtain a non privilege token.
|
|
token, err := srv.Auth().newUserToken(CreateUserTokenRequest{
|
|
Name: u.username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeResetPassword,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = srv.Auth().Identity.CreateUserToken(ctx, token)
|
|
require.NoError(t, err)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: token.GetName(),
|
|
NewDeviceName: deviceName,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "TOTP device with privilege token",
|
|
deviceName: "new-totp",
|
|
getReq: func(deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
// Obtain a privilege token.
|
|
privelegeToken, err := srv.Auth().createPrivilegeToken(ctx, u.username, UserTokenTypePrivilege)
|
|
require.NoError(t, err)
|
|
|
|
// Create token secrets.
|
|
res, err := srv.Auth().CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
|
|
TokenID: privelegeToken.GetName(),
|
|
DeviceType: proto.DeviceType_DEVICE_TYPE_TOTP,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, totpRegRes, err := NewTestDeviceFromChallenge(res, WithTestDeviceClock(srv.Auth().clock))
|
|
require.NoError(t, err)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: privelegeToken.GetName(),
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: totpRegRes,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "U2F device with privilege exception token",
|
|
deviceName: "new-u2f",
|
|
getReq: func(deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
// Obtain a privilege exception token.
|
|
privExToken, err := srv.Auth().createPrivilegeToken(ctx, u.username, UserTokenTypePrivilegeException)
|
|
require.NoError(t, err)
|
|
|
|
// Create register challenge and sign.
|
|
u2fRegRes, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), privExToken.GetName())
|
|
require.NoError(t, err)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: privExToken.GetName(),
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{
|
|
U2F: u2fRegRes,
|
|
}},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Webauthn device with privilege exception token",
|
|
deviceName: "new-webauthn",
|
|
getReq: func(deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
privExToken, err := srv.Auth().createPrivilegeToken(ctx, u.username, UserTokenTypePrivilegeException)
|
|
require.NoError(t, err)
|
|
|
|
_, webauthnRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), privExToken.GetName())
|
|
require.NoError(t, err)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: privExToken.GetName(),
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: webauthnRes,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
res, err := clt.AddMFADeviceSync(ctx, tc.getReq(tc.deviceName))
|
|
switch {
|
|
case tc.wantErr:
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
default:
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.deviceName, res.GetDevice().GetName())
|
|
|
|
// Test events emitted.
|
|
event := mockEmitter.LastEvent()
|
|
require.Equal(t, events.MFADeviceAddEvent, event.GetType())
|
|
require.Equal(t, events.MFADeviceAddEventCode, event.GetCode())
|
|
require.Equal(t, event.(*apievents.MFADeviceAdd).UserMetadata.User, u.username)
|
|
|
|
// Check it's been added.
|
|
res, err := clt.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
|
|
require.NoError(t, err)
|
|
|
|
found := false
|
|
for _, mfa := range res.GetDevices() {
|
|
if mfa.GetName() == tc.deviceName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.True(t, found, "MFA device %q not found", tc.deviceName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMFADevices_WithToken(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t)
|
|
ctx := context.Background()
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional,
|
|
U2F: &types.U2F{
|
|
AppID: "https://localhost",
|
|
Facets: []string{"https://localhost"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
username := "llama@goteleport.com"
|
|
_, _, err = CreateUserAndRole(srv.Auth(), username, []string{username})
|
|
require.NoError(t, err)
|
|
|
|
clt, err := srv.NewClient(TestUser(username))
|
|
require.NoError(t, err)
|
|
webDev, err := RegisterTestDevice(ctx, clt, "web", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
|
|
require.NoError(t, err)
|
|
totpDev, err := RegisterTestDevice(ctx, clt, "otp", proto.DeviceType_DEVICE_TYPE_TOTP, webDev, WithTestDeviceClock(srv.Clock()))
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
wantErr bool
|
|
tokenRequest *CreateUserTokenRequest
|
|
}{
|
|
{
|
|
name: "token not found",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid token type",
|
|
wantErr: true,
|
|
tokenRequest: &CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeResetPassword,
|
|
},
|
|
},
|
|
{
|
|
name: "valid token",
|
|
tokenRequest: &CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeRecoveryApproved,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tokenID := "test-token-not-found"
|
|
|
|
if tc.tokenRequest != nil {
|
|
token, err := srv.Auth().newUserToken(*tc.tokenRequest)
|
|
require.NoError(t, err)
|
|
_, err = srv.Auth().Identity.CreateUserToken(context.Background(), token)
|
|
require.NoError(t, err)
|
|
|
|
tokenID = token.GetName()
|
|
}
|
|
|
|
res, err := srv.Auth().GetMFADevices(ctx, &proto.GetMFADevicesRequest{
|
|
TokenID: tokenID,
|
|
})
|
|
|
|
switch {
|
|
case tc.wantErr:
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
default:
|
|
require.NoError(t, err)
|
|
compareDevices(t, res.GetDevices(), webDev.MFA, totpDev.MFA)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMFADevices_WithAuth(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
srv := newTestTLSServer(t)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional,
|
|
U2F: &types.U2F{
|
|
AppID: "https://localhost",
|
|
Facets: []string{"https://localhost"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
username := "llama@goteleport.com"
|
|
_, _, err = CreateUserAndRole(srv.Auth(), username, []string{username})
|
|
require.NoError(t, err)
|
|
|
|
clt, err := srv.NewClient(TestUser(username))
|
|
require.NoError(t, err)
|
|
webDev, err := RegisterTestDevice(ctx, clt, "web", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
|
|
require.NoError(t, err)
|
|
totpDev, err := RegisterTestDevice(ctx, clt, "otp", proto.DeviceType_DEVICE_TYPE_TOTP, webDev, WithTestDeviceClock(srv.Clock()))
|
|
require.NoError(t, err)
|
|
|
|
res, err := clt.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
|
|
require.NoError(t, err)
|
|
compareDevices(t, res.GetDevices(), webDev.MFA, totpDev.MFA)
|
|
}
|
|
|
|
func newTestServices(t *testing.T) Services {
|
|
bk, err := memory.New(memory.Config{})
|
|
require.NoError(t, err)
|
|
|
|
configService, err := local.NewClusterConfigurationService(bk)
|
|
require.NoError(t, err)
|
|
|
|
return Services{
|
|
Trust: local.NewCAService(bk),
|
|
Presence: local.NewPresenceService(bk),
|
|
Provisioner: local.NewProvisioningService(bk),
|
|
Identity: local.NewIdentityService(bk),
|
|
Access: local.NewAccessService(bk),
|
|
DynamicAccessExt: local.NewDynamicAccessService(bk),
|
|
ClusterConfiguration: configService,
|
|
Events: local.NewEventsService(bk),
|
|
IAuditLog: events.NewDiscardAuditLog(),
|
|
}
|
|
}
|
|
|
|
func compareDevices(t *testing.T, got []*types.MFADevice, want ...*types.MFADevice) {
|
|
sort.Slice(got, func(i, j int) bool { return got[i].GetName() < got[j].GetName() })
|
|
sort.Slice(want, func(i, j int) bool { return want[i].GetName() < want[j].GetName() })
|
|
|
|
// Remove TOTP keys before comparison.
|
|
for _, w := range want {
|
|
totp := w.GetTotp()
|
|
if totp == nil {
|
|
continue
|
|
}
|
|
if totp.Key == "" {
|
|
continue
|
|
}
|
|
key := totp.Key
|
|
// defer in loop on purpose, we want this to run at the end of the function.
|
|
defer func() {
|
|
totp.Key = key
|
|
}()
|
|
totp.Key = ""
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("compareDevices mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|