access-list.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. const _ = require('lodash');
  2. const fs = require('fs');
  3. const batchflow = require('batchflow');
  4. const logger = require('../logger').access;
  5. const error = require('../lib/error');
  6. const accessListModel = require('../models/access_list');
  7. const accessListAuthModel = require('../models/access_list_auth');
  8. const proxyHostModel = require('../models/proxy_host');
  9. const internalAuditLog = require('./audit-log');
  10. const internalNginx = require('./nginx');
  11. const utils = require('../lib/utils');
  12. function omissions () {
  13. return ['is_deleted'];
  14. }
  15. const internalAccessList = {
  16. /**
  17. * @param {Access} access
  18. * @param {Object} data
  19. * @returns {Promise}
  20. */
  21. create: (access, data) => {
  22. return access.can('access_lists:create', data)
  23. .then((/*access_data*/) => {
  24. return accessListModel
  25. .query()
  26. .omit(omissions())
  27. .insertAndFetch({
  28. name: data.name,
  29. owner_user_id: access.token.getUserId(1)
  30. });
  31. })
  32. .then((row) => {
  33. data.id = row.id;
  34. // Now add the items
  35. let promises = [];
  36. data.items.map((item) => {
  37. promises.push(accessListAuthModel
  38. .query()
  39. .insert({
  40. access_list_id: row.id,
  41. username: item.username,
  42. password: item.password
  43. })
  44. );
  45. });
  46. return Promise.all(promises);
  47. })
  48. .then(() => {
  49. // re-fetch with expansions
  50. return internalAccessList.get(access, {
  51. id: data.id,
  52. expand: ['owner', 'items']
  53. }, true /* <- skip masking */);
  54. })
  55. .then((row) => {
  56. // Audit log
  57. data.meta = _.assign({}, data.meta || {}, row.meta);
  58. return internalAccessList.build(row)
  59. .then(() => {
  60. if (row.proxy_host_count) {
  61. return internalNginx.reload();
  62. }
  63. })
  64. .then(() => {
  65. // Add to audit log
  66. return internalAuditLog.add(access, {
  67. action: 'created',
  68. object_type: 'access-list',
  69. object_id: row.id,
  70. meta: internalAccessList.maskItems(data)
  71. });
  72. })
  73. .then(() => {
  74. return internalAccessList.maskItems(row);
  75. });
  76. });
  77. },
  78. /**
  79. * @param {Access} access
  80. * @param {Object} data
  81. * @param {Integer} data.id
  82. * @param {String} [data.name]
  83. * @param {String} [data.items]
  84. * @return {Promise}
  85. */
  86. update: (access, data) => {
  87. return access.can('access_lists:update', data.id)
  88. .then((/*access_data*/) => {
  89. return internalAccessList.get(access, {id: data.id});
  90. })
  91. .then((row) => {
  92. if (row.id !== data.id) {
  93. // Sanity check that something crazy hasn't happened
  94. throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
  95. }
  96. })
  97. .then(() => {
  98. // patch name if specified
  99. if (typeof data.name !== 'undefined' && data.name) {
  100. return accessListModel
  101. .query()
  102. .where({id: data.id})
  103. .patch({
  104. name: data.name
  105. });
  106. }
  107. })
  108. .then(() => {
  109. // Check for items and add/update/remove them
  110. if (typeof data.items !== 'undefined' && data.items) {
  111. let promises = [];
  112. let items_to_keep = [];
  113. data.items.map(function (item) {
  114. if (item.password) {
  115. promises.push(accessListAuthModel
  116. .query()
  117. .insert({
  118. access_list_id: data.id,
  119. username: item.username,
  120. password: item.password
  121. })
  122. );
  123. } else {
  124. // This was supplied with an empty password, which means keep it but don't change the password
  125. items_to_keep.push(item.username);
  126. }
  127. });
  128. let query = accessListAuthModel
  129. .query()
  130. .delete()
  131. .where('access_list_id', data.id);
  132. if (items_to_keep.length) {
  133. query.andWhere('username', 'NOT IN', items_to_keep);
  134. }
  135. return query
  136. .then(() => {
  137. // Add new items
  138. if (promises.length) {
  139. return Promise.all(promises);
  140. }
  141. });
  142. }
  143. })
  144. .then(() => {
  145. // Add to audit log
  146. return internalAuditLog.add(access, {
  147. action: 'updated',
  148. object_type: 'access-list',
  149. object_id: data.id,
  150. meta: internalAccessList.maskItems(data)
  151. });
  152. })
  153. .then(() => {
  154. // re-fetch with expansions
  155. return internalAccessList.get(access, {
  156. id: data.id,
  157. expand: ['owner', 'items']
  158. }, true /* <- skip masking */);
  159. })
  160. .then((row) => {
  161. return internalAccessList.build(row)
  162. .then(() => {
  163. if (row.proxy_host_count) {
  164. return internalNginx.reload();
  165. }
  166. })
  167. .then(() => {
  168. return internalAccessList.maskItems(row);
  169. });
  170. });
  171. },
  172. /**
  173. * @param {Access} access
  174. * @param {Object} data
  175. * @param {Integer} data.id
  176. * @param {Array} [data.expand]
  177. * @param {Array} [data.omit]
  178. * @param {Boolean} [skip_masking]
  179. * @return {Promise}
  180. */
  181. get: (access, data, skip_masking) => {
  182. if (typeof data === 'undefined') {
  183. data = {};
  184. }
  185. return access.can('access_lists:get', data.id)
  186. .then((access_data) => {
  187. let query = accessListModel
  188. .query()
  189. .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
  190. .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
  191. .where('access_list.is_deleted', 0)
  192. .andWhere('access_list.id', data.id)
  193. .allowEager('[owner,items,proxy_hosts]')
  194. .omit(['access_list.is_deleted'])
  195. .first();
  196. if (access_data.permission_visibility !== 'all') {
  197. query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
  198. }
  199. // Custom omissions
  200. if (typeof data.omit !== 'undefined' && data.omit !== null) {
  201. query.omit(data.omit);
  202. }
  203. if (typeof data.expand !== 'undefined' && data.expand !== null) {
  204. query.eager('[' + data.expand.join(', ') + ']');
  205. }
  206. return query;
  207. })
  208. .then((row) => {
  209. if (row) {
  210. if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
  211. row = internalAccessList.maskItems(row);
  212. }
  213. return _.omit(row, omissions());
  214. } else {
  215. throw new error.ItemNotFoundError(data.id);
  216. }
  217. });
  218. },
  219. /**
  220. * @param {Access} access
  221. * @param {Object} data
  222. * @param {Integer} data.id
  223. * @param {String} [data.reason]
  224. * @returns {Promise}
  225. */
  226. delete: (access, data) => {
  227. return access.can('access_lists:delete', data.id)
  228. .then(() => {
  229. return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items']});
  230. })
  231. .then((row) => {
  232. if (!row) {
  233. throw new error.ItemNotFoundError(data.id);
  234. }
  235. // 1. update row to be deleted
  236. // 2. update any proxy hosts that were using it (ignoring permissions)
  237. // 3. reconfigure those hosts
  238. // 4. audit log
  239. // 1. update row to be deleted
  240. return accessListModel
  241. .query()
  242. .where('id', row.id)
  243. .patch({
  244. is_deleted: 1
  245. })
  246. .then(() => {
  247. // 2. update any proxy hosts that were using it (ignoring permissions)
  248. if (row.proxy_hosts) {
  249. return proxyHostModel
  250. .query()
  251. .where('access_list_id', '=', row.id)
  252. .patch({access_list_id: 0})
  253. .then(() => {
  254. // 3. reconfigure those hosts, then reload nginx
  255. // set the access_list_id to zero for these items
  256. row.proxy_hosts.map(function (val, idx) {
  257. row.proxy_hosts[idx].access_list_id = 0;
  258. });
  259. return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
  260. })
  261. .then(() => {
  262. return internalNginx.reload();
  263. });
  264. }
  265. })
  266. .then(() => {
  267. // delete the htpasswd file
  268. let htpasswd_file = internalAccessList.getFilename(row);
  269. try {
  270. fs.unlinkSync(htpasswd_file);
  271. } catch (err) {
  272. // do nothing
  273. }
  274. })
  275. .then(() => {
  276. // 4. audit log
  277. return internalAuditLog.add(access, {
  278. action: 'deleted',
  279. object_type: 'access-list',
  280. object_id: row.id,
  281. meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
  282. });
  283. });
  284. })
  285. .then(() => {
  286. return true;
  287. });
  288. },
  289. /**
  290. * All Lists
  291. *
  292. * @param {Access} access
  293. * @param {Array} [expand]
  294. * @param {String} [search_query]
  295. * @returns {Promise}
  296. */
  297. getAll: (access, expand, search_query) => {
  298. return access.can('access_lists:list')
  299. .then((access_data) => {
  300. let query = accessListModel
  301. .query()
  302. .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
  303. .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
  304. .where('access_list.is_deleted', 0)
  305. .groupBy('access_list.id')
  306. .omit(['access_list.is_deleted'])
  307. .allowEager('[owner,items]')
  308. .orderBy('access_list.name', 'ASC');
  309. if (access_data.permission_visibility !== 'all') {
  310. query.andWhere('owner_user_id', access.token.getUserId(1));
  311. }
  312. // Query is used for searching
  313. if (typeof search_query === 'string') {
  314. query.where(function () {
  315. this.where('name', 'like', '%' + search_query + '%');
  316. });
  317. }
  318. if (typeof expand !== 'undefined' && expand !== null) {
  319. query.eager('[' + expand.join(', ') + ']');
  320. }
  321. return query;
  322. })
  323. .then((rows) => {
  324. if (rows) {
  325. rows.map(function (row, idx) {
  326. if (typeof row.items !== 'undefined' && row.items) {
  327. rows[idx] = internalAccessList.maskItems(row);
  328. }
  329. });
  330. }
  331. return rows;
  332. });
  333. },
  334. /**
  335. * Report use
  336. *
  337. * @param {Integer} user_id
  338. * @param {String} visibility
  339. * @returns {Promise}
  340. */
  341. getCount: (user_id, visibility) => {
  342. let query = accessListModel
  343. .query()
  344. .count('id as count')
  345. .where('is_deleted', 0);
  346. if (visibility !== 'all') {
  347. query.andWhere('owner_user_id', user_id);
  348. }
  349. return query.first()
  350. .then((row) => {
  351. return parseInt(row.count, 10);
  352. });
  353. },
  354. /**
  355. * @param {Object} list
  356. * @returns {Object}
  357. */
  358. maskItems: (list) => {
  359. if (list && typeof list.items !== 'undefined') {
  360. list.items.map(function (val, idx) {
  361. let repeat_for = 8;
  362. let first_char = '*';
  363. if (typeof val.password !== 'undefined' && val.password) {
  364. repeat_for = val.password.length - 1;
  365. first_char = val.password.charAt(0);
  366. }
  367. list.items[idx].hint = first_char + ('*').repeat(repeat_for);
  368. list.items[idx].password = '';
  369. });
  370. }
  371. return list;
  372. },
  373. /**
  374. * @param {Object} list
  375. * @param {Integer} list.id
  376. * @returns {String}
  377. */
  378. getFilename: (list) => {
  379. return '/data/access/' + list.id;
  380. },
  381. /**
  382. * @param {Object} list
  383. * @param {Integer} list.id
  384. * @param {String} list.name
  385. * @param {Array} list.items
  386. * @returns {Promise}
  387. */
  388. build: (list) => {
  389. logger.info('Building Access file #' + list.id + ' for: ' + list.name);
  390. return new Promise((resolve, reject) => {
  391. let htpasswd_file = internalAccessList.getFilename(list);
  392. // 1. remove any existing access file
  393. try {
  394. fs.unlinkSync(htpasswd_file);
  395. } catch (err) {
  396. // do nothing
  397. }
  398. // 2. create empty access file
  399. try {
  400. fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'});
  401. resolve(htpasswd_file);
  402. } catch (err) {
  403. reject(err);
  404. }
  405. })
  406. .then((htpasswd_file) => {
  407. // 3. generate password for each user
  408. if (list.items.length) {
  409. return new Promise((resolve, reject) => {
  410. batchflow(list.items).sequential()
  411. .each((i, item, next) => {
  412. if (typeof item.password !== 'undefined' && item.password.length) {
  413. logger.info('Adding: ' + item.username);
  414. utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
  415. .then((/*result*/) => {
  416. next();
  417. })
  418. .catch((err) => {
  419. logger.error(err);
  420. next(err);
  421. });
  422. }
  423. })
  424. .error((err) => {
  425. logger.error(err);
  426. reject(err);
  427. })
  428. .end((results) => {
  429. logger.success('Built Access file #' + list.id + ' for: ' + list.name);
  430. resolve(results);
  431. });
  432. });
  433. }
  434. });
  435. }
  436. };
  437. module.exports = internalAccessList;