microsoft/TypeAgent

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6ff601b5d3609ff2e4707793bb3fc4141f5d84c7

Branches

Tags

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

Clone

HTTPS

Download ZIP

ts/tools/scripts/getKeys.mjs

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