SecureValues: Support inline secure values in GrafanaMetaAccessor (#107996)

pull/107884/head^2
Ryan McKinley 1 week ago committed by GitHub
parent 180a901c7d
commit 9786389ae8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 94
      pkg/apimachinery/apis/common/v0alpha1/secure_values.go
  2. 47
      pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go
  3. 16
      pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go
  4. 154
      pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go
  5. 4
      pkg/apimachinery/go.mod
  6. 103
      pkg/apimachinery/utils/meta.go
  7. 107
      pkg/apimachinery/utils/meta_mock.go
  8. 40
      pkg/apimachinery/utils/meta_test.go
  9. 12
      pkg/storage/unified/resource/server.go

@ -0,0 +1,94 @@
package v0alpha1
import (
"encoding/json"
"fmt"
"strconv"
"gopkg.in/yaml.v3"
)
const redacted = "[REDACTED]"
// RawSecureValue contains the raw decrypted secure value.
type RawSecureValue string
var (
_ fmt.Stringer = (*RawSecureValue)(nil)
_ fmt.Formatter = (*RawSecureValue)(nil)
_ fmt.GoStringer = (*RawSecureValue)(nil)
_ json.Marshaler = (*RawSecureValue)(nil)
_ yaml.Marshaler = (*RawSecureValue)(nil)
)
// Allow access to a secure value inside
// +k8s:openapi-gen=true
type InlineSecureValue struct {
// Create a secure value -- this is only used for POST/PUT
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=24576
Create RawSecureValue `json:"create,omitempty"`
// Name in the secret service (reference)
Name string `json:"name,omitempty"`
// Remove this value from the secure value map
// Values owned by this resource will be deleted if necessary
Remove bool `json:"remove,omitempty,omitzero"`
}
func (v InlineSecureValue) IsZero() bool {
return v.Create.IsZero() && v.Name == "" && !v.Remove
}
// Collection of secure values
// +k8s:openapi-gen=true
type InlineSecureValues = map[string]InlineSecureValue
// NewSecretValue creates a new exposed secure value wrapper.
func NewSecretValue(v string) RawSecureValue {
return RawSecureValue(v)
}
// DangerouslyExposeAndConsumeValue will move the decrypted secure value out of the wrapper and return it.
// Further attempts to call this method will panic.
// The function name is intentionally kept long and weird because this is a dangerous operation and should be used carefully!
func (s *RawSecureValue) DangerouslyExposeAndConsumeValue() string {
if *s == "" {
panic("underlying value is empty or was consumed")
}
tmp := *s
*s = ""
return string(tmp)
}
func (s RawSecureValue) IsZero() bool {
return s == "" // exclude from JSON
}
// String must not return the exposed secure value.
func (s RawSecureValue) String() string {
return redacted
}
// Format must not return the exposed secure value.
func (s RawSecureValue) Format(f fmt.State, _verb rune) {
_, _ = fmt.Fprint(f, redacted)
}
// GoString must not return the exposed secure value.
func (s RawSecureValue) GoString() string {
return redacted
}
// MarshalJSON must not return the exposed secure value.
func (s RawSecureValue) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(redacted)), nil
}
// MarshalYAML must not return the exposed secure value.
func (s RawSecureValue) MarshalYAML() (any, error) {
return redacted, nil
}

@ -0,0 +1,47 @@
package v0alpha1
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestSecureValues(t *testing.T) {
expected := "[REDACTED]"
rawValue := "a-password"
esv := NewSecretValue(rawValue)
// String must not return the exposed secure value.
require.Equal(t, expected, esv.String())
// Format/GoString must not return the exposed secure value.
require.Equal(t, expected, fmt.Sprintf("%+#v", esv))
require.Equal(t, expected, fmt.Sprintf("%v", esv))
require.Equal(t, expected, fmt.Sprintf("%s", esv))
buf := new(bytes.Buffer)
_, err := fmt.Fprintf(buf, "%#v", esv)
require.NoError(t, err)
require.Equal(t, expected, buf.String())
// MarshalJSON must not return the exposed secure value.
bytes, err := json.Marshal(esv)
require.NoError(t, err)
require.Equal(t, `"`+expected+`"`, string(bytes))
// MarshalYAML must not return the exposed secure value.
bytes, err = yaml.Marshal(esv)
require.NoError(t, err)
require.Equal(t, "'"+expected+"'\n", string(bytes))
// DangerouslyExposeAndConsumeValue returns the raw value.
require.Equal(t, rawValue, esv.DangerouslyExposeAndConsumeValue())
// Further calls to DangerouslyExposeAndConsumeValue will panic.
require.Panics(t, func() { esv.DangerouslyExposeAndConsumeValue() })
}

