Skip to content

Commit cdad89a

Browse files
committed
feat: Redact anonymous attributes within feature events (#193)
1 parent 475727f commit cdad89a

File tree

3 files changed

+111
-21
lines changed

3 files changed

+111
-21
lines changed

src/LaunchDarkly/Impl/Events/EventSerializer.php

+12-9
Original file line numberDiff line numberDiff line change
@@ -48,34 +48,36 @@ public function serializeEvents(array $events): string
4848

4949
private function filterEvent(array $e): array
5050
{
51+
$isFeatureEvent = ($e['kind'] ?? '') == 'feature';
52+
5153
$ret = [];
5254
foreach ($e as $key => $value) {
5355
if ($key == 'context') {
54-
$ret[$key] = $this->serializeContext($value);
56+
$ret[$key] = $this->serializeContext($value, $isFeatureEvent);
5557
} else {
5658
$ret[$key] = $value;
5759
}
5860
}
5961
return $ret;
6062
}
6163

62-
private function serializeContext(LDContext $context): array
64+
private function serializeContext(LDContext $context, bool $redactAnonymousAttributes): array
6365
{
6466
if ($context->isMultiple()) {
6567
$ret = ['kind' => 'multi'];
6668
for ($i = 0; $i < $context->getIndividualContextCount(); $i++) {
6769
$c = $context->getIndividualContext($i);
6870
if ($c !== null) {
69-
$ret[$c->getKind()] = $this->serializeContextSingleKind($c, false);
71+
$ret[$c->getKind()] = $this->serializeContextSingleKind($c, false, $redactAnonymousAttributes);
7072
}
7173
}
7274
return $ret;
7375
} else {
74-
return $this->serializeContextSingleKind($context, true);
76+
return $this->serializeContextSingleKind($context, true, $redactAnonymousAttributes);
7577
}
7678
}
7779

78-
private function serializeContextSingleKind(LDContext $c, bool $includeKind): array
80+
private function serializeContextSingleKind(LDContext $c, bool $includeKind, bool $redactAnonymousAttributes): array
7981
{
8082
$ret = ['key' => $c->getKey()];
8183
if ($includeKind) {
@@ -86,11 +88,12 @@ private function serializeContextSingleKind(LDContext $c, bool $includeKind): ar
8688
}
8789
$redacted = [];
8890
$allPrivate = array_merge($this->_privateAttributes, $c->getPrivateAttributes() ?? []);
89-
if ($c->getName() !== null && !$this->checkWholeAttributePrivate('name', $allPrivate, $redacted)) {
91+
$redactAllAttributes = $this->_allAttributesPrivate || ($redactAnonymousAttributes && $c->isAnonymous());
92+
if ($c->getName() !== null && !$this->checkWholeAttributePrivate('name', $allPrivate, $redacted, $redactAllAttributes)) {
9093
$ret['name'] = $c->getName();
9194
}
9295
foreach ($c->getCustomAttributeNames() as $attr) {
93-
if (!$this->checkWholeAttributePrivate($attr, $allPrivate, $redacted)) {
96+
if (!$this->checkWholeAttributePrivate($attr, $allPrivate, $redacted, $redactAllAttributes)) {
9497
$value = $c->get($attr);
9598
$ret[$attr] = self::redactJsonValue(null, $attr, $value, $allPrivate, $redacted);
9699
}
@@ -101,9 +104,9 @@ private function serializeContextSingleKind(LDContext $c, bool $includeKind): ar
101104
return $ret;
102105
}
103106

104-
private function checkWholeAttributePrivate(string $attr, array $allPrivate, array &$redactedOut): bool
107+
private function checkWholeAttributePrivate(string $attr, array $allPrivate, array &$redactedOut, bool $redactAllAttributes): bool
105108
{
106-
if ($this->_allAttributesPrivate) {
109+
if ($redactAllAttributes) {
107110
$redactedOut[] = $attr;
108111
return true;
109112
}

test-service/TestService.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ public function getStatus(): array
7575
'context-type',
7676
'secure-mode-hash',
7777
'migrations',
78-
'event-sampling'
78+
'event-sampling',
79+
'inline-context',
80+
'anonymous-redaction'
7981
],
8082
'clientVersion' => \LaunchDarkly\LDClient::VERSION
8183
];

tests/Impl/Events/EventSerializerTest.php

+96-11
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ private function getContext(): LDContext
1616
->set('firstName', 'Sue')
1717
->build();
1818
}
19-
19+
2020
private function getContextSpecifyingOwnPrivateAttr()
2121
{
2222
return LDContext::builder('abc')
@@ -26,7 +26,7 @@ private function getContextSpecifyingOwnPrivateAttr()
2626
->private('dizzle')
2727
->build();
2828
}
29-
29+
3030
private function getFullContextResult()
3131
{
3232
return [
@@ -37,7 +37,7 @@ private function getFullContextResult()
3737
'dizzle' => 'ghi'
3838
];
3939
}
40-
40+
4141
private function getContextResultWithAllAttrsHidden()
4242
{
4343
return [
@@ -48,7 +48,7 @@ private function getContextResultWithAllAttrsHidden()
4848
]
4949
];
5050
}
51-
51+
5252
private function getContextResultWithSomeAttrsHidden()
5353
{
5454
return [
@@ -60,7 +60,7 @@ private function getContextResultWithSomeAttrsHidden()
6060
]
6161
];
6262
}
63-
63+
6464
private function getContextResultWithOwnSpecifiedAttrHidden()
6565
{
6666
return [
@@ -73,7 +73,7 @@ private function getContextResultWithOwnSpecifiedAttrHidden()
7373
]
7474
];
7575
}
76-
76+
7777
private function makeEvent($context)
7878
{
7979
return [
@@ -83,14 +83,14 @@ private function makeEvent($context)
8383
'context' => $context
8484
];
8585
}
86-
86+
8787
private function getJsonForContextBySerializingEvent($user)
8888
{
8989
$es = new EventSerializer([]);
9090
$event = $this->makeEvent($user);
9191
return json_decode($es->serializeEvents([$event]), true)[0]['context'];
9292
}
93-
93+
9494
public function testAllContextAttrsSerialized()
9595
{
9696
$es = new EventSerializer([]);
@@ -108,7 +108,92 @@ public function testAllContextAttrsPrivate()
108108
$expected = $this->makeEvent($this->getContextResultWithAllAttrsHidden());
109109
$this->assertEquals([$expected], json_decode($json, true));
110110
}
111-
111+
112+
public function testRedactsAllAttributesFromAnonymousContextWithFeatureEvent()
113+
{
114+
$anonymousContext = LDContext::builder('abc')
115+
->anonymous(true)
116+
->set('bizzle', 'def')
117+
->set('dizzle', 'ghi')
118+
->set('firstName', 'Sue')
119+
->build();
120+
121+
$es = new EventSerializer([]);
122+
$event = $this->makeEvent($anonymousContext);
123+
$event['kind'] = 'feature';
124+
$json = $es->serializeEvents([$event]);
125+
126+
// But we redact all attributes when the context is anonymous
127+
$expectedContextOutput = $this->getContextResultWithAllAttrsHidden();
128+
$expectedContextOutput['anonymous'] = true;
129+
130+
$expected = $this->makeEvent($expectedContextOutput);
131+
$expected['kind'] = 'feature';
132+
133+
$this->assertEquals([$expected], json_decode($json, true));
134+
}
135+
136+
public function testDoesNotRedactAttributesFromAnonymousContextWithNonFeatureEvent()
137+
{
138+
$anonymousContext = LDContext::builder('abc')
139+
->anonymous(true)
140+
->set('bizzle', 'def')
141+
->set('dizzle', 'ghi')
142+
->set('firstName', 'Sue')
143+
->build();
144+
145+
$es = new EventSerializer([]);
146+
$event = $this->makeEvent($anonymousContext);
147+
$json = $es->serializeEvents([$event]);
148+
149+
// But we redact all attributes when the context is anonymous
150+
$expectedContextOutput = $this->getFullContextResult();
151+
$expectedContextOutput['anonymous'] = true;
152+
153+
$expected = $this->makeEvent($expectedContextOutput);
154+
155+
$this->assertEquals([$expected], json_decode($json, true));
156+
}
157+
158+
public function testRedactsAllAttributesOnlyIfContextIsAnonymous()
159+
{
160+
$userContext = LDContext::builder('user-key')
161+
->kind('user')
162+
->anonymous(true)
163+
->name('Example user')
164+
->build();
165+
166+
$orgContext = LDContext::builder('org-key')
167+
->kind('org')
168+
->anonymous(false)
169+
->name('Example org')
170+
->build();
171+
172+
$multiContext = LDContext::createMulti($userContext, $orgContext);
173+
174+
$es = new EventSerializer([]);
175+
$event = $this->makeEvent($multiContext);
176+
$event['kind'] = 'feature';
177+
$json = $es->serializeEvents([$event]);
178+
179+
$expectedContextOutput = [
180+
'kind' => 'multi',
181+
'user' => [
182+
'key' => 'user-key',
183+
'anonymous' => true,
184+
'_meta' => ['redactedAttributes' => ['name']]
185+
],
186+
'org' => [
187+
'key' => 'org-key',
188+
'name' => 'Example org',
189+
],
190+
];
191+
$expected = $this->makeEvent($expectedContextOutput);
192+
$expected['kind'] = 'feature';
193+
194+
$this->assertEquals([$expected], json_decode($json, true));
195+
}
196+
112197
public function testSomeContextAttrsPrivate()
113198
{
114199
$es = new EventSerializer(['private_attribute_names' => ['firstName', 'bizzle']]);
@@ -117,7 +202,7 @@ public function testSomeContextAttrsPrivate()
117202
$expected = $this->makeEvent($this->getContextResultWithSomeAttrsHidden());
118203
$this->assertEquals([$expected], json_decode($json, true));
119204
}
120-
205+
121206
public function testPerContextPrivateAttr()
122207
{
123208
$es = new EventSerializer([]);
@@ -135,7 +220,7 @@ public function testPerContextPrivateAttrPlusGlobalPrivateAttrs()
135220
$expected = $this->makeEvent($this->getContextResultWithAllAttrsHidden());
136221
$this->assertEquals([$expected], json_decode($json, true));
137222
}
138-
223+
139224
public function testObjectPropertyRedaction()
140225
{
141226
$es = new EventSerializer(['private_attribute_names' => ['/b/prop1', '/c/prop2/sub1']]);

0 commit comments

Comments
 (0)