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