From 178e554c048b43cbef16b0af9603389faddaed89 Mon Sep 17 00:00:00 2001 From: Tiago Silva Date: Wed, 10 Jul 2024 18:35:00 +0100 Subject: [PATCH] [sec_scan][13] add `AccessGraphSettings` gRPC implementation This PR introduces the gRPC implementation for the CRUD operations related to `AccessGraphSettings`. This PR is part of https://github.com/gravitational/access-graph/issues/637. Signed-off-by: Tiago Silva --- .../clusterconfig/clusterconfigv1/service.go | 207 ++++++++ .../clusterconfigv1/service_test.go | 494 ++++++++++++++++-- lib/auth/grpcserver_test.go | 7 +- 3 files changed, 675 insertions(+), 33 deletions(-) diff --git a/lib/auth/clusterconfig/clusterconfigv1/service.go b/lib/auth/clusterconfig/clusterconfigv1/service.go index 977c28aa0c4da..80f3d055b2077 100644 --- a/lib/auth/clusterconfig/clusterconfigv1/service.go +++ b/lib/auth/clusterconfig/clusterconfigv1/service.go @@ -25,6 +25,7 @@ import ( "github.com/gravitational/teleport/api/constants" 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" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib/authz" @@ -63,6 +64,10 @@ type Backend interface { CreateSessionRecordingConfig(ctx context.Context, preference types.SessionRecordingConfig) (types.SessionRecordingConfig, error) UpdateSessionRecordingConfig(ctx context.Context, preference types.SessionRecordingConfig) (types.SessionRecordingConfig, error) UpsertSessionRecordingConfig(ctx context.Context, preference types.SessionRecordingConfig) (types.SessionRecordingConfig, error) + + CreateAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) + UpdateAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) + UpsertAccessGraphSettings(context.Context, *clusterconfigpb.AccessGraphSettings) (*clusterconfigpb.AccessGraphSettings, error) } // ServiceConfig contain dependencies required to create a [Service]. @@ -933,12 +938,214 @@ func (s *Service) GetClusterAccessGraphConfig(ctx context.Context, _ *clustercon }, nil } + var sshScanEnabled bool + switch obj, err := s.readOnlyCache.GetReadOnlyAccessGraphSettings(ctx); { + case err != nil && !trace.IsNotFound(err): + return nil, trace.Wrap(err) + case err == nil: + sshScanEnabled = obj.SecretsScanConfig() == clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED + } + return &clusterconfigpb.GetClusterAccessGraphConfigResponse{ AccessGraph: &clusterconfigpb.AccessGraphConfig{ Enabled: s.accessGraph.Enabled, Address: s.accessGraph.Address, Ca: s.accessGraph.CA, Insecure: s.accessGraph.Insecure, + SecretsScanConfig: &clusterconfigpb.AccessGraphSecretsScanConfiguration{ + SshScanEnabled: sshScanEnabled, + }, }, }, nil } + +func (s *Service) GetAccessGraphSettings(ctx context.Context, _ *clusterconfigpb.GetAccessGraphSettingsRequest) (*clusterconfigpb.AccessGraphSettings, error) { + authzCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.CheckAccessToKind(types.KindAccessGraphSettings, types.VerbRead); err != nil { + return nil, trace.Wrap(err) + } + + cfg, err := s.readOnlyCache.GetReadOnlyAccessGraphSettings(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + return cfg.Clone(), nil +} + +func (s *Service) CreateAccessGraphSettings(ctx context.Context, req *clusterconfigpb.CreateAccessGraphSettingsRequest) (*clusterconfigpb.AccessGraphSettings, error) { + authzCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.CheckAccessToKind(types.KindAccessGraphSettings, types.VerbCreate); err != nil { + return nil, trace.Wrap(err) + } + + if !authz.HasBuiltinRole(*authzCtx, string(types.RoleAuth)) { + return nil, trace.AccessDenied("this request can be only executed by an auth server") + } + + cfg := req.GetAccessGraphSettings() + if err := clusterconfig.ValidateAccessGraphSettings(cfg); err != nil { + return nil, trace.Wrap(err) + } + + created, err := s.backend.CreateAccessGraphSettings(ctx, cfg) + if auditErr := s.emitter.EmitAuditEvent(ctx, &apievents.AccessGraphSettingsUpdate{ + Metadata: apievents.Metadata{ + Type: events.AccessGraphSettingsUpdateEvent, + Code: events.AccessGraphSettingsUpdateCode, + }, + UserMetadata: authzCtx.GetUserMetadata(), + ConnectionMetadata: authz.ConnectionMetadata(ctx), + Status: eventStatus(err), + }); auditErr != nil { + slog.WarnContext(ctx, "Failed to emit AccessGraphSettings update event.", "error", auditErr) + } + + // don't handle the update error until after we emit an audit event + if err != nil { + return nil, trace.Wrap(err) + } + + return created, nil +} + +func (s *Service) UpdateAccessGraphSettings(ctx context.Context, req *clusterconfigpb.UpdateAccessGraphSettingsRequest) (*clusterconfigpb.AccessGraphSettings, error) { + authzCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.CheckAccessToKind(types.KindAccessGraphSettings, types.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + if !modules.GetModules().Features().GetEntitlement(entitlements.Policy).Enabled && !modules.GetModules().Features().AccessGraph { + return nil, trace.AccessDenied("access graph is feature isn't enabled") + } + + cfg := req.GetAccessGraphSettings() + if err := clusterconfig.ValidateAccessGraphSettings(cfg); err != nil { + return nil, trace.Wrap(err) + } + + rsp, err := s.backend.UpdateAccessGraphSettings(ctx, cfg) + + if auditErr := s.emitter.EmitAuditEvent(ctx, &apievents.AccessGraphSettingsUpdate{ + Metadata: apievents.Metadata{ + Type: events.AccessGraphSettingsUpdateEvent, + Code: events.AccessGraphSettingsUpdateCode, + }, + UserMetadata: authzCtx.GetUserMetadata(), + ConnectionMetadata: authz.ConnectionMetadata(ctx), + Status: eventStatus(err), + }); auditErr != nil { + slog.WarnContext(ctx, "Failed to emit AccessGraphSettings update event.", "error", auditErr) + } + + if err != nil { + return nil, trace.Wrap(err) + } + + return rsp, nil +} + +func (s *Service) UpsertAccessGraphSettings(ctx context.Context, req *clusterconfigpb.UpsertAccessGraphSettingsRequest) (*clusterconfigpb.AccessGraphSettings, error) { + authzCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.CheckAccessToKind(types.KindAccessGraphSettings, types.VerbCreate, types.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + if !modules.GetModules().Features().GetEntitlement(entitlements.Policy).Enabled && !modules.GetModules().Features().AccessGraph { + return nil, trace.AccessDenied("access graph is feature isn't enabled") + } + + cfg := req.GetAccessGraphSettings() + if err := clusterconfig.ValidateAccessGraphSettings(cfg); err != nil { + return nil, trace.Wrap(err) + } + + rsp, err := s.backend.UpsertAccessGraphSettings(ctx, cfg) + + if auditErr := s.emitter.EmitAuditEvent(ctx, &apievents.AccessGraphSettingsUpdate{ + Metadata: apievents.Metadata{ + Type: events.AccessGraphSettingsUpdateEvent, + Code: events.AccessGraphSettingsUpdateCode, + }, + UserMetadata: authzCtx.GetUserMetadata(), + ConnectionMetadata: authz.ConnectionMetadata(ctx), + Status: eventStatus(err), + }); auditErr != nil { + slog.WarnContext(ctx, "Failed to emit AccessGraphSettings update event.", "error", auditErr) + } + + if err != nil { + return nil, trace.Wrap(err) + } + + return rsp, nil +} + +func (s *Service) ResetAccessGraphSettings(ctx context.Context, _ *clusterconfigpb.ResetAccessGraphSettingsRequest) (*clusterconfigpb.AccessGraphSettings, error) { + authzCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.CheckAccessToKind(types.KindAccessGraphSettings, types.VerbUpdate); err != nil { + return nil, trace.Wrap(err) + } + + if err := authzCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + if !modules.GetModules().Features().GetEntitlement(entitlements.Policy).Enabled && !modules.GetModules().Features().AccessGraph { + return nil, trace.AccessDenied("access graph is feature isn't enabled") + } + + obj, err := clusterconfig.NewAccessGraphSettings(&clusterconfigpb.AccessGraphSettingsSpec{ + SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED, + }) + if err != nil { + return nil, trace.Wrap(err) + } + rsp, err := s.backend.UpsertAccessGraphSettings(ctx, obj) + + if auditErr := s.emitter.EmitAuditEvent(ctx, &apievents.AccessGraphSettingsUpdate{ + Metadata: apievents.Metadata{ + Type: events.AccessGraphSettingsUpdateEvent, + Code: events.AccessGraphSettingsUpdateCode, + }, + UserMetadata: authzCtx.GetUserMetadata(), + ConnectionMetadata: authz.ConnectionMetadata(ctx), + Status: eventStatus(err), + }); auditErr != nil { + slog.WarnContext(ctx, "Failed to emit AccessGraphSettings update event.", "error", auditErr) + } + + if err != nil { + return nil, trace.Wrap(err) + } + + return rsp, nil +} diff --git a/lib/auth/clusterconfig/clusterconfigv1/service_test.go b/lib/auth/clusterconfig/clusterconfigv1/service_test.go index 196f3a3c4d22c..02cb3821646fb 100644 --- a/lib/auth/clusterconfig/clusterconfigv1/service_test.go +++ b/lib/auth/clusterconfig/clusterconfigv1/service_test.go @@ -32,6 +32,7 @@ import ( "github.com/gravitational/teleport/api/constants" 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" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib/auth/clusterconfig/clusterconfigv1" @@ -78,7 +79,7 @@ func TestCreateAuthPreference(t *testing.T) { return authRoleContext, nil }), assertion: func(t *testing.T, created types.AuthPreference, err error) { - require.NoError(t, err, "got (%v), expected auth role to create auth preference", err) + require.NoError(t, err, "got (%v), expected auth role to create auth mutator", err) require.NotNil(t, created) }, }, @@ -107,7 +108,7 @@ func TestCreateAuthPreference(t *testing.T) { pp.Spec.RequireMFAType = types.RequireMFAType_HARDWARE_KEY_PIN }, assertion: func(t *testing.T, created types.AuthPreference, err error) { - require.NoError(t, err, "got (%v), expected auth role to create auth preference", err) + require.NoError(t, err, "got (%v), expected auth role to create auth mutator", err) require.NotNil(t, created) }, }, @@ -1674,13 +1675,14 @@ func (f fakeChecker) CheckAccessToRule(context services.RuleContext, namespace s } type envConfig struct { - authorizer authz.Authorizer - emitter apievents.Emitter - defaultAuthPreference types.AuthPreference - defaultNetworkingConfig types.ClusterNetworkingConfig - defaultRecordingConfig types.SessionRecordingConfig - service services.ClusterConfiguration - accessGraphConfig clusterconfigv1.AccessGraphConfig + authorizer authz.Authorizer + emitter apievents.Emitter + defaultAuthPreference types.AuthPreference + defaultNetworkingConfig types.ClusterNetworkingConfig + defaultRecordingConfig types.SessionRecordingConfig + service services.ClusterConfiguration + accessGraphConfig clusterconfigv1.AccessGraphConfig + defaultAccessGraphSettings *clusterconfigpb.AccessGraphSettings } type serviceOpt = func(config *envConfig) @@ -1720,12 +1722,19 @@ func withAccessGraphConfig(cfg clusterconfigv1.AccessGraphConfig) serviceOpt { } } +func withAccessGraphSettings(cfg *clusterconfigpb.AccessGraphSettings) serviceOpt { + return func(config *envConfig) { + config.defaultAccessGraphSettings = cfg + } +} + type env struct { *clusterconfigv1.Service - emitter *eventstest.ChannelEmitter - defaultPreference types.AuthPreference - defaultNetworkingConfig types.ClusterNetworkingConfig - defaultRecordingConfig types.SessionRecordingConfig + emitter *eventstest.ChannelEmitter + defaultPreference types.AuthPreference + defaultNetworkingConfig types.ClusterNetworkingConfig + defaultRecordingConfig types.SessionRecordingConfig + defaultAccessGraphSettings *clusterconfigpb.AccessGraphSettings } func newTestEnv(opts ...serviceOpt) (*env, error) { @@ -1764,7 +1773,7 @@ func newTestEnv(opts ...serviceOpt) (*env, error) { if cfg.defaultAuthPreference != nil { defaultPreference, err = cfg.service.CreateAuthPreference(ctx, cfg.defaultAuthPreference) if err != nil { - return nil, trace.Wrap(err, "creating default auth preference") + return nil, trace.Wrap(err, "creating default auth mutator") } } @@ -1784,16 +1793,33 @@ func newTestEnv(opts ...serviceOpt) (*env, error) { } } + var defaultAccessGraphSettings *clusterconfigpb.AccessGraphSettings + if cfg.defaultAccessGraphSettings != nil { + defaultAccessGraphSettings, err = cfg.service.CreateAccessGraphSettings(ctx, cfg.defaultAccessGraphSettings) + if err != nil { + return nil, trace.Wrap(err, "creating access graph settings") + } + } + return &env{ - Service: svc, - defaultPreference: defaultPreference, - defaultNetworkingConfig: defaultNetworkingConfig, - defaultRecordingConfig: defaultSessionRecordingConfig, - emitter: emitter, + Service: svc, + defaultPreference: defaultPreference, + defaultNetworkingConfig: defaultNetworkingConfig, + defaultRecordingConfig: defaultSessionRecordingConfig, + defaultAccessGraphSettings: defaultAccessGraphSettings, + emitter: emitter, }, nil } func TestGetAccessGraphConfig(t *testing.T) { + + settings, err := clusterconfig.NewAccessGraphSettings( + &clusterconfigpb.AccessGraphSettingsSpec{ + SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED, + }, + ) + require.NoError(t, err) + cfgEnabled := clusterconfigv1.AccessGraphConfig{ Enabled: true, Address: "address", @@ -1801,12 +1827,13 @@ func TestGetAccessGraphConfig(t *testing.T) { Insecure: true, } cases := []struct { - name string - accessGraphConfig clusterconfigv1.AccessGraphConfig - role types.SystemRole - testSetup func(*testing.T) - errorAssertion require.ErrorAssertionFunc - responseAssertion *clusterconfigpb.GetClusterAccessGraphConfigResponse + name string + accessGraphConfig clusterconfigv1.AccessGraphConfig + role types.SystemRole + testSetup func(*testing.T) + errorAssertion require.ErrorAssertionFunc + responseAssertion *clusterconfigpb.GetClusterAccessGraphConfigResponse + accessGraphSettings *clusterconfigpb.AccessGraphSettings }{ { name: "authorized proxy with non empty access graph config; Policy module is disabled", @@ -1837,10 +1864,11 @@ func TestGetAccessGraphConfig(t *testing.T) { errorAssertion: require.NoError, responseAssertion: &clusterconfigpb.GetClusterAccessGraphConfigResponse{ AccessGraph: &clusterconfigpb.AccessGraphConfig{ - Enabled: true, - Insecure: true, - Address: "address", - Ca: []byte("ca"), + Enabled: true, + Insecure: true, + Address: "address", + Ca: []byte("ca"), + SecretsScanConfig: &clusterconfigpb.AccessGraphSecretsScanConfiguration{}, }, }, }, @@ -1859,12 +1887,41 @@ func TestGetAccessGraphConfig(t *testing.T) { }, accessGraphConfig: cfgEnabled, errorAssertion: require.NoError, + responseAssertion: &clusterconfigpb.GetClusterAccessGraphConfigResponse{ + AccessGraph: &clusterconfigpb.AccessGraphConfig{ + Enabled: true, + Insecure: true, + Address: "address", + Ca: []byte("ca"), + SecretsScanConfig: &clusterconfigpb.AccessGraphSecretsScanConfiguration{}, + }, + }, + }, + { + name: "Policy module is enabled with secrets scan option", + role: types.RoleDiscovery, + testSetup: func(t *testing.T) { + m := modules.TestModules{ + TestFeatures: modules.Features{ + Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{ + entitlements.Policy: {Enabled: true}, + }, + }, + } + modules.SetTestModules(t, &m) + }, + accessGraphConfig: cfgEnabled, + accessGraphSettings: settings, + errorAssertion: require.NoError, responseAssertion: &clusterconfigpb.GetClusterAccessGraphConfigResponse{ AccessGraph: &clusterconfigpb.AccessGraphConfig{ Enabled: true, Insecure: true, Address: "address", Ca: []byte("ca"), + SecretsScanConfig: &clusterconfigpb.AccessGraphSecretsScanConfiguration{ + SshScanEnabled: true, + }, }, }, }, @@ -1882,7 +1939,8 @@ func TestGetAccessGraphConfig(t *testing.T) { authorizer := authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { return authRoleContext, nil }) - env, err := newTestEnv(withAuthorizer(authorizer), withAccessGraphConfig(test.accessGraphConfig)) + + env, err := newTestEnv(withAuthorizer(authorizer), withAccessGraphConfig(test.accessGraphConfig), withAccessGraphSettings(test.accessGraphSettings)) require.NoError(t, err, "creating test service") got, err := env.GetClusterAccessGraphConfig(context.Background(), &clusterconfigpb.GetClusterAccessGraphConfigRequest{}) @@ -1892,3 +1950,379 @@ func TestGetAccessGraphConfig(t *testing.T) { }) } } + +func TestGetAccessGraphSettings(t *testing.T) { + cases := []struct { + name string + authorizer authz.Authorizer + assertion func(t *testing.T, err error) + }{ + { + name: "unauthorized", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{}, + }, nil + }), + assertion: func(t *testing.T, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), expected unauthorized user to be prevented from getting access graph settings", err) + }, + }, { + name: "authorized", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbRead}}, + }, + }, nil + }), + assertion: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + settings, err := clusterconfig.NewAccessGraphSettings( + &clusterconfigpb.AccessGraphSettingsSpec{ + SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED, + }, + ) + require.NoError(t, err) + env, err := newTestEnv(withAuthorizer(test.authorizer), withAccessGraphSettings(settings)) + require.NoError(t, err, "creating test service") + + got, err := env.GetAccessGraphSettings(context.Background(), &clusterconfigpb.GetAccessGraphSettingsRequest{}) + test.assertion(t, err) + if err == nil { + require.Empty(t, cmp.Diff(settings, got, cmpopts.IgnoreFields(types.Metadata{}, "Revision"), protocmp.Transform())) + } + }) + } +} + +func TestUpdateAccessGraphSettings(t *testing.T) { + cases := []struct { + name string + mutator func(p *clusterconfigpb.AccessGraphSettings) + authorizer authz.Authorizer + testSetup func(*testing.T) + assertion func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) + }{ + { + name: "unauthorized", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{}, + }, nil + }), + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), expected unauthorized user to prevent updating access graph settings", err) + }, + }, + { + name: "no admin action", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate}}, + }, + }, nil + }), + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), expected lack of admin action to prevent updating access graph settings", err) + }, + }, + { + name: "update without access graph being enabled", + + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate}}, + }, + AdminActionAuthState: authz.AdminActionAuthMFAVerified, + Identity: authz.LocalUser{ + Username: "llama", + Identity: tlsca.Identity{Username: "llama"}, + }, + }, nil + }), + mutator: func(p *clusterconfigpb.AccessGraphSettings) { + p.Spec.SecretsScanConfig = clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED + }, + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.Error(t, err) + }, + }, + { + name: "updated", + testSetup: func(t *testing.T) { + m := modules.TestModules{ + TestFeatures: modules.Features{ + Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{ + entitlements.Policy: {Enabled: true}, + }, + }, + } + modules.SetTestModules(t, &m) + }, + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate}}, + }, + AdminActionAuthState: authz.AdminActionAuthMFAVerified, + Identity: authz.LocalUser{ + Username: "llama", + Identity: tlsca.Identity{Username: "llama"}, + }, + }, nil + }), + mutator: func(p *clusterconfigpb.AccessGraphSettings) { + p.Spec.SecretsScanConfig = clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED + }, + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.NoError(t, err) + require.Equal(t, clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED, updated.GetSpec().GetSecretsScanConfig()) + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + if test.testSetup != nil { + test.testSetup(t) + } + settings, err := clusterconfig.NewAccessGraphSettings( + &clusterconfigpb.AccessGraphSettingsSpec{ + SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED, + }, + ) + require.NoError(t, err) + env, err := newTestEnv(withAuthorizer(test.authorizer), withAccessGraphSettings(settings)) + require.NoError(t, err, "creating test service") + + // Set revisions to allow the update to succeed. + pref := env.defaultAccessGraphSettings + if test.mutator != nil { + test.mutator(pref) + } + + updated, err := env.UpdateAccessGraphSettings(context.Background(), &clusterconfigpb.UpdateAccessGraphSettingsRequest{AccessGraphSettings: pref}) + test.assertion(t, updated, err) + }) + } +} + +func TestUpsertAccessGraphSettings(t *testing.T) { + cases := []struct { + name string + testSetup func(*testing.T) + mutator func(p *clusterconfigpb.AccessGraphSettings) + authorizer authz.Authorizer + assertion func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) + }{ + { + name: "unauthorized", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{}, + }, nil + }), + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), expected unauthorized user to prevent upserting access graph settings", err) + }, + }, + { + name: "access prevented", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate}}, + }, + AdminActionAuthState: authz.AdminActionAuthUnauthorized, + }, nil + }), + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), expected lack of admin action to prevent upserting access graph settings", err) + }, + }, + { + name: "no admin action", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbCreate, types.VerbUpdate}}, + }, + AdminActionAuthState: authz.AdminActionAuthUnauthorized, + }, nil + }), + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), expected lack of admin action to prevent upserting access graph settings", err) + }, + }, + { + name: "policy not enabled", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbCreate, types.VerbUpdate}}, + }, + AdminActionAuthState: authz.AdminActionAuthMFAVerified, + }, nil + }), + mutator: func(p *clusterconfigpb.AccessGraphSettings) { + + }, + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.True(t, trace.IsAccessDenied(err), "got (%v), upserting access graph settings must fail when policy isn't enabled", err) + }, + }, + + { + name: "upserted", + testSetup: func(t *testing.T) { + m := modules.TestModules{ + TestFeatures: modules.Features{ + Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{ + entitlements.Policy: {Enabled: true}, + }, + }, + } + modules.SetTestModules(t, &m) + }, + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate, types.VerbCreate}}, + }, + AdminActionAuthState: authz.AdminActionAuthMFAVerified, + Identity: authz.LocalUser{ + Username: "llama", + Identity: tlsca.Identity{Username: "llama"}, + }, + }, nil + }), + mutator: func(p *clusterconfigpb.AccessGraphSettings) { + p.Spec.SecretsScanConfig = clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED + }, + assertion: func(t *testing.T, updated *clusterconfigpb.AccessGraphSettings, err error) { + require.NoError(t, err) + require.Equal(t, clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_ENABLED, updated.Spec.SecretsScanConfig) + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + if test.testSetup != nil { + test.testSetup(t) + } + settings, err := clusterconfig.NewAccessGraphSettings( + &clusterconfigpb.AccessGraphSettingsSpec{ + SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED, + }) + + require.NoError(t, err) + + env, err := newTestEnv(withAuthorizer(test.authorizer), withAccessGraphSettings(settings)) + require.NoError(t, err, "creating test service") + + // Discard revisions to allow the update to succeed. + pref := settings + if test.mutator != nil { + test.mutator(pref) + } + + updated, err := env.UpsertAccessGraphSettings(context.Background(), &clusterconfigpb.UpsertAccessGraphSettingsRequest{AccessGraphSettings: pref}) + test.assertion(t, updated, err) + }) + } +} + +func TestResetAccessGraphSettings(t *testing.T) { + cases := []struct { + name string + authorizer authz.Authorizer + testSetup func(*testing.T) + assertion func(t *testing.T, reset *clusterconfigpb.AccessGraphSettings, err error) + }{ + { + name: "unauthorized", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{}, + }, nil + }), + assertion: func(t *testing.T, reset *clusterconfigpb.AccessGraphSettings, err error) { + assert.Nil(t, reset) + require.True(t, trace.IsAccessDenied(err), "got (%v), expected unauthorized user to prevent resetting access graph settings", err) + }, + }, + { + name: "no admin action", + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate}}, + }, + }, nil + }), + assertion: func(t *testing.T, reset *clusterconfigpb.AccessGraphSettings, err error) { + assert.Nil(t, reset) + require.True(t, trace.IsAccessDenied(err), "got (%v), expected lack of admin action to prevent resetting access graph settings", err) + }, + }, + { + name: "reset", + testSetup: func(t *testing.T) { + m := modules.TestModules{ + TestFeatures: modules.Features{ + Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{ + entitlements.Policy: {Enabled: true}, + }, + }, + } + modules.SetTestModules(t, &m) + }, + authorizer: authz.AuthorizerFunc(func(ctx context.Context) (*authz.Context, error) { + return &authz.Context{ + Checker: fakeChecker{ + rules: map[string][]string{types.KindAccessGraphSettings: {types.VerbUpdate, types.VerbCreate}}, + }, + AdminActionAuthState: authz.AdminActionAuthMFAVerified, + Identity: authz.LocalUser{ + Username: "llama", + Identity: tlsca.Identity{Username: "llama"}, + }, + }, nil + }), + assertion: func(t *testing.T, reset *clusterconfigpb.AccessGraphSettings, err error) { + require.NoError(t, err) + require.Equal(t, clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED, reset.GetSpec().GetSecretsScanConfig()) + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + if test.testSetup != nil { + test.testSetup(t) + } + settings, err := clusterconfig.NewAccessGraphSettings( + &clusterconfigpb.AccessGraphSettingsSpec{ + SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED, + }) + + require.NoError(t, err) + + env, err := newTestEnv(withAuthorizer(test.authorizer), withAccessGraphSettings(settings)) + require.NoError(t, err, "creating test service") + + reset, err := env.ResetAccessGraphSettings(context.Background(), &clusterconfigpb.ResetAccessGraphSettingsRequest{}) + test.assertion(t, reset, err) + }) + } +} diff --git a/lib/auth/grpcserver_test.go b/lib/auth/grpcserver_test.go index a96ad7abd9865..1689965c686ec 100644 --- a/lib/auth/grpcserver_test.go +++ b/lib/auth/grpcserver_test.go @@ -4482,9 +4482,10 @@ func TestGetAccessGraphConfig(t *testing.T) { user, _, err := CreateUserAndRole(server.Auth(), "test", []string{"role"}, nil) require.NoError(t, err) positiveResponse := &clusterconfigpb.AccessGraphConfig{ - Enabled: true, - Ca: []byte("ca"), - Address: "addr", + Enabled: true, + Ca: []byte("ca"), + Address: "addr", + SecretsScanConfig: &clusterconfigpb.AccessGraphSecretsScanConfiguration{}, } tests := []struct {