Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: obfuscated precomputed assignments #164

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a1cf3bf
make getMD5HashWithSalt
sameerank Dec 9, 2024
22538e1
add tests
sameerank Dec 9, 2024
7cd544c
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Dec 9, 2024
1ff6d8b
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Dec 12, 2024
b8a2d9d
Export saltedHasher
sameerank Dec 12, 2024
567e66d
Fix tests
sameerank Dec 12, 2024
c8f5171
Alter test to use obfuscated file
sameerank Dec 12, 2024
c0288fe
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Dec 14, 2024
ed3f0b3
Change branch name for test data
sameerank Dec 14, 2024
47333ec
Get all the tests to pass
sameerank Dec 15, 2024
a3ed52a
Make more obvious that the salt was decoded
sameerank Dec 15, 2024
25c6f8c
Switch to using appendBinary for the salt
sameerank Dec 16, 2024
a169e29
Clean up
sameerank Dec 16, 2024
d366925
Include salt in convenience method for setting precomputed flag store
sameerank Dec 16, 2024
8fe80dc
Add a helper to convert context attributes to subject attributes
sameerank Dec 16, 2024
b1048a3
Change default to isObfuscated since we expect the precomputed api to…
sameerank Dec 16, 2024
b81175f
v4.7.1-alpha.0
sameerank Dec 16, 2024
9f36640
Revert "v4.7.1-alpha.0"
sameerank Dec 16, 2024
aee17ce
v4.7.0-alpha.0
sameerank Dec 16, 2024
67e9446
Switch to initializing the client with an options object
sameerank Dec 16, 2024
ed8027d
Make response data not optional
sameerank Dec 16, 2024
4e8aa63
precomputedFlag variable casing
sameerank Dec 16, 2024
6040e96
update hashing
typotter Dec 17, 2024
dd6b1e9
fix lint
leoromanovsky Dec 17, 2024
b178a6c
handoff and address comments
typotter Dec 18, 2024
749e8c1
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
typotter Dec 18, 2024
e99306d
bump version
typotter Dec 18, 2024
f036edc
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Jan 6, 2025
173b3a3
Inf is a numeric attribute too
sameerank Jan 7, 2025
d43e9be
Remove unnecessary public methods
sameerank Jan 8, 2025
bba9863
Remove more unnecessary functions
sameerank Jan 8, 2025
0d1f341
Add to exported interfaces
sameerank Jan 8, 2025
bb03341
Update src/interfaces.ts
sameerank Jan 8, 2025
ad86727
Update src/attributes.ts attributes is ContextAttributes
sameerank Jan 8, 2025
b15f488
Remove redundant 'subjectAttributes as ContextAttributes'
sameerank Jan 8, 2025
3a322a7
Also print error if store is missing salt
sameerank Jan 8, 2025
928f58d
Remove buildContextAttributes
sameerank Jan 8, 2025
5241bf7
v4.8.0-alpha.0
sameerank Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test-data:
mkdir -p $(tempDir)
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
cp -r ${gitDataDir}ufc ${testDataDir}
cp -r ${gitDataDir}configuration-wire ${testDataDir}
sameerank marked this conversation as resolved.
Show resolved Hide resolved
rm -rf ${tempDir}

## prepare
Expand Down
141 changes: 59 additions & 82 deletions src/client/eppo-precomputed-client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as td from 'testdouble';

import {
MOCK_PRECOMPUTED_WIRE_FILE,
readMockConfigurationWireResponse,
} from '../../test/testHelpers';
import ApiEndpoints from '../api-endpoints';
import { IAssignmentLogger } from '../assignment-logger';
import { IPrecomputedConfigurationResponse } from '../configuration';
import { IConfigurationStore } from '../configuration-store/configuration-store';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
import { DEFAULT_POLL_INTERVAL_MS, MAX_EVENT_QUEUE_SIZE, POLL_JITTER_PCT } from '../constants';
Expand All @@ -15,61 +20,19 @@ import EppoPrecomputedClient, {
} from './eppo-precomputed-client';

