From 97c5b757d1ce32110f22dcb1c5bf26b4049d9a16 Mon Sep 17 00:00:00 2001
From: Tiago Silva <tiago.silva@goteleport.com>
Date: Wed, 10 Jul 2024 17:51:54 +0100
Subject: [PATCH] [sec_scan][11] add `AccessGraphSettings` backend service

This PR adds the backend service to be able to create, update and retrieve access graph configurations from Teleport backend.

This PR is part of https://github.com/gravitational/access-graph/issues/637.
---
 .../clusterconfig/access_graph_settings.go    |  79 ++++++++++++
 .../access_graph_settings_test.go             |  93 ++++++++++++++
 api/types/constants.go                        |   7 +
 lib/auth/authclient/clt.go                    |  25 ++++
 lib/services/access_graph_settings.go         |  37 ++++++
 lib/services/configuration.go                 |  12 ++
 lib/services/local/configuration.go           | 121 ++++++++++++++++--
 lib/services/local/configuration_test.go      |  12 ++
 lib/services/suite/suite.go                   |  37 ++++++
 9 files changed, 410 insertions(+), 13 deletions(-)
 create mode 100644 api/types/clusterconfig/access_graph_settings.go
 create mode 100644 api/types/clusterconfig/access_graph_settings_test.go
 create mode 100644 lib/services/access_graph_settings.go

