If the setting in the view is set to `denied`, only the read list option is deleted. This is not correct. The write list option must also be deleted. To ensure that the correct configuration is saved, the write and read list options are always deleted beforehand and then rewritten. Signed-off-by: Florian Eckert <fe@dev.tdt.de>
343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require fs';
|
|
'require ui';
|
|
'require uci';
|
|
'require form';
|
|
'require tools.widgets as widgets';
|
|
|
|
var aclList = {};
|
|
|
|
function globListToRegExp(section_id, option) {
|
|
var list = L.toArray(uci.get('rpcd', section_id, option)),
|
|
positivePatterns = [],
|
|
negativePatterns = [];
|
|
|
|
if (option == 'read')
|
|
list.push.apply(list, L.toArray(uci.get('rpcd', section_id, 'write')));
|
|
|
|
for (var i = 0; i < list.length; i++) {
|
|
var array, glob;
|
|
|
|
if (list[i].match(/^\s*!/)) {
|
|
glob = list[i].replace(/^\s*!/, '').trim();
|
|
array = negativePatterns;
|
|
}
|
|
else {
|
|
glob = list[i].trim(),
|
|
array = positivePatterns;
|
|
}
|
|
|
|
array.push(glob.replace(/[.*+?^${}()|[\]\\]/g, function(m) {
|
|
switch (m[0]) {
|
|
case '?':
|
|
return '.';
|
|
|
|
case '*':
|
|
return '.*';
|
|
|
|
default:
|
|
return '\\' + m[0];
|
|
}
|
|
}));
|
|
}
|
|
|
|
return [
|
|
new RegExp('^' + (positivePatterns.length ? '(' + positivePatterns.join('|') + ')' : '') + '$'),
|
|
new RegExp('^' + (negativePatterns.length ? '(' + negativePatterns.join('|') + ')' : '') + '$')
|
|
];
|
|
}
|
|
|
|
var cbiACLLevel = form.DummyValue.extend({
|
|
textvalue: function(section_id) {
|
|
var allowedAclMatches = globListToRegExp(section_id, this.option.match(/read/) ? 'read' : 'write'),
|
|
aclGroupNames = Object.keys(aclList),
|
|
matchingGroupNames = [];
|
|
|
|
for (var j = 0; j < aclGroupNames.length; j++)
|
|
if (allowedAclMatches[0].test(aclGroupNames[j]) && !allowedAclMatches[1].test(aclGroupNames[j]))
|
|
matchingGroupNames.push(aclGroupNames[j]);
|
|
|
|
if (matchingGroupNames.length == aclGroupNames.length)
|
|
return E('span', { 'class': 'label' }, [ _('full', 'All permissions granted') ]);
|
|
else if (matchingGroupNames.length > 0)
|
|
return E('span', { 'class': 'label' }, [ _('partial (%d/%d)', 'Some permissions granted').format(matchingGroupNames.length, aclGroupNames.length) ]);
|
|
else
|
|
return E('span', { 'class': 'label warning' }, [ _('denied', 'No permissions granted') ]);
|
|
}
|
|
});
|
|
|
|
var cbiACLSelect = form.Value.extend({
|
|
renderWidget: function(section_id) {
|
|
var readMatches = globListToRegExp(section_id, 'read'),
|
|
writeMatches = globListToRegExp(section_id, 'write');
|
|
|
|
var table = E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('th', { 'class': 'th' }, [ _('ACL group') ]),
|
|
E('th', { 'class': 'th' }, [ _('Description') ]),
|
|
E('th', { 'class': 'th' }, [ _('Access level') ])
|
|
]),
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, [ '' ]),
|
|
E('td', { 'class': 'td' }, [ '' ]),
|
|
E('td', { 'class': 'td' }, [
|
|
_('Set all: ', 'Set all permissions in the table below to one of the given values'),
|
|
E('a', { 'href': '#', 'click': function() {
|
|
table.querySelectorAll('select').forEach(function(select) { select.value = select.options[0].value });
|
|
} }, [ _('denied', 'No permissions granted') ]), ' | ',
|
|
E('a', { 'href': '#', 'click': function() {
|
|
table.querySelectorAll('select').forEach(function(select) { select.value = 'read' });
|
|
} }, [ _('readonly', 'Only read permissions granted') ]), ' | ',
|
|
E('a', { 'href': '#', 'click': function() {
|
|
table.querySelectorAll('select').forEach(function(select) { select.value = 'write' });
|
|
} }, [ _('full', 'All permissions granted') ]),
|
|
])
|
|
])
|
|
]);
|
|
|
|
Object.keys(aclList).sort().forEach(function(aclGroupName) {
|
|
var isRequired = (aclGroupName == 'unauthenticated' || aclGroupName == 'luci-base'),
|
|
isReadable = (readMatches[0].test(aclGroupName) && !readMatches[1].test(aclGroupName)) || null,
|
|
isWritable = (writeMatches[0].test(aclGroupName) && !writeMatches[1].test(aclGroupName)) || null;
|
|
|
|
table.appendChild(E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, [ aclGroupName ]),
|
|
E('td', { 'class': 'td' }, [ aclList[aclGroupName].description || '-' ]),
|
|
E('td', { 'class': 'td' }, [
|
|
E('select', { 'data-acl-group': aclGroupName }, [
|
|
isRequired ? E([]) : E('option', { 'value': '' }, [ _('denied', 'No permissions granted') ]),
|
|
E('option', { 'value': 'read', 'selected': isReadable }, [ _('readonly', 'Only read permissions granted') ]),
|
|
E('option', { 'value': 'write', 'selected': isWritable }, [ _('full', 'All permissions granted') ])
|
|
])
|
|
])
|
|
]));
|
|
});
|
|
|
|
return table;
|
|
},
|
|
|
|
formvalue: function(section_id) {
|
|
var node = this.map.findElement('data-field', this.cbid(section_id)),
|
|
data = {};
|
|
|
|
node.querySelectorAll('[data-acl-group]').forEach(function(select) {
|
|
var aclGroupName = select.getAttribute('data-acl-group'),
|
|
value = select.value;
|
|
|
|
if (!value)
|
|
return;
|
|
|
|
switch (value) {
|
|
case 'write':
|
|
data.write = data.write || [];
|
|
data.write.push(aclGroupName);
|
|
/* fall through */
|
|
|
|
case 'read':
|
|
data.read = data.read || [];
|
|
data.read.push(aclGroupName);
|
|
break;
|
|
}
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
write: function(section_id, value) {
|
|
uci.unset('rpcd', section_id, 'read');
|
|
uci.unset('rpcd', section_id, 'write');
|
|
|
|
if (L.isObject(value) && Array.isArray(value.read))
|
|
uci.set('rpcd', section_id, 'read', value.read);
|
|
|
|
if (L.isObject(value) && Array.isArray(value.write))
|
|
uci.set('rpcd', section_id, 'write', value.write);
|
|
}
|
|
});
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return L.resolveDefault(fs.list('/usr/share/rpcd/acl.d'), []).then(function(entries) {
|
|
var tasks = [
|
|
L.resolveDefault(fs.stat('/usr/sbin/uhttpd'), null),
|
|
fs.lines('/etc/passwd')
|
|
];
|
|
|
|
for (var i = 0; i < entries.length; i++)
|
|
if (entries[i].type == 'file' && entries[i].name.match(/\.json$/))
|
|
tasks.push(L.resolveDefault(fs.read('/usr/share/rpcd/acl.d/' + entries[i].name).then(JSON.parse)));
|
|
|
|
return Promise.all(tasks);
|
|
});
|
|
},
|
|
|
|
render: function(data) {
|
|
ui.addNotification(null, E('p', [
|
|
_('The LuCI ACL management is in an experimental stage! It does not yet work reliably with all applications')
|
|
]), 'warning');
|
|
|
|
var has_uhttpd = data[0],
|
|
known_unix_users = {};
|
|
|
|
for (var i = 0; i < data[1].length; i++) {
|
|
var parts = data[1][i].split(/:/);
|
|
|
|
if (parts.length >= 7)
|
|
known_unix_users[parts[0]] = true;
|
|
}
|
|
|
|
for (var i = 2; i < data.length; i++) {
|
|
if (!L.isObject(data[i]))
|
|
continue;
|
|
|
|
for (var aclName in data[i]) {
|
|
if (!data[i].hasOwnProperty(aclName))
|
|
continue;
|
|
|
|
aclList[aclName] = data[i][aclName];
|
|
}
|
|
}
|
|
|
|
var m, s, o;
|
|
|
|
m = new form.Map('rpcd', _('LuCI Logins'));
|
|
|
|
s = m.section(form.GridSection, 'login');
|
|
s.anonymous = true;
|
|
s.addremove = true;
|
|
|
|
s.modaltitle = function(section_id) {
|
|
return _('LuCI Logins') + ' » ' + (uci.get('rpcd', section_id, 'username') || _('New account'));
|
|
};
|
|
|
|
o = s.option(form.Value, 'username', _('Login name'));
|
|
o.rmempty = false;
|
|
|
|
o = s.option(form.ListValue, '_variant', _('Password variant'));
|
|
o.modalonly = true;
|
|
o.value('shadow', _('Use UNIX password in /etc/shadow'));
|
|
o.value('crypted', _('Use encrypted password hash'));
|
|
o.value('plain', _('Use plain password'));
|
|
o.cfgvalue = function(section_id) {
|
|
var value = uci.get('rpcd', section_id, 'password') || '';
|
|
|
|
if (value.substring(0, 3) == '$p$')
|
|
return 'shadow';
|
|
else if (value.substring(0, 3) == '$1$' || value == null)
|
|
return 'crypted';
|
|
else
|
|
return 'plain';
|
|
};
|
|
o.write = function() {};
|
|
|
|
o = s.option(widgets.UserSelect, '_account', _('UNIX account'), _('The system account to use the password from'));
|
|
o.modalonly = true;
|
|
o.depends('_variant', 'shadow');
|
|
o.cfgvalue = function(section_id) {
|
|
var value = uci.get('rpcd', section_id, 'password') || '';
|
|
return value.substring(3);
|
|
};
|
|
o.write = function(section_id, value) {
|
|
uci.set('rpcd', section_id, 'password', '$p$' + value);
|
|
};
|
|
o.remove = function() {};
|
|
|
|
o = s.option(form.Value, 'password', _('Password value'));
|
|
o.modalonly = true;
|
|
o.password = true;
|
|
o.rmempty = false;
|
|
o.depends('_variant', 'crypted');
|
|
o.depends('_variant', 'plain');
|
|
o.cfgvalue = function(section_id) {
|
|
var value = uci.get('rpcd', section_id, 'password') || '';
|
|
return (value.substring(0, 3) == '$p$') ? '' : value;
|
|
};
|
|
o.validate = function(section_id, value) {
|
|
var variant = this.map.lookupOption('_variant', section_id)[0];
|
|
|
|
switch (value.substring(0, 3)) {
|
|
case '$p$':
|
|
return _('The password may not start with "$p$".');
|
|
|
|
case '$1$':
|
|
variant.getUIElement(section_id).setValue('crypted');
|
|
break;
|
|
|
|
default:
|
|
if (variant.formvalue(section_id) == 'crypted' && value.length && !has_uhttpd)
|
|
return _('Cannot encrypt plaintext password since uhttpd is not installed.');
|
|
}
|
|
|
|
return true;
|
|
};
|
|
o.write = function(section_id, value) {
|
|
var variant = this.map.lookupOption('_variant', section_id)[0];
|
|
|
|
if (variant.formvalue(section_id) == 'crypted' && value.substring(0, 3) != '$1$')
|
|
return fs.exec('/usr/sbin/uhttpd', [ '-m', value ]).then(function(res) {
|
|
if (res.code == 0 && res.stdout)
|
|
uci.set('rpcd', section_id, 'password', res.stdout.trim());
|
|
else
|
|
throw new Error(res.stderr);
|
|
}).catch(function(err) {
|
|
throw new Error(_('Unable to encrypt plaintext password: %s').format(err.message));
|
|
});
|
|
|
|
uci.set('rpcd', section_id, 'password', value);
|
|
};
|
|
o.remove = function() {};
|
|
|
|
o = s.option(form.Value, 'timeout', _('Session timeout'));
|
|
o.default = '300';
|
|
o.datatype = 'uinteger';
|
|
o.textvalue = function(section_id) {
|
|
var value = uci.get('rpcd', section_id, 'timeout') || this.default;
|
|
return +value ? '%ds'.format(value) : E('em', [ _('does not expire') ]);
|
|
};
|
|
|
|
o = s.option(cbiACLLevel, '_read', _('Read access'));
|
|
o.modalonly = false;
|
|
|
|
o = s.option(cbiACLLevel, '_write', _('Write access'));
|
|
o.modalonly = false;
|
|
|
|
o = s.option(form.ListValue, '_level', _('Access level'));
|
|
o.modalonly = true;
|
|
o.value('write', _('full', 'All permissions granted'));
|
|
o.value('read', _('readonly', 'Only read permissions granted'));
|
|
o.value('individual', _('individual', 'Select individual permissions manually'));
|
|
o.cfgvalue = function(section_id) {
|
|
var readList = L.toArray(uci.get('rpcd', section_id, 'read')),
|
|
writeList = L.toArray(uci.get('rpcd', section_id, 'write'));
|
|
|
|
if (writeList.length == 1 && writeList[0] == '*')
|
|
return 'write';
|
|
else if (readList.length == 1 && readList[0] == '*')
|
|
return 'read';
|
|
else
|
|
return 'individual';
|
|
};
|
|
o.write = function(section_id) {
|
|
switch (this.formvalue(section_id)) {
|
|
case 'write':
|
|
uci.set('rpcd', section_id, 'read', '*');
|
|
uci.set('rpcd', section_id, 'write', '*');
|
|
break;
|
|
|
|
case 'read':
|
|
uci.set('rpcd', section_id, 'read', '*');
|
|
uci.unset('rpcd', section_id, 'write');
|
|
break;
|
|
}
|
|
};
|
|
o.remove = function() {};
|
|
|
|
o = s.option(cbiACLSelect, '_acl');
|
|
o.modalonly = true;
|
|
o.depends('_level', 'individual');
|
|
|
|
return m.render();
|
|
}
|
|
});
|