1 use anyhow
::{bail, format_err, Error}
;
2 use serde
::{Deserialize, Serialize}
;
4 use proxmox
::api
::{api, Permission, Router, RpcEnvironment}
;
5 use proxmox
::tools
::tfa
::totp
::Totp
;
6 use proxmox
::{http_bail, http_err}
;
8 use crate::api2
::types
::{Authid, Userid, PASSWORD_SCHEMA}
;
9 use crate::config
::acl
::{PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT}
;
10 use crate::config
::cached_user_info
::CachedUserInfo
;
11 use crate::config
::tfa
::{TfaInfo, TfaUserData}
;
13 /// Perform first-factor (password) authentication only. Ignore password for the root user.
14 /// Otherwise check the current user's password.
16 /// This means that user admins need to type in their own password while editing a user, and
17 /// regular users, which can only change their own TFA settings (checked at the API level), can
18 /// change their own settings using their own password.
20 rpcenv
: &mut dyn RpcEnvironment
,
22 password
: Option
<String
>,
23 ) -> Result
<(), Error
> {
24 let authid
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
26 if authid
.user() != Userid
::root_userid() {
27 let password
= password
.ok_or_else(|| format_err
!("missing password"))?
;
28 let _
: () = crate::auth
::authenticate_user(authid
.user(), &password
)?
;
31 // After authentication, verify that the to-be-modified user actually exists:
32 if authid
.user() != userid
{
33 let (config
, _digest
) = crate::config
::user
::config()?
;
35 if config
.sections
.get(userid
.as_str()).is_none() {
36 bail
!("user '{}' does not exists.", userid
);
45 #[derive(Deserialize, Serialize)]
46 #[serde(rename_all = "lowercase")]
48 /// A TOTP entry type.
50 /// A U2F token entry.
52 /// A Webauthn token entry.
60 type: { type: TfaType }
,
61 info
: { type: TfaInfo }
,
64 /// A TFA entry for a user.
65 #[derive(Deserialize, Serialize)]
66 #[serde(deny_unknown_fields)]
68 #[serde(rename = "type")]
75 fn to_data(data
: TfaUserData
) -> Vec
<TypedTfaInfo
> {
76 let mut out
= Vec
::with_capacity(
80 + if data
.has_recovery() { 1 }
else { 0 }
,
82 if data
.has_recovery() {
83 out
.push(TypedTfaInfo
{
84 ty
: TfaType
::Recovery
,
85 info
: TfaInfo
::recovery(),
88 for entry
in data
.totp
{
89 out
.push(TypedTfaInfo
{
94 for entry
in data
.webauthn
{
95 out
.push(TypedTfaInfo
{
96 ty
: TfaType
::Webauthn
,
100 for entry
in data
.u2f
{
101 out
.push(TypedTfaInfo
{
109 /// Iterate through tuples of `(type, index, id)`.
110 fn tfa_id_iter(data
: &TfaUserData
) -> impl Iterator
<Item
= (TfaType
, usize, &str)> {
114 .map(|(i
, entry
)| (TfaType
::Totp
, i
, entry
.info
.id
.as_str()))
119 .map(|(i
, entry
)| (TfaType
::Webauthn
, i
, entry
.info
.id
.as_str())),
125 .map(|(i
, entry
)| (TfaType
::U2f
, i
, entry
.info
.id
.as_str())),
130 .map(|_
| (TfaType
::Recovery
, 0, "recovery")),
137 properties
: { userid: { type: Userid }
},
140 permission
: &Permission
::Or(&[
141 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
142 &Permission
::UserParam("userid"),
146 /// Add a TOTP secret to the user.
147 fn list_user_tfa(userid
: Userid
) -> Result
<Vec
<TypedTfaInfo
>, Error
> {
148 let _lock
= crate::config
::tfa
::read_lock()?
;
150 Ok(match crate::config
::tfa
::read()?
.users
.remove(&userid
) {
151 Some(data
) => to_data(data
),
160 userid
: { type: Userid }
,
161 id
: { description: "the tfa entry id" }
165 permission
: &Permission
::Or(&[
166 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
167 &Permission
::UserParam("userid"),
171 /// Get a single TFA entry.
172 fn get_tfa_entry(userid
: Userid
, id
: String
) -> Result
<TypedTfaInfo
, Error
> {
173 let _lock
= crate::config
::tfa
::read_lock()?
;
175 if let Some(user_data
) = crate::config
::tfa
::read()?
.users
.remove(&userid
) {
177 // scope to prevent the temprary iter from borrowing across the whole match
178 let entry
= tfa_id_iter(&user_data
).find(|(_ty
, _index
, entry_id
)| id
== *entry_id
);
179 entry
.map(|(ty
, index
, _
)| (ty
, index
))
181 Some((TfaType
::Recovery
, _
)) => {
182 return Ok(TypedTfaInfo
{
183 ty
: TfaType
::Recovery
,
184 info
: TfaInfo
::recovery(),
187 Some((TfaType
::Totp
, index
)) => {
188 return Ok(TypedTfaInfo
{
190 // `into_iter().nth()` to *move* out of it
191 info
: user_data
.totp
.into_iter().nth(index
).unwrap().info
,
194 Some((TfaType
::Webauthn
, index
)) => {
195 return Ok(TypedTfaInfo
{
196 ty
: TfaType
::Webauthn
,
197 info
: user_data
.webauthn
.into_iter().nth(index
).unwrap().info
,
200 Some((TfaType
::U2f
, index
)) => {
201 return Ok(TypedTfaInfo
{
203 info
: user_data
.u2f
.into_iter().nth(index
).unwrap().info
,
210 http_bail
!(NOT_FOUND
, "no such tfa entry: {}/{}", userid
, id
);
217 userid
: { type: Userid }
,
219 description
: "the tfa entry id",
222 schema
: PASSWORD_SCHEMA
,
228 permission
: &Permission
::Or(&[
229 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
230 &Permission
::UserParam("userid"),
234 /// Get a single TFA entry.
238 password
: Option
<String
>,
239 rpcenv
: &mut dyn RpcEnvironment
,
240 ) -> Result
<(), Error
> {
241 tfa_update_auth(rpcenv
, &userid
, password
)?
;
243 let _lock
= crate::config
::tfa
::write_lock()?
;
245 let mut data
= crate::config
::tfa
::read()?
;
250 .ok_or_else(|| http_err
!(NOT_FOUND
, "no such entry: {}/{}", userid
, id
))?
;
253 // scope to prevent the temprary iter from borrowing across the whole match
254 let entry
= tfa_id_iter(&user_data
).find(|(_
, _
, entry_id
)| id
== *entry_id
);
255 entry
.map(|(ty
, index
, _
)| (ty
, index
))
257 Some((TfaType
::Recovery
, _
)) => user_data
.recovery
= None
,
258 Some((TfaType
::Totp
, index
)) => drop(user_data
.totp
.remove(index
)),
259 Some((TfaType
::Webauthn
, index
)) => drop(user_data
.webauthn
.remove(index
)),
260 Some((TfaType
::U2f
, index
)) => drop(user_data
.u2f
.remove(index
)),
261 None
=> http_bail
!(NOT_FOUND
, "no such tfa entry: {}/{}", userid
, id
),
264 if user_data
.is_empty() {
265 data
.users
.remove(&userid
);
268 crate::config
::tfa
::write(&data
)?
;
275 "userid": { type: Userid }
,
278 items
: { type: TypedTfaInfo }
,
282 #[derive(Deserialize, Serialize)]
283 #[serde(deny_unknown_fields)]
284 /// Over the API we only provide the descriptions for TFA data.
286 /// The user this entry belongs to.
290 entries
: Vec
<TypedTfaInfo
>,
299 permission
: &Permission
::Anybody
,
300 description
: "Returns all or just the logged-in user, depending on privileges.",
303 description
: "The list tuples of user and TFA entries.",
305 items
: { type: TfaUser }
308 /// List user TFA configuration.
309 fn list_tfa(rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Vec
<TfaUser
>, Error
> {
310 let authid
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
311 let user_info
= CachedUserInfo
::new()?
;
313 let top_level_privs
= user_info
.lookup_privs(&authid
, &["access", "users"]);
314 let top_level_allowed
= (top_level_privs
& PRIV_SYS_AUDIT
) != 0;
316 let _lock
= crate::config
::tfa
::read_lock()?
;
317 let tfa_data
= crate::config
::tfa
::read()?
.users
;
319 let mut out
= Vec
::<TfaUser
>::new();
320 if top_level_allowed
{
321 for (user
, data
) in tfa_data
{
324 entries
: to_data(data
),
328 if let Some(data
) = { tfa_data }
.remove(authid
.user()) {
330 userid
: authid
.into(),
331 entries
: to_data(data
),
342 description
: "A list of recovery codes as integers.",
346 description
: "A one-time usable recovery code entry.",
351 /// The result returned when adding TFA entries to a user.
352 #[derive(Default, Serialize)]
353 struct TfaUpdateInfo
{
354 /// The id if a newly added TFA entry.
357 /// When adding u2f entries, this contains a challenge the user must respond to in order to
358 /// finish the registration.
359 #[serde(skip_serializing_if = "Option::is_none")]
360 challenge
: Option
<String
>,
362 /// When adding recovery codes, this contains the list of codes to be displayed to the user
364 #[serde(skip_serializing_if = "Vec::is_empty", default)]
365 recovery
: Vec
<String
>,
369 fn id(id
: String
) -> Self {
381 userid
: { type: Userid }
,
383 description
: "A description to distinguish multiple entries from one another",
388 "type": { type: TfaType }
,
390 description
: "A totp URI.",
395 "The current value for the provided totp URI, or a Webauthn/U2F challenge response",
399 description
: "When responding to a u2f challenge: the original challenge string",
403 schema
: PASSWORD_SCHEMA
,
408 returns
: { type: TfaUpdateInfo }
,
410 permission
: &Permission
::Or(&[
411 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
412 &Permission
::UserParam("userid"),
416 /// Add a TFA entry to the user.
419 description
: Option
<String
>,
420 totp
: Option
<String
>,
421 value
: Option
<String
>,
422 challenge
: Option
<String
>,
423 password
: Option
<String
>,
425 rpcenv
: &mut dyn RpcEnvironment
,
426 ) -> Result
<TfaUpdateInfo
, Error
> {
427 tfa_update_auth(rpcenv
, &userid
, password
)?
;
429 let need_description
=
430 move || description
.ok_or_else(|| format_err
!("'description' is required for new entries"));
433 TfaType
::Totp
=> match (totp
, value
) {
434 (Some(totp
), Some(value
)) => {
435 if challenge
.is_some() {
436 bail
!("'challenge' parameter is invalid for 'totp' entries");
438 let description
= need_description()?
;
440 let totp
: Totp
= totp
.parse()?
;
442 .verify(&value
, std
::time
::SystemTime
::now(), -1..=1)?
445 bail
!("failed to verify TOTP challenge");
447 crate::config
::tfa
::add_totp(&userid
, description
, totp
).map(TfaUpdateInfo
::id
)
449 _
=> bail
!("'totp' type requires both 'totp' and 'value' parameters"),
451 TfaType
::Webauthn
=> {
453 bail
!("'totp' parameter is invalid for 'totp' entries");
457 None
=> crate::config
::tfa
::add_webauthn_registration(&userid
, need_description()?
)
458 .map(|c
| TfaUpdateInfo
{
463 let value
= value
.ok_or_else(|| {
465 "missing 'value' parameter (webauthn challenge response missing)"
468 crate::config
::tfa
::finish_webauthn_registration(&userid
, &challenge
, &value
)
469 .map(TfaUpdateInfo
::id
)
475 bail
!("'totp' parameter is invalid for 'totp' entries");
479 None
=> crate::config
::tfa
::add_u2f_registration(&userid
, need_description()?
).map(
486 let value
= value
.ok_or_else(|| {
487 format_err
!("missing 'value' parameter (u2f challenge response missing)")
489 crate::config
::tfa
::finish_u2f_registration(&userid
, &challenge
, &value
)
490 .map(TfaUpdateInfo
::id
)
494 TfaType
::Recovery
=> {
495 if totp
.or(value
).or(challenge
).is_some() {
496 bail
!("generating recovery tokens does not allow additional parameters");
499 let recovery
= crate::config
::tfa
::add_recovery(&userid
)?
;
502 id
: Some("recovery".to_string()),
514 userid
: { type: Userid }
,
516 description
: "the tfa entry id",
519 description
: "A description to distinguish multiple entries from one another",
525 description
: "Whether this entry should currently be enabled or disabled",
529 schema
: PASSWORD_SCHEMA
,
535 permission
: &Permission
::Or(&[
536 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
537 &Permission
::UserParam("userid"),
541 /// Update user's TFA entry description.
545 description
: Option
<String
>,
546 enable
: Option
<bool
>,
547 password
: Option
<String
>,
548 rpcenv
: &mut dyn RpcEnvironment
,
549 ) -> Result
<(), Error
> {
550 tfa_update_auth(rpcenv
, &userid
, password
)?
;
552 let _lock
= crate::config
::tfa
::write_lock()?
;
554 let mut data
= crate::config
::tfa
::read()?
;
559 .and_then(|user
| user
.find_entry_mut(&id
))
560 .ok_or_else(|| http_err
!(NOT_FOUND
, "no such entry: {}/{}", userid
, id
))?
;
562 if let Some(description
) = description
{
563 entry
.description
= description
;
566 if let Some(enable
) = enable
{
567 entry
.enable
= enable
;
570 crate::config
::tfa
::write(&data
)?
;
574 pub const ROUTER
: Router
= Router
::new()
575 .get(&API_METHOD_LIST_TFA
)
576 .match_all("userid", &USER_ROUTER
);
578 const USER_ROUTER
: Router
= Router
::new()
579 .get(&API_METHOD_LIST_USER_TFA
)
580 .post(&API_METHOD_ADD_TFA_ENTRY
)
581 .match_all("id", &ITEM_ROUTER
);
583 const ITEM_ROUTER
: Router
= Router
::new()
584 .get(&API_METHOD_GET_TFA_ENTRY
)
585 .put(&API_METHOD_UPDATE_TFA_ENTRY
)
586 .delete(&API_METHOD_DELETE_TFA
);