diff --git a/api/types/clusterconfig/access_graph_settings.go b/api/types/clusterconfig/access_graph_settings.go
new file mode 100644
index 0000000000000..87721a88939e3
--- /dev/null
+++ b/api/types/clusterconfig/access_graph_settings.go
@@ -0,0 +1,79 @@
+/*
+Copyright 2024 Gravitational, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package clusterconfig
+
+import (
+	"github.com/gravitational/trace"
+
+	clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
+	headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+	"github.com/gravitational/teleport/api/types"
+)
+
+// NewAccessGraphSettings creates a new AccessGraphSettings resource.
+func NewAccessGraphSettings(spec *clusterconfigpb.AccessGraphSettingsSpec) (*clusterconfigpb.AccessGraphSettings, error) {
+	settings := &clusterconfigpb.AccessGraphSettings{
+		Kind:    types.KindAccessGraphSettings,
+		Version: types.V1,
+		Metadata: &headerv1.Metadata{
+			Name: types.MetaNameAccessGraphSettings,
+		},
+		Spec: spec,
+	}
+	if err := ValidateAccessGraphSettings(settings); err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	return settings, nil
+
+}
+
+// ValidateAccessGraphSettings checks that required parameters are set
+func ValidateAccessGraphSettings(s *clusterconfigpb.AccessGraphSettings) error {
+	if s == nil {
+		return trace.BadParameter("AccessGraphSettings is nil")
+	}
+	if s.Metadata == nil {
+		return trace.BadParameter("Metadata is nil")
+	}
+	if s.Spec == nil {
+		return trace.BadParameter("Spec is nil")
+	}
+
+	if s.Metadata.Name == "" {
+		return trace.BadParameter("Name is unset")
+	}
+
+	if s.Metadata.Name != types.MetaNameAccessGraphSettings {
+		return trace.BadParameter("Name is not %s", types.MetaNameAccessGraphSettings)
+	}
+
+	if s.Kind != types.KindAccessGraphSettings {
+		return trace.BadParameter("Kind is not AccessGraphSettings")
+	}
+	if s.Version != types.V1 {
+		return trace.BadParameter("Version is not V1")
+	}
+
+	switch s.Spec.GetSecretsScanConfig() {
+	case clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED, clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED:
+	default:
+		return trace.BadParameter("SecretsScanConfig is invalid")
+	}
+
+	return nil
+}
diff --git a/api/types/clusterconfig/access_graph_settings_test.go b/api/types/clusterconfig/access_graph_settings_test.go
new file mode 100644
index 0000000000000..82f7a4e6dc9d7
--- /dev/null
+++ b/api/types/clusterconfig/access_graph_settings_test.go
@@ -0,0 +1,93 @@
+/*
+Copyright 2024 Gravitational, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package clusterconfig
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/protobuf/testing/protocmp"
+
+	clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
+	headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+	"github.com/gravitational/teleport/api/types"
+)
+
+func TestNewAccessGraphSettings(t *testing.T) {
+	tests := []struct {
+		name      string
+		spec      *clusterconfigpb.AccessGraphSettingsSpec
+		want      *clusterconfigpb.AccessGraphSettings
+		assertErr func(*testing.T, error, ...any)
+	}{
+		{
+			name: "success disabled",
+			spec: &clusterconfigpb.AccessGraphSettingsSpec{
+				SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
+			},
+			assertErr: func(t *testing.T, err error, a ...any) {
+				require.NoError(t, err)
+			},
+			want: &clusterconfigpb.AccessGraphSettings{
+				Kind:    types.KindAccessGraphSettings,
+				Version: types.V1,
+				Metadata: &headerv1.Metadata{
+					Name: types.MetaNameAccessGraphSettings,
+				},
+				Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+					SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
+				},
+			},
+		},
+		{
+			name: "success enabled",
+			spec: &clusterconfigpb.AccessGraphSettingsSpec{
+				SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED,
+			},
+			assertErr: func(t *testing.T, err error, a ...any) {
+				require.NoError(t, err)
+			},
+			want: &clusterconfigpb.AccessGraphSettings{
+				Kind:    types.KindAccessGraphSettings,
+				Version: types.V1,
+				Metadata: &headerv1.Metadata{
+					Name: types.MetaNameAccessGraphSettings,
+				},
+				Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+					SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED,
+				},
+			},
+		},
+		{
+			name: "invalid",
+			spec: &clusterconfigpb.AccessGraphSettingsSpec{
+				SecretsScanConfig: 10,
+			},
+			assertErr: func(t *testing.T, err error, a ...any) {
+				require.ErrorContains(t, err, "SecretsScanConfig is invalid")
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := NewAccessGraphSettings(tt.spec)
+			tt.assertErr(t, err)
+			require.Empty(t, cmp.Diff(got, tt.want, protocmp.Transform()))
+		})
+	}
+}
diff --git a/api/types/constants.go b/api/types/constants.go
index f1bb19d03d84d..f598dcf32e760 100644
--- a/api/types/constants.go
+++ b/api/types/constants.go
@@ -532,6 +532,13 @@ const (
 	// KindVnetConfig is a resource which holds cluster-wide configuration for VNet.
 	KindVnetConfig = "vnet_config"
 
+	// KindAccessGraphSettings is a resource which holds cluster-wide configuration for dynamic access graph settings.
+	KindAccessGraphSettings = "access_graph_settings"
+
+	// MetaNameAccessGraphSettings is the exact name of the singleton resource holding
+	// access graph settings.
+	MetaNameAccessGraphSettings = "access-graph-settings"
+
 	// V7 is the seventh version of resources.
 	V7 = "v7"
 
diff --git a/lib/auth/authclient/clt.go b/lib/auth/authclient/clt.go
index 763500782387b..a38460173723d 100644
--- a/lib/auth/authclient/clt.go
+++ b/lib/auth/authclient/clt.go
@@ -789,6 +789,31 @@ func (c *Client) UpsertUserNotification(ctx context.Context, notification *notif
 	return nil, trace.NotImplemented(notImplementedMessage)
 }
 
+// GetAccessGraphSettings gets the access graph settings from the backend.
+func (c *Client) GetAccessGraphSettings(context.Context) (*clusterconfigpb.AccessGraphSettings, error) {
+	return nil, trace.NotImplemented(notImplementedMessage)
+}
+
+// CreateAccessGraphSettings creates the access graph settings in the backend.
+func (c *Client) CreateAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+	return nil, trace.NotImplemented(notImplementedMessage)
+}
+
+// UpdateAccessGraphSettings updates the access graph settings in the backend.
+func (c *Client) UpdateAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+	return nil, trace.NotImplemented(notImplementedMessage)
+}
+
+// UpsertAccessGraphSettings creates or updates the access graph settings in the backend.
+func (c *Client) UpsertAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+	return nil, trace.NotImplemented(notImplementedMessage)
+}
+
+// DeleteAccessGraphSettings deletes the access graph settings from the backend.
+func (c *Client) DeleteAccessGraphSettings(context.Context) error {
+	return trace.NotImplemented(notImplementedMessage)
+}
+
 type WebSessionReq struct {
 	// User is the user name associated with the session id.
 	User string `json:"user"`
diff --git a/lib/services/access_graph_settings.go b/lib/services/access_graph_settings.go
new file mode 100644
index 0000000000000..9454b60b4847e
--- /dev/null
+++ b/lib/services/access_graph_settings.go
@@ -0,0 +1,37 @@
+/*
+ * 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 services
+
+import (
+	"github.com/gravitational/trace"
+
+	clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
+)
+
+// UnmarshalAccessGraphSettings unmarshals the AccessGraphSettings resource from JSON.
+func UnmarshalAccessGraphSettings(data []byte, opts ...MarshalOption) (*clusterconfigpb.AccessGraphSettings, error) {
+	out, err := UnmarshalProtoResource[*clusterconfigpb.AccessGraphSettings](data, opts...)
+	return out, trace.Wrap(err)
+}
+
+// MarshalAccessGraphSettings marshals the AccessGraphSettings resource to JSON.
+func MarshalAccessGraphSettings(c *clusterconfigpb.AccessGraphSettings, opts ...MarshalOption) ([]byte, error) {
+	bytes, err := MarshalProtoResource(c, opts...)
+	return bytes, trace.Wrap(err)
+}
diff --git a/lib/services/configuration.go b/lib/services/configuration.go
index 0ffd95f9a9672..55477eaae9906 100644
--- a/lib/services/configuration.go
+++ b/lib/services/configuration.go
@@ -21,6 +21,7 @@ package services
 import (
 	"context"
 
+	clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
 	"github.com/gravitational/teleport/api/types"
 )
 
@@ -115,4 +116,15 @@ type ClusterConfiguration interface {
 	UpdateClusterMaintenanceConfig(ctx context.Context, cfg types.ClusterMaintenanceConfig) error
 	// DeleteClusterMaintenanceConfig deletes the maintenance config singleton.
 	DeleteClusterMaintenanceConfig(ctx context.Context) error
+
+	// GetAccessGraphSettings gets the access graph settings from the backend.
+	GetAccessGraphSettings(context.Context) (*clusterconfigpb.AccessGraphSettings, error)
+	// CreateAccessGraphSettings creates the access graph settings in the backend.
+	CreateAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error)
+	// UpdateAccessGraphSettings updates the access graph settings in the backend.
+	UpdateAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error)
+	// UpsertAccessGraphSettings creates or updates the access graph settings in the backend.
+	UpsertAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error)
+	// DeleteAccessGraphSettings deletes the access graph settings from the backend.
+	DeleteAccessGraphSettings(context.Context) error
 }
diff --git a/lib/services/local/configuration.go b/lib/services/local/configuration.go
index fc2d6e2759279..38865478326cc 100644
--- a/lib/services/local/configuration.go
+++ b/lib/services/local/configuration.go
@@ -26,6 +26,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 
 	"github.com/gravitational/teleport"
+	clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
 	"github.com/gravitational/teleport/api/types"
 	"github.com/gravitational/teleport/lib/backend"
 	"github.com/gravitational/teleport/lib/modules"
@@ -721,18 +722,112 @@ func (s *ClusterConfigurationService) DeleteClusterMaintenanceConfig(ctx context
 	return nil
 }
 
+// GetAccessGraphSettings fetches the cluster *clusterconfigpb.AccessGraphSettings from the backend and return them.
+func (s *ClusterConfigurationService) GetAccessGraphSettings(ctx context.Context) (*clusterconfigpb.AccessGraphSettings, error) {
+	item, err := s.Get(ctx, backend.Key(clusterConfigPrefix, accessGraphSettingsPrefix))
+	if err != nil {
+		if trace.IsNotFound(err) {
+			return nil, trace.NotFound("AccessGraphSettings preference not found")
+		}
+		return nil, trace.Wrap(err)
+	}
+	return services.UnmarshalAccessGraphSettings(item.Value,
+		services.WithExpires(item.Expires), services.WithRevision(item.Revision))
+}
+
+// CreateAccessGraphSettings creates an *clusterconfigpb.AccessGraphSettings if it does not already exist.
+func (s *ClusterConfigurationService) CreateAccessGraphSettings(ctx context.Context, set *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+	value, err := services.MarshalAccessGraphSettings(set)
+	if err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	item := backend.Item{
+		Key:   backend.Key(clusterConfigPrefix, accessGraphSettingsPrefix),
+		Value: value,
+	}
+
+	lease, err := s.Backend.Create(ctx, item)
+	if err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	set.Metadata.Revision = lease.Revision
+	return set, nil
+}
+
+// UpdateAccessGraphSettings updates an existing *clusterconfigpb.AccessGraphSettings.
+func (s *ClusterConfigurationService) UpdateAccessGraphSettings(ctx context.Context, set *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+	rev := set.GetMetadata().GetRevision()
+
+	value, err := services.MarshalAccessGraphSettings(set)
+	if err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	item := backend.Item{
+		Key:      backend.Key(clusterConfigPrefix, accessGraphSettingsPrefix),
+		Value:    value,
+		Revision: rev,
+	}
+
+	lease, err := s.ConditionalUpdate(ctx, item)
+	if err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	set.Metadata.Revision = lease.Revision
+	return set, nil
+}
+
+// UpsertAccessGraphSettings creates or overwrites an *clusterconfigpb.AccessGraphSettings.
+func (s *ClusterConfigurationService) UpsertAccessGraphSettings(ctx context.Context, set *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+	rev := set.GetMetadata().GetRevision()
+	value, err := services.MarshalAccessGraphSettings(set)
+	if err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	item := backend.Item{
+		Key:      backend.Key(clusterConfigPrefix, accessGraphSettingsPrefix),
+		Value:    value,
+		Revision: rev,
+	}
+
+	lease, err := s.Put(ctx, item)
+	if err != nil {
+		return nil, trace.Wrap(err)
+	}
+
+	set.Metadata.Revision = lease.Revision
+	return set, nil
+}
+
+// DeleteAccessGraphSettings deletes *clusterconfigpb.AccessGraphSettings from the backend.
+func (s *ClusterConfigurationService) DeleteAccessGraphSettings(ctx context.Context) error {
+	err := s.Delete(ctx, backend.Key(clusterConfigPrefix, accessGraphSettingsPrefix))
+	if err != nil {
+		if trace.IsNotFound(err) {
+			return trace.NotFound("access graph settings not found")
+		}
+		return trace.Wrap(err)
+	}
+	return nil
+}
+
 const (
-	clusterConfigPrefix    = "cluster_configuration"
-	namePrefix             = "name"
-	staticTokensPrefix     = "static_tokens"
-	authPrefix             = "authentication"
-	preferencePrefix       = "preference"
-	generalPrefix          = "general"
-	auditPrefix            = "audit"
-	networkingPrefix       = "networking"
-	sessionRecordingPrefix = "session_recording"
-	scriptsPrefix          = "scripts"
-	uiPrefix               = "ui"
-	installerPrefix        = "installer"
-	maintenancePrefix      = "maintenance"
+	clusterConfigPrefix       = "cluster_configuration"
+	namePrefix                = "name"
+	staticTokensPrefix        = "static_tokens"
+	authPrefix                = "authentication"
+	preferencePrefix          = "preference"
+	generalPrefix             = "general"
+	auditPrefix               = "audit"
+	networkingPrefix          = "networking"
+	sessionRecordingPrefix    = "session_recording"
+	scriptsPrefix             = "scripts"
+	uiPrefix                  = "ui"
+	installerPrefix           = "installer"
+	maintenancePrefix         = "maintenance"
+	accessGraphSettingsPrefix = "access_graph_settings"
 )
diff --git a/lib/services/local/configuration_test.go b/lib/services/local/configuration_test.go
index fd1f60a402619..22adc28b16977 100644
--- a/lib/services/local/configuration_test.go
+++ b/lib/services/local/configuration_test.go
@@ -70,6 +70,18 @@ func TestAuthPreference(t *testing.T) {
 	suite.AuthPreference(t)
 }
 
+func TestAccessGraphSettings(t *testing.T) {
+	tt := setupConfigContext(context.Background(), t)
+
+	clusterConfig, err := NewClusterConfigurationService(tt.bk)
+	require.NoError(t, err)
+
+	suite := &suite.ServicesTestSuite{
+		ConfigS: clusterConfig,
+	}
+	suite.AccessGraphSettings(t)
+}
+
 func TestClusterName(t *testing.T) {
 	tt := setupConfigContext(context.Background(), t)
 
diff --git a/lib/services/suite/suite.go b/lib/services/suite/suite.go
index b41022f409801..0a36b1a17b712 100644
--- a/lib/services/suite/suite.go
+++ b/lib/services/suite/suite.go
@@ -38,11 +38,15 @@ import (
 	"github.com/stretchr/testify/require"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/ssh"
+	protobuf "google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/testing/protocmp"
 
 	"github.com/gravitational/teleport"
 	"github.com/gravitational/teleport/api/constants"
 	apidefaults "github.com/gravitational/teleport/api/defaults"
+	clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
 	"github.com/gravitational/teleport/api/types"
+	"github.com/gravitational/teleport/api/types/clusterconfig"
 	"github.com/gravitational/teleport/api/utils"
 	"github.com/gravitational/teleport/lib/auth/testauthority"
 	"github.com/gravitational/teleport/lib/backend"
@@ -1202,6 +1206,39 @@ func (s *ServicesTestSuite) AuthPreference(t *testing.T) {
 	require.Empty(t, cmp.Diff(upserted, gotAP), cmpopts.IgnoreFields(types.Metadata{}, "Revision"))
 }
 
+// AccessGraphSettings tests access graph settings service
+func (s *ServicesTestSuite) AccessGraphSettings(t *testing.T) {
+	ctx := context.Background()
+	ap, err := clusterconfig.NewAccessGraphSettings(
+		&clusterconfigpb.AccessGraphSettingsSpec{
+			SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
+		},
+	)
+	require.NoError(t, err)
+
+	created, err := s.ConfigS.CreateAccessGraphSettings(ctx, ap)
+	require.NoError(t, err)
+	require.NotEmpty(t, created.GetMetadata().GetRevision())
+
+	// Validate the created preference matches the retrieve preference.
+	got, err := s.ConfigS.GetAccessGraphSettings(ctx)
+	require.NoError(t, err)
+	require.Empty(t, cmp.Diff(ap, got, protocmp.Transform()))
+
+	// Validate that update only works if the revision matches.
+	got.Metadata.Revision = "123"
+	_, err = s.ConfigS.UpdateAccessGraphSettings(ctx, got)
+	require.True(t, trace.IsCompareFailed(err))
+
+	// Validate that upserting overwrites the value regardless of the revision.
+	upserted, err := s.ConfigS.UpsertAccessGraphSettings(ctx, protobuf.Clone(got).(*clusterconfigpb.AccessGraphSettings))
+	require.NoError(t, err)
+	require.NotEmpty(t, upserted.GetMetadata().GetRevision())
+	upserted.Metadata.Revision = ""
+	got.Metadata.Revision = ""
+	require.Empty(t, cmp.Diff(upserted, got, protocmp.Transform()))
+}
+
 // SessionRecordingConfig tests session recording configuration.
 func (s *ServicesTestSuite) SessionRecordingConfig(t *testing.T) {
 	ctx := context.Background()