@@ -21,12 +21,10 @@ package protosanitizer
2121import (
2222 "encoding/json"
2323 "fmt"
24- "reflect"
25- "strings"
2624
27- "github.com/golang/protobuf/descriptor "
28- "github.com/ golang/protobuf/proto"
29- protobufdescriptor "github.com/ golang/protobuf/protoc-gen-go/descriptor "
25+ "github.com/container-storage-interface/spec/lib/go/csi "
26+ "google. golang.org /protobuf/proto"
27+ "google. golang.org /protobuf/reflect/protoreflect "
3028)
3129
3230// StripSecrets returns a wrapper around the original CSI gRPC message
@@ -42,135 +40,81 @@ import (
4240// result to logging functions which may or may not end up serializing
4341// the parameter depending on the current log level.
4442func StripSecrets (msg interface {}) fmt.Stringer {
45- return & stripSecrets {msg , isCSI1Secret }
46- }
47-
48- // StripSecretsCSI03 is like StripSecrets, except that it works
49- // for messages based on CSI 0.3 and older. It does not work
50- // for CSI 1.0, use StripSecrets for that.
51- func StripSecretsCSI03 (msg interface {}) fmt.Stringer {
52- return & stripSecrets {msg , isCSI03Secret }
43+ return & stripSecrets {msg }
5344}
5445
5546type stripSecrets struct {
56- msg interface {}
57-
58- isSecretField func (field * protobufdescriptor.FieldDescriptorProto ) bool
47+ msg any
5948}
6049
6150func (s * stripSecrets ) String () string {
62- // First convert to a generic representation. That's less efficient
63- // than using reflect directly, but easier to work with.
64- var parsed interface {}
65- b , err := json .Marshal (s .msg )
66- if err != nil {
67- return fmt .Sprintf ("<<json.Marshal %T: %s>>" , s .msg , err )
68- }
69- if err := json .Unmarshal (b , & parsed ); err != nil {
70- return fmt .Sprintf ("<<json.Unmarshal %T: %s>>" , s .msg , err )
71- }
51+ stripped := s .msg
7252
73- // Now remove secrets from the generic representation of the message.
74- s .strip (parsed , s .msg )
53+ // also support scalar types like string, int, etc.
54+ msg , ok := s .msg .(proto.Message )
55+ if ok {
56+ stripped = stripMessage (msg .ProtoReflect ())
57+ }
7558
76- // Re-encoded the stripped representation and return that.
77- b , err = json .Marshal (parsed )
59+ b , err := json .Marshal (stripped )
7860 if err != nil {
7961 return fmt .Sprintf ("<<json.Marshal %T: %s>>" , s .msg , err )
8062 }
8163 return string (b )
8264}
8365
84- func (s * stripSecrets ) strip (parsed interface {}, msg interface {}) {
85- protobufMsg , ok := msg .(descriptor.Message )
86- if ! ok {
87- // Not a protobuf message, so we are done.
88- return
66+ func stripSingleValue (field protoreflect.FieldDescriptor , v protoreflect.Value ) any {
67+ switch field .Kind () {
68+ case protoreflect .MessageKind :
69+ return stripMessage (v .Message ())
70+ case protoreflect .EnumKind :
71+ return field .Enum ().Values ().ByNumber (v .Enum ()).Name ()
72+ default :
73+ return v .Interface ()
8974 }
75+ }
9076
91- // The corresponding map in the parsed JSON representation.
92- parsedFields , ok := parsed .(map [string ]interface {})
93- if ! ok {
94- // Probably nil.
95- return
77+ func stripValue (field protoreflect.FieldDescriptor , v protoreflect.Value ) any {
78+ if field .IsList () {
79+ l := v .List ()
80+ res := make ([]any , l .Len ())
81+ for i := range l .Len () {
82+ res [i ] = stripSingleValue (field , l .Get (i ))
83+ }
84+ return res
85+ } else if field .IsMap () {
86+ m := v .Map ()
87+ res := make (map [string ]any , m .Len ())
88+ m .Range (func (mk protoreflect.MapKey , v protoreflect.Value ) bool {
89+ res [mk .String ()] = stripSingleValue (field .MapValue (), v )
90+ return true
91+ })
92+ return res
93+ } else {
94+ return stripSingleValue (field , v )
9695 }
96+ }
97+
98+ func stripMessage (msg protoreflect.Message ) map [string ]any {
99+ stripped := make (map [string ]any )
97100
98101 // Walk through all fields and replace those with ***stripped*** that
99- // are marked as secret. This relies on protobuf adding "json:" tags
100- // on each field where the name matches the field name in the protobuf
101- // spec (like volume_capabilities). The field.GetJsonName() method returns
102- // a different name (volumeCapabilities) which we don't use.
103- _ , md := descriptor .ForMessage (protobufMsg )
104- fields := md .GetField ()
105- if fields != nil {
106- for _ , field := range fields {
107- if s .isSecretField (field ) {
108- // Overwrite only if already set.
109- if _ , ok := parsedFields [field .GetName ()]; ok {
110- parsedFields [field .GetName ()] = "***stripped***"
111- }
112- } else if field .GetType () == protobufdescriptor .FieldDescriptorProto_TYPE_MESSAGE {
113- // When we get here,
114- // the type name is something like ".csi.v1.CapacityRange" (leading dot!)
115- // and looking up "csi.v1.CapacityRange"
116- // returns the type of a pointer to a pointer
117- // to CapacityRange. We need a pointer to such
118- // a value for recursive stripping.
119- typeName := field .GetTypeName ()
120- if strings .HasPrefix (typeName , "." ) {
121- typeName = typeName [1 :]
122- }
123- t := proto .MessageType (typeName )
124- if t == nil || t .Kind () != reflect .Ptr {
125- // Shouldn't happen, but
126- // better check anyway instead
127- // of panicking.
128- continue
129- }
130- v := reflect .New (t .Elem ())
131-
132- // Recursively strip the message(s) that
133- // the field contains.
134- i := v .Interface ()
135- entry := parsedFields [field .GetName ()]
136- if slice , ok := entry .([]interface {}); ok {
137- // Array of values, like VolumeCapabilities in CreateVolumeRequest.
138- for _ , entry := range slice {
139- s .strip (entry , i )
140- }
141- } else {
142- // Single value.
143- s .strip (entry , i )
144- }
145- }
102+ // are marked as secret.
103+ msg .Range (func (field protoreflect.FieldDescriptor , v protoreflect.Value ) bool {
104+ name := field .TextName ()
105+ if isCSI1Secret (field ) {
106+ stripped [name ] = "***stripped***"
107+ } else {
108+ stripped [name ] = stripValue (field , v )
146109 }
147- }
110+ return true
111+ })
112+ return stripped
148113}
149114
150115// isCSI1Secret uses the csi.E_CsiSecret extension from CSI 1.0 to
151116// determine whether a field contains secrets.
152- func isCSI1Secret (field * protobufdescriptor.FieldDescriptorProto ) bool {
153- ex , err := proto .GetExtension (field .Options , e_CsiSecret )
154- return err == nil && ex != nil && * ex .(* bool )
155- }
156-
157- // Copied from the CSI 1.0 spec (https://github.com/container-storage-interface/spec/blob/37e74064635d27c8e33537c863b37ccb1182d4f8/lib/go/csi/csi.pb.go#L4520-L4527)
158- // to avoid a package dependency that would prevent usage of this package
159- // in repos using an older version of the spec.
160- //
161- // Future revision of the CSI spec must not change this extensions, otherwise
162- // they will break filtering in binaries based on the 1.0 version of the spec.
163- var e_CsiSecret = & proto.ExtensionDesc {
164- ExtendedType : (* protobufdescriptor .FieldOptions )(nil ),
165- ExtensionType : (* bool )(nil ),
166- Field : 1059 ,
167- Name : "csi.v1.csi_secret" ,
168- Tag : "varint,1059,opt,name=csi_secret,json=csiSecret" ,
169- Filename : "github.com/container-storage-interface/spec/csi.proto" ,
170- }
171-
172- // isCSI03Secret relies on the naming convention in CSI <= 0.3
173- // to determine whether a field contains secrets.
174- func isCSI03Secret (field * protobufdescriptor.FieldDescriptorProto ) bool {
175- return strings .HasSuffix (field .GetName (), "_secrets" )
117+ func isCSI1Secret (desc protoreflect.FieldDescriptor ) bool {
118+ ex := proto .GetExtension (desc .Options (), csi .E_CsiSecret )
119+ return ex .(bool )
176120}
0 commit comments