'use strict'; var PromiseA; try { PromiseA = require('bluebird'); } catch(e) { PromiseA = global.Promise; } var path = require('path'); var sfs = require('safe-replace'); var DB = module.exports = {}; DB._savefile = path.join(__dirname, 'permissions.json'); DB._load = function () { try { DB._perms = require(DB._savefile); } catch(e) { try { DB._perms = require(DB._savefile + '.bak'); } catch(e) { DB._perms = []; } } DB._byDomain = {}; DB._byPort = {}; DB._byEmail = {}; DB._byPpid = {}; DB._byId = {}; DB._grants = {}; DB._grantsMap = {}; DB._authz = {}; DB._perms.forEach(function (acc) { if ('authz' === acc.type) { DB._authz[acc.id] = acc; return; } if (acc.id) { // if account has an id DB._byId[acc.id] = acc; if (!DB._grants[acc.id]) { DB._grantsMap[acc.id] = {}; DB._grants[acc.id] = []; } acc.domains.forEach(function (d) { DB._grants[d.name + '|id|' + acc.id] = true; if (!DB._grantsMap[acc.id][d.name]) { DB._grantsMap[acc.id][d.name] = d; DB._grants[acc.id].push(d); } }); acc.ports.forEach(function (p) { DB._grants[p.number + '|id|' + acc.id] = true; if (!DB._grantsMap[acc.id][p.number]) { DB._grantsMap[acc.id][p.number] = p; DB._grants[acc.id].push(p); } }); } else if (acc.nodes[0] && 'email' === acc.nodes[0].type) { // if primary (first) node is email //console.log("XXXX email", acc.nodes[0].name); if (!DB._byEmail[acc.nodes[0].name]) { DB._byEmail[acc.nodes[0].name] = { account: acc , node: acc.nodes[0] }; } } // map domains to all nodes that have permission // (which permission could be granted by more than one account) acc.nodes.forEach(function (node) { if ('mailto' === node.scheme || 'email' === node.type) { if (!DB._grants[node.name]) { DB._grantsMap[node.name] = {}; DB._grants[node.name] = []; } acc.domains.forEach(function (d) { DB._grants[d.name + '|' + (node.scheme||node.type) + '|' + node.name] = true; if (!DB._grantsMap[node.name][d.name]) { DB._grantsMap[node.name][d.name] = d; DB._grants[node.name].push(d); } }); acc.ports.forEach(function (p) { DB._grants[p.number + '|' + (node.scheme||node.type) + '|' + node.name] = true; if (!DB._grantsMap[node.name][p.number]) { DB._grantsMap[node.name][p.number] = p; DB._grants[node.name].push(p); } }); } }); // TODO this also should be maps/arrays (... or just normal database) acc.domains.forEach(function (domain) { if (DB._byDomain[domain.name]) { console.warn("duplicate domain '" + domain.name + "'"); console.warn("::existing account '" + acc.nodes.map(function (node) { return node.name; }) + "'"); console.warn("::new account '" + DB._byDomain[domain.name].account.nodes.map(function (node) { return node.name; }) + "'"); } DB._byDomain[domain.name] = { account: acc , domain: domain }; }); acc.ports.forEach(function (port) { if (DB._byPort[port.number]) { console.warn("duplicate port '" + port.number + "'"); console.warn("::existing account '" + acc.nodes.map(function (node) { return node.name; }) + "'"); console.warn("::new account '" + DB._byPort[port.number].account.nodes.map(function (node) { return node.name; }) + "'"); } DB._byPort[port.number] = { account: acc , port: port }; }); }); }; DB._load(); DB.accounts = {}; DB.accounts.get = function (obj) { return PromiseA.resolve().then(function () { //console.log('XXXX obj.name', DB._byEmail[obj.name]); return DB._byId[obj.name] || (DB._byEmail[obj.name] || {}).account || null; }); }; DB.accounts.add = function (obj) { return PromiseA.resolve().then(function () { if (obj.id) { // TODO more checks DB._perms.push(obj); } else if ('email' === obj.nodes[0].type || obj.email) { obj.email = undefined; DB._perms.push(obj); } }); }; DB.authorizations = {}; DB.authorizations.create = function (acc, claim) { if (!acc.id || !claim.type || !claim.value) { throw new Error("requires account id"); } var crypto = require('crypto'); var authz = DB._authz[acc.id]; if (!authz) { authz = { id: acc.id , type: 'authz' , claims: [] }; DB._authz[acc.id] = authz; DB._perms.push(authz); } // TODO check for unique type:value pairing in claims claim.challenge = crypto.randomBytes(16).toString('hex'); claim.createdAt = Date.now(); claim.verifiedAt = 0; authz.claims.push(claim); DB.save(); return JSON.parse(JSON.stringify(claim)); }; DB.authorizations.check = function (acc, claim) { var authz = DB._authz[acc.id]; var vclaim = null; if (!authz) { return vclaim; } authz.claims.some(function (c) { console.log('authz.check', c); if (claim.challenge) { if (c.challenge === claim.challenge) { vclaim = JSON.parse(JSON.stringify(c)); return true; } } else if (claim.value === c.value) { vclaim = JSON.parse(JSON.stringify(c)); return true; } }); return vclaim; }; DB.authorizations.checkAll = function (acc) { var authz = DB._authz[acc.id]; if (!authz) { return []; } return authz.claims.map(function (claim) { return JSON.parse(JSON.stringify(claim)); }); }; DB.authorizations.verify = function (acc, claim) { var scmp = require('scmp'); var authz = DB._authz[acc.id]; var vclaim; if (!authz) { return false; } authz.claims.some(function (c) { if (scmp(c.challenge, claim.challenge)) { vclaim = c; c.verifiedAt = Date.now(); return true; } }); if (vclaim) { DB.save(); return true; } return false; }; DB.domains = {}; DB.domains.available = function (name) { return PromiseA.resolve().then(function () { return !DB._byDomain[name]; }); }; DB.domains._add = function (acc, opts) { // TODO verifications to change ownership of a domain return PromiseA.resolve().then(function () { var err; //var acc = DB._byId[aid]; var domain = { name: (opts.domain || opts.name) , hostname: opts.hostname , os: opts.os , createdAt: new Date().toISOString() , wildcard: opts.wildcard }; var pdomain; var parts = (opts.domain || domain.name).split('.').map(function (el, i, arr) { return arr.slice(i).join('.'); }).reverse(); parts.shift(); parts.pop(); if (parts.some(function (part) { if (DB._byDomain[part] && DB._byDomain[part].wildcard) { pdomain = part; return true; } })) { err = new Error("'" + domain.name + "' exists as '" + pdomain + "' and therefore requires an admin to review and approve"); err.code = "E_REQ_ADMIN"; throw err; } if (DB._byDomain[domain.name]) { if (acc !== DB._byDomain[domain.name].account) { throw new Error("domain '" + domain.name + "' exists"); } // happily ignore non-change return; } DB._byDomain[domain.name] = { account: acc , domain: domain }; acc.domains.push(domain); DB.save(); }); }; DB.ports = {}; DB.ports.available = function (number) { return PromiseA.resolve().then(function () { return !DB._byPort[number]; }); }; DB.ports._add = function (acc, opts) { return PromiseA.resolve().then(function () { //var acc = DB._byId[aid]; var port = { number: opts.port || opts.number , hostname: opts.hostname , os: opts.os , createdAt: new Date().toISOString() }; if (DB._byPort[port.number]) { // TODO verifications throw new Error("port '" + port.number + "' exists"); } DB._byPort[port.number] = { account: acc , port: port }; acc.ports.push(port); }); }; DB._save = function () { return sfs.writeFileAsync(DB._savefile, JSON.stringify(DB._perms)); }; DB._saveToken = null; DB._savePromises = []; DB._savePromise = PromiseA.resolve(); DB.save = function () { clearTimeout(DB._saveToken); return new PromiseA(function (resolve, reject) { function doSave() { DB._savePromise = DB._savePromise.then(function () { return DB._save().then(function (yep) { DB._savePromises.forEach(function (p) { p.resolve(yep); }); DB._savePromises.length = 1; }, function (err) { DB._savePromises.forEach(function (p) { p.reject(err); }); DB._savePromises.length = 1; }); }); return DB._savePromise; } DB._saveToken = setTimeout(doSave, 2500); DB._savePromises.push({ resolve: resolve, reject: reject }); }); };