K8s: Refactor authorization initialization (#79670)

This commit is contained in:
Ryan McKinley 2023-12-19 09:12:35 -08:00 committed by GitHub
parent ba69d32fa5
commit 33d2d0a12d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 112 additions and 105 deletions

View File

@ -179,31 +179,3 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref
| `externalServiceAccounts` | Automatic service account and token setup for plugins | | `externalServiceAccounts` | Automatic service account and token setup for plugins |
| `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 | | `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 |
| `ssoSettingsApi` | Enables the SSO settings API | | `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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#allow_editing), [`update_webhook`](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#update_webhook) and [`update_webhook_token`](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/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",
}
```

View File

@ -1,6 +1,7 @@
package example package example
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
@ -9,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
@ -19,6 +21,7 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/kube-openapi/pkg/validation/spec"
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" 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
})
}

View File

@ -8,6 +8,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
@ -130,3 +131,7 @@ func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinition
func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes { func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
return nil // no custom API routes return nil // no custom API routes
} }
func (b *PlaylistAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return nil // default authorizer is fine
}

View File

@ -1,4 +1,4 @@
package impersonation package authorizer
import ( import (
"context" "context"
@ -6,12 +6,12 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
) )
var _ authorizer.Authorizer = (*ImpersonationAuthorizer)(nil) var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil)
// ImpersonationAuthorizer denies all impersonation requests. // 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" { if a.GetVerb() == "impersonate" {
return authorizer.DecisionDeny, "user impersonation is not supported", nil return authorizer.DecisionDeny, "user impersonation is not supported", nil
} }

View File

