mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
* Remove client invocations of deprecated GetTokens RPC * slightly tidy instantiation of all tokens slices in GetAllTokens
6674 lines
201 KiB
Go
6674 lines
201 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_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base32"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/pquerna/otp/totp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/breaker"
|
|
"github.com/gravitational/teleport/api/client"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
apidefaults "github.com/gravitational/teleport/api/defaults"
|
|
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
|
presencev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1"
|
|
scopedaccessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
|
|
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
|
|
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
|
|
userspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/users/v1"
|
|
"github.com/gravitational/teleport/api/metadata"
|
|
"github.com/gravitational/teleport/api/types"
|
|
eventtypes "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
apiutils "github.com/gravitational/teleport/api/utils"
|
|
"github.com/gravitational/teleport/api/utils/clientutils"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/authcatest"
|
|
"github.com/gravitational/teleport/lib/auth/authclient"
|
|
"github.com/gravitational/teleport/lib/auth/authtest"
|
|
"github.com/gravitational/teleport/lib/auth/state"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/authz"
|
|
"github.com/gravitational/teleport/lib/cryptosuites"
|
|
"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/itertools/stream"
|
|
"github.com/gravitational/teleport/lib/join/joinclient"
|
|
"github.com/gravitational/teleport/lib/jwt"
|
|
"github.com/gravitational/teleport/lib/modules/modulestest"
|
|
"github.com/gravitational/teleport/lib/scopes"
|
|
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/sshca"
|
|
libsshutils "github.com/gravitational/teleport/lib/sshutils"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
func TestRejectedClients(t *testing.T) {
|
|
t.Parallel()
|
|
server, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
ClusterName: "cluster",
|
|
Clock: clockwork.NewFakeClock(),
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, server.Close()) })
|
|
|
|
user, _, err := authtest.CreateUserAndRole(server.AuthServer, "user", []string{"role"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
tlsServer, err := server.NewTestTLSServer()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, tlsServer.Close()) })
|
|
defer tlsServer.Close()
|
|
|
|
tlsConfig, err := tlsServer.ClientTLSConfig(authtest.TestUser(user.GetName()))
|
|
require.NoError(t, err)
|
|
|
|
clt, err := authclient.NewClient(client.Config{
|
|
DialInBackground: true,
|
|
Addrs: []string{tlsServer.Addr().String()},
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: breaker.NoopBreakerConfig(),
|
|
})
|
|
require.NoError(t, err)
|
|
defer clt.Close()
|
|
|
|
t.Run("reject old version", func(t *testing.T) {
|
|
version := teleport.MinClientSemVer()
|
|
version.Major--
|
|
ctx := context.WithValue(context.Background(), metadata.DisableInterceptors{}, struct{}{})
|
|
ctx = metadata.AddMetadataToContext(ctx, map[string]string{
|
|
metadata.VersionKey: version.String(),
|
|
})
|
|
resp, err := clt.Ping(ctx)
|
|
require.True(t, trace.IsConnectionProblem(err))
|
|
require.Equal(t, proto.PingResponse{}, resp)
|
|
})
|
|
|
|
t.Run("allow valid versions", func(t *testing.T) {
|
|
version := teleport.MinClientSemVer()
|
|
version.Major--
|
|
for range 5 {
|
|
version.Major++
|
|
|
|
ctx := context.WithValue(context.Background(), metadata.DisableInterceptors{}, struct{}{})
|
|
ctx = metadata.AddMetadataToContext(ctx, map[string]string{
|
|
metadata.VersionKey: version.String(),
|
|
})
|
|
resp, err := clt.Ping(ctx)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRemoteBuiltinRole tests remote builtin role
|
|
// that gets mapped to remote proxy readonly role
|
|
func TestRemoteBuiltinRole(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
remoteServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
ClusterName: "remote",
|
|
Clock: testSrv.AuthServer.AuthServerConfig.Clock,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteServer.Close()) })
|
|
|
|
certPool, err := testSrv.CertPool()
|
|
require.NoError(t, err)
|
|
|
|
// without trust, proxy server will get rejected
|
|
// remote auth server will get rejected because it is not supported
|
|
remoteProxy, err := remoteServer.NewRemoteClient(
|
|
authtest.TestBuiltin(types.RoleProxy), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteProxy.Close()) })
|
|
|
|
// certificate authority is not recognized, because
|
|
// the trust has not been established yet
|
|
_, err = remoteProxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.True(t, trace.IsConnectionProblem(err))
|
|
|
|
// after trust is established, things are good
|
|
err = testSrv.AuthServer.Trust(ctx, remoteServer, nil)
|
|
require.NoError(t, err)
|
|
|
|
// re initialize client with trust established.
|
|
//
|
|
// not using Eventually(WithT) because almost all the time the first try
|
|
// will work and Eventually will always wait for the interval to happen, and
|
|
// there's also no reason to retry if somehow we fail to build the client
|
|
for range 50 {
|
|
remoteProxy, err = remoteServer.NewRemoteClient(
|
|
authtest.TestBuiltin(types.RoleProxy), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
|
|
_, err = remoteProxy.GetNodes(ctx, apidefaults.Namespace)
|
|
remoteProxy.Close()
|
|
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// remote auth server will get rejected even with established trust
|
|
remoteAuth, err := remoteServer.NewRemoteClient(
|
|
authtest.TestBuiltin(types.RoleAuth), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteAuth.Close()) })
|
|
|
|
_, err = remoteAuth.GetDomainName(ctx)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
|
|
// TestAcceptedUsage tests scenario when server is set up
|
|
// to accept certificates with certain usage metadata restrictions
|
|
// encoded
|
|
func TestAcceptedUsage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
server, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
ClusterName: "remote",
|
|
AcceptedUsage: []string{"usage:k8s"},
|
|
Clock: clockwork.NewFakeClock(),
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, server.Close()) })
|
|
|
|
user, _, err := authtest.CreateUserAndRole(server.AuthServer, "user", []string{"role"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
tlsServer, err := server.NewTestTLSServer()
|
|
require.NoError(t, err)
|
|
defer tlsServer.Close()
|
|
|
|
// Unrestricted clients can use restricted servers
|
|
client, err := tlsServer.NewClient(authtest.TestUser(user.GetName()))
|
|
require.NoError(t, err)
|
|
|
|
// certificate authority is not recognized, because
|
|
// the trust has not been established yet
|
|
_, err = client.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// restricted clients can use restricted servers if restrictions
|
|
// exactly match
|
|
identity := authtest.TestUser(user.GetName())
|
|
identity.AcceptedUsage = []string{"usage:k8s"}
|
|
client, err = tlsServer.NewClient(identity)
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// restricted clients can will be rejected if usage does not match
|
|
identity = authtest.TestUser(user.GetName())
|
|
identity.AcceptedUsage = []string{"usage:extra"}
|
|
client, err = tlsServer.NewClient(identity)
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.GetNodes(ctx, apidefaults.Namespace)
|
|
require.Error(t, err)
|
|
|
|
// restricted clients can will be rejected, for now if there is any mismatch,
|
|
// including extra usage.
|
|
identity = authtest.TestUser(user.GetName())
|
|
identity.AcceptedUsage = []string{"usage:k8s", "usage:unknown"}
|
|
client, err = tlsServer.NewClient(identity)
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.GetNodes(ctx, apidefaults.Namespace)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestRemoteRotation tests remote builtin role
|
|
// that attempts certificate authority rotation
|
|
func TestRemoteRotation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
var ok bool
|
|
|
|
remoteServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
ClusterName: "remote",
|
|
Clock: testSrv.AuthServer.AuthServerConfig.Clock,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteServer.Close()) })
|
|
|
|
certPool, err := testSrv.CertPool()
|
|
require.NoError(t, err)
|
|
|
|
// after trust is established, things are good
|
|
err = testSrv.AuthServer.Trust(ctx, remoteServer, nil)
|
|
require.NoError(t, err)
|
|
|
|
remoteProxy, err := remoteServer.NewRemoteClient(
|
|
authtest.TestBuiltin(types.RoleProxy), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteProxy.Close()) })
|
|
|
|
remoteAuth, err := remoteServer.NewRemoteClient(
|
|
authtest.TestBuiltin(types.RoleAuth), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteAuth.Close()) })
|
|
|
|
remoteCA, err := remoteServer.AuthServer.Services.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: remoteServer.ClusterName,
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
// a CA that was never rotated has a blank state, which is equal to "standby"
|
|
require.Contains(t, []string{"", types.RotationStateStandby}, remoteCA.GetRotation().State)
|
|
|
|
// remote cluster starts rotation
|
|
gracePeriod := time.Hour
|
|
privateKey, ok := fixtures.PEMBytes["rsa"]
|
|
require.True(t, ok)
|
|
|
|
testSrv.Auth().SetPrivateKey(privateKey)
|
|
err = remoteServer.AuthServer.RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// moves to update clients
|
|
err = remoteServer.AuthServer.RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateClients,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remoteCA, err = remoteServer.AuthServer.Services.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: remoteServer.ClusterName,
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationStateInProgress, remoteCA.GetRotation().State)
|
|
require.Equal(t, types.RotationPhaseUpdateClients, remoteCA.GetRotation().Phase)
|
|
|
|
// remote proxy should be rejected when trying to rotate ca
|
|
// that is not associated with the remote cluster
|
|
clone := remoteCA.Clone()
|
|
clone.SetName(testSrv.ClusterName())
|
|
err = remoteProxy.RotateExternalCertAuthority(ctx, clone)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// remote proxy can't upsert the certificate authority,
|
|
// only to rotate it (in remote rotation only certain fields are updated)
|
|
err = remoteProxy.UpsertCertAuthority(ctx, remoteCA)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// remote proxy can't read local cert authority with secrets
|
|
_, err = remoteProxy.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// no secrets read is allowed
|
|
_, err = remoteProxy.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
|
|
// remote auth server will get rejected
|
|
err = remoteAuth.RotateExternalCertAuthority(ctx, remoteCA)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// remote proxy should be able to perform remote cert authority
|
|
// rotation
|
|
err = remoteProxy.RotateExternalCertAuthority(ctx, remoteCA)
|
|
require.NoError(t, err)
|
|
|
|
// newRemoteProxy should be trusted by the auth server
|
|
newRemoteProxy, err := remoteServer.NewRemoteClient(
|
|
authtest.TestBuiltin(types.RoleProxy), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
|
|
// the testSrv listener needs to wait for a Put event on the CA that has
|
|
// just rotated to refresh its TLS config
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err = newRemoteProxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
}, 30*time.Second, 100*time.Millisecond)
|
|
|
|
// old proxy client is still trusted
|
|
_, err = testSrv.CloneClient(t, remoteProxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestLocalProxyPermissions tests new local proxy permissions
|
|
// as it's now allowed to update host cert authorities of remote clusters
|
|
func TestLocalProxyPermissions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
remoteServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
ClusterName: "remote",
|
|
Clock: testSrv.AuthServer.AuthServerConfig.Clock,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteServer.Close()) })
|
|
|
|
// after trust is established, things are good
|
|
err = testSrv.AuthServer.Trust(ctx, remoteServer, nil)
|
|
require.NoError(t, err)
|
|
|
|
ca, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, proxy.Close()) })
|
|
|
|
// local proxy can't update local cert authorities
|
|
err = proxy.UpsertCertAuthority(ctx, ca)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// local proxy is allowed to update host CA of remote cert authorities
|
|
remoteCA, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: remoteServer.ClusterName,
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
|
|
err = proxy.UpsertCertAuthority(ctx, remoteCA)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestAutoRotation tests local automatic rotation
|
|
func TestAutoRotation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
clock := clockwork.NewFakeClock()
|
|
testSrv := newTestTLSServer(t, withClock(clock))
|
|
|
|
var ok bool
|
|
|
|
// create proxy client
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// client works before rotation is initiated
|
|
_, err = proxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// starts rotation
|
|
privateKey, ok := fixtures.PEMBytes["rsa"]
|
|
require.True(t, ok)
|
|
testSrv.Auth().SetPrivateKey(privateKey)
|
|
gracePeriod := time.Hour
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
Mode: types.RotationModeAuto,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// advance rotation by clock
|
|
clock.Advance(gracePeriod/3 + time.Minute)
|
|
err = testSrv.Auth().AutoRotateCertAuthorities(ctx)
|
|
require.NoError(t, err)
|
|
|
|
ca, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateClients, ca.GetRotation().Phase)
|
|
|
|
// old clients should work
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// new clients work as well
|
|
_, err = testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// advance rotation by clock
|
|
clock.Advance((gracePeriod*2)/3 + time.Minute)
|
|
err = testSrv.Auth().AutoRotateCertAuthorities(ctx)
|
|
require.NoError(t, err)
|
|
|
|
ca, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateServers, ca.GetRotation().Phase)
|
|
|
|
// old clients should work
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// new clients work as well
|
|
newProxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
_, err = newProxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// complete rotation - advance rotation by clock
|
|
clock.Advance(gracePeriod/3 + time.Minute)
|
|
err = testSrv.Auth().AutoRotateCertAuthorities(ctx)
|
|
require.NoError(t, err)
|
|
ca, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseStandby, ca.GetRotation().Phase)
|
|
require.NoError(t, err)
|
|
|
|
// old clients should no longer work as soon as backend modification event propagates.
|
|
// new client has to be created here to force re-create the new connection instead of
|
|
// re-using the one from pool this is not going to be a problem in real teleport
|
|
// as it reloads the full server after reload
|
|
require.Eventually(t, func() bool {
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
// TODO(rosstimothy, espadolini, jakule): figure out how to consistently
|
|
// match a certificate error and not other errors
|
|
return err != nil
|
|
}, time.Second*15, time.Millisecond*200)
|
|
|
|
// new clients work
|
|
_, err = testSrv.CloneClient(t, newProxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestAutoFallback tests local automatic rotation fallback,
|
|
// when user intervenes with rollback and rotation gets switched
|
|
// to manual mode
|
|
func TestAutoFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
clock := clockwork.NewFakeClock()
|
|
testSrv := newTestTLSServer(t, withClock(clock))
|
|
|
|
var ok bool
|
|
|
|
// create proxy client just for test purposes
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// client works before rotation is initiated
|
|
_, err = proxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// starts rotation
|
|
privateKey, ok := fixtures.PEMBytes["rsa"]
|
|
require.True(t, ok)
|
|
|
|
testSrv.Auth().SetPrivateKey(privateKey)
|
|
gracePeriod := time.Hour
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
Mode: types.RotationModeAuto,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// advance rotation by clock
|
|
clock.Advance(gracePeriod/3 + time.Minute)
|
|
err = testSrv.Auth().AutoRotateCertAuthorities(ctx)
|
|
require.NoError(t, err)
|
|
|
|
ca, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateClients, ca.GetRotation().Phase)
|
|
require.Equal(t, types.RotationModeAuto, ca.GetRotation().Mode)
|
|
|
|
// rollback rotation
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseRollback,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ca, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseRollback, ca.GetRotation().Phase)
|
|
require.Equal(t, types.RotationModeManual, ca.GetRotation().Mode)
|
|
}
|
|
|
|
// TestManualRotation tests local manual rotation
|
|
// that performs full-cycle certificate authority rotation
|
|
func TestManualRotation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
var ok bool
|
|
|
|
// create proxy client just for test purposes
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// client works before rotation is initiated
|
|
_, err = proxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// can't jump to mid-phase
|
|
gracePeriod := time.Hour
|
|
privateKey, ok := fixtures.PEMBytes["rsa"]
|
|
require.True(t, ok)
|
|
|
|
testSrv.Auth().SetPrivateKey(privateKey)
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateServers,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.True(t, trace.IsBadParameter(err))
|
|
|
|
// starts rotation
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// old clients should work
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// clients reconnect
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateClients,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// old clients should work
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// new clients work as well
|
|
newProxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
_, err = newProxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// can't jump to standy
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseStandby,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.True(t, trace.IsBadParameter(err))
|
|
|
|
// advance rotation:
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateServers,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// old clients should work
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// new clients work as well
|
|
_, err = testSrv.CloneClient(t, newProxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// complete rotation
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseStandby,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// old clients should no longer work as soon as backend modification event propagates.
|
|
// new client has to be created here to force re-create the new connection instead of
|
|
// re-using the one from pool this is not going to be a problem in real teleport
|
|
// as it reloads the full server after reload
|
|
require.Eventually(t, func() bool {
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
return err != nil
|
|
}, time.Second*15, time.Millisecond*200)
|
|
|
|
// new clients work
|
|
_, err = testSrv.CloneClient(t, newProxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestRollback tests local manual rotation rollback
|
|
func TestRollback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
var ok bool
|
|
|
|
// create proxy client just for test purposes
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
defer proxy.Close()
|
|
|
|
// client works before rotation is initiated
|
|
_, err = proxy.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// starts rotation
|
|
gracePeriod := time.Hour
|
|
privateKey, ok := fixtures.PEMBytes["rsa"]
|
|
require.True(t, ok)
|
|
testSrv.Auth().SetPrivateKey(privateKey)
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// move to update clients phase
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateClients,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// new clients work
|
|
newProxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
defer newProxy.Close()
|
|
|
|
newClient := func() *authclient.Client {
|
|
return testSrv.CloneClient(t, newProxy)
|
|
}
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err = newClient().GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
}, 15*time.Second, 100*time.Millisecond)
|
|
|
|
// advance rotation:
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateServers,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// rollback rotation
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseRollback,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// new clients work, server still accepts the creds
|
|
// because new clients should re-register and receive new certs
|
|
_, err = testSrv.CloneClient(t, newProxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
// can't jump to other phases
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateClients,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.True(t, trace.IsBadParameter(err))
|
|
|
|
// complete rollback
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseStandby,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// clients with new creds will no longer work as soon as backend modification event propagates.
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err := newClient().GetNodes(ctx, apidefaults.Namespace)
|
|
require.Error(t, err)
|
|
}, time.Second*15, time.Millisecond*100)
|
|
|
|
// clients with old creds will still work
|
|
_, err = testSrv.CloneClient(t, proxy).GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestAppTokenRotation checks that JWT tokens can be rotated and tokens can or
|
|
// can not be validated at the appropriate phase.
|
|
func TestAppTokenRotation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
client, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleApp))
|
|
require.NoError(t, err)
|
|
|
|
// Create a JWT using the current CA, this will become the "old" CA during
|
|
// rotation.
|
|
oldJWT, err := client.GenerateAppToken(context.Background(),
|
|
types.GenerateAppTokenRequest{
|
|
Username: "foo",
|
|
Roles: []string{"bar", "baz"},
|
|
Traits: map[string][]string{
|
|
"trait1": {"value1", "value2"},
|
|
"trait2": {"value3", "value4"},
|
|
"trait3": nil,
|
|
},
|
|
URI: "https://localhost:8080",
|
|
Expires: clock.Now().Add(1 * time.Minute),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Check that the "old" CA can be used to verify tokens.
|
|
oldCA, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.JWTSigner,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Len(t, oldCA.GetTrustedJWTKeyPairs(), 1)
|
|
|
|
// Verify that the JWT token validates with the JWT authority.
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), oldCA.GetTrustedJWTKeyPairs(), oldJWT)
|
|
require.NoError(t, err)
|
|
|
|
// Start rotation and move to initial phase. A new CA will be added (for
|
|
// verification), but requests will continue to be signed by the old CA.
|
|
gracePeriod := time.Hour
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.JWTSigner,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// At this point in rotation, two JWT key pairs should exist.
|
|
oldCA, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.JWTSigner,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseInit, oldCA.GetRotation().Phase)
|
|
require.Len(t, oldCA.GetTrustedJWTKeyPairs(), 2)
|
|
|
|
// Verify that the JWT token validates with the JWT authority.
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), oldCA.GetTrustedJWTKeyPairs(), oldJWT)
|
|
require.NoError(t, err)
|
|
|
|
// Move rotation into the update client phase. In this phase, requests will
|
|
// be signed by the new CA, but the old CA will be around to verify requests.
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.JWTSigner,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateClients,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// New tokens should now fail to validate with the old key.
|
|
newJWT, err := client.GenerateAppToken(ctx,
|
|
types.GenerateAppTokenRequest{
|
|
Username: "foo",
|
|
Roles: []string{"bar", "baz"},
|
|
Traits: map[string][]string{
|
|
"trait1": {"value1", "value2"},
|
|
"trait2": {"value3", "value4"},
|
|
"trait3": nil,
|
|
},
|
|
URI: "https://localhost:8080",
|
|
Expires: clock.Now().Add(1 * time.Minute),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// New tokens will validate with the new key.
|
|
newCA, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.JWTSigner,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateClients, newCA.GetRotation().Phase)
|
|
require.Len(t, newCA.GetTrustedJWTKeyPairs(), 2)
|
|
|
|
// Both JWT should now validate.
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT)
|
|
require.NoError(t, err)
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT)
|
|
require.NoError(t, err)
|
|
|
|
// Move rotation into update servers phase.
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.JWTSigner,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateServers,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// At this point only the phase on the CA should have changed.
|
|
newCA, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.JWTSigner,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateServers, newCA.GetRotation().Phase)
|
|
require.Len(t, newCA.GetTrustedJWTKeyPairs(), 2)
|
|
|
|
// Both JWT should continue to validate.
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT)
|
|
require.NoError(t, err)
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT)
|
|
require.NoError(t, err)
|
|
|
|
// Complete rotation. The old CA will be removed.
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.JWTSigner,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseStandby,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The new CA should now only have a single key.
|
|
newCA, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.JWTSigner,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseStandby, newCA.GetRotation().Phase)
|
|
require.Len(t, newCA.GetTrustedJWTKeyPairs(), 1)
|
|
|
|
// Old token should no longer validate.
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT)
|
|
require.Error(t, err)
|
|
_, err = verifyJWT(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestOIDCIdPTokenRotation checks that OIDC IdP JWT tokens can be rotated and tokens can
|
|
// be validated at the appropriate phase.
|
|
func TestOIDCIdPTokenRotation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
publicAddress := "https://localhost:8080"
|
|
|
|
issuer := "https://my-bucket.s3.amazonaws.com/prefix"
|
|
|
|
proxyServer, err := types.NewServer("proxy-hostname", types.KindProxy, types.ServerSpecV2{
|
|
PublicAddrs: []string{publicAddress},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = clt.UpsertProxyServerWithoutReturn(ctx, proxyServer)
|
|
require.NoError(t, err)
|
|
|
|
integrationName := "my-integration"
|
|
|
|
ig, err := types.NewIntegrationAWSOIDC(
|
|
types.Metadata{Name: integrationName},
|
|
&types.AWSOIDCIntegrationSpecV1{
|
|
RoleARN: "arn:aws:iam::123456789012:role/OpsTeam",
|
|
IssuerS3URI: "s3://my-bucket/prefix",
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = clt.CreateIntegration(ctx, ig)
|
|
require.NoError(t, err)
|
|
|
|
client, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleDiscovery))
|
|
require.NoError(t, err)
|
|
|
|
// Create a JWT using the current CA, this will become the "old" CA during
|
|
// rotation.
|
|
oldJWT, err := client.GenerateAWSOIDCToken(ctx, integrationName)
|
|
require.NoError(t, err)
|
|
|
|
// Check that the "old" CA can be used to verify tokens.
|
|
oldCA, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.OIDCIdPCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Len(t, oldCA.GetTrustedJWTKeyPairs(), 1)
|
|
|
|
// Verify that the JWT token validates with the JWT authority.
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), oldCA.GetTrustedJWTKeyPairs(), oldJWT, issuer)
|
|
require.NoError(t, err, clock.Now())
|
|
|
|
// Start rotation and move to initial phase. A new CA will be added (for
|
|
// verification), but requests will continue to be signed by the old CA.
|
|
gracePeriod := time.Hour
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.OIDCIdPCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// At this point in rotation, two JWT key pairs should exist.
|
|
oldCA, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.OIDCIdPCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseInit, oldCA.GetRotation().Phase)
|
|
require.Len(t, oldCA.GetTrustedJWTKeyPairs(), 2)
|
|
|
|
// Verify that the JWT token validates with the JWT authority.
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), oldCA.GetTrustedJWTKeyPairs(), oldJWT, issuer)
|
|
require.NoError(t, err)
|
|
|
|
// Move rotation into the update client phase. In this phase, requests will
|
|
// be signed by the new CA, but the old CA will be around to verify requests.
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.OIDCIdPCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateClients,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// New tokens should now fail to validate with the old key.
|
|
newJWT, err := client.GenerateAWSOIDCToken(ctx, integrationName)
|
|
require.NoError(t, err)
|
|
|
|
// New tokens will validate with the new key.
|
|
newCA, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.OIDCIdPCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateClients, newCA.GetRotation().Phase)
|
|
require.Len(t, newCA.GetTrustedJWTKeyPairs(), 2)
|
|
|
|
// Both JWT should now validate.
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT, issuer)
|
|
require.NoError(t, err)
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT, issuer)
|
|
require.NoError(t, err)
|
|
|
|
// Move rotation into update servers phase.
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.OIDCIdPCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseUpdateServers,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// At this point only the phase on the CA should have changed.
|
|
newCA, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.OIDCIdPCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseUpdateServers, newCA.GetRotation().Phase)
|
|
require.Len(t, newCA.GetTrustedJWTKeyPairs(), 2)
|
|
|
|
// Both JWT should continue to validate.
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT, issuer)
|
|
require.NoError(t, err)
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT, issuer)
|
|
require.NoError(t, err)
|
|
|
|
// Complete rotation. The old CA will be removed.
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.OIDCIdPCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseStandby,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The new CA should now only have a single key.
|
|
newCA, err = testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.OIDCIdPCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.RotationPhaseStandby, newCA.GetRotation().Phase)
|
|
require.Len(t, newCA.GetTrustedJWTKeyPairs(), 1)
|
|
|
|
// Old token should no longer validate.
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT, issuer)
|
|
require.Error(t, err)
|
|
_, err = verifyJWTAWSOIDC(clock, testSrv.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT, issuer)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestListUsers verifies basic expected behavior of remote list users calls.
|
|
func TestListUsers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
// set up some roles to assign users
|
|
_, err = authtest.CreateRole(ctx, clt, "good-role", types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
_, err = authtest.CreateRole(ctx, clt, "evil-role", types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
|
|
// set up some users with distinct names/labels/roles/traits
|
|
usersToCreate := []struct {
|
|
name string
|
|
labels map[string]string
|
|
roles []string
|
|
traits map[string][]string
|
|
}{
|
|
{
|
|
name: "[email protected]",
|
|
labels: map[string]string{
|
|
"group": "red",
|
|
"location": "mauka",
|
|
},
|
|
roles: []string{"good-role"},
|
|
traits: map[string][]string{"logins": {"good-login"}},
|
|
},
|
|
{
|
|
name: "[email protected]",
|
|
labels: map[string]string{
|
|
"group": "blue",
|
|
"location": "mauka",
|
|
},
|
|
roles: []string{"good-role"},
|
|
traits: map[string][]string{"logins": {"good-login"}},
|
|
},
|
|
{
|
|
name: "[email protected]",
|
|
labels: map[string]string{
|
|
"group": "red",
|
|
"location": "mauka",
|
|
},
|
|
roles: []string{"evil-role"},
|
|
traits: map[string][]string{"logins": {"evil-login"}},
|
|
},
|
|
{
|
|
name: "[email protected]",
|
|
labels: map[string]string{
|
|
"group": "blue",
|
|
"location": "makai",
|
|
},
|
|
roles: []string{"evil-role"},
|
|
traits: map[string][]string{"logins": {"evil-login"}},
|
|
},
|
|
}
|
|
|
|
var allUserNames []string
|
|
for _, u := range usersToCreate {
|
|
allUserNames = append(allUserNames, u.name)
|
|
|
|
user, err := types.NewUser(u.name)
|
|
require.NoError(t, err)
|
|
|
|
user.SetStaticLabels(u.labels)
|
|
user.SetRoles(u.roles)
|
|
user.SetTraits(u.traits)
|
|
|
|
_, err = clt.CreateUser(ctx, user)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
getUsers := func(t *testing.T, req *userspb.ListUsersRequest) []*types.UserV2 {
|
|
var users []*types.UserV2
|
|
for {
|
|
rsp, err := clt.ListUsers(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
users = append(users, rsp.GetUsers()...)
|
|
|
|
req.SetPageToken(rsp.GetNextPageToken())
|
|
if req.GetPageToken() == "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
return users
|
|
}
|
|
|
|
namesOf := func(users []*types.UserV2) []string {
|
|
var names []string
|
|
for _, u := range users {
|
|
names = append(names, u.GetName())
|
|
}
|
|
return names
|
|
}
|
|
|
|
users := getUsers(t, &userspb.ListUsersRequest{})
|
|
require.ElementsMatch(t, allUserNames, namesOf(users))
|
|
|
|
users = getUsers(t, userspb.ListUsersRequest_builder{
|
|
Filter: &types.UserFilter{
|
|
SearchKeywords: []string{
|
|
"mauka",
|
|
"red",
|
|
},
|
|
},
|
|
}.Build())
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"[email protected]",
|
|
"[email protected]",
|
|
}, namesOf(users))
|
|
|
|
users = getUsers(t, userspb.ListUsersRequest_builder{
|
|
Filter: &types.UserFilter{
|
|
SearchKeywords: []string{
|
|
"blue",
|
|
"good.example.com",
|
|
},
|
|
},
|
|
}.Build())
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"[email protected]",
|
|
}, namesOf(users))
|
|
|
|
users = getUsers(t, userspb.ListUsersRequest_builder{
|
|
Filter: &types.UserFilter{
|
|
SearchKeywords: []string{
|
|
"mauka",
|
|
},
|
|
},
|
|
PageSize: 2,
|
|
}.Build())
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"[email protected]",
|
|
"[email protected]",
|
|
"[email protected]",
|
|
}, namesOf(users))
|
|
|
|
users = getUsers(t, userspb.ListUsersRequest_builder{
|
|
Filter: &types.UserFilter{
|
|
SearchKeywords: []string{
|
|
"good-role",
|
|
},
|
|
},
|
|
}.Build())
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"[email protected]",
|
|
"[email protected]",
|
|
}, namesOf(users))
|
|
|
|
users = getUsers(t, userspb.ListUsersRequest_builder{
|
|
Filter: &types.UserFilter{
|
|
SearchKeywords: []string{
|
|
"good-login",
|
|
},
|
|
},
|
|
}.Build())
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"[email protected]",
|
|
"[email protected]",
|
|
}, namesOf(users))
|
|
|
|
users = getUsers(t, userspb.ListUsersRequest_builder{
|
|
Filter: &types.UserFilter{
|
|
Traits: map[string][]string{"logins": {"good-login"}},
|
|
},
|
|
}.Build())
|
|
|
|
require.ElementsMatch(t, []string{
|
|
"[email protected]",
|
|
"[email protected]",
|
|
}, namesOf(users))
|
|
}
|
|
|
|
// TestRemoteUser tests scenario when remote user connects to the local
|
|
// auth server and some edge cases.
|
|
func TestRemoteUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
remoteServer, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
ClusterName: "remote",
|
|
Clock: clock,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, remoteServer.Close()) })
|
|
|
|
remoteUser, remoteRole, err := authtest.CreateUserAndRole(remoteServer.AuthServer, "remote-user", []string{"remote-role"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
certPool, err := testSrv.CertPool()
|
|
require.NoError(t, err)
|
|
|
|
remoteClient, err := remoteServer.NewRemoteClient(authtest.TestUser(remoteUser.GetName()), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
|
|
// User is not authorized to perform any actions
|
|
// as local cluster does not trust the remote cluster yet.
|
|
_, err = remoteClient.GetDomainName(ctx)
|
|
require.True(t, trace.IsConnectionProblem(err), "expected a connection problem error, got %v", err)
|
|
|
|
// Establish trust, the request will still fail, since there is
|
|
// no role mapping set up yet.
|
|
err = testSrv.AuthServer.Trust(ctx, remoteServer, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Create a fresh client now that trust is established.
|
|
require.NoError(t, remoteClient.Close())
|
|
remoteClient, err = remoteServer.NewRemoteClient(authtest.TestUser(remoteUser.GetName()), testSrv.Addr(), certPool)
|
|
require.NoError(t, err)
|
|
|
|
// Validate that the client is not permitted to perform RPCs without a role map.
|
|
// The requests are attempted several times since it may take some time
|
|
// for the trust relationship to be established and CAs to be propagated.
|
|
_, err = remoteClient.GetDomainName(ctx)
|
|
assert.Error(t, err)
|
|
if !trace.IsAccessDenied(err) {
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err = remoteClient.GetDomainName(ctx)
|
|
assert.True(t, trace.IsAccessDenied(err), "expected an access denied error, got %v", err)
|
|
}, 15*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
// Establish trust and map the remote role to the local admin role.
|
|
_, localRole, err := authtest.CreateUserAndRole(testSrv.Auth(), "local-user", []string{"local-role"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.AuthServer.Trust(ctx, remoteServer, types.RoleMap{{Remote: remoteRole.GetName(), Local: []string{localRole.GetName()}}})
|
|
require.NoError(t, err)
|
|
|
|
// Validate that the client is now permitted to perform RPCs now that
|
|
// the role map was created. The requests are attempted several times since it
|
|
// may take some time for the trust relationship to be updated and CAs to be propagated.
|
|
_, err = remoteClient.GetDomainName(ctx)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
// The only acceptable error here is AccessDenied caused by the role map
|
|
// not being propagated yet. Any other errors should fail the test.
|
|
if !trace.IsAccessDenied(err) {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err = remoteClient.GetDomainName(ctx)
|
|
assert.NoError(t, err)
|
|
}, 15*time.Second, 100*time.Millisecond)
|
|
}
|
|
|
|
// TestNopUser tests user with no permissions except
|
|
// the ones that require other authentication methods ("nop" user)
|
|
func TestNopUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
client, err := testSrv.NewClient(authtest.TestNop())
|
|
require.NoError(t, err)
|
|
|
|
// Nop User can get cluster name
|
|
_, err = client.GetDomainName(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// But can not get users or nodes
|
|
_, err = client.GetUsers(ctx, false)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
_, err = client.GetNodes(ctx, apidefaults.Namespace)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
|
|
// TestOwnRole tests that user can read roles assigned to them (used by web UI)
|
|
func TestReadOwnRole(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
user1, userRole, err := authtest.CreateUserAndRoleWithoutRoles(clt, "user1", []string{"user1"})
|
|
require.NoError(t, err)
|
|
|
|
user2, _, err := authtest.CreateUserAndRoleWithoutRoles(clt, "user2", []string{"user2"})
|
|
require.NoError(t, err)
|
|
|
|
// user should be able to read their own roles
|
|
userClient, err := testSrv.NewClient(authtest.TestUser(user1.GetName()))
|
|
require.NoError(t, err)
|
|
|
|
_, err = userClient.GetRole(ctx, userRole.GetName())
|
|
require.NoError(t, err)
|
|
|
|
// user2 can't read user1 role
|
|
userClient2, err := testSrv.NewClient(authtest.TestIdentity{I: authz.LocalUser{Username: user2.GetName()}})
|
|
require.NoError(t, err)
|
|
|
|
_, err = userClient2.GetRole(ctx, userRole.GetName())
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
|
|
func TestGetCurrentUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
srv := newTestTLSServer(t)
|
|
|
|
user1, _, err := authtest.CreateUserAndRole(srv.Auth(), "user1", []string{"user1"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
client1, err := srv.NewClient(authtest.TestIdentity{I: authz.LocalUser{Username: user1.GetName()}})
|
|
require.NoError(t, err)
|
|
|
|
currentUser, err := client1.GetCurrentUser(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(&types.UserV2{
|
|
Kind: "user",
|
|
SubKind: "",
|
|
Version: "v2",
|
|
Metadata: types.Metadata{
|
|
Name: "user1",
|
|
Namespace: "default",
|
|
Description: "",
|
|
Labels: nil,
|
|
Expires: nil,
|
|
},
|
|
Spec: types.UserSpecV2{
|
|
Roles: []string{"user:user1"},
|
|
},
|
|
}, currentUser, cmpopts.IgnoreFields(types.Metadata{}, "Revision"), cmpopts.IgnoreFields(types.UserStatusV2{}, "MfaWeakestDevice")))
|
|
}
|
|
|
|
func TestGetCurrentUserRoles(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
srv := newTestTLSServer(t)
|
|
|
|
user1, user1Role, err := authtest.CreateUserAndRole(srv.Auth(), "user1", []string{"user-role"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
client1, err := srv.NewClient(authtest.TestIdentity{I: authz.LocalUser{Username: user1.GetName()}})
|
|
require.NoError(t, err)
|
|
|
|
roles, err := client1.GetCurrentUserRoles(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(roles, []types.Role{user1Role}, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
}
|
|
|
|
func TestAuthPreferenceSettings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
ap, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
|
|
Type: "local",
|
|
SecondFactor: "otp",
|
|
DisconnectExpiredCert: types.NewBoolOption(true),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
upsertedAP, err := clt.UpsertAuthPreference(ctx, ap)
|
|
require.NoError(t, err)
|
|
|
|
gotAP, err := clt.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "local", gotAP.GetType())
|
|
require.Equal(t, []types.SecondFactorType{types.SecondFactorType_SECOND_FACTOR_TYPE_OTP}, gotAP.GetSecondFactors())
|
|
require.True(t, gotAP.GetDisconnectExpiredCert())
|
|
require.Empty(t, cmp.Diff(upsertedAP, gotAP, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
}
|
|
|
|
func TestAuthPreferenceSettings_ScopedIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t, withScopesFeatures(scopes.Features{Enabled: true}))
|
|
ctx := t.Context()
|
|
|
|
adminClient, err := srv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
defer adminClient.Close()
|
|
|
|
scopedSvc := adminClient.ScopedAccessServiceClient()
|
|
_, err = scopedSvc.CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
|
Role: scopedaccessv1.ScopedRole_builder{
|
|
Kind: scopedaccess.KindScopedRole,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "empty-role",
|
|
}.Build(),
|
|
Scope: "/test",
|
|
Spec: scopedaccessv1.ScopedRoleSpec_builder{
|
|
AssignableScopes: []string{"/test/scope"},
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
user, err := authtest.CreateUser(ctx, srv.Auth(), "scoped-reader")
|
|
require.NoError(t, err)
|
|
|
|
createResp, err := scopedSvc.CreateScopedRoleAssignment(ctx, scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
|
Assignment: scopedaccessv1.ScopedRoleAssignment_builder{
|
|
Kind: scopedaccess.KindScopedRoleAssignment,
|
|
SubKind: scopedaccess.SubKindDynamic,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: uuid.NewString(),
|
|
}.Build(),
|
|
Scope: "/test",
|
|
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
|
|
User: user.GetName(),
|
|
Assignments: []*scopedaccessv1.Assignment{
|
|
scopedaccessv1.Assignment_builder{Role: "/test::empty-role", Scope: "/test/scope"}.Build(),
|
|
},
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err := srv.AuthServer.AuthServer.ScopedAccessCache.GetScopedRoleAssignment(ctx, scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
|
|
Name: createResp.GetAssignment().GetMetadata().GetName(),
|
|
SubKind: createResp.GetAssignment().GetSubKind(),
|
|
Scope: createResp.GetAssignment().GetScope(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
scopedClt, err := srv.NewClient(authtest.TestScopedUser(user.GetName(), "/test/scope"))
|
|
require.NoError(t, err)
|
|
defer scopedClt.Close()
|
|
|
|
want, err := srv.AuthServer.AuthServer.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
got, err := scopedClt.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
if err == nil {
|
|
require.Empty(t, cmp.Diff(want, got))
|
|
}
|
|
}
|
|
|
|
func TestTunnelConnectionsCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
clusterName := "example.com"
|
|
out, err := clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
dt := clockwork.NewFakeClock().Now()
|
|
conn, err := types.NewTunnelConnection("conn1", types.TunnelConnectionSpecV2{
|
|
ClusterName: clusterName,
|
|
ProxyName: "p1",
|
|
LastHeartbeat: dt,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = clt.UpsertTunnelConnection(ctx, conn)
|
|
require.NoError(t, err)
|
|
|
|
out, err = clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out[0], conn, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
out, err = clt.GetAllTunnelConnections(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out[0], conn, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
dt = dt.Add(time.Hour)
|
|
conn.SetLastHeartbeat(dt)
|
|
|
|
err = clt.UpsertTunnelConnection(ctx, conn)
|
|
require.NoError(t, err)
|
|
|
|
out, err = clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out[0], conn, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
// Delete all to reset test environment
|
|
out, err = clt.GetAllTunnelConnections(ctx)
|
|
require.NoError(t, err)
|
|
for _, tc := range out {
|
|
err := testSrv.Auth().DeleteTunnelConnection(ctx, tc.GetClusterName(), tc.GetName())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// test delete individual connection
|
|
err = clt.UpsertTunnelConnection(ctx, conn)
|
|
require.NoError(t, err)
|
|
|
|
out, err = clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out[0], conn, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
err = clt.DeleteTunnelConnection(ctx, clusterName, conn.GetName())
|
|
require.NoError(t, err)
|
|
|
|
out, err = clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
// Exercise the direct gRPC ListTunnelConnections RPC, including the filter.
|
|
for i := 0; i < 3; i++ {
|
|
c, err := types.NewTunnelConnection(fmt.Sprintf("grpc-conn-%d", i), types.TunnelConnectionSpecV2{
|
|
ClusterName: clusterName,
|
|
ProxyName: fmt.Sprintf("p%d", i),
|
|
LastHeartbeat: dt,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, clt.UpsertTunnelConnection(ctx, c))
|
|
}
|
|
|
|
resp, err := clt.TrustClient().ListTunnelConnections(ctx, trustpb.ListTunnelConnectionsRequest_builder{
|
|
Filter: trustpb.ListTunnelConnectionsFilter_builder{ClusterName: clusterName}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.GetTunnelConnections(), 3)
|
|
|
|
resp, err = clt.TrustClient().ListTunnelConnections(ctx, trustpb.ListTunnelConnectionsRequest_builder{
|
|
Filter: trustpb.ListTunnelConnectionsFilter_builder{ClusterName: "other.example.com"}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
require.Empty(t, resp.GetTunnelConnections())
|
|
}
|
|
|
|
// TestTunnelConnectionsLegacyHTTP exercises the legacy HTTP handlers for
|
|
// tunnel connection upsert/delete directly so they stay covered during the
|
|
// v19→v20 fallback window without re-exporting the fallback helpers.
|
|
//
|
|
// TODO(strideynet): DELETE IN v20.0.0
|
|
func TestTunnelConnectionsLegacyHTTP(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
const clusterName = "example.com"
|
|
conn, err := types.NewTunnelConnection("conn-legacy", types.TunnelConnectionSpecV2{
|
|
ClusterName: clusterName,
|
|
ProxyName: "p1",
|
|
LastHeartbeat: clockwork.NewFakeClock().Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
data, err := services.MarshalTunnelConnection(conn)
|
|
require.NoError(t, err)
|
|
_, err = clt.HTTPClient.PostJSON(ctx, clt.HTTPClient.Endpoint("tunnelconnections"), &struct {
|
|
TunnelConnection json.RawMessage `json:"tunnel_connection"`
|
|
}{TunnelConnection: data})
|
|
require.NoError(t, err)
|
|
|
|
stored, err := clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Len(t, stored, 1)
|
|
require.Equal(t, conn.GetName(), stored[0].GetName())
|
|
|
|
// Exercise the legacy HTTP GET endpoints directly, so they stay covered
|
|
// during the v19→v20 fallback window.
|
|
readFromHTTP := func(path ...string) []types.TunnelConnection {
|
|
out, err := clt.HTTPClient.Get(ctx, clt.HTTPClient.Endpoint(path...), url.Values{})
|
|
require.NoError(t, err)
|
|
var items []json.RawMessage
|
|
require.NoError(t, json.Unmarshal(out.Bytes(), &items))
|
|
result := make([]types.TunnelConnection, len(items))
|
|
for i, raw := range items {
|
|
c, err := services.UnmarshalTunnelConnection(raw)
|
|
require.NoError(t, err)
|
|
result[i] = c
|
|
}
|
|
return result
|
|
}
|
|
byCluster := readFromHTTP("tunnelconnections", clusterName)
|
|
require.Len(t, byCluster, 1)
|
|
require.Equal(t, conn.GetName(), byCluster[0].GetName())
|
|
all := readFromHTTP("tunnelconnections")
|
|
require.Len(t, all, 1)
|
|
|
|
_, err = clt.HTTPClient.Delete(ctx, clt.HTTPClient.Endpoint("tunnelconnections", clusterName, conn.GetName()))
|
|
require.NoError(t, err)
|
|
|
|
stored, err = clt.GetTunnelConnections(ctx, clusterName)
|
|
require.NoError(t, err)
|
|
require.Empty(t, stored)
|
|
}
|
|
|
|
func TestServersCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
NewServer := func(kind, name, addr, namespace string) *types.ServerV2 {
|
|
return &types.ServerV2{
|
|
Kind: kind,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{
|
|
Addr: addr,
|
|
},
|
|
}
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// SSH service.
|
|
out, err := clt.GetNodes(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
srv := NewServer(types.KindNode, "srv1", "127.0.0.1:2022", apidefaults.Namespace)
|
|
srv.Spec.Hostname = "llama"
|
|
_, err = clt.UpsertNode(ctx, srv)
|
|
require.NoError(t, err)
|
|
|
|
node, err := clt.GetSSHServer(ctx, presencev1.GetSSHServerRequest_builder{Name: srv.GetName()}.Build())
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(node, srv, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
out, err = clt.GetNodes(ctx, srv.Metadata.Namespace)
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out, []types.Server{srv}, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
err = clt.DeleteSSHServer(ctx, presencev1.DeleteSSHServerRequest_builder{Name: srv.GetName()}.Build())
|
|
require.NoError(t, err)
|
|
|
|
out, err = clt.GetNodes(ctx, srv.Metadata.Namespace)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
// Proxy service.
|
|
//nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0
|
|
out, err = clt.GetProxies()
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
proxy := NewServer(types.KindProxy, "proxy1", "127.0.0.1:2023", apidefaults.Namespace)
|
|
proxy.Spec.Hostname = "proxy.llama"
|
|
require.NoError(t, clt.UpsertProxyServerWithoutReturn(ctx, proxy))
|
|
|
|
//nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0
|
|
out, err = clt.GetProxies()
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out, []types.Server{proxy}, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
err = clt.DeleteProxyServer(ctx, proxy.GetName())
|
|
require.NoError(t, err)
|
|
|
|
//nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0
|
|
out, err = clt.GetProxies()
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
// Auth service.
|
|
//nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0
|
|
out, err = clt.GetAuthServers()
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
auth := NewServer(types.KindAuthServer, "auth1", "127.0.0.1:2025", apidefaults.Namespace)
|
|
auth.Spec.Hostname = "auth.llama"
|
|
require.NoError(t, testSrv.Auth().UpsertAuthServer(ctx, auth))
|
|
|
|
//nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0
|
|
out, err = clt.GetAuthServers()
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff(out, []types.Server{auth}, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
}
|
|
|
|
// TestDeleteProxy exercises both wire paths that coexist during the
|
|
// HTTP-to-gRPC migration window: the direct gRPC RPC on *APIClient and the
|
|
// legacy HTTP endpoint invoked via the raw HTTP client.
|
|
//
|
|
// TODO(noah): DELETE IN v20.0.0 - once the legacy HTTP endpoint is removed,
|
|
// the DeleteProxy call in TestServersCRUD is sufficient.
|
|
func TestDeleteProxy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
ctx := t.Context()
|
|
|
|
newProxy := func(name string) types.Server {
|
|
return &types.ServerV2{
|
|
Kind: types.KindProxy,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: name,
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{
|
|
Addr: "127.0.0.1:2023",
|
|
Hostname: "proxy.llama",
|
|
},
|
|
}
|
|
}
|
|
|
|
proxyNames := func(t *testing.T) []string {
|
|
proxies, _, err := testSrv.Auth().ListProxyServers(ctx, 0, "")
|
|
require.NoError(t, err)
|
|
names := make([]string, 0, len(proxies))
|
|
for _, p := range proxies {
|
|
names = append(names, p.GetName())
|
|
}
|
|
return names
|
|
}
|
|
|
|
t.Run("direct gRPC RPC", func(t *testing.T) {
|
|
proxy := newProxy("proxy-grpc")
|
|
require.NoError(t, clt.UpsertProxyServerWithoutReturn(ctx, proxy))
|
|
|
|
_, err := clt.APIClient.PresenceServiceClient().DeleteProxyServer(
|
|
ctx, presencev1.DeleteProxyServerRequest_builder{
|
|
Name: proxy.GetName(),
|
|
}.Build(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.NotContains(t, proxyNames(t), proxy.GetName())
|
|
})
|
|
|
|
t.Run("through fallback abstraction", func(t *testing.T) {
|
|
proxy := newProxy("proxy-through-fallback-grpc")
|
|
require.NoError(t, clt.UpsertProxyServerWithoutReturn(ctx, proxy))
|
|
|
|
require.NoError(t, clt.DeleteProxyServer(ctx, proxy.GetName()))
|
|
|
|
require.NotContains(t, proxyNames(t), proxy.GetName())
|
|
})
|
|
|
|
t.Run("legacy HTTP endpoint", func(t *testing.T) {
|
|
proxy := newProxy("proxy-http")
|
|
require.NoError(t, clt.UpsertProxyServerWithoutReturn(ctx, proxy))
|
|
|
|
_, err := clt.HTTPClient.Delete(ctx, clt.HTTPClient.Endpoint("proxies", proxy.GetName()))
|
|
require.NoError(t, err)
|
|
|
|
require.NotContains(t, proxyNames(t), proxy.GetName())
|
|
})
|
|
}
|
|
|
|
// TestUpsertProxy exercises both wire paths that coexist during the
|
|
// HTTP-to-gRPC migration window: the direct gRPC RPC on *APIClient and the
|
|
// legacy HTTP endpoint invoked via the raw HTTP client.
|
|
//
|
|
// TODO(noah): DELETE IN v20.0.0 - once the legacy HTTP endpoint is removed,
|
|
// the UpsertProxy call in TestServersCRUD is sufficient.
|
|
func TestUpsertProxy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
ctx := t.Context()
|
|
|
|
newProxy := func(name string) types.Server {
|
|
return &types.ServerV2{
|
|
Kind: types.KindProxy,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: name,
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{
|
|
Addr: "127.0.0.1:2023",
|
|
Hostname: "proxy.llama",
|
|
},
|
|
}
|
|
}
|
|
|
|
proxyNames := func(t *testing.T) []string {
|
|
proxies, _, err := testSrv.Auth().ListProxyServers(ctx, 0, "")
|
|
require.NoError(t, err)
|
|
names := make([]string, 0, len(proxies))
|
|
for _, p := range proxies {
|
|
names = append(names, p.GetName())
|
|
}
|
|
return names
|
|
}
|
|
|
|
t.Run("direct gRPC RPC", func(t *testing.T) {
|
|
proxy := newProxy("proxy-grpc").(*types.ServerV2)
|
|
_, err := clt.APIClient.PresenceServiceClient().UpsertProxyServer(
|
|
ctx, presencev1.UpsertProxyServerRequest_builder{
|
|
Server: proxy,
|
|
}.Build(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, proxyNames(t), proxy.GetName())
|
|
})
|
|
|
|
t.Run("through fallback abstraction", func(t *testing.T) {
|
|
proxy := newProxy("proxy-through-fallback-grpc")
|
|
require.NoError(t, clt.UpsertProxyServerWithoutReturn(ctx, proxy))
|
|
|
|
require.Contains(t, proxyNames(t), proxy.GetName())
|
|
})
|
|
|
|
t.Run("legacy HTTP endpoint", func(t *testing.T) {
|
|
proxy := newProxy("proxy-http")
|
|
data, err := services.MarshalServer(proxy)
|
|
require.NoError(t, err)
|
|
body := map[string]any{"server": json.RawMessage(data)}
|
|
_, err = clt.HTTPClient.PostJSON(ctx, clt.HTTPClient.Endpoint("proxies"), body)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, proxyNames(t), proxy.GetName())
|
|
})
|
|
}
|
|
|
|
// TestAppServerCRUD tests CRUD functionality for services.App using an auth client.
|
|
func TestAppServerCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// A RoleApp agent may only manage app servers with its own host ID, so the
|
|
// identity's host ID and the app server's HostID must match.
|
|
const appHostID = "app-host-id"
|
|
clt, err := testSrv.NewClient(authtest.TestServerID(types.RoleApp, appHostID))
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Expect not to be returned any applications and trace.NotFound.
|
|
out, err := clt.GetApplicationServers(ctx, apidefaults.Namespace)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
|
|
// Make an app and an app server.
|
|
app, err := types.NewAppV3(types.Metadata{Name: "foo"},
|
|
types.AppSpecV3{URI: "http://127.0.0.1:8080", PublicAddr: "foo.example.com"})
|
|
require.NoError(t, err)
|
|
server, err := types.NewAppServerV3(types.Metadata{
|
|
Name: app.GetName(),
|
|
Namespace: apidefaults.Namespace,
|
|
}, types.AppServerSpecV3{
|
|
Hostname: "localhost",
|
|
HostID: appHostID,
|
|
App: app,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Upsert application.
|
|
_, err = clt.UpsertApplicationServer(ctx, server)
|
|
require.NoError(t, err)
|
|
|
|
// Check again, expect a single application to be found.
|
|
out, err = clt.GetApplicationServers(ctx, server.GetNamespace())
|
|
require.NoError(t, err)
|
|
require.Len(t, out, 1)
|
|
require.Empty(t, cmp.Diff([]types.AppServer{server}, out, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
|
|
|
|
// Remove the application.
|
|
err = clt.DeleteAppServer(ctx, presencev1.DeleteAppServerRequest_builder{
|
|
HostId: server.GetHostID(),
|
|
Name: server.GetName(),
|
|
Scope: server.GetScope(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
// Now expect no applications to be returned.
|
|
out, err = clt.GetApplicationServers(ctx, server.Metadata.Namespace)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
}
|
|
|
|
func TestUsersCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
usr, err := types.NewUser("user1")
|
|
require.NoError(t, err)
|
|
_, err = clt.CreateUser(ctx, usr)
|
|
require.NoError(t, err)
|
|
|
|
users, err := clt.GetUsers(ctx, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, users, 1)
|
|
require.Equal(t, "user1", users[0].GetName())
|
|
|
|
require.NoError(t, clt.DeleteUser(ctx, "user1"))
|
|
|
|
users, err = clt.GetUsers(ctx, false)
|
|
require.NoError(t, err)
|
|
require.Empty(t, users)
|
|
}
|
|
|
|
func TestPasswordGarbage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
garbage := [][]byte{
|
|
nil,
|
|
make([]byte, defaults.MaxPasswordLength+1),
|
|
make([]byte, defaults.MinPasswordLength-1),
|
|
}
|
|
for _, g := range garbage {
|
|
err := testSrv.Auth().CheckPassword(ctx, "user1", g, "123456")
|
|
require.True(t, trace.IsBadParameter(err))
|
|
}
|
|
}
|
|
|
|
func TestPasswordCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, as.Close()) })
|
|
clock := as.Clock()
|
|
|
|
// Create a user.
|
|
u, err := types.NewUser("user1")
|
|
require.NoError(t, err)
|
|
_, err = as.AuthServer.CreateUser(ctx, u)
|
|
require.NoError(t, err)
|
|
|
|
pass := []byte("abcdef123456")
|
|
rawSecret := "def456"
|
|
otpSecret := base32.StdEncoding.EncodeToString([]byte(rawSecret))
|
|
|
|
err = as.AuthServer.CheckPassword(ctx, "user1", pass, "123456")
|
|
require.Error(t, err)
|
|
|
|
err = as.AuthServer.UpsertPassword("user1", pass)
|
|
require.NoError(t, err)
|
|
|
|
dev, err := services.NewTOTPDevice("otp", otpSecret, clock.Now())
|
|
require.NoError(t, err)
|
|
|
|
err = as.AuthServer.UpsertMFADevice(ctx, "user1", dev)
|
|
require.NoError(t, err)
|
|
|
|
validToken, err := totp.GenerateCode(otpSecret, clock.Now())
|
|
require.NoError(t, err)
|
|
|
|
err = as.AuthServer.CheckPassword(ctx, "user1", pass, validToken)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestOTPCRUD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, as.Close()) })
|
|
clock := as.Clock()
|
|
|
|
user := "user1"
|
|
pass := []byte("abcdef123456")
|
|
rawSecret := "def456"
|
|
otpSecret := base32.StdEncoding.EncodeToString([]byte(rawSecret))
|
|
|
|
// Create a user.
|
|
u, err := types.NewUser(user)
|
|
require.NoError(t, err)
|
|
_, err = as.AuthServer.CreateUser(ctx, u)
|
|
require.NoError(t, err)
|
|
|
|
// upsert a password and totp secret
|
|
err = as.AuthServer.UpsertPassword("user1", pass)
|
|
require.NoError(t, err)
|
|
dev, err := services.NewTOTPDevice("otp", otpSecret, clock.Now())
|
|
require.NoError(t, err)
|
|
|
|
err = as.AuthServer.UpsertMFADevice(ctx, user, dev)
|
|
require.NoError(t, err)
|
|
|
|
// a completely invalid token should return access denied
|
|
err = as.AuthServer.CheckPassword(ctx, "user1", pass, "123456")
|
|
require.Error(t, err)
|
|
|
|
// an invalid token should return access denied
|
|
//
|
|
// this tests makes the token 61 seconds in the future (but from a valid key)
|
|
// even though the validity period is 30 seconds. this is because a token is
|
|
// valid for 30 seconds + 30 second skew before and after for a usability
|
|
// reasons. so a token made between seconds 31 and 60 is still valid, and
|
|
// invalidity starts at 61 seconds in the future.
|
|
invalidToken, err := totp.GenerateCode(otpSecret, clock.Now().Add(61*time.Second))
|
|
require.NoError(t, err)
|
|
err = as.AuthServer.CheckPassword(ctx, "user1", pass, invalidToken)
|
|
require.Error(t, err)
|
|
|
|
// a valid token (created right now and from a valid key) should return success
|
|
validToken, err := totp.GenerateCode(otpSecret, clock.Now())
|
|
require.NoError(t, err)
|
|
|
|
err = as.AuthServer.CheckPassword(ctx, "user1", pass, validToken)
|
|
require.NoError(t, err)
|
|
|
|
// try the same valid token now it should fail because we don't allow re-use of tokens
|
|
err = as.AuthServer.CheckPassword(ctx, "user1", pass, validToken)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestWebSessions tests web sessions flow for web user,
|
|
// that logs in, extends web session and tries to perform administrative action
|
|
// but fails
|
|
func TestWebSessionWithoutAccessRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
user := "user1"
|
|
pass := []byte("abcdef123456")
|
|
|
|
_, _, err = authtest.CreateUserAndRole(clt, user, []string{user}, nil)
|
|
require.NoError(t, err)
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
req := authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
}
|
|
// authentication attempt fails with no password set up
|
|
_, err = proxy.AuthenticateWebUser(ctx, req)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
// success with password set up
|
|
ws, err := proxy.AuthenticateWebUser(ctx, req)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, ws)
|
|
|
|
web, err := testSrv.NewClientFromWebSession(ws)
|
|
require.NoError(t, err)
|
|
|
|
ns, err := web.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: ws.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ns)
|
|
|
|
// Extending also works calling the gRPC RPC directly.
|
|
grpcResp, err := web.APIClient.ExtendWebSession(ctx, &proto.ExtendWebSessionRequest{
|
|
User: user,
|
|
PrevSessionId: ws.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, grpcResp.GetSession())
|
|
|
|
// Extending also works calling the legacy HTTP endpoint directly.
|
|
// TODO(strideynet): DELETE IN v20.0.0 - remove alongside the legacy HTTP
|
|
// endpoint.
|
|
out, err := web.HTTPClient.PostJSON(ctx, web.HTTPClient.Endpoint("users", user, "web", "sessions"), authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: ws.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
httpSess, err := services.UnmarshalWebSession(out.Bytes())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, httpSess)
|
|
|
|
// Requesting forbidden action for user fails
|
|
err = web.DeleteUser(ctx, user)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
err = clt.WebSessions().Delete(ctx, types.DeleteWebSessionRequest{
|
|
User: user,
|
|
SessionID: ws.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = clt.WebSessions().Get(ctx, types.GetWebSessionRequest{
|
|
User: user,
|
|
SessionID: ws.GetName(),
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, err = web.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: ws.GetName(),
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestWebSessionMultiAccessRequests(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
|
|
testSrv := newTestTLSServer(t, withModules(modulestest.EnterpriseModules()))
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, clt.Close()) })
|
|
|
|
// Upsert a node to request access to
|
|
node := &types.ServerV2{
|
|
Kind: types.KindNode,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: "node1",
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{},
|
|
}
|
|
_, err = clt.UpsertNode(ctx, node)
|
|
require.NoError(t, err)
|
|
resourceIDs := []types.ResourceID{{
|
|
Kind: node.GetKind(),
|
|
Name: node.GetName(),
|
|
ClusterName: "foobar",
|
|
}}
|
|
|
|
// Create user and roles.
|
|
username := "user"
|
|
password := []byte("hunter2hunter2")
|
|
baseRoleName := services.RoleNameForUser(username)
|
|
requestableRoleName := "requestable"
|
|
user, err := authtest.CreateUserRoleAndRequestable(clt, username, requestableRoleName)
|
|
require.NoError(t, err)
|
|
err = testSrv.Auth().UpsertPassword(username, password)
|
|
require.NoError(t, err)
|
|
|
|
// Set search_as_roles, user can request this role only with a resource
|
|
// access request.
|
|
resourceRequestRoleName := "resource-requestable"
|
|
resourceRequestRole := services.RoleForUser(user)
|
|
resourceRequestRole.SetName(resourceRequestRoleName)
|
|
_, err = clt.UpsertRole(ctx, resourceRequestRole)
|
|
require.NoError(t, err)
|
|
baseRole, err := clt.GetRole(ctx, baseRoleName)
|
|
require.NoError(t, err)
|
|
baseRole.SetSearchAsRoles(types.Allow, []string{resourceRequestRoleName})
|
|
_, err = clt.UpsertRole(ctx, baseRole)
|
|
require.NoError(t, err)
|
|
|
|
// Create approved role request
|
|
roleReq, err := services.NewAccessRequest(username, requestableRoleName)
|
|
require.NoError(t, err)
|
|
roleReq.SetState(types.RequestState_APPROVED)
|
|
roleReq.SetAccessExpiry(clock.Now().Add(8 * time.Hour))
|
|
roleReq, err = clt.CreateAccessRequestV2(ctx, roleReq)
|
|
require.NoError(t, err)
|
|
|
|
// Create remote cluster so create access request doesn't err due to non existent cluster
|
|
rc, err := types.NewRemoteCluster("foobar")
|
|
require.NoError(t, err)
|
|
_, err = testSrv.AuthServer.AuthServer.CreateRemoteCluster(ctx, rc)
|
|
require.NoError(t, err)
|
|
|
|
// Create approved resource request
|
|
resourceReq, err := services.NewAccessRequestWithResources(username, []string{resourceRequestRoleName}, types.ResourceIDsToResourceAccessIDs(resourceIDs))
|
|
require.NoError(t, err)
|
|
resourceReq.SetState(types.RequestState_APPROVED)
|
|
resourceReq, err = clt.CreateAccessRequestV2(ctx, resourceReq)
|
|
require.NoError(t, err)
|
|
|
|
// Create a web session and client for the user.
|
|
proxyClient, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
baseWebSession, err := proxyClient.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
|
Username: username,
|
|
Pass: &authclient.PassCreds{
|
|
Password: password,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
proxyClient.Close()
|
|
baseWebClient, err := testSrv.NewClientFromWebSession(baseWebSession)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, baseWebClient.Close()) })
|
|
|
|
expectRolesAndResources := func(t *testing.T, sess types.WebSession, expectRoles []string, expectResources []types.ResourceID) {
|
|
sshcert, err := sshutils.ParseCertificate(sess.GetPub())
|
|
require.NoError(t, err)
|
|
ident, err := sshca.DecodeIdentity(sshcert)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, expectRoles, ident.Roles)
|
|
assert.ElementsMatch(t, types.ResourceIDsToResourceAccessIDs(expectResources), ident.AllowedResourceAccessIDs)
|
|
}
|
|
|
|
type extendSessionFunc func(*testing.T, *authclient.Client, types.WebSession) (*authclient.Client, types.WebSession)
|
|
assumeRequest := func(request types.AccessRequest) extendSessionFunc {
|
|
return func(t *testing.T, clt *authclient.Client, sess types.WebSession) (*authclient.Client, types.WebSession) {
|
|
newSess, err := clt.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: username,
|
|
PrevSessionID: sess.GetName(),
|
|
AccessRequestID: request.GetMetadata().Name,
|
|
})
|
|
require.NoError(t, err)
|
|
newClt, err := testSrv.NewClientFromWebSession(newSess)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, newClt.Close()) })
|
|
return newClt, newSess
|
|
}
|
|
}
|
|
failToAssumeRequest := func(request types.AccessRequest) extendSessionFunc {
|
|
return func(t *testing.T, clt *authclient.Client, sess types.WebSession) (*authclient.Client, types.WebSession) {
|
|
_, err := clt.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: username,
|
|
PrevSessionID: sess.GetName(),
|
|
AccessRequestID: request.GetMetadata().Name,
|
|
})
|
|
require.Error(t, err)
|
|
return clt, sess
|
|
}
|
|
}
|
|
switchBack := func(t *testing.T, clt *authclient.Client, sess types.WebSession) (*authclient.Client, types.WebSession) {
|
|
newSess, err := clt.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: username,
|
|
PrevSessionID: sess.GetName(),
|
|
Switchback: true,
|
|
})
|
|
require.NoError(t, err)
|
|
newClt, err := testSrv.NewClientFromWebSession(newSess)
|
|
require.NoError(t, err)
|
|
return newClt, newSess
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
desc string
|
|
steps []extendSessionFunc
|
|
expectRoles []string
|
|
expectResources []types.ResourceID
|
|
}{
|
|
{
|
|
desc: "base session",
|
|
expectRoles: []string{baseRoleName},
|
|
},
|
|
{
|
|
desc: "role request",
|
|
steps: []extendSessionFunc{
|
|
assumeRequest(roleReq),
|
|
},
|
|
expectRoles: []string{baseRoleName, requestableRoleName},
|
|
},
|
|
{
|
|
desc: "resource request",
|
|
steps: []extendSessionFunc{
|
|
assumeRequest(resourceReq),
|
|
},
|
|
expectRoles: []string{baseRoleName, resourceRequestRoleName},
|
|
expectResources: resourceIDs,
|
|
},
|
|
{
|
|
desc: "role then resource",
|
|
steps: []extendSessionFunc{
|
|
assumeRequest(roleReq),
|
|
assumeRequest(resourceReq),
|
|
},
|
|
expectRoles: []string{baseRoleName, requestableRoleName, resourceRequestRoleName},
|
|
expectResources: resourceIDs,
|
|
},
|
|
{
|
|
desc: "resource then role",
|
|
steps: []extendSessionFunc{
|
|
assumeRequest(resourceReq),
|
|
assumeRequest(roleReq),
|
|
},
|
|
expectRoles: []string{baseRoleName, requestableRoleName, resourceRequestRoleName},
|
|
expectResources: resourceIDs,
|
|
},
|
|
{
|
|
desc: "duplicates",
|
|
steps: []extendSessionFunc{
|
|
assumeRequest(resourceReq),
|
|
assumeRequest(roleReq),
|
|
// Cannot combine resource requests, this also blocks assuming
|
|
// the same one twice.
|
|
failToAssumeRequest(resourceReq),
|
|
assumeRequest(roleReq),
|
|
},
|
|
expectRoles: []string{baseRoleName, requestableRoleName, resourceRequestRoleName},
|
|
expectResources: resourceIDs,
|
|
},
|
|
{
|
|
desc: "switch back",
|
|
steps: []extendSessionFunc{
|
|
assumeRequest(roleReq),
|
|
assumeRequest(resourceReq),
|
|
switchBack,
|
|
},
|
|
expectRoles: []string{baseRoleName},
|
|
},
|
|
} {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
clt, sess := baseWebClient, baseWebSession
|
|
for _, extendSession := range tc.steps {
|
|
clt, sess = extendSession(t, clt, sess)
|
|
}
|
|
expectRolesAndResources(t, sess, tc.expectRoles, tc.expectResources)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWebSessionWithApprovedAccessRequestAndSwitchback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
user := "user2"
|
|
pass := []byte("abcdef123456")
|
|
|
|
newUser, err := authtest.CreateUserRoleAndRequestable(clt, user, "test-request-role")
|
|
require.NoError(t, err)
|
|
require.Len(t, newUser.GetRoles(), 1)
|
|
require.Empty(t, cmp.Diff(newUser.GetRoles(), []string{"user:user2"}))
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// Create a user to create a web session for.
|
|
req := authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
}
|
|
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
ws, err := proxy.AuthenticateWebUser(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
web, err := testSrv.NewClientFromWebSession(ws)
|
|
require.NoError(t, err)
|
|
|
|
initialRole := newUser.GetRoles()[0]
|
|
initialSession, err := testSrv.Auth().GetWebSession(ctx, types.GetWebSessionRequest{
|
|
User: user,
|
|
SessionID: ws.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a approved access request.
|
|
accessReq, err := services.NewAccessRequest(user, []string{"test-request-role"}...)
|
|
require.NoError(t, err)
|
|
|
|
// Set a lesser expiry date, to test switching back to default expiration later.
|
|
accessReq.SetAccessExpiry(clock.Now().Add(time.Minute * 10))
|
|
accessReq.SetState(types.RequestState_APPROVED)
|
|
|
|
accessReq, err = clt.CreateAccessRequestV2(ctx, accessReq)
|
|
require.NoError(t, err)
|
|
|
|
sess1, err := web.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: ws.GetName(),
|
|
AccessRequestID: accessReq.GetMetadata().Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.WithinDuration(t, clock.Now().Add(time.Minute*10), sess1.Expiry(), time.Second)
|
|
require.WithinDuration(t, sess1.GetLoginTime(), initialSession.GetLoginTime(), time.Second)
|
|
|
|
sshcert, err := sshutils.ParseCertificate(sess1.GetPub())
|
|
require.NoError(t, err)
|
|
|
|
// Roles extracted from cert should contain the initial role and the role assigned with access request.
|
|
ident, err := sshca.DecodeIdentity(sshcert)
|
|
require.NoError(t, err)
|
|
require.Len(t, ident.Roles, 2)
|
|
|
|
mappedRole := map[string]string{
|
|
ident.Roles[0]: "",
|
|
ident.Roles[1]: "",
|
|
}
|
|
|
|
_, hasRole := mappedRole[initialRole]
|
|
require.True(t, hasRole)
|
|
|
|
_, hasRole = mappedRole["test-request-role"]
|
|
require.True(t, hasRole)
|
|
|
|
// certRequests extracts the active requests from a PEM encoded TLS cert.
|
|
certRequests := func(tlsCert []byte) []string {
|
|
cert, err := tlsca.ParseCertificatePEM(tlsCert)
|
|
require.NoError(t, err)
|
|
|
|
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
|
|
require.NoError(t, err)
|
|
|
|
return identity.ActiveRequests
|
|
}
|
|
|
|
require.Empty(t, cmp.Diff(certRequests(sess1.GetTLSCert()), []string{accessReq.GetName()}))
|
|
|
|
// Test switch back to default role and expiry.
|
|
sess2, err := web.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: ws.GetName(),
|
|
Switchback: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, sess2.GetExpiryTime(), initialSession.GetExpiryTime())
|
|
require.Equal(t, sess2.GetLoginTime(), initialSession.GetLoginTime())
|
|
|
|
sshcert, err = sshutils.ParseCertificate(sess2.GetPub())
|
|
require.NoError(t, err)
|
|
|
|
ident, err = sshca.DecodeIdentity(sshcert)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(ident.Roles, []string{initialRole}))
|
|
|
|
require.Empty(t, certRequests(sess2.GetTLSCert()))
|
|
}
|
|
|
|
func TestExtendWebSessionWithReloadUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
user := "user2"
|
|
pass := []byte("abcdef123456")
|
|
|
|
newUser, _, err := authtest.CreateUserAndRole(clt, user, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, newUser.GetTraits())
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// Create user authn creds and web session.
|
|
req := authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
}
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
ws, err := proxy.AuthenticateWebUser(ctx, req)
|
|
require.NoError(t, err)
|
|
web, err := testSrv.NewClientFromWebSession(ws)
|
|
require.NoError(t, err)
|
|
|
|
// Retrieve updated user object.
|
|
newUser, err = clt.GetUser(ctx, user, false /* withSecrets */)
|
|
require.NoError(t, err)
|
|
|
|
// Update some traits and roles.
|
|
newRoleName := "new-role"
|
|
newUser.SetLogins([]string{"apple", "banana"})
|
|
newUser.SetDatabaseUsers([]string{"llama", "alpaca"})
|
|
_, err = authtest.CreateRole(ctx, clt, newRoleName, types.RoleSpecV6{})
|
|
require.NoError(t, err)
|
|
newUser.AddRole(newRoleName)
|
|
_, err = clt.UpdateUser(ctx, newUser)
|
|
require.NoError(t, err)
|
|
|
|
// Renew session with the updated traits.
|
|
sess1, err := web.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: ws.GetName(),
|
|
ReloadUser: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Check traits has been updated to latest.
|
|
sshcert, err := sshutils.ParseCertificate(sess1.GetPub())
|
|
require.NoError(t, err)
|
|
|
|
ident, err := sshca.DecodeIdentity(sshcert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"apple", "banana"}, ident.Traits[constants.TraitLogins])
|
|
require.Equal(t, []string{"llama", "alpaca"}, ident.Traits[constants.TraitDBUsers])
|
|
require.Contains(t, ident.Roles, newRoleName)
|
|
}
|
|
|
|
func TestExtendWebSessionWithMaxDuration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
adminClient, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
const user = "user2"
|
|
const testRequestRole = "test-request-role"
|
|
pass := []byte("abcdef123456")
|
|
|
|
newUser, err := authtest.CreateUserRoleAndRequestable(adminClient, user, testRequestRole)
|
|
require.NoError(t, err)
|
|
require.Len(t, newUser.GetRoles(), 1)
|
|
|
|
require.Len(t, newUser.GetRoles(), 1)
|
|
require.Empty(t, cmp.Diff(newUser.GetRoles(), []string{"user:user2"}))
|
|
|
|
proxyRoleClient, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// Create a user to create a web session for.
|
|
req := authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
}
|
|
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
webSession, err := proxyRoleClient.AuthenticateWebUser(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
userClient, err := testSrv.NewClientFromWebSession(webSession)
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
maxDurationRole time.Duration
|
|
expectedExpiry time.Duration
|
|
}{
|
|
{
|
|
desc: "default",
|
|
maxDurationRole: 0,
|
|
expectedExpiry: apidefaults.CertDuration,
|
|
},
|
|
{
|
|
desc: "max duration is set",
|
|
maxDurationRole: 5 * time.Hour,
|
|
expectedExpiry: 5 * time.Hour,
|
|
},
|
|
{
|
|
desc: "max duration greater than default",
|
|
maxDurationRole: 24 * time.Hour,
|
|
expectedExpiry: apidefaults.CertDuration,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
requestableRole, err := adminClient.GetRole(ctx, newUser.GetRoles()[0])
|
|
require.NoError(t, err)
|
|
|
|
// Set max duration on the role.
|
|
requestableRole.SetAccessRequestConditions(types.Allow, types.AccessRequestConditions{
|
|
Roles: []string{testRequestRole},
|
|
MaxDuration: types.Duration(tc.maxDurationRole),
|
|
})
|
|
_, err = adminClient.UpsertRole(ctx, requestableRole)
|
|
require.NoError(t, err)
|
|
|
|
// Create an approved access request.
|
|
accessReq, err := services.NewAccessRequest(user, []string{testRequestRole}...)
|
|
require.NoError(t, err)
|
|
|
|
// Set max duration higher than the role max duration. It will be capped.
|
|
accessReq.SetMaxDuration(clock.Now().Add(48 * time.Hour))
|
|
err = accessReq.SetState(types.RequestState_APPROVED)
|
|
require.NoError(t, err)
|
|
|
|
accessReq, err = adminClient.CreateAccessRequestV2(ctx, accessReq)
|
|
require.NoError(t, err)
|
|
|
|
sess1, err := userClient.ExtendWebSession(ctx, authclient.WebSessionReq{
|
|
User: user,
|
|
PrevSessionID: webSession.GetName(),
|
|
AccessRequestID: accessReq.GetMetadata().Name,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Check the expiry is capped to the max allowed duration.
|
|
require.WithinDuration(t, clock.Now().Add(tc.expectedExpiry), sess1.Expiry(), time.Second)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetCertAuthority tests certificate authority permissions
|
|
func TestGetCertAuthority(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// generate server keys for node
|
|
nodeClt, err := testSrv.NewClient(authtest.TestIdentity{I: authz.BuiltinRole{Username: "00000000-0000-0000-0000-000000000000", Role: types.RoleNode}})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, nodeClt.Close()) })
|
|
|
|
hostCAID := types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}
|
|
|
|
// node is authorized to fetch CA without secrets
|
|
ca, err := nodeClt.GetCertAuthority(ctx, hostCAID, false)
|
|
require.NoError(t, err)
|
|
for _, keyPair := range ca.GetActiveKeys().TLS {
|
|
require.Nil(t, keyPair.Key)
|
|
}
|
|
for _, keyPair := range ca.GetActiveKeys().SSH {
|
|
require.Nil(t, keyPair.PrivateKey)
|
|
}
|
|
|
|
// node is not authorized to fetch CA with secrets
|
|
_, err = nodeClt.GetCertAuthority(ctx, hostCAID, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// generate server keys for proxy
|
|
proxyClt, err := testSrv.NewClient(authtest.TestIdentity{
|
|
I: authz.BuiltinRole{
|
|
Username: "00000000-0000-0000-0000-000000000001",
|
|
Role: types.RoleProxy,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, proxyClt.Close()) })
|
|
|
|
// proxy can't fetch the host CA with secrets
|
|
_, err = proxyClt.GetCertAuthority(ctx, hostCAID, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// proxy can't fetch SAML IdP CA with secrets
|
|
_, err = proxyClt.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.SAMLIDPCA,
|
|
}, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// proxy can't fetch anything else with secrets
|
|
_, err = proxyClt.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.DatabaseCA,
|
|
}, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
_, err = proxyClt.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.DatabaseClientCA,
|
|
}, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
_, err = proxyClt.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.OIDCIdPCA,
|
|
}, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// non-admin users are not allowed to get access to private key material
|
|
user, err := types.NewUser("bob")
|
|
require.NoError(t, err)
|
|
|
|
role := services.RoleForUser(user)
|
|
role.SetLogins(types.Allow, []string{user.GetName()})
|
|
role, err = testSrv.Auth().UpsertRole(ctx, role)
|
|
require.NoError(t, err)
|
|
|
|
user.AddRole(role.GetName())
|
|
user, err = testSrv.Auth().UpsertUser(ctx, user)
|
|
require.NoError(t, err)
|
|
|
|
userClt, err := testSrv.NewClient(authtest.TestUser(user.GetName()))
|
|
require.NoError(t, err)
|
|
defer userClt.Close()
|
|
|
|
// user is authorized to fetch CA without secrets
|
|
_, err = userClt.GetCertAuthority(ctx, hostCAID, false)
|
|
require.NoError(t, err)
|
|
|
|
// user is not authorized to fetch CA with secrets
|
|
_, err = userClt.GetCertAuthority(ctx, hostCAID, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// user gets a not found message if a CA doesn't exist
|
|
require.NoError(t, testSrv.Auth().DeleteCertAuthority(ctx, hostCAID))
|
|
_, err = userClt.GetCertAuthority(ctx, hostCAID, false)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
// user is not authorized to fetch CA with secrets, even if CA doesn't exist
|
|
_, err = userClt.GetCertAuthority(ctx, hostCAID, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
|
|
func TestGetCertAuthority_ScopedIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t, withScopesFeatures(scopes.Features{Enabled: true}))
|
|
ctx := t.Context()
|
|
|
|
adminClient, err := srv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
defer adminClient.Close()
|
|
|
|
scopedSvc := adminClient.ScopedAccessServiceClient()
|
|
_, err = scopedSvc.CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
|
Role: scopedaccessv1.ScopedRole_builder{
|
|
Kind: scopedaccess.KindScopedRole,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "empty-role",
|
|
}.Build(),
|
|
Scope: "/test",
|
|
Spec: scopedaccessv1.ScopedRoleSpec_builder{
|
|
AssignableScopes: []string{"/test/scope"},
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
user, err := authtest.CreateUser(ctx, srv.Auth(), "scoped-reader")
|
|
require.NoError(t, err)
|
|
|
|
createResp, err := scopedSvc.CreateScopedRoleAssignment(ctx, scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
|
Assignment: scopedaccessv1.ScopedRoleAssignment_builder{
|
|
Kind: scopedaccess.KindScopedRoleAssignment,
|
|
SubKind: scopedaccess.SubKindDynamic,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: uuid.NewString(),
|
|
}.Build(),
|
|
Scope: "/test",
|
|
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
|
|
User: user.GetName(),
|
|
Assignments: []*scopedaccessv1.Assignment{
|
|
scopedaccessv1.Assignment_builder{Role: "/test::empty-role", Scope: "/test/scope"}.Build(),
|
|
},
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err := srv.AuthServer.AuthServer.ScopedAccessCache.GetScopedRoleAssignment(ctx, scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
|
|
Name: createResp.GetAssignment().GetMetadata().GetName(),
|
|
SubKind: createResp.GetAssignment().GetSubKind(),
|
|
Scope: createResp.GetAssignment().GetScope(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
scopedClt, err := srv.NewClient(authtest.TestScopedUser(user.GetName(), "/test/scope"))
|
|
require.NoError(t, err)
|
|
defer scopedClt.Close()
|
|
|
|
hostCAID := types.CertAuthID{
|
|
DomainName: srv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}
|
|
|
|
// scoped identity is authorized to fetch CA without secrets
|
|
_, err = scopedClt.GetCertAuthority(ctx, hostCAID, false)
|
|
require.NoError(t, err)
|
|
|
|
// scoped identity is not authorized to fetch CA with secrets
|
|
_, err = scopedClt.GetCertAuthority(ctx, hostCAID, true)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
|
|
func TestPluginData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
priv, pub, err := testauthority.GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
|
|
// make sure we can parse the private and public key
|
|
privateKey, err := ssh.ParseRawPrivateKey(priv)
|
|
require.NoError(t, err)
|
|
|
|
_, err = tlsca.MarshalPublicKeyFromPrivateKeyPEM(privateKey)
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, _, err = ssh.ParseAuthorizedKey(pub)
|
|
require.NoError(t, err)
|
|
|
|
user := "user1"
|
|
role := "some-role"
|
|
_, err = authtest.CreateUserRoleAndRequestable(testSrv.Auth(), user, role)
|
|
require.NoError(t, err)
|
|
|
|
testUser := authtest.TestUser(user)
|
|
testUser.TTL = time.Hour
|
|
userClient, err := testSrv.NewClient(testUser)
|
|
require.NoError(t, err)
|
|
|
|
plugin := "my-plugin"
|
|
_, err = authtest.CreateAccessPluginUser(ctx, testSrv.Auth(), plugin)
|
|
require.NoError(t, err)
|
|
|
|
pluginUser := authtest.TestUser(plugin)
|
|
pluginUser.TTL = time.Hour
|
|
pluginClient, err := testSrv.NewClient(pluginUser)
|
|
require.NoError(t, err)
|
|
|
|
req, err := services.NewAccessRequest(user, role)
|
|
require.NoError(t, err)
|
|
|
|
req, err = userClient.CreateAccessRequestV2(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
err = pluginClient.UpdatePluginData(ctx, types.PluginDataUpdateParams{
|
|
Kind: types.KindAccessRequest,
|
|
Resource: req.GetName(),
|
|
Plugin: plugin,
|
|
Set: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
data, err := pluginClient.GetPluginData(ctx, types.PluginDataFilter{
|
|
Kind: types.KindAccessRequest,
|
|
Resource: req.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, data, 1)
|
|
|
|
entry, ok := data[0].Entries()[plugin]
|
|
require.True(t, ok)
|
|
require.Empty(t, cmp.Diff(entry.Data, map[string]string{"foo": "bar"}))
|
|
|
|
err = pluginClient.UpdatePluginData(ctx, types.PluginDataUpdateParams{
|
|
Kind: types.KindAccessRequest,
|
|
Resource: req.GetName(),
|
|
Plugin: plugin,
|
|
Set: map[string]string{
|
|
"foo": "",
|
|
"spam": "eggs",
|
|
},
|
|
Expect: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
data, err = pluginClient.GetPluginData(ctx, types.PluginDataFilter{
|
|
Kind: types.KindAccessRequest,
|
|
Resource: req.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, data, 1)
|
|
|
|
entry, ok = data[0].Entries()[plugin]
|
|
require.True(t, ok)
|
|
require.Empty(t, cmp.Diff(entry.Data, map[string]string{"spam": "eggs"}))
|
|
}
|
|
|
|
// newSSHAndTLSKeyPairs returns newly generated keys for SSH and TLS.
|
|
// sshPrivKey and tlsPrivKey will be in PEM-encoded PKCS#1 or PKCS#8 format.
|
|
// sshPubKey will be in SSH authorized_keys format.
|
|
// tlsPubKey will be in PEM-encoded PKCS#1 or PKIX format.
|
|
func newSSHAndTLSKeyPairs(t *testing.T) (sshPrivKey, sshPubKey, tlsPrivKey, tlsPubKey []byte) {
|
|
sshKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.Ed25519)
|
|
require.NoError(t, err)
|
|
sshPrivKey, err = keys.MarshalPrivateKey(sshKey)
|
|
require.NoError(t, err)
|
|
sshPub, err := ssh.NewPublicKey(sshKey.Public())
|
|
require.NoError(t, err)
|
|
sshPubKey = ssh.MarshalAuthorizedKey(sshPub)
|
|
|
|
tlsKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
|
|
require.NoError(t, err)
|
|
tlsPrivKey, err = keys.MarshalPrivateKey(tlsKey)
|
|
require.NoError(t, err)
|
|
tlsPubKey, err = keys.MarshalPublicKey(tlsKey.Public())
|
|
require.NoError(t, err)
|
|
|
|
return sshPrivKey, sshPubKey, tlsPrivKey, tlsPubKey
|
|
}
|
|
|
|
// TestGenerateCerts tests edge cases around authorization of
|
|
// certificate generation for servers and users
|
|
func TestGenerateCerts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
srv := newTestTLSServer(t)
|
|
sshPrivKey, sshPubKey, tlsPrivKey, tlsPubKey := newSSHAndTLSKeyPairs(t)
|
|
|
|
clock := srv.Auth().GetClock()
|
|
|
|
// make sure we can parse the private and public keys
|
|
_, err := ssh.ParseRawPrivateKey(sshPrivKey)
|
|
require.NoError(t, err)
|
|
_, _, _, _, err = ssh.ParseAuthorizedKey(sshPubKey)
|
|
require.NoError(t, err)
|
|
|
|
// generate server keys for node
|
|
hostID := "00000000-0000-0000-0000-000000000000"
|
|
hostClient, err := srv.NewClient(authtest.TestIdentity{I: authz.BuiltinRole{Username: hostID, Role: types.RoleNode}})
|
|
require.NoError(t, err)
|
|
|
|
certs, err := hostClient.GenerateHostCerts(context.Background(),
|
|
&proto.HostCertsRequest{
|
|
HostID: hostID,
|
|
NodeName: srv.AuthServer.ClusterName,
|
|
Role: types.RoleNode,
|
|
AdditionalPrincipals: []string{"example.com"},
|
|
PublicSSHKey: sshPubKey,
|
|
PublicTLSKey: tlsPubKey,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
hostCert, err := sshutils.ParseCertificate(certs.SSH)
|
|
require.NoError(t, err)
|
|
require.Contains(t, hostCert.ValidPrincipals, "example.com")
|
|
|
|
// sign server public keys for node
|
|
hostID = "00000000-0000-0000-0000-000000000000"
|
|
hostClient, err = srv.NewClient(authtest.TestIdentity{I: authz.BuiltinRole{Username: hostID, Role: types.RoleNode}})
|
|
require.NoError(t, err)
|
|
|
|
certs, err = hostClient.GenerateHostCerts(context.Background(),
|
|
&proto.HostCertsRequest{
|
|
HostID: hostID,
|
|
NodeName: srv.AuthServer.ClusterName,
|
|
Role: types.RoleNode,
|
|
AdditionalPrincipals: []string{"example.com"},
|
|
PublicSSHKey: sshPubKey,
|
|
PublicTLSKey: tlsPubKey,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
hostCert, err = sshutils.ParseCertificate(certs.SSH)
|
|
require.NoError(t, err)
|
|
require.Contains(t, hostCert.ValidPrincipals, "example.com")
|
|
|
|
t.Run("HostClients", func(t *testing.T) {
|
|
// attempt to elevate privileges by getting admin role in the certificate
|
|
_, err = hostClient.GenerateHostCerts(context.Background(),
|
|
&proto.HostCertsRequest{
|
|
HostID: hostID,
|
|
NodeName: srv.AuthServer.ClusterName,
|
|
Role: types.RoleAdmin,
|
|
PublicSSHKey: sshPubKey,
|
|
PublicTLSKey: tlsPubKey,
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// attempt to get certificate for different host id
|
|
_, err = hostClient.GenerateHostCerts(context.Background(),
|
|
&proto.HostCertsRequest{
|
|
HostID: "some-other-host-id",
|
|
NodeName: srv.AuthServer.ClusterName,
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPubKey,
|
|
PublicTLSKey: tlsPubKey,
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
})
|
|
|
|
user1, userRole, err := authtest.CreateUserAndRole(srv.Auth(), "user1", []string{"user1"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
user2, userRole2, err := authtest.CreateUserAndRole(srv.Auth(), "user2", []string{"user2"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Nop", func(t *testing.T) {
|
|
// unauthenticated client should NOT be able to generate a user cert without auth
|
|
nopClient, err := srv.NewClient(authtest.TestNop())
|
|
require.NoError(t, err)
|
|
|
|
_, err = nopClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err), err.Error())
|
|
})
|
|
|
|
testUser2 := authtest.TestUser(user2.GetName())
|
|
testUser2.TTL = time.Hour
|
|
userClient2, err := srv.NewClient(testUser2)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("ImpersonateDeny", func(t *testing.T) {
|
|
// User can't generate certificates for another user by default
|
|
_, err = userClient2.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
})
|
|
|
|
parseCert := func(sshCert []byte) (*ssh.Certificate, time.Duration) {
|
|
parsedCert, err := sshutils.ParseCertificate(sshCert)
|
|
require.NoError(t, err)
|
|
validBefore := time.Unix(int64(parsedCert.ValidBefore), 0)
|
|
return parsedCert, validBefore.Sub(clock.Now())
|
|
}
|
|
|
|
t.Run("ImpersonateAllow", func(t *testing.T) {
|
|
// Super impersonator impersonate anyone and login as root
|
|
maxSessionTTL := 300 * time.Hour
|
|
authPref, err := srv.Auth().GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
authPref.SetDefaultSessionTTL(types.Duration(maxSessionTTL))
|
|
_, err = srv.Auth().UpsertAuthPreference(ctx, authPref)
|
|
require.NoError(t, err)
|
|
superImpersonatorRole, err := types.NewRole("superimpersonator", types.RoleSpecV6{
|
|
Options: types.RoleOptions{
|
|
MaxSessionTTL: types.Duration(maxSessionTTL),
|
|
},
|
|
Allow: types.RoleConditions{
|
|
Logins: []string{"root"},
|
|
Impersonate: &types.ImpersonateConditions{
|
|
Users: []string{types.Wildcard},
|
|
Roles: []string{types.Wildcard},
|
|
},
|
|
Rules: []types.Rule{},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
superImpersonator, err := authtest.CreateUser(ctx, srv.Auth(), "superimpersonator", superImpersonatorRole)
|
|
require.NoError(t, err)
|
|
|
|
// Impersonator can generate certificates for super impersonator
|
|
role, err := types.NewRole("impersonate", types.RoleSpecV6{
|
|
Allow: types.RoleConditions{
|
|
Logins: []string{superImpersonator.GetName()},
|
|
Impersonate: &types.ImpersonateConditions{
|
|
Users: []string{superImpersonator.GetName()},
|
|
Roles: []string{superImpersonatorRole.GetName()},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
impersonator, err := authtest.CreateUser(ctx, srv.Auth(), "impersonator", role)
|
|
require.NoError(t, err)
|
|
|
|
iUser := authtest.TestUser(impersonator.GetName())
|
|
iUser.TTL = time.Hour
|
|
iClient, err := srv.NewClient(iUser)
|
|
require.NoError(t, err)
|
|
|
|
// can impersonate super impersonator and request certs
|
|
// longer than their own TTL, but not exceeding super impersonator's max session ttl
|
|
userCerts, err := iClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: superImpersonator.GetName(),
|
|
Expires: clock.Now().Add(1000 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, diff := parseCert(userCerts.SSH)
|
|
require.LessOrEqual(t, diff, maxSessionTTL)
|
|
|
|
tlsCert, err := tlsca.ParseCertificatePEM(userCerts.TLS)
|
|
require.NoError(t, err)
|
|
identity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
|
|
require.NoError(t, err)
|
|
|
|
// Because the original request has maxed out the possible max
|
|
// session TTL, it will be adjusted to exactly the value (within rounding errors)
|
|
require.WithinDuration(t, clock.Now().Add(maxSessionTTL), identity.Expires, time.Second)
|
|
require.Equal(t, impersonator.GetName(), identity.Impersonator)
|
|
require.Equal(t, superImpersonator.GetName(), identity.Username)
|
|
|
|
// impersonator can't impersonate user1
|
|
_, err = iClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err), "trace.IsAccessDenied failed: err=%v (%T)", err, trace.Unwrap(err))
|
|
|
|
clientCert, err := tls.X509KeyPair(userCerts.TLS, tlsPrivKey)
|
|
require.NoError(t, err)
|
|
|
|
// client that uses impersonated certificate can't impersonate other users
|
|
// although super impersonator's roles allow it
|
|
impersonatedClient, err := srv.NewClientWithCert(clientCert)
|
|
require.NoError(t, err)
|
|
_, err = impersonatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsAccessDenied(err), "trace.IsAccessDenied failed: err=%v (%T)", err, trace.Unwrap(err))
|
|
require.Contains(t, err.Error(), "impersonated user can not impersonate anyone else")
|
|
|
|
// but can renew their own cert, for example set route to cluster
|
|
rc, err := types.NewRemoteCluster("cluster-remote")
|
|
require.NoError(t, err)
|
|
rc, err = srv.Auth().CreateRemoteCluster(ctx, rc)
|
|
require.NoError(t, err)
|
|
|
|
userCerts, err = impersonatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: superImpersonator.GetName(),
|
|
Expires: clock.Now().Add(time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
RouteToCluster: rc.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
// Make sure impersonator was not lost in the renewed cert
|
|
tlsCert, err = tlsca.ParseCertificatePEM(userCerts.TLS)
|
|
require.NoError(t, err)
|
|
identity, err = tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.WithinDuration(t, identity.Expires, clock.Now().Add(time.Hour), time.Second)
|
|
require.Equal(t, impersonator.GetName(), identity.Impersonator)
|
|
require.Equal(t, superImpersonator.GetName(), identity.Username)
|
|
})
|
|
|
|
t.Run("Renew", func(t *testing.T) {
|
|
testUser2 := authtest.TestUser(user2.GetName())
|
|
testUser2.TTL = time.Hour
|
|
userClient2, err := srv.NewClient(testUser2)
|
|
require.NoError(t, err)
|
|
|
|
rc1, err := types.NewRemoteCluster("cluster1")
|
|
require.NoError(t, err)
|
|
rc1, err = srv.Auth().CreateRemoteCluster(ctx, rc1)
|
|
require.NoError(t, err)
|
|
|
|
// User can renew their certificates, however the TTL will be limited
|
|
// to the TTL of their session for both SSH and x509 certs and
|
|
// that route to cluster will be encoded in the cert metadata
|
|
userCerts, err := userClient2.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user2.GetName(),
|
|
Expires: clock.Now().Add(100 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
RouteToCluster: rc1.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, diff := parseCert(userCerts.SSH)
|
|
require.LessOrEqual(t, diff, testUser2.TTL)
|
|
|
|
tlsCert, err := tlsca.ParseCertificatePEM(userCerts.TLS)
|
|
require.NoError(t, err)
|
|
identity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.WithinDuration(t, clock.Now().Add(testUser2.TTL), identity.Expires, time.Second)
|
|
require.Equal(t, identity.RouteToCluster, rc1.GetName())
|
|
})
|
|
|
|
t.Run("Admin", func(t *testing.T) {
|
|
// Admin should be allowed to generate certs with TTL longer than max.
|
|
adminClient, err := srv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
userCerts, err := adminClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(40 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
parsedCert, diff := parseCert(userCerts.SSH)
|
|
require.Less(t, apidefaults.MaxCertDuration, diff)
|
|
|
|
// user should have agent forwarding (default setting)
|
|
require.Contains(t, parsedCert.Extensions, teleport.CertExtensionPermitAgentForwarding)
|
|
|
|
// user should not have X11 forwarding (default setting)
|
|
require.NotContains(t, parsedCert.Extensions, teleport.CertExtensionPermitX11Forwarding)
|
|
|
|
// now update role to permit agent and X11 forwarding
|
|
roleOptions := userRole.GetOptions()
|
|
roleOptions.ForwardAgent = types.NewBool(true)
|
|
roleOptions.PermitX11Forwarding = types.NewBool(true)
|
|
userRole.SetOptions(roleOptions)
|
|
userRole, err = srv.Auth().UpsertRole(ctx, userRole)
|
|
require.NoError(t, err)
|
|
|
|
userCerts, err = adminClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(1 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.NoError(t, err)
|
|
parsedCert, _ = parseCert(userCerts.SSH)
|
|
|
|
// user should get agent forwarding
|
|
require.Contains(t, parsedCert.Extensions, teleport.CertExtensionPermitAgentForwarding)
|
|
|
|
// user should get X11 forwarding
|
|
require.Contains(t, parsedCert.Extensions, teleport.CertExtensionPermitX11Forwarding)
|
|
|
|
// apply HTTP Auth to generate user cert:
|
|
userCerts, err = adminClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user1.GetName(),
|
|
Expires: clock.Now().Add(time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, _, err = ssh.ParseAuthorizedKey(userCerts.SSH)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("DenyLeaf", func(t *testing.T) {
|
|
// User can't generate certificates for an unknown leaf cluster.
|
|
_, err = userClient2.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user2.GetName(),
|
|
Expires: clock.Now().Add(100 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
RouteToCluster: "unknown_cluster",
|
|
})
|
|
require.Error(t, err)
|
|
|
|
rc2, err := types.NewRemoteCluster("cluster2")
|
|
require.NoError(t, err)
|
|
meta := rc2.GetMetadata()
|
|
meta.Labels = map[string]string{"env": "prod"}
|
|
rc2.SetMetadata(meta)
|
|
rc2, err = srv.Auth().CreateRemoteCluster(ctx, rc2)
|
|
require.NoError(t, err)
|
|
|
|
// User can't generate certificates for leaf cluster they don't have access
|
|
// to due to labels.
|
|
_, err = userClient2.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user2.GetName(),
|
|
Expires: clock.Now().Add(100 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
RouteToCluster: rc2.GetName(),
|
|
})
|
|
require.Error(t, err)
|
|
|
|
userRole2.SetClusterLabels(types.Allow, types.Labels{"env": apiutils.Strings{"prod"}})
|
|
userRole2, err = srv.Auth().UpsertRole(ctx, userRole2)
|
|
require.NoError(t, err)
|
|
|
|
// User can generate certificates for leaf cluster they do have access to.
|
|
userCerts, err := userClient2.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: sshPubKey,
|
|
TLSPublicKey: tlsPubKey,
|
|
Username: user2.GetName(),
|
|
Expires: clock.Now().Add(100 * time.Hour).UTC(),
|
|
Format: constants.CertificateFormatStandard,
|
|
RouteToCluster: rc2.GetName(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tlsCert, err := tlsca.ParseCertificatePEM(userCerts.TLS)
|
|
require.NoError(t, err)
|
|
identity, err := tlsca.FromSubject(tlsCert.Subject, tlsCert.NotAfter)
|
|
require.NoError(t, err)
|
|
require.Equal(t, identity.RouteToCluster, rc2.GetName())
|
|
})
|
|
|
|
t.Run("SplitKeys", func(t *testing.T) {
|
|
testUser2 := authtest.TestUser(user2.GetName())
|
|
testUser2.TTL = time.Hour
|
|
userClient2, err := srv.NewClient(testUser2)
|
|
require.NoError(t, err)
|
|
|
|
// All standard library key types implement this.
|
|
type equalPublicKey interface {
|
|
Equal(crypto.PublicKey) bool
|
|
}
|
|
|
|
sshCryptoPub, err := libsshutils.CryptoPublicKey(sshPubKey)
|
|
require.NoError(t, err)
|
|
sshEqualPub := sshCryptoPub.(equalPublicKey)
|
|
|
|
for _, tc := range []struct {
|
|
desc string
|
|
publicKey []byte
|
|
sshPublicKey []byte
|
|
tlsPublicKey []byte
|
|
expectBadParameter bool
|
|
tlsPrivateKey []byte
|
|
expectSSHCertPublicKey equalPublicKey
|
|
}{
|
|
{
|
|
desc: "both keys",
|
|
sshPublicKey: sshPubKey,
|
|
tlsPublicKey: tlsPubKey,
|
|
tlsPrivateKey: tlsPrivKey,
|
|
expectSSHCertPublicKey: sshEqualPub,
|
|
},
|
|
{
|
|
desc: "only SSH",
|
|
sshPublicKey: sshPubKey,
|
|
expectSSHCertPublicKey: sshEqualPub,
|
|
},
|
|
{
|
|
desc: "only TLS",
|
|
tlsPublicKey: tlsPubKey,
|
|
tlsPrivateKey: tlsPrivKey,
|
|
},
|
|
{
|
|
desc: "no key",
|
|
expectBadParameter: true,
|
|
},
|
|
} {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
certs, err := userClient2.GenerateUserCerts(ctx, proto.UserCertsRequest{
|
|
SSHPublicKey: tc.sshPublicKey,
|
|
TLSPublicKey: tc.tlsPublicKey,
|
|
Username: user2.GetName(),
|
|
Expires: clock.Now().Add(100 * time.Hour).UTC(),
|
|
})
|
|
|
|
if tc.expectBadParameter {
|
|
var badParameterErr *trace.BadParameterError
|
|
require.ErrorAs(t, err, &badParameterErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
if tc.sshPublicKey != nil {
|
|
// Make sure the SSH cert public key matches up.
|
|
sshAuthorizedKey, _, _, _, err := ssh.ParseAuthorizedKey(certs.SSH)
|
|
require.NoError(t, err)
|
|
sshCert := sshAuthorizedKey.(*ssh.Certificate)
|
|
sshCertCryptoPub := sshCert.Key.(ssh.CryptoPublicKey).CryptoPublicKey()
|
|
require.True(t, tc.expectSSHCertPublicKey.Equal(sshCertCryptoPub))
|
|
}
|
|
|
|
if tc.tlsPublicKey != nil {
|
|
// This makes sure that the TLS cert public key matches tc.tlsPrivateKey.
|
|
_, err = tls.X509KeyPair(certs.TLS, tc.tlsPrivateKey)
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestGenerateAppToken checks the identity of the caller and makes sure only
|
|
// certain roles can request JWT tokens.
|
|
func TestGenerateAppToken(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
authClient, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleAdmin))
|
|
require.NoError(t, err)
|
|
|
|
getJWTKey := func(caType types.CertAuthType) *jwt.Key {
|
|
ca, err := authClient.GetCertAuthority(context.Background(), types.CertAuthID{
|
|
Type: caType,
|
|
DomainName: testSrv.ClusterName(),
|
|
}, true)
|
|
require.NoError(t, err)
|
|
|
|
signer, err := testSrv.AuthServer.AuthServer.GetKeyStore().GetJWTSigner(ctx, ca)
|
|
require.NoError(t, err)
|
|
key, err := services.GetJWTSigner(signer, ca.GetClusterName(), clock)
|
|
require.NoError(t, err)
|
|
return key
|
|
}
|
|
|
|
defaultKey := getJWTKey(types.JWTSigner)
|
|
oidcKey := getJWTKey(types.OIDCIdPCA)
|
|
|
|
proxyServer, err := types.NewServer("proxy-hostname", types.KindProxy, types.ServerSpecV2{
|
|
PublicAddrs: []string{"https://teleport.example.com"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, authClient.UpsertProxyServerWithoutReturn(ctx, proxyServer))
|
|
|
|
tests := []struct {
|
|
inMachineRole types.SystemRole
|
|
inComment string
|
|
inAuthorityType types.CertAuthType
|
|
outKey *jwt.Key
|
|
outIssuer string
|
|
outError bool
|
|
}{
|
|
{
|
|
inMachineRole: types.RoleNode,
|
|
inComment: "nodes should not have the ability to generate tokens",
|
|
outError: true,
|
|
},
|
|
{
|
|
inMachineRole: types.RoleProxy,
|
|
inComment: "proxies should not have the ability to generate tokens",
|
|
outError: true,
|
|
},
|
|
{
|
|
inMachineRole: types.RoleApp,
|
|
inComment: "only apps should have the ability to generate tokens",
|
|
inAuthorityType: "", // defaults to "jwt"
|
|
outKey: defaultKey,
|
|
outError: false,
|
|
},
|
|
{
|
|
inMachineRole: types.RoleApp,
|
|
inComment: "explicit jwt authority",
|
|
inAuthorityType: types.JWTSigner,
|
|
outKey: defaultKey,
|
|
outError: false,
|
|
},
|
|
{
|
|
inMachineRole: types.RoleApp,
|
|
inComment: "oidc authority",
|
|
inAuthorityType: types.OIDCIdPCA,
|
|
outKey: oidcKey,
|
|
outIssuer: "https://teleport.example.com",
|
|
outError: false,
|
|
},
|
|
}
|
|
for _, ts := range tests {
|
|
client, err := testSrv.NewClient(authtest.TestBuiltin(ts.inMachineRole))
|
|
require.NoError(t, err, ts.inComment)
|
|
|
|
token, err := client.GenerateAppToken(
|
|
context.Background(),
|
|
types.GenerateAppTokenRequest{
|
|
Username: "[email protected]",
|
|
Roles: []string{"bar", "baz"},
|
|
Traits: wrappers.Traits{
|
|
"trait1": {"value1", "value2"},
|
|
"trait2": {"value3", "value4"},
|
|
"trait3": nil,
|
|
},
|
|
URI: "https://localhost:8080",
|
|
Expires: clock.Now().Add(1 * time.Minute),
|
|
AuthorityType: ts.inAuthorityType,
|
|
})
|
|
require.Equal(t, err != nil, ts.outError, ts.inComment)
|
|
if !ts.outError {
|
|
claims, err := ts.outKey.Verify(jwt.VerifyParams{
|
|
Username: "[email protected]",
|
|
RawToken: token,
|
|
URI: "https://localhost:8080",
|
|
Issuer: ts.outIssuer,
|
|
})
|
|
require.NoError(t, err, ts.inComment)
|
|
require.Equal(t, "[email protected]", claims.Username, ts.inComment)
|
|
require.Empty(t, cmp.Diff(claims.Roles, []string{"bar", "baz"}), ts.inComment)
|
|
require.Empty(t, cmp.Diff(claims.Traits, wrappers.Traits{
|
|
"trait1": {"value1", "value2"},
|
|
"trait2": {"value3", "value4"},
|
|
}), ts.inComment)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCertificateFormat makes sure that certificates are generated with the
|
|
// correct format.
|
|
func TestCertificateFormat(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// use admin client to create user and role
|
|
user, userRole, err := authtest.CreateUserAndRole(testSrv.Auth(), "user", []string{"user"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
pass := []byte("very secure password")
|
|
err = testSrv.Auth().UpsertPassword(user.GetName(), pass)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
inRoleCertificateFormat string
|
|
inClientCertificateFormat string
|
|
outCertContainsRole bool
|
|
}{
|
|
// 0 - take whatever the role has
|
|
{
|
|
teleport.CertificateFormatOldSSH,
|
|
teleport.CertificateFormatUnspecified,
|
|
false,
|
|
},
|
|
// 1 - override the role
|
|
{
|
|
teleport.CertificateFormatOldSSH,
|
|
constants.CertificateFormatStandard,
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, ts := range tests {
|
|
roleOptions := userRole.GetOptions()
|
|
roleOptions.CertificateFormat = ts.inRoleCertificateFormat
|
|
userRole.SetOptions(roleOptions)
|
|
userRole, err = testSrv.Auth().UpsertRole(ctx, userRole)
|
|
require.NoError(t, err)
|
|
|
|
proxyClient, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
// authentication attempt fails with password auth only
|
|
re, err := proxyClient.AuthenticateSSHUser(ctx, authclient.AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: authclient.AuthenticateUserRequest{
|
|
Username: user.GetName(),
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
SSHPublicKey: []byte(sshPubKey),
|
|
TLSPublicKey: []byte(tlsPubKey),
|
|
},
|
|
CompatibilityMode: ts.inClientCertificateFormat,
|
|
TTL: apidefaults.CertDuration,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
parsedCert, err := sshutils.ParseCertificate(re.Cert)
|
|
require.NoError(t, err)
|
|
|
|
_, ok := parsedCert.Extensions[teleport.CertExtensionTeleportRoles]
|
|
require.Equal(t, ok, ts.outCertContainsRole)
|
|
}
|
|
}
|
|
|
|
// TestClusterConfigContext checks that the cluster configuration gets passed
|
|
// along in the context and permissions get updated accordingly.
|
|
func TestClusterConfigContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
_, pub, err := testauthority.GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
|
|
// try and generate a host cert, this should succeed because although
|
|
// we are recording at the nodes not at the proxy, the proxy may
|
|
// need to generate host certs if a client wants to connect to an
|
|
// agentless node
|
|
_, err = proxy.TrustClient().GenerateHostCert(ctx, trustpb.GenerateHostCertRequest_builder{
|
|
Key: pub,
|
|
HostId: "a",
|
|
NodeName: "b",
|
|
Principals: nil,
|
|
ClusterName: "localhost",
|
|
Role: string(types.RoleProxy),
|
|
Ttl: durationpb.New(0),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
// update cluster config to record at the proxy
|
|
recConfig, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
|
|
Mode: types.RecordAtProxy,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = testSrv.Auth().UpsertSessionRecordingConfig(ctx, recConfig)
|
|
require.NoError(t, err)
|
|
|
|
// try and generate a host cert
|
|
_, err = proxy.TrustClient().GenerateHostCert(ctx, trustpb.GenerateHostCertRequest_builder{
|
|
Key: pub,
|
|
HostId: "a",
|
|
NodeName: "b",
|
|
Principals: nil,
|
|
ClusterName: "localhost",
|
|
Role: string(types.RoleProxy),
|
|
Ttl: durationpb.New(0),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestAuthenticateWebUserOTP tests web authentication flow for password + OTP
|
|
func TestAuthenticateWebUserOTP(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
user := "ws-test"
|
|
pass := []byte("ws-abcdef123456")
|
|
rawSecret := "def456"
|
|
otpSecret := base32.StdEncoding.EncodeToString([]byte(rawSecret))
|
|
|
|
_, _, err = authtest.CreateUserAndRole(clt, user, []string{user}, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
dev, err := services.NewTOTPDevice("otp", otpSecret, clock.Now())
|
|
require.NoError(t, err)
|
|
err = testSrv.Auth().UpsertMFADevice(ctx, user, dev)
|
|
require.NoError(t, err)
|
|
|
|
// create a valid otp token
|
|
validToken, err := totp.GenerateCode(otpSecret, clock.Now())
|
|
require.NoError(t, err)
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorOTP,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = testSrv.Auth().UpsertAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
// authentication attempt fails with wrong password
|
|
_, err = proxy.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
OTP: &authclient.OTPCreds{Password: []byte("wrong password"), Token: validToken},
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// authentication attempt fails with wrong otp
|
|
_, err = proxy.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
OTP: &authclient.OTPCreds{Password: pass, Token: "wrong123"},
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// authentication attempt fails with password auth only
|
|
_, err = proxy.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// authentication succeeds
|
|
_, err = proxy.AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
OTP: &authclient.OTPCreds{Password: pass, Token: validToken},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestLoginAttempts makes sure the login attempt counter is incremented and
|
|
// reset correctly.
|
|
func TestLoginAttempts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
user := "user1"
|
|
pass := []byte("abcdef123456")
|
|
|
|
_, _, err = authtest.CreateUserAndRole(clt, user, []string{user}, nil)
|
|
require.NoError(t, err)
|
|
|
|
proxy, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleProxy))
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
req := authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: []byte("wrong password"),
|
|
},
|
|
}
|
|
// authentication attempt fails with bad password
|
|
_, err = proxy.AuthenticateWebUser(ctx, req)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// creates first failed login attempt
|
|
loginAttempts, err := testSrv.Auth().GetUserLoginAttempts(user)
|
|
require.NoError(t, err)
|
|
require.Len(t, loginAttempts, 1)
|
|
|
|
// try second time with wrong pass
|
|
req.Pass.Password = pass
|
|
_, err = proxy.AuthenticateWebUser(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
// clears all failed attempts after success
|
|
loginAttempts, err = testSrv.Auth().GetUserLoginAttempts(user)
|
|
require.NoError(t, err)
|
|
require.Empty(t, loginAttempts)
|
|
}
|
|
|
|
func TestChangeUserAuthenticationSettings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
AllowLocalAuth: types.NewBoolOption(true),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = testSrv.Auth().UpsertAuthPreference(ctx, authPref)
|
|
require.NoError(t, err)
|
|
|
|
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorWebauthn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "localhost",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = testSrv.Auth().UpsertAuthPreference(ctx, authPreference)
|
|
require.NoError(t, err)
|
|
|
|
username := "user1"
|
|
// Create a local user.
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Reset works when user exists", func(t *testing.T) {
|
|
_, _, err = authtest.CreateUserAndRole(clt, username, []string{"role1"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
token, err := testSrv.Auth().CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: time.Hour,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
res, err := testSrv.Auth().CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
|
|
TokenID: token.GetName(),
|
|
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, registerSolved, err := authtest.NewTestDeviceFromChallenge(res)
|
|
require.NoError(t, err)
|
|
|
|
_, err = testSrv.Auth().ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{
|
|
TokenID: token.GetName(),
|
|
NewPassword: []byte("qweqweqweqwe"),
|
|
NewMFARegisterResponse: registerSolved,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Reset link not allowed when user does not exist", func(t *testing.T) {
|
|
var tokenID string
|
|
var resp *proto.MFARegisterResponse
|
|
for range 5 {
|
|
token, err := testSrv.Auth().CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
|
|
Name: username,
|
|
TTL: time.Hour,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
res, err := testSrv.Auth().CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
|
|
TokenID: token.GetName(),
|
|
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, registerSolved, err := authtest.NewTestDeviceFromChallenge(res)
|
|
require.NoError(t, err)
|
|
|
|
tokenID = token.GetName()
|
|
resp = registerSolved
|
|
}
|
|
|
|
require.NoError(t, testSrv.Auth().DeleteUser(ctx, username))
|
|
|
|
_, err = testSrv.Auth().ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{
|
|
TokenID: tokenID,
|
|
NewPassword: []byte("qweqweqweqwe"),
|
|
NewMFARegisterResponse: resp,
|
|
})
|
|
require.Error(t, err)
|
|
|
|
userTokens, err := stream.Collect(clientutils.Resources(ctx, testSrv.Auth().ListUserTokens))
|
|
require.NoError(t, err)
|
|
require.Empty(t, userTokens)
|
|
})
|
|
}
|
|
|
|
// TestLoginNoLocalAuth makes sure that logins for local accounts can not be
|
|
// performed when local auth is disabled.
|
|
func TestLoginNoLocalAuth(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
user := "foo"
|
|
pass := []byte("feefiefoefum")
|
|
|
|
// Create a local user.
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
_, _, err = authtest.CreateUserAndRole(clt, user, []string{user}, nil)
|
|
require.NoError(t, err)
|
|
err = testSrv.Auth().UpsertPassword(user, pass)
|
|
require.NoError(t, err)
|
|
|
|
// Set auth preference to disallow local auth.
|
|
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
AllowLocalAuth: types.NewBoolOption(false),
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = testSrv.Auth().UpsertAuthPreference(ctx, authPref)
|
|
require.NoError(t, err)
|
|
|
|
// Make sure access is denied for web login.
|
|
_, err = testSrv.Auth().AuthenticateWebUser(ctx, authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// Make sure access is denied for SSH login.
|
|
_, err = testSrv.Auth().AuthenticateSSHUser(ctx, authclient.AuthenticateSSHRequest{
|
|
AuthenticateUserRequest: authclient.AuthenticateUserRequest{
|
|
Username: user,
|
|
Pass: &authclient.PassCreds{
|
|
Password: pass,
|
|
},
|
|
SSHPublicKey: []byte(sshPubKey),
|
|
TLSPublicKey: []byte(tlsPubKey),
|
|
},
|
|
})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
}
|
|
|
|
// TestCipherSuites makes sure that clients with invalid cipher suites can
|
|
// not connect.
|
|
func TestCipherSuites(t *testing.T) {
|
|
t.Parallel()
|
|
testSrv := newTestTLSServer(t)
|
|
ctx := context.Background()
|
|
|
|
otherServer, err := testSrv.AuthServer.NewTestTLSServer()
|
|
require.NoError(t, err)
|
|
defer otherServer.Close()
|
|
|
|
// Create a client with ciphersuites that the server does not support.
|
|
tlsConfig, err := testSrv.ClientTLSConfig(authtest.TestNop())
|
|
require.NoError(t, err)
|
|
tlsConfig.CipherSuites = []uint16{
|
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
}
|
|
|
|
addrs := []string{
|
|
otherServer.Addr().String(),
|
|
testSrv.Addr().String(),
|
|
}
|
|
client, err := authclient.NewClient(client.Config{
|
|
Addrs: addrs,
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: breaker.NoopBreakerConfig(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Requests should fail.
|
|
_, err = client.GetClusterName(ctx)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestTLSFailover tests HTTP client failover between two tls servers
|
|
func TestTLSFailover(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
otherServer, err := testSrv.AuthServer.NewTestTLSServer()
|
|
require.NoError(t, err)
|
|
defer otherServer.Close()
|
|
|
|
tlsConfig, err := testSrv.ClientTLSConfig(authtest.TestNop())
|
|
require.NoError(t, err)
|
|
|
|
addrs := []string{
|
|
otherServer.Addr().String(),
|
|
testSrv.Addr().String(),
|
|
}
|
|
client, err := authclient.NewClient(client.Config{
|
|
Addrs: addrs,
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: breaker.NoopBreakerConfig(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// couple of runs to get enough connections
|
|
for range 4 {
|
|
_, err = client.Get(ctx, client.Endpoint("not", "exist"), url.Values{})
|
|
require.True(t, trace.IsNotFound(err))
|
|
}
|
|
|
|
// stop the server to get response
|
|
err = otherServer.Stop()
|
|
require.NoError(t, err)
|
|
|
|
// client detects closed sockets and reconnect to the backup server
|
|
for range 4 {
|
|
_, err = client.Get(ctx, client.Endpoint("not", "exist"), url.Values{})
|
|
require.True(t, trace.IsNotFound(err))
|
|
}
|
|
}
|
|
|
|
// TestJoinCAPin makes sure that joining only works with a valid CA pin.
|
|
func TestJoinCAPin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
|
|
// Generate a token to use.
|
|
token := generateTestToken(
|
|
ctx,
|
|
t,
|
|
types.SystemRoles{types.RoleProxy},
|
|
time.Time{},
|
|
testSrv.Auth(),
|
|
)
|
|
joinParams := func() joinclient.JoinParams {
|
|
return joinclient.JoinParams{
|
|
AuthServers: []utils.NetAddr{utils.FromAddr(testSrv.Addr())},
|
|
Token: token,
|
|
ID: state.IdentityID{
|
|
NodeName: "node-name",
|
|
Role: types.RoleInstance,
|
|
},
|
|
AdditionalPrincipals: []string{"example.com"},
|
|
Clock: clock,
|
|
}
|
|
}
|
|
|
|
// Calculate what CA pin should be.
|
|
localCAResponse, err := testSrv.AuthServer.AuthServer.GetClusterCACert(ctx)
|
|
require.NoError(t, err)
|
|
caPins, err := tlsca.CalculatePins(localCAResponse.TLSCA)
|
|
require.NoError(t, err)
|
|
require.Len(t, caPins, 1)
|
|
caPin := caPins[0]
|
|
|
|
// Attempt to join with valid CA pin, should work.
|
|
params := joinParams()
|
|
params.CAPins = []string{caPin}
|
|
_, err = joinclient.Join(ctx, params)
|
|
require.NoError(t, err)
|
|
|
|
// An unrelated active lock should not block unauthenticated CA pin bootstrap.
|
|
lockTarget := types.LockTarget{User: "definitely-not-a-real-user"}
|
|
lock, err := types.NewLock("test-join-ca-pin-unrelated-lock", types.LockSpecV2{
|
|
Target: lockTarget,
|
|
})
|
|
require.NoError(t, err)
|
|
// Wait for the lock to actually propagate through the cache and be
|
|
// enforced before attempting to join.
|
|
lockWatcher, err := testSrv.Auth().SubscribeToLockTarget(ctx, lockTarget)
|
|
require.NoError(t, err)
|
|
require.NoError(t, testSrv.Auth().UpsertLock(ctx, lock))
|
|
select {
|
|
case evt := <-lockWatcher.Events():
|
|
require.Equal(t, types.OpPut, evt.Type)
|
|
require.Equal(t, types.KindLock, evt.Resource.GetKind())
|
|
case <-time.After(10 * time.Second):
|
|
require.FailNow(t, "did not receive lock put event within 10 seconds")
|
|
}
|
|
lockInForceErr := testSrv.Auth().CheckLockInForce(constants.LockingModeStrict, []types.LockTarget{{User: "definitely-not-a-real-user"}})
|
|
require.ErrorAs(t, lockInForceErr, new(*trace.AccessDeniedError))
|
|
|
|
params = joinParams()
|
|
params.CAPins = []string{caPin}
|
|
_, err = joinclient.Join(ctx, params)
|
|
require.NoError(t, err)
|
|
|
|
// Attempt to join with multiple CA pins where the auth server only
|
|
// matches one, should work.
|
|
params = joinParams()
|
|
params.CAPins = []string{"sha256:123", caPin}
|
|
_, err = joinclient.Join(ctx, params)
|
|
require.NoError(t, err)
|
|
|
|
// Attempt to join with invalid CA pin, should fail.
|
|
params = joinParams()
|
|
params.CAPins = []string{"sha256:123"}
|
|
_, err = joinclient.Join(ctx, params)
|
|
require.Error(t, err)
|
|
|
|
// Attempt to join with multiple invalid CA pins, should fail.
|
|
params = joinParams()
|
|
params.CAPins = []string{"sha256:123", "sha256:456"}
|
|
_, err = joinclient.Join(ctx, params)
|
|
require.Error(t, err)
|
|
|
|
// Add another cert to the CA (dupe the current one for simplicity)
|
|
hostCA, err := testSrv.AuthServer.AuthServer.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.AuthServer.ClusterName,
|
|
Type: types.HostCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
activeKeys := hostCA.GetActiveKeys()
|
|
activeKeys.TLS = append(activeKeys.TLS, activeKeys.TLS...)
|
|
hostCA.SetActiveKeys(activeKeys)
|
|
err = testSrv.AuthServer.AuthServer.UpsertCertAuthority(ctx, hostCA)
|
|
require.NoError(t, err)
|
|
|
|
// Calculate what CA pins should be.
|
|
localCAResponse, err = testSrv.AuthServer.AuthServer.GetClusterCACert(ctx)
|
|
require.NoError(t, err)
|
|
caPins, err = tlsca.CalculatePins(localCAResponse.TLSCA)
|
|
require.NoError(t, err)
|
|
require.Len(t, caPins, 2)
|
|
|
|
// Attempt to join with multiple CA pins, should work
|
|
params = joinParams()
|
|
params.CAPins = caPins
|
|
_, err = joinclient.Join(ctx, params)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestJoinCAPath makes sure joining only works with a valid CA
|
|
// file on disk.
|
|
func TestJoinCAPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
clock := testSrv.AuthServer.AuthServerConfig.Clock
|
|
dataDir := testSrv.AuthServer.Dir
|
|
|
|
// Generate a token to use.
|
|
token := generateTestToken(
|
|
ctx,
|
|
t,
|
|
types.SystemRoles{types.RoleProxy},
|
|
time.Time{},
|
|
testSrv.Auth(),
|
|
)
|
|
|
|
// Attempt to join with nothing at the CA path, should work.
|
|
_, err := joinclient.Join(ctx, joinclient.JoinParams{
|
|
AuthServers: []utils.NetAddr{utils.FromAddr(testSrv.Addr())},
|
|
Token: token,
|
|
ID: state.IdentityID{
|
|
NodeName: "node-name",
|
|
Role: types.RoleInstance,
|
|
},
|
|
AdditionalPrincipals: []string{"example.com"},
|
|
Clock: clock,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Extract the root CA public key and write it out to the data dir.
|
|
hostCA, err := testSrv.AuthServer.AuthServer.GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.AuthServer.ClusterName,
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
certs := services.GetTLSCerts(hostCA)
|
|
require.Len(t, certs, 1)
|
|
certPem := certs[0]
|
|
caPath := filepath.Join(dataDir, defaults.CACertFile)
|
|
err = os.WriteFile(caPath, certPem, teleport.FileMaskOwnerOnly)
|
|
require.NoError(t, err)
|
|
|
|
// Attempt to join with valid CA path, should work.
|
|
_, err = joinclient.Join(ctx, joinclient.JoinParams{
|
|
AuthServers: []utils.NetAddr{utils.FromAddr(testSrv.Addr())},
|
|
Token: token,
|
|
ID: state.IdentityID{
|
|
NodeName: "node-name",
|
|
Role: types.RoleInstance,
|
|
},
|
|
AdditionalPrincipals: []string{"example.com"},
|
|
CAPath: caPath,
|
|
Clock: clock,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestClusterAlertAck(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
alert1, err := types.NewClusterAlert("alert-1", "some msg")
|
|
require.NoError(t, err)
|
|
|
|
adminClt, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleAdmin))
|
|
require.NoError(t, err)
|
|
defer adminClt.Close()
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert1)
|
|
require.NoError(t, err)
|
|
|
|
expiry := time.Now().Add(time.Hour)
|
|
|
|
ack := types.AlertAcknowledgement{AlertID: "alert-1", Reason: "testing", Expires: expiry}
|
|
|
|
err = adminClt.CreateAlertAck(ctx, ack)
|
|
require.NoError(t, err)
|
|
|
|
acks, err := adminClt.GetAlertAcks(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, acks, 1)
|
|
|
|
require.Equal(t, "alert-1", acks[0].AlertID)
|
|
|
|
clear := proto.ClearAlertAcksRequest{
|
|
AlertID: "alert-1",
|
|
}
|
|
|
|
err = adminClt.ClearAlertAcks(ctx, clear)
|
|
require.NoError(t, err)
|
|
|
|
acks, err = adminClt.GetAlertAcks(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, acks)
|
|
}
|
|
|
|
func TestClusterAlertClearAckWildcard(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
alert1, err := types.NewClusterAlert("alert-1", "some msg")
|
|
require.NoError(t, err)
|
|
|
|
alert2, err := types.NewClusterAlert("alert-2", "some msg")
|
|
require.NoError(t, err)
|
|
|
|
adminClt, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleAdmin))
|
|
require.NoError(t, err)
|
|
defer adminClt.Close()
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert1)
|
|
require.NoError(t, err)
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert2)
|
|
require.NoError(t, err)
|
|
|
|
expiry := time.Now().Add(time.Hour)
|
|
|
|
ack := types.AlertAcknowledgement{AlertID: "alert-1", Reason: "testing", Expires: expiry}
|
|
|
|
err = adminClt.CreateAlertAck(ctx, ack)
|
|
require.NoError(t, err)
|
|
|
|
ack = types.AlertAcknowledgement{AlertID: "alert-2", Reason: "testing", Expires: expiry}
|
|
|
|
err = adminClt.CreateAlertAck(ctx, ack)
|
|
require.NoError(t, err)
|
|
|
|
acks, err := adminClt.GetAlertAcks(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, acks, 2)
|
|
|
|
require.Equal(t, "alert-1", acks[0].AlertID)
|
|
require.Equal(t, "alert-2", acks[1].AlertID)
|
|
|
|
clear := proto.ClearAlertAcksRequest{
|
|
AlertID: "*",
|
|
}
|
|
|
|
err = adminClt.ClearAlertAcks(ctx, clear)
|
|
require.NoError(t, err)
|
|
|
|
acks, err = adminClt.GetAlertAcks(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, acks)
|
|
}
|
|
|
|
// TestClusterAlertAccessControls verifies expected behaviors of cluster alert
|
|
// access controls.
|
|
func TestClusterAlertAccessControls(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
expectAlerts := func(alerts []types.ClusterAlert, names ...string) {
|
|
for _, alert := range alerts {
|
|
if slices.Contains(names, alert.Metadata.Name) {
|
|
return
|
|
}
|
|
t.Fatalf("unexpected alert %q", alert.Metadata.Name)
|
|
}
|
|
}
|
|
|
|
alert1, err := types.NewClusterAlert("alert-1", "some msg")
|
|
require.NoError(t, err)
|
|
|
|
alert2, err := types.NewClusterAlert("alert-2", "other msg")
|
|
require.NoError(t, err)
|
|
|
|
// set one of the two alerts to be viewable by all users
|
|
alert2.Metadata.Labels = map[string]string{
|
|
types.AlertPermitAll: "yes",
|
|
}
|
|
|
|
adminClt, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleAdmin))
|
|
require.NoError(t, err)
|
|
defer adminClt.Close()
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert1)
|
|
require.NoError(t, err)
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert2)
|
|
require.NoError(t, err)
|
|
|
|
// verify that admin client can see all alerts due to resource-level permissions
|
|
alerts, err := adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
|
|
WithUntargeted: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, 2)
|
|
expectAlerts(alerts, "alert-1", "alert-2")
|
|
|
|
// verify that WithUntargeted=false admin only observes the alert that specifies
|
|
// that it should be shown to all
|
|
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
|
|
WithUntargeted: false,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, 1)
|
|
expectAlerts(alerts, "alert-2")
|
|
|
|
// verify that some other client with no alert-specific permissions can
|
|
// see the "permit-all" subset of alerts (using role node here, but any
|
|
// role with no special provisions for alerts should be equivalent)
|
|
otherClt, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleNode))
|
|
require.NoError(t, err)
|
|
defer otherClt.Close()
|
|
|
|
// untargeted and targeted should result in the same behavior since otherClt
|
|
// does not have resource-level permissions for the cluster_alert type.
|
|
for _, untargeted := range []bool{true, false} {
|
|
alerts, err = otherClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
|
|
WithUntargeted: untargeted,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, 1)
|
|
expectAlerts(alerts, "alert-2")
|
|
}
|
|
|
|
// verify that we still reject unauthenticated clients
|
|
nopClt, err := testSrv.NewClient(authtest.TestUnauthenticated(types.RoleNop))
|
|
require.NoError(t, err)
|
|
defer nopClt.Close()
|
|
|
|
_, err = nopClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{})
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
// add more alerts that use resource verb permits
|
|
alert3, err := types.NewClusterAlert(
|
|
"alert-3",
|
|
"msg 3",
|
|
types.WithAlertLabel(types.AlertVerbPermit, "token:create"),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
alert4, err := types.NewClusterAlert(
|
|
"alert-4",
|
|
"msg 4",
|
|
types.WithAlertLabel(types.AlertVerbPermit, "token:create|role:read"),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert3)
|
|
require.NoError(t, err)
|
|
|
|
err = adminClt.UpsertClusterAlert(ctx, alert4)
|
|
require.NoError(t, err)
|
|
|
|
// verify that admin client can see all alerts in untargeted read mode
|
|
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
|
|
WithUntargeted: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, 4)
|
|
expectAlerts(alerts, "alert-1", "alert-2", "alert-3", "alert-4")
|
|
|
|
// verify that admin client can see all targeted alerts in targeted mode
|
|
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
|
|
WithUntargeted: false,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, 3)
|
|
expectAlerts(alerts, "alert-2", "alert-3", "alert-4")
|
|
|
|
// verify that node client can only see one of the two new alerts
|
|
alerts, err = otherClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{})
|
|
require.NoError(t, err)
|
|
require.Len(t, alerts, 2)
|
|
expectAlerts(alerts, "alert-2", "alert-4")
|
|
}
|
|
|
|
// TestEventsNodePresence tests streaming node presence API -
|
|
// announcing node and keeping node alive
|
|
func TestEventsNodePresence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
nodeName := "node1." + testSrv.ClusterName()
|
|
node := &types.ServerV2{
|
|
Kind: types.KindNode,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: nodeName,
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{
|
|
Addr: "localhost:3022",
|
|
},
|
|
}
|
|
node.SetExpiry(time.Now().Add(2 * time.Second))
|
|
clt, err := testSrv.NewClient(authtest.TestIdentity{
|
|
I: authz.BuiltinRole{
|
|
Role: types.RoleNode,
|
|
Username: nodeName,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
defer clt.Close()
|
|
|
|
keepAlive, err := clt.UpsertNode(ctx, node)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, keepAlive)
|
|
|
|
keepAliver, err := clt.NewKeepAliver(ctx)
|
|
require.NoError(t, err)
|
|
defer keepAliver.Close()
|
|
|
|
keepAlive.Expires = time.Now().Add(2 * time.Second)
|
|
select {
|
|
case keepAliver.KeepAlives() <- *keepAlive:
|
|
// ok
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("time out sending keep alive")
|
|
case <-keepAliver.Done():
|
|
t.Fatalf("unknown problem sending keep alive")
|
|
}
|
|
|
|
// upsert node and keep alives will fail for users with no privileges
|
|
nopClt, err := testSrv.NewClient(authtest.TestUnauthenticated(types.RoleNop))
|
|
require.NoError(t, err)
|
|
defer nopClt.Close()
|
|
|
|
_, err = nopClt.UpsertNode(ctx, node)
|
|
require.True(t, trace.IsAccessDenied(err))
|
|
|
|
k2, err := nopClt.NewKeepAliver(ctx)
|
|
require.NoError(t, err)
|
|
|
|
keepAlive.Expires = time.Now().Add(2 * time.Second)
|
|
go func() {
|
|
select {
|
|
case k2.KeepAlives() <- *keepAlive:
|
|
case <-k2.Done():
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("time out expecting error")
|
|
case <-k2.Done():
|
|
}
|
|
|
|
require.True(t, trace.IsAccessDenied(k2.Error()))
|
|
}
|
|
|
|
// ExpectResource expects a Put event of a certain resource
|
|
func ExpectResource(t *testing.T, w types.Watcher, timeout time.Duration, resource types.Resource) {
|
|
timeoutC := time.After(timeout)
|
|
waitLoop:
|
|
for {
|
|
select {
|
|
case <-timeoutC:
|
|
t.Fatalf("Timeout waiting for event")
|
|
case <-w.Done():
|
|
t.Fatalf("Watcher exited with error %v", w.Error())
|
|
case event := <-w.Events():
|
|
if event.Type != types.OpPut {
|
|
continue
|
|
}
|
|
if resource.GetName() != event.Resource.GetName() || resource.GetKind() != event.Resource.GetKind() || resource.GetSubKind() != event.Resource.GetSubKind() {
|
|
continue waitLoop
|
|
}
|
|
require.Empty(t, cmp.Diff(resource, event.Resource))
|
|
break waitLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExpectDeleteResource expects a delete event of a certain kind
|
|
func ExpectDeleteResource(t *testing.T, w types.Watcher, timeout time.Duration, resource types.Resource) {
|
|
timeoutC := time.After(timeout)
|
|
waitLoop:
|
|
for {
|
|
select {
|
|
case <-timeoutC:
|
|
t.Fatalf("Timeout waiting for delete resource %v", resource)
|
|
case <-w.Done():
|
|
t.Fatalf("Watcher exited with error %v", w.Error())
|
|
case event := <-w.Events():
|
|
if event.Type != types.OpDelete {
|
|
continue
|
|
}
|
|
|
|
// Server resources may have subkind set, but the backend
|
|
// generating this delete event doesn't know the subkind.
|
|
// Set it to prevent the check below from failing.
|
|
if event.Resource.GetKind() == types.KindNode {
|
|
event.Resource.SetSubKind(resource.GetSubKind())
|
|
}
|
|
|
|
require.Empty(t, cmp.Diff(resource, event.Resource))
|
|
break waitLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEventsPermissions tests events with regards
|
|
// to certificate authority rotation
|
|
func TestEventsPermissions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleNode))
|
|
require.NoError(t, err)
|
|
defer clt.Close()
|
|
|
|
w, err := clt.NewWatcher(ctx, types.Watch{Kinds: []types.WatchKind{{Kind: types.KindCertAuthority}}})
|
|
require.NoError(t, err)
|
|
defer w.Close()
|
|
|
|
select {
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Timeout waiting for init event")
|
|
case event := <-w.Events():
|
|
require.Equal(t, types.OpInit, event.Type)
|
|
}
|
|
|
|
// start rotation
|
|
gracePeriod := time.Hour
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ca, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, false)
|
|
require.NoError(t, err)
|
|
|
|
ExpectResource(t, w, 3*time.Second, ca)
|
|
|
|
type testCase struct {
|
|
name string
|
|
identity authtest.TestIdentity
|
|
watches []types.WatchKind
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
name: "node role is not authorized to get certificate authority with secret data loaded",
|
|
identity: authtest.TestBuiltin(types.RoleNode),
|
|
watches: []types.WatchKind{{Kind: types.KindCertAuthority, LoadSecrets: true}},
|
|
},
|
|
{
|
|
name: "node role is not authorized to watch static tokens",
|
|
identity: authtest.TestBuiltin(types.RoleNode),
|
|
watches: []types.WatchKind{{Kind: types.KindStaticTokens}},
|
|
},
|
|
{
|
|
name: "node role is not authorized to watch provisioning tokens",
|
|
identity: authtest.TestBuiltin(types.RoleNode),
|
|
watches: []types.WatchKind{{Kind: types.KindToken}},
|
|
},
|
|
{
|
|
name: "nop role is not authorized to watch users and roles",
|
|
identity: authtest.TestUnauthenticated(types.RoleNop),
|
|
watches: []types.WatchKind{
|
|
{Kind: types.KindUser},
|
|
{Kind: types.KindRole},
|
|
},
|
|
},
|
|
{
|
|
name: "nop role is not authorized to watch cert authorities",
|
|
identity: authtest.TestUnauthenticated(types.RoleNop),
|
|
watches: []types.WatchKind{{Kind: types.KindCertAuthority, LoadSecrets: false}},
|
|
},
|
|
{
|
|
name: "nop role is not authorized to watch cluster config resources",
|
|
identity: authtest.TestUnauthenticated(types.RoleNop),
|
|
watches: []types.WatchKind{
|
|
{Kind: types.KindClusterAuthPreference},
|
|
{Kind: types.KindClusterNetworkingConfig},
|
|
{Kind: types.KindSessionRecordingConfig},
|
|
},
|
|
},
|
|
}
|
|
|
|
tryWatch := func(tc testCase) {
|
|
client, err := testSrv.NewClient(tc.identity)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
watcher, err := client.NewWatcher(ctx, types.Watch{
|
|
Kinds: tc.watches,
|
|
})
|
|
require.NoError(t, err)
|
|
defer watcher.Close()
|
|
|
|
go func() {
|
|
select {
|
|
case <-watcher.Events():
|
|
case <-watcher.Done():
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("time out expecting error in test %q", tc.name)
|
|
case <-watcher.Done():
|
|
}
|
|
|
|
require.True(t, trace.IsAccessDenied(watcher.Error()))
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tryWatch(tc)
|
|
}
|
|
}
|
|
|
|
// TestEventsPermissionsPartialSuccess verifies that in partial success mode NewWatcher can still succeed
|
|
// if caller lacks permission to watch only some of the requested resource kinds.
|
|
func TestEventsPermissionsPartialSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
watch types.Watch
|
|
expectedConfirmedKinds []types.WatchKind
|
|
}{
|
|
{
|
|
name: "no permission for any of the requested kinds",
|
|
watch: types.Watch{
|
|
Kinds: []types.WatchKind{
|
|
{Kind: types.KindUser},
|
|
{Kind: types.KindRole},
|
|
},
|
|
AllowPartialSuccess: true,
|
|
},
|
|
},
|
|
{
|
|
name: "has permission only for some of the requested kinds",
|
|
watch: types.Watch{
|
|
Kinds: []types.WatchKind{
|
|
{Kind: types.KindUser},
|
|
{Kind: types.KindRole},
|
|
{Kind: types.KindStaticTokens},
|
|
},
|
|
AllowPartialSuccess: true,
|
|
},
|
|
expectedConfirmedKinds: []types.WatchKind{
|
|
{Kind: types.KindStaticTokens, ScopeFilter: types.ScopeFilterFromProto(scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSCOPED}.Build())},
|
|
},
|
|
},
|
|
{
|
|
name: "has permission only for some kinds but partial success is not enabled",
|
|
watch: types.Watch{
|
|
Kinds: []types.WatchKind{
|
|
{Kind: types.KindUser},
|
|
{Kind: types.KindRole},
|
|
{Kind: types.KindStaticTokens},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
testUser, testRole, err := authtest.CreateUserAndRole(testSrv.Auth(), "test", nil, []types.Rule{
|
|
types.NewRule(types.KindStaticTokens, services.RO()),
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = testSrv.Auth().UpsertRole(ctx, testRole)
|
|
require.NoError(t, err)
|
|
testIdentity := authtest.TestUser(testUser.GetName())
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
client, err := testSrv.NewClient(testIdentity)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
w, err := client.NewWatcher(ctx, tc.watch)
|
|
require.NoError(t, err)
|
|
defer w.Close()
|
|
|
|
select {
|
|
case event := <-w.Events():
|
|
if len(tc.expectedConfirmedKinds) > 0 {
|
|
require.Equal(t, types.OpInit, event.Type)
|
|
watchStatus, ok := event.Resource.(types.WatchStatus)
|
|
require.True(t, ok)
|
|
require.Equal(t, tc.expectedConfirmedKinds, watchStatus.GetKinds())
|
|
} else {
|
|
t.Fatal("unexpected event from watcher that is supposed to fail")
|
|
}
|
|
case <-w.Done():
|
|
if len(tc.expectedConfirmedKinds) > 0 {
|
|
t.Fatalf("Watcher exited with error %v", w.Error())
|
|
}
|
|
require.Error(t, w.Error())
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timeout waiting for watcher")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEvents tests events suite
|
|
func TestEvents(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
testCases := []eventTest{
|
|
{
|
|
name: "Cert authority with secrets",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindCertAuthority,
|
|
LoadSecrets: true,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
ca, err := authcatest.NewCA(types.UserCA, "example.com")
|
|
require.NoError(t, err)
|
|
require.NoError(t, testSrv.Auth().UpsertCertAuthority(ctx, ca))
|
|
|
|
out, err := testSrv.Auth().GetCertAuthority(ctx, *ca.ID(), true)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, testSrv.Auth().DeleteCertAuthority(ctx, *ca.ID()))
|
|
return out
|
|
},
|
|
},
|
|
}
|
|
runEventsTests(t, testCases, clt, types.Watch{Kinds: eventsTestKinds(testCases)})
|
|
|
|
testCases = []eventTest{
|
|
{
|
|
name: "Cert authority without secrets",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindCertAuthority,
|
|
LoadSecrets: false,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
ca, err := authcatest.NewCA(types.UserCA, "example.com")
|
|
require.NoError(t, err)
|
|
require.NoError(t, testSrv.Auth().UpsertCertAuthority(ctx, ca))
|
|
|
|
out, err := testSrv.Auth().GetCertAuthority(ctx, *ca.ID(), false)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, testSrv.Auth().DeleteCertAuthority(ctx, *ca.ID()))
|
|
return out
|
|
},
|
|
},
|
|
}
|
|
runEventsTests(t, testCases, clt, types.Watch{Kinds: eventsTestKinds(testCases)})
|
|
|
|
testCases = []eventTest{
|
|
{
|
|
name: "Token",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindToken,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
expires := time.Now().UTC().Add(time.Hour)
|
|
tok, err := types.NewProvisionToken("token",
|
|
types.SystemRoles{types.RoleAuth, types.RoleNode}, expires)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, clt.UpsertToken(ctx, tok))
|
|
|
|
token, err := clt.GetToken(ctx, "token")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, clt.DeleteToken(ctx, "token"))
|
|
return token
|
|
},
|
|
},
|
|
{
|
|
name: "Static tokens",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindStaticTokens,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{
|
|
{
|
|
Token: "tok1",
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
Expires: time.Now().UTC().Add(time.Hour),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().SetStaticTokens(staticTokens)
|
|
require.NoError(t, err)
|
|
|
|
out, err := testSrv.Auth().GetStaticTokens(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().DeleteStaticTokens()
|
|
require.NoError(t, err)
|
|
|
|
return out
|
|
},
|
|
},
|
|
{
|
|
name: "Role",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindRole,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
role, err := types.NewRole("role1", types.RoleSpecV6{
|
|
Options: types.RoleOptions{
|
|
MaxSessionTTL: types.Duration(time.Hour),
|
|
},
|
|
Allow: types.RoleConditions{
|
|
Logins: []string{"root", "bob"},
|
|
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
|
},
|
|
Deny: types.RoleConditions{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = clt.UpsertRole(ctx, role)
|
|
require.NoError(t, err)
|
|
|
|
out, err := clt.GetRole(ctx, role.GetName())
|
|
require.NoError(t, err)
|
|
|
|
err = clt.DeleteRole(ctx, role.GetName())
|
|
require.NoError(t, err)
|
|
|
|
return out
|
|
},
|
|
},
|
|
{
|
|
name: "User",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindUser,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
var user types.User = &types.UserV2{
|
|
Kind: types.KindUser,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: "user1",
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.UserSpecV2{
|
|
Roles: []string{constants.DefaultImplicitRole},
|
|
},
|
|
}
|
|
|
|
user, err := clt.UpsertUser(ctx, user)
|
|
require.NoError(t, err)
|
|
|
|
out, err := clt.GetUser(ctx, user.GetName(), false)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, clt.DeleteUser(ctx, user.GetName()))
|
|
return out
|
|
},
|
|
},
|
|
{
|
|
name: "Node",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindNode,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
srv := &types.ServerV2{
|
|
Kind: types.KindNode,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: "srv1",
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{
|
|
Addr: "127.0.0.1:2022",
|
|
},
|
|
}
|
|
|
|
_, err := testSrv.Auth().UpsertNode(ctx, srv)
|
|
require.NoError(t, err)
|
|
|
|
out, err := testSrv.Auth().GetNodes(ctx, srv.Metadata.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().DeleteAllNodes(ctx, srv.Metadata.Namespace)
|
|
require.NoError(t, err)
|
|
|
|
return out[0]
|
|
},
|
|
},
|
|
{
|
|
name: "Proxy",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindProxy,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
srv := &types.ServerV2{
|
|
Kind: types.KindProxy,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: "srv1",
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ServerSpecV2{
|
|
Addr: "127.0.0.1:2022",
|
|
},
|
|
}
|
|
|
|
_, err := testSrv.Auth().UpsertProxyServer(ctx, srv)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:staticcheck // TODO(kiosion) DELETE IN 21.0.0
|
|
out, err := testSrv.Auth().GetProxies()
|
|
require.NoError(t, err)
|
|
|
|
for _, p := range out {
|
|
require.NoError(t, testSrv.Auth().DeleteProxyServer(ctx, p.GetName()))
|
|
}
|
|
|
|
return out[0]
|
|
},
|
|
},
|
|
{
|
|
name: "Tunnel connection",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindTunnelConnection,
|
|
},
|
|
crud: func(ctx context.Context) types.Resource {
|
|
conn, err := types.NewTunnelConnection("conn1", types.TunnelConnectionSpecV2{
|
|
ClusterName: "example.com",
|
|
ProxyName: "p1",
|
|
LastHeartbeat: time.Now().UTC(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().UpsertTunnelConnection(ctx, conn)
|
|
require.NoError(t, err)
|
|
|
|
out, err := testSrv.Auth().GetTunnelConnections(ctx, "example.com")
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().DeleteTunnelConnection(ctx, conn.GetClusterName(), conn.GetName())
|
|
require.NoError(t, err)
|
|
|
|
return out[0]
|
|
},
|
|
},
|
|
{
|
|
name: "Reverse tunnel",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindReverseTunnel,
|
|
},
|
|
crud: func(context.Context) types.Resource {
|
|
tunnel := &types.ReverseTunnelV2{
|
|
Kind: types.KindReverseTunnel,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: "example.com",
|
|
Namespace: apidefaults.Namespace,
|
|
},
|
|
Spec: types.ReverseTunnelSpecV2{
|
|
ClusterName: "example.com",
|
|
DialAddrs: []string{"example.com:2023"},
|
|
},
|
|
}
|
|
|
|
_, err := testSrv.Auth().UpsertReverseTunnel(ctx, tunnel)
|
|
require.NoError(t, err)
|
|
|
|
out, _, err := testSrv.Auth().ListReverseTunnels(
|
|
ctx, 0, "",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().DeleteReverseTunnel(ctx, tunnel.Spec.ClusterName)
|
|
require.NoError(t, err)
|
|
|
|
return out[0]
|
|
},
|
|
},
|
|
{
|
|
name: "Remote cluster",
|
|
kind: types.WatchKind{
|
|
Kind: types.KindRemoteCluster,
|
|
},
|
|
crud: func(ctx context.Context) types.Resource {
|
|
rc, err := types.NewRemoteCluster("example.com")
|
|
rc.SetConnectionStatus(teleport.RemoteClusterStatusOffline)
|
|
require.NoError(t, err)
|
|
_, err = testSrv.Auth().CreateRemoteCluster(ctx, rc)
|
|
require.NoError(t, err)
|
|
|
|
out, err := testSrv.Auth().GetRemoteClusters(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().DeleteRemoteCluster(ctx, rc.GetName())
|
|
require.NoError(t, err)
|
|
|
|
return out[0]
|
|
},
|
|
},
|
|
}
|
|
// this also tests the partial success mode by requesting an unknown kind
|
|
runEventsTests(t, testCases, clt, types.Watch{
|
|
Kinds: append(eventsTestKinds(testCases), types.WatchKind{Kind: "unknown"}),
|
|
AllowPartialSuccess: true,
|
|
})
|
|
|
|
// tests that a watch fails given an unknown kind when the partial success mode is not enabled
|
|
runUnknownEventsTest(t, clt, types.Watch{Kinds: []types.WatchKind{
|
|
{Kind: types.KindNamespace},
|
|
{Kind: "unknown"},
|
|
}})
|
|
|
|
// tests that a watch fails if all given kinds are unknown even if the success mode is enabled
|
|
runUnknownEventsTest(t, clt, types.Watch{
|
|
Kinds: []types.WatchKind{
|
|
{Kind: "unrecognized"},
|
|
{Kind: "unidentified"},
|
|
},
|
|
AllowPartialSuccess: true,
|
|
})
|
|
}
|
|
|
|
func runEventsTests(t *testing.T, testCases []eventTest, events types.Events, watch types.Watch) {
|
|
ctx := context.Background()
|
|
w, err := events.NewWatcher(ctx, watch)
|
|
require.NoError(t, err)
|
|
defer w.Close()
|
|
|
|
select {
|
|
case event := <-w.Events():
|
|
require.Equal(t, types.OpInit, event.Type)
|
|
watchStatus, ok := event.Resource.(types.WatchStatus)
|
|
require.True(t, ok)
|
|
expectedKinds := eventsTestConfirmedKinds(testCases)
|
|
require.Equal(t, expectedKinds, watchStatus.GetKinds())
|
|
case <-w.Done():
|
|
t.Fatalf("Watcher exited with error %v", w.Error())
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Timeout waiting for init event")
|
|
}
|
|
|
|
// filter out all events that could have been inserted
|
|
// by the initialization routines
|
|
skiploop:
|
|
for {
|
|
select {
|
|
case <-w.Events():
|
|
continue skiploop
|
|
default:
|
|
break skiploop
|
|
case <-w.Done():
|
|
t.Fatalf("Watcher exited with error %v", w.Error())
|
|
}
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Logf("test case %q", tc.name)
|
|
resource := tc.crud(ctx)
|
|
|
|
ExpectResource(t, w, 3*time.Second, resource)
|
|
|
|
meta := resource.GetMetadata()
|
|
header := &types.ResourceHeader{
|
|
Kind: resource.GetKind(),
|
|
SubKind: resource.GetSubKind(),
|
|
Version: resource.GetVersion(),
|
|
Metadata: types.Metadata{
|
|
Name: meta.Name,
|
|
Namespace: meta.Namespace,
|
|
},
|
|
}
|
|
ExpectDeleteResource(t, w, 3*time.Second, header)
|
|
}
|
|
}
|
|
|
|
func runUnknownEventsTest(t *testing.T, events types.Events, watch types.Watch) {
|
|
ctx := context.Background()
|
|
w, err := events.NewWatcher(ctx, watch)
|
|
if err != nil {
|
|
// depending on the implementation of EventsS, it might fail here immediately
|
|
// or later before returning the first event from the watcher.
|
|
return
|
|
}
|
|
defer w.Close()
|
|
|
|
select {
|
|
case <-w.Events():
|
|
t.Fatal("unexpected event from watcher that is supposed to fail")
|
|
case <-w.Done():
|
|
require.Error(t, w.Error())
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timeout waiting for error from watcher")
|
|
}
|
|
}
|
|
|
|
type eventTest struct {
|
|
name string
|
|
kind types.WatchKind
|
|
crud func(context.Context) types.Resource
|
|
}
|
|
|
|
func eventsTestKinds(tests []eventTest) []types.WatchKind {
|
|
out := make([]types.WatchKind, len(tests))
|
|
for i, tc := range tests {
|
|
out[i] = tc.kind
|
|
}
|
|
return out
|
|
}
|
|
|
|
// eventsTestConfirmedKinds returns the watch kinds as the server confirms them for an unscoped watcher:
|
|
// every kind's confirmed watch kind carries the caller's defaulted MODE_UNSCOPED scope filter.
|
|
func eventsTestConfirmedKinds(tests []eventTest) []types.WatchKind {
|
|
out := eventsTestKinds(tests)
|
|
for i := range out {
|
|
out[i].ScopeFilter = types.ScopeFilterFromProto(scopesv1.Filter_builder{Mode: scopesv1.Mode_MODE_UNSCOPED}.Build())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestWatchEvents_ScopedIdentity verifies that scoped identities can use the
|
|
// WatchEvents RPC to watch CertAuthorities without secrets (implicit permission),
|
|
// and that watching with secrets is correctly denied.
|
|
func TestWatchEvents_ScopedIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
srv := newTestTLSServer(t, withScopesFeatures(scopes.Features{Enabled: true}))
|
|
ctx := t.Context()
|
|
|
|
adminClient, err := srv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = adminClient.Close()
|
|
})
|
|
|
|
// Create a scoped role with an empty allow block (no permissions).
|
|
scopedSvc := adminClient.ScopedAccessServiceClient()
|
|
_, err = scopedSvc.CreateScopedRole(ctx, scopedaccessv1.CreateScopedRoleRequest_builder{
|
|
Role: scopedaccessv1.ScopedRole_builder{
|
|
Kind: scopedaccess.KindScopedRole,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: "empty-role",
|
|
}.Build(),
|
|
Scope: "/test",
|
|
Spec: scopedaccessv1.ScopedRoleSpec_builder{
|
|
AssignableScopes: []string{"/test/scope"},
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
user, err := authtest.CreateUser(ctx, srv.Auth(), "scoped-watcher")
|
|
require.NoError(t, err)
|
|
|
|
createResp, err := scopedSvc.CreateScopedRoleAssignment(ctx, scopedaccessv1.CreateScopedRoleAssignmentRequest_builder{
|
|
Assignment: scopedaccessv1.ScopedRoleAssignment_builder{
|
|
Kind: scopedaccess.KindScopedRoleAssignment,
|
|
SubKind: scopedaccess.SubKindDynamic,
|
|
Version: types.V1,
|
|
Metadata: headerv1.Metadata_builder{
|
|
Name: uuid.NewString(),
|
|
}.Build(),
|
|
Scope: "/test",
|
|
Spec: scopedaccessv1.ScopedRoleAssignmentSpec_builder{
|
|
User: user.GetName(),
|
|
Assignments: []*scopedaccessv1.Assignment{
|
|
scopedaccessv1.Assignment_builder{Role: "/test::empty-role", Scope: "/test/scope"}.Build(),
|
|
},
|
|
}.Build(),
|
|
}.Build(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
_, err := srv.AuthServer.AuthServer.ScopedAccessCache.GetScopedRoleAssignment(ctx, scopedaccessv1.GetScopedRoleAssignmentRequest_builder{
|
|
Name: createResp.GetAssignment().GetMetadata().GetName(),
|
|
SubKind: createResp.GetAssignment().GetSubKind(),
|
|
Scope: createResp.GetAssignment().GetScope(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
|
|
scopedClient, err := srv.NewClient(authtest.TestScopedUser(user.GetName(), "/test/scope"))
|
|
require.NoError(t, err)
|
|
defer scopedClient.Close()
|
|
|
|
t.Run("ca without secrets", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
watcher, err := scopedClient.NewWatcher(ctx, types.Watch{
|
|
Name: "ca-watch",
|
|
Kinds: []types.WatchKind{{
|
|
Kind: types.KindCertAuthority,
|
|
LoadSecrets: false,
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
defer watcher.Close()
|
|
|
|
select {
|
|
case e := <-watcher.Events():
|
|
require.Equal(t, types.OpInit, e.Type)
|
|
case <-watcher.Done():
|
|
t.Fatalf("watcher closed unexpectedly: %v", watcher.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("spiffe_federation", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
watcher, err := scopedClient.NewWatcher(ctx, types.Watch{
|
|
Name: "spiffe-federation-watch",
|
|
Kinds: []types.WatchKind{{
|
|
Kind: types.KindSPIFFEFederation,
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
defer watcher.Close()
|
|
|
|
select {
|
|
case e := <-watcher.Events():
|
|
require.Equal(t, types.OpInit, e.Type)
|
|
case <-watcher.Done():
|
|
t.Fatalf("watcher closed unexpectedly: %v", watcher.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("ca with secrets", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
watcher, err := scopedClient.NewWatcher(ctx, types.Watch{
|
|
Name: "ca-watch-secrets",
|
|
Kinds: []types.WatchKind{{
|
|
Kind: types.KindCertAuthority,
|
|
LoadSecrets: true,
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
defer watcher.Close()
|
|
|
|
select {
|
|
case <-watcher.Events():
|
|
t.Fatal("expected watcher to close with error, got event")
|
|
case <-watcher.Done():
|
|
require.True(t, trace.IsAccessDenied(watcher.Error()),
|
|
"expected access denied, got: %v", watcher.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("unauthorized kind", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
watcher, err := scopedClient.NewWatcher(ctx, types.Watch{
|
|
Name: "user-watch",
|
|
Kinds: []types.WatchKind{{
|
|
Kind: types.KindUser,
|
|
}},
|
|
})
|
|
require.NoError(t, err)
|
|
defer watcher.Close()
|
|
|
|
select {
|
|
case <-watcher.Events():
|
|
t.Fatal("expected watcher to close with error, got event")
|
|
case <-watcher.Done():
|
|
require.True(t, trace.IsAccessDenied(watcher.Error()),
|
|
"expected access denied, got: %v", watcher.Error())
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestEventsClusterConfig test cluster configuration
|
|
func TestEventsClusterConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestBuiltin(types.RoleAdmin))
|
|
require.NoError(t, err)
|
|
defer clt.Close()
|
|
|
|
w, err := clt.NewWatcher(ctx, types.Watch{Kinds: []types.WatchKind{
|
|
{Kind: types.KindCertAuthority, LoadSecrets: true},
|
|
{Kind: types.KindStaticTokens},
|
|
{Kind: types.KindToken},
|
|
{Kind: types.KindClusterAuditConfig},
|
|
{Kind: types.KindClusterName},
|
|
}})
|
|
require.NoError(t, err)
|
|
defer w.Close()
|
|
|
|
select {
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Timeout waiting for init event")
|
|
case event := <-w.Events():
|
|
require.Equal(t, types.OpInit, event.Type)
|
|
}
|
|
|
|
// start rotation
|
|
gracePeriod := time.Hour
|
|
err = testSrv.Auth().RotateCertAuthority(ctx, types.RotateRequest{
|
|
Type: types.HostCA,
|
|
GracePeriod: &gracePeriod,
|
|
TargetPhase: types.RotationPhaseInit,
|
|
Mode: types.RotationModeManual,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ca, err := testSrv.Auth().GetCertAuthority(ctx, types.CertAuthID{
|
|
DomainName: testSrv.ClusterName(),
|
|
Type: types.HostCA,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
|
|
ExpectResource(t, w, 3*time.Second, ca)
|
|
|
|
// set static tokens
|
|
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{
|
|
{
|
|
Token: "tok1",
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
Expires: time.Now().UTC().Add(time.Hour),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().SetStaticTokens(staticTokens)
|
|
require.NoError(t, err)
|
|
|
|
staticTokens, err = testSrv.Auth().GetStaticTokens(ctx)
|
|
require.NoError(t, err)
|
|
ExpectResource(t, w, 3*time.Second, staticTokens)
|
|
|
|
// create provision token and expect the update event
|
|
token, err := types.NewProvisionToken(
|
|
"tok2", types.SystemRoles{types.RoleProxy}, time.Now().UTC().Add(3*time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
err = testSrv.Auth().UpsertToken(ctx, token)
|
|
require.NoError(t, err)
|
|
|
|
token, err = testSrv.Auth().GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
|
|
ExpectResource(t, w, 3*time.Second, token)
|
|
|
|
// delete token and expect delete event
|
|
err = testSrv.Auth().DeleteToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
ExpectDeleteResource(t, w, 3*time.Second, &types.ResourceHeader{
|
|
Kind: types.KindToken,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Namespace: apidefaults.Namespace,
|
|
Name: token.GetName(),
|
|
},
|
|
})
|
|
|
|
// update audit config
|
|
auditConfig, err := types.NewClusterAuditConfig(types.ClusterAuditConfigSpecV2{
|
|
AuditEventsURI: []string{"dynamodb://audit_table_name", "file:///home/log"},
|
|
})
|
|
require.NoError(t, err)
|
|
err = testSrv.Auth().SetClusterAuditConfig(ctx, auditConfig)
|
|
require.NoError(t, err)
|
|
|
|
auditConfigResource, err := testSrv.Auth().GetClusterAuditConfig(ctx)
|
|
require.NoError(t, err)
|
|
ExpectResource(t, w, 3*time.Second, auditConfigResource)
|
|
|
|
// update cluster name resource metadata
|
|
clusterNameResource, err := testSrv.Auth().GetClusterName(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// update the resource with different labels to test the change
|
|
clusterName := &types.ClusterNameV2{
|
|
Kind: types.KindClusterName,
|
|
Version: types.V2,
|
|
Metadata: types.Metadata{
|
|
Name: types.MetaNameClusterName,
|
|
Namespace: apidefaults.Namespace,
|
|
Labels: map[string]string{
|
|
"key": "val",
|
|
},
|
|
},
|
|
Spec: clusterNameResource.(*types.ClusterNameV2).Spec,
|
|
}
|
|
|
|
err = testSrv.Auth().DeleteClusterName()
|
|
require.NoError(t, err)
|
|
err = testSrv.Auth().SetClusterName(clusterName)
|
|
require.NoError(t, err)
|
|
|
|
clusterNameResource, err = testSrv.Auth().GetClusterName(ctx)
|
|
require.NoError(t, err)
|
|
ExpectResource(t, w, 3*time.Second, clusterNameResource)
|
|
}
|
|
|
|
func TestNetworkRestrictions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
clt, err := testSrv.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
// blank slate, should be get/delete should fail
|
|
_, err = clt.GetNetworkRestrictions(ctx)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
err = clt.DeleteNetworkRestrictions(ctx)
|
|
require.True(t, trace.IsNotFound(err))
|
|
|
|
allow := []types.AddressCondition{
|
|
{CIDR: "10.0.1.0/24"},
|
|
{CIDR: "10.0.2.2"},
|
|
}
|
|
deny := []types.AddressCondition{
|
|
{CIDR: "10.1.0.0/16"},
|
|
{CIDR: "8.8.8.8"},
|
|
}
|
|
|
|
expected := types.NewNetworkRestrictions()
|
|
expected.SetAllow(allow)
|
|
expected.SetDeny(deny)
|
|
|
|
// set and make sure we get it back
|
|
err = clt.SetNetworkRestrictions(ctx, expected)
|
|
require.NoError(t, err)
|
|
|
|
actual, err := clt.GetNetworkRestrictions(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, cmp.Diff(expected.GetAllow(), actual.GetAllow()))
|
|
require.Empty(t, cmp.Diff(expected.GetDeny(), actual.GetDeny()))
|
|
|
|
// now delete should work ok and get should fail again
|
|
err = clt.DeleteNetworkRestrictions(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = clt.DeleteNetworkRestrictions(ctx)
|
|
require.True(t, trace.IsNotFound(err))
|
|
}
|
|
|
|
func mustNewToken(
|
|
t *testing.T,
|
|
token string,
|
|
roles types.SystemRoles,
|
|
expires time.Time,
|
|
) types.ProvisionToken {
|
|
tok, err := types.NewProvisionToken(token, roles, expires)
|
|
require.NoError(t, err)
|
|
return tok
|
|
}
|
|
|
|
func mustNewTokenFromSpec(
|
|
t *testing.T,
|
|
token string,
|
|
expires time.Time,
|
|
spec types.ProvisionTokenSpecV2,
|
|
) types.ProvisionToken {
|
|
tok, err := types.NewProvisionTokenFromSpec(token, expires, spec)
|
|
require.NoError(t, err)
|
|
return tok
|
|
}
|
|
|
|
func requireAccessDenied(t require.TestingT, err error, i ...any) {
|
|
require.True(
|
|
t,
|
|
trace.IsAccessDenied(err),
|
|
"err should be access denied, was: %s", err,
|
|
)
|
|
}
|
|
|
|
func requireBadParameter(t require.TestingT, err error, i ...any) {
|
|
require.True(
|
|
t,
|
|
trace.IsBadParameter(err),
|
|
"err should be bad parameter, was: %s", err,
|
|
)
|
|
}
|
|
|
|
func requireNotFound(t require.TestingT, err error, i ...any) {
|
|
require.True(
|
|
t,
|
|
trace.IsNotFound(err),
|
|
"err should be not found, was: %s", err,
|
|
)
|
|
}
|
|
|
|
func TestGRPCServer_CreateTokenV2(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// Inject mockEmitter to capture audit event for trusted cluster
|
|
// creation.
|
|
mockEmitter := &eventstest.MockRecorderEmitter{}
|
|
testSrv.Auth().SetEmitter(mockEmitter)
|
|
|
|
// Create a user with the least privilege access to call this RPC.
|
|
privilegedUser, _, err := authtest.CreateUserAndRole(
|
|
testSrv.Auth(), "token-creator", nil, []types.Rule{
|
|
{
|
|
Resources: []string{types.KindToken},
|
|
Verbs: []string{types.VerbCreate},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// create a token to conflict with for already having been created
|
|
alreadyExistsToken := mustNewToken(
|
|
t, "already-exists", types.SystemRoles{types.RoleNode}, time.Time{},
|
|
)
|
|
require.NoError(t, testSrv.Auth().CreateToken(ctx, alreadyExistsToken))
|
|
|
|
tests := []struct {
|
|
name string
|
|
identity authtest.TestIdentity
|
|
token types.ProvisionToken
|
|
|
|
requireTokenCreated bool
|
|
requireError require.ErrorAssertionFunc
|
|
auditEvents []eventtypes.AuditEvent
|
|
}{
|
|
{
|
|
name: "success",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: mustNewTokenFromSpec(
|
|
t,
|
|
"success",
|
|
time.Time{},
|
|
types.ProvisionTokenSpecV2{
|
|
Roles: types.SystemRoles{types.RoleNode, types.RoleKube},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
),
|
|
requireError: require.NoError,
|
|
requireTokenCreated: true,
|
|
auditEvents: []eventtypes.AuditEvent{
|
|
&eventtypes.ProvisionTokenCreate{
|
|
Metadata: eventtypes.Metadata{
|
|
Type: events.ProvisionTokenCreateEvent,
|
|
Code: events.ProvisionTokenCreateCode,
|
|
},
|
|
ResourceMetadata: eventtypes.ResourceMetadata{
|
|
Name: "*******",
|
|
UpdatedBy: "token-creator",
|
|
},
|
|
UserMetadata: eventtypes.UserMetadata{
|
|
User: "token-creator",
|
|
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
|
|
UserRoles: []string{"user:token-creator"},
|
|
UserClusterName: "localhost",
|
|
},
|
|
Roles: types.SystemRoles{types.RoleNode, types.RoleKube},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "success (trusted cluster)",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: mustNewTokenFromSpec(
|
|
t,
|
|
"success-trusted-cluster",
|
|
time.Time{},
|
|
types.ProvisionTokenSpecV2{
|
|
Roles: types.SystemRoles{types.RoleTrustedCluster},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
),
|
|
requireError: require.NoError,
|
|
requireTokenCreated: true,
|
|
auditEvents: []eventtypes.AuditEvent{
|
|
&eventtypes.ProvisionTokenCreate{
|
|
Metadata: eventtypes.Metadata{
|
|
Type: events.ProvisionTokenCreateEvent,
|
|
Code: events.ProvisionTokenCreateCode,
|
|
},
|
|
ResourceMetadata: eventtypes.ResourceMetadata{
|
|
Name: "*****************luster",
|
|
UpdatedBy: "token-creator",
|
|
},
|
|
UserMetadata: eventtypes.UserMetadata{
|
|
User: "token-creator",
|
|
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
|
|
UserRoles: []string{"user:token-creator"},
|
|
UserClusterName: "localhost",
|
|
},
|
|
Roles: types.SystemRoles{types.RoleTrustedCluster},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "access denied",
|
|
identity: authtest.TestNop(),
|
|
token: mustNewToken(
|
|
t, "access denied", types.SystemRoles{types.RoleNode}, time.Time{},
|
|
),
|
|
requireError: requireAccessDenied,
|
|
},
|
|
{
|
|
name: "already exists",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: alreadyExistsToken,
|
|
requireError: func(t require.TestingT, err error, i ...any) {
|
|
require.True(
|
|
t,
|
|
trace.IsAlreadyExists(err),
|
|
"err should be already exists, was: %s", err,
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: &types.ProvisionTokenV2{},
|
|
requireError: requireBadParameter,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := testSrv.NewClient(tt.identity)
|
|
require.NoError(t, err)
|
|
|
|
mockEmitter.Reset()
|
|
err = client.CreateToken(ctx, tt.token)
|
|
tt.requireError(t, err)
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
tt.auditEvents,
|
|
mockEmitter.Events(),
|
|
cmpopts.IgnoreFields(eventtypes.Metadata{}, "Time"),
|
|
cmpopts.IgnoreFields(eventtypes.ResourceMetadata{}, "Expires"),
|
|
cmpopts.EquateEmpty(),
|
|
))
|
|
if tt.requireTokenCreated {
|
|
token, err := testSrv.Auth().GetToken(ctx, tt.token.GetName())
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(
|
|
tt.token,
|
|
token,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGRPCServer_UpsertTokenV2(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// Inject mockEmitter to capture audit event for trusted cluster
|
|
// creation.
|
|
mockEmitter := &eventstest.MockRecorderEmitter{}
|
|
testSrv.Auth().SetEmitter(mockEmitter)
|
|
|
|
// Create a user with the least privilege access to call this RPC.
|
|
privilegedUser, _, err := authtest.CreateUserAndRole(
|
|
testSrv.Auth(), "token-upserter", nil, []types.Rule{
|
|
{
|
|
Resources: []string{types.KindToken},
|
|
Verbs: []string{types.VerbCreate, types.VerbUpdate},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// create a token to ensure UpsertToken overwrites an existing token.
|
|
alreadyExistsToken := mustNewToken(
|
|
t, "already-exists", types.SystemRoles{types.RoleNode}, time.Time{},
|
|
)
|
|
require.NoError(t, testSrv.Auth().CreateToken(ctx, alreadyExistsToken))
|
|
|
|
tests := []struct {
|
|
name string
|
|
identity authtest.TestIdentity
|
|
token types.ProvisionToken
|
|
|
|
requireTokenCreated bool
|
|
requireError require.ErrorAssertionFunc
|
|
auditEvents []eventtypes.AuditEvent
|
|
}{
|
|
{
|
|
name: "success",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: mustNewTokenFromSpec(
|
|
t,
|
|
"success",
|
|
time.Time{},
|
|
types.ProvisionTokenSpecV2{
|
|
Roles: types.SystemRoles{types.RoleNode, types.RoleKube},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
),
|
|
requireError: require.NoError,
|
|
requireTokenCreated: true,
|
|
auditEvents: []eventtypes.AuditEvent{
|
|
&eventtypes.ProvisionTokenCreate{
|
|
Metadata: eventtypes.Metadata{
|
|
Type: events.ProvisionTokenCreateEvent,
|
|
Code: events.ProvisionTokenCreateCode,
|
|
},
|
|
ResourceMetadata: eventtypes.ResourceMetadata{
|
|
Name: "*******",
|
|
UpdatedBy: "token-upserter",
|
|
},
|
|
UserMetadata: eventtypes.UserMetadata{
|
|
User: "token-upserter",
|
|
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
|
|
UserRoles: []string{"user:token-upserter"},
|
|
UserClusterName: "localhost",
|
|
},
|
|
Roles: types.SystemRoles{types.RoleNode, types.RoleKube},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "success (trusted cluster)",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: mustNewTokenFromSpec(
|
|
t,
|
|
"success-trusted-cluster",
|
|
time.Time{},
|
|
types.ProvisionTokenSpecV2{
|
|
Roles: types.SystemRoles{types.RoleTrustedCluster},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
),
|
|
requireError: require.NoError,
|
|
requireTokenCreated: true,
|
|
auditEvents: []eventtypes.AuditEvent{
|
|
&eventtypes.ProvisionTokenCreate{
|
|
Metadata: eventtypes.Metadata{
|
|
Type: events.ProvisionTokenCreateEvent,
|
|
Code: events.ProvisionTokenCreateCode,
|
|
},
|
|
ResourceMetadata: eventtypes.ResourceMetadata{
|
|
Name: "*****************luster",
|
|
UpdatedBy: "token-upserter",
|
|
},
|
|
UserMetadata: eventtypes.UserMetadata{
|
|
User: "token-upserter",
|
|
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
|
|
UserRoles: []string{"user:token-upserter"},
|
|
UserClusterName: "localhost",
|
|
},
|
|
Roles: types.SystemRoles{types.RoleTrustedCluster},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing token replaced",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: mustNewTokenFromSpec(
|
|
t,
|
|
alreadyExistsToken.GetName(),
|
|
time.Time{},
|
|
types.ProvisionTokenSpecV2{
|
|
// These roles differ from the roles on the already existing
|
|
// token.
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
),
|
|
requireTokenCreated: true,
|
|
requireError: require.NoError,
|
|
auditEvents: []eventtypes.AuditEvent{
|
|
&eventtypes.ProvisionTokenCreate{
|
|
Metadata: eventtypes.Metadata{
|
|
Type: events.ProvisionTokenCreateEvent,
|
|
Code: events.ProvisionTokenCreateCode,
|
|
},
|
|
ResourceMetadata: eventtypes.ResourceMetadata{
|
|
Name: "**************",
|
|
UpdatedBy: "token-upserter",
|
|
},
|
|
UserMetadata: eventtypes.UserMetadata{
|
|
User: "token-upserter",
|
|
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
|
|
UserRoles: []string{"user:token-upserter"},
|
|
UserClusterName: "localhost",
|
|
},
|
|
Roles: types.SystemRoles{types.RoleNode},
|
|
JoinMethod: types.JoinMethodToken,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "access denied",
|
|
identity: authtest.TestNop(),
|
|
token: mustNewToken(
|
|
t, "access denied", types.SystemRoles{types.RoleNode}, time.Time{},
|
|
),
|
|
requireError: requireAccessDenied,
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
token: &types.ProvisionTokenV2{},
|
|
requireError: requireBadParameter,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := testSrv.NewClient(tt.identity)
|
|
require.NoError(t, err)
|
|
|
|
mockEmitter.Reset()
|
|
err = client.UpsertToken(ctx, tt.token)
|
|
tt.requireError(t, err)
|
|
|
|
require.Empty(t, cmp.Diff(
|
|
tt.auditEvents,
|
|
mockEmitter.Events(),
|
|
cmpopts.IgnoreFields(eventtypes.Metadata{}, "Time"),
|
|
cmpopts.IgnoreFields(eventtypes.ResourceMetadata{}, "Expires"),
|
|
cmpopts.EquateEmpty(),
|
|
))
|
|
if tt.requireTokenCreated {
|
|
token, err := testSrv.Auth().GetToken(ctx, tt.token.GetName())
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(
|
|
tt.token,
|
|
token,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGRPCServer_GetTokens(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// Create a user with the least privilege access to call this RPC.
|
|
privilegedUser, _, err := authtest.CreateUserAndRole(
|
|
testSrv.Auth(), "token-reader", nil, []types.Rule{
|
|
{
|
|
Resources: []string{types.KindToken},
|
|
Verbs: []string{types.VerbRead, types.VerbList},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// The deprecated GetTokens RPC no longer has an api client wrapper, so
|
|
// exercise the gRPC stub directly. DELETE IN 21.0.0 alongside the RPC.
|
|
getTokens := func(t *testing.T, identity authtest.TestIdentity) ([]types.ProvisionToken, error) {
|
|
clt, err := testSrv.NewClient(identity)
|
|
require.NoError(t, err)
|
|
resp, err := proto.NewAuthServiceClient(clt.GetConnection()).GetTokens(t.Context(), &emptypb.Empty{}) //nolint:staticcheck // SA1019. Deprecated RPC, kept until v21.
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tokens := make([]types.ProvisionToken, len(resp.ProvisionTokens))
|
|
for i, tok := range resp.ProvisionTokens {
|
|
tokens[i] = tok
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
t.Run("no extra tokens", func(t *testing.T) {
|
|
toks, err := getTokens(t, authtest.TestUser(privilegedUser.GetName()))
|
|
require.NoError(t, err)
|
|
require.Len(t, toks, 1) // only a single static token exists
|
|
})
|
|
|
|
// Create tokens to then assert are returned
|
|
pt := mustNewToken(
|
|
t,
|
|
"example-token",
|
|
types.SystemRoles{types.RoleNode},
|
|
time.Time{},
|
|
)
|
|
require.NoError(t, testSrv.Auth().CreateToken(ctx, pt))
|
|
pt2 := mustNewToken(
|
|
t,
|
|
"example-token-2",
|
|
types.SystemRoles{types.RoleNode},
|
|
time.Time{},
|
|
)
|
|
require.NoError(t, testSrv.Auth().CreateToken(ctx, pt2))
|
|
st, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{
|
|
{
|
|
Roles: types.SystemRoles{types.RoleProxy},
|
|
Token: "example-static-token",
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, testSrv.Auth().SetStaticTokens(st))
|
|
expectTokens := append([]types.ProvisionToken{pt, pt2}, st.GetStaticTokens()...)
|
|
|
|
tests := []struct {
|
|
name string
|
|
identity authtest.TestIdentity
|
|
|
|
requireResponse bool
|
|
requireError require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "success",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
requireError: require.NoError,
|
|
requireResponse: true,
|
|
},
|
|
{
|
|
name: "access denied",
|
|
identity: authtest.TestNop(),
|
|
requireError: requireAccessDenied,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tokens, err := getTokens(t, tt.identity)
|
|
tt.requireError(t, err)
|
|
|
|
if tt.requireResponse {
|
|
require.Empty(t, cmp.Diff(
|
|
expectTokens,
|
|
tokens,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
))
|
|
} else {
|
|
require.Empty(t, tokens)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGRPCServer_GetToken(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// Create a user with the least privilege access to call this RPC.
|
|
privilegedUser, _, err := authtest.CreateUserAndRole(
|
|
testSrv.Auth(), "token-reader", nil, []types.Rule{
|
|
{
|
|
Resources: []string{types.KindToken},
|
|
Verbs: []string{types.VerbRead},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Create Provision token
|
|
pt := mustNewToken(t, "example-token", types.SystemRoles{types.RoleNode}, time.Time{})
|
|
require.NoError(t, testSrv.Auth().CreateToken(ctx, pt))
|
|
|
|
tests := []struct {
|
|
name string
|
|
tokenName string
|
|
identity authtest.TestIdentity
|
|
|
|
requireResponse bool
|
|
requireError require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "success",
|
|
tokenName: pt.GetName(),
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
requireError: require.NoError,
|
|
requireResponse: true,
|
|
},
|
|
{
|
|
name: "access denied",
|
|
identity: authtest.TestNop(),
|
|
tokenName: pt.GetName(),
|
|
requireError: requireAccessDenied,
|
|
},
|
|
{
|
|
name: "not found",
|
|
tokenName: "does-not-exist",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
requireError: requireNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := testSrv.NewClient(tt.identity)
|
|
require.NoError(t, err)
|
|
|
|
token, err := client.GetToken(ctx, tt.tokenName)
|
|
tt.requireError(t, err)
|
|
|
|
if tt.requireResponse {
|
|
require.Empty(t, cmp.Diff(
|
|
token,
|
|
pt,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
|
))
|
|
} else {
|
|
require.Nil(t, token)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGRPCServer_DeleteToken(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
testSrv := newTestTLSServer(t)
|
|
|
|
// Create a user with the least privilege access to call this RPC.
|
|
privilegedUser, _, err := authtest.CreateUserAndRole(
|
|
testSrv.Auth(), "token-deleter", nil, []types.Rule{
|
|
{
|
|
Resources: []string{types.KindToken},
|
|
Verbs: []string{types.VerbDelete},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Create Provision token
|
|
pt := mustNewToken(t, "example-token", types.SystemRoles{types.RoleNode}, time.Time{})
|
|
require.NoError(t, testSrv.Auth().CreateToken(ctx, pt))
|
|
|
|
tests := []struct {
|
|
name string
|
|
tokenName string
|
|
identity authtest.TestIdentity
|
|
|
|
requireTokenDeleted bool
|
|
requireError require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "success",
|
|
tokenName: pt.GetName(),
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
requireError: require.NoError,
|
|
requireTokenDeleted: true,
|
|
},
|
|
{
|
|
name: "access denied",
|
|
identity: authtest.TestNop(),
|
|
tokenName: pt.GetName(),
|
|
requireError: requireAccessDenied,
|
|
},
|
|
{
|
|
name: "not found",
|
|
tokenName: "does-not-exist",
|
|
identity: authtest.TestUser(privilegedUser.GetName()),
|
|
requireError: requireNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := testSrv.NewClient(tt.identity)
|
|
require.NoError(t, err)
|
|
|
|
err = client.DeleteToken(ctx, tt.tokenName)
|
|
tt.requireError(t, err)
|
|
|
|
if tt.requireTokenDeleted {
|
|
_, err := testSrv.Auth().GetToken(ctx, tt.tokenName)
|
|
require.True(
|
|
t,
|
|
trace.IsNotFound(err),
|
|
"expected token to be deleted",
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// verifyJWT verifies that the token was signed by one the passed in key pair.
|
|
func verifyJWT(clock clockwork.Clock, clusterName string, pairs []*types.JWTKeyPair, token string) (*jwt.Claims, error) {
|
|
errs := []error{}
|
|
for _, pair := range pairs {
|
|
publicKey, err := keys.ParsePublicKey(pair.PublicKey)
|
|
if err != nil {
|
|
errs = append(errs, trace.Wrap(err))
|
|
continue
|
|
}
|
|
|
|
key, err := jwt.New(&jwt.Config{
|
|
Clock: clock,
|
|
PublicKey: publicKey,
|
|
ClusterName: clusterName,
|
|
})
|
|
if err != nil {
|
|
errs = append(errs, trace.Wrap(err))
|
|
continue
|
|
}
|
|
claims, err := key.Verify(jwt.VerifyParams{
|
|
RawToken: token,
|
|
Username: "foo",
|
|
URI: "https://localhost:8080",
|
|
})
|
|
if err != nil {
|
|
errs = append(errs, trace.Wrap(err))
|
|
continue
|
|
}
|
|
return claims, nil
|
|
}
|
|
return nil, trace.NewAggregate(errs...)
|
|
}
|
|
|
|
// verifyJWTAWSOIDC verifies that the token was signed by one the passed in key pair.
|
|
func verifyJWTAWSOIDC(clock clockwork.Clock, clusterName string, pairs []*types.JWTKeyPair, token, issuer string) (*jwt.Claims, error) {
|
|
errs := []error{}
|
|
for _, pair := range pairs {
|
|
publicKey, err := keys.ParsePublicKey(pair.PublicKey)
|
|
if err != nil {
|
|
errs = append(errs, trace.Wrap(err))
|
|
continue
|
|
}
|
|
|
|
key, err := jwt.New(&jwt.Config{
|
|
Clock: clock,
|
|
PublicKey: publicKey,
|
|
ClusterName: clusterName,
|
|
})
|
|
if err != nil {
|
|
errs = append(errs, trace.Wrap(err))
|
|
continue
|
|
}
|
|
claims, err := key.VerifyAWSOIDC(jwt.AWSOIDCVerifyParams{
|
|
RawToken: token,
|
|
Issuer: issuer,
|
|
})
|
|
if err != nil {
|
|
errs = append(errs, trace.Wrap(err))
|
|
continue
|
|
}
|
|
return claims, nil
|
|
}
|
|
return nil, trace.NewAggregate(errs...)
|
|
}
|
|
|
|
type testTLSServerOptions struct {
|
|
cacheEnabled bool
|
|
accessGraph *auth.AccessGraphConfig
|
|
clock clockwork.Clock
|
|
bufconnListener bool
|
|
modules *modulestest.Modules
|
|
scopesFeatures scopes.Features
|
|
emitter eventtypes.Emitter
|
|
}
|
|
|
|
type testTLSServerOption func(*testTLSServerOptions)
|
|
|
|
func withCacheEnabled(enabled bool) testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.cacheEnabled = enabled
|
|
}
|
|
}
|
|
|
|
func withAccessGraphConfig(cfg auth.AccessGraphConfig) testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.accessGraph = &cfg
|
|
}
|
|
}
|
|
|
|
func withClock(clock clockwork.Clock) testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.clock = clock
|
|
}
|
|
}
|
|
|
|
func withBufconnListener() testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.bufconnListener = true
|
|
}
|
|
}
|
|
|
|
func withModules(mod *modulestest.Modules) testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.modules = mod
|
|
}
|
|
}
|
|
|
|
func withScopesFeatures(scopesFeatures scopes.Features) testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.scopesFeatures = scopesFeatures
|
|
}
|
|
}
|
|
|
|
// withEmitter replaces the audit event emitter before the server starts
|
|
// serving, which is the only time it is safe to do so.
|
|
func withEmitter(emitter eventtypes.Emitter) testTLSServerOption {
|
|
return func(options *testTLSServerOptions) {
|
|
options.emitter = emitter
|
|
}
|
|
}
|
|
|
|
// newTestTLSServer is a helper that returns a *authtest.TLSServer with sensible
|
|
// defaults for most tests that are exercising Auth Service RPCs. For more advanced
|
|
// use-cases, NewTestTLSServer to provide a more detailed configuration.
|
|
//
|
|
// Prefer using authtest.AuthServer directly if Auth Service RPCs are never used.
|
|
func newTestTLSServer(t testing.TB, opts ...testTLSServerOption) *authtest.TLSServer {
|
|
var options testTLSServerOptions
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
if options.clock == nil {
|
|
options.clock = clockwork.NewFakeClockAt(time.Now().Round(time.Second).UTC())
|
|
}
|
|
if options.modules == nil {
|
|
options.modules = modulestest.OSSModules()
|
|
}
|
|
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
Clock: options.clock,
|
|
CacheEnabled: options.cacheEnabled,
|
|
Modules: options.modules,
|
|
ScopesFeatures: options.scopesFeatures,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, as.Close()) })
|
|
|
|
if options.emitter != nil {
|
|
as.AuthServer.SetEmitter(options.emitter)
|
|
}
|
|
|
|
var tlsServerOpts []authtest.TestTLSServerOption
|
|
if options.accessGraph != nil {
|
|
tlsServerOpts = append(tlsServerOpts, authtest.WithAccessGraphConfig(*options.accessGraph))
|
|
}
|
|
if options.bufconnListener {
|
|
tlsServerOpts = append(tlsServerOpts, authtest.WithBufconnListener())
|
|
}
|
|
srv, err := as.NewTestTLSServer(tlsServerOpts...)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
err := srv.Close()
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
return srv
|
|
}
|
|
|
|
func TestVerifyPeerCert(t *testing.T) {
|
|
t.Parallel()
|
|
const (
|
|
localClusterName = "local"
|
|
remoteClusterName = "remote"
|
|
)
|
|
s := newTestServices(t)
|
|
// Set up local cluster name in the backend.
|
|
cn, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
|
|
ClusterName: localClusterName,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.UpsertClusterName(cn))
|
|
|
|
now := time.Date(2020, time.November, 5, 0, 0, 0, 0, time.UTC)
|
|
|
|
var (
|
|
localUserIdentity = tlsca.Identity{
|
|
Username: "foo",
|
|
Groups: []string{"devs"},
|
|
TeleportCluster: localClusterName,
|
|
Expires: now,
|
|
}
|
|
localSystemRole = tlsca.Identity{
|
|
Username: "node",
|
|
Groups: []string{string(types.RoleNode)},
|
|
TeleportCluster: localClusterName,
|
|
Expires: now,
|
|
}
|
|
remoteUserIdentity = tlsca.Identity{
|
|
Username: "foo",
|
|
Groups: []string{"devs"},
|
|
TeleportCluster: remoteClusterName,
|
|
Expires: now,
|
|
}
|
|
remoteSystemRole = tlsca.Identity{
|
|
Username: "node",
|
|
Groups: []string{string(types.RoleNode)},
|
|
TeleportCluster: remoteClusterName,
|
|
Expires: now,
|
|
}
|
|
)
|
|
|
|
localHostCA, err := authcatest.NewCA(types.HostCA, localClusterName)
|
|
require.NoError(t, err)
|
|
remoteHostCA, err := authcatest.NewCA(types.HostCA, remoteClusterName)
|
|
require.NoError(t, err)
|
|
localUserCA, err := authcatest.NewCA(types.UserCA, localClusterName)
|
|
require.NoError(t, err)
|
|
remoteUserCA, err := authcatest.NewCA(types.UserCA, remoteClusterName)
|
|
require.NoError(t, err)
|
|
|
|
caPool := buildPoolInfo(
|
|
t,
|
|
localHostCA,
|
|
localUserCA,
|
|
remoteHostCA,
|
|
remoteUserCA,
|
|
)
|
|
|
|
tests := []struct {
|
|
desc string
|
|
peer *x509.Certificate
|
|
clusterName string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
desc: "local user issued from remote CA",
|
|
peer: generateTestCert(t, remoteUserCA, localUserIdentity, localClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "local system user issued from remote CA",
|
|
peer: generateTestCert(t, remoteHostCA, localSystemRole, localClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "local user issued from local host CA",
|
|
peer: generateTestCert(t, localHostCA, localUserIdentity, localClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "local system user issued from local user CA",
|
|
peer: generateTestCert(t, localUserCA, localSystemRole, localClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "local user with remote cluster name issued from local user CA",
|
|
peer: generateTestCert(t, localUserCA, localUserIdentity, remoteClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "local system user with remote cluster name issued from local host CA",
|
|
peer: generateTestCert(t, localHostCA, localSystemRole, remoteClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "local user issued from local user CA",
|
|
peer: generateTestCert(t, localUserCA, localUserIdentity, localClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
desc: "local system user issued from local host CA",
|
|
peer: generateTestCert(t, localHostCA, localSystemRole, localClusterName),
|
|
clusterName: localClusterName,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
desc: "remote user issued from local CA",
|
|
peer: generateTestCert(t, localUserCA, remoteUserIdentity, remoteClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "remote system user issued from local CA",
|
|
peer: generateTestCert(t, localHostCA, remoteSystemRole, remoteClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "remote user issued from remote host CA",
|
|
peer: generateTestCert(t, remoteHostCA, remoteUserIdentity, remoteClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "remote system user issued from user CA",
|
|
peer: generateTestCert(t, remoteUserCA, remoteSystemRole, remoteClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "remote user with local cluster name issued from remote user CA",
|
|
peer: generateTestCert(t, remoteUserCA, remoteUserIdentity, localClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "remote system user with local cluster name issued from host CA",
|
|
peer: generateTestCert(t, remoteHostCA, remoteSystemRole, localClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "remote user issued from remote user CA",
|
|
peer: generateTestCert(t, remoteUserCA, remoteUserIdentity, remoteClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
desc: "remote system user issued from host CA",
|
|
peer: generateTestCert(t, remoteHostCA, remoteSystemRole, remoteClusterName),
|
|
clusterName: remoteClusterName,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
verify := caPool.VerifyPeerCert()
|
|
err := verify(nil, [][]*x509.Certificate{{tt.peer}})
|
|
if tt.wantErr {
|
|
require.ErrorContains(t, err, "access denied: invalid client certificate")
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildPoolInfo(t *testing.T, ca ...types.CertAuthority) *auth.HostAndUserCAPoolInfo {
|
|
poolInfo := auth.HostAndUserCAPoolInfo{
|
|
Pool: x509.NewCertPool(),
|
|
CATypes: make(authclient.HostAndUserCAInfo),
|
|
}
|
|
|
|
for _, authority := range ca {
|
|
for _, c := range authority.GetTrustedTLSKeyPairs() {
|
|
cert, err := tlsca.ParseCertificatePEM(c.Cert)
|
|
require.NoError(t, err)
|
|
poolInfo.Pool.AddCert(cert)
|
|
|
|
poolInfo.CATypes[string(cert.RawSubject)] = authclient.CATypeInfo{
|
|
IsHostCA: authority.GetType() == types.HostCA,
|
|
IsUserCA: authority.GetType() == types.UserCA,
|
|
}
|
|
}
|
|
}
|
|
|
|
return &poolInfo
|
|
}
|
|
|
|
func generateTestCert(t *testing.T, ca types.CertAuthority, id tlsca.Identity, clusterName string) *x509.Certificate {
|
|
tlsKeyPairs := ca.GetTrustedTLSKeyPairs()
|
|
require.Len(t, tlsKeyPairs, 1)
|
|
signer, err := tlsca.FromKeys(tlsKeyPairs[0].Cert, tlsKeyPairs[0].Key)
|
|
require.NoError(t, err)
|
|
|
|
priv, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
|
|
require.NoError(t, err)
|
|
|
|
id.TeleportCluster = clusterName
|
|
subj, err := id.Subject()
|
|
require.NoError(t, err)
|
|
|
|
pemCert, err := signer.GenerateCertificate(tlsca.CertificateRequest{
|
|
PublicKey: priv.Public(),
|
|
Subject: subj,
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
})
|
|
require.NoError(t, err)
|
|
block, rest := pem.Decode(pemCert)
|
|
require.NotNil(t, block)
|
|
require.Empty(t, rest)
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
require.NoError(t, err)
|
|
|
|
return cert
|
|
}
|