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