First working implementation
This commit is contained in:
commit
62899a7d26
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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user