grafana-iam: Implement api level user authorization (#114498)
* OnGoing comment * WIP on the wrapper * Get before Delete * WIP: add an unimplemented storage authorizer * WIP implementing the resource permission authorize * Implement beforeCreate * Create, Delete, Update * List * Use a resource permissions wrapper * Switch the main authorizer to service * Add namespace * Use compile for list * Comment * Remove unecessary comments * fix bug with folder permissions * Implement tests for List * Test get * List test small refactor * Delete test * Reorganize code * imports * Start splitting the tests * test AfterDelete * actually test beforeWrite * Implement tests for wrapper create * Test delete * Test List and Get * Fix List * Remaining tests * simplify * Remove comments * Reorder * Change authorizer to allow access
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package storewrapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8srest "k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = fmt.Errorf("unauthenticated")
|
||||
ErrUnauthorized = fmt.Errorf("unauthorized")
|
||||
ErrUnexpectedType = fmt.Errorf("unexpected object type")
|
||||
)
|
||||
|
||||
// ResourceStorageAuthorizer defines authorization hooks for resource storage operations.
|
||||
type ResourceStorageAuthorizer interface {
|
||||
BeforeCreate(ctx context.Context, obj runtime.Object) error
|
||||
BeforeUpdate(ctx context.Context, obj runtime.Object) error
|
||||
BeforeDelete(ctx context.Context, obj runtime.Object) error
|
||||
AfterGet(ctx context.Context, obj runtime.Object) error
|
||||
FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error)
|
||||
}
|
||||
|
||||
// Wrapper is a k8sStorage (e.g. registry.Store) wrapper that enforces authorization based on ResourceStorageAuthorizer.
|
||||
// It overrides the identity in the context to use service identity for the underlying store operations.
|
||||
// That way, the underlying store authorization is always successful, and the authorization is enforced by the wrapper.
|
||||
type Wrapper struct {
|
||||
inner K8sStorage
|
||||
authorizer ResourceStorageAuthorizer
|
||||
}
|
||||
|
||||
type K8sStorage interface {
|
||||
k8srest.Storage
|
||||
k8srest.Scoper
|
||||
k8srest.SingularNameProvider
|
||||
k8srest.Lister
|
||||
k8srest.Getter
|
||||
k8srest.CreaterUpdater
|
||||
k8srest.GracefulDeleter
|
||||
}
|
||||
|
||||
var _ rest.Storage = (*Wrapper)(nil)
|
||||
|
||||
func New(store K8sStorage, authz ResourceStorageAuthorizer) *Wrapper {
|
||||
return &Wrapper{inner: store, authorizer: authz}
|
||||
}
|
||||
|
||||
func (w *Wrapper) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metaV1.Table, error) {
|
||||
return w.inner.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Create(ctx context.Context, obj runtime.Object, createValidation k8srest.ValidateObjectFunc, options *metaV1.CreateOptions) (runtime.Object, error) {
|
||||
// Enforce authorization based on the user permissions before creating the object
|
||||
err := w.authorizer.BeforeCreate(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
return w.inner.Create(srvCtx, obj, createValidation, options)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Delete(ctx context.Context, name string, deleteValidation k8srest.ValidateObjectFunc, options *metaV1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
// Fetch the object first to authorize
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
getOpts := &metaV1.GetOptions{TypeMeta: options.TypeMeta}
|
||||
if options.Preconditions != nil {
|
||||
getOpts.ResourceVersion = *options.Preconditions.ResourceVersion
|
||||
}
|
||||
obj, err := w.inner.Get(srvCtx, name, getOpts)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Enforce authorization based on the user permissions
|
||||
if err := w.authorizer.BeforeDelete(ctx, obj); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return w.inner.Delete(srvCtx, name, deleteValidation, options)
|
||||
}
|
||||
|
||||
func (w *Wrapper) DeleteCollection(ctx context.Context, deleteValidation k8srest.ValidateObjectFunc, options *metaV1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
// DeleteCollection is complex to authorize properly
|
||||
// For now, deny it entirely for safety
|
||||
return nil, fmt.Errorf("bulk delete operations are not supported through this API")
|
||||
}
|
||||
|
||||
func (w *Wrapper) Destroy() {
|
||||
w.inner.Destroy()
|
||||
}
|
||||
|
||||
func (w *Wrapper) Get(ctx context.Context, name string, options *metaV1.GetOptions) (runtime.Object, error) {
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
item, err := w.inner.Get(srvCtx, name, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enforce authorization based on the user permissions after retrieving the object
|
||||
err = w.authorizer.AfterGet(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) GetSingularName() string {
|
||||
return w.inner.GetSingularName()
|
||||
}
|
||||
|
||||
func (w *Wrapper) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
list, err := w.inner.List(srvCtx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enforce authorization based on the user permissions after retrieving the list
|
||||
return w.authorizer.FilterList(ctx, list)
|
||||
}
|
||||
|
||||
func (w *Wrapper) NamespaceScoped() bool {
|
||||
return w.inner.NamespaceScoped()
|
||||
}
|
||||
|
||||
func (w *Wrapper) New() runtime.Object {
|
||||
return w.inner.New()
|
||||
}
|
||||
|
||||
func (w *Wrapper) NewList() runtime.Object {
|
||||
return w.inner.NewList()
|
||||
}
|
||||
|
||||
func (w *Wrapper) Update(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
objInfo k8srest.UpdatedObjectInfo,
|
||||
createValidation k8srest.ValidateObjectFunc,
|
||||
updateValidation k8srest.ValidateObjectUpdateFunc,
|
||||
forceAllowCreate bool,
|
||||
options *metaV1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
// Create a wrapper around UpdatedObjectInfo to inject authorization
|
||||
wrappedObjInfo := &authorizedUpdateInfo{
|
||||
inner: objInfo,
|
||||
authorizer: w.authorizer,
|
||||
userCtx: ctx, // Keep original context for authorization
|
||||
}
|
||||
|
||||
// Override the identity to use service identity for the underlying store operation
|
||||
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
|
||||
|
||||
return w.inner.Update(srvCtx, name, wrappedObjInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
}
|
||||
|
||||
type authorizedUpdateInfo struct {
|
||||
inner k8srest.UpdatedObjectInfo
|
||||
authorizer ResourceStorageAuthorizer
|
||||
userCtx context.Context
|
||||
}
|
||||
|
||||
func (a *authorizedUpdateInfo) Preconditions() *metaV1.Preconditions {
|
||||
return a.inner.Preconditions()
|
||||
}
|
||||
|
||||
func (a *authorizedUpdateInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
|
||||
// Get the updated object
|
||||
updatedObj, err := a.inner.UpdatedObject(ctx, oldObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enforce authorization using the original user context
|
||||
if err := a.authorizer.BeforeUpdate(a.userCtx, updatedObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedObj, nil
|
||||
}
|
||||
Reference in New Issue
Block a user