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