]>
Commit | Line | Data |
---|---|---|
409e7115 WB |
1 | package PMG::API2::TFA; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use HTTP::Status qw(:constants); | |
7 | ||
8 | use PVE::Exception qw(raise raise_perm_exc raise_param_exc); | |
9 | use PVE::JSONSchema qw(get_standard_option); | |
10 | use PVE::RESTHandler; | |
11 | ||
12 | use PMG::AccessControl; | |
13 | use PMG::RESTEnvironment; | |
14 | use PMG::TFAConfig; | |
15 | use PMG::UserConfig; | |
16 | use PMG::Utils; | |
17 | ||
18 | use base qw(PVE::RESTHandler); | |
19 | ||
20 | my $OPTIONAL_PASSWORD_SCHEMA = { | |
21 | description => "The current password.", | |
22 | type => 'string', | |
23 | optional => 1, # Only required if not root@pam | |
24 | minLength => 5, | |
25 | maxLength => 64 | |
26 | }; | |
27 | ||
28 | my $TFA_TYPE_SCHEMA = { | |
29 | type => 'string', | |
30 | description => 'TFA Entry Type.', | |
31 | enum => [qw(totp u2f webauthn recovery)], | |
32 | }; | |
33 | ||
34 | my %TFA_INFO_PROPERTIES = ( | |
35 | id => { | |
36 | type => 'string', | |
37 | description => 'The id used to reference this entry.', | |
38 | }, | |
39 | description => { | |
40 | type => 'string', | |
41 | description => 'User chosen description for this entry.', | |
42 | }, | |
43 | created => { | |
44 | type => 'integer', | |
45 | description => 'Creation time of this entry as unix epoch.', | |
46 | }, | |
47 | enable => { | |
48 | type => 'boolean', | |
49 | description => 'Whether this TFA entry is currently enabled.', | |
50 | optional => 1, | |
51 | default => 1, | |
52 | }, | |
53 | ); | |
54 | ||
55 | my $TYPED_TFA_ENTRY_SCHEMA = { | |
56 | type => 'object', | |
57 | description => 'TFA Entry.', | |
58 | properties => { | |
59 | type => $TFA_TYPE_SCHEMA, | |
60 | %TFA_INFO_PROPERTIES, | |
61 | }, | |
62 | }; | |
63 | ||
64 | my $TFA_ID_SCHEMA = { | |
65 | type => 'string', | |
66 | description => 'A TFA entry id.', | |
67 | }; | |
68 | ||
69 | my $TFA_UPDATE_INFO_SCHEMA = { | |
70 | type => 'object', | |
71 | properties => { | |
72 | id => { | |
73 | type => 'string', | |
74 | description => 'The id of a newly added TFA entry.', | |
75 | }, | |
76 | challenge => { | |
77 | type => 'string', | |
78 | optional => 1, | |
79 | description => | |
80 | 'When adding u2f entries, this contains a challenge the user must respond to in' | |
81 | .' order to finish the registration.' | |
82 | }, | |
83 | recovery => { | |
84 | type => 'array', | |
85 | optional => 1, | |
86 | description => | |
87 | 'When adding recovery codes, this contains the list of codes to be displayed to' | |
88 | .' the user', | |
89 | items => { | |
90 | type => 'string', | |
91 | description => 'A recovery entry.' | |
92 | }, | |
93 | }, | |
94 | }, | |
95 | }; | |
96 | ||
97 | # Set TFA to enabled if $tfa_cfg is passed, or to disabled if $tfa_cfg is undef, | |
98 | # When enabling we also merge the old user.cfg keys into the $tfa_cfg. | |
99 | my sub set_user_tfa_enabled : prototype($$$) { | |
100 | my ($userid, $realm, $tfa_cfg) = @_; | |
101 | ||
102 | PMG::UserConfig::lock_config(sub { | |
103 | my $cfg = PMG::UserConfig->new(); | |
104 | my $user = $cfg->lookup_user_data($userid); | |
105 | ||
106 | # We had the 'keys' property available in PMG for a while, but never used it. | |
107 | # If the keys property had been used by someone, let's just error out here. | |
108 | my $keys = $user->{keys}; | |
109 | die "user has an unsupported 'keys' value, please remove\n" | |
110 | if defined($keys) && $keys ne 'x'; | |
111 | ||
112 | $user->{keys} = $tfa_cfg ? 'x' : undef; | |
113 | ||
114 | $cfg->write(); | |
115 | }, "enabling/disabling TFA for the user failed"); | |
116 | } | |
117 | ||
118 | # Only root may modify root, regular users need to specify their password. | |
119 | # | |
120 | # Returns the userid returned from `verify_username`. | |
121 | # Or ($userid, $realm) in list context. | |
122 | my sub check_permission_password : prototype($$$$) { | |
123 | my ($rpcenv, $authuser, $userid, $password) = @_; | |
124 | ||
125 | ($userid, my $ruid, my $realm) = PMG::Utils::verify_username($userid); | |
126 | raise("no access from quarantine\n") if $realm eq 'quarantine'; | |
127 | ||
128 | raise_perm_exc() if $userid eq 'root@pam' && $authuser ne 'root@pam'; | |
129 | ||
130 | # Regular users need to confirm their password to change TFA settings. | |
131 | if ($authuser ne 'root@pam') { | |
132 | raise_param_exc({ 'password' => 'password is required to modify TFA data' }) | |
133 | if !defined($password); | |
134 | ||
5accfdf2 | 135 | PMG::AccessControl::authenticate_user($userid, $password, 1); |
409e7115 WB |
136 | } |
137 | ||
138 | return wantarray ? ($userid, $realm) : $userid; | |
139 | } | |
140 | ||
141 | my sub check_permission_self : prototype($$) { | |
142 | my ($rpcenv, $userid) = @_; | |
143 | ||
144 | my $authuser = $rpcenv->get_user(); | |
145 | ||
146 | ($userid, my $ruid, my $realm) = PMG::Utils::verify_username($userid); | |
147 | raise("no access from quarantine\n") if $realm eq 'quarantine'; | |
148 | ||
149 | if ($authuser eq 'root@pam') { | |
150 | # OK - root can change anything | |
151 | } else { | |
152 | if ($realm eq 'pmg' && $authuser eq $userid) { | |
153 | # OK - each enable user can see their own data | |
154 | PMG::AccessControl::check_user_enabled($rpcenv->{usercfg}, $userid); | |
155 | } else { | |
156 | raise_perm_exc(); | |
157 | } | |
158 | } | |
159 | } | |
160 | ||
161 | __PACKAGE__->register_method ({ | |
162 | name => 'list_user_tfa', | |
163 | path => '{userid}', | |
164 | method => 'GET', | |
165 | proxyto => 'master', | |
166 | permissions => { | |
ea5a2b9e | 167 | description => 'Each user is allowed to view their own TFA entries.' |
409e7115 WB |
168 | .' Only root can view entries of another user.', |
169 | user => 'all', | |
170 | }, | |
171 | protected => 1, # else we can't access shadow files | |
172 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
173 | description => 'List TFA configurations of users.', | |
174 | parameters => { | |
175 | additionalProperties => 0, | |
176 | properties => { | |
177 | userid => get_standard_option('userid'), | |
178 | } | |
179 | }, | |
180 | returns => { | |
181 | description => "A list of the user's TFA entries.", | |
182 | type => 'array', | |
183 | items => $TYPED_TFA_ENTRY_SCHEMA, | |
184 | }, | |
185 | code => sub { | |
186 | my ($param) = @_; | |
187 | ||
188 | my $rpcenv = PMG::RESTEnvironment->get(); | |
189 | check_permission_self($rpcenv, $param->{userid}); | |
190 | ||
191 | my $tfa_cfg = PMG::TFAConfig->new(); | |
192 | return $tfa_cfg->api_list_user_tfa($param->{userid}); | |
193 | }}); | |
194 | ||
195 | __PACKAGE__->register_method ({ | |
196 | name => 'get_tfa_entry', | |
197 | path => '{userid}/{id}', | |
198 | method => 'GET', | |
199 | proxyto => 'master', | |
200 | permissions => { | |
ea5a2b9e | 201 | description => 'Each user is allowed to view their own TFA entries.' |
409e7115 WB |
202 | .' Only root can view entries of another user.', |
203 | user => 'all', | |
204 | }, | |
205 | protected => 1, # else we can't access shadow files | |
206 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
207 | description => 'Fetch a requested TFA entry if present.', | |
208 | parameters => { | |
209 | additionalProperties => 0, | |
210 | properties => { | |
211 | userid => get_standard_option('userid'), | |
212 | id => $TFA_ID_SCHEMA, | |
213 | } | |
214 | }, | |
215 | returns => $TYPED_TFA_ENTRY_SCHEMA, | |
216 | code => sub { | |
217 | my ($param) = @_; | |
218 | ||
219 | my $rpcenv = PMG::RESTEnvironment->get(); | |
220 | check_permission_self($rpcenv, $param->{userid}); | |
221 | ||
222 | my $tfa_cfg = PMG::TFAConfig->new(); | |
223 | my $id = $param->{id}; | |
224 | my $entry = $tfa_cfg->api_get_tfa_entry($param->{userid}, $id); | |
225 | raise("No such tfa entry '$id'", code => HTTP::Status::HTTP_NOT_FOUND) if !$entry; | |
226 | return $entry; | |
227 | }}); | |
228 | ||
229 | __PACKAGE__->register_method ({ | |
230 | name => 'delete_tfa', | |
231 | path => '{userid}/{id}', | |
232 | method => 'DELETE', | |
233 | proxyto => 'master', | |
234 | permissions => { | |
ea5a2b9e | 235 | description => 'Each user is allowed to modify their own TFA entries.' |
409e7115 | 236 | .' Only root can modify entries of another user.', |
62e3cfd4 TL |
237 | #user => 'all', # we do not support TFA for quarantine users currently |
238 | check => [ 'admin', 'qmanager', 'audit' ], | |
409e7115 WB |
239 | }, |
240 | protected => 1, # else we can't access shadow files | |
241 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
242 | description => 'Delete a TFA entry by ID.', | |
243 | parameters => { | |
244 | additionalProperties => 0, | |
245 | properties => { | |
246 | userid => get_standard_option('userid'), | |
247 | id => $TFA_ID_SCHEMA, | |
248 | password => $OPTIONAL_PASSWORD_SCHEMA, | |
249 | } | |
250 | }, | |
251 | returns => { type => 'null' }, | |
252 | code => sub { | |
253 | my ($param) = @_; | |
254 | ||
255 | my $rpcenv = PMG::RESTEnvironment->get(); | |
256 | check_permission_self($rpcenv, $param->{userid}); | |
257 | ||
258 | my $authuser = $rpcenv->get_user(); | |
259 | my $userid = | |
260 | check_permission_password($rpcenv, $authuser, $param->{userid}, $param->{password}); | |
261 | ||
262 | my $has_entries_left = PMG::TFAConfig::lock_config(sub { | |
263 | my $tfa_cfg = PMG::TFAConfig->new(); | |
264 | my $has_entries_left = $tfa_cfg->api_delete_tfa($userid, $param->{id}); | |
265 | $tfa_cfg->write(); | |
266 | return $has_entries_left; | |
267 | }); | |
268 | ||
269 | if (!$has_entries_left) { | |
270 | set_user_tfa_enabled($userid, undef, undef); | |
271 | } | |
272 | }}); | |
273 | ||
274 | __PACKAGE__->register_method ({ | |
275 | name => 'list_tfa', | |
276 | path => '', | |
277 | method => 'GET', | |
278 | proxyto => 'master', | |
279 | permissions => { | |
280 | description => "Returns all or just the logged-in user, depending on privileges.", | |
281 | user => 'all', | |
282 | }, | |
283 | protected => 1, # else we can't access shadow files | |
284 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
285 | description => 'List TFA configurations of users.', | |
286 | parameters => { | |
287 | additionalProperties => 0, | |
288 | properties => {} | |
289 | }, | |
290 | returns => { | |
291 | description => "The list tuples of user and TFA entries.", | |
292 | type => 'array', | |
293 | items => { | |
294 | type => 'object', | |
295 | properties => { | |
296 | userid => { | |
297 | type => 'string', | |
298 | description => 'User this entry belongs to.', | |
299 | }, | |
300 | entries => { | |
301 | type => 'array', | |
302 | items => $TYPED_TFA_ENTRY_SCHEMA, | |
303 | }, | |
8a1466ab WB |
304 | 'totp-locked' => { |
305 | type => 'boolean', | |
306 | optional => 1, | |
307 | description => 'True if the user is currently locked out of TOTP factors.', | |
308 | }, | |
309 | 'tfa-locked-until' => { | |
310 | type => 'integer', | |
311 | optional => 1, | |
312 | description => | |
313 | 'Contains a timestamp until when a user is locked out of 2nd factors.', | |
314 | }, | |
409e7115 WB |
315 | }, |
316 | }, | |
317 | }, | |
318 | code => sub { | |
319 | my ($param) = @_; | |
320 | ||
321 | my $rpcenv = PMG::RESTEnvironment->get(); | |
322 | my $authuser = $rpcenv->get_user(); | |
323 | my $top_level_allowed = ($authuser eq 'root@pam'); | |
324 | ||
325 | my $tfa_cfg = PMG::TFAConfig->new(); | |
326 | return $tfa_cfg->api_list_tfa($authuser, $top_level_allowed); | |
327 | }}); | |
328 | ||
329 | __PACKAGE__->register_method ({ | |
330 | name => 'add_tfa_entry', | |
331 | path => '{userid}', | |
332 | method => 'POST', | |
333 | proxyto => 'master', | |
334 | permissions => { | |
ea5a2b9e | 335 | description => 'Each user is allowed to modify their own TFA entries.' |
409e7115 | 336 | .' Only root can modify entries of another user.', |
62e3cfd4 TL |
337 | #user => 'all', # we do not support TFA for quarantine users currently |
338 | check => [ 'admin', 'qmanager', 'audit' ], | |
409e7115 WB |
339 | }, |
340 | protected => 1, # else we can't access shadow files | |
341 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
342 | description => 'Add a TFA entry for a user.', | |
343 | parameters => { | |
344 | additionalProperties => 0, | |
345 | properties => { | |
346 | userid => get_standard_option('userid'), | |
347 | type => $TFA_TYPE_SCHEMA, | |
348 | description => { | |
349 | type => 'string', | |
350 | description => 'A description to distinguish multiple entries from one another', | |
351 | maxLength => 255, | |
352 | optional => 1, | |
353 | }, | |
354 | totp => { | |
355 | type => 'string', | |
356 | description => "A totp URI.", | |
357 | optional => 1, | |
358 | }, | |
359 | value => { | |
360 | type => 'string', | |
361 | description => | |
362 | 'The current value for the provided totp URI, or a Webauthn/U2F' | |
363 | .' challenge response', | |
364 | optional => 1, | |
365 | }, | |
366 | challenge => { | |
367 | type => 'string', | |
368 | description => 'When responding to a u2f challenge: the original challenge string', | |
369 | optional => 1, | |
370 | }, | |
371 | password => $OPTIONAL_PASSWORD_SCHEMA, | |
372 | }, | |
373 | }, | |
374 | returns => $TFA_UPDATE_INFO_SCHEMA, | |
375 | code => sub { | |
376 | my ($param) = @_; | |
377 | ||
378 | my $rpcenv = PMG::RESTEnvironment->get(); | |
379 | check_permission_self($rpcenv, $param->{userid}); | |
380 | my $authuser = $rpcenv->get_user(); | |
381 | my ($userid, $realm) = | |
382 | check_permission_password($rpcenv, $authuser, $param->{userid}, $param->{password}); | |
383 | ||
384 | my $type = delete $param->{type}; | |
385 | my $value = delete $param->{value}; | |
386 | ||
387 | return PMG::TFAConfig::lock_config(sub { | |
388 | my $tfa_cfg = PMG::TFAConfig->new(); | |
389 | ||
390 | set_user_tfa_enabled($userid, $realm, $tfa_cfg); | |
391 | my $origin = undef; | |
392 | if (!$tfa_cfg->has_webauthn_origin()) { | |
5accfdf2 | 393 | $origin = 'https://'.$rpcenv->get_request_host(1); |
409e7115 WB |
394 | } |
395 | ||
396 | my $response = $tfa_cfg->api_add_tfa_entry( | |
397 | $userid, | |
398 | $param->{description}, | |
399 | $param->{totp}, | |
400 | $value, | |
401 | $param->{challenge}, | |
402 | $type, | |
403 | $origin, | |
404 | ); | |
405 | ||
406 | $tfa_cfg->write(); | |
407 | ||
408 | return $response; | |
409 | }); | |
410 | }}); | |
411 | ||
412 | __PACKAGE__->register_method ({ | |
413 | name => 'update_tfa_entry', | |
414 | path => '{userid}/{id}', | |
415 | method => 'PUT', | |
416 | proxyto => 'master', | |
417 | permissions => { | |
ea5a2b9e | 418 | description => 'Each user is allowed to modify their own TFA entries.' |
409e7115 | 419 | .' Only root can modify entries of another user.', |
62e3cfd4 TL |
420 | #user => 'all', # we do not support TFA for quarantine users currently |
421 | check => [ 'admin', 'qmanager', 'audit' ], | |
409e7115 WB |
422 | }, |
423 | protected => 1, # else we can't access shadow files | |
424 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
425 | description => 'Add a TFA entry for a user.', | |
426 | parameters => { | |
427 | additionalProperties => 0, | |
428 | properties => { | |
429 | userid => get_standard_option('userid', { | |
430 | completion => \&PVE::AccessControl::complete_username, | |
431 | }), | |
432 | id => $TFA_ID_SCHEMA, | |
433 | description => { | |
434 | type => 'string', | |
435 | description => 'A description to distinguish multiple entries from one another', | |
436 | maxLength => 255, | |
437 | optional => 1, | |
438 | }, | |
439 | enable => { | |
440 | type => 'boolean', | |
441 | description => 'Whether the entry should be enabled for login.', | |
442 | optional => 1, | |
443 | }, | |
444 | password => $OPTIONAL_PASSWORD_SCHEMA, | |
445 | }, | |
446 | }, | |
447 | returns => { type => 'null' }, | |
448 | code => sub { | |
449 | my ($param) = @_; | |
450 | ||
451 | my $rpcenv = PMG::RESTEnvironment->get(); | |
452 | check_permission_self($rpcenv, $param->{userid}); | |
453 | my $authuser = $rpcenv->get_user(); | |
454 | my $userid = | |
455 | check_permission_password($rpcenv, $authuser, $param->{userid}, $param->{password}); | |
456 | ||
457 | PMG::TFAConfig::lock_config(sub { | |
458 | my $tfa_cfg = PMG::TFAConfig->new(); | |
459 | ||
460 | $tfa_cfg->api_update_tfa_entry( | |
461 | $userid, | |
462 | $param->{id}, | |
463 | $param->{description}, | |
464 | $param->{enable}, | |
465 | ); | |
466 | ||
467 | $tfa_cfg->write(); | |
468 | }); | |
469 | }}); | |
470 | ||
471 | 1; |