cc495e98f55c8231b79e7da19d9cdfa2eef821e5
[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                                 .addClass('table table-condensed table-hover')
98                                 .attr('id', this.id(sid))
99                                 .append($('<tr />')
100                                         .append($('<th />')
101                                                 .text(L.tr('ACL Group')))
102                                         .append($('<th />')
103                                                 .text(L.trc('No access', 'N'))
104                                                 .attr('title', L.tr('Set all to no access'))
105                                                 .css('cursor', 'pointer')
106                                                 .click({ level: 0 }, this.setAll))
107                                         .append($('<th />')
108                                                 .text(L.trc('Read only access', 'R'))
109                                                 .attr('title', L.tr('Set all to read only access'))
110                                                 .css('cursor', 'pointer')
111                                                 .click({ level: 1 }, this.setAll))
112                                         .append($('<th />')
113                                                 .text(L.trc('Full access', 'F'))
114                                                 .attr('title', L.tr('Set all to full access'))
115                                                 .css('cursor', 'pointer')
116                                                 .click({ level: 2 }, this.setAll)));
117
118                         var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
119                         var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
120
121                         if (this.choices)
122                                 for (var i = 0; i < this.choices.length; i++)
123                                 {
124                                         var r = t.get(0).insertRow(-1);
125                                         var is_r = this.aclTest(acl_r, this.choices[i][0]);
126                                         var is_w = this.aclTest(acl_w, this.choices[i][0]);
127
128                                         $(r.insertCell(-1))
129                                                 .text(this.choices[i][1]);
130
131                                         for (var j = 0; j < 3; j++)
132                                         {
133                                                 $(r.insertCell(-1))
134                                                         .append($('<input />')
135                                                                 .addClass('form-control')
136                                                                 .attr('type', 'radio')
137                                                                 .attr('name', '%s_%s'.format(this.id(sid), this.choices[i][0]))
138                                                                 .attr('value', j)
139                                                                 .prop('checked', (j == 0 && !is_r && !is_w) ||
140                                                                                                  (j == 1 &&  is_r && !is_w) ||
141                                                                                                  (j == 2 &&           is_w)));
142                                         }
143                                 }
144
145                         return t;
146                 },
147
148                 textvalue: function(sid)
149                 {
150                         var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
151                         var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
152
153                         var htmlid = this.id(sid);
154                         var radios = $('#' + htmlid + ' input');
155
156                         var acls = [  ];
157
158                         for (var i = 0; i < this.choices.length; i++)
159                         {
160                                 switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
161                                 {
162                                 case '2':
163                                         acls.push('%s: %s'.format(this.choices[i][0], L.trc('Full access', 'F')));
164                                         break;
165
166                                 case '1':
167                                         acls.push('%s: %s'.format(this.choices[i][0], L.trc('Read only access', 'R')));
168                                         break;
169
170                                 case '0':
171                                         acls.push('%s: %s'.format(this.choices[i][0], L.trc('No access', 'N')));
172                                         break;
173                                 }
174                         }
175
176                         return acls.join(', ');
177                 },
178
179                 value: function(k, v)
180                 {
181                         if (!this.choices)
182                                 this.choices = [ ];
183
184                         this.choices.push([k, v || k]);
185                         return this;
186                 },
187
188                 save: function(sid)
189                 {
190                         var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
191                         var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
192
193                         var acl_r_new = [ ];
194                         var acl_w_new = [ ];
195
196                         var htmlid = this.id(sid);
197                         var radios = $('#' + htmlid + ' input');
198
199                         for (var i = 0; i < this.choices.length; i++)
200                         {
201                                 switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
202                                 {
203                                 case '2':
204                                         acl_r_new.push(this.choices[i][0]);
205                                         acl_w_new.push(this.choices[i][0]);
206                                         break;
207
208                                 case '1':
209                                         acl_r_new.push(this.choices[i][0]);
210                                         break;
211                                 }
212                         }
213
214                         if (!this.aclEqual(acl_r, acl_r_new))
215                                 this.map.set('rpcd', sid, 'read', this.aclToUCI(acl_r_new));
216
217                         if (!this.aclEqual(acl_w, acl_w_new))
218                                 this.map.set('rpcd', sid, 'write', this.aclToUCI(acl_w_new));
219                 }
220         }),
221
222         execute: function() {
223                 var self = this;
224                 return L.ui.listAvailableACLs().then(function(acls) {
225                         var m = new L.cbi.Map('rpcd', {
226                                 caption:     L.tr('Guest Logins'),
227                                 description: L.tr('Manage user accounts and permissions for accessing the LuCI ui.'),
228                                 readonly:    !self.options.acls.users
229                         });
230
231                         var s = m.section(L.cbi.TypedSection, 'login', {
232                                 caption:      L.tr('Accounts'),
233                                 collabsible:  true,
234                                 addremove:    true,
235                                 add_caption:  L.tr('Add account …'),
236                                 teasers:      [ 'username', '__shadow', '__acls' ]
237                         });
238
239                         s.option(L.cbi.InputValue, 'username', {
240                                 caption:     L.tr('Username'),
241                                 description: L.tr('Specifies the login name for the guest account'),
242                                 optional:    false
243                         });
244
245
246                         var shadow = s.option(L.cbi.CheckboxValue, '__shadow', {
247                                 caption:     L.tr('Use system account'),
248                                 description: L.tr('Use password from the Linux user database')
249                         });
250
251                         shadow.ucivalue = function(sid) {
252                                 var pw = this.map.get('rpcd', sid, 'password');
253                                 return (pw && pw.indexOf('$p$') == 0);
254                         };
255
256
257                         var password = s.option(L.cbi.PasswordValue, 'password', {
258                                 caption:     L.tr('Password'),
259                                 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.'),
260                                 optional:    false
261                         });
262
263                         password.depends('__shadow', false);
264
265                         password.toggle = function(sid) {
266                                 var id = '#' + this.id(sid);
267                                 var pw = this.map.get('rpcd', sid, 'password');
268                                 var sh = this.section.fields.__shadow.formvalue(sid);
269
270                                 if (!sh && pw && pw.indexOf('$p$') == 0)
271                                         $(id).val('');
272
273                                 this.callSuper('toggle', sid);
274                         };
275
276                         shadow.save = password.save = function(sid) {
277                                 var sh = this.section.fields.__shadow.formvalue(sid);
278                                 var pw = this.section.fields.password.formvalue(sid);
279
280                                 if (!sh && !pw)
281                                         return;
282
283                                 if (sh)
284                                         pw = '$p$' + this.section.fields.username.formvalue(sid);
285
286                                 if (pw.match(/^\$[0-9p][a-z]?\$/))
287                                 {
288                                         if (pw != this.map.get('rpcd', sid, 'password'))
289                                                 this.map.set('rpcd', sid, 'password', pw);
290                                 }
291                                 else
292                                 {
293                                         var map = this.map;
294                                         return L.ui.cryptPassword(pw).then(function(crypt) {
295                                                 map.set('rpcd', sid, 'password', crypt);
296                                         });
297                                 }
298                         };
299
300                         var o = s.option(self.aclTable, '__acls', {
301                                 caption:     L.tr('User ACLs'),
302                                 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.')
303                         });
304
305                         var groups = [ ];
306                         for (var group_name in acls)
307                                 groups.push(group_name);
308
309                         groups.sort();
310
311                         for (var i = 0; i < groups.length; i++)
312                                 o.value(groups[i], acls[groups[i]].description);
313
314                         return m.insertInto('#map');
315                 });
316         }
317 });