@ -11,6 +11,22 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InlineSecureValue) DeepCopyInto(out *InlineSecureValue) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InlineSecureValue.
func (in *InlineSecureValue) DeepCopy() *InlineSecureValue {
if in == nil {
return nil
}
out := new(InlineSecureValue)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ObjectReference) DeepCopyInto(out *ObjectReference) {
*out = *in

@ -11,68 +11,106 @@ import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
ptr "k8s.io/utils/ptr"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference": schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Scope": schema_apimachinery_apis_common_v0alpha1_Scope(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter": schema_apimachinery_apis_common_v0alpha1_ScopeFilter(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec": schema_apimachinery_apis_common_v0alpha1_ScopeSpec(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldSelectorRequirement": schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref),
"k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref),
"k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref),
"k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref),
"k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue": schema_apimachinery_apis_common_v0alpha1_InlineSecureValue(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference": schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Scope": schema_apimachinery_apis_common_v0alpha1_Scope(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter": schema_apimachinery_apis_common_v0alpha1_ScopeFilter(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec": schema_apimachinery_apis_common_v0alpha1_ScopeSpec(ref),
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldSelectorRequirement": schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref),
"k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref),
"k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref),
"k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref),
"k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref),
}
}
func schema_apimachinery_apis_common_v0alpha1_InlineSecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Allow access to a secure value inside",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"create": {
SchemaProps: spec.SchemaProps{
Description: "Create a secure value -- this is only used for POST/PUT",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](24576),
Type: []string{"string"},
Format: "",
},
},
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name in the secret service (reference)",
Type: []string{"string"},
Format: "",
},
},
"remove": {
SchemaProps: spec.SchemaProps{
Description: "Remove this value from the secure value map Values owned by this resource will be deleted if necessary",
Type: []string{"boolean"},
Format: "",
},
},
},
},
},
}
}

@ -7,9 +7,11 @@ require (
github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
)
require (
@ -48,9 +50,7 @@ require (
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect

@ -12,6 +12,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// LabelKeyGetHistory is used to select object history for an given resource
@ -146,6 +148,13 @@ type GrafanaMetaAccessor interface {
// SetSourceProperties sets the source properties of the resource.
SetSourceProperties(SourceProperties)
// GetSecureValues reads the "secure" property on a resource
GetSecureValues() (common.InlineSecureValues, error)
// SetSourceProperties sets the source properties of the resource.
// For write commands, this may include inline secrets; read will only have references
SetSecureValues(common.InlineSecureValues) error
}
var _ GrafanaMetaAccessor = (*grafanaMetaAccessor)(nil)
@ -821,3 +830,97 @@ func (m *grafanaMetaAccessor) SetSourceProperties(v SourceProperties) {
m.obj.SetAnnotations(annot)
}
// GetSecureValues implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) GetSecureValues() (vals common.InlineSecureValues, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error reading spec")
}
}()
var property any // may be map or struct
f := m.r.FieldByName("Secure")
if f.IsValid() {
property = f.Interface()
} else {
// Unstructured
u, ok := m.raw.(*unstructured.Unstructured)
if ok {
property = u.Object["secure"]
}
}
// Not found (and no error)
if property == nil {
return nil, nil
}
// Try directly casting the property
vals, ok := property.(common.InlineSecureValues)
if ok {
return vals, nil
}
// Generic map
u, ok := property.(map[string]any)
if ok {
vals = make(common.InlineSecureValues, len(u))
for k, v := range u {
sv, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("unsupported nested secure value: %t", v)
}
inline := common.InlineSecureValue{}
inline.Name, _, _ = unstructured.NestedString(sv, "name")
inline.Remove, _, _ = unstructured.NestedBool(sv, "remove")
create, _, _ := unstructured.NestedString(sv, "create")
if create != "" {
inline.Create = common.NewSecretValue(create)
}
vals[k] = inline
}
return vals, nil
}
fmt.Printf("TODO PROPERTY: (%t) %+v\n", property, property)
return nil, fmt.Errorf("support: %t", property)
}
// SetSecureValues implements GrafanaMetaAccessor.
func (m *grafanaMetaAccessor) SetSecureValues(vals common.InlineSecureValues) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error setting spec")
}
}()
f := m.r.FieldByName("Secure")
if f.IsValid() && f.CanSet() {
f.Set(reflect.ValueOf(vals))
return
}
// Unstructured object
u, ok := m.raw.(*unstructured.Unstructured)
if ok {
u.Object["secure"] = vals
return
}
return fmt.Errorf("unable to set secure values on (%T)", m.raw)
}
func ToObjectReference(obj GrafanaMetaAccessor) common.ObjectReference {
gvk := obj.GetGroupVersionKind()
return common.ObjectReference{
APIGroup: gvk.Group,
APIVersion: gvk.Version,
Kind: gvk.Kind,
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
UID: obj.GetUID(),
}
}

