diff --git a/pkg/apiserver/auditing/noop.go b/pkg/apiserver/auditing/noop.go index 5ab8f902c19..5a6b39a3b71 100644 --- a/pkg/apiserver/auditing/noop.go +++ b/pkg/apiserver/auditing/noop.go @@ -19,11 +19,18 @@ func (NoopBackend) Shutdown() {} func (NoopBackend) String() string { return "" } +// NoopPolicyRuleProvider is a no-op implementation of PolicyRuleProvider +type NoopPolicyRuleProvider struct{} + +func ProvideNoopPolicyRuleProvider() PolicyRuleProvider { return &NoopPolicyRuleProvider{} } + +func (NoopPolicyRuleProvider) PolicyRuleProvider(PolicyRuleEvaluators) audit.PolicyRuleEvaluator { + return NoopPolicyRuleEvaluator{} +} + // NoopPolicyRuleEvaluator is a no-op implementation of audit.PolicyRuleEvaluator type NoopPolicyRuleEvaluator struct{} -func ProvideNoopPolicyRuleEvaluator() audit.PolicyRuleEvaluator { return &NoopPolicyRuleEvaluator{} } - func (NoopPolicyRuleEvaluator) EvaluatePolicyRule(authorizer.Attributes) audit.RequestAuditConfig { return audit.RequestAuditConfig{Level: auditinternal.LevelNone} } diff --git a/pkg/apiserver/auditing/policy.go b/pkg/apiserver/auditing/policy.go new file mode 100644 index 00000000000..e88acf7c4cc --- /dev/null +++ b/pkg/apiserver/auditing/policy.go @@ -0,0 +1,59 @@ +package auditing + +import ( + "slices" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "k8s.io/apimachinery/pkg/runtime/schema" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +// PolicyRuleEvaluators is a map of API group+version to audit.PolicyRuleEvaluator +type PolicyRuleEvaluators = map[schema.GroupVersion]audit.PolicyRuleEvaluator + +type PolicyRuleProvider interface { + PolicyRuleProvider(evaluators PolicyRuleEvaluators) audit.PolicyRuleEvaluator +} + +// PolicyRuleEvaluator alias for easier imports. +type PolicyRuleEvaluator = audit.PolicyRuleEvaluator + +// DefaultGrafanaPolicyRuleEvaluator provides a sane default configuration for audit logging for API group+versions. +type defaultGrafanaPolicyRuleEvaluator struct{} + +var _ PolicyRuleEvaluator = &defaultGrafanaPolicyRuleEvaluator{} + +func NewDefaultGrafanaPolicyRuleEvaluator() audit.PolicyRuleEvaluator { + return defaultGrafanaPolicyRuleEvaluator{} +} + +func (defaultGrafanaPolicyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) audit.RequestAuditConfig { + // Skip non-resource and watch requests otherwise it is too noisy. + if !attrs.IsResourceRequest() || attrs.GetVerb() == utils.VerbWatch { + return audit.RequestAuditConfig{ + Level: auditinternal.LevelNone, + } + } + + // Skip auditing if the user is part of the privileged group. + // The loopback client uses this group, so requests initiated in `/api/` would be duplicated. + if u := attrs.GetUser(); u != nil && slices.Contains(u.GetGroups(), user.SystemPrivilegedGroup) { + return audit.RequestAuditConfig{ + Level: auditinternal.LevelNone, + } + } + + return audit.RequestAuditConfig{ + Level: auditinternal.LevelMetadata, + OmitStages: []auditinternal.Stage{ + // Only log on StageResponseComplete + auditinternal.StageRequestReceived, + auditinternal.StageResponseStarted, + auditinternal.StagePanic, + }, + OmitManagedFields: false, // Setting it to true causes extra copying/unmarshalling. + } +} diff --git a/pkg/apiserver/auditing/policy_test.go b/pkg/apiserver/auditing/policy_test.go new file mode 100644 index 00000000000..af18f9110fd --- /dev/null +++ b/pkg/apiserver/auditing/policy_test.go @@ -0,0 +1,73 @@ +package auditing_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apiserver/auditing" + "github.com/stretchr/testify/require" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestDefaultGrafanaPolicyRuleEvaluator(t *testing.T) { + t.Parallel() + + evaluator := auditing.NewDefaultGrafanaPolicyRuleEvaluator() + require.NotNil(t, evaluator) + + t.Run("returns audit level none for non-resource requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: false, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelNone, config.Level) + }) + + t.Run("returns audit level none for watch requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbWatch, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelNone, config.Level) + }) + + t.Run("returns audit level none for requests from privileged group", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbCreate, + User: &user.DefaultInfo{ + Groups: []string{"test-group", user.SystemPrivilegedGroup}, + }, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelNone, config.Level) + }) + + t.Run("return audit level metadata for other resource requests", func(t *testing.T) { + t.Parallel() + + attrs := authorizer.AttributesRecord{ + ResourceRequest: true, + Verb: utils.VerbCreate, + User: &user.DefaultInfo{ + Name: "test-user", + Groups: []string{"test-group"}, + }, + } + + config := evaluator.EvaluatePolicyRule(attrs) + require.Equal(t, auditinternal.LevelMetadata, config.Level) + }) +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 740f2a46cef..df38965759b 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -37,7 +37,7 @@ var WireSetExts = wire.NewSet( // Auditing Options auditing.ProvideNoopBackend, - auditing.ProvideNoopPolicyRuleEvaluator, + auditing.ProvideNoopPolicyRuleProvider, ) var provisioningExtras = wire.NewSet( diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index cdc3371db11..6e068337a29 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -834,8 +834,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) backend := auditing.ProvideNoopBackend() - policyRuleEvaluator := auditing.ProvideNoopPolicyRuleEvaluator() - apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleEvaluator) + policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider() + apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleProvider) if err != nil { return nil, err } @@ -1495,8 +1495,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) backend := auditing.ProvideNoopBackend() - policyRuleEvaluator := auditing.ProvideNoopPolicyRuleEvaluator() - apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleEvaluator) + policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider() + apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics, backend, policyRuleProvider) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/builder/common.go b/pkg/services/apiserver/builder/common.go index bebbad8e8a6..e5e46a3340d 100644 --- a/pkg/services/apiserver/builder/common.go +++ b/pkg/services/apiserver/builder/common.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" @@ -59,6 +60,13 @@ type APIGroupAuthorizer interface { GetAuthorizer() authorizer.Authorizer } +// APIGroupAuditor allows different API groups to opt-in and provide their own auditing policy evaluator function. +// Auditing is only enabled if this is implemented. If no customization is needed, you can use the default evaluator, +// `pkg/apiserver/auditing.NewDefaultGrafanaPolicyRuleEvaluator()`. +type APIGroupAuditor interface { + GetPolicyRuleEvaluator() audit.PolicyRuleEvaluator +} + type APIGroupMutation interface { // Mutate allows the builder to make changes to the object before it is persisted. // Context is used only for timeout/deadline/cancellation and tracing information. diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index a76a01dffba..c535443a91e 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -29,6 +29,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/common" + "github.com/grafana/grafana/pkg/apiserver/auditing" "github.com/grafana/grafana/pkg/apiserver/endpoints/filters" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -497,6 +498,32 @@ func AddPostStartHooks( return nil } +func EvaluatorPolicyRuleFromBuilders(builders []APIGroupBuilder) auditing.PolicyRuleEvaluators { + policyRuleEvaluators := make(auditing.PolicyRuleEvaluators, 0) + + for _, b := range builders { + auditor, ok := b.(APIGroupAuditor) + if !ok { + continue + } + + policyRuleEvaluator := auditor.GetPolicyRuleEvaluator() + if policyRuleEvaluator == nil { + continue + } + + for _, gv := range GetGroupVersions(b) { + if gv.Empty() { + continue + } + + policyRuleEvaluators[gv] = policyRuleEvaluator + } + } + + return policyRuleEvaluators +} + func allowRegisteringResourceByInfo(allowedResources []string, name string) bool { // trim any subresources from the name name = strings.Split(name, "/")[0] diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 5d9e37e649e..6c92350ec2a 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -28,6 +28,7 @@ import ( dataplaneaggregator "github.com/grafana/grafana/pkg/aggregator/apiserver" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apiserver/auditing" grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" @@ -115,8 +116,8 @@ type service struct { builderMetrics *builder.BuilderMetrics dualWriterMetrics *grafanarest.DualWriterMetrics - auditBackend audit.Backend - auditPolicyRuleEvaluator audit.PolicyRuleEvaluator + auditBackend audit.Backend + auditPolicyRuleProvider auditing.PolicyRuleProvider } func ProvideService( @@ -142,7 +143,7 @@ func ProvideService( appInstallers []appsdkapiserver.AppInstaller, builderMetrics *builder.BuilderMetrics, auditBackend audit.Backend, - auditPolicyRuleEvaluator audit.PolicyRuleEvaluator, + auditPolicyRuleProvider auditing.PolicyRuleProvider, ) (*service, error) { scheme := builder.ProvideScheme() codecs := builder.ProvideCodecFactory(scheme) @@ -174,7 +175,7 @@ func ProvideService( builderMetrics: builderMetrics, dualWriterMetrics: grafanarest.NewDualWriterMetrics(reg), auditBackend: auditBackend, - auditPolicyRuleEvaluator: auditPolicyRuleEvaluator, + auditPolicyRuleProvider: auditPolicyRuleProvider, } // This will be used when running as a dskit service s.NamedService = services.NewBasicService(s.start, s.running, nil).WithName(modules.GrafanaAPIServer) @@ -365,7 +366,7 @@ func (s *service) start(ctx context.Context) error { // Auditing Options serverConfig.AuditBackend = s.auditBackend - serverConfig.AuditPolicyRuleEvaluator = s.auditPolicyRuleEvaluator + serverConfig.AuditPolicyRuleEvaluator = s.auditPolicyRuleProvider.PolicyRuleProvider(builder.EvaluatorPolicyRuleFromBuilders(s.builders)) // Add OpenAPI specs for each group+version (existing builders) err = builder.SetupConfig(