mirror of https://github.com/grafana/grafana
K8s/ManagedBy: Enforce who can CRUD provisioning resources (#103322)
parent
577ea8f6a9
commit
d3e6e308a0
@ -0,0 +1,88 @@ |
||||
package apistore |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
|
||||
authtypes "github.com/grafana/authlib/types" |
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
) |
||||
|
||||
func checkManagerPropertiesOnDelete(auth authtypes.AuthInfo, obj utils.GrafanaMetaAccessor) error { |
||||
return enforceManagerProperties(auth, obj) |
||||
} |
||||
|
||||
func checkManagerPropertiesOnCreate(auth authtypes.AuthInfo, obj utils.GrafanaMetaAccessor) error { |
||||
return enforceManagerProperties(auth, obj) |
||||
} |
||||
|
||||
func checkManagerPropertiesOnUpdateSpec(auth authtypes.AuthInfo, obj utils.GrafanaMetaAccessor, old utils.GrafanaMetaAccessor) error { |
||||
objKind := obj.GetAnnotation(utils.AnnoKeyManagerKind) |
||||
oldKind := old.GetAnnotation(utils.AnnoKeyManagerKind) |
||||
if objKind == "" && objKind == oldKind { |
||||
return nil // not managed
|
||||
} |
||||
|
||||
// Check the current settings
|
||||
err := checkManagerPropertiesOnCreate(auth, obj) |
||||
if err != nil { // new settings failed
|
||||
return err |
||||
} |
||||
|
||||
managerNew, okNew := obj.GetManagerProperties() |
||||
managerOld, okOld := old.GetManagerProperties() |
||||
if managerNew == managerOld || (okNew && !okOld) { // added manager is OK
|
||||
return nil |
||||
} |
||||
|
||||
if !okNew && okOld { |
||||
// This allows removing the managedBy annotations if you were allowed to write them originally
|
||||
if err := checkManagerPropertiesOnCreate(auth, old); err != nil { |
||||
return &apierrors.StatusError{ErrStatus: metav1.Status{ |
||||
Status: metav1.StatusFailure, |
||||
Code: http.StatusForbidden, |
||||
Reason: metav1.StatusReasonForbidden, |
||||
Message: "Can not remove resource manager from resource", |
||||
}} |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func enforceManagerProperties(auth authtypes.AuthInfo, obj utils.GrafanaMetaAccessor) error { |
||||
kind := obj.GetAnnotation(utils.AnnoKeyManagerKind) |
||||
if kind == "" { |
||||
return nil |
||||
} |
||||
|
||||
switch utils.ParseManagerKindString(kind) { |
||||
case utils.ManagerKindUnknown: |
||||
return nil // not managed
|
||||
|
||||
case utils.ManagerKindRepo: |
||||
if auth.GetUID() == "access-policy:provisioning" { |
||||
return nil // OK!
|
||||
} |
||||
return &apierrors.StatusError{ErrStatus: metav1.Status{ |
||||
Status: metav1.StatusFailure, |
||||
Code: http.StatusForbidden, |
||||
Reason: metav1.StatusReasonForbidden, |
||||
Message: "Provisioned resources must be manaaged by the provisioning service account", |
||||
}} |
||||
|
||||
case utils.ManagerKindPlugin, utils.ManagerKindClassicFP: // nolint:staticcheck
|
||||
// ?? what identity do we use for legacy internal requests?
|
||||
return nil // no error
|
||||
|
||||
case utils.ManagerKindTerraform, utils.ManagerKindKubectl: |
||||
manager, _ := obj.GetManagerProperties() |
||||
if manager.AllowsEdits { |
||||
return nil // no error anyone can do it
|
||||
} |
||||
// TODO: check the kubectl+terraform resource
|
||||
return nil |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,136 @@ |
||||
package apistore |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
|
||||
authtypes "github.com/grafana/authlib/types" |
||||
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" |
||||
"github.com/grafana/grafana/pkg/apimachinery/identity" |
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
) |
||||
|
||||
func TestManagedAuthorizer(t *testing.T) { |
||||
user := &identity.StaticRequester{Type: authtypes.TypeUser, UserUID: "uuu"} |
||||
_, provisioner, err := identity.WithProvisioningIdentity(context.Background(), "default") |
||||
require.NoError(t, err) |
||||
|
||||
tests := []struct { |
||||
name string |
||||
auth authtypes.AuthInfo |
||||
obj runtime.Object |
||||
old runtime.Object |
||||
err string |
||||
}{ |
||||
{ |
||||
name: "user can create", |
||||
auth: user, |
||||
obj: &unstructured.Unstructured{}, |
||||
}, |
||||
{ |
||||
name: "provisioning can create", |
||||
auth: provisioner, |
||||
obj: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Annotations: map[string]string{ |
||||
utils.AnnoKeyManagerKind: string(utils.ManagerKindRepo), |
||||
utils.AnnoKeyManagerIdentity: "abc", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "user can not create provisioned resource", |
||||
auth: user, |
||||
err: "Provisioned resources must be manaaged by the provisioning service account", |
||||
obj: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Annotations: map[string]string{ |
||||
utils.AnnoKeyManagerKind: string(utils.ManagerKindRepo), |
||||
utils.AnnoKeyManagerIdentity: "abc", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "user can not update provisioned resource", |
||||
auth: user, |
||||
err: "Provisioned resources must be manaaged by the provisioning service account", |
||||
obj: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Generation: 1, |
||||
}, |
||||
}, |
||||
old: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Generation: 2, |
||||
Annotations: map[string]string{ |
||||
utils.AnnoKeyManagerKind: string(utils.ManagerKindRepo), |
||||
utils.AnnoKeyManagerIdentity: "abc", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "provisioner can remove manager flags", |
||||
auth: provisioner, |
||||
obj: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Generation: 1, |
||||
}, |
||||
}, |
||||
old: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Generation: 2, |
||||
Annotations: map[string]string{ |
||||
utils.AnnoKeyManagerKind: string(utils.ManagerKindRepo), |
||||
utils.AnnoKeyManagerIdentity: "abc", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "provisioner can add manager flags", |
||||
auth: provisioner, |
||||
old: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Generation: 1, |
||||
}, |
||||
}, |
||||
obj: &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Generation: 2, |
||||
Annotations: map[string]string{ |
||||
utils.AnnoKeyManagerKind: string(utils.ManagerKindRepo), |
||||
utils.AnnoKeyManagerIdentity: "abc", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
obj, err := utils.MetaAccessor(tt.obj) |
||||
require.NoError(t, err) |
||||
|
||||
if tt.old == nil { |
||||
err = checkManagerPropertiesOnCreate(tt.auth, obj) |
||||
} else { |
||||
old, _ := utils.MetaAccessor(tt.old) |
||||
err = checkManagerPropertiesOnUpdateSpec(tt.auth, obj, old) |
||||
} |
||||
|
||||
if tt.err != "" { |
||||
require.Error(t, err, tt.err) |
||||
} else { |
||||
require.NoError(t, err) |
||||
} |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue