luci2: add view for managing LuCI2 login accounts
authorJo-Philipp Wich <jow@openwrt.org>
Mon, 30 Sep 2013 16:41:50 +0000 (16:41 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Mon, 30 Sep 2013 16:41:50 +0000 (16:41 +0000)
luci2/htdocs/luci2/view/system.users.js [new file with mode: 0644]
luci2/share/acl.d/luci2.json
luci2/share/menu.d/system.json

diff --git a/luci2/htdocs/luci2/view/system.users.js b/luci2/htdocs/luci2/view/system.users.js
new file mode 100644 (file)
index 0000000..5447238
--- /dev/null
@@ -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 = $('<table />')
+                .attr('id', this.id(sid))
+                .append($('<tr />')
+                    .append($('<th />')
+                        .text(L.tr('ACL Group')))
+                    .append($('<th />')
+                        .text(L.trc('No access', 'N'))
+                        .attr('title', L.tr('Set all to no access'))
+                        .css('cursor', 'pointer')
+                        .click({ level: 0 }, this.setAll))
+                    .append($('<th />')
+                        .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($('<th />')
+                        .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($('<input />')
+                                .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');
+        });
+    }
+});
index e912e17..64974e8 100644 (file)
@@ -1,4 +1,15 @@
 {
 {
+       "unauthenticated": {
+               "description": "Functions allowed for unauthenticated requests",
+               "read": {
+                       "ubus": {
+                               "luci2.ui": [
+                                       "themes"
+                               ]
+                       }
+               }
+       },
+
        "core": {
                "description": "Core functions for LuCI",
                "read": {
        "core": {
                "description": "Core functions for LuCI",
                "read": {
@@ -7,7 +18,8 @@
                                        "*"
                                ],
                                "session": [
                                        "*"
                                ],
                                "session": [
-                                       "access"
+                                       "access",
+                                       "destroy"
                                ],
                                "uci": [
                                        "*"
                                ],
                                "uci": [
                                        "*"
                }
        },
 
                }
        },
 
+       "users": {
+               "description": "Guest login settings",
+               "read": {
+                       "uci": [
+                               "rpcd"
+                       ]
+               },
+               "write": {
+                       "uci": [
+                               "rpcd"
+                       ]
+               }
+       },
+
        "software": {
                "description": "Package management",
                "read": {
        "software": {
                "description": "Package management",
                "read": {
index 4b2ee72..a7753c9 100644 (file)
         "view": "system/admin",
         "index": 20
     },
         "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",
     "system/software": {
         "title": "Software",
         "acls": [ "software" ],
         "view": "system/software",
-        "index": 30
+        "index": 40
     },
     "system/startup": {
         "title": "Startup",
         "acls": [ "startup" ],
         "view": "system/startup",
     },
     "system/startup": {
         "title": "Startup",
         "acls": [ "startup" ],
         "view": "system/startup",
-        "index": 40
+        "index": 50
     },
     "system/cron": {
         "title": "Scheduled Tasks",
         "acls": [ "cron" ],
         "view": "system/cron",
     },
     "system/cron": {
         "title": "Scheduled Tasks",
         "acls": [ "cron" ],
         "view": "system/cron",
-        "index": 50
+        "index": 60
     },
     "system/leds": {
         "title": "LED Configuration",
         "acls": [ "leds" ],
         "view": "system/leds",
     },
     "system/leds": {
         "title": "LED Configuration",
         "acls": [ "leds" ],
         "view": "system/leds",
-        "index": 60
+        "index": 70
     }
 }
     }
 }