microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bec9232be52610a3acd24f99ddffecde526f8200

Branches

Tags

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

Clone

HTTPS

Download ZIP

ts/tools/scripts/getKeys.mjs

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