...

Source file src/github.com/launchdarkly/go-sdk-events/v2/context_formatter.go

Documentation: github.com/launchdarkly/go-sdk-events/v2

     1  package ldevents
     2  
     3  import (
     4  	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
     5  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
     6  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
     7  
     8  	"github.com/launchdarkly/go-jsonstream/v3/jwriter"
     9  )
    10  
    11  // eventContextFormatter provides the special JSON serialization format that is used when including Context
    12  // data in analytics events. In this format, some attribute values may be redacted based on the SDK's
    13  // events configuration and/or the per-Context setting of ldcontext.Builder.Private().
    14  type eventContextFormatter struct {
    15  	allAttributesPrivate bool
    16  	privateAttributes    map[string]*privateAttrLookupNode
    17  }
    18  
    19  type privateAttrLookupNode struct {
    20  	attribute *ldattr.Ref
    21  	children  map[string]*privateAttrLookupNode
    22  }
    23  
    24  // newEventContextFormatter creates an eventContextFormatter.
    25  //
    26  // An instance of this type is owned by the eventOutputFormatter that is responsible for writing all
    27  // JSON event data. It is created at SDK initialization time based on the SDK configuration.
    28  func newEventContextFormatter(config EventsConfiguration) eventContextFormatter {
    29  	ret := eventContextFormatter{allAttributesPrivate: config.AllAttributesPrivate}
    30  	if len(config.PrivateAttributes) != 0 {
    31  		// Reformat the list of private attributes into a map structure that will allow
    32  		// for faster lookups.
    33  		ret.privateAttributes = makePrivateAttrLookupData(config.PrivateAttributes)
    34  	}
    35  	return ret
    36  }
    37  
    38  func makePrivateAttrLookupData(attrRefList []ldattr.Ref) map[string]*privateAttrLookupNode {
    39  	// This function transforms a list of AttrRefs into a data structure that allows for more efficient
    40  	// implementation of eventContextFormatter.checkGloballyPrivate().
    41  	//
    42  	// For instance, if the original AttrRefs were "/name", "/address/street", and "/address/city",
    43  	// it would produce the following map:
    44  	//
    45  	// "name": {
    46  	//   attribute: NewAttrRef("/name"),
    47  	// },
    48  	// "address": {
    49  	//   children: {
    50  	//     "street": {
    51  	//       attribute: NewAttrRef("/address/street/"),
    52  	//     },
    53  	//     "city": {
    54  	//       attribute: NewAttrRef("/address/city/"),
    55  	//     },
    56  	//   },
    57  	// }
    58  	ret := make(map[string]*privateAttrLookupNode)
    59  	for _, a := range attrRefList {
    60  		parentMap := &ret
    61  		for i := 0; i < a.Depth(); i++ {
    62  			name := a.Component(i)
    63  			if *parentMap == nil {
    64  				*parentMap = make(map[string]*privateAttrLookupNode)
    65  			}
    66  			nextNode := (*parentMap)[name]
    67  			if nextNode == nil {
    68  				nextNode = &privateAttrLookupNode{}
    69  				if i == a.Depth()-1 {
    70  					aa := a
    71  					nextNode.attribute = &aa
    72  				}
    73  				(*parentMap)[name] = nextNode
    74  			}
    75  			parentMap = &nextNode.children
    76  		}
    77  	}
    78  	return ret
    79  }
    80  
    81  // WriteContext serializes a Context in the format appropriate for an analytics event, redacting
    82  // private attributes if necessary.
    83  func (f *eventContextFormatter) WriteContext(w *jwriter.Writer, ec *EventInputContext) {
    84  	if ec.preserialized != nil {
    85  		w.Raw(ec.preserialized)
    86  		return
    87  	}
    88  	if ec.context.Err() != nil {
    89  		w.AddError(ec.context.Err())
    90  		return
    91  	}
    92  	if ec.context.Multiple() {
    93  		f.writeContextInternalMulti(w, ec)
    94  	} else {
    95  		f.writeContextInternalSingle(w, &ec.context, true)
    96  	}
    97  }
    98  
    99  func (f *eventContextFormatter) writeContextInternalSingle(
   100  	w *jwriter.Writer,
   101  	c *ldcontext.Context,
   102  	includeKind bool,
   103  ) {
   104  	obj := w.Object()
   105  	if includeKind {
   106  		obj.Name(ldattr.KindAttr).String(string(c.Kind()))
   107  	}
   108  
   109  	obj.Name(ldattr.KeyAttr).String(c.Key())
   110  
   111  	optionalAttrNames := make([]string, 0, 50) // arbitrary capacity, expanded if necessary by GetOptionalAttributeNames
   112  	redactedAttrs := make([]string, 0, 20)
   113  
   114  	optionalAttrNames = c.GetOptionalAttributeNames(optionalAttrNames)
   115  
   116  	for _, key := range optionalAttrNames {
   117  		if value := c.GetValue(key); value.IsDefined() {
   118  			if f.allAttributesPrivate {
   119  				// If allAttributesPrivate is true, then there's no complex filtering or recursing to be done: all of
   120  				// these values are by definition private, so just add their names to the redacted list. Since the
   121  				// redacted list uses the attribute reference syntax, we may need to escape the value if the name of
   122  				// this individual attribute happens to be something like "/a/b"; the easiest way to do that is to
   123  				// call NewLiteralRef and then convert the Ref to an attribute reference string.
   124  				escapedAttrName := ldattr.NewLiteralRef(key).String()
   125  				redactedAttrs = append(redactedAttrs, escapedAttrName)
   126  				continue
   127  			}
   128  			path := make([]string, 0, 10)
   129  			f.writeFilteredAttribute(w, c, &obj, path, key, value, &redactedAttrs)
   130  		}
   131  	}
   132  
   133  	if c.Anonymous() {
   134  		obj.Name(ldattr.AnonymousAttr).Bool(true)
   135  	}
   136  
   137  	anyRedacted := len(redactedAttrs) != 0
   138  	if anyRedacted {
   139  		metaJSON := obj.Name("_meta").Object()
   140  		privateAttrsJSON := metaJSON.Name("redactedAttributes").Array()
   141  		for _, a := range redactedAttrs {
   142  			privateAttrsJSON.String(a)
   143  		}
   144  		privateAttrsJSON.End()
   145  		metaJSON.End()
   146  	}
   147  
   148  	obj.End()
   149  }
   150  
   151  func (f *eventContextFormatter) writeContextInternalMulti(w *jwriter.Writer, ec *EventInputContext) {
   152  	obj := w.Object()
   153  	obj.Name(ldattr.KindAttr).String(string(ldcontext.MultiKind))
   154  
   155  	for i := 0; i < ec.context.IndividualContextCount(); i++ {
   156  		if ic := ec.context.IndividualContextByIndex(i); ic.IsDefined() {
   157  			obj.Name(string(ic.Kind()))
   158  			f.writeContextInternalSingle(w, &ic, false)
   159  		}
   160  	}
   161  
   162  	obj.End()
   163  }
   164  
   165  // writeFilteredAttribute checks whether a given value should be considered private, and then
   166  // either writes the attribute to the output JSON object if it is *not* private, or adds the
   167  // corresponding attribute reference to the redactedAttrs list if it is private.
   168  //
   169  // The parentPath parameter indicates where we are in the context data structure. If it is empty,
   170  // we are at the top level and "key" is an attribute name. If it is not empty, we are recursing
   171  // into the properties of an attribute value that is a JSON object: for instance, if parentPath
   172  // is ["billing", "address"] and key is "street", then the top-level attribute is "billing" and
   173  // has a value in the form {"address": {"street": ...}} and we are now deciding whether to
   174  // write the "street" property. See maybeRedact() for the logic involved in that decision.
   175  //
   176  // If allAttributesPrivate is true, this method is never called.
   177  func (f *eventContextFormatter) writeFilteredAttribute(
   178  	w *jwriter.Writer,
   179  	c *ldcontext.Context,
   180  	parentObj *jwriter.ObjectState,
   181  	parentPath []string,
   182  	key string,
   183  	value ldvalue.Value,
   184  	redactedAttrs *[]string,
   185  ) {
   186  	path := append(parentPath, key) //nolint:gocritic // purposely not assigning to same slice
   187  
   188  	isRedacted, nestedPropertiesAreRedacted := f.maybeRedact(c, path, value.Type(), redactedAttrs)
   189  
   190  	if value.Type() != ldvalue.ObjectType {
   191  		// For all value types except object, the question is only "is there a private attribute
   192  		// reference that directly points to this property", since there are no nested properties.
   193  		if !isRedacted {
   194  			parentObj.Name(key)
   195  			value.WriteToJSONWriter(w)
   196  		}
   197  		return
   198  	}
   199  
   200  	// If the value is an object, then there are three possible outcomes: 1. this value is
   201  	// completely redacted, so drop it and do not recurse; 2. the value is not redacted, and
   202  	// and neither are any subproperties within it, so output the whole thing as-is; 3. the
   203  	// value itself is not redacted, but some subproperties within it are, so we'll need to
   204  	// recurse through it and filter as we go.
   205  	if isRedacted {
   206  		return // outcome 1
   207  	}
   208  	parentObj.Name(key)
   209  	if !nestedPropertiesAreRedacted {
   210  		value.WriteToJSONWriter(w) // writes the whole value unchanged
   211  		return                     // outcome 2
   212  	}
   213  	subObj := w.Object() // writes the opening brace for the output object
   214  
   215  	objectKeys := make([]string, 0, 50) // arbitrary capacity, expanded if necessary by value.Keys()
   216  	for _, subKey := range value.Keys(objectKeys) {
   217  		subValue := value.GetByKey(subKey)
   218  		// recurse to write or not write each property - outcome 3
   219  		f.writeFilteredAttribute(w, c, &subObj, path, subKey, subValue, redactedAttrs)
   220  	}
   221  	subObj.End() // writes the closing brace for the output object
   222  }
   223  
   224  // maybeRedact is called by writeFilteredAttribute to decide whether or not a given value (or,
   225  // possibly, properties within it) should be considered private, based on the private attribute
   226  // references in either 1. the eventContextFormatter configuration or 2. this specific Context.
   227  //
   228  // If the value should be private, then the first return value is true, and also the attribute
   229  // reference is added to redactedAttrs.
   230  //
   231  // The second return value indicates whether there are any private attribute references
   232  // designating properties *within* this value. That is, if attrPath is ["address"], and the
   233  // configuration says that "/address/street" is private, then the second return value will be
   234  // true, which tells us that we can't just dump the value of the "address" object directly into
   235  // the output but will need to filter its properties.
   236  //
   237  // Note that even though an AttrRef can contain numeric path components to represent an array
   238  // element lookup, for the purposes of flag evaluations (like "/animals/0" which conceptually
   239  // represents context.animals[0]), those will not work as private attribute references since
   240  // we do not recurse to redact anything within an array value. A reference like "/animals/0"
   241  // would only work if context.animals were an object with a property named "0".
   242  //
   243  // If allAttributesPrivate is true, this method is never called.
   244  func (f *eventContextFormatter) maybeRedact(
   245  	c *ldcontext.Context,
   246  	attrPath []string,
   247  	valueType ldvalue.ValueType,
   248  	redactedAttrs *[]string,
   249  ) (bool, bool) {
   250  	// First check against the eventContextFormatter configuration.
   251  	redactedAttrRef, nestedPropertiesAreRedacted := f.checkGlobalPrivateAttrRefs(attrPath)
   252  	if redactedAttrRef != nil {
   253  		*redactedAttrs = append(*redactedAttrs, redactedAttrRef.String())
   254  		return true, false
   255  		// true, false = "this attribute itself is redacted, never mind its children"
   256  	}
   257  
   258  	shouldCheckForNestedProperties := valueType == ldvalue.ObjectType
   259  
   260  	// Now check the per-Context configuration. Unlike the eventContextFormatter configuration, this
   261  	// does not have a lookup map, just a list of AttrRefs.
   262  	for i := 0; i < c.PrivateAttributeCount(); i++ {
   263  		a, _ := c.PrivateAttributeByIndex(i)
   264  		depth := a.Depth()
   265  		if depth < len(attrPath) {
   266  			// If the attribute reference is shorter than the current path, then it can't possibly be a match,
   267  			// because if it had matched the first part of our path, we wouldn't have recursed this far.
   268  			continue
   269  		}
   270  		if !shouldCheckForNestedProperties && depth > len(attrPath) {
   271  			continue
   272  		}
   273  		match := true
   274  		for j := 0; j < len(attrPath); j++ {
   275  			name := a.Component(j)
   276  			if name != attrPath[j] {
   277  				match = false
   278  				break
   279  			}
   280  		}
   281  		if match {
   282  			if depth == len(attrPath) {
   283  				*redactedAttrs = append(*redactedAttrs, a.String())
   284  				return true, false
   285  				// true, false = "this attribute itself is redacted, never mind its children"
   286  			}
   287  			nestedPropertiesAreRedacted = true
   288  		}
   289  	}
   290  	return false, nestedPropertiesAreRedacted // false = "this attribute itself is not redacted"
   291  }
   292  
   293  // Checks whether the given attribute or subproperty matches any AttrRef that was designated as
   294  // private in the SDK options given to newEventContextFormatter.
   295  //
   296  // If attrPath has just one element, it is the name of a top-level attribute. If it has multiple
   297  // elements, it is a path to a property within a custom object attribute: for instance, if you
   298  // represented the overall context as a JSON object, the attrPath ["billing", "address", "street"]
   299  // would refer to the street property within something like {"billing": {"address": {"street": "x"}}}.
   300  //
   301  // The first return value is nil if the attribute does not need to be redacted; otherwise it is the
   302  // specific attribute reference that was matched.
   303  //
   304  // The second return value is true if and only if there's at least one configured private
   305  // attribute reference for *children* of attrPath (and there is not one for attrPath itself, since if
   306  // there was, we would not bother recursing to write the children). See comments on writeFilteredAttribute.
   307  func (f eventContextFormatter) checkGlobalPrivateAttrRefs(attrPath []string) (
   308  	redactedAttrRef *ldattr.Ref, nestedPropertiesAreRedacted bool,
   309  ) {
   310  	redactedAttrRef = nil
   311  	nestedPropertiesAreRedacted = false
   312  	lookup := f.privateAttributes
   313  	if lookup == nil {
   314  		return
   315  	}
   316  	for i, pathComponent := range attrPath {
   317  		nextNode := lookup[pathComponent]
   318  		if nextNode == nil {
   319  			break
   320  		}
   321  		if i == len(attrPath)-1 {
   322  			if nextNode.attribute != nil {
   323  				redactedAttrRef = nextNode.attribute
   324  				return
   325  			}
   326  			nestedPropertiesAreRedacted = true
   327  			return
   328  		} else if nextNode.children != nil {
   329  			lookup = nextNode.children
   330  			continue
   331  		}
   332  	}
   333  	return
   334  }
   335  

View as plain text