]>
Commit | Line | Data |
---|---|---|
5f0965ed | 1 | use anyhow::{bail, format_err, Context, Error}; |
73757fe2 | 2 | use pbs_config::{acl::AclTree, token_shadow, BackupLockGuard}; |
cf4ff8a7 | 3 | use proxmox_lang::try_block; |
73757fe2 LW |
4 | use proxmox_ldap::{Config, Connection, SearchParameters, SearchResult}; |
5 | use proxmox_rest_server::WorkerTask; | |
cf4ff8a7 | 6 | use proxmox_schema::{ApiType, Schema}; |
73757fe2 | 7 | use proxmox_section_config::SectionConfigData; |
cf4ff8a7 | 8 | use proxmox_sys::{task_log, task_warn}; |
73757fe2 LW |
9 | |
10 | use std::{collections::HashSet, sync::Arc}; | |
11 | ||
12 | use pbs_api_types::{ | |
13 | ApiToken, Authid, LdapRealmConfig, Realm, RemoveVanished, SyncAttributes as LdapSyncAttributes, | |
cf4ff8a7 LW |
14 | SyncDefaultsOptions, User, Userid, EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA, |
15 | REMOVE_VANISHED_ARRAY, USER_CLASSES_ARRAY, | |
73757fe2 LW |
16 | }; |
17 | ||
18 | use crate::{auth, server::jobstate::Job}; | |
19 | ||
20 | /// Runs a realm sync job | |
21 | #[allow(clippy::too_many_arguments)] | |
22 | pub fn do_realm_sync_job( | |
23 | mut job: Job, | |
24 | realm: Realm, | |
25 | auth_id: &Authid, | |
26 | _schedule: Option<String>, | |
27 | to_stdout: bool, | |
28 | dry_run: bool, | |
29 | remove_vanished: Option<String>, | |
30 | enable_new: Option<bool>, | |
31 | ) -> Result<String, Error> { | |
32 | let worker_type = job.jobtype().to_string(); | |
33 | let upid_str = WorkerTask::spawn( | |
34 | &worker_type, | |
35 | Some(realm.as_str().to_owned()), | |
36 | auth_id.to_string(), | |
37 | to_stdout, | |
38 | move |worker| { | |
39 | job.start(&worker.upid().to_string()).unwrap(); | |
40 | ||
41 | task_log!(worker, "starting realm sync for {}", realm.as_str()); | |
42 | ||
43 | let override_settings = GeneralSyncSettingsOverride { | |
44 | remove_vanished, | |
45 | enable_new, | |
46 | }; | |
47 | ||
48 | async move { | |
49 | let sync_job = LdapRealmSyncJob::new(worker, realm, &override_settings, dry_run)?; | |
50 | sync_job.sync().await | |
51 | } | |
52 | }, | |
53 | )?; | |
54 | ||
55 | Ok(upid_str) | |
56 | } | |
57 | ||
6685122c | 58 | /// Implementation for syncing LDAP realms |
73757fe2 LW |
59 | struct LdapRealmSyncJob { |
60 | worker: Arc<WorkerTask>, | |
61 | realm: Realm, | |
62 | general_sync_settings: GeneralSyncSettings, | |
63 | ldap_sync_settings: LdapSyncSettings, | |
64 | ldap_config: Config, | |
65 | dry_run: bool, | |
66 | } | |
67 | ||
68 | impl LdapRealmSyncJob { | |
69 | /// Create new LdapRealmSyncJob | |
70 | fn new( | |
71 | worker: Arc<WorkerTask>, | |
72 | realm: Realm, | |
73 | override_settings: &GeneralSyncSettingsOverride, | |
74 | dry_run: bool, | |
75 | ) -> Result<Self, Error> { | |
76 | let (domains, _digest) = pbs_config::domains::config()?; | |
77 | let config = if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm.as_str()) { | |
78 | config | |
79 | } else { | |
80 | bail!("unknown realm '{}'", realm.as_str()); | |
81 | }; | |
82 | ||
83 | let sync_settings = GeneralSyncSettings::default() | |
132e9722 | 84 | .apply_config(config.sync_defaults_options.as_deref())? |
73757fe2 | 85 | .apply_override(override_settings)?; |
132e9722 CH |
86 | let sync_attributes = LdapSyncSettings::new( |
87 | &config.user_attr, | |
88 | config.sync_attributes.as_deref(), | |
89 | config.user_classes.as_deref(), | |
90 | config.filter.as_deref(), | |
91 | )?; | |
73757fe2 LW |
92 | |
93 | let ldap_config = auth::LdapAuthenticator::api_type_to_config(&config)?; | |
94 | ||
95 | Ok(Self { | |
96 | worker, | |
97 | realm, | |
98 | general_sync_settings: sync_settings, | |
99 | ldap_sync_settings: sync_attributes, | |
100 | ldap_config, | |
101 | dry_run, | |
102 | }) | |
103 | } | |
104 | ||
105 | /// Perform realm synchronization | |
106 | async fn sync(&self) -> Result<(), Error> { | |
107 | if self.dry_run { | |
108 | task_log!( | |
109 | self.worker, | |
110 | "this is a DRY RUN - changes will not be persisted" | |
111 | ); | |
112 | } | |
113 | ||
114 | let ldap = Connection::new(self.ldap_config.clone()); | |
115 | ||
116 | let parameters = SearchParameters { | |
117 | attributes: self.ldap_sync_settings.attributes.clone(), | |
118 | user_classes: self.ldap_sync_settings.user_classes.clone(), | |
119 | user_filter: self.ldap_sync_settings.user_filter.clone(), | |
120 | }; | |
121 | ||
122 | let users = ldap.search_entities(¶meters).await?; | |
123 | self.update_user_config(&users)?; | |
124 | ||
125 | Ok(()) | |
126 | } | |
127 | ||
128 | fn update_user_config(&self, users: &[SearchResult]) -> Result<(), Error> { | |
129 | let user_lock = pbs_config::user::lock_config()?; | |
130 | let acl_lock = pbs_config::acl::lock_config()?; | |
131 | ||
132 | let (mut user_config, _digest) = pbs_config::user::config()?; | |
133 | let (mut tree, _) = pbs_config::acl::config()?; | |
134 | ||
135 | let retrieved_users = self.create_or_update_users(&mut user_config, &user_lock, users)?; | |
136 | ||
137 | if self.general_sync_settings.should_remove_entries() { | |
138 | let vanished_users = | |
139 | self.compute_vanished_users(&user_config, &user_lock, &retrieved_users)?; | |
140 | ||
141 | self.delete_users( | |
142 | &mut user_config, | |
143 | &user_lock, | |
144 | &mut tree, | |
145 | &acl_lock, | |
146 | &vanished_users, | |
147 | )?; | |
148 | } | |
149 | ||
150 | if !self.dry_run { | |
151 | pbs_config::user::save_config(&user_config).context("could not store user config")?; | |
152 | pbs_config::acl::save_config(&tree).context("could not store acl config")?; | |
153 | } | |
154 | ||
155 | Ok(()) | |
156 | } | |
157 | ||
158 | fn create_or_update_users( | |
159 | &self, | |
160 | user_config: &mut SectionConfigData, | |
161 | _user_lock: &BackupLockGuard, | |
162 | users: &[SearchResult], | |
163 | ) -> Result<HashSet<Userid>, Error> { | |
164 | let mut retrieved_users = HashSet::new(); | |
165 | ||
166 | for result in users { | |
cf4ff8a7 LW |
167 | let user_id_attribute = &self.ldap_sync_settings.user_attr; |
168 | ||
169 | let result = try_block!({ | |
170 | let username = result | |
171 | .attributes | |
172 | .get(user_id_attribute) | |
5f0965ed WB |
173 | .ok_or_else(|| { |
174 | format_err!( | |
175 | "userid attribute `{user_id_attribute}` not in LDAP search result" | |
176 | ) | |
177 | })? | |
911279b4 | 178 | .first() |
cf4ff8a7 LW |
179 | .context("userid attribute array is empty")? |
180 | .clone(); | |
181 | ||
182 | let username = format!("{username}@{realm}", realm = self.realm.as_str()); | |
183 | ||
184 | let userid: Userid = username | |
185 | .parse() | |
5f0965ed | 186 | .map_err(|err| format_err!("could not parse username `{username}` - {err}"))?; |
cf4ff8a7 LW |
187 | retrieved_users.insert(userid.clone()); |
188 | ||
189 | self.create_or_update_user(user_config, &userid, result)?; | |
190 | anyhow::Ok(()) | |
191 | }); | |
192 | if let Err(e) = result { | |
193 | task_log!(self.worker, "could not create/update user: {e}"); | |
194 | } | |
73757fe2 LW |
195 | } |
196 | ||
197 | Ok(retrieved_users) | |
198 | } | |
199 | ||
200 | fn create_or_update_user( | |
201 | &self, | |
202 | user_config: &mut SectionConfigData, | |
cf4ff8a7 | 203 | userid: &Userid, |
73757fe2 LW |
204 | result: &SearchResult, |
205 | ) -> Result<(), Error> { | |
206 | let existing_user = user_config.lookup::<User>("user", userid.as_str()).ok(); | |
207 | let new_or_updated_user = | |
208 | self.construct_or_update_user(result, userid, existing_user.as_ref()); | |
209 | ||
210 | if let Some(existing_user) = existing_user { | |
211 | if existing_user != new_or_updated_user { | |
212 | task_log!( | |
213 | self.worker, | |
214 | "updating user {}", | |
215 | new_or_updated_user.userid.as_str() | |
216 | ); | |
217 | } | |
218 | } else { | |
219 | task_log!( | |
220 | self.worker, | |
221 | "creating user {}", | |
222 | new_or_updated_user.userid.as_str() | |
223 | ); | |
224 | } | |
225 | ||
226 | user_config.set_data( | |
227 | new_or_updated_user.userid.as_str(), | |
228 | "user", | |
229 | &new_or_updated_user, | |
230 | )?; | |
231 | Ok(()) | |
232 | } | |
233 | ||
234 | fn construct_or_update_user( | |
235 | &self, | |
236 | result: &SearchResult, | |
cf4ff8a7 | 237 | userid: &Userid, |
73757fe2 LW |
238 | existing_user: Option<&User>, |
239 | ) -> User { | |
0010d56a | 240 | let lookup = |attribute: &str, ldap_attribute: Option<&String>, schema: &'static Schema| { |
911279b4 | 241 | let value = result.attributes.get(ldap_attribute?)?.first()?; |
75070440 WB |
242 | let schema = schema.unwrap_string_schema(); |
243 | ||
244 | if let Err(e) = schema.check_constraints(value) { | |
245 | task_warn!( | |
246 | self.worker, | |
247 | "{userid}: ignoring attribute `{attribute}`: {e}" | |
248 | ); | |
249 | ||
250 | None | |
251 | } else { | |
252 | Some(value.clone()) | |
253 | } | |
73757fe2 LW |
254 | }; |
255 | ||
256 | User { | |
cf4ff8a7 | 257 | userid: userid.clone(), |
73757fe2 LW |
258 | comment: existing_user.as_ref().and_then(|u| u.comment.clone()), |
259 | enable: existing_user | |
260 | .and_then(|o| o.enable) | |
261 | .or(Some(self.general_sync_settings.enable_new)), | |
262 | expire: existing_user.and_then(|u| u.expire).or(Some(0)), | |
cf4ff8a7 LW |
263 | firstname: lookup( |
264 | "firstname", | |
265 | self.ldap_sync_settings.firstname_attr.as_ref(), | |
0010d56a | 266 | &FIRST_NAME_SCHEMA, |
cf4ff8a7 LW |
267 | ) |
268 | .or_else(|| { | |
73757fe2 LW |
269 | if !self.general_sync_settings.should_remove_properties() { |
270 | existing_user.and_then(|o| o.firstname.clone()) | |
271 | } else { | |
272 | None | |
273 | } | |
274 | }), | |
cf4ff8a7 LW |
275 | lastname: lookup( |
276 | "lastname", | |
277 | self.ldap_sync_settings.lastname_attr.as_ref(), | |
0010d56a | 278 | &LAST_NAME_SCHEMA, |
cf4ff8a7 LW |
279 | ) |
280 | .or_else(|| { | |
73757fe2 LW |
281 | if !self.general_sync_settings.should_remove_properties() { |
282 | existing_user.and_then(|o| o.lastname.clone()) | |
283 | } else { | |
284 | None | |
285 | } | |
286 | }), | |
cf4ff8a7 LW |
287 | email: lookup( |
288 | "email", | |
289 | self.ldap_sync_settings.email_attr.as_ref(), | |
0010d56a | 290 | &EMAIL_SCHEMA, |
cf4ff8a7 LW |
291 | ) |
292 | .or_else(|| { | |
73757fe2 LW |
293 | if !self.general_sync_settings.should_remove_properties() { |
294 | existing_user.and_then(|o| o.email.clone()) | |
295 | } else { | |
296 | None | |
297 | } | |
298 | }), | |
299 | } | |
300 | } | |
301 | ||
302 | fn compute_vanished_users( | |
303 | &self, | |
304 | user_config: &SectionConfigData, | |
305 | _user_lock: &BackupLockGuard, | |
306 | synced_users: &HashSet<Userid>, | |
307 | ) -> Result<Vec<Userid>, Error> { | |
308 | Ok(user_config | |
309 | .convert_to_typed_array::<User>("user")? | |
310 | .into_iter() | |
311 | .filter(|user| { | |
312 | user.userid.realm() == self.realm && !synced_users.contains(&user.userid) | |
313 | }) | |
314 | .map(|user| user.userid) | |
315 | .collect()) | |
316 | } | |
317 | ||
318 | fn delete_users( | |
319 | &self, | |
320 | user_config: &mut SectionConfigData, | |
321 | _user_lock: &BackupLockGuard, | |
322 | acl_config: &mut AclTree, | |
323 | _acl_lock: &BackupLockGuard, | |
324 | to_delete: &[Userid], | |
325 | ) -> Result<(), Error> { | |
326 | for userid in to_delete { | |
327 | task_log!(self.worker, "deleting user {}", userid.as_str()); | |
328 | ||
329 | // Delete the user | |
330 | user_config.sections.remove(userid.as_str()); | |
331 | ||
332 | if self.general_sync_settings.should_remove_acls() { | |
333 | let auth_id = userid.clone().into(); | |
334 | // Delete the user's ACL entries | |
335 | acl_config.delete_authid(&auth_id); | |
336 | } | |
337 | ||
338 | let user_tokens: Vec<ApiToken> = user_config | |
339 | .convert_to_typed_array::<ApiToken>("token")? | |
340 | .into_iter() | |
341 | .filter(|token| token.tokenid.user().eq(userid)) | |
342 | .collect(); | |
343 | ||
344 | // Delete tokens, token secrets and ACLs corresponding to all tokens for a user | |
345 | for token in user_tokens { | |
346 | if let Some(name) = token.tokenid.tokenname() { | |
347 | let tokenid = Authid::from((userid.clone(), Some(name.to_owned()))); | |
348 | let tokenid_string = tokenid.to_string(); | |
349 | ||
350 | user_config.sections.remove(&tokenid_string); | |
351 | ||
352 | if !self.dry_run { | |
cf4ff8a7 LW |
353 | if let Err(e) = token_shadow::delete_secret(&tokenid) { |
354 | task_warn!(self.worker, "could not delete token for user {userid}: {e}",) | |
355 | } | |
73757fe2 LW |
356 | } |
357 | ||
358 | if self.general_sync_settings.should_remove_acls() { | |
359 | acl_config.delete_authid(&tokenid); | |
360 | } | |
361 | } | |
362 | } | |
363 | } | |
364 | ||
365 | Ok(()) | |
366 | } | |
367 | } | |
368 | ||
6685122c | 369 | /// General realm sync settings - Override for manual invocation |
73757fe2 LW |
370 | struct GeneralSyncSettingsOverride { |
371 | remove_vanished: Option<String>, | |
372 | enable_new: Option<bool>, | |
373 | } | |
374 | ||
375 | /// General realm sync settings from the realm configuration | |
376 | struct GeneralSyncSettings { | |
377 | remove_vanished: Vec<RemoveVanished>, | |
378 | enable_new: bool, | |
379 | } | |
380 | ||
381 | /// LDAP-specific realm sync settings from the realm configuration | |
382 | struct LdapSyncSettings { | |
383 | user_attr: String, | |
384 | firstname_attr: Option<String>, | |
385 | lastname_attr: Option<String>, | |
386 | email_attr: Option<String>, | |
387 | attributes: Vec<String>, | |
388 | user_classes: Vec<String>, | |
389 | user_filter: Option<String>, | |
390 | } | |
391 | ||
392 | impl LdapSyncSettings { | |
132e9722 CH |
393 | fn new( |
394 | user_attr: &str, | |
395 | sync_attributes: Option<&str>, | |
396 | user_classes: Option<&str>, | |
397 | user_filter: Option<&str>, | |
398 | ) -> Result<Self, Error> { | |
399 | let mut attributes = vec![user_attr.to_owned()]; | |
73757fe2 LW |
400 | |
401 | let mut email = None; | |
402 | let mut firstname = None; | |
403 | let mut lastname = None; | |
404 | ||
132e9722 | 405 | if let Some(sync_attributes) = &sync_attributes { |
73757fe2 LW |
406 | let value = LdapSyncAttributes::API_SCHEMA.parse_property_string(sync_attributes)?; |
407 | let sync_attributes: LdapSyncAttributes = serde_json::from_value(value)?; | |
408 | ||
409 | email = sync_attributes.email.clone(); | |
410 | firstname = sync_attributes.firstname.clone(); | |
411 | lastname = sync_attributes.lastname.clone(); | |
412 | ||
132e9722 CH |
413 | if let Some(email_attr) = &sync_attributes.email { |
414 | attributes.push(email_attr.clone()); | |
73757fe2 LW |
415 | } |
416 | ||
132e9722 CH |
417 | if let Some(firstname_attr) = &sync_attributes.firstname { |
418 | attributes.push(firstname_attr.clone()); | |
73757fe2 LW |
419 | } |
420 | ||
132e9722 CH |
421 | if let Some(lastname_attr) = &sync_attributes.lastname { |
422 | attributes.push(lastname_attr.clone()); | |
73757fe2 LW |
423 | } |
424 | } | |
425 | ||
132e9722 | 426 | let user_classes = if let Some(user_classes) = &user_classes { |
73757fe2 LW |
427 | let a = USER_CLASSES_ARRAY.parse_property_string(user_classes)?; |
428 | serde_json::from_value(a)? | |
429 | } else { | |
430 | vec![ | |
431 | "posixaccount".into(), | |
432 | "person".into(), | |
433 | "inetorgperson".into(), | |
434 | "user".into(), | |
435 | ] | |
436 | }; | |
437 | ||
438 | Ok(Self { | |
132e9722 | 439 | user_attr: user_attr.to_owned(), |
73757fe2 LW |
440 | firstname_attr: firstname, |
441 | lastname_attr: lastname, | |
442 | email_attr: email, | |
443 | attributes, | |
444 | user_classes, | |
132e9722 | 445 | user_filter: user_filter.map(ToOwned::to_owned), |
73757fe2 LW |
446 | }) |
447 | } | |
448 | } | |
449 | ||
450 | impl Default for GeneralSyncSettings { | |
451 | fn default() -> Self { | |
452 | Self { | |
453 | remove_vanished: Default::default(), | |
454 | enable_new: true, | |
455 | } | |
456 | } | |
457 | } | |
458 | ||
459 | impl GeneralSyncSettings { | |
132e9722 | 460 | fn apply_config(self, sync_defaults_options: Option<&str>) -> Result<Self, Error> { |
73757fe2 LW |
461 | let mut enable_new = None; |
462 | let mut remove_vanished = None; | |
463 | ||
132e9722 | 464 | if let Some(sync_defaults_options) = sync_defaults_options { |
73757fe2 LW |
465 | let sync_defaults_options = Self::parse_sync_defaults_options(sync_defaults_options)?; |
466 | ||
467 | enable_new = sync_defaults_options.enable_new; | |
468 | ||
469 | if let Some(vanished) = sync_defaults_options.remove_vanished.as_deref() { | |
470 | remove_vanished = Some(Self::parse_remove_vanished(vanished)?); | |
471 | } | |
472 | } | |
473 | ||
474 | Ok(Self { | |
475 | enable_new: enable_new.unwrap_or(self.enable_new), | |
476 | remove_vanished: remove_vanished.unwrap_or(self.remove_vanished), | |
477 | }) | |
478 | } | |
479 | ||
480 | fn apply_override(self, override_config: &GeneralSyncSettingsOverride) -> Result<Self, Error> { | |
481 | let enable_new = override_config.enable_new; | |
482 | let remove_vanished = if let Some(s) = override_config.remove_vanished.as_deref() { | |
483 | Some(Self::parse_remove_vanished(s)?) | |
484 | } else { | |
485 | None | |
486 | }; | |
487 | ||
488 | Ok(Self { | |
489 | enable_new: enable_new.unwrap_or(self.enable_new), | |
490 | remove_vanished: remove_vanished.unwrap_or(self.remove_vanished), | |
491 | }) | |
492 | } | |
493 | ||
494 | fn parse_sync_defaults_options(s: &str) -> Result<SyncDefaultsOptions, Error> { | |
495 | let value = SyncDefaultsOptions::API_SCHEMA.parse_property_string(s)?; | |
496 | Ok(serde_json::from_value(value)?) | |
497 | } | |
498 | ||
499 | fn parse_remove_vanished(s: &str) -> Result<Vec<RemoveVanished>, Error> { | |
500 | Ok(serde_json::from_value( | |
501 | REMOVE_VANISHED_ARRAY.parse_property_string(s)?, | |
502 | )?) | |
503 | } | |
504 | ||
505 | fn should_remove_properties(&self) -> bool { | |
506 | self.remove_vanished.contains(&RemoveVanished::Properties) | |
507 | } | |
508 | ||
509 | fn should_remove_entries(&self) -> bool { | |
510 | self.remove_vanished.contains(&RemoveVanished::Entry) | |
511 | } | |
512 | ||
513 | fn should_remove_acls(&self) -> bool { | |
514 | self.remove_vanished.contains(&RemoveVanished::Acl) | |
515 | } | |
516 | } |