lb_rs/model/
validate.rs

1use crate::model::access_info::UserAccessMode;
2use crate::model::file_like::FileLike;
3use crate::model::file_metadata::{Diff, FileDiff, FileType, Owner};
4use crate::model::filename::MAX_ENCRYPTED_FILENAME_LENGTH;
5use crate::model::lazy::LazyTree;
6use crate::model::staged::StagedTreeLike;
7use crate::model::tree_like::TreeLike;
8use crate::model::ValidationFailure;
9use std::collections::{HashMap, HashSet};
10
11use super::errors::{LbErrKind, LbResult};
12
13pub fn file_name(name: &str) -> LbResult<()> {
14    if name.is_empty() {
15        Err(LbErrKind::FileNameEmpty)?;
16    }
17    if name.contains('/') {
18        Err(LbErrKind::FileNameContainsSlash)?;
19    }
20    Ok(())
21}
22
23pub fn not_root<F: FileLike>(file: &F) -> LbResult<()> {
24    if file.is_root() {
25        Err(LbErrKind::RootModificationInvalid)?
26    } else {
27        Ok(())
28    }
29}
30
31pub fn is_folder<F: FileLike>(file: &F) -> LbResult<()> {
32    if file.is_folder() {
33        Ok(())
34    } else {
35        Err(LbErrKind::Validation(ValidationFailure::NonFolderWithChildren(*file.id())))?
36    }
37}
38
39pub fn is_document<F: FileLike>(file: &F) -> LbResult<()> {
40    if file.is_document() {
41        Ok(())
42    } else {
43        Err(LbErrKind::FileNotDocument)?
44    }
45}
46
47pub fn path(path: &str) -> LbResult<()> {
48    if path.contains("//") || path.is_empty() {
49        Err(LbErrKind::PathContainsEmptyFileName)?;
50    }
51
52    Ok(())
53}
54
55impl<T, Base, Local> LazyTree<T>
56where
57    T: StagedTreeLike<Base = Base, Staged = Local>,
58    Base: TreeLike<F = T::F>,
59    Local: TreeLike<F = T::F>,
60{
61    pub fn validate(&mut self, owner: Owner) -> LbResult<()> {
62        // point checks
63        self.assert_no_root_changes()?;
64        self.assert_no_changes_to_deleted_files()?;
65        self.assert_all_filenames_size_limit()?;
66        self.assert_all_files_decryptable(owner)?;
67        self.assert_only_folders_have_children()?;
68        self.assert_all_files_same_owner_as_parent()?;
69
70        // structure checks
71        self.assert_no_cycles()?;
72        self.assert_no_path_conflicts()?;
73        self.assert_no_shared_links()?;
74        self.assert_no_duplicate_links()?;
75        self.assert_no_broken_links()?;
76        self.assert_no_owned_links()?;
77
78        // authorization check
79        self.assert_changes_authorized(owner)?;
80
81        Ok(())
82    }
83
84    // note: deleted access keys permissible
85    pub fn assert_all_files_decryptable(&mut self, owner: Owner) -> LbResult<()> {
86        for file in self.ids().into_iter().filter_map(|id| self.maybe_find(&id)) {
87            if self.maybe_find_parent(file).is_none()
88                && !file
89                    .user_access_keys()
90                    .iter()
91                    .any(|k| k.encrypted_for == owner.0)
92            {
93                Err(LbErrKind::Validation(ValidationFailure::Orphan(*file.id())))?;
94            }
95        }
96        Ok(())
97    }
98
99    pub fn assert_all_filenames_size_limit(&self) -> LbResult<()> {
100        for file in self.all_files()? {
101            if file.secret_name().encrypted_value.value.len() > MAX_ENCRYPTED_FILENAME_LENGTH {
102                return Err(LbErrKind::Validation(ValidationFailure::FileNameTooLong(*file.id())))?;
103            }
104        }
105        Ok(())
106    }
107
108    pub fn assert_only_folders_have_children(&self) -> LbResult<()> {
109        for file in self.all_files()? {
110            if let Some(parent) = self.maybe_find(file.parent()) {
111                if !parent.is_folder() {
112                    Err(LbErrKind::Validation(ValidationFailure::NonFolderWithChildren(
113                        *parent.id(),
114                    )))?;
115                }
116            }
117        }
118        Ok(())
119    }
120
121    // note: deleted files exempt because otherwise moving a folder with a deleted file in it
122    // to/from a folder with a different owner would require updating a deleted file
123    pub fn assert_all_files_same_owner_as_parent(&mut self) -> LbResult<()> {
124        for id in self.ids() {
125            if self.calculate_deleted(&id)? {
126                continue;
127            }
128            let file = self.find(&id)?;
129            if let Some(parent) = self.maybe_find(file.parent()) {
130                if parent.owner() != file.owner() {
131                    Err(LbErrKind::Validation(ValidationFailure::FileWithDifferentOwnerParent(
132                        *file.id(),
133                    )))?;
134                }
135            }
136        }
137        Ok(())
138    }
139
140    // assumption: no orphans
141    pub fn assert_no_cycles(&mut self) -> LbResult<()> {
142        let mut owners_with_found_roots = HashSet::new();
143        let mut no_cycles_in_ancestors = HashSet::new();
144        for id in self.ids() {
145            let mut ancestors = HashSet::new();
146            let mut current_file = self.find(&id)?;
147            loop {
148                if no_cycles_in_ancestors.contains(current_file.id()) {
149                    break;
150                } else if current_file.is_root() {
151                    if owners_with_found_roots.insert(current_file.owner()) {
152                        ancestors.insert(*current_file.id());
153                        break;
154                    } else {
155                        Err(LbErrKind::Validation(ValidationFailure::Cycle(HashSet::from([id]))))?;
156                    }
157                } else if ancestors.contains(current_file.parent()) {
158                    Err(LbErrKind::Validation(ValidationFailure::Cycle(
159                        self.ancestors(current_file.id())?,
160                    )))?;
161                }
162                ancestors.insert(*current_file.id());
163                current_file = match self.maybe_find_parent(current_file) {
164                    Some(file) => file,
165                    None => {
166                        if !current_file.user_access_keys().is_empty() {
167                            break;
168                        } else {
169                            return Err(LbErrKind::FileParentNonexistent)?;
170                        }
171                    }
172                }
173            }
174            no_cycles_in_ancestors.extend(ancestors);
175        }
176        Ok(())
177    }
178
179    pub fn assert_no_path_conflicts(&mut self) -> LbResult<()> {
180        let mut id_by_name = HashMap::new();
181        for id in self.ids() {
182            if !self.calculate_deleted(&id)? {
183                let file = self.find(&id)?;
184                if file.is_root() || self.maybe_find(file.parent()).is_none() {
185                    continue;
186                }
187                if let Some(conflicting) = id_by_name.remove(file.secret_name()) {
188                    Err(LbErrKind::Validation(ValidationFailure::PathConflict(HashSet::from([
189                        conflicting,
190                        *file.id(),
191                    ]))))?;
192                }
193                id_by_name.insert(file.secret_name().clone(), *file.id());
194            }
195        }
196        Ok(())
197    }
198
199    pub fn assert_no_shared_links(&self) -> LbResult<()> {
200        for link in self.ids() {
201            let meta = self.find(&link)?;
202            if let FileType::Link { target: _ } = meta.file_type() {
203                if meta.is_shared() {
204                    Err(LbErrKind::Validation(ValidationFailure::SharedLink {
205                        link,
206                        shared_ancestor: link,
207                    }))?;
208                }
209                for ancestor in self.ancestors(&link)? {
210                    if self.find(&ancestor)?.is_shared() {
211                        Err(LbErrKind::Validation(ValidationFailure::SharedLink {
212                            link,
213                            shared_ancestor: ancestor,
214                        }))?;
215                    }
216                }
217            }
218        }
219        Ok(())
220    }
221
222    pub fn assert_no_duplicate_links(&mut self) -> LbResult<()> {
223        let mut linked_targets = HashSet::new();
224        for link in self.ids() {
225            if self.calculate_deleted(&link)? {
226                continue;
227            }
228            if let FileType::Link { target } = self.find(&link)?.file_type() {
229                if !linked_targets.insert(target) {
230                    Err(LbErrKind::Validation(ValidationFailure::DuplicateLink { target }))?;
231                }
232            }
233        }
234        Ok(())
235    }
236
237    // note: a link to a deleted file is not considered broken, because then you would not be able
238    // to delete a file linked to by another user.
239    // note: a deleted link to a nonexistent file is not considered broken, because targets of
240    // deleted links may have their shares deleted, would not appear in the server tree for a user,
241    // and would be pruned from client trees
242    pub fn assert_no_broken_links(&mut self) -> LbResult<()> {
243        for link in self.ids() {
244            if let FileType::Link { target } = self.find(&link)?.file_type() {
245                if !self.calculate_deleted(&link)? && self.maybe_find(&target).is_none() {
246                    Err(LbErrKind::Validation(ValidationFailure::BrokenLink(link)))?;
247                }
248            }
249        }
250        Ok(())
251    }
252
253    pub fn assert_no_owned_links(&self) -> LbResult<()> {
254        for link in self.ids() {
255            if let FileType::Link { target } = self.find(&link)?.file_type() {
256                if let Some(target_owner) = self.maybe_find(&target).map(|f| f.owner()) {
257                    if self.find(&link)?.owner() == target_owner {
258                        Err(LbErrKind::Validation(ValidationFailure::OwnedLink(link)))?;
259                    }
260                }
261            }
262        }
263        Ok(())
264    }
265
266    pub fn assert_no_root_changes(&mut self) -> LbResult<()> {
267        for id in self.tree.staged().ids() {
268            // already root
269            if let Some(base) = self.tree.base().maybe_find(&id) {
270                if base.is_root() {
271                    Err(LbErrKind::RootModificationInvalid)?;
272                }
273            }
274            // newly root
275            if self.find(&id)?.is_root() {
276                Err(LbErrKind::Validation(ValidationFailure::Cycle(
277                    vec![id].into_iter().collect(),
278                )))?;
279            }
280        }
281        Ok(())
282    }
283
284    pub fn assert_no_changes_to_deleted_files(&mut self) -> LbResult<()> {
285        for id in self.tree.staged().ids() {
286            // already deleted files cannot have updates
287            let mut base = self.tree.base().to_lazy();
288            if base.maybe_find(&id).is_some() && base.calculate_deleted(&id)? {
289                Err(LbErrKind::Validation(ValidationFailure::DeletedFileUpdated(id)))?;
290            }
291            // newly deleted files cannot have non-deletion updates
292            if self.calculate_deleted(&id)? {
293                if let Some(base) = self.tree.base().maybe_find(&id) {
294                    if FileDiff::edit(&base, &self.find(&id)?)
295                        .diff()
296                        .iter()
297                        .any(|d| d != &Diff::Deleted)
298                    {
299                        Err(LbErrKind::Validation(ValidationFailure::DeletedFileUpdated(id)))?;
300                    }
301                }
302            }
303        }
304        Ok(())
305    }
306
307    pub fn assert_changes_authorized(&mut self, owner: Owner) -> LbResult<()> {
308        // Design rationale:
309        // * No combination of individually valid changes should compose into an invalid change.
310        //   * Owner and write access must be indistinguishable, otherwise you could e.g. move a
311        //     file from write shared folder into your own, modify it in a way only owners can, then
312        //     move it back. Accommodating this situation may be possible but we're not interested.
313        // * Which tree - base or staged - should we check access to for an operation?
314        //   * The only staged operations which cause permissions to be different in base and staged
315        //     are moves and share changes. Otherwise, it doesn't matter which tree is used.
316        //   * Changes by a user cannot increase the level of access of access for that user, but
317        //     they can decrease it. Therefore the maximum level of access a user may have over a
318        //     sequence of operations is represented in the base tree. We cannot use the staged
319        //     tree in case a user removes the access they required to perform a prior operation.
320        // * How do we check access for new files in new folders (which don't exist in base)?
321        //   * A user will have the same access to any created folder as they do to its parent; if a
322        //     user has access to create a folder, then they will have access to create its
323        //     descendants and to move files such that they are descendants.
324        //   * Any access checks on files with new parent folders can be skipped because the access
325        //     check on the first ancestor with an existing parent folder is sufficient.
326        let new_files = {
327            let mut new_files = HashSet::new();
328            for id in self.tree.staged().ids() {
329                if self.tree.base().maybe_find(&id).is_none() {
330                    new_files.insert(id);
331                }
332            }
333            new_files
334        };
335
336        for file_diff in self.diffs()? {
337            for field_diff in file_diff.diff() {
338                match field_diff {
339                    Diff::New | Diff::Name | Diff::Deleted => {
340                        // use oldest version for most permissive access (see rationale)
341                        let file =
342                            if let Some(ref old) = file_diff.old { old } else { &file_diff.new };
343                        // parent folder new -> rely on parent folder check
344                        if !new_files.contains(file.parent()) {
345                            // must have parent and have write access to parent
346                            if let Some(parent) = self.maybe_find(file.parent()) {
347                                if self.access_mode(owner, parent.id())?
348                                    < Some(UserAccessMode::Write)
349                                {
350                                    // parent is shared with access < write
351                                    Err(LbErrKind::InsufficientPermission)?;
352                                }
353                            } else {
354                                // this file is shared and its parent is not
355                                Err(LbErrKind::InsufficientPermission)?;
356                            }
357                        }
358                    }
359                    Diff::Parent | Diff::Owner => {
360                        // check access for base parent
361                        {
362                            let parent = if let Some(ref old) = file_diff.old {
363                                old.parent()
364                            } else {
365                                return Err(LbErrKind::Unexpected(
366                                    "Non-New FileDiff with no old".to_string(),
367                                )
368                                .into());
369                            };
370
371                            // must have parent and have write access to parent
372                            if let Some(parent) = self.maybe_find(parent) {
373                                if self.access_mode(owner, parent.id())?
374                                    < Some(UserAccessMode::Write)
375                                {
376                                    // parent is shared with access < write
377                                    Err(LbErrKind::InsufficientPermission)?;
378                                }
379                            } else {
380                                // this file is shared and its parent is not
381                                Err(LbErrKind::InsufficientPermission)?;
382                            }
383                        }
384                        // check access for staged parent
385                        {
386                            let parent = file_diff.new.parent();
387
388                            // parent folder new -> rely on parent folder check
389                            if !new_files.contains(parent) {
390                                // must have parent and have write access to parent
391                                if let Some(parent) = self.maybe_find(parent) {
392                                    if self.access_mode(owner, parent.id())?
393                                        < Some(UserAccessMode::Write)
394                                    {
395                                        // parent is shared with access < write
396                                        Err(LbErrKind::InsufficientPermission)?;
397                                    }
398                                } else {
399                                    // this file is shared and its parent is not
400                                    Err(LbErrKind::InsufficientPermission)?;
401                                }
402                            }
403                        }
404                    }
405                    Diff::Hmac => {
406                        // check self access
407                        if self.access_mode(owner, file_diff.id())? < Some(UserAccessMode::Write) {
408                            Err(LbErrKind::InsufficientPermission)?;
409                        }
410                    }
411                    Diff::UserKeys => {
412                        // change access: either changing your own access, or have write access
413                        let base_keys = {
414                            if let Some(ref old) = file_diff.old {
415                                let mut base_keys = HashMap::new();
416                                for key in old.user_access_keys() {
417                                    base_keys.insert(
418                                        (Owner(key.encrypted_by), Owner(key.encrypted_for)),
419                                        (key.mode, key.deleted),
420                                    );
421                                }
422                                base_keys
423                            } else {
424                                return Err(LbErrKind::Unexpected(
425                                    "Non-New FileDiff with no old".to_string(),
426                                ))?;
427                            }
428                        };
429                        for key in file_diff.new.user_access_keys() {
430                            if let Some((base_mode, base_deleted)) =
431                                base_keys.get(&(Owner(key.encrypted_by), Owner(key.encrypted_for)))
432                            {
433                                // editing an existing share
434
435                                let (staged_mode, staged_deleted) = (&key.mode, &key.deleted);
436                                // cannot delete someone else's share without write access
437                                if *staged_deleted
438                                    && !*base_deleted
439                                    && self.access_mode(owner, file_diff.id())?
440                                        < Some(UserAccessMode::Write)
441                                    && owner.0 != key.encrypted_for
442                                {
443                                    Err(LbErrKind::InsufficientPermission)?;
444                                }
445                                // cannot grant yourself write access
446                                if staged_mode != base_mode
447                                    && self.access_mode(owner, file_diff.id())?
448                                        < Some(UserAccessMode::Write)
449                                {
450                                    Err(LbErrKind::InsufficientPermission)?;
451                                }
452                            } else {
453                                // adding a new share
454
455                                // to add a share, need equal access
456                                if self.access_mode(owner, file_diff.id())? < Some(key.mode) {
457                                    Err(LbErrKind::InsufficientPermission)?;
458                                }
459                            }
460                        }
461                    }
462                }
463            }
464        }
465
466        Ok(())
467    }
468
469    fn diffs(&self) -> LbResult<Vec<FileDiff<Base::F>>> {
470        let mut result = Vec::new();
471        for id in self.tree.staged().ids() {
472            let staged = self.tree.staged().find(&id)?;
473            if let Some(base) = self.tree.base().maybe_find(&id) {
474                result.push(FileDiff::edit(base, staged));
475            } else {
476                result.push(FileDiff::new(staged));
477            }
478        }
479        Ok(result)
480    }
481}