use crate::failx::fail_custom;
use crate::flag::Flag;
use crate::format;
use crate::perm::Perm;
use crate::qualifier::Qualifier;
use crate::util::*;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::io;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum AclEntryKind {
User,
Group,
#[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))]
Mask,
#[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))]
Other,
#[cfg(any(docsrs, target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))]
Everyone,
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct AclEntry {
pub kind: AclEntryKind,
pub name: String,
pub perms: Perm,
#[cfg_attr(feature = "serde", serde(default))]
pub flags: Flag,
#[cfg_attr(feature = "serde", serde(default = "default_allow"))]
pub allow: bool,
}
#[cfg(feature = "serde")]
const fn default_allow() -> bool {
true
}
impl Ord for AclEntry {
fn cmp(&self, other: &Self) -> Ordering {
match (self.flags.is_empty(), other.flags.is_empty()) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
_ => (),
}
let ret = self.allow.cmp(&other.allow);
if ret != Ordering::Equal {
return ret;
}
let ret = self.kind.cmp(&other.kind);
if ret != Ordering::Equal {
return ret;
}
self.name.cmp(&other.name)
}
}
impl PartialOrd for AclEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl AclEntry {
#[must_use]
fn new(
kind: AclEntryKind,
name: &str,
perms: Perm,
flags: Option<Flag>,
allow: bool,
) -> AclEntry {
AclEntry {
kind,
name: String::from(name),
perms,
flags: flags.unwrap_or_default(),
allow,
}
}
#[must_use]
pub fn allow_user<F>(name: &str, perms: Perm, flags: F) -> AclEntry
where
F: Into<Option<Flag>>,
{
AclEntry::new(AclEntryKind::User, name, perms, flags.into(), true)
}
#[must_use]
pub fn allow_group<F>(name: &str, perms: Perm, flags: F) -> AclEntry
where
F: Into<Option<Flag>>,
{
AclEntry::new(AclEntryKind::Group, name, perms, flags.into(), true)
}
#[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))]
#[must_use]
pub fn allow_mask<F>(perms: Perm, flags: F) -> AclEntry
where
F: Into<Option<Flag>>,
{
AclEntry::new(AclEntryKind::Mask, "", perms, flags.into(), true)
}
#[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))]
#[must_use]
pub fn allow_other<F>(perms: Perm, flags: F) -> AclEntry
where
F: Into<Option<Flag>>,
{
AclEntry::new(AclEntryKind::Other, "", perms, flags.into(), true)
}
#[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))]
#[must_use]
pub fn deny_user<F>(name: &str, perms: Perm, flags: F) -> AclEntry
where
F: Into<Option<Flag>>,
{
AclEntry::new(AclEntryKind::User, name, perms, flags.into(), false)
}
#[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))]
#[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))]
#[must_use]
pub fn deny_group<F>(name: &str, perms: Perm, flags: F) -> AclEntry
where
F: Into<Option<Flag>>,
{
AclEntry::new(AclEntryKind::Group, name, perms, flags.into(), false)
}
pub(crate) fn from_raw(entry: acl_entry_t, acl: acl_t) -> io::Result<AclEntry> {
let (allow, qualifier, perms, flags) = xacl_get_entry(acl, entry)?;
let (kind, name) = match qualifier {
Qualifier::Unknown(s) => (AclEntryKind::Unknown, s),
#[cfg(target_os = "macos")]
Qualifier::User(_) | Qualifier::Guid(_) => (AclEntryKind::User, qualifier.name()?),
#[cfg(target_os = "macos")]
Qualifier::Group(_) => (AclEntryKind::Group, qualifier.name()?),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Qualifier::User(_) | Qualifier::UserObj => (AclEntryKind::User, qualifier.name()?),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Qualifier::Group(_) | Qualifier::GroupObj => (AclEntryKind::Group, qualifier.name()?),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Qualifier::Mask => (AclEntryKind::Mask, qualifier.name()?),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
Qualifier::Other => (AclEntryKind::Other, qualifier.name()?),
#[cfg(target_os = "freebsd")]
Qualifier::Everyone => (AclEntryKind::Everyone, qualifier.name()?),
};
Ok(AclEntry {
kind,
name,
perms,
flags,
allow,
})
}
pub(crate) fn add_to_acl(&self, acl: &mut acl_t) -> io::Result<()> {
let qualifier = self.qualifier()?;
xacl_add_entry(acl, self.allow, &qualifier, self.perms, self.flags)?;
Ok(())
}
fn qualifier(&self) -> io::Result<Qualifier> {
let qualifier = match self.kind {
AclEntryKind::User => Qualifier::user_named(&self.name)?,
AclEntryKind::Group => Qualifier::group_named(&self.name)?,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
AclEntryKind::Mask => Qualifier::mask_named(&self.name)?,
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
AclEntryKind::Other => Qualifier::other_named(&self.name)?,
#[cfg(target_os = "freebsd")]
AclEntryKind::Everyone => Qualifier::everyone_named(&self.name)?,
AclEntryKind::Unknown => {
return fail_custom("unsupported kind: \"unknown\"");
}
};
Ok(qualifier)
}
}
impl fmt::Display for AclEntryKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
format::write_aclentrykind(f, *self)
}
}
impl std::str::FromStr for AclEntryKind {
type Err = format::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"u" => Ok(AclEntryKind::User),
"g" => Ok(AclEntryKind::Group),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
"o" => Ok(AclEntryKind::Other),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
"m" => Ok(AclEntryKind::Mask),
_ => format::read_aclentrykind(s),
}
}
}
impl fmt::Display for AclEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let allow = if self.allow { "allow" } else { "deny" };
write!(
f,
"{}:{}:{}:{}:{}",
allow, self.flags, self.kind, self.name, self.perms
)
}
}
fn parse_allow(value: &str) -> Result<bool, format::Error> {
let result = match value {
"allow" => true,
"deny" => false,
s => {
return Err(format::Error::Message(format!(
"Unknown variant `{s}`, expected one of `allow`, `deny`"
)));
}
};
Ok(result)
}
impl std::str::FromStr for AclEntry {
type Err = format::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let fields = s.splitn(5, ':').map(str::trim).collect::<Vec<&str>>();
let entry = match fields.len() {
5 => {
let allow = parse_allow(fields[0])?;
let flags = fields[1].parse::<Flag>()?;
let kind = fields[2].parse::<AclEntryKind>()?;
let name = fields[3];
let perms = fields[4].parse::<Perm>()?;
AclEntry::new(kind, name, perms, Some(flags), allow)
}
4 => {
let allow = true;
let flags = fields[0].parse::<Flag>()?;
let kind = fields[1].parse::<AclEntryKind>()?;
let name = fields[2];
let perms = fields[3].parse::<Perm>()?;
AclEntry::new(kind, name, perms, Some(flags), allow)
}
3 => {
let allow = true;
let flags = Flag::empty();
let kind = fields[0].parse::<AclEntryKind>()?;
let name = fields[1];
let perms = fields[2].parse::<Perm>()?;
AclEntry::new(kind, name, perms, Some(flags), allow)
}
_ => return Err(format::Error::Message(format!("Unknown ACL format: `{s}`"))),
};
Ok(entry)
}
}
#[cfg(test)]
mod aclentry_tests {
use super::*;
#[test]
fn test_ordering() {
let mut acl = vec![
AclEntry::allow_user("f", Perm::WRITE, None),
AclEntry::allow_group("3", Perm::EXECUTE, None),
AclEntry::allow_group("d", Perm::EXECUTE, None),
AclEntry::allow_user("z", Perm::READ, None),
AclEntry::allow_group("z", Perm::READ, None),
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
AclEntry::deny_user("a", Perm::READ, Flag::FILE_INHERIT),
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
AclEntry::deny_user("c", Perm::READ, None),
];
acl.sort();
let acl_sorted = vec![
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
AclEntry::deny_user("c", Perm::READ, None),
AclEntry::allow_user("f", Perm::WRITE, None),
AclEntry::allow_user("z", Perm::READ, None),
AclEntry::allow_group("3", Perm::EXECUTE, None),
AclEntry::allow_group("d", Perm::EXECUTE, None),
AclEntry::allow_group("z", Perm::READ, None),
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
AclEntry::deny_user("a", Perm::READ, Flag::FILE_INHERIT),
];
assert_eq!(acl, acl_sorted);
}
#[test]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn test_ordering_linux() {
let mut acl = vec![
AclEntry::allow_user("f", Perm::WRITE, None),
AclEntry::allow_mask(Perm::READ, None),
AclEntry::allow_group("b", Perm::EXECUTE, None),
AclEntry::allow_group("d", Perm::EXECUTE, None),
AclEntry::allow_user("z", Perm::READ, None),
AclEntry::allow_user("", Perm::READ, None),
AclEntry::allow_other(Perm::EXECUTE, None),
AclEntry::allow_group("z", Perm::READ, None),
AclEntry::allow_group("", Perm::READ, None),
AclEntry::allow_group("a", Perm::READ, Flag::DEFAULT),
];
acl.sort();
let acl_sorted = vec![
AclEntry::allow_user("", Perm::READ, None),
AclEntry::allow_user("f", Perm::WRITE, None),
AclEntry::allow_user("z", Perm::READ, None),
AclEntry::allow_group("", Perm::READ, None),
AclEntry::allow_group("b", Perm::EXECUTE, None),
AclEntry::allow_group("d", Perm::EXECUTE, None),
AclEntry::allow_group("z", Perm::READ, None),
AclEntry::allow_mask(Perm::READ, None),
AclEntry::allow_other(Perm::EXECUTE, None),
AclEntry::allow_group("a", Perm::READ, Flag::DEFAULT),
];
assert_eq!(acl, acl_sorted);
}
#[test]
fn test_display_kind() {
assert_eq!(format!("{}", AclEntryKind::User), "user");
assert_eq!(format!("{}", AclEntryKind::Group), "group");
}
#[test]
#[cfg(target_os = "macos")]
fn test_display_entry() {
let perms = Perm::READ | Perm::EXECUTE;
let flags = Flag::INHERITED | Flag::FILE_INHERIT;
let entry = AclEntry::allow_user("x", perms, flags);
assert_eq!(
format!("{entry}"),
"allow:inherited,file_inherit:user:x:read,execute"
);
}
#[test]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn test_display_entry() {
let perms = Perm::READ | Perm::EXECUTE;
let flags = Flag::DEFAULT;
let entry = AclEntry::allow_user("x", perms, flags);
assert_eq!(format!("{entry}"), "allow:default:user:x:read,execute");
}
#[test]
fn test_display_entry_name() {
let perms = Perm::READ;
let entry = AclEntry::allow_user("x:y", perms, None);
assert_eq!(format!("{entry}"), "allow::user:x:y:read");
}
#[test]
#[cfg(target_os = "macos")]
fn test_entry_fromstr() {
let entry = "allow:inherited:user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow:inherited:user:x:read");
let entry = "allow::user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow::user:x:read");
let entry = "user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow::user:x:read");
let entry = " deny : inherited : user : x : read "
.parse::<AclEntry>()
.unwrap();
assert_eq!(entry.to_string(), "deny:inherited:user:x:read");
let entry = "inherited:user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow:inherited:user:x:read");
}
#[test]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn test_entry_fromstr() {
let entry = "allow:default:user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow:default:user:x:read");
let entry = "allow::user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow::user:x:read");
let entry = "user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow::user:x:read");
let entry = " deny : default : user : x : read "
.parse::<AclEntry>()
.unwrap();
assert_eq!(entry.to_string(), "deny:default:user:x:read");
let entry = "default:user:x:read".parse::<AclEntry>().unwrap();
assert_eq!(entry.to_string(), "allow:default:user:x:read");
}
#[test]
fn test_entry_fromstr_err() {
let err = "all::user:x:read".parse::<AclEntry>().unwrap_err();
assert_eq!(
err.to_string(),
"Unknown variant `all`, expected one of `allow`, `deny`"
);
let err = "allow:foo".parse::<AclEntry>().unwrap_err();
assert_eq!(err.to_string(), "Unknown ACL format: `allow:foo`");
}
#[test]
fn test_entry_fromstr_roundtrip() {
let values = [
("user:a:read", "allow::user:a:read"),
("group:b:write", "allow::group:b:write"),
("unknown:c:execute", "allow::unknown:c:execute"),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
("other:d:execute", "allow::other:d:execute"),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
("mask:e:write,read", "allow::mask:e:read,write"),
];
for (input, expected) in &values {
let entry = input.parse::<AclEntry>().unwrap();
assert_eq!(*expected, entry.to_string());
}
}
#[test]
fn test_entry_fromstr_examples() {
let values = [
("u:admin:rwx", "allow::user:admin:read,write,execute"),
("g::rw", "allow::group::read,write"),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
("default:user:admin:r", "allow:default:user:admin:read"),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
("d:group:admin:w", "allow:default:group:admin:write"),
("deny::u:self:x", "deny::user:self:execute"),
];
for (input, expected) in &values {
let entry = input.parse::<AclEntry>().unwrap();
assert_eq!(*expected, entry.to_string());
}
}
#[test]
fn test_kind_fromstr() {
assert_eq!(AclEntryKind::User, "user".parse::<AclEntryKind>().unwrap());
assert_eq!(
AclEntryKind::Group,
"group".parse::<AclEntryKind>().unwrap()
);
assert_eq!(
AclEntryKind::Unknown,
"unknown".parse::<AclEntryKind>().unwrap()
);
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
assert_eq!(AclEntryKind::Mask, "mask".parse::<AclEntryKind>().unwrap());
#[cfg(target_os = "macos")]
assert_eq!(
"unknown variant `x`, expected one of `user`, `group`, `unknown`",
"x".parse::<AclEntryKind>().unwrap_err().to_string()
);
#[cfg(target_os = "linux")]
assert_eq!(
"unknown variant `x`, expected one of `user`, `group`, `mask`, `other`, `unknown`",
"x".parse::<AclEntryKind>().unwrap_err().to_string()
);
#[cfg(target_os = "freebsd")]
assert_eq!(
"unknown variant `x`, expected one of `user`, `group`, `mask`, `other`, `everyone`, `unknown`",
"x".parse::<AclEntryKind>().unwrap_err().to_string()
);
}
}