@ -1,4 +1,4 @@
package impersonation_test package authorizer
import ( import (
"context" "context"
@ -6,12 +6,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/impersonation"
) )
func TestImpersonationAuthorizer_Authorize(t *testing.T) { func TestImpersonationAuthorizer_Authorize(t *testing.T) {
auth := impersonation.ImpersonationAuthorizer{} auth := impersonationAuthorizer{}
t.Run("impersonate verb", func(t *testing.T) { t.Run("impersonate verb", func(t *testing.T) {
attrs := &fakeAttributes{ attrs := &fakeAttributes{

View File

@ -1,8 +0,0 @@
package org
import "github.com/google/wire"
var WireSet = wire.NewSet(
ProvideOrgIDAuthorizer,
ProvideOrgRoleAuthorizer,
)

View File

@ -1,4 +1,4 @@
package org package authorizer
import ( import (
"context" "context"
@ -12,21 +12,21 @@ import (
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
) )
var _ authorizer.Authorizer = &OrgIDAuthorizer{} var _ authorizer.Authorizer = &orgIDAuthorizer{}
type OrgIDAuthorizer struct { type orgIDAuthorizer struct {
log log.Logger log log.Logger
org org.Service org org.Service
} }
func ProvideOrgIDAuthorizer(orgService org.Service) *OrgIDAuthorizer { func newOrgIDAuthorizer(orgService org.Service) *orgIDAuthorizer {
return &OrgIDAuthorizer{ return &orgIDAuthorizer{
log: log.New("grafana-apiserver.authorizer.orgid"), log: log.New("grafana-apiserver.authorizer.orgid"),
org: orgService, 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) signedInUser, err := appcontext.User(ctx)
if err != nil { if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil

View File

@ -1,4 +1,4 @@
package org package authorizer
import ( import (
"context" "context"
@ -11,17 +11,17 @@ import (
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
) )
var _ authorizer.Authorizer = &OrgIDAuthorizer{} var _ authorizer.Authorizer = &orgRoleAuthorizer{}
type OrgRoleAuthorizer struct { type orgRoleAuthorizer struct {
log log.Logger log log.Logger
} }
func ProvideOrgRoleAuthorizer(orgService org.Service) *OrgRoleAuthorizer { func newOrgRoleAuthorizer(orgService org.Service) *orgRoleAuthorizer {
return &OrgRoleAuthorizer{log: log.New("grafana-apiserver.authorizer.orgrole")} 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) signedInUser, err := appcontext.User(ctx)
if err != nil { if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil

View File

@ -1,34 +1,65 @@
package authorizer package authorizer
import ( import (
"context"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/union" "k8s.io/apiserver/pkg/authorization/union"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/impersonation" orgsvc "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/org"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer/stack"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func ProvideAuthorizer( var _ authorizer.Authorizer = (*GrafanaAuthorizer)(nil)
orgIDAuthorizer *org.OrgIDAuthorizer,
orgRoleAuthorizer *org.OrgRoleAuthorizer, type GrafanaAuthorizer struct {
stackIDAuthorizer *stack.StackIDAuthorizer, apis map[string]authorizer.Authorizer
cfg *setting.Cfg, auth authorizer.Authorizer
) authorizer.Authorizer { }
func NewGrafanaAuthorizer(cfg *setting.Cfg, orgService orgsvc.Service) *GrafanaAuthorizer {
authorizers := []authorizer.Authorizer{ authorizers := []authorizer.Authorizer{
&impersonation.ImpersonationAuthorizer{}, &impersonationAuthorizer{},
} }
// In Hosted grafana, the StackID replaces the orgID as a valid namespace // In Hosted grafana, the StackID replaces the orgID as a valid namespace
if cfg.StackID != "" { if cfg.StackID != "" {
authorizers = append(authorizers, stackIDAuthorizer) authorizers = append(authorizers, newStackIDAuthorizer(cfg))
} else { } 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 // org role is last -- and will return allow for verbs that match expectations
// Ideally FGAC happens earlier and returns an explicit answer // The apiVersion flavors will run first and can return early when FGAC has appropriate rules
authorizers = append(authorizers, orgRoleAuthorizer) authorizers = append(authorizers, newOrgRoleAuthorizer(orgService))
return union.New(authorizers...) 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
} }

View File

@ -1,7 +0,0 @@
package stack
import "github.com/google/wire"
var WireSet = wire.NewSet(
ProvideStackIDAuthorizer,
)

View File

@ -1,4 +1,4 @@
package stack package authorizer
import ( import (
"context" "context"
@ -12,21 +12,21 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
var _ authorizer.Authorizer = &StackIDAuthorizer{} var _ authorizer.Authorizer = &stackIDAuthorizer{}
type StackIDAuthorizer struct { type stackIDAuthorizer struct {
log log.Logger log log.Logger
stackID string stackID string
} }
func ProvideStackIDAuthorizer(cfg *setting.Cfg) *StackIDAuthorizer { func newStackIDAuthorizer(cfg *setting.Cfg) *stackIDAuthorizer {
return &StackIDAuthorizer{ return &stackIDAuthorizer{
log: log.New("grafana-apiserver.authorizer.stackid"), log: log.New("grafana-apiserver.authorizer.stackid"),
stackID: cfg.StackID, // this lets a single tenant grafana validate stack id (rather than orgs) 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) signedInUser, err := appcontext.User(ctx)
if err != nil { if err != nil {
return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil

View File

@ -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,
)

View File

@ -6,6 +6,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/common"
@ -33,6 +34,11 @@ type APIGroupBuilder interface {
// Get the API routes for each version // Get the API routes for each version
GetAPIRoutes() *APIRoutes 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 // This is used to implement dynamic sub-resources like pods/x/logs

View File

@ -22,7 +22,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version" "k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/authorization/authorizer"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
"k8s.io/apiserver/pkg/endpoints/responsewriter" "k8s.io/apiserver/pkg/endpoints/responsewriter"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
@ -34,7 +33,9 @@ import (
"k8s.io/component-base/logs" "k8s.io/component-base/logs"
"k8s.io/klog/v2" "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/grafana-apiserver/utils"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/appcontext"
@ -129,14 +130,14 @@ type service struct {
tracing *tracing.TracingService tracing *tracing.TracingService
authorizer authorizer.Authorizer authorizer *authorizer.GrafanaAuthorizer
} }
func ProvideService( func ProvideService(
cfg *setting.Cfg, cfg *setting.Cfg,
features featuremgmt.FeatureToggles, features featuremgmt.FeatureToggles,
rr routing.RouteRegister, rr routing.RouteRegister,
authz authorizer.Authorizer, orgService org.Service,
tracing *tracing.TracingService, tracing *tracing.TracingService,
db db.DB, db db.DB,
) (*service, error) { ) (*service, error) {
@ -147,7 +148,7 @@ func ProvideService(
rr: rr, rr: rr,
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
builders: []APIGroupBuilder{}, builders: []APIGroupBuilder{},
authorizer: authz, authorizer: authorizer.NewGrafanaAuthorizer(cfg, orgService),
tracing: tracing, tracing: tracing,
db: db, // For Unified storage db: db, // For Unified storage
} }
@ -227,6 +228,12 @@ func (s *service) start(ctx context.Context) error {
if err := b.InstallSchema(Scheme); err != nil { if err := b.InstallSchema(Scheme); err != nil {
return err 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...)) o := options.NewRecommendedOptions("/registry/grafana.app", Codecs.LegacyCodec(groupVersions...))

View File

@ -2,8 +2,6 @@ package grafanaapiserver
import ( import (
"github.com/google/wire" "github.com/google/wire"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/auth/authorizer"
) )
var WireSet = wire.NewSet( var WireSet = wire.NewSet(
@ -12,5 +10,4 @@ var WireSet = wire.NewSet(
wire.Bind(new(Service), new(*service)), wire.Bind(new(Service), new(*service)),
wire.Bind(new(APIRegistrar), new(*service)), wire.Bind(new(APIRegistrar), new(*service)),
wire.Bind(new(DirectRestConfigProvider), new(*service)), wire.Bind(new(DirectRestConfigProvider), new(*service)),
authorizer.WireSet,
) )