acl.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. 'use strict';
  2. 'require view';
  3. 'require dom';
  4. 'require fs';
  5. 'require ui';
  6. 'require uci';
  7. 'require form';
  8. 'require tools.widgets as widgets';
  9. var aclList = {};
  10. function globListToRegExp(section_id, option) {
  11. var list = L.toArray(uci.get('rpcd', section_id, option)),
  12. positivePatterns = [],
  13. negativePatterns = [];
  14. if (option == 'read')
  15. list.push.apply(list, L.toArray(uci.get('rpcd', section_id, 'write')));
  16. for (var i = 0; i < list.length; i++) {
  17. var array, glob;
  18. if (list[i].match(/^\s*!/)) {
  19. glob = list[i].replace(/^\s*!/, '').trim();
  20. array = negativePatterns;
  21. }
  22. else {
  23. glob = list[i].trim(),
  24. array = positivePatterns;
  25. }
  26. array.push(glob.replace(/[.*+?^${}()|[\]\\]/g, function(m) {
  27. switch (m[0]) {
  28. case '?':
  29. return '.';
  30. case '*':
  31. return '.*';
  32. default:
  33. return '\\' + m[0];
  34. }
  35. }));
  36. }
  37. return [
  38. new RegExp('^' + (positivePatterns.length ? '(' + positivePatterns.join('|') + ')' : '') + '$'),
  39. new RegExp('^' + (negativePatterns.length ? '(' + negativePatterns.join('|') + ')' : '') + '$')
  40. ];
  41. }
  42. var cbiACLLevel = form.DummyValue.extend({
  43. textvalue: function(section_id) {
  44. var allowedAclMatches = globListToRegExp(section_id, this.option.match(/read/) ? 'read' : 'write'),
  45. aclGroupNames = Object.keys(aclList),
  46. matchingGroupNames = [];
  47. for (var j = 0; j < aclGroupNames.length; j++)
  48. if (allowedAclMatches[0].test(aclGroupNames[j]) && !allowedAclMatches[1].test(aclGroupNames[j]))
  49. matchingGroupNames.push(aclGroupNames[j]);
  50. if (matchingGroupNames.length == aclGroupNames.length)
  51. return E('span', { 'class': 'label' }, [ _('full', 'All permissions granted') ]);
  52. else if (matchingGroupNames.length > 0)
  53. return E('span', { 'class': 'label' }, [ _('partial (%d/%d)', 'Some permissions granted').format(matchingGroupNames.length, aclGroupNames.length) ]);
  54. else
  55. return E('span', { 'class': 'label warning' }, [ _('denied', 'No permissions granted') ]);
  56. }
  57. });
  58. var cbiACLSelect = form.Value.extend({
  59. renderWidget: function(section_id) {
  60. var readMatches = globListToRegExp(section_id, 'read'),
  61. writeMatches = globListToRegExp(section_id, 'write');
  62. var table = E('table', { 'class': 'table' }, [
  63. E('tr', { 'class': 'tr' }, [
  64. E('th', { 'class': 'th' }, [ _('ACL group') ]),
  65. E('th', { 'class': 'th' }, [ _('Description') ]),
  66. E('th', { 'class': 'th' }, [ _('Access level') ])
  67. ]),
  68. E('tr', { 'class': 'tr' }, [
  69. E('td', { 'class': 'td' }, [ '' ]),
  70. E('td', { 'class': 'td' }, [ '' ]),
  71. E('td', { 'class': 'td' }, [
  72. _('Set all: ', 'Set all permissions in the table below to one of the given values'),
  73. E('a', { 'href': '#', 'click': function() {
  74. table.querySelectorAll('select').forEach(function(select) { select.value = select.options[0].value });
  75. } }, [ _('denied', 'No permissions granted') ]), ' | ',
  76. E('a', { 'href': '#', 'click': function() {
  77. table.querySelectorAll('select').forEach(function(select) { select.value = 'read' });
  78. } }, [ _('readonly', 'Only read permissions granted') ]), ' | ',
  79. E('a', { 'href': '#', 'click': function() {
  80. table.querySelectorAll('select').forEach(function(select) { select.value = 'write' });
  81. } }, [ _('full', 'All permissions granted') ]),
  82. ])
  83. ])
  84. ]);
  85. Object.keys(aclList).sort().forEach(function(aclGroupName) {
  86. var isRequired = (aclGroupName == 'unauthenticated' || aclGroupName == 'luci-base'),
  87. isReadable = (readMatches[0].test(aclGroupName) && !readMatches[1].test(aclGroupName)) || null,
  88. isWritable = (writeMatches[0].test(aclGroupName) && !writeMatches[1].test(aclGroupName)) || null;
  89. table.appendChild(E('tr', { 'class': 'tr' }, [
  90. E('td', { 'class': 'td' }, [ aclGroupName ]),
  91. E('td', { 'class': 'td' }, [ aclList[aclGroupName].description || '-' ]),
  92. E('td', { 'class': 'td' }, [
  93. E('select', { 'data-acl-group': aclGroupName }, [
  94. isRequired ? E([]) : E('option', { 'value': '' }, [ _('denied', 'No permissions granted') ]),
  95. E('option', { 'value': 'read', 'selected': isReadable }, [ _('readonly', 'Only read permissions granted') ]),
  96. E('option', { 'value': 'write', 'selected': isWritable }, [ _('full', 'All permissions granted') ])
  97. ])
  98. ])
  99. ]));
  100. });
  101. return table;
  102. },
  103. formvalue: function(section_id) {
  104. var node = this.map.findElement('data-field', this.cbid(section_id)),
  105. data = {};
  106. node.querySelectorAll('[data-acl-group]').forEach(function(select) {
  107. var aclGroupName = select.getAttribute('data-acl-group'),
  108. value = select.value;
  109. if (!value)
  110. return;
  111. switch (value) {
  112. case 'write':
  113. data.write = data.write || [];
  114. data.write.push(aclGroupName);
  115. /* fall through */
  116. case 'read':
  117. data.read = data.read || [];
  118. data.read.push(aclGroupName);
  119. break;
  120. }
  121. });
  122. return data;
  123. },
  124. write: function(section_id, value) {
  125. uci.unset('rpcd', section_id, 'read');
  126. uci.unset('rpcd', section_id, 'write');
  127. if (L.isObject(value) && Array.isArray(value.read))
  128. uci.set('rpcd', section_id, 'read', value.read);
  129. if (L.isObject(value) && Array.isArray(value.write))
  130. uci.set('rpcd', section_id, 'write', value.write);
  131. }
  132. });
  133. return view.extend({
  134. load: function() {
  135. return L.resolveDefault(fs.list('/usr/share/rpcd/acl.d'), []).then(function(entries) {
  136. var tasks = [
  137. L.resolveDefault(fs.stat('/usr/sbin/uhttpd'), null),
  138. fs.lines('/etc/passwd')
  139. ];
  140. for (var i = 0; i < entries.length; i++)
  141. if (entries[i].type == 'file' && entries[i].name.match(/\.json$/))
  142. tasks.push(L.resolveDefault(fs.read('/usr/share/rpcd/acl.d/' + entries[i].name).then(JSON.parse)));
  143. return Promise.all(tasks);
  144. });
  145. },
  146. render: function(data) {
  147. ui.addNotification(null, E('p', [
  148. _('The LuCI ACL management is in an experimental stage! It does not yet work reliably with all applications')
  149. ]), 'warning');
  150. var has_uhttpd = data[0],
  151. known_unix_users = {};
  152. for (var i = 0; i < data[1].length; i++) {
  153. var parts = data[1][i].split(/:/);
  154. if (parts.length >= 7)
  155. known_unix_users[parts[0]] = true;
  156. }
  157. for (var i = 2; i < data.length; i++) {
  158. if (!L.isObject(data[i]))
  159. continue;
  160. for (var aclName in data[i]) {
  161. if (!data[i].hasOwnProperty(aclName))
  162. continue;
  163. aclList[aclName] = data[i][aclName];
  164. }
  165. }
  166. var m, s, o;
  167. m = new form.Map('rpcd', _('LuCI Logins'));
  168. s = m.section(form.GridSection, 'login');
  169. s.anonymous = true;
  170. s.addremove = true;
  171. s.modaltitle = function(section_id) {
  172. return _('LuCI Logins') + ' » ' + (uci.get('rpcd', section_id, 'username') || _('New account'));
  173. };
  174. o = s.option(form.Value, 'username', _('Login name'));
  175. o.rmempty = false;
  176. o = s.option(form.ListValue, '_variant', _('Password variant'));
  177. o.modalonly = true;
  178. o.value('shadow', _('Use UNIX password in /etc/shadow'));
  179. o.value('crypted', _('Use encrypted password hash'));
  180. o.cfgvalue = function(section_id) {
  181. var value = uci.get('rpcd', section_id, 'password') || '';
  182. if (value.substring(0, 3) == '$p$')
  183. return 'shadow';
  184. else
  185. return 'crypted';
  186. };
  187. o.write = function() {};
  188. o = s.option(widgets.UserSelect, '_account', _('UNIX account'), _('The system account to use the password from'));
  189. o.modalonly = true;
  190. o.depends('_variant', 'shadow');
  191. o.cfgvalue = function(section_id) {
  192. var value = uci.get('rpcd', section_id, 'password') || '';
  193. return value.substring(3);
  194. };
  195. o.write = function(section_id, value) {
  196. uci.set('rpcd', section_id, 'password', '$p$' + value);
  197. };
  198. o.remove = function() {};
  199. o = s.option(form.Value, 'password', _('Password value'));
  200. o.modalonly = true;
  201. o.password = true;
  202. o.rmempty = false;
  203. o.depends('_variant', 'crypted');
  204. o.cfgvalue = function(section_id) {
  205. var value = uci.get('rpcd', section_id, 'password') || '';
  206. return (value.substring(0, 3) == '$p$') ? '' : value;
  207. };
  208. o.validate = function(section_id, value) {
  209. var variant = this.map.lookupOption('_variant', section_id)[0];
  210. switch (value.substring(0, 3)) {
  211. case '$p$':
  212. return _('The password may not start with "$p$".');
  213. case '$1$':
  214. variant.getUIElement(section_id).setValue('crypted');
  215. break;
  216. default:
  217. if (variant.formvalue(section_id) == 'crypted' && value.length && !has_uhttpd)
  218. return _('Cannot encrypt plaintext password since uhttpd is not installed.');
  219. }
  220. return true;
  221. };
  222. o.write = function(section_id, value) {
  223. var variant = this.map.lookupOption('_variant', section_id)[0];
  224. if (variant.formvalue(section_id) == 'crypted' && value.substring(0, 3) != '$1$')
  225. return fs.exec('/usr/sbin/uhttpd', [ '-m', value ]).then(function(res) {
  226. if (res.code == 0 && res.stdout)
  227. uci.set('rpcd', section_id, 'password', res.stdout.trim());
  228. else
  229. throw new Error(res.stderr);
  230. }).catch(function(err) {
  231. throw new Error(_('Unable to encrypt plaintext password: %s').format(err.message));
  232. });
  233. uci.set('rpcd', section_id, 'password', value);
  234. };
  235. o.remove = function() {};
  236. o = s.option(form.Value, 'timeout', _('Session timeout'));
  237. o.default = '300';
  238. o.datatype = 'uinteger';
  239. o.textvalue = function(section_id) {
  240. var value = uci.get('rpcd', section_id, 'timeout') || this.default;
  241. return +value ? '%ds'.format(value) : E('em', [ _('does not expire') ]);
  242. };
  243. o = s.option(cbiACLLevel, '_read', _('Read access'));
  244. o.modalonly = false;
  245. o = s.option(cbiACLLevel, '_write', _('Write access'));
  246. o.modalonly = false;
  247. o = s.option(form.ListValue, '_level', _('Access level'));
  248. o.modalonly = true;
  249. o.value('write', _('full', 'All permissions granted'));
  250. o.value('read', _('readonly', 'Only read permissions granted'));
  251. o.value('individual', _('individual', 'Select individual permissions manually'));
  252. o.cfgvalue = function(section_id) {
  253. var readList = L.toArray(uci.get('rpcd', section_id, 'read')),
  254. writeList = L.toArray(uci.get('rpcd', section_id, 'write'));
  255. if (writeList.length == 1 && writeList[0] == '*')
  256. return 'write';
  257. else if (readList.length == 1 && readList[0] == '*')
  258. return 'read';
  259. else
  260. return 'individual';
  261. };
  262. o.write = function(section_id) {
  263. switch (this.formvalue(section_id)) {
  264. case 'write':
  265. uci.set('rpcd', section_id, 'read', ['*']);
  266. uci.set('rpcd', section_id, 'write', ['*']);
  267. break;
  268. case 'read':
  269. uci.set('rpcd', section_id, 'read', ['*']);
  270. uci.unset('rpcd', section_id, 'write');
  271. break;
  272. }
  273. };
  274. o.remove = function() {};
  275. o = s.option(cbiACLSelect, '_acl');
  276. o.modalonly = true;
  277. o.depends('_level', 'individual');
  278. return m.render();
  279. }
  280. });