Observer integration step
This commit is contained in:
@@ -192,30 +192,36 @@ test('official wallet not-yet-created address-index error is recoverable but unr
|
||||
await assert.rejects(createMoneroDepositAddressRpc(config, unrelated).getSubaddress(0, 1), /method failed \(-15\)/);
|
||||
});
|
||||
|
||||
test('mTLS gateway allowlists only bounded observation and one-subaddress allocation methods', () => {
|
||||
test('mTLS gateway enforces bounded per-certificate observer and address-allocator methods', () => {
|
||||
const request = (method: string, params: Record<string, unknown> = {}) =>
|
||||
({ jsonrpc: '2.0', id: 'fixture-id', method, params });
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_height')));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0] })));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('validate_address', { address: 'x'.repeat(95) })));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 })));
|
||||
const observer = ['observer'] as const, allocator = ['address-allocator'] as const;
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_height'), observer));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0] }), observer));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 }), allocator));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('validate_address', { address: 'x'.repeat(95) }), allocator));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
|
||||
in: true, filter_by_height: true, min_height: 0, max_height: 100,
|
||||
})));
|
||||
}), observer));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('incoming_transfers', {
|
||||
transfer_type: 'all', account_index: 0, subaddr_indices: [2, 3],
|
||||
})));
|
||||
}), observer));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('incoming_transfers', {
|
||||
transfer_type: 'all', account_index: 0, subaddr_indices: [2], key_image: true,
|
||||
})));
|
||||
}), observer));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 }), observer), /not allowed for this client certificate/);
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
|
||||
in: true, filter_by_height: true, min_height: 0, max_height: 100,
|
||||
}), allocator), /not allowed for this client certificate/);
|
||||
for (const method of ['transfer', 'transfer_split', 'sweep_all', 'relay_tx', 'get_tx_key', 'query_key', 'open_wallet', 'get_balance']) {
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request(method)), /not allow-listed/);
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request(method), ['observer', 'address-allocator']), /not allowed for this client certificate/);
|
||||
}
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0], all: true })));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'x', count: 2 })));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0], all: true }), observer));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'x', count: 2 }), allocator));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
|
||||
in: true, filter_by_height: true, min_height: 0, max_height: 100_001,
|
||||
})), /range/);
|
||||
}), observer), /range/);
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_height'), []));
|
||||
});
|
||||
|
||||
test('production mTLS transport config is mandatory and requires a bare HTTPS RPC endpoint', async () => {
|
||||
@@ -223,10 +229,10 @@ test('production mTLS transport config is mandatory and requires a bare HTTPS RP
|
||||
await assert.rejects(loadMoneroRpcGatewayConfig(''), /protected Monero RPC gateway configuration/);
|
||||
await assert.rejects(createMoneroRpcGateway({ listenHost: '203.0.113.10', listenPort: 18085,
|
||||
walletRpcUrl: 'http://127.0.0.1:18083/json_rpc', server: { certificateFile: 'missing', privateKeyFile: 'missing',
|
||||
clientCaFile: 'missing' }, clientFingerprints: ['a'.repeat(64)] }), /private interface IP/);
|
||||
clientCaFile: 'missing' }, clientPolicies: [{ fingerprint: 'a'.repeat(64), roles: ['observer'] }] }), /private interface IP/);
|
||||
await assert.rejects(createMoneroRpcGateway({ listenHost: '127.0.0.1', listenPort: 18085,
|
||||
walletRpcUrl: 'http://wallet.example/json_rpc', server: { certificateFile: 'missing', privateKeyFile: 'missing',
|
||||
clientCaFile: 'missing' }, clientFingerprints: ['a'.repeat(64)] }), /loopback-only/);
|
||||
clientCaFile: 'missing' }, clientPolicies: [{ fingerprint: 'a'.repeat(64), roles: ['observer'] }] }), /loopback-only/);
|
||||
});
|
||||
|
||||
test('observer refuses non-loopback plaintext, URL credentials and malformed height ranges', async () => {
|
||||
|
||||
@@ -14,11 +14,20 @@ test('private pilot Compose stack is loopback-bound, isolated, and mock-only', a
|
||||
assert.match(compose, /financial: \{internal: true\}/);
|
||||
assert.match(compose, /pg-runtime:\/secrets\/runtime:ro/);
|
||||
assert.doesNotMatch(compose, /POSTGRES_PASSWORD:\s|5432:5432/i);
|
||||
assert.doesNotMatch(compose, /^\s+(?:monero|wallet-rpc|observer):/im);
|
||||
assert.doesNotMatch(compose, /^\s+(?:monero|wallet-rpc):/im);
|
||||
assert.match(compose, /daemon-check:\n\s+image: wikideck:private-pilot/);
|
||||
assert.match(compose, /profiles: \["monero-check"\]/);
|
||||
assert.match(compose, /scripts\/monero-daemon-check\.ts/);
|
||||
assert.match(compose, /target: \/run\/monero\/daemon\.json\n\s+read_only: true/);
|
||||
assert.match(compose, /pilot-observer:\n\s+image: wikideck:private-pilot\n\s+profiles: \["pilot-observer"\]/);
|
||||
assert.match(compose, /scripts\/pilot-monero-observe-once\.ts/);
|
||||
const observer = compose.split(' pilot-observer:')[1].split('\nnetworks:')[0];
|
||||
assert.doesNotMatch(observer, /ports:|depends_on:|pg-runtime:|pg-admin:|pg-owner:/);
|
||||
assert.match(observer, /pilot-observation-only-no-credit/);
|
||||
assert.match(observer, /target: \/run\/secrets\/pilot-observer-db-url\n\s+read_only: true/);
|
||||
assert.match(observer, /target: \/run\/secrets\/observer-client\.key\n\s+read_only: true/);
|
||||
assert.match(observer, /networks: \[observer-egress\]/);
|
||||
assert.match(observer, /restart: "no"/);
|
||||
const database = compose.split(' database:')[1].split(' migrate:')[0];
|
||||
assert.doesNotMatch(database, /ports:/);
|
||||
const app = compose.split(' wikideck:')[1].split('networks:\n financial:')[0];
|
||||
|
||||
Reference in New Issue
Block a user