Skip to main content

code_system_graph_store_sqlite/
file_permissions.rs

1use 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/// Returns the current Windows user's textual security identifier.
109///
110/// # Errors
111///
112/// Returns an I/O error when the process token or SID cannot be queried.
113#[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/// Restricts an existing regular file to the current operating-system owner.
126///
127/// On Unix this sets mode `0600` through a descriptor opened without following
128/// symlinks. On Windows this installs a protected DACL containing one
129/// full-control owner-rights ACE; inherited access is intentionally removed.
130///
131/// # Errors
132///
133/// Returns an I/O error when the file cannot be opened or its permissions
134/// cannot be restricted. Platforms other than Unix and Windows are unsupported.
135#[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    // SAFETY: the pseudo thread handle is valid and `token` points to writable storage.
147    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        // SAFETY: the pseudo process handle is valid and `token` points to writable storage.
153        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        // SAFETY: a null buffer with length zero is the documented size-query call.
160        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        // SAFETY: the aligned buffer has at least `required` writable bytes.
169        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        // SAFETY: a successful TokenUser query initialized a TOKEN_USER at the buffer start.
182        let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
183        let mut sid_text = std::ptr::null_mut();
184        // SAFETY: the SID comes from the live token buffer and the output pointer is writable.
185        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        // SAFETY: ConvertSidToStringSidW returned a NUL-terminated allocated UTF-16 string.
190        unsafe {
191            while *sid_text.add(length) != 0 {
192                length += 1;
193            }
194        }
195        // SAFETY: the preceding scan established the initialized string length.
196        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        // SAFETY: the SID string was allocated by ConvertSidToStringSidW.
200        unsafe {
201            LocalFree(sid_text.cast());
202        }
203        result
204    })();
205    // SAFETY: OpenProcessToken returned this owned token handle.
206    unsafe {
207        CloseHandle(token);
208    }
209    result
210}
211
212/// Restricts an existing regular file to the current Windows user.
213///
214/// Installs a protected DACL containing one full-control ACE for the current
215/// process user's concrete SID; inherited access is intentionally removed.
216///
217/// # Errors
218///
219/// Returns an I/O error when the SID cannot be queried or the DACL cannot be applied.
220#[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    // SAFETY: the SDDL input is NUL-terminated, `descriptor` is a valid out-pointer, and the
249    // returned LocalAlloc allocation is released exactly once below.
250    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    // SAFETY: `path_wide` is NUL-terminated and `descriptor` was initialized successfully above.
263    let applied = unsafe {
264        SetFileSecurityW(
265            path_wide.as_ptr(),
266            DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
267            descriptor,
268        )
269    };
270    // SAFETY: the descriptor was allocated by LocalAlloc inside the conversion API.
271    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/// Restricts an existing regular file to the current operating-system owner.
279///
280/// # Errors
281///
282/// Always returns [`io::ErrorKind::Unsupported`] outside Unix and Windows.
283#[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}