]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/API2/TFA.pm
bump version to 8.1.4
[pmg-api.git] / src / PMG / API2 / TFA.pm
CommitLineData
409e7115
WB
1package PMG::API2::TFA;
2
3use strict;
4use warnings;
5
6use HTTP::Status qw(:constants);
7
8use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
9use PVE::JSONSchema qw(get_standard_option);
10use PVE::RESTHandler;
11
12use PMG::AccessControl;
13use PMG::RESTEnvironment;
14use PMG::TFAConfig;
15use PMG::UserConfig;
16use PMG::Utils;
17
18use base qw(PVE::RESTHandler);
19
20my $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
28my $TFA_TYPE_SCHEMA = {
29 type => 'string',
30 description => 'TFA Entry Type.',
31 enum => [qw(totp u2f webauthn recovery)],
32};
33
34my %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
55my $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
64my $TFA_ID_SCHEMA = {
65 type => 'string',
66 description => 'A TFA entry id.',
67};
68
69my $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.
99my 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.
122my 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
141my 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
4711;