kanidmd_lib/server/
batch_modify.rs1use super::{ChangeFlag, QueryServerWriteTransaction};
2use crate::prelude::*;
3use crate::server::Plugins;
4use std::collections::BTreeMap;
5
6pub type ModSetValid = BTreeMap<Uuid, ModifyList<ModifyValid>>;
7pub type ModSetInvalid = BTreeMap<Uuid, ModifyList<ModifyInvalid>>;
8
9pub struct BatchModifyEvent {
10 pub ident: Identity,
11 pub modset: ModSetValid,
12}
13
14impl QueryServerWriteTransaction<'_> {
15 #[instrument(level = "debug", skip_all)]
30 pub fn batch_modify(&mut self, me: &BatchModifyEvent) -> Result<(), OperationError> {
31 if !me.ident.is_internal() {
39 security_info!(name = %me.ident, "batch modify initiator");
40 }
41
42 if me.modset.is_empty() {
46 request_error!("empty modify request");
47 return Err(OperationError::EmptyRequest);
48 }
49
50 let filter_or = me
51 .modset
52 .keys()
53 .copied()
54 .map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(u)))
55 .collect();
56
57 let filter = filter_all!(f_or(filter_or))
58 .validate(self.get_schema())
59 .map_err(OperationError::SchemaViolation)?;
60
61 let pre_candidates = self
63 .impersonate_search_valid(filter.clone(), filter.clone(), &me.ident)
64 .map_err(|e| {
65 admin_error!("error in pre-candidate selection {:?}", e);
66 e
67 })?;
68
69 if pre_candidates.is_empty() {
70 if me.ident.is_internal() {
71 trace!("no candidates match filter ... continuing {:?}", filter);
72 return Ok(());
73 } else {
74 request_error!("no candidates match modset request, failure {:?}", filter);
75 return Err(OperationError::NoMatchingEntries);
76 }
77 };
78
79 if pre_candidates.len() != me.modset.len() {
80 error!("Inconsistent modify, some uuids were not found in request.");
81 return Err(OperationError::MissingEntries);
82 }
83
84 trace!("pre_candidates -> {:?}", pre_candidates);
85 trace!("modset -> {:?}", me.modset);
86
87 let access = self.get_accesscontrols();
90
91 let op_allow = access
92 .batch_modify_allow_operation(me, &pre_candidates)
93 .map_err(|e| {
94 admin_error!("Unable to check batch modify access {:?}", e);
95 e
96 })?;
97 if !op_allow {
98 return Err(OperationError::AccessDenied);
99 }
100
101 let mut candidates = pre_candidates
107 .iter()
108 .map(|er| {
109 let u = er.get_uuid();
110 let mut ent_mut = er
111 .as_ref()
112 .clone()
113 .invalidate(self.cid.clone(), &self.trim_cid);
114
115 me.modset
116 .get(&u)
117 .ok_or_else(|| {
118 error!("No entry for uuid {} was found, aborting", u);
119 OperationError::NoMatchingEntries
120 })
121 .and_then(|modlist| {
122 ent_mut
123 .apply_modlist(modlist)
124 .map(|()| ent_mut)
126 .inspect_err(|_e| {
128 error!("Modification failed for {}", u);
129 })
130 })
131 })
132 .collect::<Result<Vec<EntryInvalidCommitted>, _>>()?;
133
134 if std::iter::zip(
136 pre_candidates
137 .iter()
138 .map(|e| e.mask_recycled_ts().is_none()),
139 candidates.iter().map(|e| e.mask_recycled_ts().is_none()),
140 )
141 .any(|(a, b)| a != b)
142 {
143 admin_warn!("Refusing to apply modifications that are attempting to bypass replication state machine.");
144 return Err(OperationError::AccessDenied);
145 }
146
147 Plugins::run_pre_batch_modify(self, &pre_candidates, &mut candidates, me).map_err(|e| {
150 admin_error!("Pre-Modify operation failed (plugin), {:?}", e);
151 e
152 })?;
153
154 let norm_cand = candidates
155 .into_iter()
156 .map(|entry| {
157 entry
158 .validate(&self.schema)
159 .map_err(|e| {
160 admin_error!("Schema Violation in validation of modify_pre_apply {:?}", e);
161 OperationError::SchemaViolation(e)
162 })
163 .map(|entry| entry.seal(&self.schema))
164 })
165 .collect::<Result<Vec<EntrySealedCommitted>, _>>()?;
166
167 self.be_txn
169 .modify(&self.cid, &pre_candidates, &norm_cand)
170 .map_err(|e| {
171 admin_error!("Modify operation failed (backend), {:?}", e);
172 e
173 })?;
174
175 Plugins::run_post_batch_modify(self, &pre_candidates, &norm_cand, me).map_err(|e| {
180 admin_error!("Post-Modify operation failed (plugin), {:?}", e);
181 e
182 })?;
183
184 if !self.changed_flags.contains(ChangeFlag::SCHEMA)
188 && norm_cand
189 .iter()
190 .chain(pre_candidates.iter().map(|e| e.as_ref()))
191 .any(|e| {
192 e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
193 || e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
194 })
195 {
196 self.changed_flags.insert(ChangeFlag::SCHEMA)
197 }
198
199 if !self.changed_flags.contains(ChangeFlag::ACP)
200 && norm_cand
201 .iter()
202 .chain(pre_candidates.iter().map(|e| e.as_ref()))
203 .any(|e| {
204 e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
205 })
206 {
207 self.changed_flags.insert(ChangeFlag::ACP)
208 }
209
210 if !self.changed_flags.contains(ChangeFlag::APPLICATION)
211 && norm_cand
212 .iter()
213 .chain(pre_candidates.iter().map(|e| e.as_ref()))
214 .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
215 {
216 self.changed_flags.insert(ChangeFlag::APPLICATION)
217 }
218
219 if !self.changed_flags.contains(ChangeFlag::OAUTH2)
220 && norm_cand
221 .iter()
222 .chain(pre_candidates.iter().map(|e| e.as_ref()))
223 .any(|e| {
224 e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
225 })
226 {
227 self.changed_flags.insert(ChangeFlag::OAUTH2)
228 }
229
230 if !self.changed_flags.contains(ChangeFlag::OAUTH2_CLIENT)
231 && norm_cand
232 .iter()
233 .chain(pre_candidates.iter().map(|e| e.as_ref()))
234 .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::OAuth2Client.into()))
235 {
236 self.changed_flags.insert(ChangeFlag::OAUTH2_CLIENT)
237 }
238
239 if !self.changed_flags.contains(ChangeFlag::FEATURE)
240 && norm_cand
241 .iter()
242 .chain(pre_candidates.iter().map(|e| e.as_ref()))
243 .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Feature.into()))
244 {
245 self.changed_flags.insert(ChangeFlag::FEATURE)
246 }
247
248 if !self.changed_flags.contains(ChangeFlag::DOMAIN)
249 && norm_cand
250 .iter()
251 .chain(pre_candidates.iter().map(|e| e.as_ref()))
252 .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
253 {
254 self.changed_flags.insert(ChangeFlag::DOMAIN)
255 }
256
257 if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
258 && norm_cand
259 .iter()
260 .chain(pre_candidates.iter().map(|e| e.as_ref()))
261 .any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
262 {
263 self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
264 }
265
266 if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
267 && norm_cand
268 .iter()
269 .chain(pre_candidates.iter().map(|e| e.as_ref()))
270 .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
271 {
272 self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
273 }
274
275 if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
276 && norm_cand
277 .iter()
278 .chain(pre_candidates.iter().map(|e| e.as_ref()))
279 .any(|e| {
280 e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
281 || e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
282 })
283 {
284 self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
285 }
286
287 self.changed_uuid.extend(
288 norm_cand
289 .iter()
290 .map(|e| e.get_uuid())
291 .chain(pre_candidates.iter().map(|e| e.get_uuid())),
292 );
293
294 trace!(
295 changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
296 );
297
298 if me.ident.is_internal() {
300 trace!("Modify operation success");
301 } else {
302 admin_info!("Modify operation success");
303 }
304 Ok(())
305 }
306
307 pub fn internal_batch_modify(
308 &mut self,
309 mods_iter: impl Iterator<Item = (Uuid, ModifyList<ModifyInvalid>)>,
310 ) -> Result<(), OperationError> {
311 let modset = mods_iter
312 .map(|(u, ml)| {
313 ml.validate(self.get_schema())
314 .map(|modlist| (u, modlist))
315 .map_err(OperationError::SchemaViolation)
316 })
317 .collect::<Result<ModSetValid, _>>()?;
318 let bme = BatchModifyEvent {
319 ident: Identity::from_internal(),
320 modset,
321 };
322 self.batch_modify(&bme)
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use crate::prelude::*;
329
330 #[qs_test]
331 async fn test_batch_modify_basic(server: &QueryServer) {
332 let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
333 let uuid_a = Uuid::new_v4();
335 let uuid_b = Uuid::new_v4();
336 assert!(server_txn
337 .internal_create(vec![
338 entry_init!(
339 (Attribute::Class, EntryClass::Object.to_value()),
340 (Attribute::Uuid, Value::Uuid(uuid_a))
341 ),
342 entry_init!(
343 (Attribute::Class, EntryClass::Object.to_value()),
344 (Attribute::Uuid, Value::Uuid(uuid_b))
345 ),
346 ])
347 .is_ok());
348
349 assert!(server_txn
351 .internal_batch_modify(
352 [
353 (
354 uuid_a,
355 ModifyList::new_append(Attribute::Description, Value::Utf8("a".into()))
356 ),
357 (
358 uuid_b,
359 ModifyList::new_append(Attribute::Description, Value::Utf8("b".into()))
360 ),
361 ]
362 .into_iter()
363 )
364 .is_ok());
365
366 let ent_a = server_txn
368 .internal_search_uuid(uuid_a)
369 .expect("Failed to get entry.");
370 let ent_b = server_txn
371 .internal_search_uuid(uuid_b)
372 .expect("Failed to get entry.");
373
374 assert_eq!(ent_a.get_ava_single_utf8(Attribute::Description), Some("a"));
375 assert_eq!(ent_b.get_ava_single_utf8(Attribute::Description), Some("b"));
376 }
377}