commit 37754e9621f13f16ea218edf50e3c3ab312b7c51
parent 94eaa76b21109266a669ce7e77b4c950eee5ecfd
Author: Sebastian Streich <sstreich@mozilla.com>
Date: Wed, 29 Oct 2025 12:51:57 +0000
Bug 1990490 - Patch 2 - Extend Entitlement to contain experiment metadata r=ip-protection-reviewers,rking,jhirsch
Differential Revision: https://phabricator.services.mozilla.com/D266014
Diffstat:
2 files changed, 239 insertions(+), 40 deletions(-)
diff --git a/browser/components/ipprotection/GuardianClient.sys.mjs b/browser/components/ipprotection/GuardianClient.sys.mjs
@@ -18,6 +18,14 @@ ChromeUtils.defineLazyGetter(
ChromeUtils.importESModule("resource://gre/modules/HiddenFrame.sys.mjs")
.HiddenBrowserManager
);
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "JsonSchemaValidator",
+ () =>
+ ChromeUtils.importESModule(
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs"
+ ).JsonSchemaValidator
+);
if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
throw new Error("Guardian.sys.mjs should only run in the parent process");
@@ -209,7 +217,7 @@ export class GuardianClient {
try {
const entitlement = await Entitlement.fromResponse(response);
if (!entitlement) {
- return { status, error: "invalid_response" };
+ return { status, error: "parse_error" };
}
return {
status,
@@ -369,35 +377,43 @@ export class ProxyPass {
* Immutable after creation.
*/
export class Entitlement {
- /** True if the User has any valid subscription plan to Mozilla VPN */
+ /** True if the User may Use the Autostart feature */
+ autostart = false;
+ /** The date the entitlement was added to the user */
+ created_at = new Date();
+ /** True if the User has a limited bandwidth */
+ limited_bandwidth = false;
+ /** True if the User may Use the location controls */
+ location_controls = false;
+ /** True if the User has any valid subscription plan to the Mozilla VPN (not firefox VPN) */
subscribed = false;
/** The Guardian User ID */
uid = 0;
- /** The date the entitlement was added to the user */
- created_at = new Date();
-
- constructor(subscribed, uid, created_at) {
- if (
- typeof subscribed !== "boolean" ||
- typeof uid !== "number" ||
- typeof created_at !== "string"
- ) {
- throw new TypeError("Invalid arguments for Entitlement constructor");
- }
- // Assert ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ
- const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
- if (!iso8601Regex.test(created_at)) {
- throw new TypeError(
- "entitlementDate must be in ISO 8601 format: YYYY-MM-DDTHH:mm:ss.sssZ"
- );
+ /** True if the User has website inclusion */
+ website_inclusion = false;
+
+ constructor(
+ args = {
+ autostart: false,
+ created_at: new Date().toISOString(),
+ limited_bandwidth: false,
+ location_controls: false,
+ subscribed: false,
+ uid: 0,
+ website_inclusion: false,
}
+ ) {
// Ensure it parses to a valid date
- const parsed = Date.parse(created_at);
+ const parsed = Date.parse(args.created_at);
if (isNaN(parsed)) {
throw new TypeError("entitlementDate is not a valid date string");
}
- this.subscribed = subscribed;
- this.uid = uid;
+ this.autostart = args.autostart;
+ this.limited_bandwidth = args.limited_bandwidth;
+ this.location_controls = args.location_controls;
+ this.website_inclusion = args.website_inclusion;
+ this.subscribed = args.subscribed;
+ this.uid = args.uid;
this.created_at = parsed;
Object.freeze(this);
}
@@ -407,9 +423,61 @@ export class Entitlement {
return null;
}
return response.json().then(data => {
- return new Entitlement(data.subscribed, data.uid, data.created_at);
+ const result = lazy.JsonSchemaValidator.validate(
+ data,
+ Entitlement.schema
+ );
+ if (!result.valid) {
+ return null;
+ }
+ return new Entitlement(data);
});
}
+
+ static get schema() {
+ return {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ title: "Entitlement",
+ type: "object",
+ properties: {
+ autostart: {
+ type: "boolean",
+ description: "True if the User may Use the Autostart feature",
+ },
+ created_at: {
+ type: "string",
+ description: "The date the entitlement was added to the user",
+ format: "date-time", // ISO 8601
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
+ },
+ limited_bandwidth: {
+ type: "boolean",
+ },
+ location_controls: {
+ type: "boolean",
+ },
+ subscribed: {
+ type: "boolean",
+ },
+ uid: {
+ type: "integer",
+ },
+ website_inclusion: {
+ type: "boolean",
+ },
+ },
+ required: [
+ "autostart",
+ "created_at",
+ "limited_bandwidth",
+ "location_controls",
+ "subscribed",
+ "uid",
+ "website_inclusion",
+ ],
+ additionalProperties: true,
+ };
+ }
}
/**
diff --git a/browser/components/ipprotection/tests/xpcshell/test_GuardianClient.js b/browser/components/ipprotection/tests/xpcshell/test_GuardianClient.js
@@ -41,6 +41,15 @@ const testGuardianConfig = server => ({
add_task(async function test_fetchUserInfo() {
const ok = data => {
return (request, r) => {
+ // Verify the Authorization header is present and correctly formatted
+ const authHeader = request.getHeader("Authorization");
+ Assert.ok(authHeader, "Authorization header should be present");
+ Assert.equal(
+ authHeader,
+ "Bearer test-token",
+ "Authorization header should have the correct format"
+ );
+
r.setStatusLine(request.httpVersion, 200, "OK");
r.write(JSON.stringify(data));
};
@@ -55,6 +64,10 @@ add_task(async function test_fetchUserInfo() {
subscribed: true,
uid: 42,
created_at: "2023-01-01T12:00:00.000Z",
+ limited_bandwidth: false,
+ location_controls: false,
+ autostart: false,
+ website_inclusion: false,
}),
expects: {
status: 200,
@@ -64,6 +77,118 @@ add_task(async function test_fetchUserInfo() {
subscribed: true,
uid: 42,
created_at: "2023-01-01T12:00:00.000Z",
+ limited_bandwidth: false,
+ location_controls: false,
+ autostart: false,
+ website_inclusion: false,
+ },
+ },
+ },
+ {
+ name: "Alpha experiment",
+ sends: ok({
+ autostart: false,
+ created_at: "2023-09-24T12:00:00.000Z",
+ limited_bandwidth: false,
+ location_controls: false,
+ subscribed: true,
+ uid: 12345,
+ website_inclusion: false,
+ type: "alpha",
+ }),
+ expects: {
+ status: 200,
+ error: null,
+ validEntitlement: true,
+ entitlement: {
+ autostart: false,
+ limited_bandwidth: false,
+ location_controls: false,
+ subscribed: true,
+ uid: 12345,
+ website_inclusion: false,
+ created_at: "2023-09-24T12:00:00.000Z",
+ },
+ },
+ },
+ {
+ name: "Beta experiment",
+ sends: ok({
+ autostart: true,
+ created_at: "2023-09-24T12:30:00.000Z",
+ limited_bandwidth: false,
+ location_controls: false,
+ subscribed: false,
+ uid: 67890,
+ website_inclusion: true,
+ type: "beta",
+ }),
+ expects: {
+ status: 200,
+ error: null,
+ validEntitlement: true,
+ entitlement: {
+ autostart: true,
+ limited_bandwidth: false,
+ location_controls: false,
+ subscribed: false,
+ uid: 67890,
+ website_inclusion: true,
+ created_at: "2023-09-24T12:30:00.000Z",
+ },
+ },
+ },
+ {
+ name: "gamma experiment",
+ sends: ok({
+ autostart: true,
+ created_at: "2023-09-24T13:00:00.000Z",
+ limited_bandwidth: false,
+ location_controls: true,
+ subscribed: true,
+ uid: 54321,
+ website_inclusion: false,
+ type: "gamma",
+ }),
+ expects: {
+ status: 200,
+ error: null,
+ validEntitlement: true,
+ entitlement: {
+ autostart: true,
+ limited_bandwidth: false,
+ location_controls: true,
+ subscribed: true,
+ uid: 54321,
+ website_inclusion: false,
+ created_at: "2023-09-24T13:00:00.000Z",
+ },
+ },
+ },
+ {
+ name: "Delta experiment",
+ sends: ok({
+ autostart: true,
+ created_at: "2023-09-24T13:30:00.000Z",
+ limited_bandwidth: true,
+ location_controls: true,
+ subscribed: true,
+ uid: 13579,
+ website_inclusion: true,
+ type: "delta",
+ }),
+ expects: {
+ status: 200,
+ error: null,
+ validEntitlement: true,
+ entitlement: {
+ autostart: true,
+ limited_bandwidth: true,
+ location_controls: true,
+ subscribed: true,
+ uid: 13579,
+ website_inclusion: true,
+ created_at: "2023-09-24T13:30:00.000Z",
},
},
},
@@ -72,7 +197,7 @@ add_task(async function test_fetchUserInfo() {
sends: fail(HTTP_404),
expects: {
status: 404,
- error: "invalid_response",
+ error: "parse_error",
validEntitlement: false,
},
},
@@ -91,6 +216,10 @@ add_task(async function test_fetchUserInfo() {
subscribed: "true", // Incorrect type: should be boolean
uid: "42", // Incorrect type: should be number
created_at: 1234567890, // Incorrect type: should be string
+ limited_bandwidth: "false", // Incorrect type: should be boolean
+ location_controls: "true", // Incorrect type: should be boolean
+ autostart: "true", // Incorrect type: should be boolean
+ website_inclusion: "false", // Incorrect type: should be boolean
}),
expects: {
status: 200,
@@ -128,22 +257,24 @@ add_task(async function test_fetchUserInfo() {
null,
`${name}: entitlement should not be null`
);
- Assert.equal(
- entitlement.subscribed,
- expects.entitlement.subscribed,
- `${name}: entitlement.subscribed should match`
- );
- Assert.equal(
- entitlement.uid,
- expects.entitlement.uid,
- `${name}: entitlement.uid should match`
- );
- // Compare date objects using getTime() for accurate comparison
- Assert.equal(
- new Date(entitlement.created_at).toISOString(),
- new Date(Date.parse(expects.entitlement.created_at)).toISOString(),
- `${name}: entitlement.created_at should match`
- );
+ for (const key of Object.keys(expects.entitlement)) {
+ // Special case the date case, all others can check equality directly
+ if (key === "created_at") {
+ Assert.equal(
+ new Date(entitlement.created_at).toISOString(),
+ new Date(
+ Date.parse(expects.entitlement.created_at)
+ ).toISOString(),
+ `${name}: entitlement.created_at should match`
+ );
+ } else {
+ Assert.equal(
+ entitlement[key],
+ expects.entitlement[key],
+ `${name}: entitlement.${key} should match`
+ );
+ }
+ }
} else {
Assert.equal(
entitlement,