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 |
| `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/<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
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
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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{

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 (
"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

View File

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

View File

@ -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
}

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 (
"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

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/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

View File

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

View File

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