mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
220 lines
6.5 KiB
Go
220 lines
6.5 KiB
Go
package resource
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync/atomic"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/appcontext"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/utils"
|
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
|
)
|
|
|
|
type WriteEvent struct {
|
|
EventID int64
|
|
Key *Key // the request key
|
|
Requester identity.Requester
|
|
Operation ResourceOperation
|
|
PreviousRV int64 // only for Update+Delete
|
|
Value []byte
|
|
|
|
Object utils.GrafanaMetaAccessor
|
|
OldObject utils.GrafanaMetaAccessor
|
|
|
|
// Change metadata
|
|
FolderChanged bool
|
|
|
|
// The status will be populated for any error
|
|
Status *StatusResult
|
|
Error error
|
|
}
|
|
|
|
func (e *WriteEvent) BadRequest(err error, message string, a ...any) *WriteEvent {
|
|
e.Error = err
|
|
e.Status = &StatusResult{
|
|
Status: "Failure",
|
|
Message: fmt.Sprintf(message, a...),
|
|
Code: http.StatusBadRequest,
|
|
}
|
|
return e
|
|
}
|
|
|
|
// Verify that all required fields are set, and the user has permission to set the common metadata fields
|
|
type EventValidator interface {
|
|
PrepareCreate(ctx context.Context, req *CreateRequest) (*WriteEvent, error)
|
|
PrepareUpdate(ctx context.Context, req *UpdateRequest, current []byte) (*WriteEvent, error)
|
|
}
|
|
|
|
type EventValidatorOptions struct {
|
|
// Get the next EventID
|
|
NextEventID func() int64
|
|
|
|
// Check if a user has access to write folders
|
|
// When this is nil, no resources can have folders configured
|
|
FolderAccess func(ctx context.Context, user identity.Requester, uid string) bool
|
|
|
|
// When configured, this will make sure a user is allowed to save to a given origin
|
|
OriginAccess func(ctx context.Context, user identity.Requester, origin string) bool
|
|
}
|
|
|
|
type eventValidator struct {
|
|
opts EventValidatorOptions
|
|
}
|
|
|
|
func NewEventValidator(opts EventValidatorOptions) EventValidator {
|
|
if opts.NextEventID == nil {
|
|
counter := atomic.Int64{}
|
|
opts.NextEventID = func() int64 {
|
|
return counter.Add(1)
|
|
}
|
|
}
|
|
return &eventValidator{opts}
|
|
}
|
|
|
|
type dummyObject struct {
|
|
metav1.TypeMeta `json:",inline"`
|
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
|
}
|
|
|
|
var _ EventValidator = &eventValidator{}
|
|
|
|
func (v *eventValidator) newEvent(ctx context.Context, key *Key, value, oldValue []byte) *WriteEvent {
|
|
var err error
|
|
event := &WriteEvent{
|
|
EventID: v.opts.NextEventID(),
|
|
Key: key,
|
|
Value: value,
|
|
}
|
|
event.Requester, err = appcontext.User(ctx)
|
|
if err != nil {
|
|
return event.BadRequest(err, "unable to get user")
|
|
}
|
|
|
|
dummy := &dummyObject{}
|
|
err = json.Unmarshal(value, dummy)
|
|
if err != nil {
|
|
return event.BadRequest(err, "error reading json")
|
|
}
|
|
|
|
obj, err := utils.MetaAccessor(dummy)
|
|
if err != nil {
|
|
return event.BadRequest(err, "invalid object in json")
|
|
}
|
|
if obj.GetUID() == "" {
|
|
return event.BadRequest(nil, "the UID must be set")
|
|
}
|
|
if obj.GetGenerateName() != "" {
|
|
return event.BadRequest(nil, "can not save value with generate name")
|
|
}
|
|
if obj.GetKind() == "" {
|
|
return event.BadRequest(nil, "expecting resources with a kind in the body")
|
|
}
|
|
if obj.GetName() != key.Name {
|
|
return event.BadRequest(nil, "key name does not match the name in the body")
|
|
}
|
|
if obj.GetNamespace() != key.Namespace {
|
|
return event.BadRequest(nil, "key namespace does not match the namespace in the body")
|
|
}
|
|
folder := obj.GetFolder()
|
|
if folder != "" {
|
|
if v.opts.FolderAccess == nil {
|
|
return event.BadRequest(err, "folders are not supported")
|
|
} else if !v.opts.FolderAccess(ctx, event.Requester, folder) {
|
|
return event.BadRequest(err, "unable to add resource to folder") // 403?
|
|
}
|
|
}
|
|
origin, err := obj.GetOriginInfo()
|
|
if err != nil {
|
|
return event.BadRequest(err, "invalid origin info")
|
|
}
|
|
if origin != nil && v.opts.OriginAccess != nil {
|
|
if !v.opts.OriginAccess(ctx, event.Requester, origin.Name) {
|
|
return event.BadRequest(err, "not allowed to write resource to origin (%s)", origin.Name)
|
|
}
|
|
}
|
|
event.Object = obj
|
|
|
|
// This is an update
|
|
if oldValue != nil {
|
|
dummy := &dummyObject{}
|
|
err = json.Unmarshal(oldValue, dummy)
|
|
if err != nil {
|
|
return event.BadRequest(err, "error reading old json value")
|
|
}
|
|
old, err := utils.MetaAccessor(dummy)
|
|
if err != nil {
|
|
return event.BadRequest(err, "invalid object inside old json")
|
|
}
|
|
if key.Name != old.GetName() {
|
|
return event.BadRequest(err, "the old value has a different name (%s != %s)", key.Name, old.GetName())
|
|
}
|
|
|
|
// Can not change creation timestamps+user
|
|
if obj.GetCreatedBy() != old.GetCreatedBy() {
|
|
return event.BadRequest(err, "can not change the created by metadata (%s != %s)", obj.GetCreatedBy(), old.GetCreatedBy())
|
|
}
|
|
if obj.GetCreationTimestamp() != old.GetCreationTimestamp() {
|
|
return event.BadRequest(err, "can not change the CreationTimestamp metadata (%v != %v)", obj.GetCreationTimestamp(), old.GetCreationTimestamp())
|
|
}
|
|
|
|
oldFolder := obj.GetFolder()
|
|
if oldFolder != folder {
|
|
event.FolderChanged = true
|
|
}
|
|
event.OldObject = old
|
|
} else if folder != "" {
|
|
event.FolderChanged = true
|
|
}
|
|
return event
|
|
}
|
|
|
|
func (v *eventValidator) PrepareCreate(ctx context.Context, req *CreateRequest) (*WriteEvent, error) {
|
|
event := v.newEvent(ctx, req.Key, req.Value, nil)
|
|
event.Operation = ResourceOperation_CREATED
|
|
if event.Status != nil {
|
|
return event, nil
|
|
}
|
|
|
|
// Make sure the created by user is accurate
|
|
//----------------------------------------
|
|
val := event.Object.GetCreatedBy()
|
|
if val != "" && val != event.Requester.GetUID().String() {
|
|
return event.BadRequest(nil, "created by annotation does not match: metadata.annotations#"+utils.AnnoKeyCreatedBy), nil
|
|
}
|
|
|
|
// Create can not have updated properties
|
|
//----------------------------------------
|
|
if event.Object.GetUpdatedBy() != "" {
|
|
return event.BadRequest(nil, "unexpected metadata.annotations#"+utils.AnnoKeyCreatedBy), nil
|
|
}
|
|
ts, err := event.Object.GetUpdatedTimestamp()
|
|
if err != nil {
|
|
return event.BadRequest(nil, fmt.Sprintf("invalid timestamp: %s", err)), nil
|
|
}
|
|
if ts != nil {
|
|
return event.BadRequest(nil, "unexpected metadata.annotations#"+utils.AnnoKeyUpdatedTimestamp), nil
|
|
}
|
|
return event, nil
|
|
}
|
|
|
|
func (v *eventValidator) PrepareUpdate(ctx context.Context, req *UpdateRequest, current []byte) (*WriteEvent, error) {
|
|
event := v.newEvent(ctx, req.Key, req.Value, current)
|
|
event.Operation = ResourceOperation_UPDATED
|
|
if event.Status != nil {
|
|
return event, nil
|
|
}
|
|
|
|
// Make sure the update user is accurate
|
|
//----------------------------------------
|
|
val := event.Object.GetUpdatedBy()
|
|
if val != "" && val != event.Requester.GetUID().String() {
|
|
return event.BadRequest(nil, "created by annotation does not match: metadata.annotations#"+utils.AnnoKeyUpdatedBy), nil
|
|
}
|
|
|
|
return event, nil
|
|
}
|