Skip to content

Commit 9f42604

Browse files
authored
refactor(config): simplify fs permission retrieval logic (#11946)
* refactor(config): simplify fs permission retrieval logic - merged `find_all_permissions` logic into `find_permission` - Refactored `find_permission` to return the highest privilege permission based on the longest matching path. - Added PartialOrd/Ord derive impl to `FsAccessPermission` for easy permission comparison - Updated tests to reflect changes in permission logic and ensure correctness. * fix: only `PartialOrd` is needed + simplify map_or * fix: improved r/w permission combination logic - when `Read` && `Write` are set for a path assuming `ReadWrite` - `Write` permission gives **only** `Write` access - `Read` permission gives **only** `Read` access - updated tests
1 parent c646b88 commit 9f42604

File tree

1 file changed

+59
-50
lines changed

1 file changed

+59
-50
lines changed

crates/config/src/fs_permissions.rs

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -37,66 +37,60 @@ impl FsPermissions {
3737
/// Caution: This should be called with normalized paths if the `allowed_paths` are also
3838
/// normalized.
3939
pub fn is_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
40-
self.find_all_permissions(path).iter().any(|perm| perm.is_granted(kind))
40+
self.find_permission(path).is_some_and(|perm| perm.is_granted(kind))
4141
}
4242

4343
/// Returns the permission for the matching path.
4444
///
45-
/// This finds the longest matching path with resolved sym links, e.g. if we have the following
46-
/// permissions:
45+
/// This finds the longest matching path with resolved sym links and returns the highest
46+
/// privilege permission. The algorithm works as follows:
4747
///
48-
/// `./out` = `read`
49-
/// `./out/contracts` = `read-write`
48+
/// 1. Find all permissions where the path matches (using longest path match)
49+
/// 2. Return the highest privilege permission from those matches
5050
///
51-
/// And we check for `./out/contracts/MyContract.sol` we will get `read-write` as permission.
52-
pub fn find_permission(&self, path: &Path) -> Option<FsAccessPermission> {
53-
let mut permission: Option<&PathPermission> = None;
54-
for perm in &self.permissions {
55-
let permission_path = dunce::canonicalize(&perm.path).unwrap_or(perm.path.clone());
56-
if path.starts_with(permission_path) {
57-
if let Some(active_perm) = permission.as_ref() {
58-
// the longest path takes precedence
59-
if perm.path < active_perm.path {
60-
continue;
61-
}
62-
}
63-
permission = Some(perm);
64-
}
65-
}
66-
permission.map(|perm| perm.access)
67-
}
68-
69-
/// Returns all permissions for the matching path.
51+
/// Example scenarios:
7052
///
71-
/// This finds the longest matching paths with resolved sym links, e.g. if we have the following
72-
/// permissions:
53+
/// ```text
54+
/// ./out = read
55+
/// ./out/contracts = read-write
56+
/// ```
57+
/// Checking `./out/contracts/MyContract.sol` returns `read-write` (longest path match)
7358
///
74-
/// `./out` = `read`
75-
/// `./out/contracts` = `read`
76-
/// `./out/contracts` = `write`
77-
///
78-
/// And we check for `./out/contracts/MyContract.sol`, we will get both `read` and
79-
/// `write` permissions.
80-
pub fn find_all_permissions(&self, path: &Path) -> Vec<FsAccessPermission> {
81-
let mut matching_permissions = Vec::new();
59+
/// ```text
60+
/// ./out/contracts = read
61+
/// ./out/contracts = write
62+
/// ```
63+
/// Checking `./out/contracts/MyContract.sol` returns `write` (highest privilege, which also
64+
/// grants read access)
65+
pub fn find_permission(&self, path: &Path) -> Option<FsAccessPermission> {
8266
let mut max_path_len = 0;
67+
let mut highest_permission = FsAccessPermission::None;
8368

84-
// First pass: find all matching permissions and determine the maximum path length
69+
// Find all matching permissions at the longest matching path
8570
for perm in &self.permissions {
8671
let permission_path = dunce::canonicalize(&perm.path).unwrap_or(perm.path.clone());
8772
if path.starts_with(&permission_path) {
8873
let path_len = permission_path.components().count();
8974
if path_len > max_path_len {
75+
// Found a longer matching path, reset to this permission
9076
max_path_len = path_len;
91-
matching_permissions.clear();
92-
matching_permissions.push(perm.access);
77+
highest_permission = perm.access;
9378
} else if path_len == max_path_len {
94-
matching_permissions.push(perm.access);
79+
// Same path length, keep the highest privilege
80+
highest_permission = match (highest_permission, perm.access) {
81+
(FsAccessPermission::ReadWrite, _)
82+
| (FsAccessPermission::Read, FsAccessPermission::Write)
83+
| (FsAccessPermission::Write, FsAccessPermission::Read) => {
84+
FsAccessPermission::ReadWrite
85+
}
86+
(FsAccessPermission::None, perm) => perm,
87+
(existing_perm, _) => existing_perm,
88+
}
9589
}
9690
}
9791
}
9892

99-
matching_permissions
93+
if max_path_len > 0 { Some(highest_permission) } else { None }
10094
}
10195

10296
/// Updates all `allowed_paths` and joins ([`Path::join`]) the `root` with all entries
@@ -193,22 +187,22 @@ pub enum FsAccessPermission {
193187
/// FS access is _not_ allowed
194188
#[default]
195189
None,
196-
/// FS access is allowed, this includes `read` + `write`
197-
ReadWrite,
198190
/// Only reading is allowed
199191
Read,
200192
/// Only writing is allowed
201193
Write,
194+
/// FS access is allowed, this includes `read` + `write`
195+
ReadWrite,
202196
}
203197

204198
impl FsAccessPermission {
205199
/// Returns true if the access is allowed
206200
pub fn is_granted(&self, kind: FsAccessKind) -> bool {
207201
match (self, kind) {
208202
(Self::ReadWrite, _) => true,
209-
(Self::None, _) => false,
210-
(Self::Read, FsAccessKind::Read) => true,
211203
(Self::Write, FsAccessKind::Write) => true,
204+
(Self::Read, FsAccessKind::Read) => true,
205+
(Self::None, _) => false,
212206
_ => false,
213207
}
214208
}
@@ -305,17 +299,32 @@ mod tests {
305299
}
306300

307301
#[test]
308-
fn find_all_permissions() {
302+
fn read_write_permission_combination() {
303+
// When multiple permissions are defined for the same path, highest privilege wins
309304
let permissions = FsPermissions::new(vec![
310-
PathPermission::read("./out"),
311305
PathPermission::read("./out/contracts"),
312306
PathPermission::write("./out/contracts"),
313307
]);
314308

315-
let found_permissions =
316-
permissions.find_all_permissions(Path::new("./out/contracts/MyContract.sol"));
317-
assert_eq!(found_permissions.len(), 2);
318-
assert!(found_permissions.contains(&FsAccessPermission::Write));
319-
assert!(found_permissions.contains(&FsAccessPermission::Read));
309+
let permission =
310+
permissions.find_permission(Path::new("./out/contracts/MyContract.sol")).unwrap();
311+
assert_eq!(FsAccessPermission::ReadWrite, permission);
312+
}
313+
314+
#[test]
315+
fn longest_path_takes_precedence() {
316+
let permissions = FsPermissions::new(vec![
317+
PathPermission::read_write("./out"),
318+
PathPermission::read("./out/contracts"),
319+
]);
320+
321+
// More specific path (./out/contracts) takes precedence even with lower privilege
322+
let permission =
323+
permissions.find_permission(Path::new("./out/contracts/MyContract.sol")).unwrap();
324+
assert_eq!(FsAccessPermission::Read, permission);
325+
326+
// Broader path still applies to its own files
327+
let permission = permissions.find_permission(Path::new("./out/other.sol")).unwrap();
328+
assert_eq!(FsAccessPermission::ReadWrite, permission);
320329
}
321330
}

0 commit comments

Comments
 (0)