From 9033161056cd355a3884b1ed810f427b18bfe7e8 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Mon, 30 Sep 2013 16:41:50 +0000 Subject: [PATCH] luci2: add view for managing LuCI2 login accounts --- luci2/htdocs/luci2/view/system.users.js | 314 ++++++++++++++++++++++++++++++++ luci2/share/acl.d/luci2.json | 28 ++- luci2/share/menu.d/system.json | 14 +- 3 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 luci2/htdocs/luci2/view/system.users.js diff --git a/luci2/htdocs/luci2/view/system.users.js b/luci2/htdocs/luci2/view/system.users.js new file mode 100644 index 0000000..5447238 --- /dev/null +++ b/luci2/htdocs/luci2/view/system.users.js @@ -0,0 +1,314 @@ +L.ui.view.extend({ + aclTable: L.cbi.AbstractValue.extend({ + strGlob: function(pattern, match) { + var re = new RegExp('^' + (pattern + .replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1') + .replace(/\\\*/g, '.*?')) + '$'); + + return re.test(match); + }, + + aclMatch: function(list, group) { + for (var i = 0; i < list.length; i++) + { + var x = list[i].replace(/^\s*!\s*/, ''); + if (x == list[i]) + continue; + + if (this.strGlob(x, group)) + return false; + } + + for (var i = 0; i < list.length; i++) + { + var x = list[i].replace(/^\s*!\s*/, ''); + if (x != list[i]) + continue; + + if (this.strGlob(x, group)) + return true; + } + + return false; + }, + + aclTest: function(list, group) { + for (var i = 0; i < list.length; i++) + if (list[i] == group) + return true; + + return false; + }, + + aclEqual: function(list1, list2) { + if (list1.length != list2.length) + return false; + + for (var i = 0; i < list1.length; i++) + if (list1[i] != list2[i]) + return false; + + return true; + }, + + aclFromUCI: function(value) { + var list; + if (typeof(value) == 'string') + list = value.split(/\s+/); + else if ($.isArray(value)) + list = value; + else + list = [ ]; + + var rv = [ ]; + if (this.choices) + for (var i = 0; i < this.choices.length; i++) + if (this.aclMatch(list, this.choices[i][0])) + rv.push(this.choices[i][0]); + + return rv; + }, + + aclToUCI: function(list) { + if (list.length < (this.choices.length / 2)) + return list; + + var set = { }; + for (var i = 0; i < list.length; i++) + set[list[i]] = true; + + var rv = [ '*' ]; + for (var i = 0; i < this.choices.length; i++) + if (!set[this.choices[i][0]]) + rv.push('!' + this.choices[i][0]); + + return rv; + }, + + setAll: function(ev) { + $(this).parents('table') + .find('input[value=%d]'.format(ev.data.level)) + .prop('checked', true); + }, + + widget: function(sid) + { + var t = $('') + .attr('id', this.id(sid)) + .append($('') + .append($('
') + .text(L.tr('ACL Group'))) + .append($('') + .text(L.trc('No access', 'N')) + .attr('title', L.tr('Set all to no access')) + .css('cursor', 'pointer') + .click({ level: 0 }, this.setAll)) + .append($('') + .text(L.trc('Read only access', 'R')) + .attr('title', L.tr('Set all to read only access')) + .css('cursor', 'pointer') + .click({ level: 1 }, this.setAll)) + .append($('') + .text(L.trc('Full access', 'F')) + .attr('title', L.tr('Set all to full access')) + .css('cursor', 'pointer') + .click({ level: 2 }, this.setAll))); + + var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read')); + var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write')); + + if (this.choices) + for (var i = 0; i < this.choices.length; i++) + { + var r = t.get(0).insertRow(-1); + var is_r = this.aclTest(acl_r, this.choices[i][0]); + var is_w = this.aclTest(acl_w, this.choices[i][0]); + + $(r.insertCell(-1)) + .text(this.choices[i][1]); + + for (var j = 0; j < 3; j++) + { + $(r.insertCell(-1)) + .append($('') + .attr('type', 'radio') + .attr('name', '%s_%s'.format(this.id(sid), this.choices[i][0])) + .attr('value', j) + .prop('checked', (j == 0 && !is_r && !is_w) || + (j == 1 && is_r && !is_w) || + (j == 2 && is_w))); + } + } + + return t; + }, + + textvalue: function(sid) + { + var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read')); + var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write')); + + var htmlid = this.id(sid); + var radios = $('#' + htmlid + ' input'); + + var acls = [ ]; + + for (var i = 0; i < this.choices.length; i++) + { + switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val()) + { + case '2': + acls.push('%s: %s'.format(this.choices[i][0], L.trc('Full access', 'F'))); + break; + + case '1': + acls.push('%s: %s'.format(this.choices[i][0], L.trc('Read only access', 'R'))); + break; + + case '0': + acls.push('%s: %s'.format(this.choices[i][0], L.trc('No access', 'N'))); + break; + } + } + + return acls.join(', '); + }, + + value: function(k, v) + { + if (!this.choices) + this.choices = [ ]; + + this.choices.push([k, v || k]); + return this; + }, + + save: function(sid) + { + var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read')); + var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write')); + + var acl_r_new = [ ]; + var acl_w_new = [ ]; + + var htmlid = this.id(sid); + var radios = $('#' + htmlid + ' input'); + + for (var i = 0; i < this.choices.length; i++) + { + switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val()) + { + case '2': + acl_r_new.push(this.choices[i][0]); + acl_w_new.push(this.choices[i][0]); + break; + + case '1': + acl_r_new.push(this.choices[i][0]); + break; + } + } + + if (!this.aclEqual(acl_r, acl_r_new)) + this.map.set('rpcd', sid, 'read', this.aclToUCI(acl_r_new)); + + if (!this.aclEqual(acl_w, acl_w_new)) + this.map.set('rpcd', sid, 'write', this.aclToUCI(acl_w_new)); + } + }), + + execute: function() { + var self = this; + L.ui.listAvailableACLs().then(function(acls) { + var m = new L.cbi.Map('rpcd', { + caption: L.tr('Guest Logins'), + description: L.tr('Manage user accounts and permissions for accessing the LuCI ui.'), + collabsible: true + }); + + var s = m.section(L.cbi.TypedSection, 'login', { + caption: function(sid) { + var u = sid ? this.fields.username.textvalue(sid) : undefined; + return u ? L.tr('Login "%s"').format(u) : L.tr('New login'); + }, + addremove: true, + add_caption: L.tr('Add new user …'), + teasers: [ '__shadow', '__acls' ] + }); + + s.option(L.cbi.InputValue, 'username', { + caption: L.tr('Username'), + description: L.tr('Specifies the login name for the guest account'), + optional: false + }); + + + var shadow = s.option(L.cbi.CheckboxValue, '__shadow', { + caption: L.tr('Use system account'), + description: L.tr('Use password from the Linux user database') + }); + + shadow.ucivalue = function(sid) { + var pw = this.map.get('rpcd', sid, 'password'); + return (pw && pw.indexOf('$p$') == 0); + }; + + + var password = s.option(L.cbi.PasswordValue, 'password', { + caption: L.tr('Password'), + description: L.tr('Specifies the password for the guest account. If you enter a plaintext password here, it will get replaced with a crypted password hash on save.'), + optional: false + }); + + password.depends('__shadow', false); + + password.toggle = function(sid) { + var id = '#' + this.id(sid); + var pw = this.map.get('rpcd', sid, 'password'); + var sh = this.section.fields.__shadow.formvalue(sid); + + if (!sh && pw && pw.indexOf('$p$') == 0) + $(id).val(''); + + this.callSuper('toggle', sid); + }; + + shadow.save = password.save = function(sid) { + var sh = this.section.fields.__shadow.formvalue(sid); + var pw = this.section.fields.password.formvalue(sid); + + if (sh) + pw = '$p$' + this.section.fields.username.formvalue(sid); + + if (pw.match(/^\$[0-9p][a-z]?\$/)) + { + if (pw != this.map.get('rpcd', sid, 'password')) + this.map.set('rpcd', sid, 'password', pw); + } + else + { + var map = this.map; + return L.ui.cryptPassword(pw).then(function(crypt) { + map.set('rpcd', sid, 'password', crypt); + }); + } + }; + + var o = s.option(self.aclTable, '__acls', { + caption: L.tr('User ACLs'), + description: L.tr('Specifies the access levels of this account. The "-" column means no access, "R" stands for read only access and "F" for full access.') + }); + + var groups = [ ]; + for (var group_name in acls) + groups.push(group_name); + + groups.sort(); + + for (var i = 0; i < groups.length; i++) + o.value(groups[i], acls[groups[i]].description); + + return m.insertInto('#map'); + }); + } +}); diff --git a/luci2/share/acl.d/luci2.json b/luci2/share/acl.d/luci2.json index e912e17..64974e8 100644 --- a/luci2/share/acl.d/luci2.json +++ b/luci2/share/acl.d/luci2.json @@ -1,4 +1,15 @@ { + "unauthenticated": { + "description": "Functions allowed for unauthenticated requests", + "read": { + "ubus": { + "luci2.ui": [ + "themes" + ] + } + } + }, + "core": { "description": "Core functions for LuCI", "read": { @@ -7,7 +18,8 @@ "*" ], "session": [ - "access" + "access", + "destroy" ], "uci": [ "*" @@ -110,6 +122,20 @@ } }, + "users": { + "description": "Guest login settings", + "read": { + "uci": [ + "rpcd" + ] + }, + "write": { + "uci": [ + "rpcd" + ] + } + }, + "software": { "description": "Package management", "read": { diff --git a/luci2/share/menu.d/system.json b/luci2/share/menu.d/system.json index 4b2ee72..a7753c9 100644 --- a/luci2/share/menu.d/system.json +++ b/luci2/share/menu.d/system.json @@ -15,28 +15,34 @@ "view": "system/admin", "index": 20 }, + "system/users": { + "title": "Guest Logins", + "acls": [ "users" ], + "view": "system/users", + "index": 30 + }, "system/software": { "title": "Software", "acls": [ "software" ], "view": "system/software", - "index": 30 + "index": 40 }, "system/startup": { "title": "Startup", "acls": [ "startup" ], "view": "system/startup", - "index": 40 + "index": 50 }, "system/cron": { "title": "Scheduled Tasks", "acls": [ "cron" ], "view": "system/cron", - "index": 50 + "index": 60 }, "system/leds": { "title": "LED Configuration", "acls": [ "leds" ], "view": "system/leds", - "index": 60 + "index": 70 } } -- 2.11.0