From 33d2d0a12db1c30ef7a6017e81654ba8162487ac Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 19 Dec 2023 09:12:35 -0800 Subject: [PATCH] K8s: Refactor authorization initialization (#79670) --- .../feature-toggles/index.md | 28 --------- pkg/registry/apis/example/register.go | 20 ++++++ pkg/registry/apis/playlist/register.go | 5 ++ .../{impersonation => }/impersonation.go | 8 +-- .../{impersonation => }/impersonation_test.go | 6 +- .../auth/authorizer/org/wireset.go | 8 --- .../auth/authorizer/{org => }/org_id.go | 12 ++-- .../auth/authorizer/{org => }/org_role.go | 12 ++-- .../auth/authorizer/provider.go | 61 ++++++++++++++----- .../auth/authorizer/stack/wireset.go | 7 --- .../auth/authorizer/{stack => }/stack_id.go | 12 ++-- .../auth/authorizer/wireset.go | 14 ----- pkg/services/grafana-apiserver/common.go | 6 ++ pkg/services/grafana-apiserver/service.go | 15 +++-- pkg/services/grafana-apiserver/wireset.go | 3 - 15 files changed, 112 insertions(+), 105 deletions(-) rename pkg/services/grafana-apiserver/auth/authorizer/{impersonation => }/impersonation.go (67%) rename pkg/services/grafana-apiserver/auth/authorizer/{impersonation => }/impersonation_test.go (84%) delete mode 100644 pkg/services/grafana-apiserver/auth/authorizer/org/wireset.go rename pkg/services/grafana-apiserver/auth/authorizer/{org => }/org_id.go (87%) rename pkg/services/grafana-apiserver/auth/authorizer/{org => }/org_role.go (83%) delete mode 100644 pkg/services/grafana-apiserver/auth/authorizer/stack/wireset.go rename pkg/services/grafana-apiserver/auth/authorizer/{stack => }/stack_id.go (84%) delete mode 100644 pkg/services/grafana-apiserver/auth/authorizer/wireset.go diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index ea92b0e3b70..9c33e536834 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -179,31 +179,3 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref | `externalServiceAccounts` | Automatic service account and token setup for plugins | | `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 | | `ssoSettingsApi` | Enables the SSO settings API | - -## Configure feature management - -**Feature toggles** is an Administration page that allows authorized users to view and toggle the feature flags available in their Grafana instance. For more information, refer to [Feature toggles](https://grafana.com/docs/grafana//administration/feature-toggles/). - -By default, feature toggles are in read-only mode. -Granting admin users the ability to alter the states of feature toggles requires configuring Grafana with the optional [`allow_editing`](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#allow_editing), [`update_webhook`](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#update_webhook) and [`update_webhook_token`](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#update_webhook_token) settings. - -Those configurations allow the Grafana operator to set up a webhook that Grafana must call to propagate the configuration change. - -**Example request**: - -```http -POST $update_webhook -Accept: application/json -Content-Type: application/json -Authorization: Bearer $update_webhook_token - -{ - "feature_toggles": [ - { - "featureToggle1": "true", - "featureToggle2": "false", - } - ], - "user": "admin@example.test", -} -``` diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go index 298084b33d9..ef6cccc4b99 100644 --- a/pkg/registry/apis/example/register.go +++ b/pkg/registry/apis/example/register.go @@ -1,6 +1,7 @@ package example import ( + "context" "fmt" "net/http" @@ -9,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" @@ -19,6 +21,7 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/featuremgmt" grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" ) @@ -195,3 +198,20 @@ func (b *TestingAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes { }, } } + +func (b *TestingAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // require a user + _, err = appcontext.User(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + return authorizer.DecisionNoOpinion, "", err // fallback to org/role logic + }) +} diff --git a/pkg/registry/apis/playlist/register.go b/pkg/registry/apis/playlist/register.go index be4c544227b..b1dab9754f0 100644 --- a/pkg/registry/apis/playlist/register.go +++ b/pkg/registry/apis/playlist/register.go @@ -8,6 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" @@ -130,3 +131,7 @@ func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinition func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes { return nil // no custom API routes } + +func (b *PlaylistAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return nil // default authorizer is fine +} diff --git a/pkg/services/grafana-apiserver/auth/authorizer/impersonation/impersonation.go b/pkg/services/grafana-apiserver/auth/authorizer/impersonation.go similarity index 67% rename from pkg/services/grafana-apiserver/auth/authorizer/impersonation/impersonation.go rename to pkg/services/grafana-apiserver/auth/authorizer/impersonation.go index 795d5520387..f01cc825013 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/impersonation/impersonation.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/impersonation.go @@ -1,4 +1,4 @@ -package impersonation +package authorizer import ( "context" @@ -6,12 +6,12 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" ) -var _ authorizer.Authorizer = (*ImpersonationAuthorizer)(nil) +var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil) // ImpersonationAuthorizer denies all impersonation requests. -type ImpersonationAuthorizer struct{} +type impersonationAuthorizer struct{} -func (auth ImpersonationAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { +func (auth impersonationAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { if a.GetVerb() == "impersonate" { return authorizer.DecisionDeny, "user impersonation is not supported", nil } diff --git a/pkg/services/grafana-apiserver/auth/authorizer/impersonation/impersonation_test.go b/pkg/services/grafana-apiserver/auth/authorizer/impersonation_test.go similarity index 84% rename from pkg/services/grafana-apiserver/auth/authorizer/impersonation/impersonation_test.go rename to pkg/services/grafana-apiserver/auth/authorizer/impersonation_test.go index 61c7bcb75e2..6af580d892e 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/impersonation/impersonation_test.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/impersonation_test.go @@ -1,4 +1,4 @@ -package impersonation_test +package authorizer import ( "context" @@ -6,12 +6,10 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apiserver/pkg/authorization/authorizer" - - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/impersonation" ) func TestImpersonationAuthorizer_Authorize(t *testing.T) { - auth := impersonation.ImpersonationAuthorizer{} + auth := impersonationAuthorizer{} t.Run("impersonate verb", func(t *testing.T) { attrs := &fakeAttributes{ diff --git a/pkg/services/grafana-apiserver/auth/authorizer/org/wireset.go b/pkg/services/grafana-apiserver/auth/authorizer/org/wireset.go deleted file mode 100644 index b213b088597..00000000000 --- a/pkg/services/grafana-apiserver/auth/authorizer/org/wireset.go +++ /dev/null @@ -1,8 +0,0 @@ -package org - -import "github.com/google/wire" - -var WireSet = wire.NewSet( - ProvideOrgIDAuthorizer, - ProvideOrgRoleAuthorizer, -) diff --git a/pkg/services/grafana-apiserver/auth/authorizer/org/org_id.go b/pkg/services/grafana-apiserver/auth/authorizer/org_id.go similarity index 87% rename from pkg/services/grafana-apiserver/auth/authorizer/org/org_id.go rename to pkg/services/grafana-apiserver/auth/authorizer/org_id.go index 9dee8bf81e4..4d6efdfc88a 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/org/org_id.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/org_id.go @@ -1,4 +1,4 @@ -package org +package authorizer import ( "context" @@ -12,21 +12,21 @@ import ( "github.com/grafana/grafana/pkg/services/org" ) -var _ authorizer.Authorizer = &OrgIDAuthorizer{} +var _ authorizer.Authorizer = &orgIDAuthorizer{} -type OrgIDAuthorizer struct { +type orgIDAuthorizer struct { log log.Logger org org.Service } -func ProvideOrgIDAuthorizer(orgService org.Service) *OrgIDAuthorizer { - return &OrgIDAuthorizer{ +func newOrgIDAuthorizer(orgService org.Service) *orgIDAuthorizer { + return &orgIDAuthorizer{ log: log.New("grafana-apiserver.authorizer.orgid"), org: orgService, } } -func (auth OrgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { +func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { signedInUser, err := appcontext.User(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil diff --git a/pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go b/pkg/services/grafana-apiserver/auth/authorizer/org_role.go similarity index 83% rename from pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go rename to pkg/services/grafana-apiserver/auth/authorizer/org_role.go index 3776da4377d..4717741a8d5 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/org/org_role.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/org_role.go @@ -1,4 +1,4 @@ -package org +package authorizer import ( "context" @@ -11,17 +11,17 @@ import ( "github.com/grafana/grafana/pkg/services/org" ) -var _ authorizer.Authorizer = &OrgIDAuthorizer{} +var _ authorizer.Authorizer = &orgRoleAuthorizer{} -type OrgRoleAuthorizer struct { +type orgRoleAuthorizer struct { log log.Logger } -func ProvideOrgRoleAuthorizer(orgService org.Service) *OrgRoleAuthorizer { - return &OrgRoleAuthorizer{log: log.New("grafana-apiserver.authorizer.orgrole")} +func newOrgRoleAuthorizer(orgService org.Service) *orgRoleAuthorizer { + return &orgRoleAuthorizer{log: log.New("grafana-apiserver.authorizer.orgrole")} } -func (auth OrgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { +func (auth orgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { signedInUser, err := appcontext.User(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil diff --git a/pkg/services/grafana-apiserver/auth/authorizer/provider.go b/pkg/services/grafana-apiserver/auth/authorizer/provider.go index 84a73c50757..f8ab2a79d0f 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/provider.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/provider.go @@ -1,34 +1,65 @@ package authorizer import ( + "context" + + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/union" - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/impersonation" - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/org" - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/stack" + orgsvc "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) -func ProvideAuthorizer( - orgIDAuthorizer *org.OrgIDAuthorizer, - orgRoleAuthorizer *org.OrgRoleAuthorizer, - stackIDAuthorizer *stack.StackIDAuthorizer, - cfg *setting.Cfg, -) authorizer.Authorizer { +var _ authorizer.Authorizer = (*GrafanaAuthorizer)(nil) + +type GrafanaAuthorizer struct { + apis map[string]authorizer.Authorizer + auth authorizer.Authorizer +} + +func NewGrafanaAuthorizer(cfg *setting.Cfg, orgService orgsvc.Service) *GrafanaAuthorizer { authorizers := []authorizer.Authorizer{ - &impersonation.ImpersonationAuthorizer{}, + &impersonationAuthorizer{}, } // In Hosted grafana, the StackID replaces the orgID as a valid namespace if cfg.StackID != "" { - authorizers = append(authorizers, stackIDAuthorizer) + authorizers = append(authorizers, newStackIDAuthorizer(cfg)) } else { - authorizers = append(authorizers, orgIDAuthorizer) + authorizers = append(authorizers, newOrgIDAuthorizer(orgService)) } + // Individual services may have explicit implementations + apis := make(map[string]authorizer.Authorizer) + authorizers = append(authorizers, &authorizerForAPI{apis}) + // org role is last -- and will return allow for verbs that match expectations - // Ideally FGAC happens earlier and returns an explicit answer - authorizers = append(authorizers, orgRoleAuthorizer) - return union.New(authorizers...) + // The apiVersion flavors will run first and can return early when FGAC has appropriate rules + authorizers = append(authorizers, newOrgRoleAuthorizer(orgService)) + return &GrafanaAuthorizer{ + apis: apis, + auth: union.New(authorizers...), + } +} + +func (a *GrafanaAuthorizer) Register(gv schema.GroupVersion, fn authorizer.Authorizer) { + a.apis[gv.String()] = fn +} + +// Authorize implements authorizer.Authorizer. +func (a *GrafanaAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return a.auth.Authorize(ctx, attr) +} + +type authorizerForAPI struct { + apis map[string]authorizer.Authorizer +} + +func (a *authorizerForAPI) Authorize(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + auth, ok := a.apis[attr.GetAPIGroup()+"/"+attr.GetAPIVersion()] + if ok { + return auth.Authorize(ctx, attr) + } + return authorizer.DecisionNoOpinion, "", nil } diff --git a/pkg/services/grafana-apiserver/auth/authorizer/stack/wireset.go b/pkg/services/grafana-apiserver/auth/authorizer/stack/wireset.go deleted file mode 100644 index fe1a183c79f..00000000000 --- a/pkg/services/grafana-apiserver/auth/authorizer/stack/wireset.go +++ /dev/null @@ -1,7 +0,0 @@ -package stack - -import "github.com/google/wire" - -var WireSet = wire.NewSet( - ProvideStackIDAuthorizer, -) diff --git a/pkg/services/grafana-apiserver/auth/authorizer/stack/stack_id.go b/pkg/services/grafana-apiserver/auth/authorizer/stack_id.go similarity index 84% rename from pkg/services/grafana-apiserver/auth/authorizer/stack/stack_id.go rename to pkg/services/grafana-apiserver/auth/authorizer/stack_id.go index 88cb83883d7..32887dd8142 100644 --- a/pkg/services/grafana-apiserver/auth/authorizer/stack/stack_id.go +++ b/pkg/services/grafana-apiserver/auth/authorizer/stack_id.go @@ -1,4 +1,4 @@ -package stack +package authorizer import ( "context" @@ -12,21 +12,21 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -var _ authorizer.Authorizer = &StackIDAuthorizer{} +var _ authorizer.Authorizer = &stackIDAuthorizer{} -type StackIDAuthorizer struct { +type stackIDAuthorizer struct { log log.Logger stackID string } -func ProvideStackIDAuthorizer(cfg *setting.Cfg) *StackIDAuthorizer { - return &StackIDAuthorizer{ +func newStackIDAuthorizer(cfg *setting.Cfg) *stackIDAuthorizer { + return &stackIDAuthorizer{ log: log.New("grafana-apiserver.authorizer.stackid"), stackID: cfg.StackID, // this lets a single tenant grafana validate stack id (rather than orgs) } } -func (auth StackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { +func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { signedInUser, err := appcontext.User(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil diff --git a/pkg/services/grafana-apiserver/auth/authorizer/wireset.go b/pkg/services/grafana-apiserver/auth/authorizer/wireset.go deleted file mode 100644 index ae2d31200a3..00000000000 --- a/pkg/services/grafana-apiserver/auth/authorizer/wireset.go +++ /dev/null @@ -1,14 +0,0 @@ -package authorizer - -import ( - "github.com/google/wire" - - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/org" - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/stack" -) - -var WireSet = wire.NewSet( - org.WireSet, - stack.WireSet, - ProvideAuthorizer, -) diff --git a/pkg/services/grafana-apiserver/common.go b/pkg/services/grafana-apiserver/common.go index 97be4be4fdb..44661d9658d 100644 --- a/pkg/services/grafana-apiserver/common.go +++ b/pkg/services/grafana-apiserver/common.go @@ -6,6 +6,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" @@ -33,6 +34,11 @@ type APIGroupBuilder interface { // Get the API routes for each version GetAPIRoutes() *APIRoutes + + // Optionally add an authorization hook + // Standard namespace checking will happen before this is called, specifically + // the namespace must matches an org|stack that the user belongs to + GetAuthorizer() authorizer.Authorizer } // This is used to implement dynamic sub-resources like pods/x/logs diff --git a/pkg/services/grafana-apiserver/service.go b/pkg/services/grafana-apiserver/service.go index d76616ab600..a0c8e64ddee 100644 --- a/pkg/services/grafana-apiserver/service.go +++ b/pkg/services/grafana-apiserver/service.go @@ -22,7 +22,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/version" - "k8s.io/apiserver/pkg/authorization/authorizer" openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" "k8s.io/apiserver/pkg/endpoints/responsewriter" genericapiserver "k8s.io/apiserver/pkg/server" @@ -34,7 +33,9 @@ import ( "k8s.io/component-base/logs" "k8s.io/klog/v2" + "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/appcontext" @@ -129,14 +130,14 @@ type service struct { tracing *tracing.TracingService - authorizer authorizer.Authorizer + authorizer *authorizer.GrafanaAuthorizer } func ProvideService( cfg *setting.Cfg, features featuremgmt.FeatureToggles, rr routing.RouteRegister, - authz authorizer.Authorizer, + orgService org.Service, tracing *tracing.TracingService, db db.DB, ) (*service, error) { @@ -147,7 +148,7 @@ func ProvideService( rr: rr, stopCh: make(chan struct{}), builders: []APIGroupBuilder{}, - authorizer: authz, + authorizer: authorizer.NewGrafanaAuthorizer(cfg, orgService), tracing: tracing, db: db, // For Unified storage } @@ -227,6 +228,12 @@ func (s *service) start(ctx context.Context) error { if err := b.InstallSchema(Scheme); err != nil { return err } + + // Optionally register a custom authorizer + auth := b.GetAuthorizer() + if auth != nil { + s.authorizer.Register(b.GetGroupVersion(), auth) + } } o := options.NewRecommendedOptions("/registry/grafana.app", Codecs.LegacyCodec(groupVersions...)) diff --git a/pkg/services/grafana-apiserver/wireset.go b/pkg/services/grafana-apiserver/wireset.go index aa01caa503f..25a02e898ef 100644 --- a/pkg/services/grafana-apiserver/wireset.go +++ b/pkg/services/grafana-apiserver/wireset.go @@ -2,8 +2,6 @@ package grafanaapiserver import ( "github.com/google/wire" - - "github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer" ) var WireSet = wire.NewSet( @@ -12,5 +10,4 @@ var WireSet = wire.NewSet( wire.Bind(new(Service), new(*service)), wire.Bind(new(APIRegistrar), new(*service)), wire.Bind(new(DirectRestConfigProvider), new(*service)), - authorizer.WireSet, )