refactor(wizard): rename --admin-token → --setup-token
Some checks failed
Some checks failed
Any token with policy-write + auth/token admin works; root is a convenient default but a scoped service account is fine too. The previous naming misrepresented the permission floor as root-only. - flag: --admin-token → --setup-token - wizard field: adminToken → setupToken - prompt label: "OpenBao admin / root token" → "OpenBao setup token (needs policy write + auth/token admin perms; root is fine)" - file doc + one comment reworded - tests updated for the new label - regression test (token-absent-from-stdout) kept unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
* 10. (Optional) promotes the new backend to default.
|
||||
* 11. Prints the migration command for the user to run.
|
||||
*
|
||||
* Admin token is used only for steps 2–6 and is never persisted.
|
||||
* Setup token (only needs policy-write + auth/token admin on OpenBao; root is
|
||||
* a convenient default but a scoped service account works too) is used only
|
||||
* for steps 2–6 and is never persisted.
|
||||
*
|
||||
* All prompts go through `ConfigSetupPrompt` (from `config-setup.ts`) so the
|
||||
* wizard is testable without real stdin.
|
||||
@@ -46,7 +48,7 @@ export interface WizardInput {
|
||||
name: string;
|
||||
/** Pre-filled via flags for CI; falls back to prompt. */
|
||||
url?: string | undefined;
|
||||
adminToken?: string | undefined;
|
||||
setupToken?: string | undefined;
|
||||
mount?: string | undefined;
|
||||
pathPrefix?: string | undefined;
|
||||
policyName?: string | undefined;
|
||||
@@ -64,15 +66,15 @@ export async function runSecretBackendOpenbaoWizard(
|
||||
const log = deps.log;
|
||||
|
||||
const url = input.url ?? await prompt.input('OpenBao URL', 'https://bao.ad.itaz.eu');
|
||||
const adminToken = input.adminToken ?? await prompt.password('OpenBao admin / root token');
|
||||
if (adminToken === '') throw new Error('admin token is required');
|
||||
const setupToken = input.setupToken ?? await prompt.password('OpenBao setup token (needs policy write + auth/token admin perms; root is fine)');
|
||||
if (setupToken === '') throw new Error('setup token is required');
|
||||
|
||||
const vaultDeps: VaultDeps = {};
|
||||
if (deps.fetch !== undefined) vaultDeps.fetch = deps.fetch;
|
||||
|
||||
// 1. Health check.
|
||||
log(' → checking OpenBao health …');
|
||||
const health = await verifyHealth(url, adminToken, vaultDeps);
|
||||
const health = await verifyHealth(url, setupToken, vaultDeps);
|
||||
if (!health.initialized || health.sealed) {
|
||||
throw new Error(`OpenBao is not ready (initialized=${String(health.initialized)}, sealed=${String(health.sealed)})`);
|
||||
}
|
||||
@@ -85,18 +87,18 @@ export async function runSecretBackendOpenbaoWizard(
|
||||
|
||||
// 2. Enable KV v2 if needed.
|
||||
log(` → ensuring KV v2 at ${mount}/ …`);
|
||||
const created = await ensureKvV2(url, adminToken, mount, vaultDeps);
|
||||
const created = await ensureKvV2(url, setupToken, mount, vaultDeps);
|
||||
log(` ${created ? 'mounted' : 'already mounted'}`);
|
||||
|
||||
// 3. Write policy.
|
||||
log(` → writing policy '${policyName}' …`);
|
||||
const hcl = buildAppMcpdPolicyHcl({ mount, pathPrefix, tokenRole });
|
||||
await writePolicy(url, adminToken, policyName, hcl, vaultDeps);
|
||||
await writePolicy(url, setupToken, policyName, hcl, vaultDeps);
|
||||
log(` written (scope: ${mount}/{data,metadata}/${pathPrefix}/* + self-rotation paths)`);
|
||||
|
||||
// 4. Ensure token role.
|
||||
log(` → ensuring token role '${tokenRole}' (period=720h, renewable) …`);
|
||||
await ensureTokenRole(url, adminToken, tokenRole, {
|
||||
await ensureTokenRole(url, setupToken, tokenRole, {
|
||||
allowedPolicies: [policyName],
|
||||
period: 720 * 3600,
|
||||
renewable: true,
|
||||
@@ -104,9 +106,9 @@ export async function runSecretBackendOpenbaoWizard(
|
||||
}, vaultDeps);
|
||||
log(' ok');
|
||||
|
||||
// 5. Mint the first periodic token using the admin token.
|
||||
// 5. Mint the first periodic token using the setup token.
|
||||
log(' → minting first periodic token …');
|
||||
const minted = await mintRoleToken(url, adminToken, tokenRole, vaultDeps);
|
||||
const minted = await mintRoleToken(url, setupToken, tokenRole, vaultDeps);
|
||||
if (!minted.renewable) {
|
||||
throw new Error(`minted token is not renewable — the role '${tokenRole}' config is wrong`);
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ export function createCreateCommand(deps: CreateCommandDeps): Command {
|
||||
.option('--sa-token-path <path>', "openbao kubernetes auth: filesystem path to projected SA token (default: '/var/run/secrets/kubernetes.io/serviceaccount/token')")
|
||||
.option('--config <entry>', 'Extra config as key=value (repeat for multiple)', collect, [])
|
||||
.option('--wizard', 'Interactive wizard (openbao only): provision policy + token role, mint token, store on mcpd, suggest migration')
|
||||
.option('--admin-token <token>', "openbao wizard: OpenBao admin/root token (prompted if omitted). Used only for provisioning; NEVER persisted.")
|
||||
.option('--setup-token <token>', "openbao wizard: OpenBao token with provisioning perms (policy write + auth/token admin). Root works; a scoped SA token works too. Prompted if omitted. Used only for provisioning; NEVER persisted.")
|
||||
.option('--policy-name <name>', "openbao wizard: name for the policy created on OpenBao (default: 'app-mcpd')")
|
||||
.option('--token-role <name>', "openbao wizard: name for the token role created on OpenBao (default: 'app-mcpd-role')")
|
||||
.option('--no-promote-default', 'openbao wizard: do not promote this backend to default after creation')
|
||||
@@ -341,7 +341,7 @@ export function createCreateCommand(deps: CreateCommandDeps): Command {
|
||||
const { runSecretBackendOpenbaoWizard } = await import('./create-secretbackend-wizard.js');
|
||||
const wizardInput: Parameters<typeof runSecretBackendOpenbaoWizard>[0] = { name };
|
||||
if (opts.url !== undefined) wizardInput.url = opts.url as string;
|
||||
if (opts.adminToken !== undefined) wizardInput.adminToken = opts.adminToken as string;
|
||||
if (opts.setupToken !== undefined) wizardInput.setupToken = opts.setupToken as string;
|
||||
if (opts.mount !== undefined) wizardInput.mount = opts.mount as string;
|
||||
if (opts.pathPrefix !== undefined) wizardInput.pathPrefix = opts.pathPrefix as string;
|
||||
if (opts.policyName !== undefined) wizardInput.policyName = opts.policyName as string;
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('runSecretBackendOpenbaoWizard', () => {
|
||||
'Token role name': 'app-mcpd-role',
|
||||
},
|
||||
password: {
|
||||
'OpenBao admin / root token': 'root.admin.token',
|
||||
'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': 'root.admin.token',
|
||||
},
|
||||
confirm: {
|
||||
"Promote 'bao' to default backend?": true,
|
||||
@@ -99,7 +99,7 @@ describe('runSecretBackendOpenbaoWizard', () => {
|
||||
const firstCallInit = fetchFn.mock.calls[0]![1] as RequestInit;
|
||||
expect((firstCallInit.headers as Record<string, string>)['X-Vault-Token']).toBe('root.admin.token');
|
||||
|
||||
// Secret was created with the minted token value (hvs.AAA), not the admin token
|
||||
// Secret was created with the minted token value (hvs.AAA), not the setup token
|
||||
expect(created.secret).toMatchObject({ name: 'bao-creds', data: { token: 'hvs.AAA' } });
|
||||
|
||||
// SecretBackend created with rotation config
|
||||
@@ -119,19 +119,19 @@ describe('runSecretBackendOpenbaoWizard', () => {
|
||||
expect(fullLog).toContain("You have 2 secret(s) on 'default'");
|
||||
expect(fullLog).toContain('mcpctl --direct migrate secrets --from default --to bao');
|
||||
|
||||
// Admin token never appears in the log (critical)
|
||||
// Setup token never appears in the log (critical)
|
||||
expect(fullLog).not.toContain('root.admin.token');
|
||||
});
|
||||
|
||||
it('rejects when admin token is empty', async () => {
|
||||
it('rejects when setup token is empty', async () => {
|
||||
const prompt = scriptedPrompt({
|
||||
input: { 'OpenBao URL': 'http://x' },
|
||||
password: { 'OpenBao admin / root token': '' },
|
||||
password: { 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': '' },
|
||||
});
|
||||
await expect(runSecretBackendOpenbaoWizard(
|
||||
{ name: 'bao' },
|
||||
{ client: mockClient({}), log: () => {}, prompt, fetch: vi.fn() as unknown as typeof fetch },
|
||||
)).rejects.toThrow(/admin token is required/);
|
||||
)).rejects.toThrow(/setup token is required/);
|
||||
});
|
||||
|
||||
it('rejects when vault is sealed', async () => {
|
||||
@@ -140,7 +140,7 @@ describe('runSecretBackendOpenbaoWizard', () => {
|
||||
]);
|
||||
const prompt = scriptedPrompt({
|
||||
input: { 'OpenBao URL': 'http://x' },
|
||||
password: { 'OpenBao admin / root token': 't' },
|
||||
password: { 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': 't' },
|
||||
});
|
||||
await expect(runSecretBackendOpenbaoWizard(
|
||||
{ name: 'bao' },
|
||||
|
||||
Reference in New Issue
Block a user