commit 62899a7d2662de6f712f534a1be3d06c0d82b000 Author: Fabian Stamm Date: Sat Aug 17 23:59:08 2024 +0200 First working implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a57d1c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +test.txt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5ec8f94 --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c8345e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "posix-acl" +version = "0.1.0" +edition = "2021" + +[dependencies] +acl-sys = "1.2" +anyhow = "1" +bitflags = "2" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..11d971f --- /dev/null +++ b/src/lib.rs @@ -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 { + 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, +} + +impl PosixACL { + pub fn new_from_file(path: impl Into, default: bool) -> Result { + 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 = 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 = 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>(&self, path: P) -> Result<()> { + // Write ACL to file + self.write_type(path, ACL_TYPE_ACCESS)?; + Ok(()) + } + + pub fn write_default>(&self, path: P) -> Result<()> { + // Write default ACL to file + self.write_type(path, ACL_TYPE_DEFAULT)?; + Ok(()) + } + + fn write_type>(&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::()) }, + "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(pub(crate) *mut T); + +impl Drop for AclPtr { + 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); + } +}