From 4b9a7613faf028a257b9b190de5644b18c7a8a7c Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Sun, 11 Jun 2023 21:12:09 -0500 Subject: [PATCH] fix: validation rules --- config.example.yml | 12 +++---- src/config.js | 78 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/config.example.yml b/config.example.yml index d113182..4c0e0c5 100644 --- a/config.example.yml +++ b/config.example.yml @@ -7,8 +7,8 @@ persist: ['data'] network: # valid values for interface: ['public', 'local'] - interface: '' - port: 5432 + interface: 'public' + port: 1 domain: 'db.foo.org' ssl: false pg_admin: @@ -20,8 +20,8 @@ allowed_ssh_public_keys: ['ssh-ed25519 my special ssh key'] persist: [] network: - interface: '' - port: 5432 + interface: 'public' + port: 2 domain: 'api.foo.org' ssl: true - ui: @@ -30,7 +30,7 @@ allowed_ssh_public_keys: ['ssh-ed25519 my special ssh key'] persist: [] network: - interface: '' - port: 5432 + interface: 'public' + port: 3 domain: 'foo.org' ssl: true diff --git a/src/config.js b/src/config.js index 7535b33..b8fd9f2 100644 --- a/src/config.js +++ b/src/config.js @@ -8,8 +8,10 @@ const parseSegmentLinuxUser = (o) => "linux_user" in o ? "username" in o.linux_user ? Either.right({ - username: o.linux_user.username, - allowedSshPublicKeys: o.allowed_ssh_public_keys || [], + username: o.linux_user.username.trim(), + allowedSshPublicKeys: (o.allowed_ssh_public_keys || []).map( + (pubkey) => pubkey.trim() + ), }) : Either.left(new Error("linux_user.username required")) : Either.left(new Error("linux_user required")); @@ -45,7 +47,10 @@ const parseSegmentPgAdmin = flow( ) ) ), - Either.map((pg) => ({ username: pg.username, password: pg.password })) + Either.map((pg) => ({ + username: pg.username.trim(), + password: pg.password.trim(), + })) ); const parseSegmentNetwork = flow( @@ -68,39 +73,56 @@ const parseSegmentNetwork = flow( ) ), Either.flatMap((n) => - "port" in n - ? Either.right(n) - : Either.left( - new Error( - ["network.port required", `in: ${JSON.stringify(n)}`].join("\n") - ) - ) - ), - Either.flatMap((n) => - "domain" in n + "port" in n && typeof n.port === "number" && Number.isInteger(n.port) ? Either.right(n) : Either.left( new Error( [ - "network.domain required", - " (to not listen on a public domain, set this property to empty string '')", + "network.port required and must be an integer", `in: ${JSON.stringify(n)}`, ].join("\n") ) ) ), Either.flatMap((n) => - "ssl" in n + n.interface === 'public' || (n.interface === "local" && !"domain" in n) ? Either.right(n) : Either.left( new Error( - ["network.ssl required", `in: ${JSON.stringify(n)}`].join("\n") + [ + "network.interface is 'local', network.domain must not be present.", + `in: ${JSON.stringify(n)}`, + ].join("\n") + ) + ) + ), + Either.flatMap((n) => + n.interface === 'public' && "domain" in n && typeof n.domain === "string" && n.domain.length > 0 + ? Either.right(n) + : Either.left( + new Error( + [ + "network.interface is 'public', network.domain be set to a value.", + `in: ${JSON.stringify(n)}`, + ].join("\n") + ) + ) + ), + Either.flatMap((n) => + "ssl" in n && typeof n.ssl === "boolean" + ? Either.right(n) + : Either.left( + new Error( + [ + "network.ssl required and must be a bool", + `in: ${JSON.stringify(n)}`, + ].join("\n") ) ) ), Either.map((n) => ({ ssl: n.ssl, - domain: n.domain || undefined, + domain: (n.domain || "").trim(), interface: n.interface, port: n.port, })) @@ -153,13 +175,31 @@ const parseService = (svc) => ) ); +const ensureUniq = ({getWith, onConflict, isConflict}) => array => pipe(array, + Array_.map(getWith), + Array_.reduce(Either.right([]), (res, cur) => + Either.flatMap(seen => + isConflict(seen, cur) + ? Either.left(onConflict(cur)) + : Either.right([...seen, cur]) + )(res) + ), + Either.map(() => array) + ); +const ensureServicesDoNotOverlap = flow( + ensureUniq({getWith: s => s.linuxUser.username, onConflict: usr => new Error(`linux_user.username must be unique: ${usr}`), isConflict: (seen, cur) => seen.includes(cur)}), + Either.flatMap(ensureUniq({getWith: s => s.network.port, onConflict: port => new Error(`network.port must be unique: ${port}`), isConflict: (seen, cur) => seen.includes(cur)})), + Either.flatMap(ensureUniq({getWith: s => s.network.domain, onConflict: domain => new Error(`network.domain must be unique: ${domain}`), isConflict: (seen, cur) => seen.includes(cur)})) +); + const parse = flow( (cfg) => Either.tryCatch(() => yaml.parse(cfg), id), Either.filterOrElse( (p) => p instanceof Array, () => new Error("config must have top-level array elements") ), - Either.flatMap((svcs) => Either.sequenceArray(svcs.map(parseService))) + Either.flatMap((svcs) => Either.sequenceArray(svcs.map(parseService))), + Either.tap(ensureServicesDoNotOverlap) ); module.exports = { parse };