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