mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s: Refactor authorization initialization (#79670)
This commit is contained in:
parent
ba69d32fa5
commit
33d2d0a12d
@ -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",
|
||||
}
|
||||
```
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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{
|
@ -1,8 +0,0 @@
|
||||
package org
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideOrgIDAuthorizer,
|
||||
ProvideOrgRoleAuthorizer,
|
||||
)
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package stack
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideStackIDAuthorizer,
|
||||
)
|
@ -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
|
@ -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,
|
||||
)
|
@ -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
|
||||
|
@ -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...))
|
||||
|
@ -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,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user