First working implementation
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| /target | ||||
| test.txt | ||||
							
								
								
									
										39
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| # This file is automatically @generated by Cargo. | ||||
| # It is not intended for manual editing. | ||||
| version = 3 | ||||
|  | ||||
| [[package]] | ||||
| name = "acl-sys" | ||||
| version = "1.2.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bbc079f9bdd3124fd18df23c67f7e0f79d24751ae151dcffd095fcade07a3eb2" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "anyhow" | ||||
| version = "1.0.86" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" | ||||
|  | ||||
| [[package]] | ||||
| name = "bitflags" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" | ||||
|  | ||||
| [[package]] | ||||
| name = "libc" | ||||
| version = "0.2.156" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" | ||||
|  | ||||
| [[package]] | ||||
| name = "posix-acl" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "acl-sys", | ||||
|  "anyhow", | ||||
|  "bitflags", | ||||
| ] | ||||
							
								
								
									
										9
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| [package] | ||||
| name = "posix-acl" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| acl-sys = "1.2" | ||||
| anyhow = "1" | ||||
| bitflags = "2" | ||||
							
								
								
									
										352
									
								
								src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,352 @@ | ||||
| use std::{ | ||||
|     os::raw::c_void, | ||||
|     path::{Path, PathBuf}, | ||||
|     ptr::{addr_of, null_mut}, | ||||
| }; | ||||
|  | ||||
| use acl_sys::{ | ||||
|     acl_add_perm, acl_clear_perms, acl_create_entry, acl_free, acl_get_entry, acl_get_file, | ||||
|     acl_get_permset, acl_get_qualifier, acl_get_tag_type, acl_init, acl_permset_t, acl_set_file, | ||||
|     acl_set_permset, acl_set_qualifier, acl_set_tag_type, acl_type_t, ACL_FIRST_ENTRY, ACL_GROUP, | ||||
|     ACL_GROUP_OBJ, ACL_MASK, ACL_NEXT_ENTRY, ACL_OTHER, ACL_TYPE_ACCESS, ACL_TYPE_DEFAULT, | ||||
|     ACL_USER, ACL_USER_OBJ, | ||||
| }; | ||||
| use anyhow::{anyhow, Result}; | ||||
| use bitflags::bitflags; | ||||
| use std::ffi::CString; | ||||
|  | ||||
| bitflags! { | ||||
|     #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] | ||||
|     pub struct PermSet: u32 { | ||||
|         const ACL_READ = acl_sys::ACL_READ; | ||||
|         const ACL_WRITE = acl_sys::ACL_WRITE; | ||||
|         const ACL_EXECUTE = acl_sys::ACL_EXECUTE; | ||||
|  | ||||
|         const ACL_RWX = Self::ACL_READ.bits() | Self::ACL_WRITE.bits() | Self::ACL_EXECUTE.bits(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub static ACL_READ: PermSet = PermSet::ACL_READ; | ||||
| pub static ACL_WRITE: PermSet = PermSet::ACL_WRITE; | ||||
| pub static ACL_EXECUTE: PermSet = PermSet::ACL_EXECUTE; | ||||
| pub static ACL_RWX: PermSet = PermSet::ACL_RWX; | ||||
| pub static ACL_NONE: PermSet = PermSet::empty(); | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] | ||||
| pub enum Qualifier { | ||||
|     UserObj, | ||||
|     GroupObj, | ||||
|     Other, | ||||
|     User(u32), | ||||
|     Group(u32), | ||||
|     Mask, | ||||
| } | ||||
|  | ||||
| impl Qualifier { | ||||
|     pub fn get_tag(&self) -> i32 { | ||||
|         match self { | ||||
|             Qualifier::UserObj => ACL_USER_OBJ, | ||||
|             Qualifier::GroupObj => ACL_GROUP_OBJ, | ||||
|             Qualifier::Other => ACL_OTHER, | ||||
|             Qualifier::User(_) => ACL_USER, | ||||
|             Qualifier::Group(_) => ACL_GROUP, | ||||
|             Qualifier::Mask => ACL_MASK, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_uid(&self) -> Option<u32> { | ||||
|         match self { | ||||
|             Qualifier::User(uid) => Some(*uid), | ||||
|             Qualifier::Group(uid) => Some(*uid), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] | ||||
| pub struct ACLEntry(Qualifier, PermSet); | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] | ||||
| pub struct PosixACL { | ||||
|     entries: Vec<ACLEntry>, | ||||
| } | ||||
|  | ||||
| impl PosixACL { | ||||
|     pub fn new_from_file(path: impl Into<PathBuf>, default: bool) -> Result<Self> { | ||||
|         let path: PathBuf = path.into(); | ||||
|         let cpath = CString::new(path.to_str().unwrap()).unwrap(); | ||||
|         let acl = AclPtr(unsafe { | ||||
|             acl_get_file( | ||||
|                 cpath.as_ptr(), | ||||
|                 if default { | ||||
|                     ACL_TYPE_DEFAULT | ||||
|                 } else { | ||||
|                     ACL_TYPE_ACCESS | ||||
|                 }, | ||||
|             ) | ||||
|         }); | ||||
|  | ||||
|         if acl.0.is_null() { | ||||
|             // TODO: When trying to get default ACL from a file, it returns null. | ||||
|             return Err(anyhow!("Failed to get ACL")); | ||||
|         } | ||||
|  | ||||
|         let mut entries = Vec::new(); | ||||
|         let mut first = true; | ||||
|  | ||||
|         loop { | ||||
|             let mut entry = null_mut(); | ||||
|  | ||||
|             let ret = unsafe { | ||||
|                 acl_get_entry( | ||||
|                     acl.0, | ||||
|                     if first { | ||||
|                         ACL_FIRST_ENTRY | ||||
|                     } else { | ||||
|                         ACL_NEXT_ENTRY | ||||
|                     }, | ||||
|                     &mut entry, | ||||
|                 ) | ||||
|             }; | ||||
|  | ||||
|             first = false; | ||||
|  | ||||
|             if ret == 0 { | ||||
|                 break; | ||||
|             } else if ret != 1 { | ||||
|                 return Err(anyhow!("Failed to get ACL entry")); | ||||
|             } | ||||
|  | ||||
|             println!("entry: {:?}", entry); | ||||
|  | ||||
|             let mut tag_type: i32 = 0; | ||||
|             check_return( | ||||
|                 unsafe { acl_get_tag_type(entry, &mut tag_type) }, | ||||
|                 "acl_get_tag_type", | ||||
|             ); | ||||
|  | ||||
|             let mut tag_type = 0; | ||||
|             check_return( | ||||
|                 unsafe { acl_get_tag_type(entry, &mut tag_type) }, | ||||
|                 "acl_get_tag_type", | ||||
|             ); | ||||
|  | ||||
|             let qual = match tag_type { | ||||
|                 ACL_USER_OBJ => Qualifier::UserObj, | ||||
|                 ACL_GROUP_OBJ => Qualifier::GroupObj, | ||||
|                 ACL_OTHER => Qualifier::Other, | ||||
|                 ACL_USER => { | ||||
|                     let uid: AclPtr<u32> = AclPtr(unsafe { acl_get_qualifier(entry).cast() }); | ||||
|                     if uid.0.is_null() { | ||||
|                         return Err(anyhow!("Failed to get qualifier")); | ||||
|                     } | ||||
|  | ||||
|                     Qualifier::User(unsafe { *uid.0 }) | ||||
|                 } | ||||
|                 ACL_GROUP => { | ||||
|                     let gid: AclPtr<u32> = AclPtr(unsafe { acl_get_qualifier(entry).cast() }); | ||||
|                     if gid.0.is_null() { | ||||
|                         return Err(anyhow!("Failed to get qualifier")); | ||||
|                     } | ||||
|  | ||||
|                     Qualifier::Group(unsafe { *gid.0 }) | ||||
|                 } | ||||
|                 ACL_MASK => Qualifier::Mask, | ||||
|                 _ => panic!("Unknown tag type"), | ||||
|             }; | ||||
|  | ||||
|             let mut permset: *mut c_void = null_mut(); | ||||
|             check_return( | ||||
|                 unsafe { acl_get_permset(entry, &mut permset) }, | ||||
|                 "acl_get_permset", | ||||
|             ); | ||||
|  | ||||
|             println!("permset: {:?}", permset); | ||||
|             let perm = if permset.is_null() { | ||||
|                 0 | ||||
|             } else { | ||||
|                 unsafe { *(permset.cast()) } | ||||
|             }; | ||||
|  | ||||
|             let permset = PermSet::from_bits_truncate(perm); | ||||
|  | ||||
|             entries.push(ACLEntry(qual, permset)); | ||||
|         } | ||||
|  | ||||
|         Ok(PosixACL { entries }) | ||||
|     } | ||||
|  | ||||
|     pub fn new(user: PermSet, group: PermSet, others: PermSet) -> Self { | ||||
|         let entries = vec![ | ||||
|             ACLEntry(Qualifier::UserObj, user), | ||||
|             ACLEntry(Qualifier::GroupObj, group), | ||||
|             ACLEntry(Qualifier::Other, others), | ||||
|             ACLEntry(Qualifier::Mask, ACL_RWX), | ||||
|         ]; | ||||
|  | ||||
|         PosixACL { entries } | ||||
|     } | ||||
|  | ||||
|     pub fn set(&mut self, entry: ACLEntry) { | ||||
|         // if entry already exists, replace it | ||||
|         if let Some(i) = self.entries.iter().position(|x| x.0 == entry.0) { | ||||
|             self.entries[i] = entry; | ||||
|             return; | ||||
|         } else { | ||||
|             // if entry does not exist, add it | ||||
|             self.entries.push(entry); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn write<P: AsRef<Path>>(&self, path: P) -> Result<()> { | ||||
|         // Write ACL to file | ||||
|         self.write_type(path, ACL_TYPE_ACCESS)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn write_default<P: AsRef<Path>>(&self, path: P) -> Result<()> { | ||||
|         // Write default ACL to file | ||||
|         self.write_type(path, ACL_TYPE_DEFAULT)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn write_type<P: AsRef<Path>>(&self, path: P, acl_type: acl_type_t) -> Result<()> { | ||||
|         let mut acl_buf = AclPtr(unsafe { acl_init(self.entries.len() as i32) }); | ||||
|         if acl_buf.0.is_null() { | ||||
|             return Err(anyhow!("Failed to initialize ACL")); | ||||
|         } | ||||
|  | ||||
|         let has_user = self | ||||
|             .entries | ||||
|             .iter() | ||||
|             .any(|x| matches!(x.0, Qualifier::UserObj)); | ||||
|  | ||||
|         if !has_user { | ||||
|             return Err(anyhow!("UserObj entry is required")); | ||||
|         } | ||||
|  | ||||
|         let has_group = self | ||||
|             .entries | ||||
|             .iter() | ||||
|             .any(|x| matches!(x.0, Qualifier::GroupObj)); | ||||
|         if !has_group { | ||||
|             return Err(anyhow!("GroupObj entry is required")); | ||||
|         } | ||||
|  | ||||
|         let has_other = self.entries.iter().any(|x| matches!(x.0, Qualifier::Other)); | ||||
|         if !has_other { | ||||
|             return Err(anyhow!("Other entry is required")); | ||||
|         } | ||||
|  | ||||
|         let has_mask = self.entries.iter().any(|x| matches!(x.0, Qualifier::Mask)); | ||||
|         if !has_mask { | ||||
|             return Err(anyhow!("Mask entry is required")); | ||||
|         } | ||||
|  | ||||
|         for acl_entry in &self.entries { | ||||
|             let mut entry = null_mut(); | ||||
|             check_return( | ||||
|                 unsafe { acl_create_entry(&mut acl_buf.0, &mut entry) }, | ||||
|                 "acl_create_entry", | ||||
|             ); | ||||
|  | ||||
|             if entry.is_null() { | ||||
|                 return Err(anyhow!("Failed to create ACL entry")); | ||||
|             } | ||||
|  | ||||
|             check_return( | ||||
|                 unsafe { acl_set_tag_type(entry, acl_entry.0.get_tag()) }, | ||||
|                 "acl_set_tag_type", | ||||
|             ); | ||||
|  | ||||
|             if let Some(uid) = acl_entry.0.get_uid() { | ||||
|                 check_return( | ||||
|                     unsafe { acl_set_qualifier(entry, addr_of!(uid).cast::<c_void>()) }, | ||||
|                     "acl_set_qualifier", | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             let mut permset: acl_permset_t = null_mut(); | ||||
|             check_return( | ||||
|                 unsafe { acl_get_permset(entry, &mut permset) }, | ||||
|                 "acl_get_permset", | ||||
|             ); | ||||
|  | ||||
|             check_return(unsafe { acl_clear_perms(permset) }, "acl_clear_perms"); | ||||
|  | ||||
|             check_return( | ||||
|                 unsafe { acl_add_perm(permset, acl_entry.1.bits()) }, | ||||
|                 "acl_add_perm", | ||||
|             ); | ||||
|  | ||||
|             check_return( | ||||
|                 unsafe { acl_set_permset(entry, permset) }, | ||||
|                 "acl_set_permset", | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         let cpath = CString::new(path.as_ref().to_str().unwrap()).unwrap(); | ||||
|         unsafe { acl_set_file(cpath.as_ptr(), acl_type, acl_buf.0) }; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct AclPtr<T>(pub(crate) *mut T); | ||||
|  | ||||
| impl<T> Drop for AclPtr<T> { | ||||
|     fn drop(&mut self) { | ||||
|         if !self.0.is_null() { | ||||
|             check_return(unsafe { acl_free(self.0.cast()) }, "acl_free"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(crate) fn check_return(ret: i32, func: &str) { | ||||
|     println!("ret: {} fnc: {}", ret, func); | ||||
|     assert_eq!( | ||||
|         ret, | ||||
|         0, | ||||
|         "Error in {}: {}", | ||||
|         func, | ||||
|         std::io::Error::last_os_error() | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use super::*; | ||||
|     use std::fs::File; | ||||
|     use std::io::Write; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_acl() { | ||||
|         let mut acl = PosixACL::new(ACL_RWX, ACL_RWX, ACL_RWX); | ||||
|         acl.set(ACLEntry(Qualifier::User(1000), ACL_RWX)); | ||||
|         acl.set(ACLEntry(Qualifier::Group(1000), ACL_RWX)); | ||||
|         acl.set(ACLEntry(Qualifier::Other, ACL_RWX)); | ||||
|         acl.set(ACLEntry(Qualifier::Mask, ACL_READ)); | ||||
|  | ||||
|         let path = "test.txt"; | ||||
|  | ||||
|         let mut file = File::create(path).unwrap(); | ||||
|         file.write_all(b"Hello, world!").unwrap(); | ||||
|  | ||||
|         acl.write(path).unwrap(); | ||||
|  | ||||
|         let acl_r = PosixACL::new_from_file(path, false).unwrap(); | ||||
|         println!("{:?}", acl); | ||||
|         assert_eq!(acl.entries.len(), acl_r.entries.len()); | ||||
|         let missing = acl | ||||
|             .entries | ||||
|             .iter() | ||||
|             .filter(|x| acl_r.entries.iter().find(|y| x == y).is_none()); | ||||
|         assert_eq!(missing.count(), 0); | ||||
|  | ||||
|         let additional = acl_r | ||||
|             .entries | ||||
|             .iter() | ||||
|             .filter(|x| acl.entries.iter().find(|y| x == y).is_none()); | ||||
|         assert_eq!(additional.count(), 0); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Fabian Stamm
					Fabian Stamm