Skip to content

Commit 965b0fb

Browse files
committed
fix(flags): guard inherited rules context attributes
1 parent fd1e4f7 commit 965b0fb

3 files changed

Lines changed: 122 additions & 33 deletions

File tree

packages/core/src/flags/configuration/__tests__/rules.test.ts

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('rules configuration', () => {
2828
enabled: true
2929
}
3030
})
31-
).toEqual({
31+
).toMatchObject({
3232
targetingKey: 'user-1',
3333
country: 'US',
3434
enabled: true
@@ -46,6 +46,81 @@ describe('rules configuration', () => {
4646
);
4747
});
4848

49+
it.each(['constructor', 'toString'])(
50+
'shadows an absent inherited %s context attribute',
51+
attribute => {
52+
const rulesContext = toRulesEvaluationContext({
53+
targetingKey: 'user-1'
54+
});
55+
56+
expect(Object.getPrototypeOf(rulesContext)).toBeNull();
57+
expect(
58+
Object.prototype.hasOwnProperty.call(rulesContext, attribute)
59+
).toBe(true);
60+
expect(rulesContext[attribute]).toBeUndefined();
61+
62+
const configuration = buildRulesConfiguration();
63+
const condition =
64+
configuration.flags['dynamic-flag'].allocations[0].rules?.[0]
65+
.conditions[0];
66+
if (!condition) {
67+
throw new Error('The fixture has no condition.');
68+
}
69+
condition.attribute = attribute;
70+
condition.value = [
71+
String(({} as Record<string, unknown>)[attribute])
72+
];
73+
74+
expect(
75+
flaggingCoreRulesEngine.evaluate({
76+
configuration,
77+
type: 'boolean',
78+
flagKey: 'dynamic-flag',
79+
defaultValue: false,
80+
context: rulesContext,
81+
logger: getNoopRulesLogger()
82+
})
83+
).toMatchObject({ value: false, reason: 'DEFAULT' });
84+
}
85+
);
86+
87+
it.each(['constructor', 'toString'])(
88+
'preserves an explicit own %s context attribute',
89+
attribute => {
90+
const attributes = Object.create(null) as Record<string, string>;
91+
attributes[attribute] = 'customer-value';
92+
93+
const rulesContext = toRulesEvaluationContext({
94+
targetingKey: 'user-1',
95+
attributes
96+
});
97+
expect(
98+
Object.prototype.hasOwnProperty.call(rulesContext, attribute)
99+
).toBe(true);
100+
expect(rulesContext[attribute]).toBe('customer-value');
101+
const configuration = buildRulesConfiguration();
102+
const condition =
103+
configuration.flags['dynamic-flag'].allocations[0].rules?.[0]
104+
.conditions[0];
105+
if (!condition) {
106+
throw new Error('The fixture has no condition.');
107+
}
108+
condition.attribute = attribute;
109+
condition.value = ['customer-value'];
110+
111+
expect(
112+
flaggingCoreRulesEngine.evaluate({
113+
configuration,
114+
type: 'boolean',
115+
flagKey: 'dynamic-flag',
116+
defaultValue: false,
117+
context: rulesContext,
118+
logger: getNoopRulesLogger()
119+
})
120+
).toMatchObject({ value: true, reason: 'TARGETING_MATCH' });
121+
}
122+
);
123+
49124
it('clones and freezes a valid rules configuration', () => {
50125
const source = buildRulesConfiguration();
51126
const prepared = prepareRulesConfiguration(source);
@@ -200,10 +275,15 @@ describe('rules configuration', () => {
200275

201276
// TODO(FFL-2837): Replace this legacy JSON compatibility test with a
202277
// generated protobuf fixture after a flagging-core release contains
203-
// DataDog/openfeature-js-client#344 through `03cde21`. Round-trip the
278+
// DataDog/openfeature-js-client#344 through `5a5511e`. Round-trip the
204279
// generated fixture and confirm that serialization preserves the unknown field.
205280
// Add a fixture with an unsupported minimum feature level and require a
206-
// flag-scoped `PARSE_ERROR`, not `FLAG_NOT_FOUND`.
281+
// flag-scoped `PARSE_ERROR`, not `FLAG_NOT_FOUND`. Also cover unsorted
282+
// string and SHA-256 membership indexes, invalid SHA digest lengths, and
283+
// semantic-version components at and above the unsigned 64-bit limit.
284+
// Confirm that an absent inherited `__proto__` attribute does not match and
285+
// that an explicit own `__proto__` context attribute remains usable. The
286+
// legacy evaluator's compiled object spread cannot preserve that key.
207287
it('keeps supported known data when an unknown field is present', () => {
208288
const source = buildRulesConfiguration();
209289
(source.flags['dynamic-flag'] as typeof source.flags['dynamic-flag'] & {
@@ -230,7 +310,7 @@ describe('rules configuration', () => {
230310

231311
// TODO(FFL-2837): Replace this unsafe JSON number with an out-of-range
232312
// protobuf `int64` fixture after flagging-core contains PR #344 at or after
233-
// `03cde21`. The generated parser must preserve the source value as `bigint`
313+
// `5a5511e`. The generated parser must preserve the source value as `bigint`
234314
// where supported. Run the same evaluation with
235315
// global `BigInt` unavailable and require `PARSE_ERROR`, not `GENERAL`.
236316
it('returns PARSE_ERROR instead of serving an unsafe integer', () => {

packages/core/src/flags/configuration/rules.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import type { EvaluationContext, JsonValue, PrimitiveValue } from '../types';
1414

1515
// TODO(FFL-2837): Replace this legacy UFC v1 alias with
1616
// `NonNullable<FlagsConfiguration['rules']>['response']` after a flagging-core
17-
// release contains DataDog/openfeature-js-client#344 through `03cde21` and
18-
// restores 32-byte SHA digest validation. Keep the `FlagsConfiguration` type
17+
// release contains DataDog/openfeature-js-client#344 through `5a5511e`.
18+
// Keep the `FlagsConfiguration` type
1919
// import on the flagging-core package root. PR #344 preserves protobuf integers
2020
// as `bigint`, does not call global `BigInt` during safe conversion, and reports
21-
// unsafe conversions as deterministic per-flag `PARSE_ERROR` results.
21+
// unsafe conversions and malformed SHA digests as deterministic per-flag
22+
// `PARSE_ERROR` results.
2223
type RulesConfigurationResponse = UniversalFlagConfigurationV1;
2324

2425
export type RulesValueType = 'boolean' | 'string' | 'number' | 'object';
@@ -119,18 +120,27 @@ export const toRulesEvaluationContext = (
119120
attributes.set(key, value);
120121
}
121122

122-
return {
123-
...Object.fromEntries(attributes),
124-
targetingKey: context.targetingKey
125-
};
123+
// TODO(FFL-2837): Delete the inherited-name shadows after a flagging-core
124+
// release contains DataDog/openfeature-js-client#344 through `aa93230` and
125+
// this adapter uses its generated protobuf evaluator. That evaluator uses
126+
// own-property lookup for condition and shard context attributes.
127+
const rulesContext = Object.create(null) as RulesEvaluationContext;
128+
for (const key of Object.getOwnPropertyNames(Object.prototype)) {
129+
rulesContext[key] = undefined;
130+
}
131+
for (const [key, value] of attributes) {
132+
rulesContext[key] = value;
133+
}
134+
rulesContext.targetingKey = context.targetingKey;
135+
136+
return rulesContext;
126137
};
127138

128139
const hasOwn = (value: object, key: PropertyKey): boolean =>
129140
Object.prototype.hasOwnProperty.call(value, key);
130141

131142
// TODO(FFL-2837): Delete this compatibility error store after a flagging-core
132-
// release contains DataDog/openfeature-js-client#344 through `03cde21` and the
133-
// required SHA digest validation follow-up.
143+
// release contains DataDog/openfeature-js-client#344 through `5a5511e`.
134144
// The generated protobuf evaluator validates the requested flag and the data
135145
// that evaluation reaches. It does not build this error map during parsing.
136146
// It returns deterministic `PARSE_ERROR` results, including for an integer that
@@ -209,7 +219,7 @@ const validateCondition = (value: unknown): string | undefined => {
209219
try {
210220
// TODO(FFL-2837): Define a bounded regular expression policy before
211221
// dynamic offline rules leave draft state. Upstream PR #344 through
212-
// `03cde21` compiles protobuf regular expressions lazily and caches
222+
// `5a5511e` compiles protobuf regular expressions lazily and caches
213223
// them by configuration and index, but it does not limit patterns.
214224
RegExp(value.value); // dd-iac-scan ignore-line
215225
} catch {
@@ -464,8 +474,7 @@ export const prepareRulesConfiguration = (
464474
const clone = cloneValue(value);
465475

466476
// TODO(FFL-2837): Delete this legacy JSON clone and validator after a
467-
// flagging-core release contains upstream PR #344 through `03cde21` and the
468-
// required SHA digest validation follow-up. That
477+
// flagging-core release contains upstream PR #344 through `5a5511e`. That
469478
// implementation preserves protobuf integers as `bigint` and validates only
470479
// the requested flag data that evaluation reaches. It does not call global
471480
// `BigInt` when it returns a deterministic per-flag error for an unsafe number.
@@ -520,7 +529,7 @@ const normalizeVariationType = (
520529

521530
// TODO(FFL-2837): Delete this legacy UFC v1 metadata fallback after the
522531
// flagging-core dependency contains DataDog/openfeature-js-client#344 through
523-
// `03cde21`. The protobuf evaluator maps only safely represented integer
532+
// `5a5511e`. The protobuf evaluator maps only safely represented integer
524533
// variations, and all numeric variations, to the OpenFeature type `number`.
525534
const recoverVariationType = (
526535
configuration: RulesConfigurationResponse,
@@ -547,7 +556,7 @@ export const flaggingCoreRulesEngine: RulesEngine = {
547556

548557
// TODO(FFL-2837): Delete this local compatibility guard after the
549558
// flagging-core dependency contains DataDog/openfeature-js-client#344
550-
// through `03cde21`. Keep the reserved-name contract tests.
559+
// through `5a5511e`. Keep the reserved-name contract tests.
551560
if (!hasOwn(flags, request.flagKey)) {
552561
return {
553562
value: request.defaultValue,
@@ -558,7 +567,7 @@ export const flaggingCoreRulesEngine: RulesEngine = {
558567
}
559568

560569
// TODO(FFL-2837): Delete this compatibility check with the local error
561-
// store after the published PR #344 evaluator through `03cde21` validates
570+
// store after the published PR #344 evaluator through `5a5511e` validates
562571
// reached flag data and reports deterministic flag-scoped errors, including
563572
// unsupported feature levels and unsafe integer conversions with and
564573
// without global `BigInt` when supported.

packages/core/src/flags/configuration/wire.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@ import type {
2424

2525
// TODO(FFL-2837): Delete the pending `rulesBased` types, reader, and wrappers
2626
// after a flagging-core release contains DataDog/openfeature-js-client#344
27-
// through `03cde21` plus the required SHA digest follow-up. The `03cde21` tree
28-
// is identical to the previous `939da97` tree. Its final `1db13d4` and
29-
// `03cde21` commits update generated Node-server artifacts and browser test
30-
// isolation; they do not change this React Native boundary.
27+
// through `5a5511e`.
3128
// Import and re-export the wire functions and `FlagsConfigurationWire` type from
32-
// `@datadog/flagging-core/configuration`. Keep `FlagsConfiguration` and the rules
33-
// evaluator on the package root. The new `@datadog/flagging-core/precomputed`
29+
// `@datadog/flagging-core/configuration`. Do not use the deprecated package-root
30+
// aliases because they parse precomputed data only and ignore rules. Keep
31+
// `FlagsConfiguration` and the rules evaluator on the package root. The
32+
// `@datadog/flagging-core/precomputed`
3433
// subpath is protobuf-free, ignores rules, and is not the parser for this module.
35-
// PR #336 through `772167b` adds browser providers and shared lifecycle error
36-
// selection. Its tree is identical to the previous `6d3d6a4` tree, so it does
37-
// not change this core parser boundary. Do not import
34+
// PR #336 through `dde93ea` adds browser providers and shared lifecycle error
35+
// selection. Its latest commit removes unrelated `extraLogging` test coverage
36+
// and does not change this core parser boundary. Do not import
3837
// `@datadog/openfeature-browser` in React Native.
3938
// Use `FlagsConfiguration.rules`. The distribution layer must put one base64
4039
// encoding of the raw dd-source#34959 protobuf response in the version 1
@@ -43,7 +42,8 @@ import type {
4342
// service transport or envelope construction here. PR #344 preserves decoded
4443
// protobuf flags and integers. Its evaluator reports invalid reached data,
4544
// unsupported feature levels, and unsafe integer conversion as deterministic
46-
// flag-scoped `PARSE_ERROR` results.
45+
// flag-scoped `PARSE_ERROR` results. It also validates membership ordering,
46+
// semantic-version bounds, and 32-byte SHA-256 digests.
4747
type PendingRulesConfiguration = FlagsConfiguration & {
4848
configurationError?: string;
4949
rulesError?: string;
@@ -122,9 +122,9 @@ export const configurationFromString = (source: string): FlagsConfiguration => {
122122
// validator that PR #344 removed in favor of the Protobuf-ES decoder. The
123123
// published parser must also include PR #344's unknown-field tolerance,
124124
// unknown-field serialization, and lossless integer parsing through
125-
// `03cde21`. Its safe-integer conversion does not require global `BigInt`.
125+
// `5a5511e`. Its safe-integer conversion does not require global `BigInt`.
126126
// TODO(FFL-2837): Delete this parse-error compatibility behavior when the
127-
// dependency contains PR #344 through `03cde21`. The upstream parser uses
127+
// dependency contains PR #344 through `5a5511e`. The upstream parser uses
128128
// `configurationError` for an invalid envelope and `rulesError` for an
129129
// invalid rules entry or response. It keeps a valid sibling branch.
130130
const pendingRules = readPendingRulesWire(source);
@@ -155,8 +155,8 @@ export const configurationToString = (
155155
const pendingConfiguration = configuration as PendingRulesConfiguration;
156156

157157
// TODO(FFL-2837): Delete this legacy serialization wrapper with the pending
158-
// types above after the dependency contains PR #344 through `03cde21` and
159-
// its required SHA digest follow-up. The upstream serializer encodes generated protobuf
158+
// types above after the dependency contains PR #344 through `5a5511e`.
159+
// The upstream serializer encodes generated protobuf
160160
// rules back to base64 and preserves unknown protobuf fields.
161161
// This temporary UFC v1 shim serializes its legacy JSON response instead.
162162
if (pendingConfiguration.rulesBased) {

0 commit comments

Comments
 (0)