]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/access/tfa.rs
ui: tfa: rework removal confirmation dialog
[proxmox-backup.git] / src / api2 / access / tfa.rs
CommitLineData
027ef213
WB
1use anyhow::{bail, format_err, Error};
2use serde::{Deserialize, Serialize};
027ef213
WB
3
4use proxmox::api::{api, Permission, Router, RpcEnvironment};
5use proxmox::tools::tfa::totp::Totp;
6use proxmox::{http_bail, http_err};
7
8use crate::api2::types::{Authid, Userid, PASSWORD_SCHEMA};
9use crate::config::acl::{PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT};
10use crate::config::cached_user_info::CachedUserInfo;
11use crate::config::tfa::{TfaInfo, TfaUserData};
12
13/// Perform first-factor (password) authentication only. Ignore password for the root user.
14/// Otherwise check the current user's password.
15///
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.
19fn tfa_update_auth(
20 rpcenv: &mut dyn RpcEnvironment,
21 userid: &Userid,
22 password: Option<String>,
eab25e2f 23 must_exist: bool,
027ef213
WB
24) -> Result<(), Error> {
25 let authid: Authid = rpcenv.get_auth_id().unwrap().parse()?;
26
27 if authid.user() != Userid::root_userid() {
7ad33e80
WB
28 let password = password.ok_or_else(|| http_err!(UNAUTHORIZED, "missing password"))?;
29 let _: () = crate::auth::authenticate_user(authid.user(), &password)
30 .map_err(|err| http_err!(UNAUTHORIZED, "{}", err))?;
027ef213
WB
31 }
32
33 // After authentication, verify that the to-be-modified user actually exists:
eab25e2f 34 if must_exist && authid.user() != userid {
027ef213
WB
35 let (config, _digest) = crate::config::user::config()?;
36
4bda5168
WB
37 if config
38 .lookup::<crate::config::user::User>("user", userid.as_str())
39 .is_err()
40 {
7ad33e80 41 http_bail!(UNAUTHORIZED, "user '{}' does not exists.", userid);
027ef213
WB
42 }
43 }
44
45 Ok(())
46}
47
48#[api]
49/// A TFA entry type.
50#[derive(Deserialize, Serialize)]
51#[serde(rename_all = "lowercase")]
759af9f0 52enum TfaType {
027ef213
WB
53 /// A TOTP entry type.
54 Totp,
55 /// A U2F token entry.
56 U2f,
57 /// A Webauthn token entry.
58 Webauthn,
59 /// Recovery tokens.
60 Recovery,
61}
62
63#[api(
64 properties: {
65 type: { type: TfaType },
66 info: { type: TfaInfo },
67 },
68)]
69/// A TFA entry for a user.
70#[derive(Deserialize, Serialize)]
71#[serde(deny_unknown_fields)]
759af9f0 72struct TypedTfaInfo {
027ef213
WB
73 #[serde(rename = "type")]
74 pub ty: TfaType,
75
76 #[serde(flatten)]
77 pub info: TfaInfo,
78}
79
80fn to_data(data: TfaUserData) -> Vec<TypedTfaInfo> {
81 let mut out = Vec::with_capacity(
82 data.totp.len()
83 + data.u2f.len()
84 + data.webauthn.len()
ad5cee1d 85 + if data.recovery().is_some() { 1 } else { 0 },
027ef213 86 );
ad5cee1d 87 if let Some(recovery) = data.recovery() {
027ef213
WB
88 out.push(TypedTfaInfo {
89 ty: TfaType::Recovery,
ad5cee1d 90 info: TfaInfo::recovery(recovery.created),
027ef213
WB
91 })
92 }
93 for entry in data.totp {
94 out.push(TypedTfaInfo {
95 ty: TfaType::Totp,
96 info: entry.info,
97 });
98 }
99 for entry in data.webauthn {
100 out.push(TypedTfaInfo {
101 ty: TfaType::Webauthn,
102 info: entry.info,
103 });
104 }
105 for entry in data.u2f {
106 out.push(TypedTfaInfo {
107 ty: TfaType::U2f,
108 info: entry.info,
109 });
110 }
111 out
112}
113
f58e5132
WB
114/// Iterate through tuples of `(type, index, id)`.
115fn tfa_id_iter(data: &TfaUserData) -> impl Iterator<Item = (TfaType, usize, &str)> {
116 data.totp
117 .iter()
118 .enumerate()
119 .map(|(i, entry)| (TfaType::Totp, i, entry.info.id.as_str()))
120 .chain(
121 data.webauthn
122 .iter()
123 .enumerate()
124 .map(|(i, entry)| (TfaType::Webauthn, i, entry.info.id.as_str())),
125 )
126 .chain(
127 data.u2f
128 .iter()
129 .enumerate()
130 .map(|(i, entry)| (TfaType::U2f, i, entry.info.id.as_str())),
131 )
132 .chain(
133 data.recovery
134 .iter()
135 .map(|_| (TfaType::Recovery, 0, "recovery")),
136 )
137}
138
027ef213
WB
139#[api(
140 protected: true,
141 input: {
142 properties: { userid: { type: Userid } },
143 },
144 access: {
145 permission: &Permission::Or(&[
146 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
147 &Permission::UserParam("userid"),
148 ]),
149 },
150)]
151/// Add a TOTP secret to the user.
759af9f0 152fn list_user_tfa(userid: Userid) -> Result<Vec<TypedTfaInfo>, Error> {
027ef213
WB
153 let _lock = crate::config::tfa::read_lock()?;
154
155 Ok(match crate::config::tfa::read()?.users.remove(&userid) {
156 Some(data) => to_data(data),
157 None => Vec::new(),
158 })
159}
160
161#[api(
162 protected: true,
163 input: {
164 properties: {
165 userid: { type: Userid },
166 id: { description: "the tfa entry id" }
167 },
168 },
169 access: {
170 permission: &Permission::Or(&[
171 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
172 &Permission::UserParam("userid"),
173 ]),
174 },
175)]
176/// Get a single TFA entry.
759af9f0 177fn get_tfa_entry(userid: Userid, id: String) -> Result<TypedTfaInfo, Error> {
027ef213
WB
178 let _lock = crate::config::tfa::read_lock()?;
179
180 if let Some(user_data) = crate::config::tfa::read()?.users.remove(&userid) {
f58e5132
WB
181 match {
182 // scope to prevent the temprary iter from borrowing across the whole match
183 let entry = tfa_id_iter(&user_data).find(|(_ty, _index, entry_id)| id == *entry_id);
184 entry.map(|(ty, index, _)| (ty, index))
185 } {
186 Some((TfaType::Recovery, _)) => {
ad5cee1d
WB
187 if let Some(recovery) = user_data.recovery() {
188 return Ok(TypedTfaInfo {
189 ty: TfaType::Recovery,
190 info: TfaInfo::recovery(recovery.created),
191 });
192 }
027ef213 193 }
f58e5132
WB
194 Some((TfaType::Totp, index)) => {
195 return Ok(TypedTfaInfo {
196 ty: TfaType::Totp,
197 // `into_iter().nth()` to *move* out of it
198 info: user_data.totp.into_iter().nth(index).unwrap().info,
199 });
027ef213 200 }
f58e5132
WB
201 Some((TfaType::Webauthn, index)) => {
202 return Ok(TypedTfaInfo {
203 ty: TfaType::Webauthn,
204 info: user_data.webauthn.into_iter().nth(index).unwrap().info,
205 });
027ef213 206 }
f58e5132
WB
207 Some((TfaType::U2f, index)) => {
208 return Ok(TypedTfaInfo {
209 ty: TfaType::U2f,
210 info: user_data.u2f.into_iter().nth(index).unwrap().info,
211 });
027ef213 212 }
f58e5132 213 None => (),
027ef213
WB
214 }
215 }
216
217 http_bail!(NOT_FOUND, "no such tfa entry: {}/{}", userid, id);
218}
219
220#[api(
221 protected: true,
222 input: {
223 properties: {
224 userid: { type: Userid },
225 id: {
226 description: "the tfa entry id",
227 },
228 password: {
229 schema: PASSWORD_SCHEMA,
230 optional: true,
231 },
232 },
233 },
234 access: {
235 permission: &Permission::Or(&[
236 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
237 &Permission::UserParam("userid"),
238 ]),
239 },
240)]
241/// Get a single TFA entry.
759af9f0 242fn delete_tfa(
027ef213
WB
243 userid: Userid,
244 id: String,
245 password: Option<String>,
246 rpcenv: &mut dyn RpcEnvironment,
247) -> Result<(), Error> {
eab25e2f 248 tfa_update_auth(rpcenv, &userid, password, false)?;
027ef213
WB
249
250 let _lock = crate::config::tfa::write_lock()?;
251
252 let mut data = crate::config::tfa::read()?;
253
254 let user_data = data
255 .users
256 .get_mut(&userid)
257 .ok_or_else(|| http_err!(NOT_FOUND, "no such entry: {}/{}", userid, id))?;
258
f58e5132
WB
259 match {
260 // scope to prevent the temprary iter from borrowing across the whole match
261 let entry = tfa_id_iter(&user_data).find(|(_, _, entry_id)| id == *entry_id);
262 entry.map(|(ty, index, _)| (ty, index))
263 } {
264 Some((TfaType::Recovery, _)) => user_data.recovery = None,
265 Some((TfaType::Totp, index)) => drop(user_data.totp.remove(index)),
266 Some((TfaType::Webauthn, index)) => drop(user_data.webauthn.remove(index)),
267 Some((TfaType::U2f, index)) => drop(user_data.u2f.remove(index)),
268 None => http_bail!(NOT_FOUND, "no such tfa entry: {}/{}", userid, id),
027ef213
WB
269 }
270
271 if user_data.is_empty() {
272 data.users.remove(&userid);
273 }
274
275 crate::config::tfa::write(&data)?;
276
277 Ok(())
278}
279
280#[api(
281 properties: {
282 "userid": { type: Userid },
283 "entries": {
284 type: Array,
285 items: { type: TypedTfaInfo },
286 },
287 },
288)]
289#[derive(Deserialize, Serialize)]
290#[serde(deny_unknown_fields)]
291/// Over the API we only provide the descriptions for TFA data.
759af9f0 292struct TfaUser {
027ef213
WB
293 /// The user this entry belongs to.
294 userid: Userid,
295
296 /// TFA entries.
297 entries: Vec<TypedTfaInfo>,
298}
299
300#[api(
301 protected: true,
302 input: {
303 properties: {},
304 },
305 access: {
306 permission: &Permission::Anybody,
307 description: "Returns all or just the logged-in user, depending on privileges.",
308 },
759af9f0
WB
309 returns: {
310 description: "The list tuples of user and TFA entries.",
311 type: Array,
312 items: { type: TfaUser }
313 },
027ef213
WB
314)]
315/// List user TFA configuration.
759af9f0 316fn list_tfa(rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<TfaUser>, Error> {
027ef213
WB
317 let authid: Authid = rpcenv.get_auth_id().unwrap().parse()?;
318 let user_info = CachedUserInfo::new()?;
319
320 let top_level_privs = user_info.lookup_privs(&authid, &["access", "users"]);
321 let top_level_allowed = (top_level_privs & PRIV_SYS_AUDIT) != 0;
322
323 let _lock = crate::config::tfa::read_lock()?;
324 let tfa_data = crate::config::tfa::read()?.users;
325
326 let mut out = Vec::<TfaUser>::new();
327 if top_level_allowed {
328 for (user, data) in tfa_data {
329 out.push(TfaUser {
330 userid: user,
331 entries: to_data(data),
332 });
333 }
334 } else {
335 if let Some(data) = { tfa_data }.remove(authid.user()) {
336 out.push(TfaUser {
337 userid: authid.into(),
338 entries: to_data(data),
339 });
340 }
341 }
342
759af9f0 343 Ok(out)
027ef213
WB
344}
345
346#[api(
347 properties: {
348 recovery: {
349 description: "A list of recovery codes as integers.",
350 type: Array,
351 items: {
352 type: Integer,
353 description: "A one-time usable recovery code entry.",
354 },
355 },
356 },
357)]
358/// The result returned when adding TFA entries to a user.
359#[derive(Default, Serialize)]
360struct TfaUpdateInfo {
361 /// The id if a newly added TFA entry.
362 id: Option<String>,
363
364 /// When adding u2f entries, this contains a challenge the user must respond to in order to
365 /// finish the registration.
366 #[serde(skip_serializing_if = "Option::is_none")]
367 challenge: Option<String>,
368
369 /// When adding recovery codes, this contains the list of codes to be displayed to the user
370 /// this one time.
371 #[serde(skip_serializing_if = "Vec::is_empty", default)]
372 recovery: Vec<String>,
373}
374
375impl TfaUpdateInfo {
376 fn id(id: String) -> Self {
377 Self {
378 id: Some(id),
379 ..Default::default()
380 }
381 }
382}
383
384#[api(
385 protected: true,
386 input: {
387 properties: {
388 userid: { type: Userid },
389 description: {
390 description: "A description to distinguish multiple entries from one another",
391 type: String,
392 max_length: 255,
393 optional: true,
394 },
395 "type": { type: TfaType },
396 totp: {
397 description: "A totp URI.",
398 optional: true,
399 },
400 value: {
401 description:
402 "The current value for the provided totp URI, or a Webauthn/U2F challenge response",
403 optional: true,
404 },
405 challenge: {
406 description: "When responding to a u2f challenge: the original challenge string",
407 optional: true,
408 },
409 password: {
410 schema: PASSWORD_SCHEMA,
411 optional: true,
412 },
413 },
414 },
415 returns: { type: TfaUpdateInfo },
416 access: {
417 permission: &Permission::Or(&[
418 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
419 &Permission::UserParam("userid"),
420 ]),
421 },
422)]
423/// Add a TFA entry to the user.
424fn add_tfa_entry(
425 userid: Userid,
426 description: Option<String>,
427 totp: Option<String>,
428 value: Option<String>,
429 challenge: Option<String>,
430 password: Option<String>,
d8318467 431 r#type: TfaType,
027ef213
WB
432 rpcenv: &mut dyn RpcEnvironment,
433) -> Result<TfaUpdateInfo, Error> {
eab25e2f 434 tfa_update_auth(rpcenv, &userid, password, true)?;
027ef213 435
027ef213
WB
436 let need_description =
437 move || description.ok_or_else(|| format_err!("'description' is required for new entries"));
438
d8318467 439 match r#type {
027ef213
WB
440 TfaType::Totp => match (totp, value) {
441 (Some(totp), Some(value)) => {
442 if challenge.is_some() {
443 bail!("'challenge' parameter is invalid for 'totp' entries");
444 }
445 let description = need_description()?;
446
447 let totp: Totp = totp.parse()?;
448 if totp
449 .verify(&value, std::time::SystemTime::now(), -1..=1)?
450 .is_none()
451 {
452 bail!("failed to verify TOTP challenge");
453 }
454 crate::config::tfa::add_totp(&userid, description, totp).map(TfaUpdateInfo::id)
455 }
456 _ => bail!("'totp' type requires both 'totp' and 'value' parameters"),
457 },
458 TfaType::Webauthn => {
459 if totp.is_some() {
460 bail!("'totp' parameter is invalid for 'totp' entries");
461 }
462
463 match challenge {
464 None => crate::config::tfa::add_webauthn_registration(&userid, need_description()?)
465 .map(|c| TfaUpdateInfo {
466 challenge: Some(c),
467 ..Default::default()
468 }),
469 Some(challenge) => {
470 let value = value.ok_or_else(|| {
471 format_err!(
472 "missing 'value' parameter (webauthn challenge response missing)"
473 )
474 })?;
475 crate::config::tfa::finish_webauthn_registration(&userid, &challenge, &value)
476 .map(TfaUpdateInfo::id)
477 }
478 }
479 }
480 TfaType::U2f => {
481 if totp.is_some() {
482 bail!("'totp' parameter is invalid for 'totp' entries");
483 }
484
485 match challenge {
486 None => crate::config::tfa::add_u2f_registration(&userid, need_description()?).map(
487 |c| TfaUpdateInfo {
488 challenge: Some(c),
489 ..Default::default()
490 },
491 ),
492 Some(challenge) => {
493 let value = value.ok_or_else(|| {
494 format_err!("missing 'value' parameter (u2f challenge response missing)")
495 })?;
496 crate::config::tfa::finish_u2f_registration(&userid, &challenge, &value)
497 .map(TfaUpdateInfo::id)
498 }
499 }
500 }
501 TfaType::Recovery => {
502 if totp.or(value).or(challenge).is_some() {
503 bail!("generating recovery tokens does not allow additional parameters");
504 }
505
506 let recovery = crate::config::tfa::add_recovery(&userid)?;
507
508 Ok(TfaUpdateInfo {
509 id: Some("recovery".to_string()),
510 recovery,
511 ..Default::default()
512 })
513 }
514 }
515}
516
517#[api(
518 protected: true,
519 input: {
520 properties: {
521 userid: { type: Userid },
522 id: {
523 description: "the tfa entry id",
524 },
525 description: {
526 description: "A description to distinguish multiple entries from one another",
527 type: String,
528 max_length: 255,
529 optional: true,
530 },
531 enable: {
532 description: "Whether this entry should currently be enabled or disabled",
533 optional: true,
534 },
535 password: {
536 schema: PASSWORD_SCHEMA,
537 optional: true,
538 },
539 },
540 },
541 access: {
542 permission: &Permission::Or(&[
543 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
544 &Permission::UserParam("userid"),
545 ]),
546 },
547)]
548/// Update user's TFA entry description.
759af9f0 549fn update_tfa_entry(
027ef213
WB
550 userid: Userid,
551 id: String,
552 description: Option<String>,
553 enable: Option<bool>,
554 password: Option<String>,
555 rpcenv: &mut dyn RpcEnvironment,
556) -> Result<(), Error> {
eab25e2f 557 tfa_update_auth(rpcenv, &userid, password, true)?;
027ef213
WB
558
559 let _lock = crate::config::tfa::write_lock()?;
560
561 let mut data = crate::config::tfa::read()?;
562
563 let mut entry = data
564 .users
565 .get_mut(&userid)
566 .and_then(|user| user.find_entry_mut(&id))
567 .ok_or_else(|| http_err!(NOT_FOUND, "no such entry: {}/{}", userid, id))?;
568
569 if let Some(description) = description {
570 entry.description = description;
571 }
572
573 if let Some(enable) = enable {
574 entry.enable = enable;
575 }
576
577 crate::config::tfa::write(&data)?;
578 Ok(())
579}
580
581pub const ROUTER: Router = Router::new()
582 .get(&API_METHOD_LIST_TFA)
583 .match_all("userid", &USER_ROUTER);
584
585const USER_ROUTER: Router = Router::new()
586 .get(&API_METHOD_LIST_USER_TFA)
587 .post(&API_METHOD_ADD_TFA_ENTRY)
588 .match_all("id", &ITEM_ROUTER);
589
590const ITEM_ROUTER: Router = Router::new()
1fc9ac04 591 .get(&API_METHOD_GET_TFA_ENTRY)
027ef213
WB
592 .put(&API_METHOD_UPDATE_TFA_ENTRY)
593 .delete(&API_METHOD_DELETE_TFA);