]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/realm_sync_job.rs
realm sync: generic-ify `LdapSyncSettings` and `GeneralSyncSettings`
[proxmox-backup.git] / src / server / realm_sync_job.rs
CommitLineData
5f0965ed 1use anyhow::{bail, format_err, Context, Error};
73757fe2 2use pbs_config::{acl::AclTree, token_shadow, BackupLockGuard};
cf4ff8a7 3use proxmox_lang::try_block;
73757fe2
LW
4use proxmox_ldap::{Config, Connection, SearchParameters, SearchResult};
5use proxmox_rest_server::WorkerTask;
cf4ff8a7 6use proxmox_schema::{ApiType, Schema};
73757fe2 7use proxmox_section_config::SectionConfigData;
cf4ff8a7 8use proxmox_sys::{task_log, task_warn};
73757fe2
LW
9
10use std::{collections::HashSet, sync::Arc};
11
12use 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
18use crate::{auth, server::jobstate::Job};
19
20/// Runs a realm sync job
21#[allow(clippy::too_many_arguments)]
22pub 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
59struct 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
68impl 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(&parameters).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
370struct GeneralSyncSettingsOverride {
371 remove_vanished: Option<String>,
372 enable_new: Option<bool>,
373}
374
375/// General realm sync settings from the realm configuration
376struct GeneralSyncSettings {
377 remove_vanished: Vec<RemoveVanished>,
378 enable_new: bool,
379}
380
381/// LDAP-specific realm sync settings from the realm configuration
382struct 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
392impl 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
450impl Default for GeneralSyncSettings {
451 fn default() -> Self {
452 Self {
453 remove_vanished: Default::default(),
454 enable_new: true,
455 }
456 }
457}
458
459impl 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}