@ -1,4 +1,4 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
// Code generated by mockery v2.53.4. DO NOT EDIT.
package utils
@ -12,6 +12,8 @@ import (
types "k8s.io/apimachinery/pkg/types"
v0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -1248,6 +1250,63 @@ func (_c *MockGrafanaMetaAccessor_GetRuntimeObject_Call) RunAndReturn(run func()
return _c
}
// GetSecureValues provides a mock function with no fields
func (_m *MockGrafanaMetaAccessor) GetSecureValues() (map[string]v0alpha1.InlineSecureValue, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetSecureValues")
}
var r0 map[string]v0alpha1.InlineSecureValue
var r1 error
if rf, ok := ret.Get(0).(func() (map[string]v0alpha1.InlineSecureValue, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() map[string]v0alpha1.InlineSecureValue); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]v0alpha1.InlineSecureValue)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGrafanaMetaAccessor_GetSecureValues_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSecureValues'
type MockGrafanaMetaAccessor_GetSecureValues_Call struct {
*mock.Call
}
// GetSecureValues is a helper method to define mock.On call
func (_e *MockGrafanaMetaAccessor_Expecter) GetSecureValues() *MockGrafanaMetaAccessor_GetSecureValues_Call {
return &MockGrafanaMetaAccessor_GetSecureValues_Call{Call: _e.mock.On("GetSecureValues")}
}
func (_c *MockGrafanaMetaAccessor_GetSecureValues_Call) Run(run func()) *MockGrafanaMetaAccessor_GetSecureValues_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockGrafanaMetaAccessor_GetSecureValues_Call) Return(_a0 map[string]v0alpha1.InlineSecureValue, _a1 error) *MockGrafanaMetaAccessor_GetSecureValues_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGrafanaMetaAccessor_GetSecureValues_Call) RunAndReturn(run func() (map[string]v0alpha1.InlineSecureValue, error)) *MockGrafanaMetaAccessor_GetSecureValues_Call {
_c.Call.Return(run)
return _c
}
// GetSelfLink provides a mock function with no fields
func (_m *MockGrafanaMetaAccessor) GetSelfLink() string {
ret := _m.Called()
@ -2369,6 +2428,52 @@ func (_c *MockGrafanaMetaAccessor_SetResourceVersionInt64_Call) RunAndReturn(run
return _c
}
// SetSecureValues provides a mock function with given fields: _a0
func (_m *MockGrafanaMetaAccessor) SetSecureValues(_a0 map[string]v0alpha1.InlineSecureValue) error {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for SetSecureValues")
}
var r0 error
if rf, ok := ret.Get(0).(func(map[string]v0alpha1.InlineSecureValue) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockGrafanaMetaAccessor_SetSecureValues_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetSecureValues'
type MockGrafanaMetaAccessor_SetSecureValues_Call struct {
*mock.Call
}
// SetSecureValues is a helper method to define mock.On call
// - _a0 map[string]v0alpha1.InlineSecureValue
func (_e *MockGrafanaMetaAccessor_Expecter) SetSecureValues(_a0 interface{}) *MockGrafanaMetaAccessor_SetSecureValues_Call {
return &MockGrafanaMetaAccessor_SetSecureValues_Call{Call: _e.mock.On("SetSecureValues", _a0)}
}
func (_c *MockGrafanaMetaAccessor_SetSecureValues_Call) Run(run func(_a0 map[string]v0alpha1.InlineSecureValue)) *MockGrafanaMetaAccessor_SetSecureValues_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(map[string]v0alpha1.InlineSecureValue))
})
return _c
}
func (_c *MockGrafanaMetaAccessor_SetSecureValues_Call) Return(_a0 error) *MockGrafanaMetaAccessor_SetSecureValues_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockGrafanaMetaAccessor_SetSecureValues_Call) RunAndReturn(run func(map[string]v0alpha1.InlineSecureValue) error) *MockGrafanaMetaAccessor_SetSecureValues_Call {
_c.Call.Return(run)
return _c
}
// SetSelfLink provides a mock function with given fields: selfLink
func (_m *MockGrafanaMetaAccessor) SetSelfLink(selfLink string) {
_m.Called(selfLink)

@ -10,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
@ -24,6 +25,9 @@ type TestResource struct {
// Read/write raw status
Status Spec `json:"status,omitempty"`
// Secure values as map
Secure common.InlineSecureValues `json:"secure,omitempty"`
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -84,6 +88,9 @@ type TestResource2 struct {
// Exercise read/write pointer status
Status *Spec `json:"status,omitempty"`
// This time defined with a strict struct
SecureValues ExplictSecureValues `json:"secure,omitempty"`
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -112,6 +119,12 @@ func (in *TestResource2) DeepCopyObject() runtime.Object {
return nil
}
// Spec defines model for Spec.
type ExplictSecureValues struct {
// Sample token value
Prop common.InlineSecureValue `json:"token,omitempty"`
}
// Spec defines model for Spec.
type Spec2 struct{}
@ -247,6 +260,15 @@ func TestMetaAccessor(t *testing.T) {
status, err = meta.GetStatus()
require.NoError(t, err)
require.Equal(t, res.Object["status"], status)
// Check write/read on unstructured object
err = meta.SetSecureValues(common.InlineSecureValues{
"a": {Name: "bbbb"},
})
require.NoError(t, err)
secure, err := meta.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true))
})
t.Run("get and set grafana metadata (TestResource)", func(t *testing.T) {
@ -254,6 +276,11 @@ func TestMetaAccessor(t *testing.T) {
Spec: Spec{
Title: "test",
},
Secure: common.InlineSecureValues{
"x": common.InlineSecureValue{
Create: "hello",
},
},
// Status is empty, but not nil!
}
meta, err := utils.MetaAccessor(res)
@ -302,6 +329,19 @@ func TestMetaAccessor(t *testing.T) {
require.Equal(t, res.Status, status)
require.Equal(t, "111", res.Status.Title)
require.Equal(t, `{"title":"111"}`, asJSON(status, false))
// Check read/write secure values
secure, err := meta.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{"x": {"create": "[REDACTED]"}}`, asJSON(secure, true))
err = meta.SetSecureValues(common.InlineSecureValues{
"a": {Name: "bbbb"},
})
require.NoError(t, err)
secure, err = meta.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true))
})
t.Run("get and set grafana metadata (TestResource2)", func(t *testing.T) {

@ -404,6 +404,8 @@ func (s *server) Stop(ctx context.Context) error {
}
// Old value indicates an update -- otherwise a create
//
//nolint:gocyclo
func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resourcepb.ResourceKey, value, oldValue []byte) (*WriteEvent, *resourcepb.ErrorResult) {
tmp := &unstructured.Unstructured{}
err := tmp.UnmarshalJSON(value)
@ -436,6 +438,16 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resour
return nil, NewBadRequestError("can not save annotation: " + utils.AnnoKeyGrantPermissions)
}
// Verify that this resource can reference secure values
secure, err := obj.GetSecureValues()
if err != nil {
return nil, AsErrorResult(err)
}
if len(secure) > 0 {
// See: https://github.com/grafana/grafana/pull/107803
return nil, NewBadRequestError("Saving secure values is not yet supported")
}
event := &WriteEvent{
Value: value,
Key: key,

Loading…
Cancel
Save