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 });