microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.1.0-py

Branches

Tags

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

Clone

HTTPS

Download ZIP

ts/tools/scripts/getKeys.mjs

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