Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Mar 15, 2024
1 parent 475727f commit cdad89a
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 21 deletions.
21 changes: 12 additions & 9 deletions src/LaunchDarkly/Impl/Events/EventSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,34 +48,36 @@ public function serializeEvents(array $events): string

private function filterEvent(array $e): array
{
$isFeatureEvent = ($e['kind'] ?? '') == 'feature';

$ret = [];
foreach ($e as $key => $value) {
if ($key == 'context') {
$ret[$key] = $this->serializeContext($value);
$ret[$key] = $this->serializeContext($value, $isFeatureEvent);
} else {
$ret[$key] = $value;
}
}
return $ret;
}

private function serializeContext(LDContext $context): array
private function serializeContext(LDContext $context, bool $redactAnonymousAttributes): array
{
if ($context->isMultiple()) {
$ret = ['kind' => 'multi'];
for ($i = 0; $i < $context->getIndividualContextCount(); $i++) {
$c = $context->getIndividualContext($i);
if ($c !== null) {
$ret[$c->getKind()] = $this->serializeContextSingleKind($c, false);
$ret[$c->getKind()] = $this->serializeContextSingleKind($c, false, $redactAnonymousAttributes);
}
}
return $ret;
} else {
return $this->serializeContextSingleKind($context, true);
return $this->serializeContextSingleKind($context, true, $redactAnonymousAttributes);
}
}

private function serializeContextSingleKind(LDContext $c, bool $includeKind): array
private function serializeContextSingleKind(LDContext $c, bool $includeKind, bool $redactAnonymousAttributes): array
{
$ret = ['key' => $c->getKey()];
if ($includeKind) {
Expand All @@ -86,11 +88,12 @@ private function serializeContextSingleKind(LDContext $c, bool $includeKind): ar
}
$redacted = [];
$allPrivate = array_merge($this->_privateAttributes, $c->getPrivateAttributes() ?? []);
if ($c->getName() !== null && !$this->checkWholeAttributePrivate('name', $allPrivate, $redacted)) {
$redactAllAttributes = $this->_allAttributesPrivate || ($redactAnonymousAttributes && $c->isAnonymous());
if ($c->getName() !== null && !$this->checkWholeAttributePrivate('name', $allPrivate, $redacted, $redactAllAttributes)) {
$ret['name'] = $c->getName();
}
foreach ($c->getCustomAttributeNames() as $attr) {
if (!$this->checkWholeAttributePrivate($attr, $allPrivate, $redacted)) {
if (!$this->checkWholeAttributePrivate($attr, $allPrivate, $redacted, $redactAllAttributes)) {
$value = $c->get($attr);
$ret[$attr] = self::redactJsonValue(null, $attr, $value, $allPrivate, $redacted);
}
Expand All @@ -101,9 +104,9 @@ private function serializeContextSingleKind(LDContext $c, bool $includeKind): ar
return $ret;
}

private function checkWholeAttributePrivate(string $attr, array $allPrivate, array &$redactedOut): bool
private function checkWholeAttributePrivate(string $attr, array $allPrivate, array &$redactedOut, bool $redactAllAttributes): bool
{
if ($this->_allAttributesPrivate) {
if ($redactAllAttributes) {
$redactedOut[] = $attr;
return true;
}
Expand Down
4 changes: 3 additions & 1 deletion test-service/TestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ public function getStatus(): array
'context-type',
'secure-mode-hash',
'migrations',
'event-sampling'
'event-sampling',
'inline-context',
'anonymous-redaction'
],
'clientVersion' => \LaunchDarkly\LDClient::VERSION
];
Expand Down
107 changes: 96 additions & 11 deletions tests/Impl/Events/EventSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ private function getContext(): LDContext
->set('firstName', 'Sue')
->build();
}

private function getContextSpecifyingOwnPrivateAttr()
{
return LDContext::builder('abc')
Expand All @@ -26,7 +26,7 @@ private function getContextSpecifyingOwnPrivateAttr()
->private('dizzle')
->build();
}

private function getFullContextResult()
{
return [
Expand All @@ -37,7 +37,7 @@ private function getFullContextResult()
'dizzle' => 'ghi'
];
}

private function getContextResultWithAllAttrsHidden()
{
return [
Expand All @@ -48,7 +48,7 @@ private function getContextResultWithAllAttrsHidden()
]
];
}

private function getContextResultWithSomeAttrsHidden()
{
return [
Expand All @@ -60,7 +60,7 @@ private function getContextResultWithSomeAttrsHidden()
]
];
}

