microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1adc19e53b9dfd93f84ff36cd1ee7fdfdefe1548

Branches

Tags

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

Clone

HTTPS

Download ZIP

ts/tools/scripts/getKeys.mjs

401lines · 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 sharedVault = config.vault.shared;
24let privateVault = 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 } catch (e) {
50 console.warn(
51 chalk.yellow(
52 "Elevation failed...attempting to get secrets without elevation.",
53 ),
54 );
55 }
56 return await keyVaultClient.getSecrets(vaultName);
57 }
58}
59
60async function getSecrets(keyVaultClient, shared) {
61 const vaultName = shared ? sharedVault : privateVault;
62 console.log(
63 `Getting existing ${shared ? "shared" : "private"} secrets from ${chalk.cyanBright(vaultName)} key vault.`,
64 );
65 const secretList = await getSecretListWithElevation(
66 keyVaultClient,
67 vaultName,
68 );
69 const p = [];
70 for (const secret of secretList) {
71 if (secret.attributes.enabled) {
72 const secretName = secret.id.split("/").pop();
73 p.push(
74 (async () => {
75 const response = await keyVaultClient.readSecret(
76 vaultName,
77 secretName,
78 );
79 return [secretName, response.value];
80 })(),
81 );
82 }
83 }
84
85 return Promise.all(p);
86}
87
88async function execAsync(command, options) {
89 return new Promise((res, rej) => {
90 child_process.exec(command, options, (err, stdout, stderr) => {
91 if (err) {
92 rej(err);
93 return;
94 }
95 if (stderr) {
96 console.log(stderr + stdout);
97 }
98 res(stdout);
99 });
100 });
101}
102
103class AzCliKeyVaultClient {
104 static async get() {
105 // We use this to validate that the user is logged in (already ran `az login`).
106 try {
107 const account = JSON.parse(await execAsync("az account show"));
108 console.log(`Logged in as ${chalk.cyanBright(account.user.name)}`);
109 } catch (e) {
110 console.error(
111 "ERROR: User not logged in to Azure CLI. Run 'az login'.",
112 );
113 process.exit(1);
114 }
115 // Note: 'az keyvault' commands work regardless of which subscription is currently "in context",
116 // as long as the user is listed in the vault's access policy, so we don't need to do 'az account set'.
117 return new AzCliKeyVaultClient();
118 }
119
120 async getSecrets(vaultName) {
121 return JSON.parse(
122 await execAsync(
123 `az keyvault secret list --vault-name ${vaultName}`,
124 ),
125 );
126 }
127
128 async readSecret(vaultName, secretName) {
129 return JSON.parse(
130 await execAsync(
131 `az keyvault secret show --vault-name ${vaultName} --name ${secretName}`,
132 ),
133 );
134 }
135
136 async writeSecret(vaultName, secretName, secretValue) {
137 return JSON.parse(
138 await execAsync(
139 `az keyvault secret set --vault-name ${vaultName} --name ${secretName} --value '${secretValue}'`,
140 ),
141 );
142 }
143}
144
145async function getKeyVaultClient() {
146 return AzCliKeyVaultClient.get();
147}
148
149async function readDotenv() {
150 if (!fs.existsSync(dotenvPath)) {
151 return [];
152 }
153 const dotenvFile = await fs.promises.readFile(dotenvPath, "utf8");
154 const dotEnv = dotenvFile.split("\n").map((line) => {
155 const [key, ...value] = line.split("=");
156 if (key.includes("-")) {
157 throw new Error(
158 `Invalid dotenv key '${key}' for key vault. Keys cannot contain dashes.`,
159 );
160 }
161 return [key, value.join("=")];
162 });
163 return dotEnv;
164}
165
166function toSecretKey(envKey) {
167 return envKey.split("_").join("-");
168}
169
170function toEnvKey(secretKey) {
171 return secretKey.split("-").join("_");
172}
173
174// Return 0 if the value is the same. -1 if the user skipped. 1 if the value was updated.
175async function pushSecret(
176 stdio,
177 keyVaultClient,
178 vault,
179 secrets,
180 secretKey,
181 value,
182 shared = true,
183) {
184 const suffix = shared ? "" : " (private)";
185 const secretValue = secrets.get(secretKey);
186 if (secretValue === value) {
187 return 0;
188 }
189 if (secrets.has(secretKey)) {
190 const answer = await stdio.question(
191 ` ${secretKey} changed.\n Current value: ${secretValue}\n New value: ${value}\n Are you sure you want to overwrite the value of ${secretKey}? (y/n)`,
192 );
193 if (answer.toLowerCase() !== "y") {
194 console.log("Skipping...");
195 return -1;
196 }
197 console.log(` Overwriting ${secretKey}${suffix}`);
198 } else {
199 console.log(` Creating ${secretKey}${suffix}`);
200 }
201 await keyVaultClient.writeSecret(vault, secretKey, value);
202 return 1;
203}
204
205async function pushSecrets() {
206 const dotEnv = await readDotenv();
207 const keyVaultClient = await getKeyVaultClient();
208 const sharedSecrets = new Map(await getSecrets(keyVaultClient, true));
209 const privateSecrets = new Map(
210 privateVault ? await getSecrets(keyVaultClient, false) : undefined,
211 );
212
213 console.log(`Pushing secrets from ${dotenvPath} to key vault.`);
214 let updated = 0;
215 let skipped = 0;
216 const stdio = readline.createInterface(process.stdin, process.stdout);
217 try {
218 for (const [envKey, value] of dotEnv) {
219 const secretKey = toSecretKey(envKey);
220 if (sharedKeys.includes(envKey)) {
221 const result = await pushSecret(
222 stdio,
223 keyVaultClient,
224 sharedVault,
225 sharedSecrets,
226 secretKey,
227 value,
228 );
229 if (result === 1) {
230 updated++;
231 }
232 if (result === -1) {
233 skipped++;
234 }
235 } else if (privateKeys.includes(envKey)) {
236 if (privateVault === undefined) {
237 console.log(` Skipping private key ${envKey}.`);
238 continue;
239 }
240 const result = await pushSecret(
241 stdio,
242 keyVaultClient,
243 privateVault,
244 privateSecrets,
245 secretKey,
246 value,
247 false,
248 );
249 if (result === 1) {
250 updated++;
251 }
252 if (result === -1) {
253 skipped++;
254 }
255 } else {
256 console.log(` Skipping unknown key ${envKey}.`);
257 }
258 }
259 } finally {
260 stdio.close();
261 }
262 if (skipped === 0 && updated === 0) {
263 console.log("All values up to date in key vault.");
264 return;
265 }
266 if (skipped !== 0) {
267 console.log(`${skipped} secrets skipped.`);
268 }
269 if (updated !== 0) {
270 console.log(`${updated} secrets updated.`);
271 }
272}
273
274async function pullSecretsFromVault(keyVaultClient, shared, dotEnv) {
275 const vaultName = shared ? sharedVault : privateVault;
276 const keys = shared ? sharedKeys : privateKeys;
277 const secrets = await getSecrets(keyVaultClient, shared);
278 if (secrets.length === 0) {
279 console.log(
280 chalk.yellow(
281 `WARNING: No secrets found in key vault ${chalk.cyanBright(vaultName)}.`,
282 ),
283 );
284 return undefined;
285 }
286
287 let updated = 0;
288 for (const [secretKey, value] of secrets) {
289 const envKey = toEnvKey(secretKey);
290 if (keys.includes(envKey) && dotEnv.get(envKey) !== value) {
291 console.log(` Updating ${envKey}`);
292 dotEnv.set(envKey, value);
293 updated++;
294 }
295 }
296 return updated;
297}
298
299async function pullSecrets() {
300 const dotEnv = new Map(await readDotenv());
301 const keyVaultClient = await getKeyVaultClient();
302 console.log(`Pulling secrets to ${chalk.cyanBright(dotenvPath)}`);
303 const sharedUpdated = await pullSecretsFromVault(
304 keyVaultClient,
305 true,
306 dotEnv,
307 );
308 const privateUpdated = privateVault
309 ? await pullSecretsFromVault(keyVaultClient, false, dotEnv)
310 : undefined;
311
312 if (sharedUpdated === undefined && privateUpdated === undefined) {
313 throw new Error("No secrets found in key vaults.");
314 }
315
316 let updated = (sharedUpdated ?? 0) + (privateUpdated ?? 0);
317 for (const key of deleteKeys) {
318 if (dotEnv.has(key)) {
319 console.log(` Deleting ${key}`);
320 dotEnv.delete(key);
321 updated++;
322 }
323 }
324
325 if (updated === 0) {
326 console.log(
327 `\nAll values up to date in ${chalk.cyanBright(dotenvPath)}`,
328 );
329 return;
330 }
331 console.log(
332 `\n${updated} values updated.\nWriting '${chalk.cyanBright(dotenvPath)}'.`,
333 );
334 await fs.promises.writeFile(
335 dotenvPath,
336 [...dotEnv.entries()]
337 .map(([key, value]) => `${key}=${value}`)
338 .join("\n"),
339 );
340}
341
342const commands = ["push", "pull", "help"];
343(async () => {
344 const command = commands.includes(process.argv[2])
345 ? process.argv[2]
346 : undefined;
347 const start = command !== undefined ? 3 : 2;
348 for (let i = start; i < process.argv.length; i++) {
349 const arg = process.argv[i];
350 if (arg === "--vault") {
351 sharedVault = process.argv[i + 1];
352 if (sharedVault === undefined) {
353 throw new Error("Missing value for --vault");
354 }
355 i++;
356 continue;
357 }
358
359 if (arg === "--private") {
360 privateVault = process.argv[i + 1];
361 if (privateVault === undefined) {
362 throw new Error("Missing value for --private");
363 }
364 i++;
365 continue;
366 }
367
368 throw new Error(`Unknown argument: ${arg}`);
369 }
370 switch (command) {
371 case "push":
372 await pushSecrets();
373 break;
374 case "pull":
375 case undefined:
376 await pullSecrets();
377 break;
378 case "help":
379 printHelp();
380 return;
381 default:
382 throw new Error(`Unknown argument '${process.argv[2]}'`);
383 }
384})().catch((e) => {
385 if (
386 e.message.includes(
387 "'az' is not recognized as an internal or external command",
388 )
389 ) {
390 console.error(
391 chalk.red(
392 `ERROR: Azure CLI is not installed. Install it and run 'az login' before running this tool.`,
393 ),
394 );
395 // eslint-disable-next-line no-undef
396 exit(0);
397 }
398
399 console.error(chalk.red(`FATAL ERROR: ${e.stack}`));
400 process.exit(-1);
401});
402