@zincapp/znvault-cli 2.16.0 → 2.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/apikey.d.ts.map +1 -1
- package/dist/commands/apikey.js +13 -7
- package/dist/commands/apikey.js.map +1 -1
- package/dist/commands/dynamic-secrets.d.ts +3 -0
- package/dist/commands/dynamic-secrets.d.ts.map +1 -0
- package/dist/commands/dynamic-secrets.js +743 -0
- package/dist/commands/dynamic-secrets.js.map +1 -0
- package/dist/commands/secret.d.ts.map +1 -1
- package/dist/commands/secret.js +26 -37
- package/dist/commands/secret.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +2 -2
- package/dist/lib/client.js.map +1 -1
- package/dist/types/index.d.ts +10 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
// Path: znvault-cli/src/commands/dynamic-secrets.ts
|
|
2
|
+
// CLI commands for dynamic secrets management (on-demand database credentials)
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import Table from 'cli-table3';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { client } from '../lib/client.js';
|
|
7
|
+
import * as output from '../lib/output.js';
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Helper Functions
|
|
10
|
+
// ============================================================================
|
|
11
|
+
function formatDuration(seconds) {
|
|
12
|
+
if (seconds < 60)
|
|
13
|
+
return `${seconds}s`;
|
|
14
|
+
if (seconds < 3600)
|
|
15
|
+
return `${Math.floor(seconds / 60)}m`;
|
|
16
|
+
if (seconds < 86400)
|
|
17
|
+
return `${Math.floor(seconds / 3600)}h`;
|
|
18
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
19
|
+
}
|
|
20
|
+
function formatStatus(status) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 'ACTIVE': return output.isPlainMode() ? 'ACTIVE' : '\x1b[32mACTIVE\x1b[0m';
|
|
23
|
+
case 'DISABLED': return output.isPlainMode() ? 'DISABLED' : '\x1b[33mDISABLED\x1b[0m';
|
|
24
|
+
case 'FAILED': return output.isPlainMode() ? 'FAILED' : '\x1b[31mFAILED\x1b[0m';
|
|
25
|
+
case 'TESTING': return output.isPlainMode() ? 'TESTING' : '\x1b[36mTESTING\x1b[0m';
|
|
26
|
+
case 'EXPIRED': return output.isPlainMode() ? 'EXPIRED' : '\x1b[33mEXPIRED\x1b[0m';
|
|
27
|
+
case 'REVOKED': return output.isPlainMode() ? 'REVOKED' : '\x1b[31mREVOKED\x1b[0m';
|
|
28
|
+
default: return status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function formatDate(dateStr) {
|
|
32
|
+
if (!dateStr)
|
|
33
|
+
return '-';
|
|
34
|
+
return new Date(dateStr).toLocaleString();
|
|
35
|
+
}
|
|
36
|
+
function formatTtl(seconds) {
|
|
37
|
+
if (seconds === null)
|
|
38
|
+
return 'inherit';
|
|
39
|
+
return formatDuration(seconds);
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Connection Commands
|
|
43
|
+
// ============================================================================
|
|
44
|
+
async function listConnections(options) {
|
|
45
|
+
const spinner = ora('Fetching connections...').start();
|
|
46
|
+
try {
|
|
47
|
+
const response = await client.get('/v1/dynamic-secrets/connections');
|
|
48
|
+
spinner.stop();
|
|
49
|
+
if (options.json) {
|
|
50
|
+
output.json(response);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (response.length === 0) {
|
|
54
|
+
output.info('No database connections found.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const table = new Table({
|
|
58
|
+
head: ['Name', 'Type', 'Status', 'Default TTL', 'Max TTL', 'Roles', 'Active Leases'],
|
|
59
|
+
style: { head: ['cyan'] },
|
|
60
|
+
});
|
|
61
|
+
for (const conn of response) {
|
|
62
|
+
table.push([
|
|
63
|
+
conn.name,
|
|
64
|
+
conn.connectionType,
|
|
65
|
+
formatStatus(conn.status),
|
|
66
|
+
formatTtl(conn.defaultTtlSeconds),
|
|
67
|
+
formatTtl(conn.maxTtlSeconds),
|
|
68
|
+
String(conn.roleCount ?? 0),
|
|
69
|
+
String(conn.activeLeases ?? 0),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
console.log(table.toString());
|
|
73
|
+
output.info(`${response.length} connection(s) found`);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
spinner.fail('Failed to list connections');
|
|
77
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function getConnection(nameOrId, options) {
|
|
82
|
+
const spinner = ora('Fetching connection...').start();
|
|
83
|
+
try {
|
|
84
|
+
const response = await client.get(`/v1/dynamic-secrets/connections/${nameOrId}`);
|
|
85
|
+
spinner.stop();
|
|
86
|
+
if (options.json) {
|
|
87
|
+
output.json(response);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
output.keyValue({
|
|
91
|
+
'ID': response.id,
|
|
92
|
+
'Name': response.name,
|
|
93
|
+
'Description': response.description || '-',
|
|
94
|
+
'Type': response.connectionType,
|
|
95
|
+
'Status': formatStatus(response.status),
|
|
96
|
+
'Max Connections': response.maxOpenConnections,
|
|
97
|
+
'Timeout': `${response.connectionTimeoutSeconds}s`,
|
|
98
|
+
'Default TTL': formatTtl(response.defaultTtlSeconds),
|
|
99
|
+
'Max TTL': formatTtl(response.maxTtlSeconds),
|
|
100
|
+
'Last Health Check': formatDate(response.lastHealthCheck),
|
|
101
|
+
'Health Status': response.lastHealthCheckStatus === null ? '-' : (response.lastHealthCheckStatus ? 'Healthy' : 'Unhealthy'),
|
|
102
|
+
'Roles': String(response.roleCount ?? 0),
|
|
103
|
+
'Active Leases': String(response.activeLeases ?? 0),
|
|
104
|
+
'Created': formatDate(response.createdAt),
|
|
105
|
+
'Updated': formatDate(response.updatedAt),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
spinner.fail('Failed to get connection');
|
|
110
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function createConnection(options) {
|
|
115
|
+
// Interactive prompts if options not provided
|
|
116
|
+
const name = options.name || (await inquirer.prompt([{
|
|
117
|
+
type: 'input',
|
|
118
|
+
name: 'name',
|
|
119
|
+
message: 'Connection name:',
|
|
120
|
+
validate: (input) => input.trim() ? true : 'Name is required',
|
|
121
|
+
}])).name;
|
|
122
|
+
const connectionType = options.type?.toUpperCase() || (await inquirer.prompt([{
|
|
123
|
+
type: 'list',
|
|
124
|
+
name: 'type',
|
|
125
|
+
message: 'Database type:',
|
|
126
|
+
choices: ['POSTGRESQL', 'MYSQL'],
|
|
127
|
+
}])).type;
|
|
128
|
+
const connectionString = options.connectionString || (await inquirer.prompt([{
|
|
129
|
+
type: 'password',
|
|
130
|
+
name: 'connectionString',
|
|
131
|
+
message: 'Connection string:',
|
|
132
|
+
mask: '*',
|
|
133
|
+
validate: (input) => input.trim() ? true : 'Connection string is required',
|
|
134
|
+
}])).connectionString;
|
|
135
|
+
const spinner = ora('Creating connection...').start();
|
|
136
|
+
try {
|
|
137
|
+
const body = {
|
|
138
|
+
name,
|
|
139
|
+
connectionType,
|
|
140
|
+
connectionString,
|
|
141
|
+
};
|
|
142
|
+
if (options.description)
|
|
143
|
+
body.description = options.description;
|
|
144
|
+
if (options.maxConnections)
|
|
145
|
+
body.maxOpenConnections = parseInt(options.maxConnections, 10);
|
|
146
|
+
if (options.timeout)
|
|
147
|
+
body.connectionTimeoutSeconds = parseInt(options.timeout, 10);
|
|
148
|
+
if (options.defaultTtl)
|
|
149
|
+
body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
|
|
150
|
+
if (options.maxTtl)
|
|
151
|
+
body.maxTtlSeconds = parseInt(options.maxTtl, 10);
|
|
152
|
+
const response = await client.post('/v1/dynamic-secrets/connections', body);
|
|
153
|
+
spinner.succeed('Connection created');
|
|
154
|
+
if (options.json) {
|
|
155
|
+
output.json(response);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
output.success(`Connection "${response.name}" created with ID: ${response.id}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
spinner.fail('Failed to create connection');
|
|
163
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function updateConnection(nameOrId, options) {
|
|
168
|
+
const spinner = ora('Updating connection...').start();
|
|
169
|
+
try {
|
|
170
|
+
const body = {};
|
|
171
|
+
if (options.description !== undefined)
|
|
172
|
+
body.description = options.description;
|
|
173
|
+
if (options.maxConnections)
|
|
174
|
+
body.maxOpenConnections = parseInt(options.maxConnections, 10);
|
|
175
|
+
if (options.timeout)
|
|
176
|
+
body.connectionTimeoutSeconds = parseInt(options.timeout, 10);
|
|
177
|
+
if (options.defaultTtl)
|
|
178
|
+
body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
|
|
179
|
+
if (options.maxTtl)
|
|
180
|
+
body.maxTtlSeconds = parseInt(options.maxTtl, 10);
|
|
181
|
+
if (options.status)
|
|
182
|
+
body.status = options.status.toUpperCase();
|
|
183
|
+
const response = await client.patch(`/v1/dynamic-secrets/connections/${nameOrId}`, body);
|
|
184
|
+
spinner.succeed('Connection updated');
|
|
185
|
+
if (options.json) {
|
|
186
|
+
output.json(response);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
output.success(`Connection "${response.name}" updated`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
spinner.fail('Failed to update connection');
|
|
194
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function deleteConnection(nameOrId, options) {
|
|
199
|
+
if (!options.force) {
|
|
200
|
+
const { confirm } = await inquirer.prompt([{
|
|
201
|
+
type: 'confirm',
|
|
202
|
+
name: 'confirm',
|
|
203
|
+
message: `Are you sure you want to delete connection "${nameOrId}"? This will also delete all associated roles.`,
|
|
204
|
+
default: false,
|
|
205
|
+
}]);
|
|
206
|
+
if (!confirm) {
|
|
207
|
+
output.info('Cancelled');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const spinner = ora('Deleting connection...').start();
|
|
212
|
+
try {
|
|
213
|
+
await client.delete(`/v1/dynamic-secrets/connections/${nameOrId}`);
|
|
214
|
+
spinner.succeed(`Connection "${nameOrId}" deleted`);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
spinner.fail('Failed to delete connection');
|
|
218
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function testConnection(nameOrId, options) {
|
|
223
|
+
const spinner = ora('Testing connection...').start();
|
|
224
|
+
try {
|
|
225
|
+
const response = await client.post(`/v1/dynamic-secrets/connections/${nameOrId}/test`, {});
|
|
226
|
+
if (response.success) {
|
|
227
|
+
spinner.succeed('Connection test successful');
|
|
228
|
+
if (options.json) {
|
|
229
|
+
output.json(response);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
spinner.fail('Connection test failed');
|
|
234
|
+
output.error(response.error || 'Unknown error');
|
|
235
|
+
if (options.json) {
|
|
236
|
+
output.json(response);
|
|
237
|
+
}
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
spinner.fail('Failed to test connection');
|
|
243
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// Role Commands
|
|
249
|
+
// ============================================================================
|
|
250
|
+
async function listRoles(options) {
|
|
251
|
+
const spinner = ora('Fetching roles...').start();
|
|
252
|
+
try {
|
|
253
|
+
let url = '/v1/dynamic-secrets/roles';
|
|
254
|
+
if (options.connection) {
|
|
255
|
+
url = `/v1/dynamic-secrets/connections/${options.connection}/roles`;
|
|
256
|
+
}
|
|
257
|
+
const response = await client.get(url);
|
|
258
|
+
spinner.stop();
|
|
259
|
+
if (options.json) {
|
|
260
|
+
output.json(response);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (response.length === 0) {
|
|
264
|
+
output.info('No roles found.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const table = new Table({
|
|
268
|
+
head: ['Name', 'Connection', 'Enabled', 'Default TTL', 'Max TTL', 'Active Leases'],
|
|
269
|
+
style: { head: ['cyan'] },
|
|
270
|
+
});
|
|
271
|
+
for (const role of response) {
|
|
272
|
+
table.push([
|
|
273
|
+
role.name,
|
|
274
|
+
role.connectionName || role.connectionId.substring(0, 8),
|
|
275
|
+
role.isEnabled ? 'Yes' : 'No',
|
|
276
|
+
formatTtl(role.defaultTtlSeconds),
|
|
277
|
+
formatTtl(role.maxTtlSeconds),
|
|
278
|
+
String(role.activeLeases ?? 0),
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
console.log(table.toString());
|
|
282
|
+
output.info(`${response.length} role(s) found`);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
spinner.fail('Failed to list roles');
|
|
286
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function getRole(roleId, options) {
|
|
291
|
+
const spinner = ora('Fetching role...').start();
|
|
292
|
+
try {
|
|
293
|
+
const response = await client.get(`/v1/dynamic-secrets/roles/${roleId}`);
|
|
294
|
+
spinner.stop();
|
|
295
|
+
if (options.json) {
|
|
296
|
+
output.json(response);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
output.keyValue({
|
|
300
|
+
'ID': response.id,
|
|
301
|
+
'Name': response.name,
|
|
302
|
+
'Description': response.description || '-',
|
|
303
|
+
'Connection': response.connectionName || response.connectionId,
|
|
304
|
+
'Enabled': response.isEnabled ? 'Yes' : 'No',
|
|
305
|
+
'Username Template': response.usernameTemplate,
|
|
306
|
+
'Default TTL': formatTtl(response.defaultTtlSeconds),
|
|
307
|
+
'Max TTL': formatTtl(response.maxTtlSeconds),
|
|
308
|
+
'Active Leases': String(response.activeLeases ?? 0),
|
|
309
|
+
'Created': formatDate(response.createdAt),
|
|
310
|
+
'Updated': formatDate(response.updatedAt),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
spinner.fail('Failed to get role');
|
|
315
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function createRole(connectionId, options) {
|
|
320
|
+
// Interactive prompts if options not provided
|
|
321
|
+
const name = options.name || (await inquirer.prompt([{
|
|
322
|
+
type: 'input',
|
|
323
|
+
name: 'name',
|
|
324
|
+
message: 'Role name:',
|
|
325
|
+
validate: (input) => input.trim() ? true : 'Name is required',
|
|
326
|
+
}])).name;
|
|
327
|
+
const creationStatements = options.creationStatements?.split(';').filter(s => s.trim()) || (await inquirer.prompt([{
|
|
328
|
+
type: 'editor',
|
|
329
|
+
name: 'statements',
|
|
330
|
+
message: 'Creation SQL statements (one per line, use {{username}} and {{password}} placeholders):',
|
|
331
|
+
}])).statements.split('\n').filter((s) => s.trim());
|
|
332
|
+
const revocationStatements = options.revocationStatements?.split(';').filter(s => s.trim()) || (await inquirer.prompt([{
|
|
333
|
+
type: 'editor',
|
|
334
|
+
name: 'statements',
|
|
335
|
+
message: 'Revocation SQL statements (one per line, use {{username}} placeholder):',
|
|
336
|
+
}])).statements.split('\n').filter((s) => s.trim());
|
|
337
|
+
const spinner = ora('Creating role...').start();
|
|
338
|
+
try {
|
|
339
|
+
const body = {
|
|
340
|
+
name,
|
|
341
|
+
creationStatements,
|
|
342
|
+
revocationStatements,
|
|
343
|
+
};
|
|
344
|
+
if (options.description)
|
|
345
|
+
body.description = options.description;
|
|
346
|
+
if (options.renewStatements)
|
|
347
|
+
body.renewStatements = options.renewStatements.split(';').filter(s => s.trim());
|
|
348
|
+
if (options.defaultTtl)
|
|
349
|
+
body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
|
|
350
|
+
if (options.maxTtl)
|
|
351
|
+
body.maxTtlSeconds = parseInt(options.maxTtl, 10);
|
|
352
|
+
if (options.usernameTemplate)
|
|
353
|
+
body.usernameTemplate = options.usernameTemplate;
|
|
354
|
+
const response = await client.post(`/v1/dynamic-secrets/connections/${connectionId}/roles`, body);
|
|
355
|
+
spinner.succeed('Role created');
|
|
356
|
+
if (options.json) {
|
|
357
|
+
output.json(response);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
output.success(`Role "${response.name}" created with ID: ${response.id}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
spinner.fail('Failed to create role');
|
|
365
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function updateRole(roleId, options) {
|
|
370
|
+
const spinner = ora('Updating role...').start();
|
|
371
|
+
try {
|
|
372
|
+
const body = {};
|
|
373
|
+
if (options.description !== undefined)
|
|
374
|
+
body.description = options.description;
|
|
375
|
+
if (options.defaultTtl)
|
|
376
|
+
body.defaultTtlSeconds = parseInt(options.defaultTtl, 10);
|
|
377
|
+
if (options.maxTtl)
|
|
378
|
+
body.maxTtlSeconds = parseInt(options.maxTtl, 10);
|
|
379
|
+
if (options.enabled !== undefined)
|
|
380
|
+
body.isEnabled = options.enabled === 'true';
|
|
381
|
+
const response = await client.patch(`/v1/dynamic-secrets/roles/${roleId}`, body);
|
|
382
|
+
spinner.succeed('Role updated');
|
|
383
|
+
if (options.json) {
|
|
384
|
+
output.json(response);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
output.success(`Role "${response.name}" updated`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
spinner.fail('Failed to update role');
|
|
392
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function deleteRole(roleId, options) {
|
|
397
|
+
if (!options.force) {
|
|
398
|
+
const { confirm } = await inquirer.prompt([{
|
|
399
|
+
type: 'confirm',
|
|
400
|
+
name: 'confirm',
|
|
401
|
+
message: `Are you sure you want to delete this role? Active leases will be revoked.`,
|
|
402
|
+
default: false,
|
|
403
|
+
}]);
|
|
404
|
+
if (!confirm) {
|
|
405
|
+
output.info('Cancelled');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const spinner = ora('Deleting role...').start();
|
|
410
|
+
try {
|
|
411
|
+
await client.delete(`/v1/dynamic-secrets/roles/${roleId}`);
|
|
412
|
+
spinner.succeed('Role deleted');
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
spinner.fail('Failed to delete role');
|
|
416
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Credential Commands
|
|
422
|
+
// ============================================================================
|
|
423
|
+
async function generateCredentials(roleId, options) {
|
|
424
|
+
const spinner = ora('Generating credentials...').start();
|
|
425
|
+
try {
|
|
426
|
+
const body = {};
|
|
427
|
+
if (options.ttl)
|
|
428
|
+
body.ttlSeconds = parseInt(options.ttl, 10);
|
|
429
|
+
const response = await client.post(`/v1/dynamic-secrets/roles/${roleId}/credentials`, body);
|
|
430
|
+
spinner.succeed('Credentials generated');
|
|
431
|
+
if (options.json) {
|
|
432
|
+
output.json(response);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
console.log('');
|
|
436
|
+
output.keyValue({
|
|
437
|
+
'Lease ID': response.leaseId,
|
|
438
|
+
'Username': response.username,
|
|
439
|
+
'Password': response.password,
|
|
440
|
+
'TTL': formatDuration(response.ttlSeconds),
|
|
441
|
+
'Expires At': formatDate(response.expiresAt),
|
|
442
|
+
'Max Expires At': formatDate(response.maxExpiresAt),
|
|
443
|
+
});
|
|
444
|
+
console.log('');
|
|
445
|
+
output.warn('The password is shown only once. Store it securely or use it immediately.');
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
spinner.fail('Failed to generate credentials');
|
|
449
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// Lease Commands
|
|
455
|
+
// ============================================================================
|
|
456
|
+
async function listLeases(options) {
|
|
457
|
+
const spinner = ora('Fetching leases...').start();
|
|
458
|
+
try {
|
|
459
|
+
const params = new URLSearchParams();
|
|
460
|
+
if (options.role)
|
|
461
|
+
params.append('roleId', options.role);
|
|
462
|
+
if (options.status)
|
|
463
|
+
params.append('status', options.status.toUpperCase());
|
|
464
|
+
const url = `/v1/dynamic-secrets/leases${params.toString() ? '?' + params.toString() : ''}`;
|
|
465
|
+
const response = await client.get(url);
|
|
466
|
+
spinner.stop();
|
|
467
|
+
if (options.json) {
|
|
468
|
+
output.json(response);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (response.length === 0) {
|
|
472
|
+
output.info('No leases found.');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const table = new Table({
|
|
476
|
+
head: ['Lease ID', 'Username', 'Role', 'Status', 'TTL Remaining', 'Renewals'],
|
|
477
|
+
style: { head: ['cyan'] },
|
|
478
|
+
});
|
|
479
|
+
for (const lease of response) {
|
|
480
|
+
table.push([
|
|
481
|
+
lease.id.substring(0, 12),
|
|
482
|
+
lease.username,
|
|
483
|
+
lease.roleName || lease.roleId.substring(0, 8),
|
|
484
|
+
formatStatus(lease.status),
|
|
485
|
+
lease.status === 'ACTIVE' ? formatDuration(lease.ttlRemaining) : '-',
|
|
486
|
+
String(lease.renewalCount),
|
|
487
|
+
]);
|
|
488
|
+
}
|
|
489
|
+
console.log(table.toString());
|
|
490
|
+
output.info(`${response.length} lease(s) found`);
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
spinner.fail('Failed to list leases');
|
|
494
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function getLease(leaseId, options) {
|
|
499
|
+
const spinner = ora('Fetching lease...').start();
|
|
500
|
+
try {
|
|
501
|
+
const response = await client.get(`/v1/dynamic-secrets/leases/${leaseId}`);
|
|
502
|
+
spinner.stop();
|
|
503
|
+
if (options.json) {
|
|
504
|
+
output.json(response);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
output.keyValue({
|
|
508
|
+
'Lease ID': response.id,
|
|
509
|
+
'Username': response.username,
|
|
510
|
+
'Role': response.roleName || response.roleId,
|
|
511
|
+
'Connection': response.connectionName || response.connectionId,
|
|
512
|
+
'Status': formatStatus(response.status),
|
|
513
|
+
'TTL Remaining': response.status === 'ACTIVE' ? formatDuration(response.ttlRemaining) : '-',
|
|
514
|
+
'Renewal Count': String(response.renewalCount),
|
|
515
|
+
'Issued At': formatDate(response.issuedAt),
|
|
516
|
+
'Expires At': formatDate(response.expiresAt),
|
|
517
|
+
'Max Expires At': formatDate(response.maxExpiresAt),
|
|
518
|
+
'Last Renewed': formatDate(response.lastRenewedAt),
|
|
519
|
+
'Revoked At': formatDate(response.revokedAt),
|
|
520
|
+
'Revoked By': response.revokedBy || '-',
|
|
521
|
+
'Revoke Reason': response.revokeReason || '-',
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
spinner.fail('Failed to get lease');
|
|
526
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function renewLease(leaseId, options) {
|
|
531
|
+
const spinner = ora('Renewing lease...').start();
|
|
532
|
+
try {
|
|
533
|
+
const body = {};
|
|
534
|
+
if (options.ttl)
|
|
535
|
+
body.ttlSeconds = parseInt(options.ttl, 10);
|
|
536
|
+
const response = await client.post(`/v1/dynamic-secrets/leases/${leaseId}/renew`, body);
|
|
537
|
+
spinner.succeed('Lease renewed');
|
|
538
|
+
if (options.json) {
|
|
539
|
+
output.json(response);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
output.success(`Lease renewed. New TTL: ${formatDuration(response.ttlSeconds)}, Renewal count: ${response.renewalCount}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
spinner.fail('Failed to renew lease');
|
|
547
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function revokeLease(leaseId, options) {
|
|
552
|
+
if (!options.force) {
|
|
553
|
+
const { confirm } = await inquirer.prompt([{
|
|
554
|
+
type: 'confirm',
|
|
555
|
+
name: 'confirm',
|
|
556
|
+
message: `Are you sure you want to revoke lease "${leaseId}"? This will immediately revoke the database credentials.`,
|
|
557
|
+
default: false,
|
|
558
|
+
}]);
|
|
559
|
+
if (!confirm) {
|
|
560
|
+
output.info('Cancelled');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const spinner = ora('Revoking lease...').start();
|
|
565
|
+
try {
|
|
566
|
+
const body = {};
|
|
567
|
+
if (options.reason)
|
|
568
|
+
body.reason = options.reason;
|
|
569
|
+
await client.post(`/v1/dynamic-secrets/leases/${leaseId}/revoke`, body);
|
|
570
|
+
spinner.succeed('Lease revoked');
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
spinner.fail('Failed to revoke lease');
|
|
574
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// Command Registration
|
|
580
|
+
// ============================================================================
|
|
581
|
+
export function registerDynamicSecretsCommands(program) {
|
|
582
|
+
const dynasec = program
|
|
583
|
+
.command('dynasec')
|
|
584
|
+
.description('Dynamic secrets management (on-demand database credentials)')
|
|
585
|
+
.addHelpText('after', `
|
|
586
|
+
Examples:
|
|
587
|
+
# List all database connections
|
|
588
|
+
znvault dynasec connection list
|
|
589
|
+
|
|
590
|
+
# Create a PostgreSQL connection
|
|
591
|
+
znvault dynasec connection create --name my-pg --type postgresql \\
|
|
592
|
+
--connection-string "postgresql://admin:pass@localhost:5432/mydb"
|
|
593
|
+
|
|
594
|
+
# Create a role for the connection
|
|
595
|
+
znvault dynasec role create <connection-id> --name readonly \\
|
|
596
|
+
--creation-statements "CREATE ROLE \\"{{username}}\\" WITH LOGIN PASSWORD '{{password}}'" \\
|
|
597
|
+
--revocation-statements "DROP ROLE IF EXISTS \\"{{username}}\\""
|
|
598
|
+
|
|
599
|
+
# Generate credentials
|
|
600
|
+
znvault dynasec creds generate <role-id> --ttl 3600
|
|
601
|
+
|
|
602
|
+
# List active leases
|
|
603
|
+
znvault dynasec lease list --status active
|
|
604
|
+
|
|
605
|
+
# Revoke a lease
|
|
606
|
+
znvault dynasec lease revoke <lease-id> --reason "No longer needed"
|
|
607
|
+
`);
|
|
608
|
+
// -------------------------------------------------------------------------
|
|
609
|
+
// Connection Commands
|
|
610
|
+
// -------------------------------------------------------------------------
|
|
611
|
+
const connection = dynasec.command('connection').alias('conn').description('Manage database connections');
|
|
612
|
+
connection
|
|
613
|
+
.command('list')
|
|
614
|
+
.alias('ls')
|
|
615
|
+
.description('List all database connections')
|
|
616
|
+
.option('--json', 'Output as JSON')
|
|
617
|
+
.action(listConnections);
|
|
618
|
+
connection
|
|
619
|
+
.command('get <name-or-id>')
|
|
620
|
+
.description('Get connection details')
|
|
621
|
+
.option('--json', 'Output as JSON')
|
|
622
|
+
.action(getConnection);
|
|
623
|
+
connection
|
|
624
|
+
.command('create')
|
|
625
|
+
.description('Create a new database connection')
|
|
626
|
+
.option('--name <name>', 'Connection name')
|
|
627
|
+
.option('--type <type>', 'Database type (POSTGRESQL or MYSQL)')
|
|
628
|
+
.option('--connection-string <string>', 'Database connection string')
|
|
629
|
+
.option('--description <desc>', 'Connection description')
|
|
630
|
+
.option('--max-connections <n>', 'Maximum open connections')
|
|
631
|
+
.option('--timeout <seconds>', 'Connection timeout in seconds')
|
|
632
|
+
.option('--default-ttl <seconds>', 'Default credential TTL')
|
|
633
|
+
.option('--max-ttl <seconds>', 'Maximum credential TTL')
|
|
634
|
+
.option('--json', 'Output as JSON')
|
|
635
|
+
.action(createConnection);
|
|
636
|
+
connection
|
|
637
|
+
.command('update <name-or-id>')
|
|
638
|
+
.description('Update a database connection')
|
|
639
|
+
.option('--description <desc>', 'Connection description')
|
|
640
|
+
.option('--max-connections <n>', 'Maximum open connections')
|
|
641
|
+
.option('--timeout <seconds>', 'Connection timeout in seconds')
|
|
642
|
+
.option('--default-ttl <seconds>', 'Default credential TTL')
|
|
643
|
+
.option('--max-ttl <seconds>', 'Maximum credential TTL')
|
|
644
|
+
.option('--status <status>', 'Connection status (ACTIVE or DISABLED)')
|
|
645
|
+
.option('--json', 'Output as JSON')
|
|
646
|
+
.action(updateConnection);
|
|
647
|
+
connection
|
|
648
|
+
.command('delete <name-or-id>')
|
|
649
|
+
.alias('rm')
|
|
650
|
+
.description('Delete a database connection')
|
|
651
|
+
.option('--force', 'Skip confirmation')
|
|
652
|
+
.action(deleteConnection);
|
|
653
|
+
connection
|
|
654
|
+
.command('test <name-or-id>')
|
|
655
|
+
.description('Test a database connection')
|
|
656
|
+
.option('--json', 'Output as JSON')
|
|
657
|
+
.action(testConnection);
|
|
658
|
+
// -------------------------------------------------------------------------
|
|
659
|
+
// Role Commands
|
|
660
|
+
// -------------------------------------------------------------------------
|
|
661
|
+
const role = dynasec.command('role').description('Manage credential roles');
|
|
662
|
+
role
|
|
663
|
+
.command('list')
|
|
664
|
+
.alias('ls')
|
|
665
|
+
.description('List all roles')
|
|
666
|
+
.option('--connection <id>', 'Filter by connection ID')
|
|
667
|
+
.option('--json', 'Output as JSON')
|
|
668
|
+
.action(listRoles);
|
|
669
|
+
role
|
|
670
|
+
.command('get <role-id>')
|
|
671
|
+
.description('Get role details')
|
|
672
|
+
.option('--json', 'Output as JSON')
|
|
673
|
+
.action(getRole);
|
|
674
|
+
role
|
|
675
|
+
.command('create <connection-id>')
|
|
676
|
+
.description('Create a new role for a connection')
|
|
677
|
+
.option('--name <name>', 'Role name')
|
|
678
|
+
.option('--description <desc>', 'Role description')
|
|
679
|
+
.option('--creation-statements <sql>', 'SQL statements to create credentials (semicolon-separated)')
|
|
680
|
+
.option('--revocation-statements <sql>', 'SQL statements to revoke credentials (semicolon-separated)')
|
|
681
|
+
.option('--renew-statements <sql>', 'SQL statements to renew credentials (semicolon-separated)')
|
|
682
|
+
.option('--default-ttl <seconds>', 'Default credential TTL')
|
|
683
|
+
.option('--max-ttl <seconds>', 'Maximum credential TTL')
|
|
684
|
+
.option('--username-template <template>', 'Username template (e.g., v_{{role}}_{{random:8}})')
|
|
685
|
+
.option('--json', 'Output as JSON')
|
|
686
|
+
.action(createRole);
|
|
687
|
+
role
|
|
688
|
+
.command('update <role-id>')
|
|
689
|
+
.description('Update a role')
|
|
690
|
+
.option('--description <desc>', 'Role description')
|
|
691
|
+
.option('--default-ttl <seconds>', 'Default credential TTL')
|
|
692
|
+
.option('--max-ttl <seconds>', 'Maximum credential TTL')
|
|
693
|
+
.option('--enabled <bool>', 'Enable or disable role (true/false)')
|
|
694
|
+
.option('--json', 'Output as JSON')
|
|
695
|
+
.action(updateRole);
|
|
696
|
+
role
|
|
697
|
+
.command('delete <role-id>')
|
|
698
|
+
.alias('rm')
|
|
699
|
+
.description('Delete a role')
|
|
700
|
+
.option('--force', 'Skip confirmation')
|
|
701
|
+
.action(deleteRole);
|
|
702
|
+
// -------------------------------------------------------------------------
|
|
703
|
+
// Credentials Commands
|
|
704
|
+
// -------------------------------------------------------------------------
|
|
705
|
+
const creds = dynasec.command('creds').alias('credentials').description('Generate database credentials');
|
|
706
|
+
creds
|
|
707
|
+
.command('generate <role-id>')
|
|
708
|
+
.alias('gen')
|
|
709
|
+
.description('Generate new database credentials')
|
|
710
|
+
.option('--ttl <seconds>', 'Credential TTL in seconds')
|
|
711
|
+
.option('--json', 'Output as JSON')
|
|
712
|
+
.action(generateCredentials);
|
|
713
|
+
// -------------------------------------------------------------------------
|
|
714
|
+
// Lease Commands
|
|
715
|
+
// -------------------------------------------------------------------------
|
|
716
|
+
const lease = dynasec.command('lease').description('Manage credential leases');
|
|
717
|
+
lease
|
|
718
|
+
.command('list')
|
|
719
|
+
.alias('ls')
|
|
720
|
+
.description('List credential leases')
|
|
721
|
+
.option('--role <id>', 'Filter by role ID')
|
|
722
|
+
.option('--status <status>', 'Filter by status (ACTIVE, EXPIRED, REVOKED)')
|
|
723
|
+
.option('--json', 'Output as JSON')
|
|
724
|
+
.action(listLeases);
|
|
725
|
+
lease
|
|
726
|
+
.command('get <lease-id>')
|
|
727
|
+
.description('Get lease details')
|
|
728
|
+
.option('--json', 'Output as JSON')
|
|
729
|
+
.action(getLease);
|
|
730
|
+
lease
|
|
731
|
+
.command('renew <lease-id>')
|
|
732
|
+
.description('Renew a lease')
|
|
733
|
+
.option('--ttl <seconds>', 'New TTL in seconds')
|
|
734
|
+
.option('--json', 'Output as JSON')
|
|
735
|
+
.action(renewLease);
|
|
736
|
+
lease
|
|
737
|
+
.command('revoke <lease-id>')
|
|
738
|
+
.description('Revoke a lease (immediately revokes database credentials)')
|
|
739
|
+
.option('--reason <reason>', 'Revocation reason')
|
|
740
|
+
.option('--force', 'Skip confirmation')
|
|
741
|
+
.action(revokeLease);
|
|
742
|
+
}
|
|
743
|
+
//# sourceMappingURL=dynamic-secrets.js.map
|