describe('EppoPrecomputedClient E2E test', () => {
const precomputedFlags = {
createdAt: '2024-11-18T14:23:39.456Z',
format: 'PRECOMPUTED',
environment: {
name: 'Test',
},
flags: {
'string-flag': {
allocationKey: 'allocation-123',
variationKey: 'variation-123',
variationType: 'STRING',
variationValue: 'red',
extraLogging: {},
doLog: true,
},
'boolean-flag': {
allocationKey: 'allocation-124',
variationKey: 'variation-124',
variationType: 'BOOLEAN',
variationValue: true,
extraLogging: {},
doLog: true,
},
'integer-flag': {
allocationKey: 'allocation-125',
variationKey: 'variation-125',
variationType: 'INTEGER',
variationValue: 42,
extraLogging: {},
doLog: true,
},
'numeric-flag': {
allocationKey: 'allocation-126',
variationKey: 'variation-126',
variationType: 'NUMERIC',
variationValue: 3.14,
extraLogging: {},
doLog: true,
},
'json-flag': {
allocationKey: 'allocation-127',
variationKey: 'variation-127',
variationType: 'JSON',
variationValue: '{"key": "value", "number": 123}',
extraLogging: {},
doLog: true,
},
},
}; // TODO: readMockPrecomputedFlagsResponse(MOCK_PRECOMPUTED_FLAGS_RESPONSE_FILE);
const precomputedConfigurationWire = readMockConfigurationWireResponse(
MOCK_PRECOMPUTED_WIRE_FILE,
);
const unparsedPrecomputedResponse = JSON.parse(precomputedConfigurationWire).precomputed.response;
const precomputedResponse: IPrecomputedConfigurationResponse = JSON.parse(
unparsedPrecomputedResponse,
);
sameerank marked this conversation as resolved.
Show resolved Hide resolved

global.fetch = jest.fn(() => {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(precomputedFlags),
json: () => Promise.resolve(precomputedResponse),
});
}) as jest.Mock;
const storage = new MemoryOnlyConfigurationStore<PrecomputedFlag>();
Expand Down Expand Up @@ -106,7 +69,7 @@ describe('EppoPrecomputedClient E2E test', () => {

beforeAll(() => {
storage.setEntries({ [precomputedFlagKey]: mockPrecomputedFlag });
client = new EppoPrecomputedClient(storage);
client = new EppoPrecomputedClient(storage, false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: reading this code for the first time (without looking at implementation), I have no clue what false here means. It would help if constructor used an options object for initialization or a named enum instead of boolean

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and updated to use an options object!

});

afterAll(() => {
Expand All @@ -126,7 +89,7 @@ describe('EppoPrecomputedClient E2E test', () => {
it('Invokes logger for queued events', () => {
const mockLogger = td.object<IAssignmentLogger>();

const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);
client.getStringAssignment(precomputedFlagKey, 'default-value');
client.setAssignmentLogger(mockLogger);

Expand All @@ -138,7 +101,7 @@ describe('EppoPrecomputedClient E2E test', () => {
it('Does not log same queued event twice', () => {
const mockLogger = td.object<IAssignmentLogger>();

const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);

client.getStringAssignment(precomputedFlagKey, 'default-value');
client.setAssignmentLogger(mockLogger);
Expand All @@ -149,7 +112,7 @@ describe('EppoPrecomputedClient E2E test', () => {

it('Does not invoke logger for events that exceed queue size', () => {
const mockLogger = td.object<IAssignmentLogger>();
const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);

for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
client.getStringAssignment(precomputedFlagKey, 'default-value');
Expand All @@ -160,23 +123,23 @@ describe('EppoPrecomputedClient E2E test', () => {
});

it('returns null if getStringAssignment was called for the subject before any precomputed flags were loaded', () => {
const localClient = new EppoPrecomputedClient(new MemoryOnlyConfigurationStore());
const localClient = new EppoPrecomputedClient(new MemoryOnlyConfigurationStore(), false);
expect(localClient.getStringAssignment(precomputedFlagKey, 'hello world')).toEqual(
'hello world',
);
expect(localClient.isInitialized()).toBe(false);
});

it('returns default value when key does not exist', async () => {
const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);
const nonExistentFlag = 'non-existent-flag';
expect(client.getStringAssignment(nonExistentFlag, 'default')).toBe('default');
});

it('logs variation assignment with correct metadata', () => {
const mockLogger = td.object<IAssignmentLogger>();
storage.setEntries({ [precomputedFlagKey]: mockPrecomputedFlag });
const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);
client.setAssignmentLogger(mockLogger);

client.getStringAssignment(precomputedFlagKey, 'default');
Expand All @@ -197,7 +160,7 @@ describe('EppoPrecomputedClient E2E test', () => {
td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(new Error('logging error'));

storage.setEntries({ [precomputedFlagKey]: mockPrecomputedFlag });
const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);
client.setAssignmentLogger(mockLogger);

const assignment = client.getStringAssignment(precomputedFlagKey, 'default');
Expand All @@ -212,7 +175,7 @@ describe('EppoPrecomputedClient E2E test', () => {
beforeEach(() => {
mockLogger = td.object<IAssignmentLogger>();
storage.setEntries({ [precomputedFlagKey]: mockPrecomputedFlag });
client = new EppoPrecomputedClient(storage);
client = new EppoPrecomputedClient(storage, false);
client.setAssignmentLogger(mockLogger);
});

Expand Down Expand Up @@ -382,7 +345,7 @@ describe('EppoPrecomputedClient E2E test', () => {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(precomputedFlags),
json: () => Promise.resolve(precomputedResponse),
});
}) as jest.Mock;
});
Expand Down Expand Up @@ -431,7 +394,7 @@ describe('EppoPrecomputedClient E2E test', () => {
});

it('Fetches initial configuration with parameters in constructor', async () => {
client = new EppoPrecomputedClient(precomputedFlagStore);
client = new EppoPrecomputedClient(precomputedFlagStore, true);
client.setSubjectAndPrecomputedFlagsRequestParameters(requestParameters);
// no configuration loaded
let variation = client.getStringAssignment(precomputedFlagKey, 'default');
Expand All @@ -443,7 +406,7 @@ describe('EppoPrecomputedClient E2E test', () => {
});

it('Fetches initial configuration with parameters provided later', async () => {
client = new EppoPrecomputedClient(precomputedFlagStore);
client = new EppoPrecomputedClient(precomputedFlagStore, true);
client.setSubjectAndPrecomputedFlagsRequestParameters(requestParameters);
// no configuration loaded
let variation = client.getStringAssignment(precomputedFlagKey, 'default');
Expand All @@ -464,7 +427,7 @@ describe('EppoPrecomputedClient E2E test', () => {
}
}

client = new EppoPrecomputedClient(new MockStore());
client = new EppoPrecomputedClient(new MockStore(), true);
client.setSubjectAndPrecomputedFlagsRequestParameters({
...requestParameters,
pollAfterSuccessfulInitialization: true,
Expand Down Expand Up @@ -494,7 +457,7 @@ describe('EppoPrecomputedClient E2E test', () => {
}
}

client = new EppoPrecomputedClient(new MockStore());
client = new EppoPrecomputedClient(new MockStore(), false);
client.setSubjectAndPrecomputedFlagsRequestParameters(requestParameters);
// no configuration loaded
let variation = client.getStringAssignment(precomputedFlagKey, 'default');
Expand All @@ -509,7 +472,7 @@ describe('EppoPrecomputedClient E2E test', () => {
let client: EppoPrecomputedClient;

beforeEach(async () => {
client = new EppoPrecomputedClient(storage);
client = new EppoPrecomputedClient(storage, true);
client.setSubjectAndPrecomputedFlagsRequestParameters(requestParameters);
await client.fetchPrecomputedFlags();
});
Expand Down Expand Up @@ -570,7 +533,7 @@ describe('EppoPrecomputedClient E2E test', () => {
ok: true,
status: 200,
json: () => {
return precomputedFlags;
return precomputedResponse;
},
});
}
Expand All @@ -581,7 +544,7 @@ describe('EppoPrecomputedClient E2E test', () => {
...requestParameters,
pollAfterSuccessfulInitialization,
};
client = new EppoPrecomputedClient(precomputedFlagStore);
client = new EppoPrecomputedClient(precomputedFlagStore, true);
client.setSubjectAndPrecomputedFlagsRequestParameters(requestParameters);
// no configuration loaded
let variation = client.getStringAssignment(precomputedFlagKey, 'default');
Expand Down Expand Up @@ -629,7 +592,7 @@ describe('EppoPrecomputedClient E2E test', () => {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(precomputedFlags),
json: () => Promise.resolve(precomputedResponse),
} as Response);
}
});
Expand All @@ -646,7 +609,7 @@ describe('EppoPrecomputedClient E2E test', () => {
throwOnFailedInitialization,
pollAfterFailedInitialization,
};
client = new EppoPrecomputedClient(precomputedFlagStore);
client = new EppoPrecomputedClient(precomputedFlagStore, true);
client.setSubjectAndPrecomputedFlagsRequestParameters(requestParameters);
// no configuration loaded
expect(client.getStringAssignment(precomputedFlagKey, 'default')).toBe('default');
Expand All @@ -673,36 +636,40 @@ describe('EppoPrecomputedClient E2E test', () => {
});

describe('Obfuscated precomputed flags', () => {
let client: EppoPrecomputedClient;
it('returns decoded variation value', () => {
const salt = 'sodium-chloride';
const saltedAndHashedFlagKey = getMD5Hash(precomputedFlagKey, salt);

beforeAll(() => {
storage.setEntries({
[getMD5Hash(precomputedFlagKey)]: {
[saltedAndHashedFlagKey]: {
...mockPrecomputedFlag,
allocationKey: encodeBase64(mockPrecomputedFlag.allocationKey),
variationKey: encodeBase64(mockPrecomputedFlag.variationKey),
variationValue: encodeBase64(mockPrecomputedFlag.variationValue),
extraLogging: {},
},
});
client = new EppoPrecomputedClient(storage, true);
});

afterAll(() => {
td.reset();
});
const client = new EppoPrecomputedClient(storage, true);
client.setSubjectSaltAndPrecomputedFlagStore(
'test-subject',
{ attr1: 'value1' },
encodeBase64(salt),
storage,
);

it('returns decoded variation value', () => {
expect(client.getStringAssignment(precomputedFlagKey, 'default')).toBe(
mockPrecomputedFlag.variationValue,
);

td.reset();
});
});

it('logs variation assignment with format from precomputed flags response', () => {
const mockLogger = td.object<IAssignmentLogger>();
storage.setEntries({ [precomputedFlagKey]: mockPrecomputedFlag });
const client = new EppoPrecomputedClient(storage);
const client = new EppoPrecomputedClient(storage, false);
client.setAssignmentLogger(mockLogger);

client.getStringAssignment(precomputedFlagKey, 'default');
Expand All @@ -721,12 +688,17 @@ describe('EppoPrecomputedClient E2E test', () => {
beforeEach(() => {
store = new MemoryOnlyConfigurationStore<PrecomputedFlag>();
mockLogger = td.object<IAssignmentLogger>();
client = new EppoPrecomputedClient(store);
client = new EppoPrecomputedClient(store, false);
client.setAssignmentLogger(mockLogger);
});

it('returns default value and does not log when store is not initialized', () => {
client.setSubjectAndPrecomputedFlagStore('test-subject', {}, store);
client.setSubjectSaltAndPrecomputedFlagStore(
'test-subject',
{},
encodeBase64('sodium-chloride'),
store,
);
expect(client.getStringAssignment('test-flag', 'default')).toBe('default');
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
});
Expand All @@ -746,7 +718,12 @@ describe('EppoPrecomputedClient E2E test', () => {
extraLogging: {},
},
});
client.setSubjectAndPrecomputedFlagStore(subjectKey, subjectAttributes, store);
client.setSubjectSaltAndPrecomputedFlagStore(
subjectKey,
subjectAttributes,
encodeBase64('sodium-chloride'),
store,
);
expect(client.getStringAssignment('test-flag', 'default')).toBe('test-value');

expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
Expand Down
Loading
Loading