Skip to content

Commit 618ad26

Browse files
committed
Bug 2065965 - Tolerate quoted booleans and per-entry errors in policy validation. r=bsmth
Differential Revision: https://phabricator.services.mozilla.com/D320759
1 parent 35c02c4 commit 618ad26

5 files changed

Lines changed: 343 additions & 23 deletions

File tree

‎browser/components/enterprisepolicies/tests/xpcshell/test_PolicySchemaValidator.js‎

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const { PolicySchemaValidator } = ChromeUtils.importESModule(
77
"resource://gre/modules/policies/PolicySchemaValidator.sys.mjs"
88
);
99

10+
const { TestUtils } = ChromeUtils.importESModule(
11+
"resource://testing-common/TestUtils.sys.mjs"
12+
);
13+
1014
function assertValid(value, schema, options) {
1115
const result = PolicySchemaValidator.validate(value, schema, options);
1216
Assert.ok(result.valid, `Expected ${JSON.stringify(value)} to validate`);
@@ -39,7 +43,27 @@ add_task(function test_boolean_coercion_from_gpo_dword() {
3943
Assert.strictEqual(assertValid(0, schema), false, "0 coerces to false");
4044
Assert.strictEqual(assertValid(true, schema), true, "true stays true");
4145
assertInvalid(2, schema);
42-
assertInvalid("true", schema);
46+
});
47+
48+
add_task(function test_boolean_coercion_from_string() {
49+
const schema = { type: "boolean" };
50+
51+
Assert.strictEqual(assertValid("true", schema), true, '"true" coerces');
52+
Assert.strictEqual(assertValid("false", schema), false, '"false" coerces');
53+
Assert.strictEqual(
54+
assertValid("True", schema),
55+
true,
56+
"the match is case insensitive"
57+
);
58+
assertInvalid("yes", schema);
59+
assertInvalid("", schema);
60+
});
61+
62+
add_task(function test_string_field_keeps_its_quoted_boolean() {
63+
// A field that accepts a string must not have "true" turned into a boolean.
64+
const schema = { type: ["string", "boolean"] };
65+
66+
Assert.strictEqual(assertValid("true", schema), "true", '"true" stays text');
4367
});
4468

4569
add_task(function test_numeric_field_keeps_its_integer() {
@@ -168,3 +192,101 @@ add_task(function test_additional_properties_rejected_by_default() {
168192

169193
assertInvalid({ known: "value", extra: "x" }, schema);
170194
});
195+
196+
// ExtensionSettings shape: a "*" entry holding the defaults, plus open-ended
197+
// per-extension-ID entries.
198+
const EXTENSION_SETTINGS_SHAPE = {
199+
type: "object",
200+
properties: {
201+
"*": {
202+
type: "object",
203+
properties: {
204+
installation_mode: { type: "string" },
205+
},
206+
},
207+
},
208+
patternProperties: {
209+
"^(?!\\*$).*$": {
210+
type: "object",
211+
properties: {
212+
installation_mode: { type: "string" },
213+
updates_disabled: { type: "boolean" },
214+
},
215+
},
216+
},
217+
};
218+
219+
add_task(function test_map_drops_invalid_entries_and_keeps_the_rest() {
220+
// One malformed entry must not discard every other extension's settings.
221+
Assert.deepEqual(
222+
assertValid(
223+
{
224+
"*": { installation_mode: "blocked" },
225+
"good@example.com": { installation_mode: "force_installed" },
226+
"bad@example.com": { updates_disabled: 42 },
227+
},
228+
EXTENSION_SETTINGS_SHAPE
229+
),
230+
{
231+
"*": { installation_mode: "blocked" },
232+
"good@example.com": { installation_mode: "force_installed" },
233+
},
234+
"the invalid entry is dropped and the rest of the policy applies"
235+
);
236+
});
237+
238+
add_task(function test_map_never_drops_a_fixed_property() {
239+
// "*" is not an independently droppable entry: dropping it would turn a
240+
// global block into a policy that looks healthy and installs everything.
241+
assertInvalid(
242+
{
243+
"*": { installation_mode: 42 },
244+
"ok@example.com": { installation_mode: "force_installed" },
245+
},
246+
EXTENSION_SETTINGS_SHAPE
247+
);
248+
});
249+
250+
add_task(function test_fixed_properties_are_not_dropped_individually() {
251+
// An object with only fixed properties is one value, not a collection: a bad
252+
// property fails the policy rather than being silently discarded.
253+
const schema = {
254+
type: "object",
255+
properties: { host: { type: "string" }, port: { type: "number" } },
256+
};
257+
258+
assertInvalid({ host: "example.com", port: "not a number" }, schema);
259+
});
260+
261+
add_task(async function test_dropped_entry_names_the_failing_field() {
262+
// The log line is the only signal an admin gets that an entry went missing,
263+
// so it must name the field that failed, including when that field's name
264+
// needs unescaping out of the JSON pointer the validator reports.
265+
const schema = {
266+
type: "object",
267+
patternProperties: {
268+
"^.*$": {
269+
type: "object",
270+
properties: { "application/pdf": { type: "string" } },
271+
},
272+
},
273+
};
274+
275+
const stopListening = TestUtils.listenForConsoleMessages();
276+
assertValid({ mimeTypes: { "application/pdf": 3 } }, schema, {
277+
policyName: "Handlers",
278+
});
279+
const messages = await stopListening();
280+
281+
Assert.ok(
282+
messages.some(
283+
msg =>
284+
msg.level == "error" &&
285+
msg.arguments[0].includes(
286+
'Handlers["mimeTypes"]["application/pdf"]: Instance type "number"'
287+
) &&
288+
msg.arguments[0].includes("Received 3")
289+
),
290+
"the dropped entry names the failing field and the value received"
291+
);
292+
});

‎browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js‎

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,69 @@ add_task(async function test_force_installed_updates_disabled() {
162162
);
163163
});
164164

165+
add_task(async function test_invalid_entry_does_not_discard_the_others() {
166+
await setupPolicyEngineWithJson({
167+
policies: {
168+
ExtensionSettings: {
169+
"*": {
170+
blocked_install_message: "Generic error message.",
171+
},
172+
"broken@mozilla.com": {
173+
update_url: "not-a-valid-url",
174+
},
175+
"good@mozilla.com": {
176+
blocked_install_message: "Good error message.",
177+
},
178+
},
179+
},
180+
});
181+
182+
equal(
183+
Services.policies.getExtensionSettings("good@mozilla.com")
184+
.blocked_install_message,
185+
"Good error message.",
186+
"A valid entry still applies alongside an invalid one."
187+
);
188+
equal(
189+
Services.policies.getExtensionSettings("broken@mozilla.com")
190+
.blocked_install_message,
191+
"Generic error message.",
192+
"The invalid entry is dropped and falls back to the generic one."
193+
);
194+
});
195+
196+
add_task(async function test_quoted_booleans_are_accepted() {
197+
await setupPolicyEngineWithJson({
198+
policies: {
199+
ExtensionSettings: {
200+
"force@mozilla.com": {
201+
installation_mode: "force_installed",
202+
install_url: "https://example.com/test.xpi",
203+
updates_disabled: "true",
204+
},
205+
"normal@mozilla.com": {
206+
installation_mode: "normal_installed",
207+
install_url: "https://example.com/test.xpi",
208+
private_browsing: "false",
209+
},
210+
},
211+
},
212+
});
213+
214+
strictEqual(
215+
Services.policies.getExtensionSettings("force@mozilla.com")
216+
.updates_disabled,
217+
true,
218+
'updates_disabled: "true" is read as the boolean true'
219+
);
220+
strictEqual(
221+
Services.policies.getExtensionSettings("normal@mozilla.com")
222+
.private_browsing,
223+
false,
224+
'private_browsing: "false" is read as the boolean false'
225+
);
226+
});
227+
165228
add_task(async function test_addon_blocked() {
166229
await setupPolicyEngineWithJson({
167230
policies: {
@@ -967,9 +1030,8 @@ add_task(
9671030
const updateURLValidationError = messages.find(
9681031
msg =>
9691032
msg.level == "error" &&
970-
msg.arguments[0].includes(
971-
"Invalid parameters specified for ExtensionSettings"
972-
)
1033+
msg.arguments[0].includes(addonID) &&
1034+
msg.arguments[0].includes("update_url")
9731035
);
9741036
Assert.ok(
9751037
updateURLValidationError,

‎browser/components/enterprisepolicies/tests/xpcshell/test_preferences.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,27 @@ add_task(async function test_bug_1666836() {
255255
`browser.tabs.warnOnClose should be false`
256256
);
257257
});
258+
259+
add_task(async function test_invalid_entry_does_not_discard_the_others() {
260+
await setupPolicyEngineWithJson({
261+
policies: {
262+
Preferences: {
263+
"browser.policies.test.default.survivor": {
264+
Value: "kept",
265+
Status: "default",
266+
},
267+
"browser.policies.test.default.broken": {
268+
Value: true,
269+
Status: "not-a-status",
270+
},
271+
},
272+
},
273+
});
274+
275+
checkDefaultPref("browser.policies.test.default.survivor", "kept");
276+
equal(
277+
Preferences.get("browser.policies.test.default.broken", undefined),
278+
undefined,
279+
"The invalid entry is dropped without taking the valid ones with it"
280+
);
281+
});

‎toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ EnterprisePoliciesManager.prototype = {
207207
error: validationError,
208208
} = lazy.PolicySchemaValidator.validate(policyParameters, policySchema, {
209209
allowAdditionalProperties: true,
210+
policyName,
210211
});
211212

212213
if (!parametersAreValid) {

0 commit comments

Comments
 (0)