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