]>
Commit | Line | Data |
---|---|---|
bf78f708 DM |
1 | //! User Management |
2 | ||
08ac90f9 | 3 | use anyhow::{bail, format_err, Error}; |
dc7a5b34 TL |
4 | use hex::FromHex; |
5 | use serde::{Deserialize, Serialize}; | |
942078c4 | 6 | use serde_json::{json, Value}; |
6746bbb1 | 7 | use std::collections::HashMap; |
579728c6 | 8 | |
dc7a5b34 | 9 | use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment, SubdirMap}; |
6ef1b649 | 10 | use proxmox_schema::api; |
9b85fdc7 | 11 | use proxmox_tfa::api::TfaConfig; |
579728c6 | 12 | |
2b7f8dd5 | 13 | use pbs_api_types::{ |
dc7a5b34 TL |
14 | ApiToken, Authid, Tokenname, User, UserUpdater, UserWithTokens, Userid, ENABLE_USER_SCHEMA, |
15 | EXPIRE_USER_SCHEMA, PBS_PASSWORD_SCHEMA, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT, | |
16 | PROXMOX_CONFIG_DIGEST_SCHEMA, SINGLE_LINE_COMMENT_SCHEMA, | |
2b7f8dd5 | 17 | }; |
1cb08a0a | 18 | use pbs_config::token_shadow; |
2b7f8dd5 | 19 | |
ba3d7e19 | 20 | use pbs_config::CachedUserInfo; |
579728c6 | 21 | |
9b85fdc7 | 22 | fn new_user_with_tokens(user: User, tfa: &TfaConfig) -> UserWithTokens { |
2b7f8dd5 | 23 | UserWithTokens { |
9b85fdc7 WB |
24 | totp_locked: tfa |
25 | .users | |
26 | .get(user.userid.as_str()) | |
27 | .map(|data| data.totp_locked) | |
28 | .unwrap_or(false), | |
29 | tfa_locked_until: tfa | |
30 | .users | |
31 | .get(user.userid.as_str()) | |
32 | .and_then(|data| data.tfa_locked_until), | |
2b7f8dd5 WB |
33 | userid: user.userid, |
34 | comment: user.comment, | |
35 | enable: user.enable, | |
36 | expire: user.expire, | |
37 | firstname: user.firstname, | |
38 | lastname: user.lastname, | |
39 | email: user.email, | |
40 | tokens: Vec::new(), | |
6746bbb1 FG |
41 | } |
42 | } | |
43 | ||
579728c6 | 44 | #[api( |
9b85fdc7 | 45 | protected: true, |
579728c6 | 46 | input: { |
6746bbb1 FG |
47 | properties: { |
48 | include_tokens: { | |
49 | type: bool, | |
50 | description: "Include user's API tokens in returned list.", | |
51 | optional: true, | |
52 | default: false, | |
53 | }, | |
54 | }, | |
579728c6 DM |
55 | }, |
56 | returns: { | |
57 | description: "List users (with config digest).", | |
58 | type: Array, | |
906ef6c5 | 59 | items: { type: UserWithTokens }, |
579728c6 | 60 | }, |
d4f020f4 | 61 | access: { |
be3bd0f9 | 62 | permission: &Permission::Anybody, |
08ac90f9 | 63 | description: "Returns all or just the logged-in user (/API token owner), depending on privileges.", |
d4f020f4 | 64 | }, |
579728c6 | 65 | )] |
be3bd0f9 | 66 | /// List users |
579728c6 | 67 | pub fn list_users( |
6746bbb1 | 68 | include_tokens: bool, |
579728c6 | 69 | _info: &ApiMethod, |
41c1a179 | 70 | rpcenv: &mut dyn RpcEnvironment, |
6746bbb1 | 71 | ) -> Result<Vec<UserWithTokens>, Error> { |
ba3d7e19 | 72 | let (config, digest) = pbs_config::user::config()?; |
579728c6 | 73 | |
08ac90f9 FG |
74 | let auth_id: Authid = rpcenv |
75 | .get_auth_id() | |
76 | .ok_or_else(|| format_err!("no authid available"))? | |
77 | .parse()?; | |
78 | ||
79 | let userid = auth_id.user(); | |
e6dc35ac | 80 | |
be3bd0f9 FG |
81 | let user_info = CachedUserInfo::new()?; |
82 | ||
e6dc35ac | 83 | let top_level_privs = user_info.lookup_privs(&auth_id, &["access", "users"]); |
be3bd0f9 FG |
84 | let top_level_allowed = (top_level_privs & PRIV_SYS_AUDIT) != 0; |
85 | ||
dc7a5b34 | 86 | let filter_by_privs = |user: &User| top_level_allowed || user.userid == *userid; |
6746bbb1 | 87 | |
dc7a5b34 | 88 | let list: Vec<User> = config.convert_to_typed_array("user")?; |
522c0da0 | 89 | |
16f6766a | 90 | rpcenv["digest"] = hex::encode(digest).into(); |
579728c6 | 91 | |
9b85fdc7 WB |
92 | let tfa_data = crate::config::tfa::read()?; |
93 | ||
6746bbb1 FG |
94 | let iter = list.into_iter().filter(filter_by_privs); |
95 | let list = if include_tokens { | |
b65dfff5 | 96 | let tokens: Vec<ApiToken> = config.convert_to_typed_array("token")?; |
dc7a5b34 TL |
97 | let mut user_to_tokens = tokens.into_iter().fold( |
98 | HashMap::new(), | |
99 | |mut map: HashMap<Userid, Vec<ApiToken>>, token: ApiToken| { | |
6746bbb1 | 100 | if token.tokenid.is_token() { |
dc7a5b34 | 101 | map.entry(token.tokenid.user().clone()) |
6746bbb1 FG |
102 | .or_default() |
103 | .push(token); | |
104 | } | |
105 | map | |
dc7a5b34 TL |
106 | }, |
107 | ); | |
108 | iter.map(|user: User| { | |
9b85fdc7 | 109 | let mut user = new_user_with_tokens(user, &tfa_data); |
dc7a5b34 TL |
110 | user.tokens = user_to_tokens.remove(&user.userid).unwrap_or_default(); |
111 | user | |
112 | }) | |
113 | .collect() | |
6746bbb1 | 114 | } else { |
9b85fdc7 WB |
115 | iter.map(|user: User| new_user_with_tokens(user, &tfa_data)) |
116 | .collect() | |
6746bbb1 FG |
117 | }; |
118 | ||
119 | Ok(list) | |
579728c6 DM |
120 | } |
121 | ||
122 | #[api( | |
123 | protected: true, | |
124 | input: { | |
125 | properties: { | |
b65dfff5 DM |
126 | config: { |
127 | type: User, | |
128 | flatten: true, | |
579728c6 DM |
129 | }, |
130 | password: { | |
131 | schema: PBS_PASSWORD_SCHEMA, | |
132 | optional: true, | |
133 | }, | |
579728c6 DM |
134 | }, |
135 | }, | |
d4f020f4 | 136 | access: { |
74c08a57 | 137 | permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), |
d4f020f4 | 138 | }, |
579728c6 DM |
139 | )] |
140 | /// Create new user. | |
5aa10190 OB |
141 | pub fn create_user( |
142 | password: Option<String>, | |
b65dfff5 | 143 | config: User, |
dc7a5b34 | 144 | rpcenv: &mut dyn RpcEnvironment, |
5aa10190 | 145 | ) -> Result<(), Error> { |
ba3d7e19 | 146 | let _lock = pbs_config::user::lock_config()?; |
579728c6 | 147 | |
ba3d7e19 | 148 | let (mut section_config, _digest) = pbs_config::user::config()?; |
579728c6 | 149 | |
dc7a5b34 TL |
150 | if section_config |
151 | .sections | |
152 | .get(config.userid.as_str()) | |
153 | .is_some() | |
154 | { | |
b65dfff5 | 155 | bail!("user '{}' already exists.", config.userid); |
579728c6 DM |
156 | } |
157 | ||
b65dfff5 | 158 | section_config.set_data(config.userid.as_str(), "user", &config)?; |
579728c6 | 159 | |
b65dfff5 | 160 | let realm = config.userid.realm(); |
5aa10190 OB |
161 | |
162 | // Fails if realm does not exist! | |
163 | let authenticator = crate::auth::lookup_authenticator(realm)?; | |
164 | ||
ba3d7e19 | 165 | pbs_config::user::save_config(§ion_config)?; |
579728c6 | 166 | |
7d817b03 | 167 | if let Some(password) = password { |
5aa10190 OB |
168 | let user_info = CachedUserInfo::new()?; |
169 | let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
170 | if realm == "pam" && !user_info.is_superuser(¤t_auth_id) { | |
171 | bail!("only superuser can edit pam credentials!"); | |
172 | } | |
177ee20b WB |
173 | let client_ip = rpcenv.get_client_ip().map(|sa| sa.ip()); |
174 | authenticator.store_password(config.userid.name(), &password, client_ip.as_ref())?; | |
7d817b03 DM |
175 | } |
176 | ||
579728c6 DM |
177 | Ok(()) |
178 | } | |
179 | ||
180 | #[api( | |
181 | input: { | |
182 | properties: { | |
183 | userid: { | |
e7cb4dc5 | 184 | type: Userid, |
579728c6 | 185 | }, |
685e1334 | 186 | }, |
579728c6 | 187 | }, |
b65dfff5 | 188 | returns: { type: User }, |
d4f020f4 | 189 | access: { |
be3bd0f9 FG |
190 | permission: &Permission::Or(&[ |
191 | &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), | |
192 | &Permission::UserParam("userid"), | |
193 | ]), | |
d4f020f4 | 194 | }, |
579728c6 DM |
195 | )] |
196 | /// Read user configuration data. | |
41c1a179 | 197 | pub fn read_user(userid: Userid, rpcenv: &mut dyn RpcEnvironment) -> Result<User, Error> { |
ba3d7e19 | 198 | let (config, digest) = pbs_config::user::config()?; |
e7cb4dc5 | 199 | let user = config.lookup("user", userid.as_str())?; |
16f6766a | 200 | rpcenv["digest"] = hex::encode(digest).into(); |
522c0da0 | 201 | Ok(user) |
579728c6 DM |
202 | } |
203 | ||
c0026563 DC |
204 | #[api()] |
205 | #[derive(Serialize, Deserialize)] | |
dc7a5b34 | 206 | #[serde(rename_all = "kebab-case")] |
c0026563 DC |
207 | pub enum DeletableProperty { |
208 | /// Delete the comment property. | |
a2055c38 | 209 | Comment, |
c0026563 | 210 | /// Delete the firstname property. |
a2055c38 | 211 | Firstname, |
c0026563 | 212 | /// Delete the lastname property. |
a2055c38 | 213 | Lastname, |
c0026563 | 214 | /// Delete the email property. |
a2055c38 | 215 | Email, |
c0026563 DC |
216 | } |
217 | ||
579728c6 DM |
218 | #[api( |
219 | protected: true, | |
220 | input: { | |
221 | properties: { | |
222 | userid: { | |
e7cb4dc5 | 223 | type: Userid, |
579728c6 | 224 | }, |
b65dfff5 DM |
225 | update: { |
226 | type: UserUpdater, | |
227 | flatten: true, | |
579728c6 DM |
228 | }, |
229 | password: { | |
230 | schema: PBS_PASSWORD_SCHEMA, | |
231 | optional: true, | |
232 | }, | |
c0026563 DC |
233 | delete: { |
234 | description: "List of properties to delete.", | |
235 | type: Array, | |
236 | optional: true, | |
237 | items: { | |
238 | type: DeletableProperty, | |
239 | } | |
240 | }, | |
579728c6 DM |
241 | digest: { |
242 | optional: true, | |
243 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
244 | }, | |
245 | }, | |
246 | }, | |
d4f020f4 | 247 | access: { |
be3bd0f9 FG |
248 | permission: &Permission::Or(&[ |
249 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
250 | &Permission::UserParam("userid"), | |
251 | ]), | |
d4f020f4 | 252 | }, |
579728c6 DM |
253 | )] |
254 | /// Update user configuration. | |
367c0ff7 | 255 | #[allow(clippy::too_many_arguments)] |
28b9f84e | 256 | pub async fn update_user( |
e7cb4dc5 | 257 | userid: Userid, |
b65dfff5 | 258 | update: UserUpdater, |
579728c6 | 259 | password: Option<String>, |
c0026563 | 260 | delete: Option<Vec<DeletableProperty>>, |
579728c6 | 261 | digest: Option<String>, |
5aa10190 | 262 | rpcenv: &mut dyn RpcEnvironment, |
579728c6 | 263 | ) -> Result<(), Error> { |
28b9f84e WB |
264 | if password.is_some() { |
265 | super::user_update_auth(rpcenv, &userid, password.as_deref(), false).await?; | |
266 | } | |
267 | ||
ba3d7e19 | 268 | let _lock = pbs_config::user::lock_config()?; |
579728c6 | 269 | |
ba3d7e19 | 270 | let (mut config, expected_digest) = pbs_config::user::config()?; |
579728c6 DM |
271 | |
272 | if let Some(ref digest) = digest { | |
25877d05 | 273 | let digest = <[u8; 32]>::from_hex(digest)?; |
579728c6 DM |
274 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
275 | } | |
276 | ||
b65dfff5 | 277 | let mut data: User = config.lookup("user", userid.as_str())?; |
579728c6 | 278 | |
c0026563 DC |
279 | if let Some(delete) = delete { |
280 | for delete_prop in delete { | |
281 | match delete_prop { | |
a2055c38 WB |
282 | DeletableProperty::Comment => data.comment = None, |
283 | DeletableProperty::Firstname => data.firstname = None, | |
284 | DeletableProperty::Lastname => data.lastname = None, | |
285 | DeletableProperty::Email => data.email = None, | |
c0026563 DC |
286 | } |
287 | } | |
288 | } | |
289 | ||
b65dfff5 | 290 | if let Some(comment) = update.comment { |
579728c6 DM |
291 | let comment = comment.trim().to_string(); |
292 | if comment.is_empty() { | |
293 | data.comment = None; | |
294 | } else { | |
295 | data.comment = Some(comment); | |
296 | } | |
297 | } | |
298 | ||
b65dfff5 | 299 | if let Some(enable) = update.enable { |
579728c6 DM |
300 | data.enable = if enable { None } else { Some(false) }; |
301 | } | |
302 | ||
b65dfff5 | 303 | if let Some(expire) = update.expire { |
579728c6 DM |
304 | data.expire = if expire > 0 { Some(expire) } else { None }; |
305 | } | |
306 | ||
307 | if let Some(password) = password { | |
5aa10190 OB |
308 | let user_info = CachedUserInfo::new()?; |
309 | let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
310 | let self_service = current_auth_id.user() == &userid; | |
311 | let target_realm = userid.realm(); | |
312 | if !self_service && target_realm == "pam" && !user_info.is_superuser(¤t_auth_id) { | |
313 | bail!("only superuser can edit pam credentials!"); | |
314 | } | |
e7cb4dc5 | 315 | let authenticator = crate::auth::lookup_authenticator(userid.realm())?; |
177ee20b WB |
316 | let client_ip = rpcenv.get_client_ip().map(|sa| sa.ip()); |
317 | authenticator.store_password(userid.name(), &password, client_ip.as_ref())?; | |
579728c6 DM |
318 | } |
319 | ||
b65dfff5 | 320 | if let Some(firstname) = update.firstname { |
dc7a5b34 TL |
321 | data.firstname = if firstname.is_empty() { |
322 | None | |
323 | } else { | |
324 | Some(firstname) | |
325 | }; | |
579728c6 DM |
326 | } |
327 | ||
b65dfff5 | 328 | if let Some(lastname) = update.lastname { |
dc7a5b34 TL |
329 | data.lastname = if lastname.is_empty() { |
330 | None | |
331 | } else { | |
332 | Some(lastname) | |
333 | }; | |
579728c6 | 334 | } |
b65dfff5 | 335 | if let Some(email) = update.email { |
579728c6 DM |
336 | data.email = if email.is_empty() { None } else { Some(email) }; |
337 | } | |
338 | ||
e7cb4dc5 | 339 | config.set_data(userid.as_str(), "user", &data)?; |
579728c6 | 340 | |
ba3d7e19 | 341 | pbs_config::user::save_config(&config)?; |
579728c6 DM |
342 | |
343 | Ok(()) | |
344 | } | |
345 | ||
346 | #[api( | |
347 | protected: true, | |
348 | input: { | |
349 | properties: { | |
350 | userid: { | |
e7cb4dc5 | 351 | type: Userid, |
579728c6 DM |
352 | }, |
353 | digest: { | |
354 | optional: true, | |
355 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
356 | }, | |
357 | }, | |
358 | }, | |
d4f020f4 | 359 | access: { |
be3bd0f9 FG |
360 | permission: &Permission::Or(&[ |
361 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
362 | &Permission::UserParam("userid"), | |
363 | ]), | |
d4f020f4 | 364 | }, |
579728c6 DM |
365 | )] |
366 | /// Remove a user from the configuration file. | |
e7cb4dc5 | 367 | pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> { |
ba3d7e19 | 368 | let _lock = pbs_config::user::lock_config()?; |
f22dfb5e | 369 | let _tfa_lock = crate::config::tfa::write_lock()?; |
dc7a5b34 | 370 | |
ba3d7e19 | 371 | let (mut config, expected_digest) = pbs_config::user::config()?; |
579728c6 DM |
372 | |
373 | if let Some(ref digest) = digest { | |
25877d05 | 374 | let digest = <[u8; 32]>::from_hex(digest)?; |
579728c6 DM |
375 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
376 | } | |
377 | ||
e7cb4dc5 | 378 | match config.sections.get(userid.as_str()) { |
dc7a5b34 TL |
379 | Some(_) => { |
380 | config.sections.remove(userid.as_str()); | |
381 | } | |
579728c6 DM |
382 | None => bail!("user '{}' does not exist.", userid), |
383 | } | |
384 | ||
ba3d7e19 | 385 | pbs_config::user::save_config(&config)?; |
579728c6 | 386 | |
a4e871f5 | 387 | let authenticator = crate::auth::lookup_authenticator(userid.realm())?; |
6aff2de5 MS |
388 | if let Err(err) = authenticator.remove_password(userid.name()) { |
389 | eprintln!("error removing password after deleting user {userid:?}: {err}",); | |
a4e871f5 DC |
390 | } |
391 | ||
6aff2de5 | 392 | if let Err(err) = crate::config::tfa::read().and_then(|mut cfg| { |
9407810f | 393 | let _: proxmox_tfa::api::NeedsSaving = |
d97ff8ae | 394 | cfg.remove_user(&crate::config::tfa::UserAccess, userid.as_str())?; |
f22dfb5e WB |
395 | crate::config::tfa::write(&cfg) |
396 | }) { | |
6aff2de5 | 397 | eprintln!("error updating TFA config after deleting user {userid:?} {err}",); |
f22dfb5e WB |
398 | } |
399 | ||
579728c6 DM |
400 | Ok(()) |
401 | } | |
402 | ||
942078c4 FG |
403 | #[api( |
404 | input: { | |
405 | properties: { | |
406 | userid: { | |
407 | type: Userid, | |
408 | }, | |
f54634a8 | 409 | "token-name": { |
942078c4 FG |
410 | type: Tokenname, |
411 | }, | |
412 | }, | |
413 | }, | |
b65dfff5 | 414 | returns: { type: ApiToken }, |
942078c4 FG |
415 | access: { |
416 | permission: &Permission::Or(&[ | |
417 | &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), | |
418 | &Permission::UserParam("userid"), | |
419 | ]), | |
420 | }, | |
421 | )] | |
422 | /// Read user's API token metadata | |
423 | pub fn read_token( | |
424 | userid: Userid, | |
f54634a8 | 425 | token_name: Tokenname, |
942078c4 | 426 | _info: &ApiMethod, |
41c1a179 | 427 | rpcenv: &mut dyn RpcEnvironment, |
b65dfff5 | 428 | ) -> Result<ApiToken, Error> { |
ba3d7e19 | 429 | let (config, digest) = pbs_config::user::config()?; |
942078c4 | 430 | |
f54634a8 | 431 | let tokenid = Authid::from((userid, Some(token_name))); |
942078c4 | 432 | |
16f6766a | 433 | rpcenv["digest"] = hex::encode(digest).into(); |
942078c4 FG |
434 | config.lookup("token", &tokenid.to_string()) |
435 | } | |
436 | ||
437 | #[api( | |
438 | protected: true, | |
439 | input: { | |
440 | properties: { | |
441 | userid: { | |
442 | type: Userid, | |
443 | }, | |
f54634a8 | 444 | "token-name": { |
942078c4 FG |
445 | type: Tokenname, |
446 | }, | |
447 | comment: { | |
448 | optional: true, | |
449 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
450 | }, | |
451 | enable: { | |
b65dfff5 | 452 | schema: ENABLE_USER_SCHEMA, |
942078c4 FG |
453 | optional: true, |
454 | }, | |
455 | expire: { | |
b65dfff5 | 456 | schema: EXPIRE_USER_SCHEMA, |
942078c4 FG |
457 | optional: true, |
458 | }, | |
459 | digest: { | |
460 | optional: true, | |
461 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
462 | }, | |
463 | }, | |
464 | }, | |
465 | access: { | |
466 | permission: &Permission::Or(&[ | |
467 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
468 | &Permission::UserParam("userid"), | |
469 | ]), | |
470 | }, | |
471 | returns: { | |
472 | description: "API token identifier + generated secret.", | |
473 | properties: { | |
474 | value: { | |
475 | type: String, | |
476 | description: "The API token secret", | |
477 | }, | |
478 | tokenid: { | |
479 | type: String, | |
480 | description: "The API token identifier", | |
481 | }, | |
482 | }, | |
483 | }, | |
484 | )] | |
485 | /// Generate a new API token with given metadata | |
486 | pub fn generate_token( | |
487 | userid: Userid, | |
f54634a8 | 488 | token_name: Tokenname, |
942078c4 FG |
489 | comment: Option<String>, |
490 | enable: Option<bool>, | |
491 | expire: Option<i64>, | |
492 | digest: Option<String>, | |
493 | ) -> Result<Value, Error> { | |
ba3d7e19 | 494 | let _lock = pbs_config::user::lock_config()?; |
942078c4 | 495 | |
ba3d7e19 | 496 | let (mut config, expected_digest) = pbs_config::user::config()?; |
942078c4 FG |
497 | |
498 | if let Some(ref digest) = digest { | |
25877d05 | 499 | let digest = <[u8; 32]>::from_hex(digest)?; |
942078c4 FG |
500 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
501 | } | |
502 | ||
f54634a8 | 503 | let tokenid = Authid::from((userid.clone(), Some(token_name.clone()))); |
942078c4 FG |
504 | let tokenid_string = tokenid.to_string(); |
505 | ||
3984a5fd | 506 | if config.sections.get(&tokenid_string).is_some() { |
dc7a5b34 TL |
507 | bail!( |
508 | "token '{}' for user '{}' already exists.", | |
509 | token_name.as_str(), | |
510 | userid | |
511 | ); | |
942078c4 FG |
512 | } |
513 | ||
6ef1b649 | 514 | let secret = format!("{:x}", proxmox_uuid::Uuid::generate()); |
942078c4 FG |
515 | token_shadow::set_secret(&tokenid, &secret)?; |
516 | ||
b65dfff5 | 517 | let token = ApiToken { |
44288184 | 518 | tokenid, |
942078c4 FG |
519 | comment, |
520 | enable, | |
521 | expire, | |
522 | }; | |
523 | ||
524 | config.set_data(&tokenid_string, "token", &token)?; | |
525 | ||
ba3d7e19 | 526 | pbs_config::user::save_config(&config)?; |
942078c4 FG |
527 | |
528 | Ok(json!({ | |
529 | "tokenid": tokenid_string, | |
530 | "value": secret | |
531 | })) | |
532 | } | |
533 | ||
534 | #[api( | |
535 | protected: true, | |
536 | input: { | |
537 | properties: { | |
538 | userid: { | |
539 | type: Userid, | |
540 | }, | |
f54634a8 | 541 | "token-name": { |
942078c4 FG |
542 | type: Tokenname, |
543 | }, | |
544 | comment: { | |
545 | optional: true, | |
546 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
547 | }, | |
548 | enable: { | |
b65dfff5 | 549 | schema: ENABLE_USER_SCHEMA, |
942078c4 FG |
550 | optional: true, |
551 | }, | |
552 | expire: { | |
b65dfff5 | 553 | schema: EXPIRE_USER_SCHEMA, |
942078c4 FG |
554 | optional: true, |
555 | }, | |
556 | digest: { | |
557 | optional: true, | |
558 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
559 | }, | |
560 | }, | |
561 | }, | |
562 | access: { | |
563 | permission: &Permission::Or(&[ | |
564 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
565 | &Permission::UserParam("userid"), | |
566 | ]), | |
567 | }, | |
568 | )] | |
569 | /// Update user's API token metadata | |
570 | pub fn update_token( | |
571 | userid: Userid, | |
f54634a8 | 572 | token_name: Tokenname, |
942078c4 FG |
573 | comment: Option<String>, |
574 | enable: Option<bool>, | |
575 | expire: Option<i64>, | |
576 | digest: Option<String>, | |
577 | ) -> Result<(), Error> { | |
ba3d7e19 | 578 | let _lock = pbs_config::user::lock_config()?; |
942078c4 | 579 | |
ba3d7e19 | 580 | let (mut config, expected_digest) = pbs_config::user::config()?; |
942078c4 FG |
581 | |
582 | if let Some(ref digest) = digest { | |
25877d05 | 583 | let digest = <[u8; 32]>::from_hex(digest)?; |
942078c4 FG |
584 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
585 | } | |
586 | ||
f54634a8 | 587 | let tokenid = Authid::from((userid, Some(token_name))); |
942078c4 FG |
588 | let tokenid_string = tokenid.to_string(); |
589 | ||
b65dfff5 | 590 | let mut data: ApiToken = config.lookup("token", &tokenid_string)?; |
942078c4 FG |
591 | |
592 | if let Some(comment) = comment { | |
593 | let comment = comment.trim().to_string(); | |
594 | if comment.is_empty() { | |
595 | data.comment = None; | |
596 | } else { | |
597 | data.comment = Some(comment); | |
598 | } | |
599 | } | |
600 | ||
601 | if let Some(enable) = enable { | |
602 | data.enable = if enable { None } else { Some(false) }; | |
603 | } | |
604 | ||
605 | if let Some(expire) = expire { | |
606 | data.expire = if expire > 0 { Some(expire) } else { None }; | |
607 | } | |
608 | ||
609 | config.set_data(&tokenid_string, "token", &data)?; | |
610 | ||
ba3d7e19 | 611 | pbs_config::user::save_config(&config)?; |
942078c4 FG |
612 | |
613 | Ok(()) | |
614 | } | |
615 | ||
616 | #[api( | |
617 | protected: true, | |
618 | input: { | |
619 | properties: { | |
620 | userid: { | |
621 | type: Userid, | |
622 | }, | |
f54634a8 | 623 | "token-name": { |
942078c4 FG |
624 | type: Tokenname, |
625 | }, | |
626 | digest: { | |
627 | optional: true, | |
628 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
629 | }, | |
630 | }, | |
631 | }, | |
632 | access: { | |
633 | permission: &Permission::Or(&[ | |
634 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
635 | &Permission::UserParam("userid"), | |
636 | ]), | |
637 | }, | |
638 | )] | |
639 | /// Delete a user's API token | |
640 | pub fn delete_token( | |
641 | userid: Userid, | |
f54634a8 | 642 | token_name: Tokenname, |
942078c4 FG |
643 | digest: Option<String>, |
644 | ) -> Result<(), Error> { | |
ba3d7e19 | 645 | let _lock = pbs_config::user::lock_config()?; |
942078c4 | 646 | |
ba3d7e19 | 647 | let (mut config, expected_digest) = pbs_config::user::config()?; |
942078c4 FG |
648 | |
649 | if let Some(ref digest) = digest { | |
25877d05 | 650 | let digest = <[u8; 32]>::from_hex(digest)?; |
942078c4 FG |
651 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; |
652 | } | |
653 | ||
f54634a8 | 654 | let tokenid = Authid::from((userid.clone(), Some(token_name.clone()))); |
942078c4 FG |
655 | let tokenid_string = tokenid.to_string(); |
656 | ||
657 | match config.sections.get(&tokenid_string) { | |
dc7a5b34 TL |
658 | Some(_) => { |
659 | config.sections.remove(&tokenid_string); | |
660 | } | |
661 | None => bail!( | |
662 | "token '{}' of user '{}' does not exist.", | |
663 | token_name.as_str(), | |
664 | userid | |
665 | ), | |
942078c4 FG |
666 | } |
667 | ||
668 | token_shadow::delete_secret(&tokenid)?; | |
669 | ||
ba3d7e19 | 670 | pbs_config::user::save_config(&config)?; |
942078c4 FG |
671 | |
672 | Ok(()) | |
673 | } | |
674 | ||
f54634a8 DC |
675 | #[api( |
676 | properties: { | |
677 | "token-name": { type: Tokenname }, | |
678 | token: { type: ApiToken }, | |
679 | } | |
680 | )] | |
681 | #[derive(Serialize, Deserialize)] | |
dc7a5b34 | 682 | #[serde(rename_all = "kebab-case")] |
f54634a8 DC |
683 | /// A Token Entry that contains the token-name |
684 | pub struct TokenApiEntry { | |
685 | /// The Token name | |
686 | pub token_name: Tokenname, | |
687 | #[serde(flatten)] | |
688 | pub token: ApiToken, | |
689 | } | |
690 | ||
942078c4 FG |
691 | #[api( |
692 | input: { | |
693 | properties: { | |
694 | userid: { | |
695 | type: Userid, | |
696 | }, | |
697 | }, | |
698 | }, | |
699 | returns: { | |
700 | description: "List user's API tokens (with config digest).", | |
701 | type: Array, | |
f54634a8 | 702 | items: { type: TokenApiEntry }, |
942078c4 FG |
703 | }, |
704 | access: { | |
705 | permission: &Permission::Or(&[ | |
706 | &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), | |
707 | &Permission::UserParam("userid"), | |
708 | ]), | |
709 | }, | |
710 | )] | |
711 | /// List user's API tokens | |
712 | pub fn list_tokens( | |
713 | userid: Userid, | |
714 | _info: &ApiMethod, | |
41c1a179 | 715 | rpcenv: &mut dyn RpcEnvironment, |
f54634a8 | 716 | ) -> Result<Vec<TokenApiEntry>, Error> { |
ba3d7e19 | 717 | let (config, digest) = pbs_config::user::config()?; |
942078c4 | 718 | |
dc7a5b34 | 719 | let list: Vec<ApiToken> = config.convert_to_typed_array("token")?; |
942078c4 | 720 | |
16f6766a | 721 | rpcenv["digest"] = hex::encode(digest).into(); |
942078c4 | 722 | |
f54634a8 DC |
723 | let filter_by_owner = |token: ApiToken| { |
724 | if token.tokenid.is_token() && token.tokenid.user() == &userid { | |
725 | let token_name = token.tokenid.tokenname().unwrap().to_owned(); | |
dc7a5b34 | 726 | Some(TokenApiEntry { token_name, token }) |
942078c4 | 727 | } else { |
f54634a8 | 728 | None |
942078c4 FG |
729 | } |
730 | }; | |
731 | ||
f54634a8 DC |
732 | let res = list.into_iter().filter_map(filter_by_owner).collect(); |
733 | ||
734 | Ok(res) | |
942078c4 FG |
735 | } |
736 | ||
8c7b80e4 WB |
737 | #[api( |
738 | protected: true, | |
739 | input: { | |
740 | properties: { | |
741 | userid: { | |
742 | type: Userid, | |
743 | }, | |
744 | }, | |
745 | }, | |
746 | returns: { | |
747 | type: bool, | |
748 | description: "Whether the user was previously locked out of any 2nd factor.", | |
749 | }, | |
750 | access: { | |
751 | permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
752 | }, | |
753 | )] | |
754 | /// Unlock a user's TFA authentication. | |
755 | pub fn unlock_tfa(userid: Userid) -> Result<bool, Error> { | |
756 | let _lock = crate::config::tfa::write_lock()?; | |
757 | ||
758 | let mut config = crate::config::tfa::read()?; | |
3e35b972 WB |
759 | if proxmox_tfa::api::methods::unlock_and_reset_tfa( |
760 | &mut config, | |
761 | &crate::config::tfa::UserAccess, | |
762 | userid.as_str(), | |
763 | )? { | |
8c7b80e4 WB |
764 | crate::config::tfa::write(&config)?; |
765 | Ok(true) | |
766 | } else { | |
767 | Ok(false) | |
768 | } | |
769 | } | |
770 | ||
942078c4 FG |
771 | const TOKEN_ITEM_ROUTER: Router = Router::new() |
772 | .get(&API_METHOD_READ_TOKEN) | |
773 | .put(&API_METHOD_UPDATE_TOKEN) | |
774 | .post(&API_METHOD_GENERATE_TOKEN) | |
775 | .delete(&API_METHOD_DELETE_TOKEN); | |
776 | ||
777 | const TOKEN_ROUTER: Router = Router::new() | |
778 | .get(&API_METHOD_LIST_TOKENS) | |
f54634a8 | 779 | .match_all("token-name", &TOKEN_ITEM_ROUTER); |
942078c4 | 780 | |
8c7b80e4 WB |
781 | const UNLOCK_TFA_ROUTER: Router = Router::new().put(&API_METHOD_UNLOCK_TFA); |
782 | ||
783 | const USER_SUBDIRS: SubdirMap = &[("token", &TOKEN_ROUTER), ("unlock-tfa", &UNLOCK_TFA_ROUTER)]; | |
942078c4 FG |
784 | |
785 | const USER_ROUTER: Router = Router::new() | |
579728c6 DM |
786 | .get(&API_METHOD_READ_USER) |
787 | .put(&API_METHOD_UPDATE_USER) | |
942078c4 FG |
788 | .delete(&API_METHOD_DELETE_USER) |
789 | .subdirs(USER_SUBDIRS); | |
579728c6 DM |
790 | |
791 | pub const ROUTER: Router = Router::new() | |
792 | .get(&API_METHOD_LIST_USERS) | |
793 | .post(&API_METHOD_CREATE_USER) | |
942078c4 | 794 | .match_all("userid", &USER_ROUTER); |