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()