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