Skip to content

Commit 2951d83

Browse files
committed
fix(storage): keep proxy secrets inside credential vault
Generated-by: OpenAI Codex
1 parent ea96f9f commit 2951d83

4 files changed

Lines changed: 184 additions & 241 deletions

File tree

packages/storage/src/__tests__/runtime-policy-stores.test.ts

Lines changed: 115 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2998,73 +2998,132 @@ describe('runtime policy stores', () => {
29982998
});
29992999
});
30003000

3001-
test('proxy policy and credential replacement recover from every persistence cut', {
3001+
test('proxy replacement never persists its secret outside the credential vault', {
30023002
skip:
30033003
process.platform === 'win32'
30043004
? 'POSIX file handles are required to inject persistence failures'
30053005
: false,
30063006
}, async () => {
3007-
for (let failedSync = 1; failedSync <= 7; failedSync += 1) {
3008-
await withInteractiveOwner(async ({ root, stores }) => {
3009-
const initial = await stores.runtimePolicy.getSnapshot();
3010-
const target = {
3007+
await withInteractiveOwner(async ({ root, stores }) => {
3008+
const initial = await stores.runtimePolicy.getSnapshot();
3009+
const secret = 'vault-only-proxy-secret';
3010+
const probe = await open(root, 'r');
3011+
const fileHandlePrototype = Object.getPrototypeOf(probe) as {
3012+
sync: typeof probe.sync;
3013+
};
3014+
const originalSync = fileHandlePrototype.sync;
3015+
await probe.close();
3016+
let syncCalls = 0;
3017+
const syncMock = mock.method(
3018+
fileHandlePrototype,
3019+
'sync',
3020+
async function (this: typeof probe) {
3021+
syncCalls += 1;
3022+
if (syncCalls === 3) throw new Error('injected proxy policy persistence failure');
3023+
return originalSync.call(this);
3024+
},
3025+
);
3026+
3027+
try {
3028+
await assert.rejects(
3029+
stores.operations.updateNetworkProxy({
3030+
expectedPolicyRevision: initial.revision,
3031+
expectedCredential: null,
3032+
networkProxy: {
3033+
...initial.policy.networkProxy,
3034+
enabled: true,
3035+
host: 'vault-only.proxy.internal',
3036+
port: 7897,
3037+
authEnabled: true,
3038+
username: 'vault-only-user',
3039+
},
3040+
credential: { kind: 'replace', secret },
3041+
}),
3042+
isStoreError('commit_outcome_unknown'),
3043+
);
3044+
} finally {
3045+
syncMock.mock.restore();
3046+
}
3047+
3048+
const filesContainingSecret: string[] = [];
3049+
for (const entry of await readdir(root)) {
3050+
if (!entry.endsWith('.json')) continue;
3051+
if ((await readFile(join(root, entry), 'utf8')).includes(secret)) {
3052+
filesContainingSecret.push(entry);
3053+
}
3054+
}
3055+
assert.deepEqual(filesContainingSecret, ['credential-vault.json']);
3056+
assert.equal(existsSync(join(root, 'runtime-policy-network-proxy.json')), false);
3057+
assert.equal(existsSync(join(root, 'runtime-policy.json')), false);
3058+
});
3059+
});
3060+
3061+
test('disabling proxy authentication commits policy before deleting its credential', {
3062+
skip:
3063+
process.platform === 'win32'
3064+
? 'POSIX file handles are required to inject persistence failures'
3065+
: false,
3066+
}, async () => {
3067+
await withInteractiveOwner(async ({ root, stores }) => {
3068+
const initial = await stores.runtimePolicy.getSnapshot();
3069+
const secret = 'retained-disabled-proxy-secret';
3070+
const configured = await stores.operations.updateNetworkProxy({
3071+
expectedPolicyRevision: initial.revision,
3072+
expectedCredential: null,
3073+
networkProxy: {
30113074
...initial.policy.networkProxy,
30123075
enabled: true,
3013-
host: 'recoverable.proxy.internal',
3076+
host: 'disable-order.proxy.internal',
30143077
port: 7897,
30153078
authEnabled: true,
3016-
username: 'recoverable-user',
3017-
};
3018-
const probe = await open(root, 'r');
3019-
const fileHandlePrototype = Object.getPrototypeOf(probe) as {
3020-
sync: typeof probe.sync;
3021-
};
3022-
const originalSync = fileHandlePrototype.sync;
3023-
await probe.close();
3024-
let syncCalls = 0;
3025-
const syncMock = mock.method(
3026-
fileHandlePrototype,
3027-
'sync',
3028-
async function (this: typeof probe) {
3029-
syncCalls += 1;
3030-
if (syncCalls === failedSync) {
3031-
throw new Error(`injected proxy update persistence cut ${failedSync}`);
3032-
}
3033-
return originalSync.call(this);
3034-
},
3035-
);
3079+
username: 'disable-order-user',
3080+
},
3081+
credential: { kind: 'replace', secret },
3082+
});
3083+
assert.equal(configured.kind, 'committed');
3084+
if (configured.kind !== 'committed') return;
30363085

3037-
try {
3038-
await assert.rejects(
3039-
stores.operations.updateNetworkProxy({
3040-
expectedPolicyRevision: initial.revision,
3041-
expectedCredential: null,
3042-
networkProxy: target,
3043-
credential: { kind: 'replace', secret: 'recoverable-secret' },
3044-
}),
3045-
isStoreError(failedSync === 1 ? 'io_failed' : 'commit_outcome_unknown'),
3046-
);
3047-
} finally {
3048-
syncMock.mock.restore();
3049-
}
3050-
assert.equal(syncCalls, failedSync);
3051-
3052-
const policy = await stores.runtimePolicy.getSnapshot();
3053-
const credential = await getCredentialStatus(stores.credentialVault, proxyCredential());
3054-
if (failedSync === 1) {
3055-
assert.deepEqual(policy, initial);
3056-
assert.equal(credential.configured, false);
3057-
assert.equal(existsSync(join(root, 'runtime-policy-network-proxy.json')), false);
3058-
return;
3059-
}
3086+
const probe = await open(root, 'r');
3087+
const fileHandlePrototype = Object.getPrototypeOf(probe) as {
3088+
sync: typeof probe.sync;
3089+
};
3090+
const originalSync = fileHandlePrototype.sync;
3091+
await probe.close();
3092+
let syncCalls = 0;
3093+
const syncMock = mock.method(
3094+
fileHandlePrototype,
3095+
'sync',
3096+
async function (this: typeof probe) {
3097+
syncCalls += 1;
3098+
if (syncCalls === 3) throw new Error('injected proxy credential deletion failure');
3099+
return originalSync.call(this);
3100+
},
3101+
);
30603102

3061-
assert.deepEqual(policy.policy.networkProxy, target);
3062-
assert.equal(credential.configured, true);
3063-
assert.equal(existsSync(join(root, 'runtime-policy-network-proxy.json')), false);
3064-
const material = await stores.operations.exportCredentialMaterial(proxyCredential());
3065-
assert.equal(material?.secret, 'recoverable-secret');
3066-
});
3067-
}
3103+
try {
3104+
await assert.rejects(
3105+
stores.operations.updateNetworkProxy({
3106+
expectedPolicyRevision: configured.snapshot.revision,
3107+
expectedCredential: credentialBasis(configured.credentialStatus),
3108+
networkProxy: {
3109+
...configured.snapshot.policy.networkProxy,
3110+
authEnabled: false,
3111+
username: '',
3112+
},
3113+
credential: { kind: 'delete' },
3114+
}),
3115+
isStoreError('commit_outcome_unknown'),
3116+
);
3117+
} finally {
3118+
syncMock.mock.restore();
3119+
}
3120+
3121+
const persistedPolicy = JSON.parse(
3122+
await readFile(join(root, 'runtime-policy.json'), 'utf8'),
3123+
) as { readonly policy: { readonly networkProxy: RuntimePolicy['networkProxy'] } };
3124+
assert.equal(persistedPolicy.policy.networkProxy.authEnabled, false);
3125+
assert.ok((await readFile(join(root, 'credential-vault.json'), 'utf8')).includes(secret));
3126+
});
30683127
});
30693128

30703129
test('blocks WebFetch while privacy mode is active', async () => {

0 commit comments

Comments
 (0)