1 package PMG
::API2
::TFA
;
6 use HTTP
::Status
qw(:constants);
8 use PVE
::Exception
qw(raise raise_perm_exc raise_param_exc);
9 use PVE
::JSONSchema
qw(get_standard_option);
12 use PMG
::AccessControl
;
13 use PMG
::RESTEnvironment
;
18 use base
qw(PVE::RESTHandler);
20 my $OPTIONAL_PASSWORD_SCHEMA = {
21 description
=> "The current password.",
23 optional
=> 1, # Only required if not root@pam
28 my $TFA_TYPE_SCHEMA = {
30 description
=> 'TFA Entry Type.',
31 enum
=> [qw(totp u2f webauthn recovery)],
34 my %TFA_INFO_PROPERTIES = (
37 description
=> 'The id used to reference this entry.',
41 description
=> 'User chosen description for this entry.',
45 description
=> 'Creation time of this entry as unix epoch.',
49 description
=> 'Whether this TFA entry is currently enabled.',
55 my $TYPED_TFA_ENTRY_SCHEMA = {
57 description
=> 'TFA Entry.',
59 type
=> $TFA_TYPE_SCHEMA,
66 description
=> 'A TFA entry id.',
69 my $TFA_UPDATE_INFO_SCHEMA = {
74 description
=> 'The id of a newly added TFA entry.',
80 'When adding u2f entries, this contains a challenge the user must respond to in'
81 .' order to finish the registration.'
87 'When adding recovery codes, this contains the list of codes to be displayed to'
91 description
=> 'A recovery entry.'
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) = @_;
102 PMG
::UserConfig
::lock_config
(sub {
103 my $cfg = PMG
::UserConfig-
>new();
104 my $user = $cfg->lookup_user_data($userid);
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';
112 $user->{keys} = $tfa_cfg ?
'x' : undef;
115 }, "enabling/disabling TFA for the user failed");
118 # Only root may modify root, regular users need to specify their password.
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) = @_;
125 ($userid, my $ruid, my $realm) = PMG
::Utils
::verify_username
($userid);
126 raise
("no access from quarantine\n") if $realm eq 'quarantine';
128 raise_perm_exc
() if $userid eq 'root@pam' && $authuser ne 'root@pam';
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);
135 PMG
::AccessControl
::authenticate_user
($userid, $password, 1);
138 return wantarray ?
($userid, $realm) : $userid;
141 my sub check_permission_self
: prototype($$) {
142 my ($rpcenv, $userid) = @_;
144 my $authuser = $rpcenv->get_user();
146 ($userid, my $ruid, my $realm) = PMG
::Utils
::verify_username
($userid);
147 raise
("no access from quarantine\n") if $realm eq 'quarantine';
149 if ($authuser eq 'root@pam') {
150 # OK - root can change anything
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);
161 __PACKAGE__-
>register_method ({
162 name
=> 'list_user_tfa',
167 description
=> 'Each user is allowed to view their own TFA entries.'
168 .' Only root can view entries of another user.',
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.',
175 additionalProperties
=> 0,
177 userid
=> get_standard_option
('userid'),
181 description
=> "A list of the user's TFA entries.",
183 items
=> $TYPED_TFA_ENTRY_SCHEMA,
188 my $rpcenv = PMG
::RESTEnvironment-
>get();
189 check_permission_self
($rpcenv, $param->{userid
});
191 my $tfa_cfg = PMG
::TFAConfig-
>new();
192 return $tfa_cfg->api_list_user_tfa($param->{userid
});
195 __PACKAGE__-
>register_method ({
196 name
=> 'get_tfa_entry',
197 path
=> '{userid}/{id}',
201 description
=> 'Each user is allowed to view their own TFA entries.'
202 .' Only root can view entries of another user.',
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.',
209 additionalProperties
=> 0,
211 userid
=> get_standard_option
('userid'),
212 id
=> $TFA_ID_SCHEMA,
215 returns
=> $TYPED_TFA_ENTRY_SCHEMA,
219 my $rpcenv = PMG
::RESTEnvironment-
>get();
220 check_permission_self
($rpcenv, $param->{userid
});
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;
229 __PACKAGE__-
>register_method ({
230 name
=> 'delete_tfa',
231 path
=> '{userid}/{id}',
235 description
=> 'Each user is allowed to modify their own TFA entries.'
236 .' Only root can modify entries of another user.',
237 #user => 'all', # we do not support TFA for quarantine users currently
238 check
=> [ 'admin', 'qmanager', 'audit' ],
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.',
244 additionalProperties
=> 0,
246 userid
=> get_standard_option
('userid'),
247 id
=> $TFA_ID_SCHEMA,
248 password
=> $OPTIONAL_PASSWORD_SCHEMA,
251 returns
=> { type
=> 'null' },
255 my $rpcenv = PMG
::RESTEnvironment-
>get();
256 check_permission_self
($rpcenv, $param->{userid
});
258 my $authuser = $rpcenv->get_user();
260 check_permission_password
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
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
});
266 return $has_entries_left;
269 if (!$has_entries_left) {
270 set_user_tfa_enabled
($userid, undef, undef);
274 __PACKAGE__-
>register_method ({
280 description
=> "Returns all or just the logged-in user, depending on privileges.",
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.',
287 additionalProperties
=> 0,
291 description
=> "The list tuples of user and TFA entries.",
298 description
=> 'User this entry belongs to.',
302 items
=> $TYPED_TFA_ENTRY_SCHEMA,
307 description
=> 'True if the user is currently locked out of TOTP factors.',
309 'tfa-locked-until' => {
313 'Contains a timestamp until when a user is locked out of 2nd factors.',
321 my $rpcenv = PMG
::RESTEnvironment-
>get();
322 my $authuser = $rpcenv->get_user();
323 my $top_level_allowed = ($authuser eq 'root@pam');
325 my $tfa_cfg = PMG
::TFAConfig-
>new();
326 return $tfa_cfg->api_list_tfa($authuser, $top_level_allowed);
329 __PACKAGE__-
>register_method ({
330 name
=> 'add_tfa_entry',
335 description
=> 'Each user is allowed to modify their own TFA entries.'
336 .' Only root can modify entries of another user.',
337 #user => 'all', # we do not support TFA for quarantine users currently
338 check
=> [ 'admin', 'qmanager', 'audit' ],
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.',
344 additionalProperties
=> 0,
346 userid
=> get_standard_option
('userid'),
347 type
=> $TFA_TYPE_SCHEMA,
350 description
=> 'A description to distinguish multiple entries from one another',
356 description
=> "A totp URI.",
362 'The current value for the provided totp URI, or a Webauthn/U2F'
363 .' challenge response',
368 description
=> 'When responding to a u2f challenge: the original challenge string',
371 password
=> $OPTIONAL_PASSWORD_SCHEMA,
374 returns
=> $TFA_UPDATE_INFO_SCHEMA,
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
});
384 my $type = delete $param->{type
};
385 my $value = delete $param->{value
};
387 return PMG
::TFAConfig
::lock_config
(sub {
388 my $tfa_cfg = PMG
::TFAConfig-
>new();
390 set_user_tfa_enabled
($userid, $realm, $tfa_cfg);
392 if (!$tfa_cfg->has_webauthn_origin()) {
393 $origin = 'https://'.$rpcenv->get_request_host(1);
396 my $response = $tfa_cfg->api_add_tfa_entry(
398 $param->{description
},
412 __PACKAGE__-
>register_method ({
413 name
=> 'update_tfa_entry',
414 path
=> '{userid}/{id}',
418 description
=> 'Each user is allowed to modify their own TFA entries.'
419 .' Only root can modify entries of another user.',
420 #user => 'all', # we do not support TFA for quarantine users currently
421 check
=> [ 'admin', 'qmanager', 'audit' ],
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.',
427 additionalProperties
=> 0,
429 userid
=> get_standard_option
('userid', {
430 completion
=> \
&PVE
::AccessControl
::complete_username
,
432 id
=> $TFA_ID_SCHEMA,
435 description
=> 'A description to distinguish multiple entries from one another',
441 description
=> 'Whether the entry should be enabled for login.',
444 password
=> $OPTIONAL_PASSWORD_SCHEMA,
447 returns
=> { type
=> 'null' },
451 my $rpcenv = PMG
::RESTEnvironment-
>get();
452 check_permission_self
($rpcenv, $param->{userid
});
453 my $authuser = $rpcenv->get_user();
455 check_permission_password
($rpcenv, $authuser, $param->{userid
}, $param->{password
});
457 PMG
::TFAConfig
::lock_config
(sub {
458 my $tfa_cfg = PMG
::TFAConfig-
>new();
460 $tfa_cfg->api_update_tfa_entry(
463 $param->{description
},