mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 23:31:08 +08:00
Consumes the new auth client created in #41460. The changes introduced were performed with a series of gofmt rewrite rules. ```bash $ find ./lib/auth -type f -name '*.go' -exec gofmt -w -l -r "ClientI -> authclient.ClientI" {} \; $ find ./lib/auth -type f -name '*.go' -exec gofmt -w -l -r "Client -> authclient.Client" {} \; $ find ./lib/auth -type f -name '*.go' -exec gofmt -w -l -r "NewClient -> authclient.NewClientI" {} \; $ find . -type f -name '*.go' -exec gofmt -w -l -r "auth.ClientI -> authclient.ClientI" {} \; $ find . -type f -name '*.go' -exec gofmt -w -l -r "auth.Client -> authclient.Client" {} \; $ find . -type f -name '*.go' -exec gofmt -w -l -r "auth.NewClient -> authclient.NewClient" {} \; ```
3933 lines
124 KiB
Go
3933 lines
124 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/license"
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
apidefaults "github.com/gravitational/teleport/api/defaults"
|
|
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
|
|
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/types/header"
|
|
"github.com/gravitational/teleport/api/types/installers"
|
|
"github.com/gravitational/teleport/api/types/trait"
|
|
"github.com/gravitational/teleport/api/types/userloginstate"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth/authclient"
|
|
"github.com/gravitational/teleport/lib/auth/keystore"
|
|
"github.com/gravitational/teleport/lib/auth/native"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
|
|
"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.MockRecorderEmitter
|
|
}
|
|
|
|
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.MockRecorderEmitter{}
|
|
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.UpsertAuthPreference(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.UpsertClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig()); err != nil {
|
|
return p, trace.Wrap(err)
|
|
}
|
|
if _, err := p.a.UpsertSessionRecordingConfig(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)
|
|
modules.SetInsecureTestMode(true)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestSessions(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
|
|
user := "user1"
|
|
pass := []byte("abcdef123456")
|
|
|
|
_, 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.Empty(t, cmp.Diff(ws, out, cmpopts.IgnoreFields(types.Metadata{}, "ID", "Revision")))
|
|
|
|
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 TestAuthenticateWebUser_deviceWebToken(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
authServer := s.a
|
|
|
|
const user = "llama"
|
|
const pass = "supersecretpassword!!1!"
|
|
|
|
// Prepare user and password.
|
|
// 2nd factors are not important for this test.
|
|
_, _, err := CreateUserAndRole(authServer, user, []string{user}, nil /* allowRules */)
|
|
require.NoError(t, err, "CreateUserAndRole failed")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(user, []byte(pass)),
|
|
"UpsertPassword failed")
|
|
|
|
const userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
|
const remoteIP = "40.89.244.232"
|
|
const remoteAddr = remoteIP + ":4242"
|
|
|
|
makeTokenSuccess := func(t *testing.T) CreateDeviceWebTokenFunc {
|
|
return func(ctx context.Context, dwt *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
|
|
if !assert.NotNil(t, dwt, "dwt parameter is nil") {
|
|
return nil, errors.New("dtw parameter is nil")
|
|
}
|
|
assert.NotEmpty(t, dwt.WebSessionId, "dwt.WebSessionId is empty")
|
|
assert.Equal(t, userAgent, dwt.BrowserUserAgent, "dwt.BrowserUserAgent mismatch")
|
|
assert.Equal(t, remoteIP, dwt.BrowserIp, "dwt.BrowserIp mismatch")
|
|
assert.Equal(t, user, dwt.User, "dwt.User mismatch")
|
|
|
|
return &devicepb.DeviceWebToken{
|
|
Id: "this is an opaque ID",
|
|
Token: "this is an opaque token",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
makeTokenError := func(t *testing.T) CreateDeviceWebTokenFunc {
|
|
return func(ctx context.Context, dwt *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
|
|
return nil, errors.New("something bad happened")
|
|
}
|
|
}
|
|
|
|
ctx := context.Background()
|
|
validReq := &AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &PassCreds{
|
|
Password: []byte(pass),
|
|
},
|
|
ClientMetadata: &ForwardedClientMetadata{
|
|
UserAgent: userAgent,
|
|
RemoteAddr: remoteAddr,
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
makeTokenFunc func(t *testing.T) CreateDeviceWebTokenFunc
|
|
req *AuthenticateUserRequest
|
|
wantErr string
|
|
wantToken bool
|
|
}{
|
|
{
|
|
name: "success",
|
|
makeTokenFunc: makeTokenSuccess,
|
|
req: validReq,
|
|
wantToken: true,
|
|
},
|
|
{
|
|
name: "CreateDeviceWebToken fails",
|
|
makeTokenFunc: makeTokenError,
|
|
req: validReq,
|
|
},
|
|
{
|
|
name: "empty ClientMetadata.UserAgent",
|
|
makeTokenFunc: makeTokenSuccess,
|
|
req: func() *AuthenticateUserRequest {
|
|
req := *validReq
|
|
req.ClientMetadata = &ForwardedClientMetadata{
|
|
RemoteAddr: remoteAddr, // AuthenticateWebUser fails if RemoteAddr is missing.
|
|
}
|
|
return &req
|
|
}(),
|
|
},
|
|
{
|
|
name: "nil ClientMetadata",
|
|
makeTokenFunc: makeTokenSuccess,
|
|
req: func() *AuthenticateUserRequest {
|
|
req := *validReq
|
|
req.ClientMetadata = nil
|
|
return &req
|
|
}(),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var gotSessionID string // captured session ID from DeviceWebToken
|
|
tokenFn := test.makeTokenFunc(t)
|
|
captureSessionFn := func(ctx context.Context, dwt *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
|
|
gotSessionID = dwt.GetWebSessionId()
|
|
return tokenFn(ctx, dwt)
|
|
}
|
|
|
|
// Set a fake SetCreateDeviceWebTokenFunc.
|
|
// This is set during server creation for Enterprise servers.
|
|
authServer.SetCreateDeviceWebTokenFunc(captureSessionFn)
|
|
|
|
webSession, err := authServer.AuthenticateWebUser(ctx, *test.req)
|
|
// AuthenticateWebUser is never expected to fail in this test.
|
|
// Either a DeviceWebToken exists in the response, or it doesn't, but the
|
|
// method itself always works.
|
|
require.NoError(t, err, "AuthenticateWebUser failed unexpectedly")
|
|
|
|
// Verify the token itself.
|
|
deviceToken := webSession.GetDeviceWebToken()
|
|
if !test.wantToken {
|
|
assert.Nil(t, deviceToken, "WebSession.GetDeviceWebToken is not nil")
|
|
return
|
|
}
|
|
require.NotNil(t, deviceToken, "WebSession.GetDeviceWebToken is nil")
|
|
assert.NotEmpty(t, deviceToken.Id, "DeviceWebToken.Id is empty")
|
|
assert.NotEmpty(t, deviceToken.Token, "DeviceWebToken.Token is empty")
|
|
|
|
// Verify the WebSessionId sent to CreateDeviceWebTokenFunc.
|
|
assert.Equal(t, webSession.GetName(), gotSessionID, "Captured DeviceWebToken.WebSessionId mismatch")
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
_, err = s.a.CreateRemoteCluster(ctx, leaf)
|
|
require.NoError(t, err)
|
|
|
|
user := "user1"
|
|
pass := []byte("abcdef123456")
|
|
|
|
// 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"})
|
|
|
|
role, 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, user, resp.Username)
|
|
// 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, inSSHPub, gotSSHCert.Key)
|
|
require.Equal(t, []string{user, teleport.SSHSessionJoinPrincipal}, gotSSHCert.ValidPrincipals)
|
|
// 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, wantID, *gotID)
|
|
|
|
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, wantID, *gotID)
|
|
|
|
// Login without specifying kube cluster. Kube cluster in the certificate should be empty.
|
|
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, user, resp.Username)
|
|
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"},
|
|
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 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, user, resp.Username)
|
|
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, wantID, *gotID)
|
|
|
|
// Login without specifying kube cluster. Kube cluster in the certificate should be empty.
|
|
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, user, resp.Username)
|
|
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"},
|
|
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 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 TestAuthenticateUser_mfaDeviceLocked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
|
|
ctx := context.Background()
|
|
const user = "llama"
|
|
const pass = "supersecret!!1!one"
|
|
|
|
// Configure auth preferences.
|
|
authPref, err := authServer.GetAuthPreference(ctx)
|
|
require.NoError(t, err, "GetAuthPreference")
|
|
authPref.SetSecondFactor(constants.SecondFactorOptional) // good enough
|
|
authPref.SetWebauthn(&types.Webauthn{
|
|
RPID: "localhost",
|
|
})
|
|
_, err = authServer.UpdateAuthPreference(ctx, authPref)
|
|
require.NoError(t, err, "UpdateAuthPreference")
|
|
|
|
// Prepare user, password and MFA device.
|
|
_, _, err = CreateUserAndRole(authServer, user, []string{user}, nil /* allowRules */)
|
|
require.NoError(t, err, "CreateUserAndRole")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(user, []byte(pass)),
|
|
"UpsertPassword")
|
|
|
|
userClient, err := testServer.NewClient(TestUser(user))
|
|
require.NoError(t, err, "NewClient")
|
|
|
|
// OTP devices would work for this test too.
|
|
dev1, err := RegisterTestDevice(ctx, userClient, "dev1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
|
|
require.NoError(t, err, "RegisterTestDevice")
|
|
dev2, err := RegisterTestDevice(ctx, userClient, "dev2", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, dev1 /* authenticator */)
|
|
require.NoError(t, err, "RegisterTestDevice")
|
|
|
|
// Prepare an SSH public key for testing.
|
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, "GenerateKey")
|
|
signer, err := ssh.NewSignerFromSigner(privKey)
|
|
require.NoError(t, err, "NewSignerFromSigner")
|
|
pubKey := ssh.MarshalAuthorizedKey(signer.PublicKey())
|
|
|
|
// Users initially authenticate via Proxy, as there isn't a userClient before
|
|
// authn.
|
|
proxyClient, err := testServer.NewClient(TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err, "NewClient")
|
|
|
|
authenticateSSH := func(dev *TestDevice) (*SSHLoginResponse, error) {
|
|
chal, err := proxyClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
|
|
Request: &proto.CreateAuthenticateChallengeRequest_UserCredentials{
|
|
UserCredentials: &proto.UserCredentials{
|
|
Username: user,
|
|
Password: []byte(pass),
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create challenge: %w", err)
|
|
}
|
|
|
|
chalResp, err := dev.SolveAuthn(chal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("solve challenge: %w", err)
|
|
}
|
|
|
|
return proxyClient.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: AuthenticateUserRequest{
|
|
Username: user,
|
|
PublicKey: pubKey,
|
|
Pass: &PassCreds{
|
|
Password: []byte(pass),
|
|
},
|
|
Webauthn: wantypes.CredentialAssertionResponseFromProto(chalResp.GetWebauthn()),
|
|
},
|
|
TTL: 1 * time.Hour,
|
|
})
|
|
}
|
|
|
|
// Lock dev1.
|
|
const lockMessage = "device locked for testing"
|
|
lock, err := types.NewLock("dev1-lock", types.LockSpecV2{
|
|
Target: types.LockTarget{
|
|
MFADevice: dev1.MFA.Id,
|
|
},
|
|
Message: lockMessage,
|
|
})
|
|
require.NoError(t, err, "NewLock")
|
|
require.NoError(t,
|
|
userClient.UpsertLock(ctx, lock),
|
|
"UpsertLock")
|
|
|
|
t.Run("locked device", func(t *testing.T) {
|
|
_, err := authenticateSSH(dev1)
|
|
assert.ErrorContains(t, err, lockMessage)
|
|
})
|
|
|
|
t.Run("unlocked device", func(t *testing.T) {
|
|
_, err := authenticateSSH(dev2)
|
|
assert.NoError(t, err, "authenticateSSH failed unexpectedly")
|
|
})
|
|
|
|
t.Run("locked device password change", func(t *testing.T) {
|
|
chal, err := userClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
|
|
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
|
|
ContextUser: &proto.ContextUser{},
|
|
},
|
|
ChallengeExtensions: &mfav1.ChallengeExtensions{
|
|
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
|
|
},
|
|
})
|
|
require.NoError(t, err, "CreateAuthenticateChallenge")
|
|
|
|
// dev1 is still locked.
|
|
chalResp, err := dev1.SolveAuthn(chal)
|
|
require.NoError(t, err, "SolveAuthn")
|
|
|
|
assert.ErrorContains(t,
|
|
userClient.ChangePassword(ctx, &proto.ChangePasswordRequest{
|
|
User: user,
|
|
OldPassword: []byte(pass),
|
|
NewPassword: []byte("evenmoresecret!!1!ONE"),
|
|
Webauthn: chalResp.GetWebauthn(),
|
|
}),
|
|
lockMessage)
|
|
})
|
|
}
|
|
|
|
func TestUserLock(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
ctx := context.Background()
|
|
|
|
username := "user1"
|
|
pass := []byte("abcdef123456")
|
|
|
|
_, 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 password")},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
user, err := s.a.GetUser(ctx, 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 TestAuth_SetStaticTokens(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
ctx := context.Background()
|
|
|
|
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(defaults.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 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.Empty(t, st.GetStaticTokens())
|
|
|
|
// 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)
|
|
|
|
clientAddr := &net.TCPAddr{IP: net.IPv4(10, 255, 0, 0)}
|
|
ctx := authz.ContextWithClientSrcAddr(context.Background(), clientAddr)
|
|
|
|
// test create user, happy path
|
|
user.SetCreatedBy(types.CreatedBy{
|
|
User: types.UserRef{Name: "some-auth-user"},
|
|
})
|
|
user, err = s.a.CreateUser(ctx, user)
|
|
require.NoError(t, err)
|
|
require.Equal(t, events.UserCreateEvent, s.mockEmitter.LastEvent().GetType())
|
|
createEvt := s.mockEmitter.LastEvent().(*apievents.UserCreate)
|
|
require.Equal(t, "some-auth-user", createEvt.User)
|
|
require.Equal(t, clientAddr.String(), createEvt.ConnectionMetadata.RemoteAddr)
|
|
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, events.UserCreateEvent, s.mockEmitter.LastEvent().GetType())
|
|
createEvt = s.mockEmitter.LastEvent().(*apievents.UserCreate)
|
|
require.Equal(t, teleport.UserSystem, createEvt.User)
|
|
require.Equal(t, clientAddr.String(), createEvt.ConnectionMetadata.RemoteAddr)
|
|
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, events.UserUpdatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
updateEvt := s.mockEmitter.LastEvent().(*apievents.UserUpdate)
|
|
require.Equal(t, teleport.UserSystem, updateEvt.User)
|
|
require.Equal(t, clientAddr.String(), updateEvt.ConnectionMetadata.RemoteAddr)
|
|
}
|
|
|
|
func TestTrustedClusterCRUDEventEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
clientAddr := &net.TCPAddr{IP: net.IPv4(10, 255, 0, 0)}
|
|
ctx := authz.ContextWithClientSrcAddr(context.Background(), clientAddr)
|
|
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, events.TrustedClusterCreateEvent, s.mockEmitter.LastEvent().GetType())
|
|
createEvt := s.mockEmitter.LastEvent().(*apievents.TrustedClusterCreate)
|
|
require.Equal(t, clientAddr.String(), createEvt.ConnectionMetadata.RemoteAddr)
|
|
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, events.TrustedClusterCreateEvent, s.mockEmitter.LastEvent().GetType())
|
|
createEvt = s.mockEmitter.LastEvent().(*apievents.TrustedClusterCreate)
|
|
require.Equal(t, clientAddr.String(), createEvt.ConnectionMetadata.RemoteAddr)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test delete event
|
|
err = s.a.DeleteTrustedCluster(ctx, "test")
|
|
require.NoError(t, err)
|
|
require.Equal(t, events.TrustedClusterDeleteEvent, s.mockEmitter.LastEvent().GetType())
|
|
deleteEvt := s.mockEmitter.LastEvent().(*apievents.TrustedClusterDelete)
|
|
require.Equal(t, clientAddr.String(), deleteEvt.ConnectionMetadata.RemoteAddr)
|
|
}
|
|
|
|
func TestGithubConnectorCRUDEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
clientAddr := &net.TCPAddr{IP: net.IPv4(10, 255, 0, 0)}
|
|
ctx := authz.ContextWithClientSrcAddr(context.Background(), clientAddr)
|
|
github, err := types.NewGithubConnector("test", types.GithubConnectorSpecV3{
|
|
TeamsToRoles: []types.TeamRolesMapping{
|
|
{
|
|
Organization: "octocats",
|
|
Team: "dummy",
|
|
Roles: []string{"dummy"},
|
|
},
|
|
},
|
|
})
|
|
// test github create event
|
|
require.NoError(t, err)
|
|
github, err = s.a.createGithubConnector(ctx, github)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.GithubConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.GithubConnectorCreatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
createEvt := s.mockEmitter.LastEvent().(*apievents.GithubConnectorCreate)
|
|
require.Equal(t, clientAddr.String(), createEvt.ConnectionMetadata.RemoteAddr)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test github update event
|
|
github.SetDisplay("llama")
|
|
github, err = s.a.updateGithubConnector(ctx, github)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.GithubConnectorUpdate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.GithubConnectorUpdatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
updateEvt := s.mockEmitter.LastEvent().(*apievents.GithubConnectorUpdate)
|
|
require.Equal(t, clientAddr.String(), updateEvt.ConnectionMetadata.RemoteAddr)
|
|
s.mockEmitter.Reset()
|
|
|
|
// test github upsert event
|
|
github.SetDisplay("alpaca")
|
|
upserted, err := s.a.upsertGithubConnector(ctx, github)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, upserted)
|
|
require.IsType(t, &apievents.GithubConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.GithubConnectorCreatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
createEvt = s.mockEmitter.LastEvent().(*apievents.GithubConnectorCreate)
|
|
require.Equal(t, clientAddr.String(), createEvt.ConnectionMetadata.RemoteAddr)
|
|
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, events.GithubConnectorDeletedEvent, s.mockEmitter.LastEvent().GetType())
|
|
deleteEvt := s.mockEmitter.LastEvent().(*apievents.GithubConnectorDelete)
|
|
require.Equal(t, clientAddr.String(), deleteEvt.ConnectionMetadata.RemoteAddr)
|
|
}
|
|
|
|
func TestOIDCConnectorCRUDEventsEmitted(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
|
|
ctx := context.Background()
|
|
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)
|
|
|
|
// test oidc create event
|
|
oidc, err = s.a.CreateOIDCConnector(ctx, oidc)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.OIDCConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.OIDCConnectorCreatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
s.mockEmitter.Reset()
|
|
|
|
// test oidc update event
|
|
oidc.SetDisplay("llama")
|
|
oidc, err = s.a.UpdateOIDCConnector(ctx, oidc)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.OIDCConnectorUpdate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.OIDCConnectorUpdatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
s.mockEmitter.Reset()
|
|
|
|
// test oidc upsert event
|
|
oidc.SetDisplay("alpaca")
|
|
upserted, err := s.a.UpsertOIDCConnector(ctx, oidc)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, upserted)
|
|
require.IsType(t, &apievents.OIDCConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.OIDCConnectorCreatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
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, events.OIDCConnectorDeletedEvent, s.mockEmitter.LastEvent().GetType())
|
|
}
|
|
|
|
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)
|
|
role, err = s.a.CreateRole(ctx, role)
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
// test saml create
|
|
saml, err = s.a.CreateSAMLConnector(ctx, saml)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.SAMLConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.SAMLConnectorCreatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
s.mockEmitter.Reset()
|
|
|
|
// test saml update event
|
|
saml.SetDisplay("llama")
|
|
saml, err = s.a.UpdateSAMLConnector(ctx, saml)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.SAMLConnectorUpdate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.SAMLConnectorUpdatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
s.mockEmitter.Reset()
|
|
|
|
// test saml upsert event
|
|
saml.SetDisplay("alapaca")
|
|
_, err = s.a.UpsertSAMLConnector(ctx, saml)
|
|
require.NoError(t, err)
|
|
require.IsType(t, &apievents.SAMLConnectorCreate{}, s.mockEmitter.LastEvent())
|
|
require.Equal(t, events.SAMLConnectorCreatedEvent, s.mockEmitter.LastEvent().GetType())
|
|
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, events.SAMLConnectorDeletedEvent, s.mockEmitter.LastEvent().GetType())
|
|
}
|
|
|
|
func TestEmitSSOLoginFailureEvent(t *testing.T) {
|
|
mockE := &eventstest.MockRecorderEmitter{}
|
|
|
|
emitSSOLoginFailureEvent(context.Background(), mockE, "test", trace.BadParameter("some error"), false)
|
|
|
|
expectedLoginFailure := &apievents.UserLogin{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.UserLoginEvent,
|
|
Code: events.UserSSOLoginFailureCode,
|
|
},
|
|
Method: "test",
|
|
Status: apievents.Status{
|
|
Success: false,
|
|
Error: "some error",
|
|
UserMessage: "some error",
|
|
},
|
|
}
|
|
require.Equal(t, expectedLoginFailure, mockE.LastEvent())
|
|
|
|
emitSSOLoginFailureEvent(context.Background(), mockE, "test", trace.BadParameter("some error"), true)
|
|
|
|
expectedTestFailure := &apievents.UserLogin{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.UserLoginEvent,
|
|
Code: events.UserSSOTestFlowLoginFailureCode,
|
|
},
|
|
Method: "test",
|
|
Status: apievents.Status{
|
|
Success: false,
|
|
Error: "some error",
|
|
UserMessage: "some error",
|
|
},
|
|
}
|
|
require.Equal(t, expectedTestFailure, mockE.LastEvent())
|
|
}
|
|
|
|
func TestServer_AugmentContextUserCertificates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
emitter := &eventstest.MockRecorderEmitter{}
|
|
authServer.emitter = emitter
|
|
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.Less(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")
|
|
})
|
|
|
|
// Assert audit events.
|
|
lastEvent := emitter.LastEvent()
|
|
require.NotNil(t, lastEvent, "emitter.LastEvent() is nil")
|
|
// Assert the code, that is a good enough proxy for other fields.
|
|
assert.Equal(t, events.CertificateCreateEvent, lastEvent.GetType(), "lastEvent type mismatch")
|
|
// Assert event DeviceExtensions.
|
|
certEvent, ok := lastEvent.(*apievents.CertificateCreate)
|
|
if assert.True(t, ok, "lastEvent is not an apievents.CertificateCreate, got %T", lastEvent) {
|
|
got := certEvent.Identity.DeviceExtensions
|
|
want := &apievents.DeviceExtensions{
|
|
DeviceId: test.opts.DeviceExtensions.DeviceID,
|
|
AssetTag: test.opts.DeviceExtensions.AssetTag,
|
|
CredentialId: test.opts.DeviceExtensions.CredentialID,
|
|
}
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("certEvent.Identity.DeviceExtensions mismatch (-want +got)\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 TestServer_AugmentWebSessionCertificates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
ctx := context.Background()
|
|
|
|
userData := setupUserForAugmentWebSessionCertificatesTest(t, testServer)
|
|
|
|
// Safe to reuse, user-independent.
|
|
deviceExts := &DeviceExtensions{
|
|
DeviceID: "my-device-id",
|
|
AssetTag: "my-device-asset-tag",
|
|
CredentialID: "my-device-credential-id",
|
|
}
|
|
|
|
assertSSHCert := func(t *testing.T, sshCert []byte) {
|
|
cert, err := sshutils.ParseCertificate(sshCert)
|
|
require.NoError(t, err, "ParseCertificate")
|
|
|
|
// Not empty is good enough here, other tests assert this deeply.
|
|
assert.NotEmpty(t, cert.Extensions[teleport.CertExtensionDeviceID], "DeviceID empty")
|
|
assert.NotEmpty(t, cert.Extensions[teleport.CertExtensionDeviceAssetTag], "AssetTag empty")
|
|
assert.NotEmpty(t, cert.Extensions[teleport.CertExtensionDeviceCredentialID], "CredentialID empty")
|
|
}
|
|
|
|
assertX509Cert := func(t *testing.T, x509PEM []byte) {
|
|
_, identity := parseX509PEMAndIdentity(t, x509PEM)
|
|
|
|
// Not empty is good enough here, other tests assert this deeply.
|
|
assert.NotEmpty(t, identity.DeviceExtensions.DeviceID, "DeviceID empty")
|
|
assert.NotEmpty(t, identity.DeviceExtensions.AssetTag, "AssetTag empty")
|
|
assert.NotEmpty(t, identity.DeviceExtensions.CredentialID, "CredentialID empty")
|
|
}
|
|
|
|
t.Run("ok", func(t *testing.T) {
|
|
t.Parallel() // Get the errors suite going asap.
|
|
|
|
opts := &AugmentWebSessionCertificatesOpts{
|
|
WebSessionID: userData.webSessionID,
|
|
User: userData.user,
|
|
DeviceExtensions: deviceExts,
|
|
}
|
|
err := authServer.AugmentWebSessionCertificates(ctx, opts)
|
|
require.NoError(t, err, "AugmentWebSessionCertificates")
|
|
|
|
// Verify WebSession certificates.
|
|
webSession, err := authServer.WebSessions().Get(ctx, types.GetWebSessionRequest{
|
|
User: userData.user,
|
|
SessionID: userData.webSessionID,
|
|
})
|
|
require.NoError(t, err, "WebSessions().Get() failed: %v", err)
|
|
assertSSHCert(t, webSession.GetPub())
|
|
assertX509Cert(t, webSession.GetTLSCert())
|
|
assert.True(t, webSession.GetHasDeviceExtensions(), "webSesssion.GetHasDeviceExtensions() mismatch")
|
|
|
|
// Scenario requires augmented certs to work.
|
|
t.Run("cannot re-augment the same session", func(t *testing.T) {
|
|
err := authServer.AugmentWebSessionCertificates(ctx, opts)
|
|
const wantErr = "extensions already present"
|
|
assert.ErrorContains(t, err, wantErr, "AugmentWebSessionCertificates error mismatch")
|
|
})
|
|
})
|
|
|
|
user2Data := setupUserForAugmentWebSessionCertificatesTest(t, testServer)
|
|
user2Opts := &AugmentWebSessionCertificatesOpts{
|
|
WebSessionID: user2Data.webSessionID,
|
|
User: user2Data.user,
|
|
DeviceExtensions: deviceExts,
|
|
}
|
|
|
|
t.Run("errors", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts *AugmentWebSessionCertificatesOpts
|
|
wantErr string
|
|
assertErr func(error) bool // defaults to trace.IsBadParameter
|
|
}{
|
|
{
|
|
name: "opts nil",
|
|
wantErr: "opts required",
|
|
},
|
|
{
|
|
name: "opts.WebSessionID is empty",
|
|
opts: func() *AugmentWebSessionCertificatesOpts {
|
|
opts := *user2Opts
|
|
opts.WebSessionID = ""
|
|
return &opts
|
|
}(),
|
|
wantErr: "WebSessionID required",
|
|
},
|
|
{
|
|
name: "opts.User is empty",
|
|
opts: func() *AugmentWebSessionCertificatesOpts {
|
|
opts := *user2Opts
|
|
opts.User = ""
|
|
return &opts
|
|
}(),
|
|
wantErr: "User required",
|
|
},
|
|
{
|
|
name: "opts.DeviceExtensions nil",
|
|
opts: func() *AugmentWebSessionCertificatesOpts {
|
|
opts := *user2Opts
|
|
opts.DeviceExtensions = nil
|
|
return &opts
|
|
}(),
|
|
wantErr: "at least one opts extension",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := authServer.AugmentWebSessionCertificates(ctx, test.opts)
|
|
assert.ErrorContains(t, err, test.wantErr, "AugmentWebSessionCertificates error mismatch")
|
|
|
|
assertErr := test.assertErr
|
|
if assertErr == nil {
|
|
assertErr = trace.IsBadParameter
|
|
}
|
|
assert.True(t,
|
|
assertErr(err),
|
|
"AugmentWebSessionCertificates: assertErr failed: err=%v (%T)", err, trace.Unwrap(err))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestServer_ExtendWebSession_deviceExtensions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
ctx := context.Background()
|
|
|
|
userData := setupUserForAugmentWebSessionCertificatesTest(t, testServer)
|
|
|
|
deviceExts := &DeviceExtensions{
|
|
DeviceID: "my-device-id",
|
|
AssetTag: "my-device-asset-tag",
|
|
CredentialID: "my-device-credential-id",
|
|
}
|
|
|
|
// Augment the user's session, then later extend it.
|
|
err := authServer.AugmentWebSessionCertificates(ctx, &AugmentWebSessionCertificatesOpts{
|
|
WebSessionID: userData.webSessionID,
|
|
User: userData.user,
|
|
DeviceExtensions: deviceExts,
|
|
})
|
|
require.NoError(t, err, "AugmentWebSessionCertificates() failed")
|
|
|
|
// Retrieve augmented session and parse its identity.
|
|
webSession, err := authServer.WebSessions().Get(ctx, types.GetWebSessionRequest{
|
|
User: userData.user,
|
|
SessionID: userData.webSessionID,
|
|
})
|
|
require.NoError(t, err, "WebSessions().Get() failed")
|
|
|
|
_, sessionIdentity := parseX509PEMAndIdentity(t, webSession.GetTLSCert())
|
|
|
|
parseSSHDeviceExtensions := func(t *testing.T, sshCert []byte) tlsca.DeviceExtensions {
|
|
cert, err := sshutils.ParseCertificate(sshCert)
|
|
require.NoError(t, err, "ParseCertificate")
|
|
|
|
return tlsca.DeviceExtensions{
|
|
DeviceID: cert.Extensions[teleport.CertExtensionDeviceID],
|
|
AssetTag: cert.Extensions[teleport.CertExtensionDeviceAssetTag],
|
|
CredentialID: cert.Extensions[teleport.CertExtensionDeviceCredentialID],
|
|
}
|
|
}
|
|
|
|
t.Run("ok", func(t *testing.T) {
|
|
newSession, err := authServer.ExtendWebSession(ctx, WebSessionReq{
|
|
User: webSession.GetUser(),
|
|
PrevSessionID: webSession.GetName(),
|
|
}, *sessionIdentity)
|
|
require.NoError(t, err, "ExtendWebSession() failed")
|
|
|
|
// Assert extensions flag.
|
|
assert.True(t, newSession.GetHasDeviceExtensions(), "newSession.GetHasDeviceExtensions() mismatch")
|
|
|
|
// Assert TLS extensions.
|
|
_, newIdentity := parseX509PEMAndIdentity(t, newSession.GetTLSCert())
|
|
wantExts := tlsca.DeviceExtensions(*deviceExts)
|
|
if diff := cmp.Diff(wantExts, newIdentity.DeviceExtensions); diff != "" {
|
|
t.Errorf("newSession.TLSCert DeviceExtensions mismatch (-want +got)\n%s", diff)
|
|
}
|
|
|
|
// Assert SSH extensions.
|
|
if diff := cmp.Diff(wantExts, parseSSHDeviceExtensions(t, newSession.GetPub())); diff != "" {
|
|
t.Errorf("newSession.Pub DeviceExtensions mismatch (-want +got)\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
type augmentUserData struct {
|
|
user string
|
|
pass []byte
|
|
pubKey []byte // SSH "AuthorizedKey" format
|
|
webSessionID string
|
|
}
|
|
|
|
func setupUserForAugmentWebSessionCertificatesTest(t *testing.T, testServer *TestTLSServer) *augmentUserData {
|
|
authServer := testServer.Auth()
|
|
ctx := context.Background()
|
|
|
|
user := &augmentUserData{
|
|
user: "llama_" + uuid.NewString(),
|
|
pass: []byte("passwordforllamaA1!"),
|
|
}
|
|
|
|
// Create user and assign a password.
|
|
_, _, err := CreateUserAndRole(authServer, user.user, []string{user.user}, nil /* allowRules */)
|
|
require.NoError(t, err, "CreateUserAndRole")
|
|
require.NoError(t,
|
|
authServer.UpsertPassword(user.user, user.pass),
|
|
"UpsertPassword",
|
|
)
|
|
|
|
// Generate underlying keys for SSH and TLS.
|
|
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, "GenerateKey")
|
|
pubKeySSH, err := ssh.NewPublicKey(privKey.Public())
|
|
require.NoError(t, err, "NewPublicKey")
|
|
user.pubKey = ssh.MarshalAuthorizedKey(pubKeySSH)
|
|
|
|
// Prepare a WebSession to be augmented.
|
|
authnReq := AuthenticateUserRequest{
|
|
Username: user.user,
|
|
PublicKey: user.pubKey,
|
|
Pass: &PassCreds{
|
|
Password: user.pass,
|
|
},
|
|
}
|
|
session, err := authServer.AuthenticateWebUser(ctx, authnReq)
|
|
require.NoError(t, err, "AuthenticateWebUser")
|
|
user.webSessionID = session.GetName()
|
|
|
|
return user
|
|
}
|
|
|
|
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("abcdef123456")
|
|
|
|
// 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 contextWithGRPCClientUserAgent(ctx context.Context, userAgent string) context.Context {
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
if !ok {
|
|
md = make(metadata.MD)
|
|
}
|
|
md["user-agent"] = append(md["user-agent"], userAgent)
|
|
return metadata.NewIncomingContext(ctx, md)
|
|
}
|
|
|
|
func TestGenerateUserCertWithCertExtension(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := contextWithGRPCClientUserAgent(context.Background(), "test-user-agent/1.0")
|
|
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.AccessInfoFromUserState(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(ctx, 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)
|
|
|
|
// Validate audit event.
|
|
lastEvent := p.mockEmitter.LastEvent()
|
|
require.IsType(t, &apievents.CertificateCreate{}, lastEvent)
|
|
require.Empty(t, cmp.Diff(
|
|
&apievents.CertificateCreate{
|
|
Metadata: apievents.Metadata{
|
|
Type: events.CertificateCreateEvent,
|
|
Code: events.CertificateCreateCode,
|
|
},
|
|
Identity: &apievents.Identity{
|
|
User: "test-user",
|
|
Roles: []string{"user:test-user"},
|
|
RouteToCluster: "test.localhost",
|
|
TeleportCluster: "test.localhost",
|
|
PrivateKeyPolicy: "none",
|
|
},
|
|
CertificateType: events.CertificateTypeUser,
|
|
ClientMetadata: apievents.ClientMetadata{
|
|
UserAgent: "test-user-agent/1.0",
|
|
},
|
|
},
|
|
lastEvent.(*apievents.CertificateCreate),
|
|
cmpopts.IgnoreFields(apievents.Identity{}, "Logins", "Expires"),
|
|
))
|
|
}
|
|
|
|
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
|
|
logins := []string{"login1", "login2"}
|
|
u, r, err := CreateUserAndRole(p.a, "test-user", logins, nil)
|
|
require.NoError(t, err)
|
|
|
|
user, ok := u.(*types.UserV2)
|
|
require.True(t, ok)
|
|
role, ok := r.(*types.RoleV6)
|
|
require.True(t, ok)
|
|
|
|
priv, err := native.GeneratePrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
reply, err := p.a.GenerateOpenSSHCert(ctx, &proto.OpenSSHCertRequest{
|
|
User: user,
|
|
Roles: []*types.RoleV6{role},
|
|
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())
|
|
|
|
// verify that user's logins are present in cert
|
|
logins = append(logins, teleport.SSHSessionJoinPrincipal)
|
|
require.Equal(t, logins, signedCert.ValidPrincipals)
|
|
}
|
|
|
|
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.AccessInfoFromUserState(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(ctx, 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(ctx, 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{ServerID: 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 server IDs should apply to other system roles.
|
|
_, err = p.a.GenerateHostCert(ctx, pub, hostID, "test-proxy", []string{}, p.clusterName.GetClusterName(), types.RoleProxy, time.Minute)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestGenerateUserCertWithUserLoginState(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)
|
|
userState, err := p.a.GetUserOrLoginState(ctx, user.GetName())
|
|
require.NoError(t, err)
|
|
accessInfo := services.AccessInfoFromUserState(userState)
|
|
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)
|
|
|
|
// Generate cert with no user login state.
|
|
certReq := certRequest{
|
|
user: user,
|
|
checker: accessChecker,
|
|
publicKey: pub,
|
|
traits: accessChecker.Traits(),
|
|
}
|
|
resp, err := p.a.generateUserCert(ctx, certReq)
|
|
require.NoError(t, err)
|
|
|
|
sshCert, err := sshutils.ParseCertificate(resp.SSH)
|
|
require.NoError(t, err)
|
|
|
|
roles, err := services.UnmarshalCertRoles(sshCert.Extensions[teleport.CertExtensionTeleportRoles])
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{role.GetName()}, roles)
|
|
|
|
traits := wrappers.Traits{}
|
|
err = wrappers.UnmarshalTraits([]byte(sshCert.Extensions[teleport.CertExtensionTeleportTraits]), &traits)
|
|
require.NoError(t, err)
|
|
require.Empty(t, traits)
|
|
|
|
uls, err := userloginstate.New(
|
|
header.Metadata{
|
|
Name: user.GetName(),
|
|
},
|
|
userloginstate.Spec{
|
|
Roles: []string{
|
|
role.GetName(), // We'll try to grant a duplicate role, which should be deduplicated.
|
|
"uls-role1",
|
|
"uls-role2",
|
|
},
|
|
Traits: trait.Traits{
|
|
"uls-trait1": []string{"value1", "value2"},
|
|
"uls-trait2": []string{"value3", "value4"},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
_, err = p.a.UpsertUserLoginState(ctx, uls)
|
|
require.NoError(t, err)
|
|
|
|
ulsRole1, err := types.NewRole("uls-role1", types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
ulsRole2, err := types.NewRole("uls-role2", types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = p.a.UpsertRole(ctx, ulsRole1)
|
|
require.NoError(t, err)
|
|
_, err = p.a.UpsertRole(ctx, ulsRole2)
|
|
require.NoError(t, err)
|
|
|
|
userState, err = p.a.GetUserOrLoginState(ctx, user.GetName())
|
|
require.NoError(t, err)
|
|
accessInfo = services.AccessInfoFromUserState(userState)
|
|
accessChecker, err = services.NewAccessChecker(accessInfo, p.clusterName.GetClusterName(), p.a)
|
|
require.NoError(t, err)
|
|
|
|
certReq = certRequest{
|
|
user: user,
|
|
checker: accessChecker,
|
|
publicKey: pub,
|
|
traits: accessChecker.Traits(),
|
|
}
|
|
|
|
resp, err = p.a.generateUserCert(ctx, certReq)
|
|
require.NoError(t, err)
|
|
|
|
sshCert, err = sshutils.ParseCertificate(resp.SSH)
|
|
require.NoError(t, err)
|
|
|
|
roles, err = services.UnmarshalCertRoles(sshCert.Extensions[teleport.CertExtensionTeleportRoles])
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{role.GetName(), "uls-role1", "uls-role2"}, roles)
|
|
|
|
traits = wrappers.Traits{}
|
|
err = wrappers.UnmarshalTraits([]byte(sshCert.Extensions[teleport.CertExtensionTeleportTraits]), &traits)
|
|
require.NoError(t, err)
|
|
require.Equal(t, map[string][]string{
|
|
"uls-trait1": {"value1", "value2"},
|
|
"uls-trait2": {"value3", "value4"},
|
|
}, map[string][]string(traits))
|
|
}
|
|
|
|
func TestGenerateUserCertWithHardwareKeySupport(t *testing.T) {
|
|
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)
|
|
user.SetTraits(map[string][]string{
|
|
// add in other random serial numbers to test comparison logic.
|
|
"hardware_key_serial_numbers": {"other1", "other2,12345678,other3"},
|
|
// custom trait name
|
|
"known_yubikeys": {"13572468"},
|
|
})
|
|
_, err = p.a.UpdateUser(ctx, user)
|
|
require.NoError(t, err)
|
|
|
|
accessInfo := services.AccessInfoFromUserState(user)
|
|
accessChecker, err := services.NewAccessChecker(accessInfo, p.clusterName.GetClusterName(), p.a)
|
|
require.NoError(t, err)
|
|
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
key, err := keys.NewPrivateKey(priv, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, err)
|
|
certReq := certRequest{
|
|
user: user,
|
|
checker: accessChecker,
|
|
publicKey: key.MarshalSSHPublicKey(),
|
|
}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
cap types.AuthPreferenceSpecV2
|
|
mockAttestationData *keys.AttestationData
|
|
assertErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "private key policy satified",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
|
|
},
|
|
assertErr: require.NoError,
|
|
}, {
|
|
name: "no attestation data",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err, "expected private key policy error but got %v", err)
|
|
require.True(t, keys.IsPrivateKeyPolicyError(err), "expected private key policy error but got %v", err)
|
|
},
|
|
}, {
|
|
name: "private key policy not satisfied",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKey,
|
|
SerialNumber: 12345678,
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.Error(t, err, "expected private key policy error but got %v", err)
|
|
require.True(t, keys.IsPrivateKeyPolicyError(err), "expected private key policy error but got %v", err)
|
|
},
|
|
}, {
|
|
name: "known hardware key",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
HardwareKey: &types.HardwareKey{
|
|
SerialNumberValidation: &types.HardwareKeySerialNumberValidation{
|
|
Enabled: true,
|
|
},
|
|
},
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
|
|
SerialNumber: 12345678,
|
|
},
|
|
assertErr: require.NoError,
|
|
}, {
|
|
name: "partial serial number is unknown",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
HardwareKey: &types.HardwareKey{
|
|
SerialNumberValidation: &types.HardwareKeySerialNumberValidation{
|
|
Enabled: true,
|
|
},
|
|
},
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
|
|
SerialNumber: 1234,
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.True(t, trace.IsBadParameter(err), "expected bad parameter error but got %v", err)
|
|
require.ErrorContains(t, err, "unknown hardware key")
|
|
},
|
|
}, {
|
|
name: "known hardware key custom trait name",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
HardwareKey: &types.HardwareKey{
|
|
SerialNumberValidation: &types.HardwareKeySerialNumberValidation{
|
|
Enabled: true,
|
|
SerialNumberTraitName: "known_yubikeys",
|
|
},
|
|
},
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
|
|
SerialNumber: 13572468,
|
|
},
|
|
assertErr: require.NoError,
|
|
}, {
|
|
name: "unknown hardware key",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
HardwareKey: &types.HardwareKey{
|
|
SerialNumberValidation: &types.HardwareKeySerialNumberValidation{
|
|
Enabled: true,
|
|
},
|
|
},
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
|
|
SerialNumber: 87654321,
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.True(t, trace.IsBadParameter(err), "expected bad parameter error but got %v", err)
|
|
require.ErrorContains(t, err, "unknown hardware key")
|
|
},
|
|
}, {
|
|
name: "no known hardware keys",
|
|
cap: types.AuthPreferenceSpecV2{
|
|
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
|
|
HardwareKey: &types.HardwareKey{
|
|
SerialNumberValidation: &types.HardwareKeySerialNumberValidation{
|
|
Enabled: true,
|
|
SerialNumberTraitName: "none",
|
|
},
|
|
},
|
|
},
|
|
mockAttestationData: &keys.AttestationData{
|
|
PrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
|
|
SerialNumber: 12345678,
|
|
},
|
|
assertErr: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.True(t, trace.IsBadParameter(err), "expected bad parameter error but got %v", err)
|
|
require.ErrorContains(t, err, "no known hardware keys")
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modules.SetTestModules(t, &modules.TestModules{
|
|
MockAttestationData: tt.mockAttestationData,
|
|
})
|
|
|
|
authPref, err := types.NewAuthPreference(tt.cap)
|
|
require.NoError(t, err)
|
|
_, err = p.a.UpsertAuthPreference(ctx, authPref)
|
|
require.NoError(t, err)
|
|
|
|
_, err = p.a.generateUserCert(ctx, certReq)
|
|
tt.assertErr(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.UpsertClusterNetworkingConfig(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 := NewWebSessionRequest{
|
|
User: user.GetName(),
|
|
Roles: user.GetRoles(),
|
|
Traits: user.GetTraits(),
|
|
LoginTime: p.a.clock.Now().UTC(),
|
|
SessionTTL: apidefaults.CertDuration,
|
|
}
|
|
bearerTokenTTL := min(req.SessionTTL, defaults.BearerTokenTTL)
|
|
|
|
ws, err := p.a.newWebSession(ctx, req, nil /* opts */)
|
|
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()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
mockEmitter := &eventstest.MockRecorderEmitter{}
|
|
authServer.emitter = mockEmitter
|
|
|
|
ctx := context.Background()
|
|
|
|
const username = "llama@goteleport.com"
|
|
_, _, err := CreateUserAndRole(authServer, username, []string{username}, nil /* allowRules */)
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional, // "optional" lets all user devices be deleted.
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "localhost",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
userClient, err := testServer.NewClient(TestUser(username))
|
|
require.NoError(t, err)
|
|
|
|
// webDev1 is used as the authenticator for various checks.
|
|
webDev1, err := RegisterTestDevice(ctx, userClient, "web1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
|
|
require.NoError(t, err, "RegisterTestDevice(web1)")
|
|
|
|
// Insert devices for deletion.
|
|
deviceOpts := []TestDeviceOpt{WithTestDeviceClock(testServer.Clock())}
|
|
registerDevice := func(t *testing.T, deviceName string, deviceType proto.DeviceType) *TestDevice {
|
|
t.Helper()
|
|
testDev, err := RegisterTestDevice(
|
|
ctx, userClient, deviceName, deviceType, webDev1 /* authenticator */, deviceOpts...)
|
|
require.NoError(t, err, "RegisterTestDevice(%v)", deviceName)
|
|
return testDev
|
|
}
|
|
deleteWeb1 := registerDevice(t, "delete-web1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN)
|
|
deleteWeb2 := registerDevice(t, "delete-web2", proto.DeviceType_DEVICE_TYPE_WEBAUTHN)
|
|
deleteTOTP1 := registerDevice(t, "delete-totp1", proto.DeviceType_DEVICE_TYPE_TOTP)
|
|
deleteTOTP2 := registerDevice(t, "delete-totp2", proto.DeviceType_DEVICE_TYPE_TOTP)
|
|
|
|
deleteReqUsingToken := func(tokenReq CreateUserTokenRequest) func(t *testing.T) *proto.DeleteMFADeviceSyncRequest {
|
|
return func(t *testing.T) *proto.DeleteMFADeviceSyncRequest {
|
|
token, err := authServer.newUserToken(tokenReq)
|
|
require.NoError(t, err, "newUserToken")
|
|
|
|
_, err = authServer.CreateUserToken(ctx, token)
|
|
require.NoError(t, err, "CreateUserToken")
|
|
|
|
return &proto.DeleteMFADeviceSyncRequest{
|
|
TokenID: token.GetName(),
|
|
}
|
|
}
|
|
}
|
|
|
|
deleteReqUsingChallenge := func(authenticator *TestDevice) func(t *testing.T) *proto.DeleteMFADeviceSyncRequest {
|
|
return func(t *testing.T) *proto.DeleteMFADeviceSyncRequest {
|
|
authnChal, err := userClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
|
|
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
|
|
ContextUser: &proto.ContextUser{},
|
|
},
|
|
})
|
|
require.NoError(t, err, "CreateAuthenticateChallenge")
|
|
|
|
authnSolved, err := authenticator.SolveAuthn(authnChal)
|
|
require.NoError(t, err, "SolveAuthn")
|
|
|
|
return &proto.DeleteMFADeviceSyncRequest{
|
|
ExistingMFAResponse: authnSolved,
|
|
}
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
createDeleteReq func(t *testing.T) *proto.DeleteMFADeviceSyncRequest
|
|
deviceToDelete string
|
|
}{
|
|
{
|
|
name: "recovery approved token",
|
|
createDeleteReq: deleteReqUsingToken(CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeRecoveryApproved,
|
|
}),
|
|
deviceToDelete: deleteWeb1.MFA.GetName(),
|
|
},
|
|
{
|
|
name: "privilege token",
|
|
createDeleteReq: deleteReqUsingToken(CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypePrivilege,
|
|
}),
|
|
deviceToDelete: deleteTOTP1.MFA.GetName(),
|
|
},
|
|
{
|
|
name: "Webauthn using challenge",
|
|
createDeleteReq: deleteReqUsingChallenge(webDev1),
|
|
deviceToDelete: deleteWeb2.MFA.GetName(),
|
|
},
|
|
{
|
|
name: "TOTP using challenge",
|
|
createDeleteReq: deleteReqUsingChallenge(webDev1),
|
|
deviceToDelete: deleteTOTP2.MFA.GetName(),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
deleteReq := test.createDeleteReq(t)
|
|
deleteReq.DeviceName = test.deviceToDelete
|
|
|
|
// Delete the device.
|
|
mockEmitter.Reset()
|
|
err = userClient.DeleteMFADeviceSync(ctx, deleteReq)
|
|
require.NoError(t, err, "DeleteMFADeviceSync failed")
|
|
|
|
// Verify device deletion.
|
|
devs, err := authServer.Services.GetMFADevices(ctx, username, false /* withSecrets */)
|
|
require.NoError(t, err, "GetMFADevices failed")
|
|
for _, dev := range devs {
|
|
if dev.GetName() == test.deviceToDelete {
|
|
t.Errorf("DeleteMFADeviceSync(%q): device not deleted", test.deviceToDelete)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Verify deletion event.
|
|
event := mockEmitter.LastEvent()
|
|
assert.Equal(t, events.MFADeviceDeleteEvent, event.GetType(), "event.Type")
|
|
assert.Equal(t, events.MFADeviceDeleteEventCode, event.GetCode(), "event.Code")
|
|
require.IsType(t, &apievents.MFADeviceDelete{}, event, "underlying event type")
|
|
deleteEvent := event.(*apievents.MFADeviceDelete) // asserted above
|
|
assert.Equal(t, username, deleteEvent.User, "event.User")
|
|
assert.Contains(t, deleteEvent.ConnectionMetadata.RemoteAddr, "127.0.0.1", "client remote addr must be localhost")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteMFADeviceSync_WithErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
clock := testServer.Clock()
|
|
ctx := context.Background()
|
|
|
|
const username = "llama@goteleport.com"
|
|
_, _, err := CreateUserAndRole(authServer, username, []string{username}, nil)
|
|
require.NoError(t, err)
|
|
|
|
const origin = "localhost"
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOptional,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: origin,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
userClient, err := testServer.NewClient(TestUser(username))
|
|
require.NoError(t, err)
|
|
|
|
// Insert a device.
|
|
const devName = "otp"
|
|
_, err = RegisterTestDevice(
|
|
ctx, userClient, devName, proto.DeviceType_DEVICE_TYPE_TOTP, nil /* authenticator */, WithTestDeviceClock(clock))
|
|
require.NoError(t, err)
|
|
|
|
createReq := func(name string) *proto.DeleteMFADeviceSyncRequest {
|
|
return &proto.DeleteMFADeviceSyncRequest{
|
|
DeviceName: name,
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
tokenRequest *CreateUserTokenRequest
|
|
deleteReq *proto.DeleteMFADeviceSyncRequest
|
|
wantErr string
|
|
assertErr func(error) bool
|
|
}{
|
|
{
|
|
name: "token not found",
|
|
deleteReq: &proto.DeleteMFADeviceSyncRequest{
|
|
TokenID: "unknown-token-id",
|
|
DeviceName: devName,
|
|
},
|
|
wantErr: "invalid token",
|
|
assertErr: trace.IsAccessDenied,
|
|
},
|
|
{
|
|
name: "invalid token type",
|
|
tokenRequest: &CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: "unknown-token-type",
|
|
},
|
|
deleteReq: createReq(devName),
|
|
wantErr: "invalid token",
|
|
assertErr: trace.IsAccessDenied,
|
|
},
|
|
{
|
|
name: "device not found",
|
|
tokenRequest: &CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeRecoveryApproved,
|
|
},
|
|
deleteReq: &proto.DeleteMFADeviceSyncRequest{
|
|
DeviceName: "does-not-exist",
|
|
},
|
|
wantErr: "does not exist",
|
|
assertErr: trace.IsNotFound,
|
|
},
|
|
{
|
|
name: "neither token nor challenge provided",
|
|
deleteReq: createReq(devName),
|
|
wantErr: "either a privilege token or",
|
|
assertErr: trace.IsBadParameter,
|
|
},
|
|
{
|
|
name: "invalid challenge",
|
|
deleteReq: &proto.DeleteMFADeviceSyncRequest{
|
|
DeviceName: devName,
|
|
ExistingMFAResponse: &proto.MFAAuthenticateResponse{
|
|
Response: &proto.MFAAuthenticateResponse_TOTP{
|
|
TOTP: &proto.TOTPResponse{
|
|
Code: "not an OTP code",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: "invalid totp token",
|
|
assertErr: trace.IsAccessDenied,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
deleteReq := test.deleteReq
|
|
|
|
if test.tokenRequest != nil {
|
|
token, err := authServer.newUserToken(*test.tokenRequest)
|
|
require.NoError(t, err)
|
|
_, err = authServer.CreateUserToken(context.Background(), token)
|
|
require.NoError(t, err)
|
|
|
|
deleteReq.TokenID = token.GetName()
|
|
}
|
|
|
|
err := userClient.DeleteMFADeviceSync(ctx, deleteReq)
|
|
assert.ErrorContains(t, err, test.wantErr, "DeleteMFADeviceSync error mismatch")
|
|
assert.True(t,
|
|
test.assertErr(err),
|
|
"DeleteMFADeviceSync error type assertion failed, got err=%q (%T)", err, trace.Unwrap(err))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeleteMFADeviceSync_lastDevice tests for preventing deletion of last
|
|
// device when second factor is required.
|
|
func TestDeleteMFADeviceSync_lastDevice(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
ctx := context.Background()
|
|
|
|
// Create a user with TOTP and Webauthn.
|
|
userCreds, err := createUserWithSecondFactors(testServer)
|
|
require.NoError(t, err, "createUserWithSecondFactors")
|
|
|
|
userClient, err := testServer.NewClient(TestUser(userCreds.username))
|
|
require.NoError(t, err, "NewClient")
|
|
totpDev := userCreds.totpDev
|
|
webDev := userCreds.webDev
|
|
|
|
// Reuse Webauthn config from createUserWithSecondFactors.
|
|
authPref, err := authServer.GetAuthPreference(ctx)
|
|
require.NoError(t, err, "GetAuthPreference")
|
|
webConfig, _ := authPref.GetWebauthn()
|
|
|
|
// Define various test helpers.
|
|
setSecondFactor := func(t *testing.T, sf constants.SecondFactorType) {
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: sf,
|
|
Webauthn: webConfig,
|
|
})
|
|
require.NoError(t, err, "NewAuthPreference")
|
|
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err, "UpsertAuthPreference")
|
|
}
|
|
|
|
deleteDevice := func(userClient *authclient.Client, testDev *TestDevice) error {
|
|
// Issue and solve authn challenge.
|
|
authnChal, err := userClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
|
|
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
|
|
ContextUser: &proto.ContextUser{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
authnSolved, err := testDev.SolveAuthn(authnChal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return userClient.DeleteMFADeviceSync(ctx, &proto.DeleteMFADeviceSyncRequest{
|
|
DeviceName: testDev.MFA.GetName(),
|
|
ExistingMFAResponse: authnSolved,
|
|
})
|
|
}
|
|
|
|
makeTest := func(sf constants.SecondFactorType, deviceToDelete *TestDevice) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Helper()
|
|
|
|
setSecondFactor(t, sf)
|
|
|
|
// Attempt deletion.
|
|
const wantErr = "cannot delete the last"
|
|
assert.ErrorContains(t,
|
|
deleteDevice(userClient, deviceToDelete),
|
|
wantErr)
|
|
|
|
devicesResp, err := userClient.GetMFADevices(ctx, &proto.GetMFADevicesRequest{})
|
|
require.NoError(t, err, "GetMFADevices")
|
|
devName := deviceToDelete.MFA.GetName()
|
|
for _, dev := range devicesResp.Devices {
|
|
if dev.GetName() == devName {
|
|
return // Success, device not deleted.
|
|
}
|
|
}
|
|
t.Errorf("Device %q wrongly deleted", devName)
|
|
}
|
|
}
|
|
|
|
// First attempt deletions on specific modes like TOTP and Webauthn.
|
|
// These shouldn't work because we only have one of each device.
|
|
t.Run("second factor otp", makeTest(constants.SecondFactorOTP, totpDev))
|
|
t.Run("second factor webauthn", makeTest(constants.SecondFactorWebauthn, webDev))
|
|
|
|
// Make sure only one device is left, then attempt deletion with
|
|
// second_factor=on.
|
|
setSecondFactor(t, constants.SecondFactorOptional)
|
|
require.NoError(t,
|
|
deleteDevice(userClient, webDev),
|
|
"Second-to-last device deletion failed",
|
|
)
|
|
t.Run("second factor on, otp device", makeTest(constants.SecondFactorOn, totpDev))
|
|
|
|
// Same as above, but now delete the last Webauthn device.
|
|
webDev, err = RegisterTestDevice(ctx, userClient, "web1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, totpDev /* authenticator */)
|
|
require.NoError(t, err, "RegisterTestDevice")
|
|
require.NoError(t,
|
|
deleteDevice(userClient, totpDev),
|
|
"Second-to-last device deletion failed",
|
|
)
|
|
t.Run("second factor on, otp device", makeTest(constants.SecondFactorOn, webDev))
|
|
}
|
|
|
|
func TestAddMFADeviceSync(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testServer := newTestTLSServer(t)
|
|
authServer := testServer.Auth()
|
|
mockEmitter := &eventstest.MockRecorderEmitter{}
|
|
authServer.SetEmitter(mockEmitter)
|
|
clock := authServer.GetClock()
|
|
ctx := context.Background()
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "localhost",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
u, err := createUserWithSecondFactors(testServer)
|
|
require.NoError(t, err)
|
|
|
|
userClient, err := testServer.NewClient(TestUser(u.username))
|
|
require.NoError(t, err)
|
|
|
|
solveChallengeWithToken := func(
|
|
t *testing.T,
|
|
tokenType string,
|
|
deviceType proto.DeviceType,
|
|
deviceUsage proto.DeviceUsage,
|
|
) (token string, testDev *TestDevice, registerSolved *proto.MFARegisterResponse) {
|
|
privilegeToken, err := authServer.createPrivilegeToken(ctx, u.username, tokenType)
|
|
require.NoError(t, err, "createPrivilegeToken")
|
|
token = privilegeToken.GetName()
|
|
|
|
registerChal, err := authServer.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
|
|
TokenID: token,
|
|
DeviceType: deviceType,
|
|
DeviceUsage: deviceUsage,
|
|
})
|
|
require.NoError(t, err, "CreateRegisterChallenge")
|
|
|
|
testDev, registerSolved, err = NewTestDeviceFromChallenge(registerChal, WithTestDeviceClock(clock))
|
|
require.NoError(t, err, "NewTestDeviceFromChallenge")
|
|
return token, testDev, registerSolved
|
|
}
|
|
|
|
solveChallengeWithUser := func(
|
|
t *testing.T,
|
|
deviceType proto.DeviceType,
|
|
deviceUsage proto.DeviceUsage,
|
|
) (*TestDevice, *proto.MFARegisterResponse) {
|
|
// Create and solve a registration challenge.
|
|
authChal, err := userClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
|
|
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
|
|
ContextUser: &proto.ContextUser{},
|
|
},
|
|
})
|
|
require.NoError(t, err, "CreateAuthenticateChallenge")
|
|
|
|
authSolved, err := u.webDev.SolveAuthn(authChal)
|
|
require.NoError(t, err, "SolveAuthn")
|
|
|
|
registerChal, err := userClient.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
|
|
ExistingMFAResponse: authSolved,
|
|
DeviceType: deviceType,
|
|
DeviceUsage: deviceUsage,
|
|
})
|
|
require.NoError(t, err, "CreateRegisterChallenge")
|
|
|
|
testDev, registerSolved, err := NewTestDeviceFromChallenge(
|
|
registerChal,
|
|
WithTestDeviceClock(clock),
|
|
)
|
|
require.NoError(t, err, "NewTestDeviceFromChallenge")
|
|
return testDev, registerSolved
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
deviceName string
|
|
wantErr bool
|
|
getReq func(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest
|
|
}{
|
|
{
|
|
name: "invalid token type",
|
|
wantErr: true,
|
|
getReq: func(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
// Obtain a non privilege token.
|
|
token, err := authServer.newUserToken(CreateUserTokenRequest{
|
|
Name: u.username,
|
|
TTL: 5 * time.Minute,
|
|
Type: UserTokenTypeResetPassword,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = authServer.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(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
token, _, registerSolved := solveChallengeWithToken(
|
|
t, UserTokenTypePrivilege, proto.DeviceType_DEVICE_TYPE_TOTP, proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: token,
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: registerSolved,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Webauthn device with privilege exception token",
|
|
deviceName: "new-webauthn",
|
|
getReq: func(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
token, _, registerSolved := solveChallengeWithToken(
|
|
t, UserTokenTypePrivilegeException, proto.DeviceType_DEVICE_TYPE_WEBAUTHN, proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: token,
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: registerSolved,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid device name length",
|
|
deviceName: strings.Repeat("A", mfaDeviceNameMaxLen+1),
|
|
wantErr: true,
|
|
getReq: func(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
token, _, registerSolved := solveChallengeWithToken(
|
|
t, UserTokenTypePrivilegeException, proto.DeviceType_DEVICE_TYPE_WEBAUTHN, proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
TokenID: token,
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: registerSolved,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "WebAuthn with context user",
|
|
deviceName: "context-webauthn1",
|
|
getReq: func(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
_, registerSolved := solveChallengeWithUser(
|
|
t,
|
|
proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
|
|
proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
ContextUser: &proto.ContextUser{},
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: registerSolved,
|
|
DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_MFA,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "TOTP with context user",
|
|
deviceName: "context-totp1",
|
|
getReq: func(t *testing.T, deviceName string) *proto.AddMFADeviceSyncRequest {
|
|
_, registerSolved := solveChallengeWithUser(
|
|
t,
|
|
proto.DeviceType_DEVICE_TYPE_TOTP,
|
|
proto.DeviceUsage_DEVICE_USAGE_MFA)
|
|
|
|
return &proto.AddMFADeviceSyncRequest{
|
|
ContextUser: &proto.ContextUser{},
|
|
NewDeviceName: deviceName,
|
|
NewMFAResponse: registerSolved,
|
|
DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_MFA,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
res, err := userClient.AddMFADeviceSync(ctx, tc.getReq(t, 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())
|
|
addEvt := event.(*apievents.MFADeviceAdd)
|
|
require.Equal(t, u.username, addEvt.UserMetadata.User)
|
|
assert.Contains(t, addEvt.ConnectionMetadata.RemoteAddr, "127.0.0.1", "client remote addr must be localhost")
|
|
|
|
// Check it's been added.
|
|
res, err := userClient.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().UpsertAuthPreference(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().UpsertAuthPreference(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{
|
|
TrustInternal: local.NewCAService(bk),
|
|
PresenceInternal: local.NewPresenceService(bk),
|
|
Provisioner: local.NewProvisioningService(bk),
|
|
Identity: local.NewTestIdentityService(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.Error(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.NoError(t, err)
|
|
assert.Equal(t, fmt.Sprintf("%s%s", l.CertPEM, l.KeyPEM), actual)
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
func TestGetTokens(t *testing.T) {
|
|
t.Parallel()
|
|
s := newAuthSuite(t)
|
|
ctx := context.Background()
|
|
|
|
_, _, err := CreateUserAndRole(s.a, "username", []string{"username"}, nil)
|
|
require.NoError(t, err)
|
|
_, err = s.a.CreateResetPasswordToken(ctx, CreateUserTokenRequest{
|
|
Name: "username",
|
|
TTL: time.Minute,
|
|
Type: UserTokenTypeResetPasswordInvite,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
for _, role := range types.LocalServiceMappings() {
|
|
generateTestToken(ctx, t, types.SystemRoles{role}, s.a.GetClock().Now().Add(time.Minute*30), s.a)
|
|
}
|
|
|
|
_, err = s.a.GetTokens(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAccessRequestAuditLog(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
requester, _, _ := createSessionTestUsers(t, p.a)
|
|
|
|
paymentsRole, err := types.NewRole("paymentsRole", types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Request: &types.AccessRequestConditions{
|
|
Roles: []string{"requestRole"},
|
|
Annotations: map[string][]string{
|
|
"pagerduty_services": {"payments"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
requestRole, err := types.NewRole("requestRole", types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
|
|
p.a.CreateRole(ctx, requestRole)
|
|
p.a.CreateRole(ctx, paymentsRole)
|
|
|
|
user, err := p.a.GetUser(ctx, requester, true)
|
|
require.NoError(t, err)
|
|
user.AddRole(paymentsRole.GetName())
|
|
_, err = p.a.UpsertUser(ctx, user)
|
|
require.NoError(t, err)
|
|
|
|
accessRequest, err := types.NewAccessRequest(uuid.NewString(), requester, "requestRole")
|
|
require.NoError(t, err)
|
|
req, err := p.a.CreateAccessRequestV2(ctx, accessRequest, TestUser(requester).I.GetIdentity())
|
|
require.NoError(t, err)
|
|
|
|
expectedAnnotations, err := apievents.EncodeMapStrings(paymentsRole.GetAccessRequestConditions(types.Allow).Annotations)
|
|
require.NoError(t, err)
|
|
|
|
arc, ok := p.mockEmitter.LastEvent().(*apievents.AccessRequestCreate)
|
|
require.True(t, ok)
|
|
require.Equal(t, expectedAnnotations, arc.Annotations)
|
|
require.Equal(t, "PENDING", arc.RequestState)
|
|
|
|
err = p.a.SetAccessRequestState(ctx, types.AccessRequestUpdate{
|
|
RequestID: req.GetName(),
|
|
State: types.RequestState_APPROVED,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
arc, ok = p.mockEmitter.LastEvent().(*apievents.AccessRequestCreate)
|
|
require.True(t, ok)
|
|
require.Equal(t, expectedAnnotations, arc.Annotations)
|
|
require.Equal(t, "APPROVED", arc.RequestState)
|
|
}
|