microsoft/TypeAgent

Public

mirrored fromhttps://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
desktop-agent-improvements

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

ts/tools/scripts/getKeys.mjs

464lines · modecode

1#!/usr/bin/env node
2// Copyright (c) Microsoft Corporation.
3// Licensed under the MIT License.
4
5import fs from "node:fs";
6import path from "node:path";
7import child_process from "node:child_process";
8import readline from "node:readline/promises";
9import { getClient as getPIMClient } from "./lib/pimClient.mjs";
10import { fileURLToPath } from "node:url";
11import { createRequire } from "node:module";
12import chalk from "chalk";
13import { exit } from "node:process";
14
15const require = createRequire(import.meta.url);
16const config = require("./getKeys.config.json");
17
18const __dirname = path.dirname(fileURLToPath(import.meta.url));
19const dotenvPath = path.resolve(__dirname, config.defaultDotEnvPath);
20const sharedKeys = config.env.shared;
21const privateKeys = config.env.private;
22const deleteKeys = config.env.delete;
23let paramSharedVault = undefined;
24let paramPrivateVault = undefined;
25
26async function getSecretListWithElevation(keyVaultClient, vaultName) {
27 try {
28 return await keyVaultClient.getSecrets(vaultName);
29 } catch (e) {
30 if (!e.message.includes("ForbiddenByRbac")) {
31 throw e;
32 }
33
34 try {
35 console.warn(chalk.yellowBright("Elevating to get secrets..."));
36 const pimClient = await getPIMClient();
37 await pimClient.elevate({
38 requestType: "SelfActivate",
39 roleName: "Key Vault Administrator",
40 expirationType: "AfterDuration",
41 expirationDuration: "PT5M", // activate for 5 minutes
42 continueOnFailure: true,
43 });
44
45 // Wait for the role to be activated
46 console.log(chalk.green("Elevation successful."));
47 console.warn(chalk.yellowBright("Waiting 5 seconds..."));
48 await new Promise((res) => setTimeout(res, 5000));
49
50 return await keyVaultClient.getSecrets(vaultName);
51 } catch (e) {
52 console.warn(
53 chalk.yellow(
54 "Elevation to key vault admin failed...attempting to get secrets as key vault reader.",
55 ),
56 );
57 }
58
59 try {
60 console.warn(chalk.yellowBright("Elevating to get secrets..."));
61 const pimClient = await getPIMClient();
62 await pimClient.elevate({
63 requestType: "SelfActivate",
64 roleName: "Key Vault Secrets User",
65 expirationType: "AfterDuration",
66 expirationDuration: "PT5M", // activate for 5 minutes
67 continueOnFailure: true,
68 });
69
70 // Wait for the role to be activated
71 console.log(chalk.green("Elevation successful."));
72 console.warn(chalk.yellowBright("Waiting 5 seconds..."));
73 await new Promise((res) => setTimeout(res, 5000));
74 } catch (e) {
75 console.warn(
76 chalk.yellow(
77 "Elevation failed...attempting to get secrets without elevation.",
78 ),
79 );
80 }
81
82 return await keyVaultClient.getSecrets(vaultName);
83 }
84}
85
86async function getSecrets(keyVaultClient, vaultName, shared) {
87 console.log(
88 `Getting existing ${shared ? "shared" : "private"} secrets from ${chalk.cyanBright(vaultName)} key vault.`,
89 );
90 const secretList = await getSecretListWithElevation(
91 keyVaultClient,
92 vaultName,
93 );
94 const p = [];
95 for (const secret of secretList) {
96 if (secret.attributes.enabled) {
97 const secretName = secret.id.split("/").pop();
98 p.push(
99 (async () => {
100 const response = await keyVaultClient.readSecret(
101 vaultName,
102 secretName,
103 );
104 return [secretName, response.value];
105 })(),
106 );
107 }
108 }
109
110 return Promise.all(p);
111}
112
113async function execAsync(command, options) {
114 return new Promise((res, rej) => {
115 child_process.exec(command, options, (err, stdout, stderr) => {
116 if (err) {
117 rej(err);
118 return;
119 }
120 if (stderr) {
121 console.log(stderr + stdout);
122 }
123 res(stdout);
124 });
125 });
126}
127
128class AzCliKeyVaultClient {
129 static async get() {
130 // We use this to validate that the user is logged in (already ran `az login`).
131 try {
132 const account = JSON.parse(await execAsync("az account show"));
133 console.log(`Logged in as ${chalk.cyanBright(account.user.name)}`);
134 } catch (e) {
135 console.error(
136 "ERROR: User not logged in to Azure CLI. Run 'az login'.",
137 );
138 process.exit(1);
139 }
140 // Note: 'az keyvault' commands work regardless of which subscription is currently "in context",
141 // as long as the user is listed in the vault's access policy, so we don't need to do 'az account set'.
142 return new AzCliKeyVaultClient();
143 }
144
145 async getSecrets(vaultName) {
146 return JSON.parse(
147 await execAsync(
148 `az keyvault secret list --vault-name ${vaultName}`,
149 ),
150 );
151 }
152
153 async readSecret(vaultName, secretName) {
154 return JSON.parse(
155 await execAsync(
156 `az keyvault secret show --vault-name ${vaultName} --name ${secretName}`,
157 ),
158 );
159 }
160
161 async writeSecret(vaultName, secretName, secretValue) {
162 return JSON.parse(
163 await execAsync(
164 `az keyvault secret set --vault-name ${vaultName} --name ${secretName} --value '${secretValue}'`,
165 ),
166 );
167 }
168}
169
170async function getKeyVaultClient() {
171 return AzCliKeyVaultClient.get();
172}
173
174async function readDotenv() {
175 if (!fs.existsSync(dotenvPath)) {
176 return [];
177 }
178 const dotenvFile = await fs.promises.readFile(dotenvPath, "utf8");
179 const dotEnv = dotenvFile.split("\n").map((line) => {
180 const [key, ...value] = line.split("=");
181 if (key.includes("-")) {
182 throw new Error(
183 `Invalid dotenv key '${key}' for key vault. Keys cannot contain dashes.`,
184 );
185 }
186 return [key, value.join("=")];
187 });
188 return dotEnv;
189}
190
191function toSecretKey(envKey) {
192 return envKey.split("_").join("-");
193}
194
195function toEnvKey(secretKey) {
196 return secretKey.split("-").join("_");
197}
198
199// Return 0 if the value is the same. -1 if the user skipped. 1 if the value was updated.
200async function pushSecret(
201 stdio,
202 keyVaultClient,
203 vault,
204 secrets,
205 secretKey,
206 value,
207 shared = true,
208) {
209 const suffix = shared ? "" : " (private)";
210 const secretValue = secrets.get(secretKey);
211 if (secretValue === value) {
212 return 0;
213 }
214 if (secrets.has(secretKey)) {
215 const answer = await stdio.question(
216 ` ${secretKey} changed.\n Current value: ${secretValue}\n New value: ${value}\n Are you sure you want to overwrite the value of ${secretKey}? (y/n)`,
217 );
218 if (answer.toLowerCase() !== "y") {
219 console.log("Skipping...");
220 return -1;
221 }
222 console.log(` Overwriting ${secretKey}${suffix}`);
223 } else {
224 console.log(` Creating ${secretKey}${suffix}`);
225 }
226 await keyVaultClient.writeSecret(vault, secretKey, value);
227 return 1;
228}
229
230function getVaultNames(dotEnv) {
231 return {
232 shared:
233 paramSharedVault ??
234 dotEnv.get("TYPEAGENT_SHAREDVAULT") ??
235 config.vault.shared,
236 private:
237 paramPrivateVault ??
238 dotEnv.get("TYPEAGENT_PRIVATEVAULT") ??
239 undefined,
240 };
241}
242
243async function pushSecrets() {
244 const dotEnv = await readDotenv();
245 const keyVaultClient = await getKeyVaultClient();
246 const vaultNames = getVaultNames(dotEnv);
247 const sharedSecrets = new Map(
248 await getSecrets(keyVaultClient, vaultNames.shared, true),
249 );
250 const privateSecrets = new Map(
251 vaultNames.private
252 ? await getSecrets(keyVaultClient, vaultNames.private, false)
253 : undefined,
254 );
255
256 console.log(`Pushing secrets from ${dotenvPath} to key vault.`);
257 let updated = 0;
258 let skipped = 0;
259 const stdio = readline.createInterface(process.stdin, process.stdout);
260 try {
261 for (const [envKey, value] of dotEnv) {
262 const secretKey = toSecretKey(envKey);
263 if (sharedKeys.includes(envKey)) {
264 const result = await pushSecret(
265 stdio,
266 keyVaultClient,
267 vaultNames.shared,
268 sharedSecrets,
269 secretKey,
270 value,
271 );
272 if (result === 1) {
273 updated++;
274 }
275 if (result === -1) {
276 skipped++;
277 }
278 } else if (privateKeys.includes(envKey)) {
279 if (privateVault === undefined) {
280 console.log(` Skipping private key ${envKey}.`);
281 continue;
282 }
283 const result = await pushSecret(
284 stdio,
285 keyVaultClient,
286 vaultNames.private,
287 privateSecrets,
288 secretKey,
289 value,
290 false,
291 );
292 if (result === 1) {
293 updated++;
294 }
295 if (result === -1) {
296 skipped++;
297 }
298 } else {
299 console.log(` Skipping unknown key ${envKey}.`);
300 }
301 }
302 } finally {
303 stdio.close();
304 }
305 if (skipped === 0 && updated === 0) {
306 console.log("All values up to date in key vault.");
307 return;
308 }
309 if (skipped !== 0) {
310 console.log(`${skipped} secrets skipped.`);
311 }
312 if (updated !== 0) {
313 console.log(`${updated} secrets updated.`);
314 }
315}
316
317async function pullSecretsFromVault(keyVaultClient, vaultName, shared, dotEnv) {
318 const keys = shared ? sharedKeys : privateKeys;
319 const secrets = await getSecrets(keyVaultClient, vaultName, shared);
320 if (secrets.length === 0) {
321 console.log(
322 chalk.yellow(
323 `WARNING: No secrets found in key vault ${chalk.cyanBright(vaultName)}.`,
324 ),
325 );
326 return undefined;
327 }
328
329 let updated = 0;
330 for (const [secretKey, value] of secrets) {
331 const envKey = toEnvKey(secretKey);
332 if (keys.includes(envKey) && dotEnv.get(envKey) !== value) {
333 console.log(` Updating ${envKey}`);
334 dotEnv.set(envKey, value);
335 updated++;
336 }
337 }
338 return updated;
339}
340
341async function pullSecrets() {
342 const dotEnv = new Map(await readDotenv());
343 const keyVaultClient = await getKeyVaultClient();
344 const vaultNames = getVaultNames(dotEnv);
345 console.log(`Pulling secrets to ${chalk.cyanBright(dotenvPath)}`);
346 const sharedUpdated = await pullSecretsFromVault(
347 keyVaultClient,
348 vaultNames.shared,
349 true,
350 dotEnv,
351 );
352 const privateUpdated = vaultNames.private
353 ? await pullSecretsFromVault(
354 keyVaultClient,
355 vaultNames.private,
356 false,
357 dotEnv,
358 )
359 : undefined;
360
361 if (sharedUpdated === undefined && privateUpdated === undefined) {
362 throw new Error("No secrets found in key vaults.");
363 }
364
365 let updated = (sharedUpdated ?? 0) + (privateUpdated ?? 0);
366 for (const key of deleteKeys) {
367 if (dotEnv.has(key)) {
368 console.log(` Deleting ${key}`);
369 dotEnv.delete(key);
370 updated++;
371 }
372 }
373 if (dotEnv.get("TYPEAGENT_SHAREDVAULT") !== vaultNames.shared) {
374 console.log(` Updating TYPEAGENT_SHAREDVAULT`);
375 dotEnv.set("TYPEAGENT_SHAREDVAULT", vaultNames.shared);
376 updated++;
377 }
378 if (
379 vaultNames.private &&
380 dotEnv.get("TYPEAGENT_PRIVATEVAULT") !== vaultNames.private
381 ) {
382 console.log(` Updating TYPEAGENT_PRIVATEVAULT`);
383 dotEnv.set("TYPEAGENT_PRIVATEVAULT", vaultNames.private);
384 updated++;
385 }
386
387 if (updated === 0) {
388 console.log(
389 `\nAll values up to date in ${chalk.cyanBright(dotenvPath)}`,
390 );
391 return;
392 }
393 console.log(
394 `\n${updated} values updated.\nWriting '${chalk.cyanBright(dotenvPath)}'.`,
395 );
396
397 await fs.promises.writeFile(
398 dotenvPath,
399 [...dotEnv.entries()]
400 .map(([key, value]) => (key ? `${key}=${value}` : ""))
401 .join("\n"),
402 );
403}
404
405const commands = ["push", "pull", "help"];
406(async () => {
407 const command = commands.includes(process.argv[2])
408 ? process.argv[2]
409 : undefined;
410 const start = command !== undefined ? 3 : 2;
411 for (let i = start; i < process.argv.length; i++) {
412 const arg = process.argv[i];
413 if (arg === "--vault") {
414 paramSharedVault = process.argv[i + 1];
415 if (paramSharedVault === undefined) {
416 throw new Error("Missing value for --vault");
417 }
418 i++;
419 continue;
420 }
421
422 if (arg === "--private") {
423 paramPrivateVault = process.argv[i + 1];
424 if (paramPrivateVault === undefined) {
425 throw new Error("Missing value for --private");
426 }
427 i++;
428 continue;
429 }
430
431 throw new Error(`Unknown argument: ${arg}`);
432 }
433 switch (command) {
434 case "push":
435 await pushSecrets();
436 break;
437 case "pull":
438 case undefined:
439 await pullSecrets();
440 break;
441 case "help":
442 printHelp();
443 return;
444 default:
445 throw new Error(`Unknown argument '${process.argv[2]}'`);
446 }
447})().catch((e) => {
448 if (
449 e.message.includes(
450 "'az' is not recognized as an internal or external command",
451 )
452 ) {
453 console.error(
454 chalk.red(
455 `ERROR: Azure CLI is not installed. Install it and run 'az login' before running this tool.`,
456 ),
457 );
458 // eslint-disable-next-line no-undef
459 exit(0);
460 }
461
462 console.error(chalk.red(`FATAL ERROR: ${e.stack}`));
463 process.exit(-1);
464});
465