Skip to content

Commit 08704a9

Browse files
authored
[FF-4069] Update plain and obfuscated configs for flags for variation of type STRING with special characters in values (#119)
* feat: update plain and obfuscated configs for flags, add obfuscation utility * doc: update readme
1 parent 5c2b812 commit 08704a9

File tree

8 files changed

+27846
-82
lines changed

8 files changed

+27846
-82
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SDK test data
2+
## Description
3+
This is a set of test cases bundled as UFC - universal flag configuration. The purpose of these test cases is to ensure SDK libraries are compliant with core application.
4+
5+
## Dependencies
6+
Node.JS v18, Jest v29, Typescript v4
7+
8+
## Usage
9+
1. install dependencies by runnning CLI command `yarn install`.
10+
2. Update content of the [flags-v1.json](ufc/flags-v1.json).
11+
3. Validate tests by runnning CLI command `yarn run validate:tests`.
12+
4. Obfuscate file [flags-v1.json](ufc/flags-v1.json) by runnning CLI command `yarn run obfuscate:ufc`. It will update file [flags-v1-obfuscated.json](ufc/flags-v1-obfuscated.json) with obfuscated version of the file [flags-v1.json](ufc/flags-v1.json).

obfuscation/obfuscation.helper.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { createHash } from 'crypto';
2+
3+
import {
4+
AllocationDto,
5+
BanditFlagVariationDto,
6+
BanditReferenceDto,
7+
FlagDto,
8+
RuleDto,
9+
ShardDto,
10+
SplitDto,
11+
UFCFormatEnum,
12+
UniversalFlagConfig,
13+
VariationDto,
14+
ITargetingRuleCondition
15+
} from './ufc.dto';
16+
17+
export function md5Hash(input: string): string {
18+
return createHash('md5').update(input).digest('hex');
19+
}
20+
21+
export function base64Encode(input: string): string {
22+
return Buffer.from(input).toString('base64');
23+
}
24+
25+
export function obfuscateUniversalFlagConfig(ufc: UniversalFlagConfig): UniversalFlagConfig {
26+
// Start building a new, obfuscated, UniversalFlagConfig
27+
// We opted to do this instead of starting with a copy and mutating in-place for two reasons:
28+
// 1. The code is easier to understand as it doesn't mutate in-place
29+
// 2. If anything sensitive is added to the UFC but forgotten to be explicitly obfuscated, it could slip through
30+
const obfuscatedUfc = new UniversalFlagConfig();
31+
32+
// update format
33+
obfuscatedUfc.format = UFCFormatEnum.CLIENT;
34+
35+
// copy over fields that are not obfuscated
36+
obfuscatedUfc.environment = ufc.environment;
37+
if (ufc.createdAt) {
38+
obfuscatedUfc.createdAt = ufc.createdAt;
39+
}
40+
41+
// Obfuscate flags
42+
obfuscatedUfc.flags = {};
43+
Object.values(ufc.flags).forEach((flagDto) => {
44+
const obfuscatedFlagDto = obfuscateFlag(flagDto);
45+
obfuscatedUfc.flags[obfuscatedFlagDto.key] = obfuscatedFlagDto;
46+
});
47+
48+
// Obfuscate bandits
49+
if (ufc.bandits) {
50+
const obfuscatedBandits: Record<string, BanditFlagVariationDto[]> = {};
51+
Object.entries(ufc.bandits).forEach(([banditKey, banditFlagVariations]) => {
52+
obfuscatedBandits[md5Hash(banditKey)] = banditFlagVariations.map(
53+
obfuscateBanditFlagVariation,
54+
);
55+
});
56+
obfuscatedUfc.bandits = obfuscatedBandits;
57+
}
58+
if (ufc.banditReferences) {
59+
const obfuscatedBanditReferences: Record<string, BanditReferenceDto> = {};
60+
Object.entries(ufc.banditReferences).forEach(([banditKey, banditReference]) => {
61+
obfuscatedBanditReferences[md5Hash(banditKey)] = obfuscateBanditReference(banditReference);
62+
});
63+
obfuscatedUfc.banditReferences = obfuscatedBanditReferences;
64+
}
65+
return obfuscatedUfc;
66+
}
67+
68+
function obfuscateFlag(flagDto: FlagDto): FlagDto {
69+
const obfuscatedFlagDto = new FlagDto();
70+
71+
// We hash the flag key as the SDK will have an exact value to hash and compare
72+
obfuscatedFlagDto.key = md5Hash(flagDto.key);
73+
74+
// copy over fields we are not obfuscating
75+
obfuscatedFlagDto.enabled = flagDto.enabled;
76+
obfuscatedFlagDto.variationType = flagDto.variationType;
77+
obfuscatedFlagDto.totalShards = flagDto.totalShards;
78+
79+
// Obfuscate variations
80+
obfuscatedFlagDto.variations = {};
81+
Object.values(flagDto.variations).forEach((variationDto) => {
82+
const obfuscatedVariationDto = obfuscateVariationDto(variationDto);
83+
obfuscatedFlagDto.variations[obfuscatedVariationDto.key] = obfuscatedVariationDto;
84+
});
85+
86+
// Obfuscate allocations (Note: we map as it's an array, and we want to preserve order)
87+
obfuscatedFlagDto.allocations = flagDto.allocations.map(obfuscateAllocation);
88+
89+
return obfuscatedFlagDto;
90+
}
91+
92+
function obfuscateVariationDto(variationDto: VariationDto): VariationDto {
93+
const obfuscatedVariationDto = new VariationDto();
94+
obfuscatedVariationDto.key = base64Encode(variationDto.key);
95+
96+
// Obfuscate any non-null/non-undefined (note loose comparison) values
97+
obfuscatedVariationDto.value =
98+
variationDto.value != null ? base64Encode(variationDto.value.toString()) : variationDto.value;
99+
100+
// Copy over algorithmType, unobfuscated, if present
101+
if (variationDto.algorithmType) {
102+
obfuscatedVariationDto.algorithmType = variationDto.algorithmType;
103+
}
104+
105+
return obfuscatedVariationDto;
106+
}
107+
108+
function obfuscateAllocation(allocationDto: AllocationDto): AllocationDto {
109+
const obfuscatedAllocationDto = new AllocationDto();
110+
obfuscatedAllocationDto.key = base64Encode(allocationDto.key);
111+
112+
// Leave doLog unobfuscated
113+
obfuscatedAllocationDto.doLog = allocationDto.doLog;
114+
115+
// Obfuscate startAt and endAt, if present
116+
if (allocationDto.startAt) {
117+
obfuscatedAllocationDto.startAt = base64Encode(allocationDto.startAt);
118+
}
119+
if (allocationDto.endAt) {
120+
obfuscatedAllocationDto.endAt = base64Encode(allocationDto.endAt);
121+
}
122+
123+
// Obfuscate rules, if present, conserving order
124+
if (allocationDto.rules) {
125+
obfuscatedAllocationDto.rules = allocationDto.rules.map(obfuscateRule);
126+
}
127+
128+
// Obfuscate splits, preserving order
129+
obfuscatedAllocationDto.splits = allocationDto.splits.map(obfuscateSplit);
130+
131+
return obfuscatedAllocationDto;
132+
}
133+
134+
function obfuscateRule(ruleDto: RuleDto): RuleDto {
135+
const obfuscatedRuleDto = new RuleDto();
136+
137+
// Obfuscate conditions, preserving order
138+
obfuscatedRuleDto.conditions = ruleDto.conditions.map(obfuscateConditionInPlace);
139+
return obfuscatedRuleDto;
140+
}
141+
142+
function obfuscateConditionInPlace(condition: ITargetingRuleCondition): ITargetingRuleCondition {
143+
// Note: we return the object in one shot instead of building out the keys as it's an interface not a DTO object
144+
return {
145+
operator: md5Hash(condition.operator), // We can hash, as the SDK knows the set of possibilities
146+
attribute: md5Hash(condition.attribute), // We can hash, as the SDK has the exact attributes to check for exact matches
147+
value: obfuscateConditionValueForOperator(condition.value, condition.operator), // How we obfuscate depends on the operator
148+
};
149+
}
150+
151+
function obfuscateConditionValueForOperator<T extends ITargetingRuleCondition>(
152+
value: T['value'],
153+
operator: string,
154+
): string | string[] {
155+
if (['ONE_OF', 'NOT_ONE_OF', 'IS_NULL'].includes(operator)) {
156+
// We hash values when it's an exact comparison because we can compare hashes
157+
if (Array.isArray(value)) {
158+
return value.map(md5Hash);
159+
} else {
160+
return md5Hash(value.toString());
161+
}
162+
}
163+
164+
// Everything else (e.g., inequalities, regular expressions) we need to just encode, so that we
165+
// can reverse for evaluation
166+
return base64Encode(value.toString());
167+
}
168+
169+
function obfuscateSplit(splitDto: SplitDto): SplitDto {
170+
const obfuscatedSplitDto = new SplitDto();
171+
obfuscatedSplitDto.variationKey = base64Encode(splitDto.variationKey);
172+
173+
// Obfuscate shards, if present, as their keys could contain sensitive information
174+
if (splitDto.shards) {
175+
obfuscatedSplitDto.shards = splitDto.shards.map(obfuscateShard);
176+
}
177+
178+
// Obfuscate extra logging, if present
179+
if (splitDto.extraLogging) {
180+
obfuscatedSplitDto.extraLogging = {};
181+
for (const [extraLoggingKey, extraLoggingValue] of Object.entries(splitDto.extraLogging)) {
182+
obfuscatedSplitDto.extraLogging[base64Encode(extraLoggingKey)] = base64Encode(
183+
extraLoggingValue,
184+
);
185+
}
186+
}
187+
188+
return obfuscatedSplitDto;
189+
}
190+
191+
function obfuscateShard(shardDto: ShardDto): ShardDto {
192+
const obfuscatedShardDto = new ShardDto();
193+
194+
// Copy over ranges unobfuscated
195+
obfuscatedShardDto.ranges = structuredClone(shardDto.ranges);
196+
197+
// Obfuscate salt
198+
obfuscatedShardDto.salt = base64Encode(shardDto.salt);
199+
200+
return obfuscatedShardDto;
201+
}
202+
203+
function obfuscateBanditReference(properties: BanditReferenceDto) {
204+
const obfuscatedProperties = new BanditReferenceDto();
205+
obfuscatedProperties.modelVersion = properties.modelVersion
206+
? md5Hash(properties.modelVersion)
207+
: null;
208+
obfuscatedProperties.flagVariations = properties.flagVariations.map(obfuscateBanditFlagVariation);
209+
return obfuscatedProperties;
210+
}
211+
212+
function obfuscateBanditFlagVariation(banditFlagVariation: BanditFlagVariationDto) {
213+
const obfuscatedBanditFlagVariation = new BanditFlagVariationDto();
214+
// Everything can be hashed as we'll have the values and be doing direct lookups
215+
obfuscatedBanditFlagVariation.key = md5Hash(banditFlagVariation.key);
216+
obfuscatedBanditFlagVariation.flagKey = md5Hash(banditFlagVariation.flagKey);
217+
obfuscatedBanditFlagVariation.allocationKey = md5Hash(banditFlagVariation.allocationKey);
218+
obfuscatedBanditFlagVariation.variationKey = md5Hash(banditFlagVariation.variationKey);
219+
obfuscatedBanditFlagVariation.variationValue = md5Hash(banditFlagVariation.variationValue);
220+
221+
return obfuscatedBanditFlagVariation;
222+
}

obfuscation/obfuscation.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
import * as fs from "fs";
3+
import * as path from "path";
4+
import * as ufcFlags from "../ufc/flags-v1.json";
5+
import { obfuscateUniversalFlagConfig } from "./obfuscation.helper";
6+
import { UniversalFlagConfig } from "./ufc.dto";
7+
8+
describe('Obfuscate UFC config', () => {
9+
10+
it("Obfuscate UFC flags config", () => {
11+
const ufc = ufcFlags as UniversalFlagConfig;
12+
const resultObfuscatedUfc = obfuscateUniversalFlagConfig(ufc);
13+
fs.writeFileSync(path.join(__dirname, "../ufc/flags-v1-obfuscated.json"), JSON.stringify(resultObfuscatedUfc, null, 2));
14+
});
15+
16+
});

obfuscation/ufc.dto.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
export interface ITargetingRuleCondition {
2+
operator: string;
3+
4+
attribute: string;
5+
6+
value: number | boolean | string | string[];
7+
}
8+
9+
export enum ExperimentVariationAlgorithmTypeEnum {
10+
CONSTANT = 'CONSTANT',
11+
CONTEXTUAL_BANDIT = 'CONTEXTUAL_BANDIT',
12+
}
13+
14+
export enum ExperimentVariationValueTypeEnum {
15+
BOOLEAN = 'BOOLEAN',
16+
INTEGER = 'INTEGER',
17+
JSON = 'JSON',
18+
NUMERIC = 'NUMERIC',
19+
STRING = 'STRING',
20+
}
21+
22+
export enum UFCFormatEnum {
23+
SERVER = 'SERVER',
24+
CLIENT = 'CLIENT',
25+
}
26+
27+
export class UniversalFlagConfig {
28+
createdAt!: string; // ISODate
29+
format!: UFCFormatEnum;
30+
environment!: EnvironmentDto; // used for evaluation "details"
31+
flags!: Record<string, FlagDto>;
32+
banditReferences?: Record<string, BanditReferenceDto>;
33+
/**
34+
* @deprecated Moving bandit information to `banditReferences`
35+
*/
36+
bandits?: Record<string, BanditFlagVariationDto[]>;
37+
}
38+
39+
export class EnvironmentDto {
40+
name!: string;
41+
}
42+
43+
export class FlagDto {
44+
key!: string;
45+
enabled!: boolean;
46+
variationType!: ExperimentVariationValueTypeEnum;
47+
variations!: Record<string, VariationDto>;
48+
allocations!: AllocationDto[];
49+
totalShards!: number;
50+
}
51+
52+
export class VariationDto {
53+
key!: string;
54+
value!: boolean | number | string; // JSON represented as string
55+
algorithmType?: ExperimentVariationAlgorithmTypeEnum;
56+
}
57+
58+
export class AllocationDto {
59+
key!: string;
60+
rules?: RuleDto[];
61+
startAt?: string; // ISODate
62+
endAt?: string; // ISODate
63+
splits!: SplitDto[];
64+
doLog!: boolean;
65+
}
66+
67+
export class RuleDto {
68+
conditions!: ITargetingRuleCondition[];
69+
}
70+
71+
export class SplitDto {
72+
variationKey!: string;
73+
shards!: ShardDto[];
74+
extraLogging?: Record<string, string>;
75+
}
76+
77+
export class ShardDto {
78+
salt!: string;
79+
ranges!: RangeDto[];
80+
}
81+
82+
export class RangeDto {
83+
start!: number; // inclusive
84+
end!: number; // exclusive
85+
}
86+
87+
export class BanditFlagVariationDto {
88+
key!: string;
89+
flagKey!: string;
90+
allocationKey!: string;
91+
variationKey!: string;
92+
variationValue!: string;
93+
}
94+
95+
export class BanditReferenceDto {
96+
modelVersion!: string | null;
97+
flagVariations!: BanditFlagVariationDto[];
98+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"node": ">=18.x"
99
},
1010
"scripts": {
11-
"validate:tests": "jest ./test-validation"
11+
"validate:tests": "jest ./test-validation",
12+
"obfuscate:ufc": "jest ./obfuscation"
1213
},
1314
"devDependencies": {
1415
"@types/jest": "^29.5.12",

0 commit comments

Comments
 (0)