Files
teleport/lib/services/local/trust_test.go
T
Noah Stride a43fd71911 Migrate GetTunnelConnections/GetAllTunnelConnections to gRPC (#66066)
* hack on migrating list RPC

* Fix tests

* Tidy up/fix tests

* Use `clientutils.CollectWithFallback`

* Use clearer c.APIClient rather than rely on embeding

* Cover ListTunnelConnections in cache test

* tweak logging

* update deprecation version

* Move fallback impls

* fix span ctx prop

* fix pagination bug when unmarshal bad

* USe t.Context()

* fix ctx propagation

* Update tunn conns to use range pattern - thanks luke

* fix unneeded dependency

* fix deprecation

* env: dev
use opaque API
2026-06-11 12:44:50 +00:00

812 lines
27 KiB
Go

/*
* Teleport
* Copyright (C) 2024 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 local
import (
"context"
"crypto/x509/pkix"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/memory"
"github.com/gravitational/teleport/lib/itertools/stream"
"github.com/gravitational/teleport/lib/tlsca"
)
func TestUpdateCertAuthorityCondActs(t *testing.T) {
t.Parallel()
ctx := t.Context()
// setup closure creates our initial state and returns its components
setup := func(active bool) (types.TrustedCluster, types.CertAuthority, *CA) {
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, bk.Close()) })
service := NewCAService(bk)
tc, err := types.NewTrustedCluster("tc", types.TrustedClusterSpecV2{
Enabled: active,
Roles: []string{"rrr"},
Token: "xxx",
ProxyAddress: "xxx",
ReverseTunnelAddress: "xxx",
})
require.NoError(t, err)
ca := newCertAuthority(t, types.HostCA, "tc")
revision, err := service.CreateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
return tc, ca, service
}
// putCA is a helper for injecting a CA into the backend, bypassing atomic condition protections
putCA := func(ctx context.Context, service *CA, ca types.CertAuthority, active bool) {
key := activeCAKey(ca.GetID())
if !active {
key = inactiveCAKey(ca.GetID())
}
item, err := caToItem(key, ca)
require.NoError(t, err)
_, err = service.Put(ctx, item)
require.NoError(t, err)
}
// delCA is a helper for deleting a CA from the backend, bypassing atomic condition protections
delCA := func(ctx context.Context, service *CA, ca types.CertAuthority, active bool) {
key := activeCAKey(ca.GetID())
if !active {
key = inactiveCAKey(ca.GetID())
}
require.NoError(t, service.Delete(ctx, key))
}
// -- update active in place ---
tc, ca, service := setup(true /* active */)
// verify basic update works
tc.SetRoles([]string{"rrr", "zzz"})
revision, err := service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err := service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
// verify that an inactive CA doesn't prevent update
putCA(ctx, service, ca, false /* inactive */)
tc.SetRoles([]string{"rrr", "zzz", "aaa"})
revision, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err = service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
// verify that concurrent update of the active CA causes update to fail
putCA(ctx, service, ca, true /* active */)
tc.SetRoles([]string{"rrr", "zzz", "aaa", "bbb"})
_, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.True(t, trace.IsCompareFailed(err), "err=%v", err)
// --- update inactive in place ---
tc, ca, service = setup(false /* inactive */)
// verify basic update works
tc.SetRoles([]string{"rrr", "zzz"})
revision, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err = service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
// verify that an active CA prevents update
putCA(ctx, service, ca, true /* active */)
tc.SetRoles([]string{"rrr", "zzz", "aaa"})
_, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.True(t, trace.IsCompareFailed(err), "err=%v", err)
delCA(ctx, service, ca, true /* active */)
// verify that concurrent update of the inactive CA causes update to fail
putCA(ctx, service, ca, false /* inactive */)
tc.SetRoles([]string{"rrr", "zzz", "aaa", "bbb"})
_, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.True(t, trace.IsCompareFailed(err), "err=%v", err)
// --- activate/deactivate ---
tc, ca, service = setup(false /* inactive */)
// verify that activating works
tc.SetEnabled(true)
revision, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err = service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
// verify that deactivating works
tc.SetEnabled(false)
revision, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err = service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
// verify that an active CA conflicts with activation
putCA(ctx, service, ca, true /* active */)
tc.SetEnabled(true)
_, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.True(t, trace.IsCompareFailed(err), "err=%v", err)
delCA(ctx, service, ca, true /* active */)
// activation should work after deleting conlicting CA
revision, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err = service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
// verify that deactivation works even if there is an inaactive CA present
putCA(ctx, service, ca, false /* inactive */)
tc.SetEnabled(false)
revision, err = service.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
tc.SetRevision(revision)
ca.SetRevision(revision)
gotTC, err = service.GetTrustedCluster(ctx, tc.GetName())
require.NoError(t, err)
require.Empty(t, cmp.Diff(tc, gotTC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
_, err = service.GetCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
_, err = service.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
}
func TestRemoteClusterCRUD(t *testing.T) {
t.Parallel()
ctx := context.Background()
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, bk.Close()) })
trustService := NewCAService(bk)
clock := clockwork.NewFakeClockAt(time.Now())
originalLabels := map[string]string{
"a": "b",
"c": "d",
}
rc, err := types.NewRemoteCluster("foo")
require.NoError(t, err)
rc.SetConnectionStatus(teleport.RemoteClusterStatusOffline)
rc.SetLastHeartbeat(clock.Now())
rc.SetMetadata(types.Metadata{
Name: "foo",
Labels: originalLabels,
})
src, err := types.NewRemoteCluster("bar")
require.NoError(t, err)
src.SetConnectionStatus(teleport.RemoteClusterStatusOnline)
src.SetLastHeartbeat(clock.Now().Add(-time.Hour))
// set up fake CAs for the remote clusters
ca := newCertAuthority(t, types.HostCA, "foo")
sca := newCertAuthority(t, types.HostCA, "bar")
// create remote cluster
revision, err := trustService.CreateRemoteClusterInternal(ctx, rc, []types.CertAuthority{ca})
require.NoError(t, err)
rc.SetRevision(revision)
ca.SetRevision(revision)
_, err = trustService.CreateRemoteClusterInternal(ctx, rc, []types.CertAuthority{ca})
require.True(t, trace.IsAlreadyExists(err), "err=%v", err)
revision, err = trustService.CreateRemoteClusterInternal(ctx, src, []types.CertAuthority{sca})
require.NoError(t, err)
src.SetRevision(revision)
sca.SetRevision(revision)
// get remote cluster make sure it's correct
gotRC, err := trustService.GetRemoteCluster(ctx, "foo")
require.NoError(t, err)
require.Equal(t, "foo", gotRC.GetName())
require.Equal(t, teleport.RemoteClusterStatusOffline, gotRC.GetConnectionStatus())
require.Equal(t, clock.Now().Nanosecond(), gotRC.GetLastHeartbeat().Nanosecond())
require.Equal(t, originalLabels, gotRC.GetMetadata().Labels)
// get remote cluster CA make sure it's correct
gotCA, err := trustService.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
require.Empty(t, cmp.Diff(ca, gotCA, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
rc = gotRC
updatedLabels := map[string]string{
"e": "f",
"g": "h",
}
// update remote clusters
rc.SetConnectionStatus(teleport.RemoteClusterStatusOnline)
rc.SetLastHeartbeat(clock.Now().Add(time.Hour))
meta := rc.GetMetadata()
meta.Labels = updatedLabels
rc.SetMetadata(meta)
gotRC, err = trustService.UpdateRemoteCluster(ctx, rc)
require.NoError(t, err)
require.Empty(t, cmp.Diff(rc, gotRC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
src.SetConnectionStatus(teleport.RemoteClusterStatusOffline)
src.SetLastHeartbeat(clock.Now())
gotSRC, err := trustService.UpdateRemoteCluster(ctx, src)
require.NoError(t, err)
require.Empty(t, cmp.Diff(src, gotSRC, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
// get remote cluster make sure it's correct
gotRC, err = trustService.GetRemoteCluster(ctx, "foo")
require.NoError(t, err)
require.Equal(t, "foo", gotRC.GetName())
require.Equal(t, teleport.RemoteClusterStatusOnline, gotRC.GetConnectionStatus())
require.Equal(t, clock.Now().Add(time.Hour).Nanosecond(), gotRC.GetLastHeartbeat().Nanosecond())
require.Equal(t, updatedLabels, gotRC.GetMetadata().Labels)
gotRC, err = trustService.GetRemoteCluster(ctx, "bar")
require.NoError(t, err)
require.Equal(t, "bar", gotRC.GetName())
require.Equal(t, teleport.RemoteClusterStatusOffline, gotRC.GetConnectionStatus())
require.Equal(t, clock.Now().Nanosecond(), gotRC.GetLastHeartbeat().Nanosecond())
// get all clusters
allRC, err := trustService.GetRemoteClusters(ctx)
require.NoError(t, err)
require.Len(t, allRC, 2)
// delete cluster
err = trustService.DeleteRemoteClusterInternal(ctx, "foo", []types.CertAuthID{ca.GetID()})
require.NoError(t, err)
// make sure it's really gone
_, err = trustService.GetRemoteCluster(ctx, "foo")
require.True(t, trace.IsNotFound(err))
_, err = trustService.GetCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err))
// make sure we can't create trusted clusters with the same name as an extant remote cluster
tc, err := types.NewTrustedCluster("bar", types.TrustedClusterSpecV2{
Enabled: true,
Roles: []string{"bar", "baz"},
Token: "qux",
ProxyAddress: "quux",
ReverseTunnelAddress: "quuz",
})
require.NoError(t, err)
_, err = trustService.CreateTrustedCluster(ctx, tc, nil)
require.True(t, trace.IsBadParameter(err), "err=%v", err)
}
func TestPresenceService_PatchRemoteCluster(t *testing.T) {
t.Parallel()
ctx := context.Background()
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, bk.Close()) })
trustService := NewCAService(bk)
rc, err := types.NewRemoteCluster("bar")
require.NoError(t, err)
rc.SetConnectionStatus(teleport.RemoteClusterStatusOffline)
_, err = trustService.CreateRemoteCluster(ctx, rc)
require.NoError(t, err)
updatedRC, err := trustService.PatchRemoteCluster(
ctx,
rc.GetName(),
func(rc types.RemoteCluster) (types.RemoteCluster, error) {
require.Equal(t, teleport.RemoteClusterStatusOffline, rc.GetConnectionStatus())
rc.SetConnectionStatus(teleport.RemoteClusterStatusOnline)
return rc, nil
},
)
require.NoError(t, err)
require.Equal(t, teleport.RemoteClusterStatusOnline, updatedRC.GetConnectionStatus())
// Ensure this was persisted.
fetchedRC, err := trustService.GetRemoteCluster(ctx, rc.GetName())
require.NoError(t, err)
require.Equal(t, teleport.RemoteClusterStatusOnline, fetchedRC.GetConnectionStatus())
// Ensure other fields unchanged
require.Empty(t,
cmp.Diff(
rc,
fetchedRC,
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
cmpopts.IgnoreFields(types.RemoteClusterStatusV3{}, "Connection"),
),
)
// Ensure that name cannot be updated
_, err = trustService.PatchRemoteCluster(
ctx,
rc.GetName(),
func(rc types.RemoteCluster) (types.RemoteCluster, error) {
rc.SetName("baz")
return rc, nil
},
)
require.Error(t, err)
require.True(t, trace.IsBadParameter(err))
require.Contains(t, err.Error(), "metadata.name: cannot be patched")
// Ensure that revision cannot be updated
_, err = trustService.PatchRemoteCluster(
ctx,
rc.GetName(),
func(rc types.RemoteCluster) (types.RemoteCluster, error) {
rc.SetRevision("baz")
return rc, nil
},
)
require.Error(t, err)
require.True(t, trace.IsBadParameter(err))
require.Contains(t, err.Error(), "metadata.revision: cannot be patched")
}
func TestPresenceService_ListRemoteClusters(t *testing.T) {
t.Parallel()
ctx := context.Background()
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, bk.Close()) })
trustService := NewCAService(bk)
// With no resources, we should not get an error but we should get an empty
// token and an empty slice.
rcs, pageToken, err := trustService.ListRemoteClusters(ctx, 0, "")
require.NoError(t, err)
require.Empty(t, pageToken)
require.Empty(t, rcs)
// Create a few remote clusters
for i := range 10 {
rc, err := types.NewRemoteCluster(fmt.Sprintf("rc-%d", i))
require.NoError(t, err)
_, err = trustService.CreateRemoteCluster(ctx, rc)
require.NoError(t, err)
}
// Check limit behaves
rcs, pageToken, err = trustService.ListRemoteClusters(ctx, 1, "")
require.NoError(t, err)
require.NotEmpty(t, pageToken)
require.Len(t, rcs, 1)
// Iterate through all pages with a low limit to ensure that pageToken
// behaves correctly.
rcs = []types.RemoteCluster{}
pageToken = ""
for i := range 10 {
var got []types.RemoteCluster
got, pageToken, err = trustService.ListRemoteClusters(ctx, 1, pageToken)
require.NoError(t, err)
if i == 9 {
// For the final page, we should not get a page token
require.Empty(t, pageToken)
} else {
require.NotEmpty(t, pageToken)
}
require.Len(t, got, 1)
rcs = append(rcs, got...)
}
require.Len(t, rcs, 10)
// Check that with a higher limit, we get all resources
rcs, pageToken, err = trustService.ListRemoteClusters(ctx, 20, "")
require.NoError(t, err)
require.Empty(t, pageToken)
require.Len(t, rcs, 10)
}
// TestCA_TunnelConnections_CorruptItem ensures that a single malformed tunnel
// connection in the backend is skipped rather than failing or truncating the
// read. For List* this means pagination is not cut short; for Get*/GetAll* this
// means the bad item is skipped instead of erroring the whole call.
func TestCA_TunnelConnections_CorruptItem(t *testing.T) {
t.Parallel()
ctx := t.Context()
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, bk.Close()) })
trustService := NewCAService(bk)
const cluster = "leaf.example.com"
want := []string{"conn-0", "conn-1", "conn-2", "conn-3", "conn-4"}
for _, name := range want {
conn, err := types.NewTunnelConnection(name, types.TunnelConnectionSpecV2{
ClusterName: cluster,
ProxyName: "proxy",
LastHeartbeat: time.Now().UTC(),
Type: types.ProxyTunnel,
})
require.NoError(t, err)
require.NoError(t, trustService.UpsertTunnelConnection(ctx, conn))
}
// Write an unparseable item whose key sorts in the middle of the set
// (between conn-2 and conn-3).
_, err = bk.Put(ctx, backend.Item{
Key: backend.NewKey(tunnelConnectionsPrefix, cluster, "conn-2x"),
Value: []byte("}not a valid tunnel connection{"),
})
require.NoError(t, err)
// Page through with a small page size so the corrupt item lands inside a
// page window. Before the fix this dropped every connection after the
// corrupt one.
var got []string
var pageToken string
for {
page, next, err := trustService.ListTunnelConnections(ctx, 2, pageToken, nil)
require.NoError(t, err)
for _, c := range page {
got = append(got, c.GetName())
}
if next == "" {
break
}
pageToken = next
}
require.Equal(t, want, got)
// The cluster filter path must behave identically.
got = nil
pageToken = ""
for {
page, next, err := trustService.ListTunnelConnections(ctx, 2, pageToken, &trustpb.ListTunnelConnectionsFilter{
ClusterName: cluster,
})
require.NoError(t, err)
for _, c := range page {
got = append(got, c.GetName())
}
if next == "" {
break
}
pageToken = next
}
require.Equal(t, want, got)
// GetTunnelConnections and GetAllTunnelConnections skip the corrupt item
// rather than returning an error.
conns, err := trustService.GetTunnelConnections(ctx, cluster)
require.NoError(t, err)
gotNames := make([]string, 0, len(conns))
for _, c := range conns {
gotNames = append(gotNames, c.GetName())
}
require.ElementsMatch(t, want, gotNames)
allConns, err := trustService.GetAllTunnelConnections(ctx)
require.NoError(t, err)
gotNames = gotNames[:0]
for _, c := range allConns {
gotNames = append(gotNames, c.GetName())
}
require.ElementsMatch(t, want, gotNames)
}
func TestTrustedClusterCRUD(t *testing.T) {
t.Parallel()
ctx := context.Background()
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, bk.Close()) })
trustService := NewCAService(bk)
tc, err := types.NewTrustedCluster("foo", types.TrustedClusterSpecV2{
Enabled: true,
Roles: []string{"bar", "baz"},
Token: "qux",
ProxyAddress: "quux",
ReverseTunnelAddress: "quuz",
})
require.NoError(t, err)
// we just insert this one for get all
stc, err := types.NewTrustedCluster("bar", types.TrustedClusterSpecV2{
Enabled: false,
Roles: []string{"baz", "aux"},
Token: "quux",
ProxyAddress: "quuz",
ReverseTunnelAddress: "corge",
})
require.NoError(t, err)
ca := newCertAuthority(t, types.HostCA, "foo")
sca := newCertAuthority(t, types.HostCA, "bar")
// create trusted clusters
_, err = trustService.CreateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
_, err = trustService.CreateTrustedCluster(ctx, stc, []types.CertAuthority{sca})
require.NoError(t, err)
// get trusted cluster make sure it's correct
gotTC, err := trustService.GetTrustedCluster(ctx, "foo")
require.NoError(t, err)
require.Equal(t, "foo", gotTC.GetName())
require.True(t, gotTC.GetEnabled())
require.EqualValues(t, []string{"bar", "baz"}, gotTC.GetRoles())
require.Equal(t, "qux", gotTC.GetToken())
require.Equal(t, "quux", gotTC.GetProxyAddress())
require.Equal(t, "quuz", gotTC.GetReverseTunnelAddress())
// get trusted cluster CA make sure it's correct
gotCA, err := trustService.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
require.Empty(t, cmp.Diff(ca, gotCA, cmpopts.IgnoreFields(types.Metadata{}, "Revision")))
wantTcs := []types.TrustedCluster{tc, stc}
compareOpts := []cmp.Option{
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
cmpopts.SortSlices(func(a, b types.TrustedCluster) bool {
return a.GetName() < b.GetName()
},
)}
// get all clusters
allTC, err := trustService.GetTrustedClusters(ctx)
require.NoError(t, err)
require.Len(t, allTC, 2)
require.Empty(t, cmp.Diff(wantTcs, allTC, compareOpts...))
// No page limit.
allTC, next, err := trustService.ListTrustedClusters(ctx, 0, "")
require.NoError(t, err)
require.Len(t, allTC, 2)
require.Empty(t, next)
require.Empty(t, cmp.Diff(wantTcs, allTC, compareOpts...))
// Check page limits.
firstPage, next, err := trustService.ListTrustedClusters(ctx, 1, "")
require.NoError(t, err)
require.Len(t, firstPage, 1)
require.NotEmpty(t, next)
// Ensure upper limit works.
rangeEnd, err := stream.Collect(trustService.RangeTrustedClusters(ctx, "", next))
require.NoError(t, err)
require.Len(t, rangeEnd, 1)
require.NotEmpty(t, next)
require.Empty(t, cmp.Diff(firstPage, rangeEnd, compareOpts...))
// Full range.
allTC, err = stream.Collect(trustService.RangeTrustedClusters(ctx, "", ""))
require.NoError(t, err)
require.Len(t, allTC, 2)
require.Empty(t, cmp.Diff(wantTcs, allTC, compareOpts...))
// Ensure start token work for range.
secondPage, err := stream.Collect(trustService.RangeTrustedClusters(ctx, next, ""))
require.NoError(t, err)
require.Len(t, secondPage, 1)
require.Empty(t, cmp.Diff(wantTcs, append(firstPage, secondPage...), compareOpts...))
// verify that enabling/disabling correctly shows/hides CAs
tc.SetEnabled(false)
tc.SetRevision(gotTC.GetRevision())
ca.SetRevision(gotCA.GetRevision())
revision, err := trustService.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
_, err = trustService.GetCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
_, err = trustService.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
tc.SetEnabled(true)
tc.SetRevision(revision)
ca.SetRevision(revision)
_, err = trustService.UpdateTrustedCluster(ctx, tc, []types.CertAuthority{ca})
require.NoError(t, err)
_, err = trustService.GetCertAuthority(ctx, ca.GetID(), true)
require.NoError(t, err)
_, err = trustService.GetInactiveCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
// delete cluster
err = trustService.DeleteTrustedClusterInternal(ctx, "foo", []types.CertAuthID{ca.GetID()})
require.NoError(t, err)
// make sure it's really gone
_, err = trustService.GetTrustedCluster(ctx, "foo")
require.True(t, trace.IsNotFound(err), "err=%v", err)
_, err = trustService.GetCertAuthority(ctx, ca.GetID(), true)
require.True(t, trace.IsNotFound(err), "err=%v", err)
// make sure we can't create remote clusters with the same name as an extant trusted cluster
rc, err := types.NewRemoteCluster("bar")
require.NoError(t, err)
_, err = trustService.CreateRemoteCluster(ctx, rc)
require.True(t, trace.IsBadParameter(err), "err=%v", err)
}
func newCertAuthority(t *testing.T, caType types.CertAuthType, domain string) types.CertAuthority {
t.Helper()
priv, pub, err := testauthority.GenerateKeyPair()
require.NoError(t, err)
key, cert, err := tlsca.GenerateSelfSignedCA(pkix.Name{CommonName: domain}, nil, time.Hour)
require.NoError(t, err)
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: caType,
ClusterName: domain,
ActiveKeys: types.CAKeySet{
SSH: []*types.SSHKeyPair{{
PrivateKey: priv,
PrivateKeyType: types.PrivateKeyType_RAW,
PublicKey: pub,
}},
TLS: []*types.TLSKeyPair{{
Cert: cert,
Key: key,
}},
JWT: []*types.JWTKeyPair{{
PublicKey: pub,
PrivateKey: priv,
PrivateKeyType: types.PrivateKeyType_RAW,
}},
},
})
require.NoError(t, err)
return ca
}
func TestRemoteCluster_SkipsUnmarshalErrorsHittingPageBoundary(t *testing.T) {
ctx := t.Context()
const pageLimit = 64
const numberOfPages = 5
clock := clockwork.NewFakeClock()
mem, err := memory.New(memory.Config{
Context: t.Context(),
Clock: clock,
})
require.NoError(t, err)
service := NewCAService(mem)
createResource := func(name string) {
rc, err := types.NewRemoteCluster(name)
require.NoError(t, err)
_, err = service.CreateRemoteCluster(ctx, rc)
require.NoError(t, err)
}
createMalformedApp := func(name string) {
_, err := mem.Put(ctx, backend.Item{
Key: backend.NewKey(remoteClustersPrefix, name),
Value: []byte("not-valid-json"),
})
require.NoError(t, err)
}
for i := range pageLimit * numberOfPages {
key := fmt.Sprintf("r%d", i)
if i%2 == 0 {
createMalformedApp(key)
} else {
createResource(key)
}
}
page1, next, err := service.ListRemoteClusters(ctx, pageLimit, "")
require.NoError(t, err)
require.Len(t, page1, pageLimit)
require.NotEmpty(t, next)
page2, next, err := service.ListRemoteClusters(ctx, pageLimit, next)
require.NoError(t, err)
require.Len(t, page2, pageLimit)
require.NotEmpty(t, next)
page3, next, err := service.ListRemoteClusters(ctx, pageLimit, next)
require.NoError(t, err)
require.Len(t, page3, pageLimit/2)
require.Empty(t, next)
slices := [][]types.RemoteCluster{page1, page2, page3}
for i := range len(slices) {
for j := i + 1; j < len(slices); j++ {
assert.NotEqual(t, slices[i], slices[j], "slices %d and %d should differ", i, j)
}
}
}