private function getContextResultWithOwnSpecifiedAttrHidden()
{
return [
Expand All @@ -73,7 +73,7 @@ private function getContextResultWithOwnSpecifiedAttrHidden()
]
];
}

private function makeEvent($context)
{
return [
Expand All @@ -83,14 +83,14 @@ private function makeEvent($context)
'context' => $context
];
}

private function getJsonForContextBySerializingEvent($user)
{
$es = new EventSerializer([]);
$event = $this->makeEvent($user);
return json_decode($es->serializeEvents([$event]), true)[0]['context'];
}

public function testAllContextAttrsSerialized()
{
$es = new EventSerializer([]);
Expand All @@ -108,7 +108,92 @@ public function testAllContextAttrsPrivate()
$expected = $this->makeEvent($this->getContextResultWithAllAttrsHidden());
$this->assertEquals([$expected], json_decode($json, true));
}


public function testRedactsAllAttributesFromAnonymousContextWithFeatureEvent()
{
$anonymousContext = LDContext::builder('abc')
->anonymous(true)
->set('bizzle', 'def')
->set('dizzle', 'ghi')
->set('firstName', 'Sue')
->build();

$es = new EventSerializer([]);
$event = $this->makeEvent($anonymousContext);
$event['kind'] = 'feature';
$json = $es->serializeEvents([$event]);

// But we redact all attributes when the context is anonymous
$expectedContextOutput = $this->getContextResultWithAllAttrsHidden();
$expectedContextOutput['anonymous'] = true;

$expected = $this->makeEvent($expectedContextOutput);
$expected['kind'] = 'feature';

$this->assertEquals([$expected], json_decode($json, true));
}

public function testDoesNotRedactAttributesFromAnonymousContextWithNonFeatureEvent()
{
$anonymousContext = LDContext::builder('abc')
->anonymous(true)
->set('bizzle', 'def')
->set('dizzle', 'ghi')
->set('firstName', 'Sue')
->build();

$es = new EventSerializer([]);
$event = $this->makeEvent($anonymousContext);
$json = $es->serializeEvents([$event]);

// But we redact all attributes when the context is anonymous
$expectedContextOutput = $this->getFullContextResult();
$expectedContextOutput['anonymous'] = true;

$expected = $this->makeEvent($expectedContextOutput);

$this->assertEquals([$expected], json_decode($json, true));
}

public function testRedactsAllAttributesOnlyIfContextIsAnonymous()
{
$userContext = LDContext::builder('user-key')
->kind('user')
->anonymous(true)
->name('Example user')
->build();

$orgContext = LDContext::builder('org-key')
->kind('org')
->anonymous(false)
->name('Example org')
->build();

$multiContext = LDContext::createMulti($userContext, $orgContext);

$es = new EventSerializer([]);
$event = $this->makeEvent($multiContext);
$event['kind'] = 'feature';
$json = $es->serializeEvents([$event]);

$expectedContextOutput = [
'kind' => 'multi',
'user' => [
'key' => 'user-key',
'anonymous' => true,
'_meta' => ['redactedAttributes' => ['name']]
],
'org' => [
'key' => 'org-key',
'name' => 'Example org',
],
];
$expected = $this->makeEvent($expectedContextOutput);
$expected['kind'] = 'feature';

$this->assertEquals([$expected], json_decode($json, true));
}

public function testSomeContextAttrsPrivate()
{
$es = new EventSerializer(['private_attribute_names' => ['firstName', 'bizzle']]);
Expand All @@ -117,7 +202,7 @@ public function testSomeContextAttrsPrivate()
$expected = $this->makeEvent($this->getContextResultWithSomeAttrsHidden());
$this->assertEquals([$expected], json_decode($json, true));
}

public function testPerContextPrivateAttr()
{
$es = new EventSerializer([]);
Expand All @@ -135,7 +220,7 @@ public function testPerContextPrivateAttrPlusGlobalPrivateAttrs()
$expected = $this->makeEvent($this->getContextResultWithAllAttrsHidden());
$this->assertEquals([$expected], json_decode($json, true));
}

public function testObjectPropertyRedaction()
{
$es = new EventSerializer(['private_attribute_names' => ['/b/prop1', '/c/prop2/sub1']]);
Expand Down

0 comments on commit cdad89a

Please sign in to comment.