mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:04:33 +08:00
Adds `UpsertCertAuthority` to the `teleport.v1.trust.TrustService` and deprecates the HTTP api. The `services.Trust` interface was also updated to include a `context.Context` in both `CreateCertAuthority` and `UpsertCertAuthority. To ensure backwards compatibility the `auth.Client` will first attempt to use the gRPC client and if that fails with a not implemented error it will fallback to using HTTP. Part of #6394
2938 lines
89 KiB
Go
2938 lines
89 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/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
mathrand "math/rand"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/license"
|
|
reporting "github.com/gravitational/reporting/types"
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"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/types/installers"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth/keystore"
|
|
"github.com/gravitational/teleport/lib/auth/native"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/authz"
|
|
"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/events/eventstest"
|
|
"github.com/gravitational/teleport/lib/fixtures"
|
|
"github.com/gravitational/teleport/lib/modules"
|
|
"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"
|
|
)
|
|
|
|
type testPack struct {
|
|
bk backend.Backend
|
|
clusterName types.ClusterName
|
|
a *Server
|
|
mockEmitter *eventstest.MockEmitter
|
|
}
|
|
|
|
func newTestPack(
|
|
ctx context.Context, dataDir string, opts ...ServerOption,
|
|
) (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)
|
|
}
|
|
|
|
p.mockEmitter = &eventstest.MockEmitter{}
|
|
authConfig := &InitConfig{
|
|
Backend: p.bk,
|
|
ClusterName: p.clusterName,
|
|
Authority: testauthority.New(),
|
|
Emitter: p.mockEmitter,
|
|
SkipPeriodicOperations: true,
|
|
KeyStoreConfig: keystore.Config{
|
|
Software: keystore.SoftwareConfig{
|
|
RSAKeyPairSource: testauthority.New().GenerateKeyPair,
|
|
},
|
|
},
|
|
}
|
|
p.a, err = NewServer(authConfig, opts...)
|
|
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(ctx, suite.NewTestCA(types.UserCA, p.clusterName.GetClusterName())); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.UpsertCertAuthority(ctx, suite.NewTestCA(types.HostCA, p.clusterName.GetClusterName())); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if err := p.a.UpsertCertAuthority(ctx, suite.NewTestCA(types.OpenSSHCA, p.clusterName.GetClusterName())); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
if err := p.a.UpsertNamespace(types.DefaultNamespace()); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
|
|
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()
|
|
native.PrecomputeTestKeys(m)
|
|
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(ctx, AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, _, err = CreateUserAndRole(s.a, user, []string{user}, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
ws, err := s.a.AuthenticateWebUser(ctx, 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(ctx, 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}, nil)
|
|
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(ctx, 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, teleport.SSHSessionJoinPrincipal})
|
|
// 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, teleport.SSHSessionJoinPrincipal},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyNone,
|
|
}
|
|
gotID, err := tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, wantID, *gotID)
|
|
|
|
// Login to the leaf cluster.
|
|
resp, err = s.a.AuthenticateSSHUser(ctx, 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, teleport.SSHSessionJoinPrincipal},
|
|
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(),
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyNone,
|
|
}
|
|
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.
|
|
kubeCluster, err := types.NewKubernetesClusterV3(
|
|
types.Metadata{
|
|
Name: "root-kube-cluster",
|
|
},
|
|
types.KubernetesClusterSpecV3{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
kubeServer, err := types.NewKubernetesServerV3FromCluster(kubeCluster, "host", "uuid")
|
|
require.NoError(t, err)
|
|
_, err = s.a.UpsertKubernetesServer(ctx, kubeServer)
|
|
require.NoError(t, err)
|
|
|
|
// Login specifying a valid kube cluster. It should appear in the TLS cert.
|
|
resp, err = s.a.AuthenticateSSHUser(ctx, 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, teleport.SSHSessionJoinPrincipal},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyNone,
|
|
}
|
|
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(ctx, 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, teleport.SSHSessionJoinPrincipal},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyNone,
|
|
}
|
|
gotID, err = tlsca.FromSubject(gotTLSCert.Subject, gotTLSCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, *gotID, wantID)
|
|
|
|
// Login specifying a valid kube cluster. It should appear in the TLS cert.
|
|
resp, err = s.a.AuthenticateSSHUser(ctx, 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, teleport.SSHSessionJoinPrincipal},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyNone,
|
|
}
|
|
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(ctx, 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, teleport.SSHSessionJoinPrincipal},
|
|
KubernetesUsers: []string{user},
|
|
KubernetesGroups: []string{"system:masters"},
|
|
KubernetesCluster: "root-kube-cluster",
|
|
Expires: gotTLSCert.NotAfter,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
TeleportCluster: s.clusterName.GetClusterName(),
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyNone,
|
|
}
|
|
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(ctx, 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)
|
|
ctx := context.Background()
|
|
|
|
username := "user1"
|
|
pass := []byte("abc123")
|
|
|
|
_, err := s.a.AuthenticateWebUser(ctx, AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, _, err = CreateUserAndRole(s.a, username, []string{username}, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertPassword(username, pass)
|
|
require.NoError(t, err)
|
|
|
|
// successful log in
|
|
ws, err := s.a.AuthenticateWebUser(ctx, 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(ctx, AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: []byte("wrong pass")},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
user, err := s.a.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(ctx, AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{Password: pass},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func requireTokenExpiry(t *testing.T, token types.ProvisionToken, expectExpiry time.Duration) {
|
|
t.Helper()
|
|
actualTTL := time.Until(token.Expiry())
|
|
diff := actualTTL - expectExpiry
|
|
require.True(
|
|
t,
|
|
diff <= time.Minute && diff >= (-1*time.Minute),
|
|
"Token TTL should be within one minute of the desired TTL",
|
|
)
|
|
}
|
|
|
|
func TestTokensCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
ctx := context.Background()
|
|
|
|
t.Run("GenerateToken: default TTL", func(t *testing.T) {
|
|
tokenName, err := s.a.GenerateToken(ctx, &proto.GenerateTokenRequest{
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, tokenName, 2*TokenLenBytes)
|
|
|
|
// Ensure GetTokens returns token
|
|
tokens, err := s.a.GetTokens(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, tokens, 1)
|
|
require.Equal(t, tokens[0].GetName(), tokenName)
|
|
|
|
tokenResource, err := s.a.ValidateToken(ctx, tokenName)
|
|
require.NoError(t, err)
|
|
roles := tokenResource.GetRoles()
|
|
require.True(t, roles.Include(types.RoleNode))
|
|
require.False(t, roles.Include(types.RoleProxy))
|
|
// Check that GenerateToken applies a default TTL
|
|
requireTokenExpiry(t, tokenResource, defaults.ProvisioningTokenTTL)
|
|
})
|
|
|
|
t.Run("GenerateToken: defined TTL", func(t *testing.T) {
|
|
// generate persistent token with defined TTL
|
|
desiredTTL := 6 * time.Hour
|
|
tokenName, err := s.a.GenerateToken(ctx, &proto.GenerateTokenRequest{
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
TTL: proto.Duration(desiredTTL),
|
|
})
|
|
require.NoError(t, err)
|
|
token, err := s.a.GetToken(ctx, tokenName)
|
|
require.NoError(t, err)
|
|
requireTokenExpiry(t, token, desiredTTL)
|
|
require.NoError(t, s.a.DeleteToken(ctx, tokenName))
|
|
})
|
|
|
|
t.Run("GenerateToken: defined token name", func(t *testing.T) {
|
|
customToken := "custom-token"
|
|
tokenName, err := s.a.GenerateToken(ctx, &proto.GenerateTokenRequest{
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
Token: customToken,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tokenName, customToken)
|
|
token, err := s.a.ValidateToken(ctx, tokenName)
|
|
require.NoError(t, err)
|
|
roles := token.GetRoles()
|
|
require.True(t, roles.Include(types.RoleNode))
|
|
require.False(t, roles.Include(types.RoleProxy))
|
|
err = s.a.DeleteToken(ctx, customToken)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
// TODO(strideynet): In Teleport 14.0.0 when GenerateToken is removed
|
|
// break this SetStaticTokens test out into its own test as the rest of
|
|
// this test is removed.
|
|
t.Run("SetStaticTokens", func(t *testing.T) {
|
|
// 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)
|
|
token, err := s.a.ValidateToken(ctx, "static-token-value")
|
|
require.NoError(t, err)
|
|
fetchesRoles := token.GetRoles()
|
|
require.Equal(t, fetchesRoles, roles)
|
|
})
|
|
}
|
|
|
|
type tokenCreatorAndDeleter interface {
|
|
CreateToken(ctx context.Context, token types.ProvisionToken) error
|
|
DeleteToken(ctx context.Context, token string) error
|
|
}
|
|
|
|
func generateTestToken(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
roles types.SystemRoles,
|
|
expires time.Time,
|
|
auth tokenCreatorAndDeleter,
|
|
) string {
|
|
t.Helper()
|
|
token, err := utils.CryptoRandomHex(TokenLenBytes)
|
|
require.NoError(t, err)
|
|
|
|
pt, err := types.NewProvisionToken(token, roles, expires)
|
|
require.NoError(t, err)
|
|
require.NoError(t, auth.CreateToken(ctx, pt))
|
|
|
|
return token
|
|
}
|
|
|
|
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 := generateTestToken(
|
|
ctx, t,
|
|
types.SystemRoles{types.RoleNode},
|
|
time.Time{},
|
|
s.a,
|
|
)
|
|
tampered := string(tok[0]+1) + tok[1:]
|
|
_, err = s.a.ValidateToken(ctx, tampered)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestLocalControlStream verifies that local control stream behaves as expected.
|
|
func TestLocalControlStream(t *testing.T) {
|
|
const serverID = "test-server"
|
|
|
|
t.Parallel()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
s := newAuthSuite(t)
|
|
|
|
stream := s.a.MakeLocalInventoryControlStream()
|
|
defer stream.Close()
|
|
|
|
err := stream.Send(ctx, proto.UpstreamInventoryHello{
|
|
ServerID: serverID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case msg := <-stream.Recv():
|
|
_, ok := msg.(proto.DownstreamInventoryHello)
|
|
require.True(t, ok)
|
|
case <-stream.Done():
|
|
t.Fatalf("stream closed unexpectedly: %v", stream.Error())
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatal("timeout waiting for downstream hello")
|
|
}
|
|
|
|
// wait for control stream to get inserted into the controller (happens after
|
|
// hello exchange is finished).
|
|
require.Eventually(t, func() bool {
|
|
_, ok := s.a.inventory.GetControlStream(serverID)
|
|
return ok
|
|
}, time.Second*5, time.Millisecond*200)
|
|
|
|
// try performing a normal operation against the control stream to double-check that it is healthy
|
|
go s.a.PingInventory(ctx, proto.InventoryPingRequest{
|
|
ServerID: serverID,
|
|
})
|
|
|
|
select {
|
|
case msg := <-stream.Recv():
|
|
_, ok := msg.(proto.DownstreamInventoryPing)
|
|
require.True(t, ok)
|
|
case <-stream.Done():
|
|
t.Fatalf("stream closed unexpectedly: %v", stream.Error())
|
|
case <-time.After(time.Second * 10):
|
|
t.Fatal("timeout waiting for downstream hello")
|
|
}
|
|
}
|
|
|
|
func TestGenerateTokenEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
// test trusted cluster token emit
|
|
_, err := s.a.GenerateToken(ctx, &proto.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, &proto.GenerateTokenRequest{Roles: types.SystemRoles{
|
|
types.RoleNode,
|
|
types.RoleTrustedCluster,
|
|
types.RoleAuth,
|
|
}})
|
|
require.NoError(t, err)
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.TrustedClusterTokenCreateEvent)
|
|
}
|
|
|
|
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: testauthority.New(),
|
|
SkipPeriodicOperations: true,
|
|
KeyStoreConfig: keystore.Config{
|
|
Software: keystore.SoftwareConfig{
|
|
RSAKeyPairSource: testauthority.New().GenerateKeyPair,
|
|
},
|
|
},
|
|
}
|
|
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 user, 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)
|
|
// use the UpsertTrustedCluster in Uncached as we just want the resource in
|
|
// the backend, we don't want to actually connect
|
|
_, err = s.a.Services.UpsertTrustedCluster(ctx, tc)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.a.UpsertCertAuthority(ctx, suite.NewTestCA(types.UserCA, "test")))
|
|
require.NoError(t, s.a.UpsertCertAuthority(ctx, 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{
|
|
TeamsToLogins: []types.TeamMapping{
|
|
{
|
|
Organization: "octocats",
|
|
Team: "dummy",
|
|
Logins: []string{"dummy"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
err = s.a.upsertGithubConnector(ctx, github)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.GithubConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
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.IsType(t, &apievents.GithubConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
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.IsType(t, &apievents.GithubConnectorDelete{}, s.mockEmitter.LastEvent())
|
|
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",
|
|
ClaimsToRoles: []types.ClaimMapping{
|
|
{
|
|
Claim: "dummy",
|
|
Value: "dummy",
|
|
Roles: []string{"dummy"},
|
|
},
|
|
},
|
|
RedirectURLs: []string{"https://proxy.example.com/v1/webapi/oidc/callback"},
|
|
})
|
|
require.NoError(t, err)
|
|
err = s.a.UpsertOIDCConnector(ctx, oidc)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.OIDCConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
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.IsType(t, &apievents.OIDCConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
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.IsType(t, &apievents.OIDCConnectorDelete{}, s.mockEmitter.LastEvent())
|
|
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)
|
|
|
|
// SAML connector validation requires the roles in mappings exist.
|
|
role, err := types.NewRole("dummy", types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
err = s.a.CreateRole(ctx, role)
|
|
require.NoError(t, err)
|
|
|
|
// test saml create
|
|
saml, err := types.NewSAMLConnector("test", types.SAMLConnectorSpecV2{
|
|
AssertionConsumerService: "a",
|
|
Issuer: "b",
|
|
SSO: "c",
|
|
AttributesToRoles: []types.AttributeMapping{
|
|
{
|
|
Name: "dummy",
|
|
Value: "dummy",
|
|
Roles: []string{role.GetName()},
|
|
},
|
|
},
|
|
Cert: string(certBytes),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertSAMLConnector(ctx, saml)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.SAMLConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
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.IsType(t, &apievents.SAMLConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
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.IsType(t, &apievents.SAMLConnectorDelete{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, s.mockEmitter.LastEvent().GetType(), events.SAMLConnectorDeletedEvent)
|
|
}
|
|
|
|
func TestEmitSSOLoginFailureEvent(t *testing.T) {
|
|
mockE := &eventstest.MockEmitter{}
|
|
|
|
emitSSOLoginFailureEvent(context.Background(), mockE, "test", trace.BadParameter("some error"), false)
|
|
|
|
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",
|
|
},
|
|
})
|
|
|
|
emitSSOLoginFailureEvent(context.Background(), mockE, "test", trace.BadParameter("some error"), true)
|
|
|
|
require.Equal(t, mockE.LastEvent(), &apievents.UserLogin{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.UserLoginEvent,
|
|
Code: events.UserSSOTestFlowLoginFailureCode,
|
|
},
|
|
Method: "test",
|
|
Status: apievents.Status{
|
|
Success: false,
|
|
Error: "some error",
|
|
UserMessage: "some error",
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestServer_AugmentContextUserCertificates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
ctx := context.Background()
|
|
|
|
const username = "llama"
|
|
const pass = "secret!!1!"
|
|
|
|
// Use a >1 list of principals.
|
|
// This is enough to cause ordering issues between the TLS and SSH principal
|
|
// lists, which caused a bug in the device trust preview.
|
|
principals := []string{"login0", username, "-teleport-internal-join"}
|
|
|
|
// Prepare the user to test with.
|
|
_, _, err := CreateUserAndRole(authServer, username, principals, nil)
|
|
require.NoError(t, err, "CreateUserAndRole failed")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(username, []byte(pass)),
|
|
"UpsertPassword failed")
|
|
|
|
// Authenticate and create certificates.
|
|
_, pub, err := testauthority.New().GetNewKeyPairFromPool()
|
|
require.NoError(t, err, "GetNewKeyPairFromPool failed")
|
|
authResp, err := authServer.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &PassCreds{
|
|
Password: []byte(pass),
|
|
},
|
|
PublicKey: pub,
|
|
},
|
|
TTL: 1 * time.Hour,
|
|
})
|
|
require.NoError(t, err, "AuthenticateSSHUser failed")
|
|
|
|
const devID = "deviceid1"
|
|
const devTag = "devicetag1"
|
|
const devCred = "devicecred1"
|
|
|
|
advanceClock := func(d time.Duration) {
|
|
if fc, ok := testServer.Clock().(clockwork.FakeClock); ok {
|
|
fc.Advance(d)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
x509PEM []byte
|
|
opts *AugmentUserCertificateOpts
|
|
wantSSHCert bool
|
|
assertX509Cert func(t *testing.T, c *x509.Certificate)
|
|
assertSSHCert func(t *testing.T, c *ssh.Certificate)
|
|
}{
|
|
{
|
|
name: "device extensions",
|
|
x509PEM: authResp.TLSCert,
|
|
opts: &AugmentUserCertificateOpts{
|
|
SSHAuthorizedKey: authResp.Cert,
|
|
DeviceExtensions: &DeviceExtensions{
|
|
DeviceID: devID,
|
|
AssetTag: devTag,
|
|
CredentialID: devCred,
|
|
},
|
|
},
|
|
wantSSHCert: true,
|
|
assertX509Cert: func(t *testing.T, c *x509.Certificate) {
|
|
id, err := tlsca.FromSubject(c.Subject, c.NotAfter)
|
|
require.NoError(t, err, "FromSubject failed")
|
|
assert.Equal(t, devID, id.DeviceExtensions.DeviceID, "DeviceID mismatch")
|
|
assert.Equal(t, devTag, id.DeviceExtensions.AssetTag, "AssetTag mismatch")
|
|
assert.Equal(t, devCred, id.DeviceExtensions.CredentialID, "CredentialID mismatch")
|
|
},
|
|
assertSSHCert: func(t *testing.T, c *ssh.Certificate) {
|
|
assert.Equal(t, devID, c.Extensions[teleport.CertExtensionDeviceID], "DeviceID mismatch")
|
|
assert.Equal(t, devTag, c.Extensions[teleport.CertExtensionDeviceAssetTag], "AssetTag mismatch")
|
|
assert.Equal(t, devCred, c.Extensions[teleport.CertExtensionDeviceCredentialID], "CredentialID mismatch")
|
|
},
|
|
},
|
|
{
|
|
name: "augment without SSH",
|
|
x509PEM: authResp.TLSCert,
|
|
opts: &AugmentUserCertificateOpts{
|
|
DeviceExtensions: &DeviceExtensions{
|
|
DeviceID: devID,
|
|
AssetTag: devTag,
|
|
CredentialID: devCred,
|
|
},
|
|
},
|
|
// Nothing to assert, we are just looking for the absence of errors here.
|
|
assertX509Cert: func(t *testing.T, c *x509.Certificate) {},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
xCert, identity := parseX509PEMAndIdentity(t, test.x509PEM)
|
|
|
|
// Prepare ctx and authz.Context.
|
|
ctx = authz.ContextWithUserCertificate(ctx, xCert)
|
|
ctx = authz.ContextWithUser(ctx, authz.LocalUser{
|
|
Username: username,
|
|
Identity: *identity,
|
|
})
|
|
authCtx, err := testServer.APIConfig.Authorizer.Authorize(ctx)
|
|
require.NoError(t, err, "Authorize failed")
|
|
|
|
// Advance time before issuing new certs. This makes timestamp checks
|
|
// effective under fake clocks.
|
|
// 1m is enough to make tests fail if the timestamps aren't correct.
|
|
advanceClock(1 * time.Minute)
|
|
validAfter := testServer.Clock().Now().UTC().Add(-61 * time.Second)
|
|
|
|
// Test!
|
|
certs, err := authServer.AugmentContextUserCertificates(ctx, authCtx, test.opts)
|
|
require.NoError(t, err, "AugmentContextUserCertificates failed")
|
|
require.NotNil(t, certs, "AugmentContextUserCertificates returned nil certs")
|
|
|
|
// Assert X.509 certificate.
|
|
newXCert, _ := parseX509PEMAndIdentity(t, certs.TLS)
|
|
test.assertX509Cert(t, newXCert)
|
|
assert.True(t,
|
|
validAfter.Before(newXCert.NotBefore),
|
|
"got newXCert.NotBefore = %v, want > %v", newXCert.NotBefore, validAfter)
|
|
assert.Equal(t, xCert.NotAfter, newXCert.NotAfter, "newXCert.NotAfter mismatch")
|
|
|
|
// Assert SSH certificate.
|
|
if test.wantSSHCert && len(certs.SSH) == 0 {
|
|
t.Errorf("AugmentContextUserCertificates returned no SSH certificate")
|
|
} else if !test.wantSSHCert {
|
|
return
|
|
}
|
|
newSSHCert, err := sshutils.ParseCertificate(certs.SSH)
|
|
require.NoError(t, err, "ParseCertificate failed")
|
|
test.assertSSHCert(t, newSSHCert)
|
|
assert.True(t,
|
|
uint64(validAfter.Unix()) < newSSHCert.ValidAfter,
|
|
"got newSSHCert.ValidAfter = %v, want > %v", newSSHCert.ValidAfter, validAfter.Unix())
|
|
assert.Equal(t, uint64(xCert.NotAfter.Unix()), newSSHCert.ValidBefore, "newSSHCert.ValidBefore mismatch")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_AugmentContextUserCertificates_errors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
ctx := context.Background()
|
|
|
|
const pass1 = "secret!!1!"
|
|
const pass2 = "secret!!2!"
|
|
const pass3 = "secret!!3!"
|
|
|
|
// Prepare a few distinct users.
|
|
user1, _, err := CreateUserAndRole(authServer, "llama", []string{"llama"}, nil)
|
|
require.NoError(t, err, "CreateUserAndRole failed")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(user1.GetName(), []byte(pass1)),
|
|
"UpsertPassword failed")
|
|
|
|
user2, _, err := CreateUserAndRole(authServer, "alpaca", []string{"alpaca"}, nil)
|
|
require.NoError(t, err, "CreateUserAndRole failed")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(user2.GetName(), []byte(pass2)),
|
|
"UpsertPassword failed")
|
|
|
|
user3, _, err := CreateUserAndRole(authServer, "camel", []string{"camel"}, nil)
|
|
require.NoError(t, err, "CreateUserAndRole failed")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(user3.GetName(), []byte(pass3)),
|
|
"UpsertPassword failed")
|
|
|
|
// authenticate authenticates the specified user, creating a new key pair, a
|
|
// new pair of certificates, and parsing all relevant responses.
|
|
authenticate := func(t *testing.T, user, pass string) (tlsRaw, sshRaw []byte, xCert *x509.Certificate, sshCert *ssh.Certificate, identity *tlsca.Identity) {
|
|
// Avoid using recycled keys here, otherwise the test may flake.
|
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, "GenerateKey failed")
|
|
sPubKey, err := ssh.NewPublicKey(privKey.Public())
|
|
require.NoError(t, err, "NewPublicKey failed")
|
|
|
|
authResp, err := authServer.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{
|
|
Password: []byte(pass),
|
|
},
|
|
PublicKey: ssh.MarshalAuthorizedKey(sPubKey),
|
|
},
|
|
TTL: 1 * time.Hour,
|
|
})
|
|
require.NoError(t, err, "AuthenticateSSHUser(%q) failed", user)
|
|
|
|
xCert, identity = parseX509PEMAndIdentity(t, authResp.TLSCert)
|
|
// parseX509PEMAndIdentity reports errors via t.
|
|
|
|
sshCert, err = sshutils.ParseCertificate(authResp.Cert)
|
|
require.NoError(t, err, "ParseCertificate failed")
|
|
|
|
return authResp.TLSCert, authResp.Cert, xCert, sshCert, identity
|
|
}
|
|
|
|
// Authenticate.
|
|
// user1 covers most of the tests.
|
|
// user2 is mainly used to test mismatched certificates against user1.
|
|
// user3 is used to test user locks.
|
|
_, sshRaw1, xCert1, sshCert1, identity1 := authenticate(t, user1.GetName(), pass1)
|
|
_, sshRaw2, xCert2, _, _ := authenticate(t, user2.GetName(), pass2)
|
|
_, _, xCert3, _, identity3 := authenticate(t, user3.GetName(), pass3)
|
|
|
|
// sshRaw11 is identical to sshRaw1, except it is backed by a different
|
|
// key pair.
|
|
_, sshRaw11, _, _, _ := authenticate(t, user1.GetName(), pass1)
|
|
|
|
// wrongKey is used to represent an invalid/unknown CA.
|
|
wrongKey, err := rsa.GenerateKey(rand.Reader, 2048 /* bits */)
|
|
require.NoError(t, err, "GenerateKey failed")
|
|
|
|
// Build an invalid version of xCert1 (signed using wrongKey).
|
|
userCA, err := authServer.GetCertAuthority(ctx, types.CertAuthID{
|
|
Type: types.UserCA,
|
|
DomainName: testServer.ClusterName(),
|
|
}, true /* loadKeys */)
|
|
require.NoError(t, err, "GetCertAuthority failed")
|
|
caXPEM := userCA.GetActiveKeys().TLS[0].Cert
|
|
|
|
caXCert, _ := parseX509PEMAndIdentity(t, caXPEM)
|
|
caXCert.PublicKey = wrongKey.Public()
|
|
wrongXCert1DER, err := x509.CreateCertificate(rand.Reader, xCert1, caXCert, xCert1.PublicKey, wrongKey)
|
|
require.NoError(t, err, "CreateCertificate failed")
|
|
wrongXCert1, err := x509.ParseCertificate(wrongXCert1DER)
|
|
require.NoError(t, err, "ParseCertificate failed")
|
|
|
|
// Build an invalid version of sshCert1 (signed using wrongKey).
|
|
sshSigner, err := ssh.NewSignerFromKey(wrongKey)
|
|
require.NoError(t, err, "NewSignerFromKey failed")
|
|
wrongSSHCert1, err := sshutils.ParseCertificate(sshRaw1)
|
|
require.NoError(t, err, "ParseCertificate failed")
|
|
require.NoError(t, wrongSSHCert1.SignCert(rand.Reader, sshSigner), "SignCert failed")
|
|
wrongSSHRaw1 := ssh.MarshalAuthorizedKey(wrongSSHCert1)
|
|
|
|
// Issue augmented certs for user1.
|
|
// Used to test that re-issue of augmented certs is not allowed.
|
|
ctxFromAuthorize := testServer.APIConfig.Authorizer.Authorize
|
|
aCtx := authz.ContextWithUserCertificate(context.Background(), xCert1)
|
|
aCtx = authz.ContextWithUser(aCtx, authz.LocalUser{
|
|
Username: identity1.Username,
|
|
Identity: *identity1,
|
|
})
|
|
aaCtx, err := ctxFromAuthorize(aCtx)
|
|
require.NoError(t, err, "ctxFromAuthorize failed")
|
|
augResp, err := authServer.AugmentContextUserCertificates(aCtx, aaCtx, &AugmentUserCertificateOpts{
|
|
SSHAuthorizedKey: sshRaw1,
|
|
DeviceExtensions: &DeviceExtensions{
|
|
DeviceID: "device1",
|
|
AssetTag: "tag1",
|
|
CredentialID: "credential1",
|
|
},
|
|
})
|
|
require.NoError(t, err, "AugmentContextUserCertificates failed")
|
|
augCert1, augIdentity1 := parseX509PEMAndIdentity(t, augResp.TLS)
|
|
augSSHRaw1 := augResp.SSH
|
|
|
|
// signAndMarshalSSH is used to create variations of SSH certificates signed
|
|
// by the Teleport CA.
|
|
signAndMarshalSSH := func(t *testing.T, c *ssh.Certificate) (sshRaw []byte) {
|
|
signer, err := authServer.GetKeyStore().GetSSHSigner(ctx, userCA)
|
|
require.NoError(t, err, "GetSSHSigner failed")
|
|
|
|
err = c.SignCert(rand.Reader, signer)
|
|
require.NoError(t, err, "SignCert failed")
|
|
|
|
return ssh.MarshalAuthorizedKey(c)
|
|
}
|
|
|
|
baseOpts := &AugmentUserCertificateOpts{
|
|
DeviceExtensions: &DeviceExtensions{
|
|
DeviceID: "deviceid1",
|
|
AssetTag: "devicetag1",
|
|
CredentialID: "credentialid1",
|
|
},
|
|
}
|
|
optsFromBase := func(_ *testing.T) *AugmentUserCertificateOpts { return baseOpts }
|
|
|
|
tests := []struct {
|
|
name string
|
|
x509Cert *x509.Certificate
|
|
identity *tlsca.Identity
|
|
// createAuthCtx defaults to ctxFromAuthorize.
|
|
createAuthCtx func(ctx context.Context) (*authz.Context, error)
|
|
// createOpts defaults to optsFromBase.
|
|
createOpts func(t *testing.T) *AugmentUserCertificateOpts
|
|
wantErr string
|
|
}{
|
|
// Simple input validation errors.
|
|
{
|
|
name: "authCtx nil",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createAuthCtx: func(ctx context.Context) (*authz.Context, error) { return nil, nil },
|
|
wantErr: "authCtx",
|
|
},
|
|
{
|
|
name: "opts nil",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts { return nil },
|
|
wantErr: "opts",
|
|
},
|
|
{
|
|
name: "opts missing extensions",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.DeviceExtensions = nil
|
|
return &cp
|
|
},
|
|
wantErr: "at least one opts extension",
|
|
},
|
|
|
|
// DeviceExtensions.
|
|
{
|
|
name: "opts.DeviceExtensions.DeviceID empty",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.DeviceExtensions = &DeviceExtensions{
|
|
DeviceID: "",
|
|
AssetTag: "asset1",
|
|
CredentialID: "credential1",
|
|
}
|
|
return &cp
|
|
},
|
|
wantErr: "DeviceID",
|
|
},
|
|
{
|
|
name: "opts.DeviceExtensions.AssetTag empty",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.DeviceExtensions = &DeviceExtensions{
|
|
DeviceID: "id1",
|
|
AssetTag: "",
|
|
CredentialID: "credential1",
|
|
}
|
|
return &cp
|
|
},
|
|
wantErr: "AssetTag",
|
|
},
|
|
{
|
|
name: "opts.DeviceExtensions.CredentialID empty",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.DeviceExtensions = &DeviceExtensions{
|
|
DeviceID: "id1",
|
|
AssetTag: "asset1",
|
|
CredentialID: "",
|
|
}
|
|
return &cp
|
|
},
|
|
wantErr: "CredentialID",
|
|
},
|
|
|
|
// Identity and certificate mismatch scenarios.
|
|
{
|
|
name: "x509/identity mismatch",
|
|
x509Cert: xCert2, // should be xCert1
|
|
identity: identity1,
|
|
wantErr: "x509 user mismatch",
|
|
},
|
|
{
|
|
name: "x509/SSH public key mismatch",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = sshRaw11 // should be sshRaw1
|
|
return &cp
|
|
},
|
|
wantErr: "public key mismatch",
|
|
},
|
|
{
|
|
name: "SSH/identity mismatch",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = sshRaw2 // should be sshRaw1
|
|
return &cp
|
|
},
|
|
wantErr: "SSH user mismatch",
|
|
},
|
|
{
|
|
name: "SSH/identity principals mismatch",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(t *testing.T) *AugmentUserCertificateOpts {
|
|
changedPrincipals := *sshCert1
|
|
changedPrincipals.ValidPrincipals = append(changedPrincipals.ValidPrincipals, "camel")
|
|
sshRaw := signAndMarshalSSH(t, &changedPrincipals)
|
|
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = sshRaw
|
|
return &cp
|
|
},
|
|
wantErr: "principals mismatch",
|
|
},
|
|
{
|
|
name: "SSH cert type mismatch",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(t *testing.T) *AugmentUserCertificateOpts {
|
|
changedType := *sshCert1
|
|
changedType.CertType = ssh.HostCert // shouldn't happen!
|
|
sshRaw := signAndMarshalSSH(t, &changedType)
|
|
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = sshRaw
|
|
return &cp
|
|
},
|
|
wantErr: "cert type mismatch",
|
|
},
|
|
|
|
// Invalid certificates.
|
|
{
|
|
name: "x509 cert unknown authority",
|
|
x509Cert: wrongXCert1, // signed by a different CA
|
|
identity: identity1,
|
|
wantErr: "unknown authority",
|
|
},
|
|
{
|
|
name: "SSH cert unknown authority",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = wrongSSHRaw1 // signed by a different CA
|
|
return &cp
|
|
},
|
|
wantErr: "unknown authority",
|
|
},
|
|
{
|
|
name: "SSH cert expired",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(t *testing.T) *AugmentUserCertificateOpts {
|
|
// Fake a 1h TTL, expired cert.
|
|
now := testServer.Clock().Now()
|
|
after := now.Add(-1 * time.Hour)
|
|
before := now.Add(-1 * time.Minute)
|
|
|
|
expiredCert := *sshCert1
|
|
expiredCert.ValidAfter = uint64(after.Unix())
|
|
expiredCert.ValidBefore = uint64(before.Unix())
|
|
sshRaw := signAndMarshalSSH(t, &expiredCert)
|
|
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = sshRaw
|
|
return &cp
|
|
},
|
|
wantErr: "cert has expired",
|
|
},
|
|
|
|
// Certificates with existing extensions are not reissued.
|
|
{
|
|
name: "x509 cert with device extensions not reissued",
|
|
x509Cert: augCert1, // already has extensions
|
|
identity: augIdentity1, // already has extensions
|
|
wantErr: "extensions already present",
|
|
},
|
|
{
|
|
name: "SSH cert with device extensions not reissued",
|
|
x509Cert: xCert1,
|
|
identity: identity1,
|
|
createOpts: func(_ *testing.T) *AugmentUserCertificateOpts {
|
|
cp := *baseOpts
|
|
cp.SSHAuthorizedKey = augSSHRaw1 // already has extensions.
|
|
return &cp
|
|
},
|
|
wantErr: "extensions already present",
|
|
},
|
|
|
|
// Locks.
|
|
{
|
|
name: "locked user",
|
|
x509Cert: xCert3,
|
|
identity: identity3, // user3 locked below.
|
|
createAuthCtx: func(ctx context.Context) (*authz.Context, error) {
|
|
// Authorize user3...
|
|
authCtx, err := ctxFromAuthorize(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lockTarget := types.LockTarget{
|
|
User: user3.GetName(),
|
|
}
|
|
watcher, err := authServer.lockWatcher.Subscribe(ctx, lockTarget)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer watcher.Close()
|
|
|
|
// ...and lock them right after.
|
|
user3Lock, err := types.NewLock("user3-lock", types.LockSpecV2{
|
|
Target: lockTarget,
|
|
Message: "user locked",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := authServer.UpsertLock(ctx, user3Lock); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wait for the lock to propagate.
|
|
<-watcher.Events()
|
|
return authCtx, nil
|
|
},
|
|
wantErr: "user locked",
|
|
},
|
|
{
|
|
name: "locked device",
|
|
x509Cert: xCert1,
|
|
identity: identity1, // device locked below.
|
|
createOpts: func(t *testing.T) *AugmentUserCertificateOpts {
|
|
opts := &AugmentUserCertificateOpts{
|
|
DeviceExtensions: &DeviceExtensions{
|
|
DeviceID: "bad-device-1",
|
|
AssetTag: "bad-device-tag",
|
|
CredentialID: "bad-device-credential",
|
|
},
|
|
}
|
|
|
|
// Create a target matching the device device.
|
|
lockTarget := types.LockTarget{
|
|
Device: opts.DeviceExtensions.DeviceID,
|
|
}
|
|
watcher, err := authServer.lockWatcher.Subscribe(ctx, lockTarget)
|
|
require.NoError(t, err, "Subscribe failed")
|
|
defer watcher.Close()
|
|
|
|
// Lock the device before returning opts.
|
|
lock, err := types.NewLock("bad-device-lock", types.LockSpecV2{
|
|
Target: lockTarget,
|
|
Message: "device locked",
|
|
})
|
|
require.NoError(t, err, "NewLock failed")
|
|
require.NoError(t,
|
|
authServer.UpsertLock(ctx, lock),
|
|
"NewLock failed")
|
|
|
|
// Wait for the lock to propagate.
|
|
<-watcher.Events()
|
|
return opts
|
|
},
|
|
wantErr: "device locked",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.createAuthCtx == nil {
|
|
test.createAuthCtx = ctxFromAuthorize
|
|
}
|
|
if test.createOpts == nil {
|
|
test.createOpts = optsFromBase
|
|
}
|
|
|
|
// Prepare ctx and authz.Context.
|
|
ctx = authz.ContextWithUserCertificate(ctx, test.x509Cert)
|
|
ctx = authz.ContextWithUser(ctx, authz.LocalUser{
|
|
Username: test.identity.Username,
|
|
Identity: *test.identity,
|
|
})
|
|
authCtx, err := test.createAuthCtx(ctx)
|
|
require.NoError(t, err, "createAuthCtx failed")
|
|
|
|
// Test!
|
|
_, err = authServer.AugmentContextUserCertificates(ctx, authCtx, test.createOpts(t))
|
|
assert.ErrorContains(t, err, test.wantErr, "AugmentContextUserCertificates error mismatch")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateUserCertIPPinning(t *testing.T) {
|
|
modules.SetTestModules(t, &modules.TestModules{TestBuildType: modules.BuildEnterprise})
|
|
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
|
|
const pinnedUser = "pinnedUser"
|
|
const unpinnedUser = "unpinnedUser"
|
|
pass := []byte("abc123")
|
|
|
|
// Create the user without IP pinning
|
|
_, _, err := CreateUserAndRole(s.a, unpinnedUser, []string{unpinnedUser}, nil)
|
|
require.NoError(t, err)
|
|
err = s.a.UpsertPassword(unpinnedUser, pass)
|
|
require.NoError(t, err)
|
|
|
|
// Create the user with IP pinning enabled
|
|
_, pinnedRole, err := CreateUserAndRole(s.a, pinnedUser, []string{pinnedUser}, nil)
|
|
require.NoError(t, err)
|
|
err = s.a.UpsertPassword(pinnedUser, pass)
|
|
require.NoError(t, err)
|
|
options := pinnedRole.GetOptions()
|
|
options.PinSourceIP = true
|
|
pinnedRole.SetOptions(options)
|
|
|
|
keygen := testauthority.New()
|
|
_, pub, err := keygen.GetNewKeyPairFromPool()
|
|
require.NoError(t, err)
|
|
|
|
err = s.a.UpsertRole(ctx, pinnedRole)
|
|
require.NoError(t, err)
|
|
|
|
findTLSLoginIP := func(names []pkix.AttributeTypeAndValue) any {
|
|
for _, name := range names {
|
|
if name.Type.Equal(tlsca.LoginIPASN1ExtensionOID) {
|
|
return name.Value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
findTLSPinnedIP := func(names []pkix.AttributeTypeAndValue) any {
|
|
for _, name := range names {
|
|
if name.Type.Equal(tlsca.PinnedIPASN1ExtensionOID) {
|
|
return name.Value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
user string
|
|
loginIP string
|
|
wantPinned bool
|
|
}{
|
|
{desc: "no client ip, not pinned", user: unpinnedUser, loginIP: "", wantPinned: false},
|
|
{desc: "client ip, not pinned", user: unpinnedUser, loginIP: "1.2.3.4", wantPinned: false},
|
|
{desc: "client ip, pinned", user: pinnedUser, loginIP: "1.2.3.4", wantPinned: true},
|
|
{desc: "no client ip, pinned", user: pinnedUser, loginIP: "", wantPinned: true},
|
|
}
|
|
|
|
baseAuthRequest := AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Pass: &PassCreds{Password: pass},
|
|
PublicKey: pub,
|
|
},
|
|
TTL: time.Hour,
|
|
RouteToCluster: s.clusterName.GetClusterName(),
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
authRequest := baseAuthRequest
|
|
authRequest.AuthenticateUserRequest.Username = tt.user
|
|
if tt.loginIP != "" {
|
|
authRequest.ClientMetadata = &ForwardedClientMetadata{
|
|
RemoteAddr: tt.loginIP,
|
|
}
|
|
}
|
|
resp, err := s.a.AuthenticateSSHUser(ctx, authRequest)
|
|
if tt.wantPinned && tt.loginIP == "" {
|
|
require.ErrorContains(t, err, "source IP pinning is enabled but client IP is unknown")
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, resp.Username, tt.user)
|
|
|
|
sshCert, err := sshutils.ParseCertificate(resp.Cert)
|
|
require.NoError(t, err)
|
|
|
|
tlsCert, err := tlsca.ParseCertificatePEM(resp.TLSCert)
|
|
require.NoError(t, err)
|
|
|
|
tlsLoginIP := findTLSLoginIP(tlsCert.Subject.Names)
|
|
tlsPinnedIP := findTLSPinnedIP(tlsCert.Subject.Names)
|
|
sshLoginIP, sshLoginIPOK := sshCert.Extensions[teleport.CertExtensionLoginIP]
|
|
sshCriticalAddress, sshCriticalAddressOK := sshCert.CriticalOptions["source-address"]
|
|
|
|
if tt.loginIP != "" {
|
|
require.NotNil(t, tlsLoginIP, "client IP not found on TLS cert")
|
|
require.Equal(t, tlsLoginIP, tt.loginIP, "TLS LoginIP mismatch")
|
|
|
|
require.True(t, sshLoginIPOK, "SSH LoginIP extension not present")
|
|
require.Equal(t, tt.loginIP, sshLoginIP, "SSH LoginIP mismatch")
|
|
} else {
|
|
require.Nil(t, tlsLoginIP, "client IP unexpectedly found on TLS cert")
|
|
|
|
require.False(t, sshLoginIPOK, "client IP unexpectedly found on SSH cert")
|
|
}
|
|
|
|
if tt.wantPinned {
|
|
require.NotNil(t, tlsPinnedIP, "pinned IP not found on TLS cert")
|
|
require.Equal(t, tt.loginIP, tlsPinnedIP, "pinned IP on TLS cert mismatch")
|
|
|
|
require.True(t, sshCriticalAddressOK, "source address not found on SSH cert")
|
|
require.Equal(t, tt.loginIP+"/32", sshCriticalAddress, "SSH source address mismatch")
|
|
} else {
|
|
require.Nil(t, tlsPinnedIP, "pinned IP unexpectedly found on TLS cert")
|
|
|
|
require.False(t, sshCriticalAddressOK, "source address unexpectedly found on SSH cert")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func parseX509PEMAndIdentity(t *testing.T, rawPEM []byte) (*x509.Certificate, *tlsca.Identity) {
|
|
b, _ := pem.Decode(rawPEM)
|
|
require.NotNil(t, b, "Decode failed")
|
|
|
|
cert, err := x509.ParseCertificate(b.Bytes)
|
|
require.NoError(t, err, "ParseCertificate failed: %v", err)
|
|
|
|
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
|
|
require.NoError(t, err, "FromSubject failed: %v", err)
|
|
|
|
return cert, identity
|
|
}
|
|
|
|
func TestGenerateUserCertWithCertExtension(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{}, nil)
|
|
require.NoError(t, err)
|
|
|
|
extension := types.CertExtension{
|
|
Name: "abc",
|
|
Value: "cde",
|
|
Type: types.CertExtensionType_SSH,
|
|
Mode: types.CertExtensionMode_EXTENSION,
|
|
}
|
|
options := role.GetOptions()
|
|
options.CertExtensions = []*types.CertExtension{&extension}
|
|
role.SetOptions(options)
|
|
err = p.a.UpsertRole(ctx, role)
|
|
require.NoError(t, err)
|
|
|
|
accessInfo := services.AccessInfoFromUser(user)
|
|
accessChecker, err := services.NewAccessChecker(accessInfo, p.clusterName.GetClusterName(), p.a)
|
|
require.NoError(t, err)
|
|
|
|
keygen := testauthority.New()
|
|
_, pub, err := keygen.GetNewKeyPairFromPool()
|
|
require.NoError(t, err)
|
|
certReq := certRequest{
|
|
user: user,
|
|
checker: accessChecker,
|
|
publicKey: pub,
|
|
}
|
|
certs, err := p.a.generateUserCert(certReq)
|
|
require.NoError(t, err)
|
|
|
|
key, err := sshutils.ParseCertificate(certs.SSH)
|
|
require.NoError(t, err)
|
|
|
|
val, ok := key.Extensions[extension.Name]
|
|
require.True(t, ok)
|
|
require.Equal(t, extension.Value, val)
|
|
}
|
|
|
|
func TestGenerateOpenSSHCert(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// create keypair and sign with OpenSSH CA
|
|
user, _, err := CreateUserAndRole(p.a, "test-user", []string{}, nil)
|
|
require.NoError(t, err)
|
|
|
|
priv, err := native.GeneratePrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
reply, err := p.a.GenerateOpenSSHCert(ctx, &proto.OpenSSHCertRequest{
|
|
Username: user.GetName(),
|
|
PublicKey: priv.MarshalSSHPublicKey(),
|
|
TTL: proto.Duration(time.Hour),
|
|
Cluster: p.clusterName.GetClusterName(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// verify that returned cert is signed with OpenSSH CA
|
|
signedCert, err := sshutils.ParseCertificate(reply.Cert)
|
|
require.NoError(t, err)
|
|
|
|
ca, err := p.a.GetCertAuthority(
|
|
ctx,
|
|
types.CertAuthID{
|
|
Type: types.OpenSSHCA,
|
|
DomainName: p.clusterName.GetClusterName(),
|
|
},
|
|
false,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
keys := ca.GetActiveKeys().SSH
|
|
require.NotEmpty(t, keys)
|
|
caPubkey, _, _, _, err := ssh.ParseAuthorizedKey(keys[0].PublicKey)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, caPubkey.Marshal(), signedCert.SignatureKey.Marshal())
|
|
}
|
|
|
|
func TestGenerateUserCertWithLocks(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
user, _, err := CreateUserAndRole(p.a, "test-user", []string{}, nil)
|
|
require.NoError(t, err)
|
|
accessInfo := services.AccessInfoFromUser(user)
|
|
accessChecker, err := services.NewAccessChecker(accessInfo, p.clusterName.GetClusterName(), p.a)
|
|
require.NoError(t, err)
|
|
const mfaID = "test-mfa-id"
|
|
const requestID = "test-access-request"
|
|
const deviceID = "deviceid1"
|
|
keygen := testauthority.New()
|
|
_, pub, err := keygen.GetNewKeyPairFromPool()
|
|
require.NoError(t, err)
|
|
certReq := certRequest{
|
|
user: user,
|
|
checker: accessChecker,
|
|
mfaVerified: mfaID,
|
|
publicKey: pub,
|
|
activeRequests: services.RequestIDs{AccessRequests: []string{requestID}},
|
|
deviceExtensions: DeviceExtensions{
|
|
DeviceID: deviceID,
|
|
AssetTag: "assettag1",
|
|
CredentialID: "credentialid1",
|
|
},
|
|
}
|
|
_, err = p.a.generateUserCert(certReq)
|
|
require.NoError(t, err)
|
|
|
|
testTargets := append(
|
|
[]types.LockTarget{
|
|
{User: user.GetName()},
|
|
{MFADevice: mfaID},
|
|
{AccessRequest: requestID},
|
|
{Device: deviceID},
|
|
},
|
|
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(ctx, 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(ctx, 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(ctx, pub, hostID, "test-proxy", []string{}, p.clusterName.GetClusterName(), types.RoleProxy, time.Minute)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestNewWebSession(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// Set a web idle timeout.
|
|
duration := time.Duration(5) * time.Minute
|
|
cfg := types.DefaultClusterNetworkingConfig()
|
|
cfg.SetWebIdleTimeout(duration)
|
|
err = p.a.SetClusterNetworkingConfig(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
// Create a user.
|
|
user, _, err := CreateUserAndRole(p.a, "test-user", []string{"test-role"}, nil)
|
|
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(ctx, 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 := &eventstest.MockEmitter{}
|
|
srv.Auth().emitter = mockEmitter
|
|
|
|
username := "llama@goteleport.com"
|
|
_, _, err := CreateUserAndRole(srv.Auth(), username, []string{username}, nil)
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "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().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().Services.GetMFADevices(ctx, username, false)
|
|
require.NoError(t, err)
|
|
compareDevices(t, false /* ignoreUpdateAndCounter */, 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}, nil)
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "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().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()
|
|
|
|
webConfig := &types.Webauthn{
|
|
RPID: "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,
|
|
Webauthn: webConfig,
|
|
})
|
|
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: webConfig,
|
|
})
|
|
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,
|
|
Webauthn: webConfig,
|
|
})
|
|
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}, nil)
|
|
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().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 := &eventstest.MockEmitter{}
|
|
srv.Auth().emitter = mockEmitter
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "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().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: "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(), proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
require.NoError(t, err)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: privExToken.GetName(),
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: webauthnRes,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid device name length",
|
|
deviceName: strings.Repeat("A", mfaDeviceNameMaxLen+1),
|
|
wantErr: true,
|
|
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(), proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
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:
|
|
expectedErr := trace.IsAccessDenied(err) || trace.IsBadParameter(err)
|
|
require.True(t, expectedErr)
|
|
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,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "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}, nil)
|
|
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().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, true /* ignoreUpdateAndCounter */, 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,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "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}, nil)
|
|
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, true /* ignoreUpdateAndCounter */, 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),
|
|
PresenceInternal: 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),
|
|
AuditLogSessionStreamer: events.NewDiscardAuditLog(),
|
|
}
|
|
}
|
|
|
|
func compareDevices(t *testing.T, ignoreUpdateAndCounter bool, 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 = ""
|
|
}
|
|
|
|
// Ignore LastUsed and SignatureCounter?
|
|
var opts []cmp.Option
|
|
if ignoreUpdateAndCounter {
|
|
opts = append(opts, cmp.FilterPath(func(path cmp.Path) bool {
|
|
p := path.String()
|
|
return p == "LastUsed" || p == "Device.Webauthn.SignatureCounter"
|
|
}, cmp.Ignore()))
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got, opts...); diff != "" {
|
|
t.Errorf("compareDevices mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
type mockCache struct {
|
|
Cache
|
|
|
|
resources []types.ResourceWithLabels
|
|
resourcesError error
|
|
}
|
|
|
|
func (m mockCache) ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
|
|
if m.resourcesError != nil {
|
|
return nil, m.resourcesError
|
|
}
|
|
|
|
if req.StartKey != "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return &types.ListResourcesResponse{Resources: m.resources}, nil
|
|
}
|
|
|
|
func TestFilterResources(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
fail := errors.New("fail")
|
|
|
|
const resourceCount = 100
|
|
nodes := make([]types.ResourceWithLabels, 0, resourceCount)
|
|
|
|
for i := 0; i < resourceCount; i++ {
|
|
s, err := types.NewServer(uuid.NewString(), types.KindNode, types.ServerSpecV2{})
|
|
require.NoError(t, err)
|
|
nodes = append(nodes, s)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
limit int32
|
|
filterFn func(labels types.ResourceWithLabels) error
|
|
errorAssertion require.ErrorAssertionFunc
|
|
cache mockCache
|
|
}{
|
|
{
|
|
name: "ListResources fails",
|
|
cache: mockCache{resourcesError: fail},
|
|
errorAssertion: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err, i...)
|
|
require.ErrorIs(t, err, fail)
|
|
},
|
|
},
|
|
{
|
|
name: "Done returns no errors",
|
|
cache: mockCache{resources: nodes},
|
|
errorAssertion: require.NoError,
|
|
filterFn: func(labels types.ResourceWithLabels) error {
|
|
return ErrDone
|
|
},
|
|
},
|
|
{
|
|
name: "fatal errors are propagated",
|
|
cache: mockCache{resources: nodes},
|
|
errorAssertion: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err, i...)
|
|
require.ErrorIs(t, err, fail)
|
|
},
|
|
filterFn: func(labels types.ResourceWithLabels) error {
|
|
return fail
|
|
},
|
|
},
|
|
{
|
|
name: "no errors iterates the entire resource set",
|
|
cache: mockCache{resources: nodes},
|
|
errorAssertion: require.NoError,
|
|
filterFn: func(labels types.ResourceWithLabels) error {
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := &Server{Cache: tt.cache}
|
|
|
|
err := srv.IterateResources(ctx, proto.ListResourcesRequest{
|
|
ResourceType: types.KindNode,
|
|
Namespace: apidefaults.Namespace,
|
|
Limit: tt.limit,
|
|
}, tt.filterFn)
|
|
tt.errorAssertion(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCAGeneration(t *testing.T) {
|
|
ctx := context.Background()
|
|
const (
|
|
clusterName = "cluster1"
|
|
HostUUID = "0000-000-000-0000"
|
|
)
|
|
native.PrecomputeKeys()
|
|
// Cache key for better performance as we don't care about the value being unique.
|
|
privKey, pubKey, err := testauthority.New().GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
|
|
ksConfig := keystore.Config{
|
|
Software: keystore.SoftwareConfig{
|
|
RSAKeyPairSource: func() (priv []byte, pub []byte, err error) {
|
|
return privKey, pubKey, nil
|
|
},
|
|
},
|
|
}
|
|
keyStore, err := keystore.NewManager(ctx, ksConfig)
|
|
require.NoError(t, err)
|
|
|
|
for _, caType := range types.CertAuthTypes {
|
|
t.Run(string(caType), func(t *testing.T) {
|
|
testKeySet := suite.NewTestCA(caType, clusterName, privKey).Spec.ActiveKeys
|
|
keySet, err := newKeySet(ctx, keyStore, types.CertAuthID{Type: caType, DomainName: clusterName})
|
|
require.NoError(t, err)
|
|
|
|
// Don't compare values as those are different. Only check if the key is set/not set in both cases.
|
|
require.Equal(t, len(testKeySet.SSH) > 0, len(keySet.SSH) > 0,
|
|
"test CA and production CA have different SSH keys for type %v", caType)
|
|
require.Equal(t, len(testKeySet.TLS) > 0, len(keySet.TLS) > 0,
|
|
"test CA and production CA have different TLS keys for type %v", caType)
|
|
require.Equal(t, len(testKeySet.JWT) > 0, len(keySet.JWT) > 0,
|
|
"test CA and production CA have different JWT keys for type %v", caType)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetLicense(t *testing.T) {
|
|
s := newAuthSuite(t)
|
|
|
|
// GetLicense should return error if license is not set
|
|
_, err := s.a.GetLicense(context.Background())
|
|
assert.NotNil(t, err)
|
|
|
|
// GetLicense should return cert and key pem concatenated, when license is set
|
|
l := license.License{
|
|
CertPEM: []byte("cert"),
|
|
KeyPEM: []byte("key"),
|
|
}
|
|
s.a.SetLicense(&l)
|
|
|
|
actual, err := s.a.GetLicense(context.Background())
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, fmt.Sprintf("%s%s", l.CertPEM, l.KeyPEM), actual)
|
|
}
|
|
|
|
type mockEnforcer struct {
|
|
services.Enforcer
|
|
notifications []reporting.Notification
|
|
}
|
|
|
|
func (m mockEnforcer) GetLicenseCheckResult(ctx context.Context) (*reporting.Heartbeat, error) {
|
|
return &reporting.Heartbeat{
|
|
Spec: reporting.HeartbeatSpec{
|
|
Notifications: m.notifications,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func TestEnforcerGetLicenseCheckResult(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newAuthSuite(t)
|
|
|
|
expected := []reporting.Notification{
|
|
{
|
|
Type: "test",
|
|
Severity: "warning",
|
|
Text: "test warning",
|
|
HTML: "test warning",
|
|
},
|
|
}
|
|
|
|
s.a.SetEnforcer(&mockEnforcer{
|
|
notifications: expected,
|
|
})
|
|
|
|
heartbeat, err := s.a.GetLicenseCheckResult(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, heartbeat.Spec.Notifications)
|
|
}
|
|
|
|
func TestInstallerCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
ctx := context.Background()
|
|
|
|
var inst types.Installer
|
|
var err error
|
|
contents := "#! just some script contents"
|
|
inst, err = types.NewInstallerV1(installers.InstallerScriptName, contents)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.a.SetInstaller(ctx, inst))
|
|
|
|
inst, err = s.a.GetInstaller(ctx, installers.InstallerScriptName)
|
|
require.NoError(t, err)
|
|
require.Equal(t, contents, inst.GetScript())
|
|
|
|
newContents := "nothing useful here"
|
|
newInstaller, err := types.NewInstallerV1("other-script", newContents)
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.a.SetInstaller(ctx, newInstaller))
|
|
|
|
newInst, err := s.a.GetInstaller(ctx, "other-script")
|
|
require.NoError(t, err)
|
|
require.Equal(t, newContents, newInst.GetScript())
|
|
|
|
instcoll, err := s.a.GetInstallers(ctx)
|
|
require.NoError(t, err)
|
|
var instScripts []string
|
|
for _, inst := range instcoll {
|
|
instScripts = append(instScripts, inst.GetScript())
|
|
}
|
|
|
|
require.ElementsMatch(t,
|
|
[]string{inst.GetScript(), newInst.GetScript()},
|
|
instScripts,
|
|
)
|
|
|
|
err = s.a.DeleteInstaller(ctx, installers.InstallerScriptName)
|
|
require.NoError(t, err)
|
|
|
|
_, err = s.a.GetInstaller(ctx, installers.InstallerScriptName)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
}
|