luci2: rework system administration view with fancier ssh pubkey handling
[project/luci2/ui.git] / luci2 / htdocs / luci2 / view / system.users.js
1 L.ui.view.extend({
2     aclTable: L.cbi.AbstractValue.extend({
3         strGlob: function(pattern, match) {
4             var re = new RegExp('^' + (pattern
5                 .replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
6                 .replace(/\\\*/g, '.*?')) + '$');
7
8             return re.test(match);
9         },
10
11         aclMatch: function(list, group) {
12             for (var i = 0; i < list.length; i++)
13             {
14                 var x = list[i].replace(/^\s*!\s*/, '');
15                 if (x == list[i])
16                     continue;
17
18                 if (this.strGlob(x, group))
19                     return false;
20             }
21
22             for (var i = 0; i < list.length; i++)
23             {
24                 var x = list[i].replace(/^\s*!\s*/, '');
25                 if (x != list[i])
26                     continue;
27
28                 if (this.strGlob(x, group))
29                     return true;
30             }
31
32             return false;
33         },
34
35         aclTest: function(list, group) {
36             for (var i = 0; i < list.length; i++)
37                 if (list[i] == group)
38                     return true;
39
40             return false;
41         },
42
43         aclEqual: function(list1, list2) {
44             if (list1.length != list2.length)
45                 return false;
46
47             for (var i = 0; i < list1.length; i++)
48                 if (list1[i] != list2[i])
49                     return false;
50
51             return true;
52         },
53
54         aclFromUCI: function(value) {
55             var list;
56             if (typeof(value) == 'string')
57                 list = value.split(/\s+/);
58             else if ($.isArray(value))
59                 list = value;
60             else
61                 list = [ ];
62
63             var rv = [ ];
64             if (this.choices)
65                 for (var i = 0; i < this.choices.length; i++)
66                     if (this.aclMatch(list, this.choices[i][0]))
67                         rv.push(this.choices[i][0]);
68
69             return rv;
70         },
71
72         aclToUCI: function(list) {
73             if (list.length < (this.choices.length / 2))
74                 return list;
75
76             var set = { };
77             for (var i = 0; i < list.length; i++)
78                 set[list[i]] = true;
79
80             var rv = [ '*' ];
81             for (var i = 0; i < this.choices.length; i++)
82                 if (!set[this.choices[i][0]])
83                     rv.push('!' + this.choices[i][0]);
84
85             return rv;
86         },
87
88         setAll: function(ev) {
89             $(this).parents('table')
90                 .find('input[value=%d]'.format(ev.data.level))
91                 .prop('checked', true);
92         },
93
94                 widget: function(sid)
95                 {
96             var t = $('<table />')
97                 .attr('id', this.id(sid))
98                 .append($('<tr />')
99                     .append($('<th />')
100                         .text(L.tr('ACL Group')))
101                     .append($('<th />')
102                         .text(L.trc('No access', 'N'))
103                         .attr('title', L.tr('Set all to no access'))
104                         .css('cursor', 'pointer')
105                         .click({ level: 0 }, this.setAll))
106                     .append($('<th />')
107                         .text(L.trc('Read only access', 'R'))
108                         .attr('title', L.tr('Set all to read only access'))
109                         .css('cursor', 'pointer')
110                         .click({ level: 1 }, this.setAll))
111                     .append($('<th />')
112                         .text(L.trc('Full access', 'F'))
113                         .attr('title', L.tr('Set all to full access'))
114                         .css('cursor', 'pointer')
115                         .click({ level: 2 }, this.setAll)));
116
117             var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
118             var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
119
120             if (this.choices)
121                 for (var i = 0; i < this.choices.length; i++)
122                 {
123                     var r = t.get(0).insertRow(-1);
124                     var is_r = this.aclTest(acl_r, this.choices[i][0]);
125                     var is_w = this.aclTest(acl_w, this.choices[i][0]);
126
127                     $(r.insertCell(-1))
128                         .text(this.choices[i][1]);
129
130                     for (var j = 0; j < 3; j++)
131                     {
132                         $(r.insertCell(-1))
133                             .append($('<input />')
134                                 .attr('type', 'radio')
135                                 .attr('name', '%s_%s'.format(this.id(sid), this.choices[i][0]))
136                                 .attr('value', j)
137                                 .prop('checked', (j == 0 && !is_r && !is_w) ||
138                                                  (j == 1 &&  is_r && !is_w) ||
139                                                  (j == 2 &&           is_w)));
140                     }
141                 }
142
143             return t;
144                 },
145
146         textvalue: function(sid)
147         {
148             var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
149             var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
150
151             var htmlid = this.id(sid);
152             var radios = $('#' + htmlid + ' input');
153
154             var acls = [  ];
155
156             for (var i = 0; i < this.choices.length; i++)
157             {
158                 switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
159                 {
160                 case '2':
161                     acls.push('%s: %s'.format(this.choices[i][0], L.trc('Full access', 'F')));
162                     break;
163
164                 case '1':
165                     acls.push('%s: %s'.format(this.choices[i][0], L.trc('Read only access', 'R')));
166                     break;
167
168                 case '0':
169                     acls.push('%s: %s'.format(this.choices[i][0], L.trc('No access', 'N')));
170                     break;
171                 }
172             }
173
174             return acls.join(', ');
175         },
176
177                 value: function(k, v)
178                 {
179                         if (!this.choices)
180                                 this.choices = [ ];
181
182                         this.choices.push([k, v || k]);
183                         return this;
184                 },
185
186         save: function(sid)
187         {
188             var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
189             var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
190
191             var acl_r_new = [ ];
192             var acl_w_new = [ ];
193
194             var htmlid = this.id(sid);
195             var radios = $('#' + htmlid + ' input');
196
197             for (var i = 0; i < this.choices.length; i++)
198             {
199                 switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
200                 {
201                 case '2':
202                     acl_r_new.push(this.choices[i][0]);
203                     acl_w_new.push(this.choices[i][0]);
204                     break;
205
206                 case '1':
207                     acl_r_new.push(this.choices[i][0]);
208                     break;
209                 }
210             }
211
212             if (!this.aclEqual(acl_r, acl_r_new))
213                 this.map.set('rpcd', sid, 'read', this.aclToUCI(acl_r_new));
214
215             if (!this.aclEqual(acl_w, acl_w_new))
216                 this.map.set('rpcd', sid, 'write', this.aclToUCI(acl_w_new));
217         }
218         }),
219
220     execute: function() {
221         var self = this;
222         L.ui.listAvailableACLs().then(function(acls) {
223             var m = new L.cbi.Map('rpcd', {
224                 caption:     L.tr('Guest Logins'),
225                 description: L.tr('Manage user accounts and permissions for accessing the LuCI ui.'),
226                 readonly:    !self.options.acls.users
227             });
228
229             var s = m.section(L.cbi.TypedSection, 'login', {
230                 caption:      function(sid) {
231                     var u = sid ? this.fields.username.textvalue(sid) : undefined;
232                     return u ? L.tr('Login "%s"').format(u) : L.tr('New login');
233                 },
234                 collabsible:  true,
235                 addremove:    true,
236                 add_caption:  L.tr('Add new user …'),
237                 teasers:      [ '__shadow', '__acls' ]
238             });
239
240             s.option(L.cbi.InputValue, 'username', {
241                 caption:     L.tr('Username'),
242                 description: L.tr('Specifies the login name for the guest account'),
243                 optional:    false
244             });
245
246
247             var shadow = s.option(L.cbi.CheckboxValue, '__shadow', {
248                 caption:     L.tr('Use system account'),
249                 description: L.tr('Use password from the Linux user database')
250             });
251
252             shadow.ucivalue = function(sid) {
253                 var pw = this.map.get('rpcd', sid, 'password');
254                 return (pw && pw.indexOf('$p$') == 0);
255             };
256
257
258             var password = s.option(L.cbi.PasswordValue, 'password', {
259                 caption:     L.tr('Password'),
260                 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.'),
261                 optional:    false
262             });
263
264             password.depends('__shadow', false);
265
266             password.toggle = function(sid) {
267                 var id = '#' + this.id(sid);
268                 var pw = this.map.get('rpcd', sid, 'password');
269                 var sh = this.section.fields.__shadow.formvalue(sid);
270
271                 if (!sh && pw && pw.indexOf('$p$') == 0)
272                     $(id).val('');
273
274                 this.callSuper('toggle', sid);
275             };
276
277             shadow.save = password.save = function(sid) {
278                 var sh = this.section.fields.__shadow.formvalue(sid);
279                 var pw = this.section.fields.password.formvalue(sid);
280
281                 if (!sh && !pw)
282                     return;
283
284                 if (sh)
285                     pw = '$p$' + this.section.fields.username.formvalue(sid);
286
287                 if (pw.match(/^\$[0-9p][a-z]?\$/))
288                 {
289                     if (pw != this.map.get('rpcd', sid, 'password'))
290                         this.map.set('rpcd', sid, 'password', pw);
291                 }
292                 else
293                 {
294                     var map = this.map;
295                     return L.ui.cryptPassword(pw).then(function(crypt) {
296                         map.set('rpcd', sid, 'password', crypt);
297                     });
298                 }
299             };
300
301             var o = s.option(self.aclTable, '__acls', {
302                 caption:     L.tr('User ACLs'),
303                 description: L.tr('Specifies the access levels of this account. The "N" column means no access, "R" stands for read only access and "F" for full access.')
304             });
305
306             var groups = [ ];
307             for (var group_name in acls)
308                 groups.push(group_name);
309
310             groups.sort();
311
312             for (var i = 0; i < groups.length; i++)
313                 o.value(groups[i], acls[groups[i]].description);
314
315             return m.insertInto('#map');
316         });
317     }
318 });