code_system_graph_store_sqlite/
file_permissions.rs1use std::fs::{self, OpenOptions};
2use std::io;
3use std::path::{Path, PathBuf};
4
5use super::StoreError;
6
7#[cfg(test)]
8mod tests;
9
10pub(crate) const SQLITE_ARTIFACT_SUFFIXES: [&str; 4] = ["", "-wal", "-shm", "-journal"];
11
12pub(crate) fn artifact_path(database_path: &Path, suffix: &str) -> PathBuf {
13 if suffix.is_empty() {
14 return database_path.to_path_buf();
15 }
16 let mut value = database_path.as_os_str().to_os_string();
17 value.push(suffix);
18 PathBuf::from(value)
19}
20
21pub(crate) fn prepare_database_file(path: &Path) -> Result<(), StoreError> {
22 if let Some(parent) = path.parent() {
23 fs::create_dir_all(parent).map_err(|source| StoreError::Io {
24 path: parent.to_path_buf(),
25 source,
26 })?;
27 }
28 if path.exists() {
29 let metadata = fs::symlink_metadata(path).map_err(|source| StoreError::Io {
30 path: path.to_path_buf(),
31 source,
32 })?;
33 if metadata.file_type().is_symlink() || !metadata.is_file() {
34 return Err(StoreError::Io {
35 path: path.to_path_buf(),
36 source: io::Error::new(
37 io::ErrorKind::InvalidInput,
38 "database path must be a regular file",
39 ),
40 });
41 }
42 return Ok(());
43 }
44 let mut options = OpenOptions::new();
45 options.create_new(true).read(true).write(true);
46 #[cfg(unix)]
47 {
48 use std::os::unix::fs::OpenOptionsExt;
49
50 options.mode(0o600);
51 }
52 options.open(path).map_err(|source| StoreError::Io {
53 path: path.to_path_buf(),
54 source,
55 })?;
56 Ok(())
57}
58
59pub(crate) fn restrict_store_permissions(database_path: &Path) -> Result<(), StoreError> {
60 for suffix in SQLITE_ARTIFACT_SUFFIXES {
61 let sidecar_path = artifact_path(database_path, suffix);
62 let metadata = match fs::symlink_metadata(&sidecar_path) {
63 Ok(metadata) => metadata,
64 Err(source) if source.kind() == io::ErrorKind::NotFound => continue,
65 Err(source) => {
66 return Err(StoreError::Io {
67 path: sidecar_path,
68 source,
69 });
70 }
71 };
72 if metadata.file_type().is_symlink() || !metadata.is_file() {
73 return Err(StoreError::Io {
74 path: sidecar_path,
75 source: io::Error::new(
76 io::ErrorKind::InvalidInput,
77 "database artifact path must be a regular file",
78 ),
79 });
80 }
81 set_owner_only_file(&sidecar_path).map_err(|source| StoreError::Io {
82 path: sidecar_path,
83 source,
84 })?;
85 }
86 Ok(())
87}
88
89#[cfg(test)]
90pub(crate) fn remove_database_artifacts(database_path: &Path) -> Result<(), StoreError> {
91 let mut first_error = None;
92 for suffix in SQLITE_ARTIFACT_SUFFIXES {
93 let artifact = artifact_path(database_path, suffix);
94 match fs::remove_file(&artifact) {
95 Ok(()) => {}
96 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
97 Err(source) => {
98 first_error.get_or_insert_with(|| StoreError::Io {
99 path: artifact,
100 source,
101 });
102 }
103 }
104 }
105 first_error.map_or(Ok(()), Err)
106}
107
108#[cfg(unix)]
114pub fn set_owner_only_file(path: &Path) -> io::Result<()> {
115 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
116
117 let file = OpenOptions::new()
118 .read(true)
119 .write(true)
120 .custom_flags(libc::O_NOFOLLOW)
121 .open(path)?;
122 file.set_permissions(fs::Permissions::from_mode(0o600))
123}
124
125#[cfg(windows)]
136#[allow(unsafe_code)]
137pub(crate) fn current_user_sid_string() -> io::Result<String> {
138 use windows_sys::Win32::Foundation::{CloseHandle, ERROR_NO_TOKEN, LocalFree};
139 use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
140 use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TOKEN_USER, TokenUser};
141 use windows_sys::Win32::System::Threading::{
142 GetCurrentProcess, GetCurrentThread, OpenProcessToken, OpenThreadToken
143 };
144
145 let mut token = std::ptr::null_mut();
146 if unsafe { OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, 1, &raw mut token) } == 0 {
148 let thread_error = io::Error::last_os_error();
149 if thread_error.raw_os_error() != Some(ERROR_NO_TOKEN.cast_signed()) {
150 return Err(thread_error);
151 }
152 if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) } == 0 {
154 return Err(io::Error::last_os_error());
155 }
156 }
157 let result = (|| {
158 let mut required = 0_u32;
159 unsafe {
161 GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &raw mut required);
162 }
163 if required == 0 {
164 return Err(io::Error::last_os_error());
165 }
166 let word_size = std::mem::size_of::<usize>();
167 let mut buffer = vec![0_usize; (required as usize).div_ceil(word_size)];
168 if unsafe {
170 GetTokenInformation(
171 token,
172 TokenUser,
173 buffer.as_mut_ptr().cast(),
174 required,
175 &raw mut required,
176 )
177 } == 0
178 {
179 return Err(io::Error::last_os_error());
180 }
181 let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
183 let mut sid_text = std::ptr::null_mut();
184 if unsafe { ConvertSidToStringSidW(user.User.Sid, &raw mut sid_text) } == 0 {
186 return Err(io::Error::last_os_error());
187 }
188 let mut length = 0;
189 unsafe {
191 while *sid_text.add(length) != 0 {
192 length += 1;
193 }
194 }
195 let sid = unsafe { std::slice::from_raw_parts(sid_text, length) };
197 let result = String::from_utf16(sid)
198 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string()));
199 unsafe {
201 LocalFree(sid_text.cast());
202 }
203 result
204 })();
205 unsafe {
207 CloseHandle(token);
208 }
209 result
210}
211
212#[cfg(windows)]
221#[allow(unsafe_code)]
222pub fn set_owner_only_file(path: &Path) -> io::Result<()> {
223 use std::ffi::c_void;
224 use std::os::windows::ffi::OsStrExt;
225 use std::ptr;
226
227 use windows_sys::Win32::Foundation::LocalFree;
228 use windows_sys::Win32::Security::Authorization::{
229 ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1
230 };
231 use windows_sys::Win32::Security::{
232 DACL_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, SetFileSecurityW
233 };
234
235 let mut path_wide = path.as_os_str().encode_wide().collect::<Vec<_>>();
236 if path_wide.contains(&0) {
237 return Err(io::Error::new(
238 io::ErrorKind::InvalidInput,
239 "file path contains a NUL code unit",
240 ));
241 }
242 path_wide.push(0);
243
244 let descriptor_sddl = format!("D:P(A;;FA;;;{})\0", current_user_sid_string()?)
245 .encode_utf16()
246 .collect::<Vec<_>>();
247 let mut descriptor: PSECURITY_DESCRIPTOR = ptr::null_mut();
248 let converted = unsafe {
251 ConvertStringSecurityDescriptorToSecurityDescriptorW(
252 descriptor_sddl.as_ptr(),
253 SDDL_REVISION_1,
254 &raw mut descriptor,
255 ptr::null_mut(),
256 )
257 };
258 if converted == 0 {
259 return Err(io::Error::last_os_error());
260 }
261
262 let applied = unsafe {
264 SetFileSecurityW(
265 path_wide.as_ptr(),
266 DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
267 descriptor,
268 )
269 };
270 let _released = unsafe { LocalFree(descriptor.cast::<c_void>()) };
272 if applied == 0 {
273 return Err(io::Error::last_os_error());
274 }
275 Ok(())
276}
277
278#[cfg(not(any(unix, windows)))]
284pub fn set_owner_only_file(_path: &Path) -> io::Result<()> {
285 Err(io::Error::new(
286 io::ErrorKind::Unsupported,
287 "owner-only file permissions are unsupported on this platform",
288 ))
289}