Auditing: Refactor policy rule provider and add default policy rule evaluator (#115318)

* Auditing: Add policy rule provider to fix wiring

* Auditing: Add default policy rule evaluator for APIs
This commit is contained in:
Matheus Macabu
2025-12-16 10:36:46 +01:00
committed by GitHub
parent a5f52fb40d
commit 409a1d88f1
8 changed files with 187 additions and 12 deletions
+9 -2
View File
@@ -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}
}
+59
View File
@@ -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.
}
}
+73
View File
@@ -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)
})
}
+1 -1
View File
@@ -37,7 +37,7 @@ var WireSetExts = wire.NewSet(
// Auditing Options
auditing.ProvideNoopBackend,
auditing.ProvideNoopPolicyRuleEvaluator,
auditing.ProvideNoopPolicyRuleProvider,
)
var provisioningExtras = wire.NewSet(
+4 -4
View File
@@ -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
}
+8
View File
@@ -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.
+27
View File
@@ -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]
+6 -5
View File
@@ -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(