-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathupdate-script.js
256 lines (237 loc) · 9.4 KB
/
update-script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
const KintoClient = require("kinto-http").default;
const btoa = require("btoa");
const fetch = require("node-fetch");
const AppConstants = require("./app-constants");
const RELATED_REALMS_COLLECTION_ID = "websites-with-shared-credential-backends";
const PASSWORD_RULES_COLLECTION_ID = "password-rules";
/** @type {String} */
/** @type {String} */
const AUTHORIZATION = AppConstants.AUTHORIZATION;
/** @type {String} */
const SERVER_ADDRESS = AppConstants.SERVER;
const BUCKET = "main-workspace";
const SHARED_CREDENTIALS_API_ENDPOINT =
"https://api.github.com/repos/apple/password-manager-resources/contents/quirks/shared-credentials.json";
const SHARED_CREDENTIALS_HISTORICAL_API_ENDPOINT =
"https://api.github.com/repos/apple/password-manager-resources/contents/quirks/shared-credentials-historical.json";
const PASSWORD_RULES_API_ENDPOINT =
"https://api.github.com/repos/apple/password-manager-resources/contents/quirks/password-rules.json";
/**
* Converts the shared-credentials.json and shared-credentials-historical.json from apple/password-manager-resources into the legacy
* format (previously contained in websites-with-shared-credential-backends.json) that firefox expects.
* Converted from ruby script here: https://github.com/apple/password-manager-resources/blob/9917b5c8/tools/convert-shared-credential-to-legacy-format.rb
*/
async function getSharedCredentialsLegacyFormat() {
const credentialEntries = await Promise.all([
getSourceRecords(SHARED_CREDENTIALS_API_ENDPOINT),
getSourceRecords(SHARED_CREDENTIALS_HISTORICAL_API_ENDPOINT),
]);
const legacyOutput = [];
for (const entry of credentialEntries.flat()) {
if (entry.shared) {
legacyOutput.push(entry.shared);
} else if (entry.from && entry.to) {
legacyOutput.push([...entry.from, ...entry.to]);
} else {
console.error("ERROR: Could not convert entry to legacy format.", entry);
}
}
return legacyOutput.sort();
}
/**
* Fetches the source records from the apiEndpoint param
*
* Since this script should run once every two weeks, we don't need a GitHub token.
* See also: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
* @param {string} apiEndpoint either `RELATED_REALMS_API_ENDPOINT` or `PASSWORD_RULES_API_ENDPOINT`
* @return {String[][]} The source records
*/
const getSourceRecords = async (apiEndpoint) => {
const response = await fetch(apiEndpoint, {
headers: {
"Accept": "application/vnd.github.v3.raw"
}
});
const data = await response.json();
return data;
}
const arrayEquals = (a, b) => {
return Array.isArray(a) &&
Array.isArray(b) &&
a.length === b.length &&
a.every((val, index) => val === b[index]);
};
/**
* Updates the existing record in the "websites-with-shared-credential-backends" Remote Settings collection with the updated data from Apple's GitHub repository
*
* @param {KintoClient} client KintoClient instance
* @param {string} bucket Name of the Remote Settings bucket
* @param {Object} newRecord Object containing the updated related realms object
* @param {string} newRecord.id ID from the current related realms object from the Remote Settings server
* @param {string[][]} newRecord.relatedRealms Updated related realms array from GitHub
*/
const updateRelatedRealmsRecord = async (client, bucket, newRecord) => {
const cid = RELATED_REALMS_COLLECTION_ID;
await client.bucket(bucket).collection(cid).updateRecord(newRecord);
await client.bucket(bucket).collection(cid).setData({ status: "to-review" }, { patch: true });
console.log(`Found new records, committed changes to ${cid} collection.`);
};
/**
* Creates a new record in Remote Settings if there are no records in the WEBSITES_WITH_SHARED_CREDENTIAL_COLLECTION
*
* @param {KintoClient} client
* @param {string} bucket
*/
const createRelatedRealmsRecord = async (client, bucket, sourceRecords) => {
const cid = RELATED_REALMS_COLLECTION_ID;
const result = await client.bucket(bucket).collection(cid).createRecord({
relatedRealms: sourceRecords
});
await client.bucket(bucket).collection(cid).setData({ status: "to-review" }, { patch: true });
console.log(`Added new record to ${cid}`, result);
};
const printSuccessMessage = () => {
console.log("Script finished successfully!");
}
/**
* Determines if there are new records from the GitHub source for the "websites-with-shared-credential-backends" collection
*
* @param {String[][]} sourceRecords Related realms from Apple's GitHub
* @param {String[][]} destinationRecords Related realms from Remote Settings
* @return {Boolean} `true` if there are new records, `false` if there are no new records
*/
const checkIfNewRelatedRealmsRecords = (sourceRecords, destinationRecords) => {
let areNewRecords = false;
if (sourceRecords.length !== destinationRecords.length) {
areNewRecords = true;
}
for (let i = 0; i < sourceRecords.length; i++) {
if (areNewRecords) {
break;
}
areNewRecords = !arrayEquals(sourceRecords[i], destinationRecords[i]);
}
return areNewRecords;
}
/**
* Converts the records from the "password-rules" Remote Settings collection into a Map
* for easier comparison against the GitHub source of truth records.
*
* @param {Object[]} records
* @param {string} records.Domain
* @param {string} records[password-rules]
* @return {Map}
*/
const passwordRulesRecordsToMap = (records) => {
let map = new Map();
for (let record of records) {
let { id, Domain: domain, "password-rules": rules } = record;
map.set(domain, { id: id, "password-rules": rules });
}
return map;
}
/**
* Creates and/or updates the existing records in the "password-rules" Remote Settings collection with the updated data from Apple's GitHub repository
*
* @param {KintoClient} client KintoClient instance
* @param {string} bucket Name of the Remote Settings bucket
*/
const createAndUpdateRulesRecords = async (client, bucket) => {
let collection = client.bucket(bucket).collection(PASSWORD_RULES_COLLECTION_ID);
let sourceRulesByDomain = await getSourceRecords(PASSWORD_RULES_API_ENDPOINT);
let { data: remoteSettingsRecords } = await collection.listRecords();
debugger;
let remoteSettingsRulesByDomain = passwordRulesRecordsToMap(remoteSettingsRecords);
let batchRecords = [];
for (let domain in sourceRulesByDomain) {
let passwordRules = sourceRulesByDomain[domain]["password-rules"];
let id;
let oldRules;
let _record = remoteSettingsRulesByDomain.get(domain);
if (_record) {
id = _record.id;
oldRules = _record["password-rules"];
}
if (!id) {
let newRecord = { "Domain": domain, "password-rules": passwordRules };
batchRecords.push(newRecord);
console.log("Added new record to batch!", newRecord);
}
if (id && oldRules !== passwordRules) {
let updatedRecord = { id, "Domain": domain, "password-rules": passwordRules };
batchRecords.push(updatedRecord);
console.log("Added updated record to batch!", updatedRecord);
}
}
await collection.batch(batch => {
for (let record of batchRecords) {
if (record.id) {
batch.updateRecord(record);
} else {
batch.createRecord(record);
}
}
});
await collection.setData({ status: "to-review" }, { patch: true });
if (batchRecords.length) {
console.log(`Found new and/or updated records, committed changes to ${PASSWORD_RULES_COLLECTION_ID} collection.`);
} else {
console.log(`Found no new or updated records for the ${PASSWORD_RULES_COLLECTION_ID} collection.`);
}
};
/**
* Creates and/or updates the existing records in the "websites-with-shared-credential-backends" Remote Settings collection
* with the updated data from Apple's GitHub repository.
*
* @param {KintoClient} client
* @param {string} bucket
*/
const createAndUpdateRelatedRealmsRecords = async (client, bucket) => {
let { data: relatedRealmsData } = await client.bucket(bucket).collection(RELATED_REALMS_COLLECTION_ID).listRecords();
let realmsGithubRecords = await getSharedCredentialsLegacyFormat();
let id = relatedRealmsData[0]?.id;
// If there is no ID from Remote Settings, we need to create a new record in the related realms collection
if (!id) {
await createRelatedRealmsRecord(client, bucket, realmsGithubRecords);
} else {
// If there is an ID, we can compare the source and destination records
let currentRecords = relatedRealmsData[0].relatedRealms;
let areNewRecords = checkIfNewRelatedRealmsRecords(realmsGithubRecords, currentRecords);
// If there are new records, we need to update the data of the record using the current ID
if (areNewRecords) {
let newRecord = {
id: id,
relatedRealms: realmsGithubRecords
};
await updateRelatedRealmsRecord(client, bucket, newRecord)
} else {
console.log(`No new records! Not committing any changes to ${RELATED_REALMS_COLLECTION_ID} collection.`);
}
}
};
/**
* The runner for the script.
*
* @return {Number} 0 for success, 1 for failure.
*/
const main = async () => {
if (AUTHORIZATION === "") {
console.error("No username or password set, quitting!");
return 1;
}
try {
const client = new KintoClient(SERVER_ADDRESS, {
headers: {
Authorization: "Basic " + btoa(AUTHORIZATION)
}
});
await createAndUpdateRelatedRealmsRecords(client, BUCKET);
await createAndUpdateRulesRecords(client, BUCKET);
} catch (e) {
console.error(e);
return 1;
}
printSuccessMessage();
return 0;
};
main();