kanidmd_lib/plugins/
valuedeny.rs

1use std::sync::Arc;
2
3use crate::plugins::Plugin;
4use crate::prelude::*;
5
6pub struct ValueDeny {}
7
8impl Plugin for ValueDeny {
9    fn id() -> &'static str {
10        "plugin_value_deny"
11    }
12
13    #[instrument(level = "debug", name = "denied_names_pre_create_transform", skip_all)]
14    #[allow(clippy::cognitive_complexity)]
15    fn pre_create_transform(
16        qs: &mut QueryServerWriteTransaction,
17        cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
18        _ce: &CreateEvent,
19    ) -> Result<(), OperationError> {
20        let denied_names = qs.denied_names();
21
22        if denied_names.is_empty() {
23            // Nothing to check.
24            return Ok(());
25        }
26
27        let mut pass = true;
28
29        for entry in cand {
30            // If the entry doesn't have a uuid, it's invalid anyway and will fail schema.
31            if let Some(e_uuid) = entry.get_uuid() {
32                // SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
33                // assert that the break glass and system accounts are NOT subject to
34                // this process.
35                if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
36                    // These entries are exempt
37                    continue;
38                }
39            }
40
41            if let Some(name) = entry.get_ava_single_iname(Attribute::Name) {
42                if denied_names.contains(name) {
43                    pass = false;
44                    error!(?name, "name denied by system configuration");
45                }
46            }
47        }
48
49        if pass {
50            Ok(())
51        } else {
52            Err(OperationError::ValueDenyName)
53        }
54    }
55
56    fn pre_modify(
57        qs: &mut QueryServerWriteTransaction,
58        pre_cand: &[Arc<EntrySealedCommitted>],
59        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
60        _me: &ModifyEvent,
61    ) -> Result<(), OperationError> {
62        Self::modify(qs, pre_cand, cand)
63    }
64
65    fn pre_batch_modify(
66        qs: &mut QueryServerWriteTransaction,
67        pre_cand: &[Arc<EntrySealedCommitted>],
68        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
69        _me: &BatchModifyEvent,
70    ) -> Result<(), OperationError> {
71        Self::modify(qs, pre_cand, cand)
72    }
73
74    fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
75        let denied_names = qs.denied_names().clone();
76
77        let mut results = Vec::with_capacity(0);
78
79        for denied_name in denied_names {
80            let filt = filter!(f_eq(Attribute::Name, PartialValue::new_iname(&denied_name)));
81            match qs.internal_search(filt) {
82                Ok(entries) => {
83                    for entry in entries {
84                        let e_uuid = entry.get_uuid();
85                        // SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
86                        // assert that the break glass accounts are NOT subject to this process.
87                        if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
88                            // These entries are exempt
89                            continue;
90                        }
91
92                        results.push(Err(ConsistencyError::DeniedName(e_uuid)));
93                    }
94                }
95                Err(err) => {
96                    error!(?err);
97                    results.push(Err(ConsistencyError::QueryServerSearchFailure))
98                }
99            }
100        }
101
102        results
103    }
104}
105
106impl ValueDeny {
107    #[instrument(level = "debug", name = "denied_names_modify", skip_all)]
108    fn modify(
109        qs: &mut QueryServerWriteTransaction,
110        pre_cand: &[Arc<EntrySealedCommitted>],
111        cand: &mut [EntryInvalidCommitted],
112    ) -> Result<(), OperationError> {
113        let denied_names = qs.denied_names();
114
115        if denied_names.is_empty() {
116            // Nothing to check.
117            return Ok(());
118        }
119
120        let mut pass = true;
121
122        for (pre_entry, post_entry) in pre_cand.iter().zip(cand.iter()) {
123            // If the entry doesn't have a uuid, it's invalid anyway and will fail schema.
124            let e_uuid = pre_entry.get_uuid();
125            // SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
126            // assert that the break glass accounts are NOT subject to this process.
127            if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
128                // These entries are exempt
129                continue;
130            }
131
132            let pre_name = pre_entry.get_ava_single_iname(Attribute::Name);
133            let post_name = post_entry.get_ava_single_iname(Attribute::Name);
134
135            if let Some(name) = post_name {
136                // Only if the name is changing, and is denied.
137                if pre_name != post_name && denied_names.contains(name) {
138                    pass = false;
139                    error!(?name, "name denied by system configuration");
140                }
141            }
142        }
143
144        if pass {
145            Ok(())
146        } else {
147            Err(OperationError::ValueDenyName)
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use crate::prelude::*;
155
156    async fn setup_name_deny(server: &QueryServer) {
157        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
158
159        let me_inv_m = ModifyEvent::new_internal_invalid(
160            filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())),
161            ModifyList::new_list(vec![
162                Modify::Present(Attribute::DeniedName, Value::new_iname("tobias")),
163                Modify::Present(Attribute::DeniedName, Value::new_iname("ellie")),
164            ]),
165        );
166        assert!(server_txn.modify(&me_inv_m).is_ok());
167
168        assert!(server_txn.commit().is_ok());
169    }
170
171    #[qs_test]
172    async fn test_valuedeny_create(server: &QueryServer) {
173        setup_name_deny(server).await;
174
175        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
176        let t_uuid = Uuid::new_v4();
177        assert!(server_txn
178            .internal_create(vec![entry_init!(
179                (Attribute::Class, EntryClass::Object.to_value()),
180                (Attribute::Class, EntryClass::Account.to_value()),
181                (Attribute::Class, EntryClass::Person.to_value()),
182                (Attribute::Name, Value::new_iname("tobias")),
183                (Attribute::Uuid, Value::Uuid(t_uuid)),
184                (Attribute::Description, Value::new_utf8s("Tobias")),
185                (Attribute::DisplayName, Value::new_utf8s("Tobias"))
186            ),])
187            .is_err());
188    }
189
190    #[qs_test]
191    async fn test_valuedeny_modify(server: &QueryServer) {
192        // Create an entry that has a name which will become denied to test how it
193        // interacts.
194        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
195        let e_uuid = Uuid::new_v4();
196        assert!(server_txn
197            .internal_create(vec![entry_init!(
198                (Attribute::Class, EntryClass::Object.to_value()),
199                (Attribute::Class, EntryClass::Account.to_value()),
200                (Attribute::Class, EntryClass::Person.to_value()),
201                (Attribute::Name, Value::new_iname("ellie")),
202                (Attribute::Uuid, Value::Uuid(e_uuid)),
203                (Attribute::Description, Value::new_utf8s("Ellie Meow")),
204                (Attribute::DisplayName, Value::new_utf8s("Ellie Meow"))
205            ),])
206            .is_ok());
207
208        assert!(server_txn.commit().is_ok());
209
210        setup_name_deny(server).await;
211
212        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
213
214        // Attempt to mod ellie.
215
216        // Can mod a different attribute
217        assert!(server_txn
218            .internal_modify_uuid(
219                e_uuid,
220                &ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("tobias"))
221            )
222            .is_ok());
223
224        // Can't mod to another invalid name.
225        assert!(server_txn
226            .internal_modify_uuid(
227                e_uuid,
228                &ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
229            )
230            .is_err());
231
232        // Can mod to a valid name.
233        assert!(server_txn
234            .internal_modify_uuid(
235                e_uuid,
236                &ModifyList::new_purge_and_set(
237                    Attribute::Name,
238                    Value::new_iname("miss_meowington")
239                )
240            )
241            .is_ok());
242
243        // Now mod from the valid name to an invalid one.
244        assert!(server_txn
245            .internal_modify_uuid(
246                e_uuid,
247                &ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
248            )
249            .is_err());
250
251        assert!(server_txn.commit().is_ok());
252    }
253
254    #[qs_test]
255    async fn test_valuedeny_jpwarren_special(server: &QueryServer) {
256        // Assert that our break glass accounts are exempt from this processing.
257        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
258
259        let me_inv_m = ModifyEvent::new_internal_invalid(
260            filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())),
261            ModifyList::new_list(vec![
262                Modify::Present(Attribute::DeniedName, Value::new_iname("admin")),
263                Modify::Present(Attribute::DeniedName, Value::new_iname("idm_admin")),
264            ]),
265        );
266        assert!(server_txn.modify(&me_inv_m).is_ok());
267        assert!(server_txn.commit().is_ok());
268
269        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
270
271        assert!(server_txn
272            .internal_modify_uuid(
273                UUID_IDM_ADMIN,
274                &ModifyList::new_purge_and_set(
275                    Attribute::DisplayName,
276                    Value::new_utf8s("Idm Admin")
277                )
278            )
279            .is_ok());
280
281        assert!(server_txn
282            .internal_modify_uuid(
283                UUID_ADMIN,
284                &ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("Admin"))
285            )
286            .is_ok());
287
288        assert!(server_txn.commit().is_ok());
289    }
290
291    #[qs_test]
292    async fn test_valuedeny_batch_modify(server: &QueryServer) {
293        setup_name_deny(server).await;
294
295        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
296        let t_uuid = Uuid::new_v4();
297        assert!(server_txn
298            .internal_create(vec![entry_init!(
299                (Attribute::Class, EntryClass::Object.to_value()),
300                (Attribute::Class, EntryClass::Account.to_value()),
301                (Attribute::Class, EntryClass::Person.to_value()),
302                (Attribute::Name, Value::new_iname("newname")),
303                (Attribute::Uuid, Value::Uuid(t_uuid)),
304                (Attribute::Description, Value::new_utf8s("Tobias")),
305                (Attribute::DisplayName, Value::new_utf8s("Tobias"))
306            ),])
307            .is_ok());
308
309        // Now batch mod
310
311        assert!(server_txn
312            .internal_batch_modify(
313                [(
314                    t_uuid,
315                    ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
316                )]
317                .into_iter()
318            )
319            .is_err());
320    }
321}