-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.ts
More file actions
152 lines (135 loc) · 5.27 KB
/
app.ts
File metadata and controls
152 lines (135 loc) · 5.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import fs from 'node:fs/promises';
import express from 'express';
import asyncHandler from "express-async-handler";
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
import { exec } from 'promisify-child-process';
import { file } from 'tmp-promise'
// Names of Google Secret Manager secrets
const PROVIDER_FILE_SECRET_NAME = (() => {
const envVar = process.env.PROVIDER_FILE_SECRET_NAME;
if (!envVar) {
throw new Error("PROVIDER_FILE_SECRET_NAME environment variable is required");
}
return envVar;
})()
const BZERO_FILE_SECRET_NAME = (() => {
const envVar = process.env.BZERO_FILE_SECRET_NAME;
if (!envVar) {
throw new Error("BZERO_FILE_SECRET_NAME environment variable is required");
}
return envVar;
})()
// Verify the zli executable is available at startup instead of waiting for a
// first request
await fs.access('/usr/bin/zli', fs.constants.X_OK);
// Use secret manager to get credentials required to run `zli service-account
// login`
const secretClient = new SecretManagerServiceClient();
/**
* Load a secret from the secret manager
* @param secretName Name of the secret
* @returns The contents of the secret value
*/
async function loadSecretFromSecretManager(secretName: string) {
const [accessResponse] = await secretClient.accessSecretVersion({
name: secretName,
});
return accessResponse.payload?.data?.toString();
}
// Fetch the credentials
const providerCredentials = await loadSecretFromSecretManager(PROVIDER_FILE_SECRET_NAME);
const bzeroCredentials = await loadSecretFromSecretManager(BZERO_FILE_SECRET_NAME);
// Fail early if fetched credentials are empty
if (!providerCredentials || !bzeroCredentials) {
throw new Error("One of the required credential secrets is empty");
}
/**
* Execute a command, wait for it to complete, and capture stdout and stderr
* @param cmd The command to execute
* @returns The captured stdout and stderr outputs
*/
async function execCommand(cmd: string): Promise<string> {
const { stdout, stderr } = await exec(cmd);
return [stdout, stderr].join('\n');
}
/**
* Runs `zli service-account login` using the fetched credentials
* @returns The stdout and stderr output from the `zli`
*/
async function zliServiceAccountLogin(): Promise<string> {
// Create temp files (cleaned up after done using them) to store credentials
// on disk. `zli service-account login` only takes in filepaths right now
const { path: providerFilePath, cleanup: cleanupProviderFile } = await file();
const { path: bzeroFilePath, cleanup: cleanupBzeroFile } = await file();
try {
await fs.writeFile(providerFilePath, providerCredentials as string);
await fs.writeFile(bzeroFilePath, bzeroCredentials as string);
return await execCommand(`zli service-account login --providerCreds ${providerFilePath} --bzeroCreds ${bzeroFilePath}`);
} finally {
await cleanupBzeroFile();
await cleanupProviderFile();
}
}
// Define route handlers
export const app = express();
// Display zli version
app.get('/', asyncHandler(async (_, res) => {
res.send(await execCommand('zli --version'))
}));
// Login to BastionZero using SA credentials
app.get('/login', asyncHandler(async (_, res) => {
const zliLoginOutput = await zliServiceAccountLogin()
res.send(zliLoginOutput);
}));
// Generate SSH config
app.get('/generate', asyncHandler(async (_, res) => {
const generateOutput = await execCommand('zli generate sshConfig')
res.send(generateOutput);
}));
// In-memory constants used to skip some steps in /ssh after first successful
// request for a given CloudRun container. Call `/generate` to force re-generate
// the sshConfig. Call `/login` to force re-login.
let loggedIn: boolean = false;
let generatedSshConfig: boolean = false;
// SSH example
app.get('/ssh', asyncHandler(async (req, res) => {
if (!loggedIn) {
// Login to BastionZero using SA credentials
const zliLoginOutput = await zliServiceAccountLogin();
console.log(`zli service-account login: ${zliLoginOutput}`);
loggedIn = true;
}
// Build ssh command from query. Set some defaults if query parameters are
// missing
let sshUserString: string = "root";
let sshHostString: string = "";
let sshCommandString: string = "uname -a";
if (req.query.user) {
sshUserString = req.query.user as string;
}
if (req.query.host) {
sshHostString = req.query.host as string;
} else {
throw new Error("Please specify a host in the query parameters");
}
if (req.query.cmd) {
sshCommandString = req.query.cmd as string;
}
const constructedSshCmd = `ssh -F /home/.ssh/config ${sshUserString}@${sshHostString} ${sshCommandString}`;
console.log(`SSH command: ${constructedSshCmd}`);
try {
if (!generatedSshConfig) {
// Generate ssh config
const generateOutput = await execCommand('zli generate sshConfig')
console.log(`zli generate sshConfig: ${generateOutput}`);
generatedSshConfig = true;
}
// SSH!
const sshOutput = await execCommand(constructedSshCmd);
res.send(sshOutput);
} catch (error) {
// It could be that re-login fixes the issue
loggedIn = false;
throw error;
}
}));