@@ -37,66 +37,60 @@ impl FsPermissions {
37
37
/// Caution: This should be called with normalized paths if the `allowed_paths` are also
38
38
/// normalized.
39
39
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) )
41
41
}
42
42
43
43
/// Returns the permission for the matching path.
44
44
///
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 :
47
47
///
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
50
50
///
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:
70
52
///
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)
73
58
///
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 > {
82
66
let mut max_path_len = 0 ;
67
+ let mut highest_permission = FsAccessPermission :: None ;
83
68
84
- // First pass: find all matching permissions and determine the maximum path length
69
+ // Find all matching permissions at the longest matching path
85
70
for perm in & self . permissions {
86
71
let permission_path = dunce:: canonicalize ( & perm. path ) . unwrap_or ( perm. path . clone ( ) ) ;
87
72
if path. starts_with ( & permission_path) {
88
73
let path_len = permission_path. components ( ) . count ( ) ;
89
74
if path_len > max_path_len {
75
+ // Found a longer matching path, reset to this permission
90
76
max_path_len = path_len;
91
- matching_permissions. clear ( ) ;
92
- matching_permissions. push ( perm. access ) ;
77
+ highest_permission = perm. access ;
93
78
} 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
+ }
95
89
}
96
90
}
97
91
}
98
92
99
- matching_permissions
93
+ if max_path_len > 0 { Some ( highest_permission ) } else { None }
100
94
}
101
95
102
96
/// Updates all `allowed_paths` and joins ([`Path::join`]) the `root` with all entries
@@ -193,22 +187,22 @@ pub enum FsAccessPermission {
193
187
/// FS access is _not_ allowed
194
188
#[ default]
195
189
None ,
196
- /// FS access is allowed, this includes `read` + `write`
197
- ReadWrite ,
198
190
/// Only reading is allowed
199
191
Read ,
200
192
/// Only writing is allowed
201
193
Write ,
194
+ /// FS access is allowed, this includes `read` + `write`
195
+ ReadWrite ,
202
196
}
203
197
204
198
impl FsAccessPermission {
205
199
/// Returns true if the access is allowed
206
200
pub fn is_granted ( & self , kind : FsAccessKind ) -> bool {
207
201
match ( self , kind) {
208
202
( Self :: ReadWrite , _) => true ,
209
- ( Self :: None , _) => false ,
210
- ( Self :: Read , FsAccessKind :: Read ) => true ,
211
203
( Self :: Write , FsAccessKind :: Write ) => true ,
204
+ ( Self :: Read , FsAccessKind :: Read ) => true ,
205
+ ( Self :: None , _) => false ,
212
206
_ => false ,
213
207
}
214
208
}
@@ -305,17 +299,32 @@ mod tests {
305
299
}
306
300
307
301
#[ 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
309
304
let permissions = FsPermissions :: new ( vec ! [
310
- PathPermission :: read( "./out" ) ,
311
305
PathPermission :: read( "./out/contracts" ) ,
312
306
PathPermission :: write( "./out/contracts" ) ,
313
307
] ) ;
314
308
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) ;
320
329
}
321
330
}
0 commit comments