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