Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Mar 13, 2024
1 parent 8b6f37b commit d208612
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 13 deletions.
3 changes: 2 additions & 1 deletion contract-tests/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def status():
'migrations',
'event-sampling',
'polling-gzip',
'inline-context'
'inline-context',
'anonymous-redaction',
]
}
return (json.dumps(body), 200, {'Content-type': 'application/json'})
Expand Down
32 changes: 25 additions & 7 deletions ldclient/impl/events/event_context_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,38 @@ def __init__(self, all_attributes_private: bool, private_attributes: List[str]):
self._private_attributes.append(ar)

def format_context(self, context: Context) -> Dict:
"""
Formats a context for use in an analytic event, performing any
necessary attribute redaction.
"""
return self._format_context(context, False)

def format_context_redact_anonymous(self, context: Context) -> Dict:
"""
Formats a context for use in an analytic event, performing any
necessary attribute redaction.
If a context is anonoymous, all attributes will be redacted except for
key, kind, and anonoymous.
"""
return self._format_context(context, True)

def _format_context(self, context: Context, redact_anonymous: bool) -> Dict:
if context.multiple:
out = {'kind': 'multi'} # type: Dict[str, Any]
for i in range(context.individual_context_count):
c = context.get_individual_context(i)
if c is not None:
out[c.kind] = self._format_context_single(c, False)
out[c.kind] = self._format_context_single(c, False, redact_anonymous)
return out
else:
return self._format_context_single(context, True)
return self._format_context_single(context, True, redact_anonymous)

def _format_context_single(self, context: Context, include_kind: bool) -> Dict:
def _format_context_single(self, context: Context, include_kind: bool, redact_anonymous: bool) -> Dict:
out = {'key': context.key} # type: Dict[str, Any]
if include_kind:
out['kind'] = context.kind

if context.anonymous:
out['anonymous'] = True

Expand All @@ -44,11 +62,11 @@ def _format_context_single(self, context: Context, include_kind: bool) -> Dict:
if ar.valid:
all_private.append(ar)

if context.name is not None and not self._check_whole_attr_private('name', all_private, redacted):
if context.name is not None and not self._check_whole_attr_private('name', all_private, redacted, context.anonymous and redact_anonymous):
out['name'] = context.name

for attr in context.custom_attributes:
if not self._check_whole_attr_private(attr, all_private, redacted):
if not self._check_whole_attr_private(attr, all_private, redacted, context.anonymous and redact_anonymous):
value = context.get(attr)
out[attr] = self._redact_json_value(None, attr, value, all_private, redacted)

Expand All @@ -57,8 +75,8 @@ def _format_context_single(self, context: Context, include_kind: bool) -> Dict:

return out

def _check_whole_attr_private(self, attr: str, all_private: List[AttributeRef], redacted: List[str]) -> bool:
if self._all_attributes_private:
def _check_whole_attr_private(self, attr: str, all_private: List[AttributeRef], redacted: List[str], redact_all: bool) -> bool:
if self._all_attributes_private or redact_all:
redacted.append(attr)
return True
for p in all_private:
Expand Down
13 changes: 8 additions & 5 deletions ldclient/impl/events/event_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,23 @@ def make_output_events(self, events: List[Any], summary: EventSummary):
def make_output_event(self, e: Any):
if isinstance(e, EventInputEvaluation):
out = self._base_eval_props(e, 'feature')
out['context'] = self._process_context(e.context)
out['context'] = self._process_context(e.context, True)
return out
elif isinstance(e, DebugEvent):
out = self._base_eval_props(e.original_input, 'debug')
out['context'] = self._process_context(e.original_input.context)
out['context'] = self._process_context(e.original_input.context, False)
return out
elif isinstance(e, EventInputIdentify):
return {
'kind': 'identify',
'creationDate': e.timestamp,
'context': self._process_context(e.context)
'context': self._process_context(e.context, False)
}
elif isinstance(e, IndexEvent):
return {
'kind': 'index',
'creationDate': e.timestamp,
'context': self._process_context(e.context)
'context': self._process_context(e.context, False)
}
elif isinstance(e, EventInputCustom):
out = {
Expand Down Expand Up @@ -193,7 +193,10 @@ def make_summary_event(self, summary: EventSummary):
'features': flags_out
}

def _process_context(self, context: Context):
def _process_context(self, context: Context, redact_anonymous: bool):
if redact_anonymous:
return self._context_formatter.format_context_redact_anonymous(context)

return self._context_formatter.format_context(context)

def _context_keys(self, context: Context):
Expand Down
35 changes: 35 additions & 0 deletions testing/impl/events/test_event_context_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ def test_context_with_more_attributes():
'd': 2
}

def test_context_can_redact_anonymous_attributes():
f = EventContextFormatter(False, [])
c = Context.builder('a').name('b').anonymous(True).set('c', True).set('d', 2).build()
assert f.format_context_redact_anonymous(c) == {
'kind': 'user',
'key': 'a',
'anonymous': True,
'_meta': {
'redactedAttributes': ['name', 'c', 'd']
}
}

def test_multi_kind_context_can_redact_anonymous_attributes():
f = EventContextFormatter(False, [])
user = Context.builder('user-key').name('b').anonymous(True).set('c', True).set('d', 2).build()
org = Context.builder('org-key').kind('org').name('b').set('c', True).set('d', 2).build()
multi = Context.create_multi(user, org)

assert f.format_context_redact_anonymous(multi) == {
'kind': 'multi',
'user': {
'key': 'user-key',
'anonymous': True,
'_meta': {
'redactedAttributes': ['name', 'c', 'd']
}
},
'org': {
'key': 'org-key',
'name': 'b',
'c': True,
'd': 2
}
}

def test_multi_context():
f = EventContextFormatter(False, [])
c = Context.create_multi(
Expand Down

0 comments on commit d208612

Please sign in to comment.