Secrets: Manage secure values inside any resource (#107803)
This commit is contained in:
@@ -5,9 +5,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/google/uuid"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -16,59 +16,97 @@ import (
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
authtypes "github.com/grafana/authlib/types"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
func logN(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
type objectForStorage struct {
|
||||
// The value to save in unistore
|
||||
raw bytes.Buffer
|
||||
|
||||
// Reference to the owner object
|
||||
ref common.ObjectReference
|
||||
|
||||
// apply permissions after create (defined in the resource body)
|
||||
grantPermissions string
|
||||
|
||||
// Synchronous AfterCreate permissions -- allows users to become "admin" of the thing they made
|
||||
permissionCreator permissionCreatorFunc
|
||||
|
||||
// These secrets where created, should be cleaned up if storage fails
|
||||
createdSecureValues []string
|
||||
|
||||
// These should be deleted if storage succeeds
|
||||
deleteSecureValues []string
|
||||
|
||||
// We know something changed
|
||||
// This will ensure that the generation increments
|
||||
hasChanged bool
|
||||
}
|
||||
|
||||
// Slightly modified function from https://github.com/dustin/go-humanize (MIT).
|
||||
func formatBytes(numBytes int) string {
|
||||
base := 1024.0
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
if numBytes < 10 {
|
||||
return fmt.Sprintf("%d B", numBytes)
|
||||
func (v *objectForStorage) finish(ctx context.Context, err error, secrets secrets.InlineSecureValueSupport) error {
|
||||
if err != nil {
|
||||
// Remove the secure values that were created
|
||||
for _, s := range v.createdSecureValues {
|
||||
if e := secrets.DeleteWhenOwnedByResource(ctx, v.ref, s); e != nil {
|
||||
logging.FromContext(ctx).Warn("unable to clean up new secure value", "name", s, "err", e)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
e := math.Floor(logN(float64(numBytes), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(numBytes)/math.Pow(base, e)*10+0.5) / 10
|
||||
return fmt.Sprintf("%.1f %s", val, suffix)
|
||||
|
||||
// Delete secure values after successfully saving the object
|
||||
if len(v.deleteSecureValues) > 0 {
|
||||
for _, s := range v.deleteSecureValues {
|
||||
if e := secrets.DeleteWhenOwnedByResource(ctx, v.ref, s); e != nil {
|
||||
logging.FromContext(ctx).Warn("unable to clean up new secure value", "name", s, "err", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create permissions
|
||||
if v.permissionCreator != nil {
|
||||
return v.permissionCreator(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Called on create
|
||||
func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime.Object) ([]byte, string, error) {
|
||||
info, ok := authtypes.AuthInfoFrom(ctx)
|
||||
func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime.Object) (objectForStorage, error) {
|
||||
v := objectForStorage{}
|
||||
info, ok := authlib.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, "", errors.New("missing auth info")
|
||||
return v, errors.New("missing auth info")
|
||||
}
|
||||
|
||||
obj, err := utils.MetaAccessor(newObject)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return v, err
|
||||
}
|
||||
if obj.GetName() == "" {
|
||||
return nil, "", storage.NewInvalidObjError("", "missing name")
|
||||
return v, storage.NewInvalidObjError("", "missing name")
|
||||
}
|
||||
if obj.GetResourceVersion() != "" {
|
||||
return nil, "", storage.ErrResourceVersionSetOnCreate
|
||||
return v, storage.ErrResourceVersionSetOnCreate
|
||||
}
|
||||
if obj.GetUID() == "" {
|
||||
obj.SetUID(types.UID(uuid.NewString()))
|
||||
}
|
||||
if obj.GetFolder() != "" && !s.opts.EnableFolderSupport {
|
||||
return nil, "", apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
|
||||
return v, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
|
||||
}
|
||||
|
||||
grantPermisions := obj.GetAnnotation(utils.AnnoKeyGrantPermissions)
|
||||
if grantPermisions != "" {
|
||||
v.grantPermissions = obj.GetAnnotation(utils.AnnoKeyGrantPermissions)
|
||||
if v.grantPermissions != "" {
|
||||
obj.SetAnnotation(utils.AnnoKeyGrantPermissions, "") // remove the annotation
|
||||
}
|
||||
if err := checkManagerPropertiesOnCreate(info, obj); err != nil {
|
||||
return nil, "", err
|
||||
return v, err
|
||||
}
|
||||
|
||||
if s.opts.RequireDeprecatedInternalID {
|
||||
@@ -92,33 +130,37 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime
|
||||
obj.SetCreatedBy(info.GetUID())
|
||||
obj.SetGeneration(1) // the first time we write
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = s.codec.Encode(newObject, &buf); err != nil {
|
||||
return nil, "", err
|
||||
err = prepareSecureValues(ctx, s.opts.SecureValues, obj, nil, &v)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
val, err := s.handleLargeResources(ctx, obj, buf)
|
||||
return val, grantPermisions, err
|
||||
err = s.codec.Encode(newObject, &v.raw)
|
||||
if err == nil {
|
||||
err = s.handleLargeResources(ctx, obj, &v.raw)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Called on update
|
||||
func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runtime.Object, previousObject runtime.Object) ([]byte, error) {
|
||||
info, ok := authtypes.AuthInfoFrom(ctx)
|
||||
func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runtime.Object, previousObject runtime.Object) (objectForStorage, error) {
|
||||
v := objectForStorage{}
|
||||
info, ok := authlib.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("missing auth info")
|
||||
return v, errors.New("missing auth info")
|
||||
}
|
||||
|
||||
obj, err := utils.MetaAccessor(updateObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return v, err
|
||||
}
|
||||
if obj.GetName() == "" {
|
||||
return nil, fmt.Errorf("updated object must have a name")
|
||||
return v, fmt.Errorf("updated object must have a name")
|
||||
}
|
||||
|
||||
previous, err := utils.MetaAccessor(previousObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return v, err
|
||||
}
|
||||
|
||||
if previous.GetUID() == "" {
|
||||
@@ -133,7 +175,7 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti
|
||||
}
|
||||
|
||||
if obj.GetName() != previous.GetName() {
|
||||
return nil, fmt.Errorf("name mismatch between existing and updated object")
|
||||
return v, fmt.Errorf("name mismatch between existing and updated object")
|
||||
}
|
||||
|
||||
obj.SetCreatedBy(previous.GetCreatedBy())
|
||||
@@ -148,34 +190,39 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti
|
||||
obj.SetDeprecatedInternalID(previousInternalID) // nolint:staticcheck
|
||||
}
|
||||
|
||||
err = prepareSecureValues(ctx, s.opts.SecureValues, obj, previous, &v)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Check if we should bump the generation
|
||||
changed := obj.GetFolder() != previous.GetFolder()
|
||||
if changed {
|
||||
if obj.GetFolder() != previous.GetFolder() {
|
||||
if !s.opts.EnableFolderSupport {
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
|
||||
return v, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
|
||||
}
|
||||
// TODO: check that we can move the folder?
|
||||
v.hasChanged = true
|
||||
} else if obj.GetDeletionTimestamp() != nil && previous.GetDeletionTimestamp() == nil {
|
||||
changed = true // bump generation when deleted
|
||||
} else {
|
||||
v.hasChanged = true // bump generation when deleted
|
||||
} else if !v.hasChanged {
|
||||
spec, e1 := obj.GetSpec()
|
||||
oldSpec, e2 := previous.GetSpec()
|
||||
if e1 == nil && e2 == nil {
|
||||
if !apiequality.Semantic.DeepEqual(spec, oldSpec) {
|
||||
changed = true
|
||||
v.hasChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the resource as changed
|
||||
if changed {
|
||||
if v.hasChanged {
|
||||
obj.SetGeneration(previous.GetGeneration() + 1)
|
||||
obj.SetUpdatedBy(info.GetUID())
|
||||
obj.SetUpdatedTimestampMillis(time.Now().UnixMilli())
|
||||
|
||||
// Only validate when the generation has changed
|
||||
if err := checkManagerPropertiesOnUpdateSpec(info, obj, previous); err != nil {
|
||||
return nil, err
|
||||
return v, err
|
||||
}
|
||||
} else {
|
||||
obj.SetGeneration(previous.GetGeneration())
|
||||
@@ -183,19 +230,20 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti
|
||||
obj.SetAnnotation(utils.AnnoKeyUpdatedTimestamp, previous.GetAnnotation(utils.AnnoKeyUpdatedTimestamp))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = s.codec.Encode(updateObject, &buf); err != nil {
|
||||
return nil, err
|
||||
err = s.codec.Encode(updateObject, &v.raw)
|
||||
if err == nil {
|
||||
err = s.handleLargeResources(ctx, obj, &v.raw)
|
||||
}
|
||||
return s.handleLargeResources(ctx, obj, buf)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s *Storage) handleLargeResources(ctx context.Context, obj utils.GrafanaMetaAccessor, buf bytes.Buffer) ([]byte, error) {
|
||||
// The bytes buffer will be reset with the proper value
|
||||
func (s *Storage) handleLargeResources(ctx context.Context, obj utils.GrafanaMetaAccessor, buf *bytes.Buffer) error {
|
||||
support := s.opts.LargeObjectSupport
|
||||
size := buf.Len()
|
||||
if support != nil && size > support.Threshold() {
|
||||
if support.MaxSize() > 0 && size > support.MaxSize() {
|
||||
return nil, fmt.Errorf("request object is too big (%s > %s)", formatBytes(size), formatBytes(support.MaxSize()))
|
||||
return fmt.Errorf("request object is too big (%s > %s)", humanize.Bytes(uint64(size)), humanize.Bytes(uint64(support.MaxSize())))
|
||||
}
|
||||
|
||||
key := &resourcepb.ResourceKey{
|
||||
@@ -207,19 +255,17 @@ func (s *Storage) handleLargeResources(ctx context.Context, obj utils.GrafanaMet
|
||||
|
||||
err := support.Deconstruct(ctx, key, s.store, obj, buf.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
orig, ok := obj.GetRuntimeObject()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error using object as runtime object")
|
||||
return fmt.Errorf("error using object as runtime object")
|
||||
}
|
||||
|
||||
// Now encode the smaller version
|
||||
if err = s.codec.Encode(orig, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.codec.Encode(orig, buf)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user