mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
test(buzz-agent): check the Windows cache DACL in-process
On the Windows runner every assert_owner_only call parsed empty output from a powershell Get-Acl child whose status and stderr it discarded, so the three save and load owner-only tests failed without saying why. Move the helper crate's in-process DACL reader out of its test module behind a test-support feature, and have buzz-agent's Windows arm call it. The tests no longer shell out, both crates share one SDDL check, and buzz-agent still has no unsafe code. Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -77,6 +77,9 @@ nix = { version = "0.31", default-features = false, features = ["signal", "proce
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
buzz-private-file = { path = "../buzz-private-file" }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
buzz-private-file = { path = "../buzz-private-file", features = ["test-support"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# Synthetic TLS OAuth/catalog servers exercise the strict HTTPS production path.
|
||||
rcgen = { version = "0.14", default-features = false, features = ["crypto", "ring"] }
|
||||
|
||||
@@ -2731,25 +2731,7 @@ mod tests {
|
||||
assert_eq!(mode, 0o600, "{why}: got {mode:o}");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// SDDL, unlike `icacls` output, is not localized.
|
||||
let out = std::process::Command::new("powershell")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"(Get-Acl -LiteralPath $env:ACL_PATH).Sddl",
|
||||
])
|
||||
.env("ACL_PATH", path)
|
||||
.output()
|
||||
.unwrap();
|
||||
let sddl = String::from_utf8_lossy(&out.stdout);
|
||||
let dacl = sddl.trim().split_once("D:").map_or("", |(_, dacl)| dacl);
|
||||
let (flags, aces) = dacl.split_once('(').unwrap_or_default();
|
||||
assert!(
|
||||
flags.contains('P') && aces == "A;;FA;;;OW)",
|
||||
"{why}: got {sddl}"
|
||||
);
|
||||
}
|
||||
buzz_private_file::assert_owner_only(&fs::File::open(path).unwrap(), why);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -7,6 +7,10 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Owner-only files on Windows, keeping Win32 security FFI out of buzz-agent"
|
||||
|
||||
[features]
|
||||
# `assert_owner_only`, for other crates' tests.
|
||||
test-support = []
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
|
||||
@@ -237,62 +237,68 @@ fn verbatim(path: &Path) -> io::Result<Vec<u16>> {
|
||||
Ok(prefix.encode_utf16().chain(wide).chain(Some(0)).collect())
|
||||
}
|
||||
|
||||
/// The file's DACL as SDDL, such as `D:P(A;;FA;;;OW)`.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[allow(unsafe_code)]
|
||||
fn dacl(file: &File) -> String {
|
||||
use windows_sys::Win32::Security::Authorization::ConvertSecurityDescriptorToStringSecurityDescriptorW;
|
||||
|
||||
let mut descriptor = null_mut();
|
||||
// SAFETY: the handle is open; on success Windows allocates `descriptor`.
|
||||
let status = unsafe {
|
||||
GetSecurityInfo(
|
||||
file.as_raw_handle(),
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
&mut descriptor,
|
||||
)
|
||||
};
|
||||
win32(status).unwrap();
|
||||
let descriptor = Local(descriptor);
|
||||
let (mut sddl, mut len) = (null_mut(), 0);
|
||||
// SAFETY: `descriptor` is valid; on success Windows allocates `sddl`
|
||||
// holding `len` UTF-16 units.
|
||||
let ok = unsafe {
|
||||
ConvertSecurityDescriptorToStringSecurityDescriptorW(
|
||||
descriptor.0,
|
||||
SDDL_REVISION_1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
&mut sddl,
|
||||
&mut len,
|
||||
)
|
||||
};
|
||||
assert_ne!(ok, 0, "{}", io::Error::last_os_error());
|
||||
let _sddl = Local(sddl.cast());
|
||||
// SAFETY: `sddl` holds `len` UTF-16 units.
|
||||
let units = unsafe { std::slice::from_raw_parts(sddl, len as usize) };
|
||||
String::from_utf16_lossy(units)
|
||||
.trim_end_matches('\0')
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Test support: panic with `why` unless `file` has the owner-only DACL
|
||||
/// [`create_new`] sets. Reads the DACL in-process, so tests need no shell.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_owner_only(file: &File, why: &str) {
|
||||
let sddl = dacl(file);
|
||||
let (flags, aces) = sddl
|
||||
.strip_prefix("D:")
|
||||
.and_then(|rest| rest.split_once('('))
|
||||
.unwrap_or_else(|| panic!("{why}: unexpected DACL {sddl}"));
|
||||
assert!(flags.contains('P'), "{why}: DACL is not protected: {sddl}");
|
||||
assert_eq!(aces, "A;;FA;;;OW)", "{why}: DACL is not owner-only: {sddl}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::process::Command;
|
||||
use windows_sys::Win32::Security::Authorization::ConvertSecurityDescriptorToStringSecurityDescriptorW;
|
||||
|
||||
/// The file's DACL as SDDL, such as `D:P(A;;FA;;;OW)`.
|
||||
#[allow(unsafe_code)]
|
||||
fn dacl(file: &File) -> String {
|
||||
let mut descriptor = null_mut();
|
||||
// SAFETY: the handle is open; on success Windows allocates `descriptor`.
|
||||
let status = unsafe {
|
||||
GetSecurityInfo(
|
||||
file.as_raw_handle(),
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
&mut descriptor,
|
||||
)
|
||||
};
|
||||
win32(status).unwrap();
|
||||
let descriptor = Local(descriptor);
|
||||
let (mut sddl, mut len) = (null_mut(), 0);
|
||||
// SAFETY: `descriptor` is valid; on success Windows allocates `sddl`
|
||||
// holding `len` UTF-16 units.
|
||||
let ok = unsafe {
|
||||
ConvertSecurityDescriptorToStringSecurityDescriptorW(
|
||||
descriptor.0,
|
||||
SDDL_REVISION_1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
&mut sddl,
|
||||
&mut len,
|
||||
)
|
||||
};
|
||||
assert_ne!(ok, 0, "{}", io::Error::last_os_error());
|
||||
let _sddl = Local(sddl.cast());
|
||||
// SAFETY: `sddl` holds `len` UTF-16 units.
|
||||
let units = unsafe { std::slice::from_raw_parts(sddl, len as usize) };
|
||||
String::from_utf16_lossy(units)
|
||||
.trim_end_matches('\0')
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn assert_owner_only(file: &File) {
|
||||
let sddl = dacl(file);
|
||||
let (flags, aces) = sddl
|
||||
.strip_prefix("D:")
|
||||
.and_then(|rest| rest.split_once('('))
|
||||
.unwrap_or_else(|| panic!("unexpected DACL {sddl}"));
|
||||
assert!(flags.contains('P'), "DACL is not protected: {sddl}");
|
||||
assert_eq!(aces, "A;;FA;;;OW)", "DACL is not owner-only: {sddl}");
|
||||
}
|
||||
|
||||
fn read(mut file: File) -> String {
|
||||
let mut body = String::new();
|
||||
@@ -306,7 +312,7 @@ mod tests {
|
||||
let path = dir.path().join("token.json");
|
||||
let mut file = create_new(&path).unwrap();
|
||||
file.write_all(b"secret").unwrap();
|
||||
assert_owner_only(&file);
|
||||
assert_owner_only(&file, "new file");
|
||||
drop(file);
|
||||
|
||||
let err = create_new(&path).unwrap_err();
|
||||
@@ -321,7 +327,7 @@ mod tests {
|
||||
std::fs::create_dir_all(&parent).unwrap();
|
||||
let path = parent.join("token.json");
|
||||
create_new(&path).unwrap();
|
||||
assert_owner_only(&open_owned(&path).unwrap());
|
||||
assert_owner_only(&open_owned(&path).unwrap(), "long path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -330,7 +336,7 @@ mod tests {
|
||||
let path = dir.path().join("token.json");
|
||||
std::fs::write(&path, "legacy").unwrap();
|
||||
let file = open_owned(&path).unwrap();
|
||||
assert_owner_only(&file);
|
||||
assert_owner_only(&file, "tightened file");
|
||||
assert_eq!(read(file), "legacy");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user