use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use runner_manager_domain::path::LocalAbsolutePath;
use crate::paths::AppPaths;
#[cfg(windows)]
use crate::runner_root::RootPreflight;
use crate::runner_root::{RootOwner, RunnerRootError};
pub const ADMITTED_RIGHTS: &str = "FRFWFXSD";
pub const INHERITANCE: &str = "OICI";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootAdmission {
LocalSystem,
Account(String),
}
impl RootAdmission {
#[cfg(windows)]
pub fn of_this_account() -> Result<Self, RootAccessError> {
crate::process::current_user_sid()
.map(Self::Account)
.map_err(|source| RootAccessError::Identity { source })
}
#[must_use]
pub fn sid(&self) -> Option<&str> {
match self {
Self::LocalSystem => None,
Self::Account(sid) => Some(sid),
}
}
#[must_use]
pub fn admits(&self) -> Vec<AdmittedTrustee> {
let mut admitted = vec![
AdmittedTrustee::LocalSystem,
AdmittedTrustee::Administrators,
];
if matches!(self, Self::Account(_)) {
admitted.push(AdmittedTrustee::SelectedAccount);
}
admitted
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AdmittedTrustee {
LocalSystem,
Administrators,
SelectedAccount,
}
impl fmt::Display for AdmittedTrustee {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::LocalSystem => "NT AUTHORITY\\SYSTEM",
Self::Administrators => "the local Administrators group",
Self::SelectedAccount => "the invoking user",
})
}
}
#[must_use]
pub fn default_root_sddl(admission: &RootAdmission) -> String {
let mut sddl = format!("D:P(A;{INHERITANCE};FA;;;SY)(A;{INHERITANCE};FA;;;BA)");
if let Some(sid) = admission.sid()
&& !already_admitted(sid)
{
sddl.push_str(&format!("(A;{INHERITANCE};{ADMITTED_RIGHTS};;;{sid})"));
}
sddl
}
fn already_admitted(sid: &str) -> bool {
const COVERED: [&str; 4] = ["SY", "BA", SID_LOCAL_SYSTEM, SID_ADMINISTRATORS];
COVERED.iter().any(|known| sid.eq_ignore_ascii_case(known))
}
const SID_LOCAL_SYSTEM: &str = "S-1-5-18";
const SID_ADMINISTRATORS: &str = "S-1-5-32-544";
pub const WRITE_MASK: u32 = 0x1000_0000
| 0x4000_0000
| 0x0008_0000
| 0x0004_0000
| 0x0001_0000
| 0x0000_0100
| 0x0000_0040
| 0x0000_0010
| 0x0000_0004
| 0x0000_0002;
const BROAD_TRUSTEES: &[&str] = &[
"WD", "S-1-1-0", "AU", "S-1-5-11", "BU", "S-1-5-32-545", "BG", "S-1-5-32-546", "DU", "IU", "S-1-5-4", "AN", "S-1-5-7", "WR", "LU", ];
struct Ace<'a> {
kind: &'a str,
rights: &'a str,
trustee: &'a str,
}
fn aces(descriptor: &str) -> Option<Vec<Ace<'_>>> {
let body = descriptor.split("D:").nth(1)?;
if body
.split('(')
.next()
.is_some_and(|flags| flags.contains("NO_ACCESS_CONTROL"))
{
return None;
}
Some(
body.split('(')
.skip(1)
.filter_map(|ace| {
let ace = ace.split(')').next()?;
let fields: Vec<&str> = ace.split(';').collect();
Some(Ace {
kind: fields.first()?.trim(),
rights: fields.get(2)?.trim(),
trustee: fields.get(5)?.trim(),
})
})
.collect(),
)
}
impl Ace<'_> {
fn is_allow(&self) -> bool {
self.kind.starts_with('A') || (self.kind.starts_with('X') && self.kind.contains('A'))
}
fn grants_write(&self) -> bool {
rights_mask(self.rights).is_none_or(|mask| mask & WRITE_MASK != 0)
}
}
fn rights_mask(field: &str) -> Option<u32> {
if field.is_empty() {
return Some(0);
}
if !field.is_ascii() {
return None;
}
if let Some(hex) = field
.strip_prefix("0x")
.or_else(|| field.strip_prefix("0X"))
{
return u32::from_str_radix(hex, 16).ok();
}
if !field.len().is_multiple_of(2) {
return None;
}
let mut mask = 0u32;
for index in (0..field.len()).step_by(2) {
let token = field[index..index + 2].to_ascii_uppercase();
mask |= match token.as_str() {
"GA" => 0x1000_0000,
"GR" => 0x8000_0000,
"GW" => 0x4000_0000,
"GX" => 0x2000_0000,
"SD" => 0x0001_0000,
"RC" => 0x0002_0000,
"WD" => 0x0004_0000,
"WO" => 0x0008_0000,
"CC" => 0x0000_0001,
"DC" => 0x0000_0002,
"LC" => 0x0000_0004,
"SW" => 0x0000_0008,
"RP" => 0x0000_0010,
"WP" => 0x0000_0020,
"DT" => 0x0000_0040,
"LO" => 0x0000_0080,
"CR" => 0x0000_0100,
"FA" => 0x001F_01FF,
"FR" => 0x0012_0089,
"FW" => 0x0012_0116,
"FX" => 0x0012_00A0,
"KA" => 0x000F_003F,
"KR" | "KX" => 0x0002_0019,
"KW" => 0x0002_0006,
_ => return None,
};
}
Some(mask)
}
#[must_use]
pub fn grants_broad_write(descriptor: &str) -> bool {
let Some(aces) = aces(descriptor) else {
return true;
};
aces.iter().any(|ace| {
ace.is_allow()
&& BROAD_TRUSTEES
.iter()
.any(|broad| ace.trustee.eq_ignore_ascii_case(broad))
&& ace.grants_write()
})
}
#[must_use]
pub fn is_protected(descriptor: &str) -> bool {
descriptor.split("D:").nth(1).is_some_and(|body| {
body.chars()
.take_while(|character| *character != '(')
.any(|character| character == 'P')
})
}
#[must_use]
pub fn write_trustees(descriptor: &str) -> BTreeSet<String> {
write_grants(descriptor).into_keys().collect()
}
fn write_grants(descriptor: &str) -> BTreeMap<String, u32> {
let mut grants: BTreeMap<String, u32> = BTreeMap::new();
for ace in aces(descriptor).unwrap_or_default() {
if ace.is_allow() && ace.grants_write() {
*grants.entry(canonical_trustee(ace.trustee)).or_default() |=
rights_mask(ace.rights).unwrap_or(u32::MAX);
}
}
grants
}
fn canonical_trustee(trustee: &str) -> String {
let upper = trustee.to_ascii_uppercase();
match upper.as_str() {
"SY" => SID_LOCAL_SYSTEM.to_owned(),
"BA" => SID_ADMINISTRATORS.to_owned(),
_ => upper,
}
}
#[must_use]
pub fn admits_exactly(descriptor: &str, admission: &RootAdmission) -> bool {
if !is_protected(descriptor) {
return false;
}
let full_control = rights_mask("FA").unwrap_or(u32::MAX);
let mut expected: BTreeMap<String, u32> = [
(SID_LOCAL_SYSTEM.to_owned(), full_control),
(SID_ADMINISTRATORS.to_owned(), full_control),
]
.into_iter()
.collect();
if let Some(sid) = admission.sid() {
*expected.entry(canonical_trustee(sid)).or_default() |=
rights_mask(ADMITTED_RIGHTS).unwrap_or(u32::MAX);
}
write_grants(descriptor) == expected
}
#[must_use]
pub fn redact(descriptor: &str) -> String {
const PREFIX: &str = "S-1-5-21-";
let mut out = String::with_capacity(descriptor.len());
let mut rest = descriptor;
while let Some(start) = rest.find(PREFIX) {
out.push_str(&rest[..start]);
out.push_str("S-1-5-21-<account>");
let tail = &rest[start + PREFIX.len()..];
let end = tail
.find(|character: char| !character.is_ascii_digit() && character != '-')
.unwrap_or(tail.len());
rest = &tail[end..];
}
out.push_str(rest);
out
}
#[derive(Debug, thiserror::Error)]
pub enum RootAccessError {
#[error("{source}")]
Resolve {
#[source]
source: Box<RunnerRootError>,
},
#[error(
"this account's identity could not be read, so the runner root cannot be given the \
access a login-mode registration needs: {source}"
)]
Identity {
#[source]
source: io::Error,
},
#[error(
"{} already exists and grants write access to ordinary local users, so it is not a \
safe place to run jobs: its access control is {dacl}. This is what a directory \
created below {} with inheritance left on looks like, and it is refused rather \
than tightened because the contents of a directory anybody could write cannot be \
trusted. Remove or empty it and run this again, or point the runner root somewhere \
this account controls with `{remediation}`.",
path.display(),
volume.display()
)]
BroadExistingAccess {
path: PathBuf,
dacl: String,
volume: PathBuf,
remediation: String,
},
#[error(
"the default runner root {} could not be created: {source}. Create it as an \
administrator, or configure a directory this account owns with `{remediation}`.",
path.display()
)]
Create {
path: PathBuf,
#[source]
source: io::Error,
remediation: String,
},
#[error(
"the access control of the default runner root {} could not be read: {source}. \
Without it there is no way to tell whether unrelated local users can write there, \
so this fails closed. Read it as an administrator, or configure a directory this \
account owns with `{remediation}`.",
path.display()
)]
Inspect {
path: PathBuf,
#[source]
source: io::Error,
remediation: String,
},
#[error(
"the access control of the default runner root {} could not be applied: {source}. \
Changing a directory's access control needs WRITE_DAC, which this account has \
only as its owner or as an administrator — an elevated shell is the usual answer. \
Otherwise configure a directory this account owns with `{remediation}`.",
path.display()
)]
Apply {
path: PathBuf,
#[source]
source: io::Error,
remediation: String,
},
}
impl RootAccessError {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match self {
Self::Resolve { .. } | Self::Identity { .. } => None,
Self::BroadExistingAccess { path, .. }
| Self::Create { path, .. }
| Self::Inspect { path, .. }
| Self::Apply { path, .. } => Some(path),
}
}
}
#[cfg_attr(
not(windows),
allow(
dead_code,
reason = "only `reconcile`, which is Windows-only, builds an error that carries a remedy"
)
)]
fn remediation() -> String {
RootOwner::Host.remediation()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootAccessSummary {
NotApplicable,
Created {
path: PathBuf,
admits: Vec<AdmittedTrustee>,
},
AlreadyReconciled {
path: PathBuf,
admits: Vec<AdmittedTrustee>,
},
Reconciled {
path: PathBuf,
admits: Vec<AdmittedTrustee>,
},
}
impl RootAccessSummary {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match self {
Self::NotApplicable => None,
Self::Created { path, .. }
| Self::AlreadyReconciled { path, .. }
| Self::Reconciled { path, .. } => Some(path),
}
}
#[must_use]
pub const fn created(&self) -> bool {
matches!(self, Self::Created { .. })
}
}
impl fmt::Display for RootAccessSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (verb, path, admits) = match self {
Self::NotApplicable => {
return f.write_str(
"The runner root's access control was not created or changed by this \
operation.",
);
}
Self::Created { path, admits } => ("was created admitting", path, admits),
Self::AlreadyReconciled { path, admits } => ("already admitted", path, admits),
Self::Reconciled { path, admits } => ("was reconciled to admit", path, admits),
};
let names: Vec<String> = admits.iter().map(ToString::to_string).collect();
write!(
f,
"The runner root {} {verb} {}, and inherits nothing from the volume above it, so \
unrelated local users cannot write there.",
path.display(),
names.join(", ")
)
}
}
#[derive(Debug, Clone)]
pub struct RootAccessChange {
summary: RootAccessSummary,
#[cfg(windows)]
previous_dacl: Option<String>,
}
impl RootAccessChange {
#[must_use]
pub const fn not_applicable() -> Self {
Self {
summary: RootAccessSummary::NotApplicable,
#[cfg(windows)]
previous_dacl: None,
}
}
#[must_use]
pub const fn summary(&self) -> &RootAccessSummary {
&self.summary
}
#[must_use]
pub fn revert(&self) -> Reversal {
#[cfg(windows)]
{
match &self.summary {
RootAccessSummary::NotApplicable | RootAccessSummary::AlreadyReconciled { .. } => {
Reversal::NothingToUndo
}
RootAccessSummary::Created { path, .. } => match std::fs::remove_dir(path) {
Ok(()) => Reversal::Removed { path: path.clone() },
Err(source) if source.kind() == io::ErrorKind::NotFound => {
Reversal::NothingToUndo
}
Err(source) => Reversal::Retained {
path: path.clone(),
detail: format!(
"the directory this operation created could not be removed again \
({source}); it is empty unless something else has written to it, \
and removing it by hand is safe"
),
},
},
RootAccessSummary::Reconciled { path, .. } => {
let Some(previous) = self.previous_dacl.as_deref() else {
return Reversal::NothingToUndo;
};
match sys::write_dacl(path, previous) {
Ok(()) => Reversal::Restored { path: path.clone() },
Err(source) => Reversal::Retained {
path: path.clone(),
detail: format!(
"this directory existed before this operation and could not be \
removed by it; its previous access control could not be put \
back either ({source}), so it now carries the access control \
this operation applied"
),
},
}
}
}
}
#[cfg(not(windows))]
{
Reversal::NothingToUndo
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Reversal {
NothingToUndo,
Removed {
path: PathBuf,
},
Restored {
path: PathBuf,
},
Retained {
path: PathBuf,
detail: String,
},
}
impl fmt::Display for Reversal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NothingToUndo => f.write_str("the runner root was left as it was found"),
Self::Removed { path } => {
write!(f, "the runner root {} was removed again", path.display())
}
Self::Restored { path } => write!(
f,
"the previous access control of {} was restored",
path.display()
),
Self::Retained { path, detail } => write!(f, "{}: {detail}", path.display()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootAccessReport {
NotApplicable,
Absent,
Unreadable {
detail: String,
},
Present {
dacl: String,
protected: bool,
broad_write: bool,
},
}
impl fmt::Display for RootAccessReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotApplicable => f.write_str("no Windows access control applies"),
Self::Absent => f.write_str("does not exist yet"),
Self::Unreadable { detail } => {
write!(f, "exists, but its access control cannot be read: {detail}")
}
Self::Present {
dacl,
protected,
broad_write,
} => write!(
f,
"{}; {}; {dacl}",
if *broad_write {
"ordinary local users can write there"
} else {
"no ordinary local user can write there"
},
if *protected {
"inherits nothing from the volume above"
} else {
"inherits from the volume above"
}
),
}
}
}
#[must_use]
pub fn report(path: &Path) -> RootAccessReport {
#[cfg(windows)]
{
match std::fs::symlink_metadata(path) {
Ok(_) => {}
Err(source) if source.kind() == io::ErrorKind::NotFound => {
return RootAccessReport::Absent;
}
Err(source) => {
return RootAccessReport::Unreadable {
detail: source.to_string(),
};
}
}
match crate::process::permissions_summary(path) {
Ok(summary) => RootAccessReport::Present {
protected: is_protected(&summary.description),
broad_write: grants_broad_write(&summary.description),
dacl: redact(&summary.description),
},
Err(source) => RootAccessReport::Unreadable {
detail: source.to_string(),
},
}
}
#[cfg(not(windows))]
{
let _ = path;
RootAccessReport::NotApplicable
}
}
pub fn ensure_default_root(
paths: &AppPaths,
admission: &RootAdmission,
) -> Result<RootAccessChange, RootAccessError> {
#[cfg(windows)]
{
let root = crate::runner_root::default_runner_root(paths).map_err(|source| {
RootAccessError::Resolve {
source: Box::new(source),
}
})?;
reconcile(paths, &root, admission)
}
#[cfg(not(windows))]
{
let _ = (paths, admission);
Ok(RootAccessChange::not_applicable())
}
}
#[cfg(windows)]
pub(crate) fn reconcile(
paths: &AppPaths,
root: &LocalAbsolutePath,
admission: &RootAdmission,
) -> Result<RootAccessChange, RootAccessError> {
let checked = RootPreflight::new(paths)
.check(&RootOwner::Host, root)
.map_err(|source| RootAccessError::Resolve {
source: Box::new(source),
})?;
let desired = default_root_sddl(admission);
let path = root.as_path().to_path_buf();
if checked.leaf_to_create().is_some() {
match sys::create_with_dacl(&path, &desired) {
Ok(()) => {
return Ok(RootAccessChange {
summary: RootAccessSummary::Created {
path,
admits: admission.admits(),
},
previous_dacl: None,
});
}
Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
Err(source) => {
return Err(RootAccessError::Create {
path,
source,
remediation: remediation(),
});
}
}
}
let current = sys::read_dacl(&path).map_err(|source| RootAccessError::Inspect {
path: path.clone(),
source,
remediation: remediation(),
})?;
if grants_broad_write(¤t) {
return Err(RootAccessError::BroadExistingAccess {
dacl: redact(¤t),
volume: volume_of(&path),
path,
remediation: remediation(),
});
}
if admits_exactly(¤t, admission) {
return Ok(RootAccessChange {
summary: RootAccessSummary::AlreadyReconciled {
path,
admits: admission.admits(),
},
previous_dacl: None,
});
}
sys::write_dacl(&path, &desired).map_err(|source| RootAccessError::Apply {
path: path.clone(),
source,
remediation: remediation(),
})?;
Ok(RootAccessChange {
summary: RootAccessSummary::Reconciled {
path,
admits: admission.admits(),
},
previous_dacl: Some(current),
})
}
#[cfg(all(test, windows))]
pub(crate) fn create_with_descriptor_for_tests(path: &Path, sddl: &str) -> io::Result<()> {
sys::create_with_dacl(path, sddl)
}
#[cfg(windows)]
fn volume_of(path: &Path) -> PathBuf {
path.ancestors()
.last()
.map_or_else(|| path.to_path_buf(), Path::to_path_buf)
}
#[cfg(windows)]
mod sys {
use std::ffi::OsStr;
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows::Win32::Foundation::{ERROR_SUCCESS, HLOCAL, LocalFree};
use windows::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, SE_FILE_OBJECT,
SetNamedSecurityInfoW,
};
use windows::Win32::Security::{
ACL, DACL_SECURITY_INFORMATION, GetSecurityDescriptorDacl,
PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
UNPROTECTED_DACL_SECURITY_INFORMATION,
};
use windows::Win32::Storage::FileSystem::CreateDirectoryW;
use windows::core::PCWSTR;
fn to_wide(value: &OsStr) -> Vec<u16> {
value.encode_wide().chain(std::iter::once(0)).collect()
}
fn io_error(error: &windows::core::Error) -> io::Error {
const FACILITY_WIN32: u32 = 0x8007_0000;
let hresult = error.code().0;
let bits = hresult.cast_unsigned();
if bits & 0xFFFF_0000 == FACILITY_WIN32 {
io::Error::from_raw_os_error((bits & 0x0000_FFFF).cast_signed())
} else {
io::Error::from_raw_os_error(hresult)
}
}
struct Descriptor(PSECURITY_DESCRIPTOR);
impl Descriptor {
fn from_sddl(sddl: &str) -> io::Result<Self> {
let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
let mut descriptor = PSECURITY_DESCRIPTOR(std::ptr::null_mut());
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
PCWSTR(wide.as_ptr()),
SDDL_REVISION_1,
&mut descriptor,
None,
)
}
.map_err(|error| io_error(&error))?;
Ok(Self(descriptor))
}
fn dacl(&self) -> io::Result<*const ACL> {
let mut present = windows::core::BOOL(0);
let mut acl: *mut ACL = std::ptr::null_mut();
let mut defaulted = windows::core::BOOL(0);
unsafe { GetSecurityDescriptorDacl(self.0, &mut present, &mut acl, &mut defaulted) }
.map_err(|error| io_error(&error))?;
if !present.as_bool() || acl.is_null() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"the security descriptor built from SDDL carries no DACL",
));
}
Ok(acl.cast_const())
}
}
impl Drop for Descriptor {
fn drop(&mut self) {
unsafe {
let _ = LocalFree(Some(HLOCAL(self.0.0)));
}
}
}
pub(super) fn create_with_dacl(path: &Path, sddl: &str) -> io::Result<()> {
let descriptor = Descriptor::from_sddl(sddl)?;
let attributes = SECURITY_ATTRIBUTES {
nLength: u32::try_from(size_of::<SECURITY_ATTRIBUTES>()).unwrap_or(u32::MAX),
lpSecurityDescriptor: descriptor.0.0,
bInheritHandle: windows::core::BOOL(0),
};
let wide = to_wide(path.as_os_str());
unsafe { CreateDirectoryW(PCWSTR(wide.as_ptr()), Some(&raw const attributes)) }
.map_err(|error| io_error(&error))
}
pub(super) fn read_dacl(path: &Path) -> io::Result<String> {
crate::process::permissions_summary(path)
.map(|summary| summary.description)
.map_err(|error| io::Error::other(error.to_string()))
}
pub(super) fn write_dacl(path: &Path, sddl: &str) -> io::Result<()> {
let descriptor = Descriptor::from_sddl(sddl)?;
let acl = descriptor.dacl()?;
let information = DACL_SECURITY_INFORMATION
| if super::is_protected(sddl) {
PROTECTED_DACL_SECURITY_INFORMATION
} else {
UNPROTECTED_DACL_SECURITY_INFORMATION
};
let wide = to_wide(path.as_os_str());
let status = unsafe {
SetNamedSecurityInfoW(
PCWSTR(wide.as_ptr()),
SE_FILE_OBJECT,
information,
None,
None,
Some(acl),
None,
)
};
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(io::Error::from_raw_os_error(
i32::try_from(status.0).unwrap_or(i32::MAX),
))
}
}
#[cfg(test)]
mod tests {
use std::io;
#[test]
fn a_directory_that_already_exists_reads_back_as_already_exists() {
let directory = tempfile::tempdir().expect("a temporary directory");
let error = super::create_with_dacl(directory.path(), "D:P(A;OICI;FA;;;SY)")
.expect_err("creating a directory that is already there fails");
assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const VOLUME_ROOT: &str = "D:PAI(A;;FA;;;SY)(A;OICIIO;GA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;BA)\
(A;;0x1200a9;;;BU)(A;OICIIO;GXGR;;;BU)(A;;LC;;;BU)(A;CI;DC;;;BU)\
(A;;0x1301bf;;;AU)(A;OICIIO;SDGXGWGR;;;AU)";
const INHERITED_FROM_VOLUME: &str =
"D:AI(A;OICIID;GA;;;SY)(A;OICIID;GA;;;BA)(A;OICIID;GXGR;;;BU)(A;OICIID;SDGXGWGR;;;AU)";
fn account() -> RootAdmission {
RootAdmission::Account("S-1-5-21-1-2-3-1001".to_owned())
}
#[test]
fn a_boot_root_admits_system_and_administrators_and_nothing_else() {
assert_eq!(
default_root_sddl(&RootAdmission::LocalSystem),
"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)"
);
}
#[test]
fn a_login_root_admits_the_selected_account_with_modify_rather_than_full_control() {
assert_eq!(
default_root_sddl(&account()),
"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-1-2-3-1001)"
);
let mask = rights_mask(ADMITTED_RIGHTS).expect("the constant parses");
assert_eq!(mask & 0x0004_0000, 0, "WRITE_DAC must not be granted");
assert_eq!(mask & 0x0008_0000, 0, "WRITE_OWNER must not be granted");
for (bit, name) in [
(0x0000_0002, "FILE_WRITE_DATA"),
(0x0000_0004, "FILE_APPEND_DATA"),
(0x0001_0000, "DELETE"),
(0x0000_0001, "FILE_READ_DATA"),
] {
assert_ne!(mask & bit, 0, "{name} must be granted");
}
}
#[test]
fn a_daemon_running_as_local_system_does_not_add_an_ace_for_itself() {
let as_system = RootAdmission::Account("S-1-5-18".to_owned());
assert_eq!(
default_root_sddl(&as_system),
default_root_sddl(&RootAdmission::LocalSystem)
);
let as_administrators = RootAdmission::Account("s-1-5-32-544".to_owned());
assert_eq!(
default_root_sddl(&as_administrators),
default_root_sddl(&RootAdmission::LocalSystem)
);
}
#[test]
fn every_ace_the_default_writes_is_inherited_by_children() {
for admission in [RootAdmission::LocalSystem, account()] {
let sddl = default_root_sddl(&admission);
for ace in sddl.split('(').skip(1) {
let flags = ace.split(';').nth(1).expect("an ACE has a flags field");
assert_eq!(flags, INHERITANCE, "in {sddl}");
}
}
}
#[test]
fn the_descriptor_this_module_writes_grants_no_broad_write() {
for admission in [RootAdmission::LocalSystem, account()] {
let sddl = default_root_sddl(&admission);
assert!(!grants_broad_write(&sddl), "{sddl}");
assert!(is_protected(&sddl), "{sddl}");
}
}
#[test]
fn a_root_that_inherited_the_volumes_grants_is_broadly_writable() {
assert!(grants_broad_write(INHERITED_FROM_VOLUME));
assert!(!is_protected(INHERITED_FROM_VOLUME));
assert!(grants_broad_write(VOLUME_ROOT));
}
#[test]
fn the_directory_service_spellings_of_create_file_and_create_folder_are_caught() {
assert!(grants_broad_write("D:P(A;OICI;LC;;;BU)"));
assert!(grants_broad_write("D:P(A;OICI;DC;;;BU)"));
assert!(!grants_broad_write("D:P(A;OICI;CC;;;BU)"));
}
#[test]
fn a_broad_read_only_grant_is_not_a_write_grant() {
assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICI;FR;;;WD)"));
assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICI;GR;;;AU)"));
assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICI;FX;;;BU)"));
}
#[test]
fn a_hexadecimal_rights_field_is_read_as_bits() {
assert!(grants_broad_write("D:P(A;;0x1301bf;;;AU)"));
assert!(!grants_broad_write("D:P(A;;0x1200a9;;;BU)"));
}
#[test]
fn a_deny_ace_naming_everyone_is_a_tightening_not_a_leak() {
assert!(!grants_broad_write("D:P(D;OICI;FA;;;WD)(A;OICI;FA;;;SY)"));
}
#[test]
fn an_unparseable_or_missing_descriptor_fails_closed() {
assert!(
grants_broad_write("O:BAG:BA"),
"no DACL is not an empty DACL"
);
assert!(grants_broad_write("D:P(A;OICI;QQ;;;WD)"), "unknown rights");
assert!(grants_broad_write("D:P(A;OICI;FAX;;;AU)"), "odd length");
assert!(grants_broad_write("D:P(A;OICI;0xzz;;;AU)"), "bad hex");
assert!(
grants_broad_write("D:NO_ACCESS_CONTROL"),
"a NULL DACL grants everyone everything; read as a flags field it would otherwise \
parse to zero ACEs and be adopted as the narrowest directory on the machine"
);
assert!(!is_protected("D:NO_ACCESS_CONTROL"));
assert!(write_trustees("D:NO_ACCESS_CONTROL").is_empty());
}
#[test]
fn a_creator_owner_grant_is_not_a_grant_to_an_unrelated_user() {
assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICIIO;GA;;;CO)"));
}
#[test]
fn an_inherit_only_broad_ace_still_counts() {
assert!(grants_broad_write("D:P(A;OICIIO;GW;;;AU)"));
}
#[test]
fn a_root_already_carrying_this_modules_descriptor_needs_no_rewrite() {
for admission in [RootAdmission::LocalSystem, account()] {
let sddl = default_root_sddl(&admission);
assert!(admits_exactly(&sddl, &admission), "{sddl}");
}
}
#[test]
fn a_mode_change_is_visible_as_a_descriptor_that_no_longer_matches() {
let boot = default_root_sddl(&RootAdmission::LocalSystem);
let login = default_root_sddl(&account());
assert!(!admits_exactly(&boot, &account()));
assert!(!admits_exactly(&login, &RootAdmission::LocalSystem));
}
#[test]
fn a_root_that_admits_a_second_account_does_not_match() {
let extra = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-1-2-3-1001)\
(A;OICI;FRFWFXSD;;;S-1-5-21-1-2-3-1002)";
assert!(!admits_exactly(extra, &account()));
}
#[test]
fn a_root_that_grants_the_account_full_control_is_reconciled_rather_than_accepted() {
let too_much = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;S-1-5-21-1-2-3-1001)";
assert!(!grants_broad_write(too_much), "no broad trustee is named");
assert_eq!(
write_trustees(too_much),
write_trustees(&default_root_sddl(&account()))
);
assert!(!admits_exactly(too_much, &account()), "{too_much}");
}
#[test]
fn windows_own_spelling_of_the_admitted_rights_still_matches() {
let rendered = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1301bf;;;S-1-5-21-1-2-3-1001)";
assert!(admits_exactly(rendered, &account()), "{rendered}");
}
#[test]
fn an_unprotected_root_never_matches_however_narrow_it_looks() {
let narrow = "D:AI(A;OICIID;FA;;;SY)(A;OICIID;FA;;;BA)";
assert!(!grants_broad_write(narrow), "nothing broad is granted");
assert!(
!admits_exactly(narrow, &RootAdmission::LocalSystem),
"but it still inherits, so it is reconciled rather than accepted"
);
}
#[test]
fn the_two_well_known_trustees_compare_equal_in_either_spelling() {
let aliases = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)";
let sids = "D:P(A;OICI;FA;;;S-1-5-18)(A;OICI;FA;;;S-1-5-32-544)";
assert_eq!(write_trustees(aliases), write_trustees(sids));
assert!(admits_exactly(sids, &RootAdmission::LocalSystem));
}
#[test]
fn an_account_alias_windows_substituted_is_reconciled_rather_than_trusted() {
let substituted = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;LA)";
assert!(!grants_broad_write(substituted));
assert!(!admits_exactly(
substituted,
&RootAdmission::Account("S-1-5-21-1-2-3-500".to_owned())
));
}
#[test]
fn redaction_removes_the_machine_and_the_user_from_an_account_sid() {
let redacted = redact(
"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-4004-77-9-1001)",
);
assert_eq!(
redacted,
"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-<account>)"
);
assert!(!redacted.contains("4004"), "{redacted}");
assert!(!redacted.contains("1001"), "{redacted}");
}
#[test]
fn redaction_keeps_the_well_known_trustees_that_identify_nobody() {
let descriptor = "D:P(A;OICI;FA;;;S-1-5-18)(A;OICI;FA;;;S-1-5-32-544)";
assert_eq!(redact(descriptor), descriptor);
}
#[test]
fn redaction_handles_several_accounts_and_a_trailing_one() {
assert_eq!(
redact("(A;;FA;;;S-1-5-21-1-2-3-1001)(A;;FA;;;S-1-5-21-9-8-7-1002)"),
"(A;;FA;;;S-1-5-21-<account>)(A;;FA;;;S-1-5-21-<account>)"
);
assert_eq!(redact("S-1-5-21-1-2-3-1001"), "S-1-5-21-<account>");
}
#[test]
fn a_summary_names_trustees_without_naming_an_account() {
let summary = RootAccessSummary::Created {
path: PathBuf::from("C:\\rman"),
admits: account().admits(),
};
let rendered = summary.to_string();
assert!(rendered.contains("C:\\rman"), "{rendered}");
assert!(rendered.contains("the invoking user"), "{rendered}");
assert!(!rendered.contains("S-1-5-21"), "{rendered}");
assert!(summary.created());
}
#[test]
fn a_boot_summary_does_not_claim_to_admit_an_invoking_user() {
let rendered = RootAccessSummary::Reconciled {
path: PathBuf::from("C:\\rman"),
admits: RootAdmission::LocalSystem.admits(),
}
.to_string();
assert!(!rendered.contains("the invoking user"), "{rendered}");
assert!(rendered.contains("NT AUTHORITY\\SYSTEM"), "{rendered}");
}
#[test]
fn a_reversal_that_left_something_behind_says_so() {
let retained = Reversal::Retained {
path: PathBuf::from("C:\\rman"),
detail: "it existed before this operation".to_owned(),
};
assert!(retained.to_string().contains("existed before"));
assert_ne!(retained, Reversal::NothingToUndo);
}
#[test]
fn a_not_applicable_change_reverts_to_nothing() {
let change = RootAccessChange::not_applicable();
assert_eq!(change.summary(), &RootAccessSummary::NotApplicable);
assert_eq!(change.revert(), Reversal::NothingToUndo);
assert_eq!(change.summary().path(), None);
}
#[test]
fn the_broad_access_refusal_names_the_path_the_volume_and_the_remedy() {
let error = RootAccessError::BroadExistingAccess {
path: PathBuf::from("C:\\rman"),
dacl: redact(INHERITED_FROM_VOLUME),
volume: PathBuf::from("C:\\"),
remediation: remediation(),
};
let message = error.to_string();
assert!(message.contains("C:\\rman"), "{message}");
assert!(message.contains("host set-runtime-root"), "{message}");
assert!(
message.contains("refused rather than tightened"),
"an operator has to be told why it was not simply fixed: {message}"
);
assert_eq!(error.path(), Some(Path::new("C:\\rman")));
}
#[cfg(not(windows))]
#[test]
fn nothing_is_created_or_re_permissioned_off_windows() {
let root = tempfile::tempdir().expect("a temporary directory");
let paths = AppPaths::rooted_at(root.path());
let change =
ensure_default_root(&paths, &RootAdmission::LocalSystem).expect("a no-op succeeds");
assert_eq!(change.summary(), &RootAccessSummary::NotApplicable);
assert_eq!(report(root.path()), RootAccessReport::NotApplicable);
}
#[cfg(windows)]
#[test]
fn a_directory_this_process_created_is_reported_as_narrow() {
let root = tempfile::tempdir().expect("a temporary directory");
let directory = root.path().join("narrow");
let admission = RootAdmission::of_this_account().expect("this process has an account");
let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
.expect("a local absolute path");
let elsewhere = tempfile::tempdir().expect("a second temporary directory");
let app_paths = AppPaths::rooted_at(elsewhere.path());
let change = reconcile(&app_paths, &checked, &admission).expect("creation succeeds");
assert!(change.summary().created(), "{:?}", change.summary());
match report(&directory) {
RootAccessReport::Present {
protected,
broad_write,
dacl,
} => {
assert!(protected, "{dacl}");
assert!(!broad_write, "{dacl}");
assert!(!dacl.contains("S-1-5-21-1"), "unredacted account: {dacl}");
}
other => panic!("expected a readable descriptor, got {other:?}"),
}
let after_creation = read_back(&directory);
let again = reconcile(&app_paths, &checked, &admission).expect("a second pass succeeds");
assert!(!again.summary().created(), "{:?}", again.summary());
assert_same_grants(
&read_back(&directory),
&after_creation,
"a second pass must leave the descriptor granting what creation wrote",
);
let reversal = again.revert();
assert!(
matches!(
reversal,
Reversal::NothingToUndo | Reversal::Restored { .. }
),
"a second pass has nothing of its own to undo: {reversal:?}"
);
assert!(directory.is_dir(), "the second pass must not remove it");
assert_same_grants(
&read_back(&directory),
&after_creation,
"and reverting it must leave those grants alone",
);
let child = directory.join("s1");
std::fs::create_dir(&child).expect("a child below the root");
std::fs::write(child.join("marker"), b"job").expect("content inside the child");
std::fs::remove_dir_all(&child).expect("the child is removable again");
}
#[cfg(windows)]
#[test]
fn an_existing_broad_directory_is_refused_rather_than_tightened() {
let root = tempfile::tempdir().expect("a temporary directory");
let directory = root.path().join("open");
sys::create_with_dacl(&directory, "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;WD)")
.expect("a deliberately open directory");
let before = read_back(&directory);
assert!(grants_broad_write(&before), "{before}");
let elsewhere = tempfile::tempdir().expect("a second temporary directory");
let app_paths = AppPaths::rooted_at(elsewhere.path());
let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
.expect("a local absolute path");
let error = reconcile(&app_paths, &checked, &RootAdmission::LocalSystem)
.expect_err("an open directory is refused");
assert!(
matches!(error, RootAccessError::BroadExistingAccess { .. }),
"{error}"
);
assert_eq!(read_back(&directory), before);
}
#[cfg(windows)]
#[test]
fn reverting_a_reconciliation_puts_the_previous_descriptor_back() {
let root = tempfile::tempdir().expect("a temporary directory");
let directory = root.path().join("narrow");
let admission = RootAdmission::of_this_account().expect("this process has an account");
let sid = admission
.sid()
.expect("an ordinary account has a SID")
.to_owned();
sys::create_with_dacl(&directory, &format!("D:P(A;OICI;FA;;;{sid})"))
.expect("a narrow directory");
let before = read_back(&directory);
assert!(!grants_broad_write(&before), "{before}");
let elsewhere = tempfile::tempdir().expect("a second temporary directory");
let app_paths = AppPaths::rooted_at(elsewhere.path());
let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
.expect("a local absolute path");
let change =
reconcile(&app_paths, &checked, &admission).expect("a narrow directory is reconciled");
assert!(matches!(
change.summary(),
RootAccessSummary::Reconciled { .. }
));
assert_ne!(read_back(&directory), before, "it was actually rewritten");
assert_eq!(
change.revert(),
Reversal::Restored {
path: directory.clone()
}
);
let after = read_back(&directory);
assert_eq!(aces_of(&after), aces_of(&before), "{after} vs {before}");
assert!(is_protected(&after), "{after}");
assert!(
directory.is_dir(),
"a pre-existing directory is never removed"
);
}
#[cfg(windows)]
fn aces_of(descriptor: &str) -> &str {
descriptor
.find('(')
.map_or(descriptor, |start| &descriptor[start..])
}
#[cfg(windows)]
fn assert_same_grants(actual: &str, expected: &str, context: &str) {
assert_eq!(
aces_of(actual),
aces_of(expected),
"{context}: {actual} vs {expected}"
);
assert!(is_protected(actual), "{context}: {actual}");
}
#[cfg(windows)]
#[test]
fn reverting_a_creation_removes_the_directory_it_created() {
let root = tempfile::tempdir().expect("a temporary directory");
let directory = root.path().join("created");
let elsewhere = tempfile::tempdir().expect("a second temporary directory");
let app_paths = AppPaths::rooted_at(elsewhere.path());
let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
.expect("a local absolute path");
let admission = RootAdmission::of_this_account().expect("this process has an account");
let change = reconcile(&app_paths, &checked, &admission).expect("creation succeeds");
assert!(directory.is_dir());
assert_eq!(
change.revert(),
Reversal::Removed {
path: directory.clone()
}
);
assert!(!directory.exists(), "the rollback is a real rollback");
}
#[cfg(windows)]
#[test]
fn a_rollback_that_cannot_finish_reports_what_it_left_behind() {
let root = tempfile::tempdir().expect("a temporary directory");
let directory = root.path().join("boot-owned");
let elsewhere = tempfile::tempdir().expect("a second temporary directory");
let app_paths = AppPaths::rooted_at(elsewhere.path());
let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
.expect("a local absolute path");
let change = reconcile(&app_paths, &checked, &RootAdmission::LocalSystem)
.expect("creation succeeds");
match change.revert() {
Reversal::Removed { path } => assert_eq!(path, directory),
Reversal::Retained { path, detail } => {
assert_eq!(path, directory);
assert!(
detail.contains("removing it by hand is safe"),
"a non-reversible state must say what to do about it: {detail}"
);
}
other => panic!("expected a removal or an explicit retention, got {other:?}"),
}
}
#[cfg(windows)]
#[test]
fn a_custom_root_is_described_without_being_changed() {
let root = tempfile::tempdir().expect("a temporary directory");
let directory = root.path().join("operators-own");
sys::create_with_dacl(&directory, "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;WD)")
.expect("an operator directory this product did not create");
let before = read_back(&directory);
match report(&directory) {
RootAccessReport::Present { broad_write, .. } => assert!(broad_write),
other => panic!("expected a readable descriptor, got {other:?}"),
}
assert_eq!(read_back(&directory), before, "reporting must not rewrite");
}
#[cfg(windows)]
#[test]
fn an_absent_directory_reports_absent() {
let root = tempfile::tempdir().expect("a temporary directory");
assert_eq!(
report(&root.path().join("nothing")),
RootAccessReport::Absent
);
}
#[cfg(windows)]
fn read_back(path: &Path) -> String {
sys::read_dacl(path).expect("this process can read the descriptor it just wrote")
}
}