lockbook_shared/
validate.rs

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