lb_rs/model/
validate.rs

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