diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4c81e..064af47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# v1.13.2 +- chore: Update CI pipeline integration + # Changelog All notable changes to this project are documented here. Format loosely diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js index 636aab9..360c472 100644 --- a/nodejs/models/dns_provider.js +++ b/nodejs/models/dns_provider.js @@ -3,6 +3,7 @@ const crypto = require("crypto"); const conf = require('@simpleworkjs/conf'); +const baoConf = require('@simpleworkjs/bao-conf'); const Table = require('.'); const ModelPs = require('../utils/model_pubsub'); @@ -139,11 +140,26 @@ class DnsProvider extends Table{ let __intraModel = this.__intraModel(data.dnsProvider); Provider = __intraModel.Provider; + if (!data.id) data.id = crypto.randomBytes(8).toString("hex"); + + let secrets = {}; + for (let key in Provider._keyMap) { + if (Provider._keyMap[key].isPrivate && data[key] !== undefined) { + secrets[key] = data[key]; + } + } + // This is here test if the given API key is valid let provider = new __intraModel.Provider(data, ...args); let domains = await provider.listDomains(); + for (let key in secrets) data[key] = '********'; + let instance = await super.create.call(__intraModel, data, ...args); + + if (Object.keys(secrets).length > 0) { + await baoConf.set(`proxy/dns-providers/${instance.id}`, secrets); + } try{ await instance.updateDomains(domains); }catch(updateError){ @@ -189,7 +205,63 @@ class DnsProvider extends Table{ let instance = await super.get(data, ...args); let __intraModel = this.__intraModel(instance.dnsProvider); - return await super.get.call(__intraModel, data, ...args); + let resolved = await super.get.call(__intraModel, data, ...args); + try { + let secrets = await baoConf.get(`proxy/dns-providers/${resolved.id}`); + if (secrets) Object.assign(resolved, secrets); + } catch(e) {} + return resolved; + } + + static async findall(...args){ + let instances = await super.findall(...args); + for (let inst of instances) { + try { + let secrets = await baoConf.get(`proxy/dns-providers/${inst.id}`); + if (secrets) Object.assign(inst, secrets); + } catch(e) {} + } + return instances; + } + + static async find(...args){ + let instances = await super.find(...args); + for (let inst of instances) { + try { + let secrets = await baoConf.get(`proxy/dns-providers/${inst.id}`); + if (secrets) Object.assign(inst, secrets); + } catch(e) {} + } + return instances; + } + + async update(data){ + let Provider = this.constructor.Provider || providers[this.dnsProvider]; + let secrets = {}; + if (Provider) { + for (let key in Provider._keyMap) { + if (Provider._keyMap[key].isPrivate && data[key] !== undefined && data[key] !== '********') { + secrets[key] = 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 + } + } + } + + let res = await super.update(data); + + if (Object.keys(secrets).length > 0) { + let existing = await baoConf.get(`proxy/dns-providers/${this.id}`) || {}; + await baoConf.set(`proxy/dns-providers/${this.id}`, { ...existing, ...secrets }); + Object.assign(this, secrets); + } + return res; + } + + async remove(...args){ + await baoConf.request('DELETE', `proxy/dns-providers/${this.id}`).catch(()=>{}); + return await super.remove(...args); } static listProviders(){ diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 3098ec7..af08a07 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxy-api", - "version": "1.13.0", + "version": "1.13.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxy-api", - "version": "1.13.0", + "version": "1.13.2", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", diff --git a/nodejs/package.json b/nodejs/package.json index 2e14975..5cee527 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "proxy-api", - "version": "1.13.1", + "version": "1.13.2", "author": [ { "name": "William Mantly", 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'); + }); +});