diff --git a/apps/alerting/notifications/kinds/v0alpha2/receiver_spec.cue b/apps/alerting/notifications/kinds/v0alpha2/receiver_spec.cue index 534039248f5..47190d643d0 100644 --- a/apps/alerting/notifications/kinds/v0alpha2/receiver_spec.cue +++ b/apps/alerting/notifications/kinds/v0alpha2/receiver_spec.cue @@ -5,8 +5,14 @@ BaseIntegration: { disable_resolve_message?: bool } +#SecretString: string + +#RedactedSecret: { + specified: bool +} + // A string that contain sensitive information. -#Secret: string +#Secret: #SecretString | #RedactedSecret AlertmanagerIntegration: BaseIntegration & { url: string diff --git a/apps/alerting/notifications/pkg/apis/receiver/v0alpha2/receiver_spec_gen.go b/apps/alerting/notifications/pkg/apis/receiver/v0alpha2/receiver_spec_gen.go index ac810987741..b45b86a2190 100644 --- a/apps/alerting/notifications/pkg/apis/receiver/v0alpha2/receiver_spec_gen.go +++ b/apps/alerting/notifications/pkg/apis/receiver/v0alpha2/receiver_spec_gen.go @@ -18,7 +18,20 @@ func NewAlertmanagerIntegration() *AlertmanagerIntegration { // A string that contain sensitive information. // +k8s:openapi-gen=true -type Secret string +type Secret interface{} + +// +k8s:openapi-gen=true +type SecretString string + +// +k8s:openapi-gen=true +type RedactedSecret struct { + Specified bool `json:"specified"` +} + +// NewRedactedSecret creates a new RedactedSecret object. +func NewRedactedSecret() *RedactedSecret { + return &RedactedSecret{} +} // +k8s:openapi-gen=true type DingdingIntegration struct { diff --git a/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions.go b/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions.go index 3e8925e2e39..7fe4b196915 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions.go +++ b/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions.go @@ -45,7 +45,7 @@ func convertToK8sResources( metadata = &m } } - k8sResource, err := convertToK8sResource(orgID, receiver, access, metadata, namespacer) + k8sResource, err := convertToK8sResource(orgID, receiver, access, metadata, namespacer, false) if err != nil { return nil, err } @@ -63,8 +63,9 @@ func convertToK8sResource( access *ngmodels.ReceiverPermissionSet, metadata *ngmodels.ReceiverMetadata, namespacer request.NamespaceMapper, + keepSecrets bool, ) (*model.Receiver, error) { - spec, err := specFromDomainReceiver(receiver) + spec, err := specFromDomainReceiver(receiver, keepSecrets) if err != nil { return nil, err } @@ -111,9 +112,11 @@ var permissionMapper = map[ngmodels.ReceiverPermission]string{ // ContactPointFromContactPointExport parses the database model of the contact point (group of integrations) where settings are represented in JSON, // to strongly typed ContactPoint. -func specFromDomainReceiver(domain *ngmodels.Receiver) (model.Spec, error) { +func specFromDomainReceiver(domain *ngmodels.Receiver, secrets bool) (model.Spec, error) { j := jsoniter.ConfigCompatibleWithStandardLibrary - j.RegisterExtension(&contactPointsExtension{}) + j.RegisterExtension(&contactPointsExtension{ + KeepSecret: secrets, + }) result := model.Spec{ Title: domain.Name, @@ -492,9 +495,28 @@ func parseIntegration(json jsoniter.API, result *model.Spec, integration *ngmode // contactPointsExtension extends jsoniter with special codecs for some integrations' fields that are encoded differently in the legacy configuration. type contactPointsExtension struct { jsoniter.DummyExtension + KeepSecret bool } -func (c contactPointsExtension) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) { +// CreateEncoder creates a custom encoder for MyInterface +func (c *contactPointsExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder { + if typ == reflect2.TypeOfPtr((*model.Secret)(nil)).Elem() { + return &SecretEncoder{} + } + return nil +} + +// CreateDecoder creates a custom decoder for MyInterface +func (c *contactPointsExtension) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder { + if typ == reflect2.TypeOfPtr((*model.Secret)(nil)).Elem() { + return &SecretDecoder{ + KeepSecret: c.KeepSecret, + } + } + return nil +} + +func (c *contactPointsExtension) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) { if structDescriptor.Type == reflect2.TypeOf(model.EmailIntegration{}) { bind := structDescriptor.GetField("Addresses") codec := &emailAddressCodec{} @@ -609,3 +631,67 @@ func (d *numberAsStringCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator } *((*(*int64))(ptr)) = &value } + +type SecretEncoder struct{} + +func (encoder *SecretEncoder) IsEmpty(ptr unsafe.Pointer) bool { + return *(*model.Secret)(ptr) == nil +} + +func (encoder *SecretEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { + val := *(*model.Secret)(ptr) + if val == nil { + stream.WriteNil() + return + } + + switch v := val.(type) { + case string: + // If it's a raw string, write it as-is + stream.WriteString(v) + case model.SecretString: + // If it's a SecretString, write it as a string + stream.WriteString(string(v)) + case *model.RedactedSecret: + // If it's a RedactedSecret (has "specified" field), write null + stream.WriteNil() + default: + stream.Error = fmt.Errorf("unsupported Secret type: %T", val) + } +} + +type SecretDecoder struct { + KeepSecret bool +} + +func (decoder *SecretDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + // Peek at the next token to determine the JSON type + switch iter.WhatIsNext() { + case jsoniter.StringValue: + // If it's a string, decode as SecretString + str := iter.ReadString() + if decoder.KeepSecret { + *(*model.Secret)(ptr) = model.SecretString(str) + return + } + *(*model.Secret)(ptr) = &model.RedactedSecret{ + Specified: len(str) > 0, + } + // case jsoniter.NilValue: + // // If it's a string, decode as SecretString + // _ = iter.ReadNil() + // if !decoder.KeepSecret { + // // If it's an object, decode as RedactedSecret + // redacted := &model.RedactedSecret{ + // Specified: len(str) > 0, + // } + // iter.ReadVal(redacted) + // if iter.Error != nil { + // return + // } + // *(*model.Secret)(ptr) = redacted + // } + default: + iter.Error = fmt.Errorf("invalid JSON type for Secret; expected string or object") + } +} diff --git a/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions_test.go b/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions_test.go index 2ffe9decd17..bb4bd1a9e9b 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions_test.go +++ b/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/conversions_test.go @@ -37,7 +37,7 @@ func TestConvertToK8sResource(t *testing.T) { Version: "1234", } - result, err := convertToK8sResource(1, recCfg, &models.ReceiverPermissionSet{}, &models.ReceiverMetadata{}, request.GetNamespaceMapper(nil)) + result, err := convertToK8sResource(1, recCfg, &models.ReceiverPermissionSet{}, &models.ReceiverMetadata{}, request.GetNamespaceMapper(nil), true) require.NoError(t, err) back, err := convertToDomainModel(result) diff --git a/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/legacy_storage.go b/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/legacy_storage.go index 8888525da8b..35d7903e677 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/legacy_storage.go +++ b/pkg/registry/apps/alerting/notifications/receiver/v0alpha2/legacy_storage.go @@ -109,6 +109,10 @@ func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOpti } func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) { + return s.get(ctx, uid, false, true) +} + +func (s *legacyStorage) get(ctx context.Context, uid string, keepSecrets bool, fillMetadata bool) (runtime.Object, error) { info, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -121,7 +125,7 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption q := ngmodels.GetReceiverQuery{ OrgID: info.OrgID, Name: name, - Decrypt: false, + Decrypt: true, } user, err := identity.GetRequester(ctx) @@ -135,26 +139,28 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption } var access *ngmodels.ReceiverPermissionSet - accesses, err := s.metadata.AccessControlMetadata(ctx, user, r) - if err == nil { - if a, ok := accesses[r.GetUID()]; ok { - access = &a - } - } else { - return nil, fmt.Errorf("failed to get access control metadata: %w", err) - } - var inUse *ngmodels.ReceiverMetadata - inUses, err := s.metadata.InUseMetadata(ctx, info.OrgID, r) - if err == nil { - if a, ok := inUses[r.GetUID()]; ok { - inUse = &a - } - } else { - return nil, fmt.Errorf("failed to get access control metadata: %w", err) - } + if fillMetadata { - return convertToK8sResource(info.OrgID, r, access, inUse, s.namespacer) + accesses, err := s.metadata.AccessControlMetadata(ctx, user, r) + if err == nil { + if a, ok := accesses[r.GetUID()]; ok { + access = &a + } + } else { + return nil, fmt.Errorf("failed to get access control metadata: %w", err) + } + + inUses, err := s.metadata.InUseMetadata(ctx, info.OrgID, r) + if err == nil { + if a, ok := inUses[r.GetUID()]; ok { + inUse = &a + } + } else { + return nil, fmt.Errorf("failed to get access control metadata: %w", err) + } + } + return convertToK8sResource(info.OrgID, r, access, inUse, s.namespacer, keepSecrets) } func (s *legacyStorage) Create(ctx context.Context, @@ -192,7 +198,7 @@ func (s *legacyStorage) Create(ctx context.Context, if err != nil { return nil, err } - return convertToK8sResource(info.OrgID, out, nil, nil, s.namespacer) + return convertToK8sResource(info.OrgID, out, nil, nil, s.namespacer, false) } func (s *legacyStorage) Update(ctx context.Context, @@ -213,7 +219,7 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } - old, err := s.Get(ctx, uid, nil) + old, err := s.get(ctx, uid, true, false) if err != nil { return old, false, err } @@ -240,7 +246,7 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } - r, err := convertToK8sResource(info.OrgID, updated, nil, nil, s.namespacer) + r, err := convertToK8sResource(info.OrgID, updated, nil, nil, s.namespacer, false) return r, false, err }