diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js index 12305ba..360c472 100644 --- a/nodejs/models/dns_provider.js +++ b/nodejs/models/dns_provider.js @@ -153,7 +153,7 @@ class DnsProvider extends Table{ let provider = new __intraModel.Provider(data, ...args); let domains = await provider.listDomains(); - for (let key in secrets) delete data[key]; + for (let key in secrets) data[key] = '********'; let instance = await super.create.call(__intraModel, data, ...args); @@ -240,9 +240,11 @@ class DnsProvider extends Table{ let secrets = {}; if (Provider) { for (let key in Provider._keyMap) { - if (Provider._keyMap[key].isPrivate && data[key] !== undefined) { + if (Provider._keyMap[key].isPrivate && data[key] !== undefined && data[key] !== '********') { secrets[key] = data[key]; - delete data[key]; + data[key] = '********'; + } else if (Provider._keyMap[key].isPrivate && data[key] === '********') { + delete data[key]; // Do not update the masked value if it's sent back } } } diff --git a/nodejs/test/unit/dns_provider_vault.test.js b/nodejs/test/unit/dns_provider_vault.test.js new file mode 100644 index 0000000..37db341 --- /dev/null +++ b/nodejs/test/unit/dns_provider_vault.test.js @@ -0,0 +1,67 @@ +const { describe, test, beforeEach, afterEach, after, mock } = require('node:test'); +const assert = require('node:assert'); +const crypto = require('crypto'); + +const baoConf = require('@simpleworkjs/bao-conf'); +const Table = require('../../models/index'); +const DnsProvider = Table.models.DnsProvider; +const DuckDns = require('../../models/dns_provider/duckdns'); + +describe('DnsProvider Vault Integration', () => { + let originalSet, originalGet, originalRequest; + + after(async () => { + if (Table._redis && Table._redis.quit) { + await Table._redis.quit(); + } + }); + + beforeEach(() => { + // Mock baoConf + originalSet = baoConf.set; + originalGet = baoConf.get; + originalRequest = baoConf.request; + + const vaultStore = {}; + baoConf.set = mock.fn(async (path, data) => { vaultStore[path] = data; return true; }); + baoConf.get = mock.fn(async (path) => vaultStore[path] || {}); + baoConf.request = mock.fn(async () => ({})); + + mock.method(DuckDns.prototype, 'listDomains', async () => []); + mock.method(DnsProvider.prototype, 'updateDomains', async () => {}); + }); + + afterEach(() => { + baoConf.set = originalSet; + baoConf.get = originalGet; + baoConf.request = originalRequest; + mock.restoreAll(); + }); + + test('create() writes isPrivate keys to OpenBao and get() retrieves them', async () => { + const payload = { + name: 'My Duck', + dnsProvider: 'DuckDns', + token: 'super-secret-vault-token', + subdomains: 'myduck', + created_by: 'admin' + }; + + const instance = await DnsProvider.create(payload); + + // 1. Should have called OpenBao set + assert.strictEqual(baoConf.set.mock.callCount(), 1); + const [path, secrets] = baoConf.set.mock.calls[0].arguments; + + assert.strictEqual(path, `proxy/dns-providers/${instance.id}`); + assert.deepStrictEqual(secrets, { token: 'super-secret-vault-token' }); + + // 2. The returned instance should have the secret injected back + assert.strictEqual(instance.token, 'super-secret-vault-token'); + + // 3. get() should fetch public data from Redis and merge secrets from OpenBao + // (baoConf.get is already mocked to return from vaultStore) + const fetched = await DnsProvider.get(instance.id); + assert.strictEqual(fetched.token, 'super-secret-vault-token'); + }); +});