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