diff --git a/api/types/header/convert/legacy/header.go b/api/types/header/convert/legacy/header.go
index c9e82a5dd7ade..3f989c4a9cc33 100644
--- a/api/types/header/convert/legacy/header.go
+++ b/api/types/header/convert/legacy/header.go
@@ -17,6 +17,8 @@ limitations under the License.
package legacy
import (
+ "time"
+
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/header"
)
@@ -33,3 +35,19 @@ func FromHeaderMetadata(metadata header.Metadata) types.Metadata {
Revision: metadata.Revision,
}
}
+
+// ToHeaderMetadata will convert a types.Metadata object to this metadata object.
+// TODO: Remove this once we get rid of the old Metadata object.
+func ToHeaderMetadata(metadata types.Metadata) header.Metadata {
+ var expires time.Time
+ if metadata.Expires != nil {
+ expires = *metadata.Expires
+ }
+ return header.Metadata{
+ Name: metadata.Name,
+ Expires: expires,
+ Description: metadata.Description,
+ Labels: metadata.Labels,
+ Revision: metadata.Revision,
+ }
+}
diff --git a/lib/services/presets.go b/lib/services/presets.go
index 1d7992d1bea3a..1dc9874f5fc5e 100644
--- a/lib/services/presets.go
+++ b/lib/services/presets.go
@@ -176,6 +176,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindServerInfo, RW()),
types.NewRule(types.KindAppServer, RW()),
types.NewRule(types.KindAccessMonitoringRule, RW()),
+ types.NewRule(types.KindAccessGraphSettings, RW()),
},
},
},
diff --git a/lib/services/resource.go b/lib/services/resource.go
index aeba9adb6131a..411264c716c9c 100644
--- a/lib/services/resource.go
+++ b/lib/services/resource.go
@@ -246,6 +246,8 @@ func ParseShortcut(in string) (string, error) {
return types.KindAccessRequest, nil
case types.KindPlugin, types.KindPlugin + "s":
return types.KindPlugin, nil
+ case types.KindAccessGraphSettings, "ags":
+ return types.KindAccessGraphSettings, nil
}
return "", trace.BadParameter("unsupported resource: %q - resources should be expressed as 'type/name', for example 'connector/github'", in)
}
diff --git a/tool/tctl/common/clusterconfig/accessgraphsettings.go b/tool/tctl/common/clusterconfig/accessgraphsettings.go
new file mode 100644
index 0000000000000..939b8ae23efc0
--- /dev/null
+++ b/tool/tctl/common/clusterconfig/accessgraphsettings.go
@@ -0,0 +1,148 @@
+/*
+ * 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 .
+ */
+
+package clusterconfig
+
+import (
+ "github.com/gravitational/trace"
+
+ clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/api/types/header/convert/legacy"
+ headerv1 "github.com/gravitational/teleport/api/types/header/convert/v1"
+ "github.com/gravitational/teleport/lib/utils"
+)
+
+// AccessGraphSettings is a type to represent [clusterconfigpb.AcccessGraphSettings]
+// which implements types.Resource and custom YAML (un)marshaling.
+// This satisfies the expected YAML format for // the resource, which would be
+// hard/impossible to do for the proto resource directly
+type AccessGraphSettings struct {
+ // ResourceHeader is embedded to implement types.Resource
+ types.ResourceHeader
+ // Spec is the specification
+ Spec accessGraphSettingsSpec `json:"spec"`
+}
+
+// accessGraphSettingsSpec holds the AccessGraphSettings properties.
+type accessGraphSettingsSpec struct {
+ SecretsScanConfig string `json:"secrets_scan_config"`
+}
+
+// CheckAndSetDefaults sanity checks AccessGraphSettings fields to catch simple errors, and
+// sets default values for all fields with defaults.
+func (r *AccessGraphSettings) CheckAndSetDefaults() error {
+ if err := r.Metadata.CheckAndSetDefaults(); err != nil {
+ return trace.Wrap(err)
+ }
+ if r.Kind == "" {
+ r.Kind = types.KindAccessGraphSettings
+ } else if r.Kind != types.KindAccessGraphSettings {
+ return trace.BadParameter("unexpected resource kind %q, must be %q", r.Kind, types.KindAccessGraphSettings)
+ }
+ if r.Version == "" {
+ r.Version = types.V1
+ } else if r.Version != types.V1 {
+ return trace.BadParameter("unsupported resource version %q, %q is currently the only supported version", r.Version, types.V1)
+ }
+ if r.Metadata.Name == "" {
+ r.Metadata.Name = types.MetaNameAccessGraphSettings
+ } else if r.Metadata.Name != types.MetaNameAccessGraphSettings {
+ return trace.BadParameter("access graph settings must have a name %q", types.MetaNameAccessGraphSettings)
+ }
+
+ if _, err := stringToSecretsScanConfig(r.Spec.SecretsScanConfig); err != nil {
+ return trace.BadParameter("secrets_scan_config must be one of [enabled, disabled]")
+ }
+
+ return nil
+}
+
+// UnmarshalAccessGraphSettings parses a [*clusterconfigpb.AccessGraphSettings] in the [AccessGraphSettings]
+// format which matches the expected YAML format for Teleport resources, sets default values, and
+// converts to [*clusterconfigpb.AccessGraphSettings].
+func UnmarshalAccessGraphSettings(raw []byte) (*clusterconfigpb.AccessGraphSettings, error) {
+ var resource AccessGraphSettings
+ if err := utils.FastUnmarshal(raw, &resource); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := resource.CheckAndSetDefaults(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ rec, err := resourceToProto(&resource)
+ return rec, trace.Wrap(err)
+}
+
+// ProtoToResource converts a [*clusterconfigpb.AccessGraphSettings] into a [*AccessGraphSettings] which
+// implements types.Resource and can be marshaled to YAML or JSON in a
+// human-friendly format.
+func ProtoToResource(set *clusterconfigpb.AccessGraphSettings) (*AccessGraphSettings, error) {
+ conf, err := secretsScanConfigToString(set.Spec.SecretsScanConfig)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ r := &AccessGraphSettings{
+ ResourceHeader: types.ResourceHeader{
+ Kind: set.Kind,
+ Version: set.Version,
+ Metadata: legacy.FromHeaderMetadata(headerv1.FromMetadataProto(set.Metadata)),
+ },
+ Spec: accessGraphSettingsSpec{
+ SecretsScanConfig: conf,
+ },
+ }
+ return r, nil
+}
+
+func resourceToProto(r *AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) {
+ secretsScanConfig, err := stringToSecretsScanConfig(r.Spec.SecretsScanConfig)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &clusterconfigpb.AccessGraphSettings{
+ Kind: r.Kind,
+ SubKind: r.SubKind,
+ Version: r.Version,
+ Metadata: headerv1.ToMetadataProto(legacy.ToHeaderMetadata(r.Metadata)),
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: secretsScanConfig,
+ },
+ }, nil
+}
+
+func secretsScanConfigToString(secretsScanConfig clusterconfigpb.AccessGraphSecretsScanConfig) (string, error) {
+ switch secretsScanConfig {
+ case clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED:
+ return "disabled", nil
+ case clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED:
+ return "enabled", nil
+ default:
+ return "", trace.BadParameter("unexpected secrets scan config %q", secretsScanConfig)
+ }
+}
+
+func stringToSecretsScanConfig(secretsScanConfig string) (clusterconfigpb.AccessGraphSecretsScanConfig, error) {
+ switch secretsScanConfig {
+ case "disabled", "off":
+ return clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED, nil
+ case "enabled", "on":
+ return clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED, nil
+ default:
+ return clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_UNSPECIFIED, trace.BadParameter("secrets scan config must be one of [enabled, disabled]")
+ }
+}
diff --git a/tool/tctl/common/clusterconfig/accessgraphsettings_test.go b/tool/tctl/common/clusterconfig/accessgraphsettings_test.go
new file mode 100644
index 0000000000000..01a1a7e3f4366
--- /dev/null
+++ b/tool/tctl/common/clusterconfig/accessgraphsettings_test.go
@@ -0,0 +1,269 @@
+/*
+ * 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 .
+ */
+
+package clusterconfig
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/testing/protocmp"
+ kyaml "k8s.io/apimachinery/pkg/util/yaml"
+
+ 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"
+ "github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/services"
+)
+
+func TestUnmarshalAccessGraphSettings(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ desc string
+ input string
+ errorContains string
+ expected *clusterconfigpb.AccessGraphSettings
+ }{
+ {
+ desc: "disabled",
+ input: `---
+kind: access_graph_settings
+version: v1
+metadata:
+ name: access-graph-settings
+spec:
+ secrets_scan_config: "disabled"
+`,
+ expected: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
+ },
+ },
+ },
+ {
+ desc: "off",
+ input: `---
+kind: access_graph_settings
+version: v1
+metadata:
+ name: access-graph-settings
+spec:
+ secrets_scan_config: "off"
+`,
+ expected: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
+ },
+ },
+ },
+ {
+ desc: "enabled",
+ input: `---
+kind: access_graph_settings
+version: v1
+metadata:
+ name: access-graph-settings
+spec:
+ secrets_scan_config: "enabled"
+`,
+ expected: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED,
+ },
+ },
+ },
+ {
+ desc: "on",
+ input: `---
+kind: access_graph_settings
+version: v1
+metadata:
+ name: access-graph-settings
+spec:
+ secrets_scan_config: "on"
+`,
+ expected: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED,
+ },
+ },
+ },
+ {
+ desc: "invalid settings",
+ input: `---
+kind: access_graph_settings
+version: v1
+metadata:
+ name: access-graph-settings
+spec:
+ secrets_scan_config: "invalidasd"
+`,
+ errorContains: "secrets_scan_config must be one of [enabled, disabled]",
+ },
+ {
+ desc: "wrong name",
+ input: `---
+kind: access_graph_settings
+version: v1
+metadata:
+ name: access
+spec:
+ secrets_scan_config: "on"
+`,
+ errorContains: "access graph settings must have a name \"access-graph-settings\"",
+ },
+ {
+ desc: "wrong version",
+ input: `---
+kind: access_graph_settings
+version: v2
+metadata:
+ name: access-graph-settings
+spec:
+ secrets_scan_config: "on"
+`,
+ errorContains: "unsupported resource version",
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ // Mimic tctl resource command by using the same decoder and
+ // initially unmarshalling into services.UnknownResource
+ reader := strings.NewReader(tc.input)
+ decoder := kyaml.NewYAMLOrJSONDecoder(reader, defaults.LookaheadBufSize)
+ var raw services.UnknownResource
+ err := decoder.Decode(&raw)
+ require.NoError(t, err)
+ require.Equal(t, types.KindAccessGraphSettings, raw.Kind)
+
+ out, err := UnmarshalAccessGraphSettings(raw.Raw)
+ if tc.errorContains != "" {
+ require.ErrorContains(t, err, tc.errorContains, "error from UnmarshalAccessGraphSettings does not contain the expected string")
+ return
+ }
+ require.NoError(t, err, "UnmarshalAccessGraphSettings returned unexpected error")
+
+ require.Empty(t, cmp.Diff(tc.expected, out, protocmp.Transform()), "unmarshalled data does not match what was expected")
+ })
+ }
+}
+
+func TestProtoToResource(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ desc string
+ expected *AccessGraphSettings
+ errorContains string
+ input *clusterconfigpb.AccessGraphSettings
+ }{
+ {
+ desc: "disabled",
+ expected: &AccessGraphSettings{
+ ResourceHeader: types.ResourceHeader{
+ Kind: types.KindAccessGraphSettings,
+ Version: types.V1,
+ Metadata: types.Metadata{Name: types.MetaNameAccessGraphSettings},
+ },
+ Spec: accessGraphSettingsSpec{
+ SecretsScanConfig: "disabled",
+ },
+ },
+ input: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
+ },
+ },
+ },
+ {
+ desc: "enabled",
+ expected: &AccessGraphSettings{
+ ResourceHeader: types.ResourceHeader{
+ Kind: types.KindAccessGraphSettings,
+ Version: types.V1,
+ Metadata: types.Metadata{Name: types.MetaNameAccessGraphSettings},
+ },
+ Spec: accessGraphSettingsSpec{
+ SecretsScanConfig: "enabled",
+ },
+ },
+ input: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED,
+ },
+ },
+ },
+ {
+ desc: "incorrect data",
+ errorContains: "unexpected secrets scan config",
+ input: &clusterconfigpb.AccessGraphSettings{
+ Version: types.V1,
+ Kind: types.KindAccessGraphSettings,
+ Metadata: &headerv1.Metadata{
+ Name: types.MetaNameAccessGraphSettings,
+ },
+ Spec: &clusterconfigpb.AccessGraphSettingsSpec{
+ SecretsScanConfig: 5,
+ },
+ },
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+
+ out, err := ProtoToResource(tc.input)
+ if tc.errorContains != "" {
+ require.ErrorContains(t, err, tc.errorContains, "error from ProtoToResource does not contain the expected string")
+ return
+ }
+ require.NoError(t, err, "ProtoToResource returned unexpected error")
+
+ require.Empty(t, cmp.Diff(tc.expected, out, protocmp.Transform()))
+ })
+ }
+}
diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go
index 3ed204084af06..f1863230fcb0b 100644
--- a/tool/tctl/common/collection.go
+++ b/tool/tctl/common/collection.go
@@ -48,6 +48,7 @@ import (
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/tool/common"
+ clusterconfigrec "github.com/gravitational/teleport/tool/tctl/common/clusterconfig"
"github.com/gravitational/teleport/tool/tctl/common/databaseobject"
"github.com/gravitational/teleport/tool/tctl/common/databaseobjectimportrule"
"github.com/gravitational/teleport/tool/tctl/common/loginrule"
@@ -1466,6 +1467,23 @@ func (c *accessListCollection) writeText(w io.Writer, verbose bool) error {
return trace.Wrap(err)
}
+type accessGraphSettings struct {
+ accessGraphSettings *clusterconfigrec.AccessGraphSettings
+}
+
+func (c *accessGraphSettings) resources() []types.Resource {
+ return []types.Resource{c.accessGraphSettings}
+}
+
+func (c *accessGraphSettings) writeText(w io.Writer, verbose bool) error {
+ t := asciitable.MakeTable([]string{"SSH Keys Scan"})
+ t.AddRow([]string{
+ c.accessGraphSettings.Spec.SecretsScanConfig,
+ })
+ _, err := t.AsBuffer().WriteTo(w)
+ return trace.Wrap(err)
+}
+
type accessRequestCollection struct {
accessRequests []types.AccessRequest
}
diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go
index e985f3686fff4..f3083fa3d114a 100644
--- a/tool/tctl/common/resource_command.go
+++ b/tool/tctl/common/resource_command.go
@@ -42,6 +42,7 @@ import (
apiclient "github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
apidefaults "github.com/gravitational/teleport/api/defaults"
+ clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
crownjewelv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/crownjewel/v1"
dbobjectv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobject/v1"
dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
@@ -64,6 +65,7 @@ import (
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
+ clusterconfigrec "github.com/gravitational/teleport/tool/tctl/common/clusterconfig"
"github.com/gravitational/teleport/tool/tctl/common/databaseobject"
"github.com/gravitational/teleport/tool/tctl/common/databaseobjectimportrule"
"github.com/gravitational/teleport/tool/tctl/common/loginrule"
@@ -159,6 +161,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec
types.KindDatabaseObject: rc.createDatabaseObject,
types.KindAccessMonitoringRule: rc.createAccessMonitoringRule,
types.KindCrownJewel: rc.createCrownJewel,
+ types.KindAccessGraphSettings: rc.upsertAccessGraphSettings,
types.KindPlugin: rc.createPlugin,
}
rc.UpdateHandlers = map[ResourceKind]ResourceCreateHandler{
@@ -172,6 +175,7 @@ func (rc *ResourceCommand) Initialize(app *kingpin.Application, config *servicec
types.KindSessionRecordingConfig: rc.updateSessionRecordingConfig,
types.KindAccessMonitoringRule: rc.updateAccessMonitoringRule,
types.KindCrownJewel: rc.updateCrownJewel,
+ types.KindAccessGraphSettings: rc.updateAccessGraphSettings,
types.KindPlugin: rc.updatePlugin,
}
rc.config = config
@@ -2814,6 +2818,16 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient
startKey = resp.NextKey
}
return &pluginCollection{plugins: plugins}, nil
+ case types.KindAccessGraphSettings:
+ settings, err := client.ClusterConfigClient().GetAccessGraphSettings(ctx, &clusterconfigpb.GetAccessGraphSettingsRequest{})
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ rec, err := clusterconfigrec.ProtoToResource(settings)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &accessGraphSettings{accessGraphSettings: rec}, nil
}
return nil, trace.BadParameter("getting %q is not supported", rc.ref.String())
}
@@ -3101,3 +3115,30 @@ func (rc *ResourceCommand) createPlugin(ctx context.Context, client *authclient.
fmt.Printf("plugin %q has been updated\n", item.GetName())
return nil
}
+
+func (rc *ResourceCommand) upsertAccessGraphSettings(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error {
+ settings, err := clusterconfigrec.UnmarshalAccessGraphSettings(raw.Raw)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if _, err = client.ClusterConfigClient().UpsertAccessGraphSettings(ctx, &clusterconfigpb.UpsertAccessGraphSettingsRequest{AccessGraphSettings: settings}); err != nil {
+ return trace.Wrap(err)
+ }
+
+ fmt.Println("access_graph_settings has been upserted")
+ return nil
+}
+
+func (rc *ResourceCommand) updateAccessGraphSettings(ctx context.Context, client *authclient.Client, raw services.UnknownResource) error {
+ settings, err := clusterconfigrec.UnmarshalAccessGraphSettings(raw.Raw)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if _, err = client.ClusterConfigClient().UpdateAccessGraphSettings(ctx, &clusterconfigpb.UpdateAccessGraphSettingsRequest{AccessGraphSettings: settings}); err != nil {
+ return trace.Wrap(err)
+ }
+ fmt.Println("access_graph_settings has been updated")
+ return nil
+}