rcman 0.1.7

Framework-agnostic settings management with schema, backup/restore, secrets and derive macro support
Documentation
use crate::error::{Error, Result};
use std::path::Path;

/// Ensure the directory exists and has secure permissions for the current user only.
///
/// On Unix, the directory is created with mode `0700` (owner rwx only).
/// On Windows, ACLs are set to grant access only to the current user.
///
/// # Errors
///
/// Returns `Error::DirectoryCreate` if directory creation fails, or any error returned
/// by [`set_secure_dir_permissions`].
pub fn ensure_secure_dir(path: &Path) -> Result<()> {
    std::fs::create_dir_all(path).map_err(|e| Error::DirectoryCreate {
        path: path.to_path_buf(),
        source: e,
    })?;
    set_secure_dir_permissions(path)
}

// =============================================================================
// Unix
// =============================================================================

#[cfg(unix)]
/// Set permission bits so directories are owner read-write-execute only (mode `0700`).
///
/// # Errors
///
/// Returns `Error::FileRead` if metadata lookup fails, or `Error::FileWrite` if
/// setting permissions fails.
pub fn set_secure_dir_permissions(path: &Path) -> Result<()> {
    use std::fs;
    use std::os::unix::fs::PermissionsExt;

    let mut perms = fs::metadata(path)
        .map_err(|e| Error::FileRead {
            path: path.to_path_buf(),
            source: e,
        })?
        .permissions();

    perms.set_mode(0o700);

    fs::set_permissions(path, perms).map_err(|e| Error::FileWrite {
        path: path.to_path_buf(),
        source: e,
    })
}

#[cfg(unix)]
/// Set permission bits so files are owner read-write only (mode `0600`).
///
/// # Errors
///
/// Returns `Error::FileRead` if metadata lookup fails, or `Error::FileWrite` if
/// setting permissions fails.
pub fn set_secure_file_permissions(path: &Path) -> Result<()> {
    use std::fs;
    use std::os::unix::fs::PermissionsExt;

    let mut perms = fs::metadata(path)
        .map_err(|e| Error::FileRead {
            path: path.to_path_buf(),
            source: e,
        })?
        .permissions();

    perms.set_mode(0o600);

    fs::set_permissions(path, perms).map_err(|e| Error::FileWrite {
        path: path.to_path_buf(),
        source: e,
    })
}

// =============================================================================
// Non-Unix (Windows + other)
// =============================================================================

#[cfg(not(unix))]
/// Set ACL-based permissions so only the current user can access the file.
///
/// # Errors
///
/// Returns an error if the ACL update fails.
pub fn set_secure_file_permissions(path: &Path) -> Result<()> {
    set_secure_acl(path)
}

#[cfg(not(unix))]
/// Set ACL-based permissions so only the current user can access the directory.
///
/// # Errors
///
/// Returns an error if the ACL update fails.
pub fn set_secure_dir_permissions(path: &Path) -> Result<()> {
    set_secure_acl(path)
}

// =============================================================================
// Windows ACL implementation
// =============================================================================

#[cfg(windows)]
fn set_secure_acl(path: &Path) -> Result<()> {
    use std::mem::MaybeUninit;
    use std::os::windows::ffi::OsStrExt;
    use windows::Win32::Foundation::{CloseHandle, HANDLE, HLOCAL, LocalFree};
    use windows::Win32::Security::Authorization::{
        EXPLICIT_ACCESS_W, SE_FILE_OBJECT, SET_ACCESS, SetEntriesInAclW, SetNamedSecurityInfoW,
        TRUSTEE_IS_SID, TRUSTEE_W,
    };
    use windows::Win32::Security::{
        ACL, DACL_SECURITY_INFORMATION, GetTokenInformation, NO_INHERITANCE,
        PROTECTED_DACL_SECURITY_INFORMATION, TOKEN_QUERY, TOKEN_USER, TokenUser,
    };
    use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
    use windows::core::PWSTR;

    /// RAII guard that closes a Win32 HANDLE on drop.
    struct OwnedHandle(HANDLE);
    impl Drop for OwnedHandle {
        fn drop(&mut self) {
            // SAFETY: self.0 was obtained from OpenProcessToken and is valid until this drop runs.
            unsafe {
                let _ = CloseHandle(self.0);
            }
        }
    }

    /// RAII guard that calls LocalFree on a pointer allocated by a Win32 API on drop.
    struct LocalAlloced<T>(*mut T);
    impl<T> Drop for LocalAlloced<T> {
        fn drop(&mut self) {
            // SAFETY: self.0 was allocated by SetEntriesInAclW via LocalAlloc.
            unsafe {
                let _ = LocalFree(Some(HLOCAL(self.0.cast())));
            }
        }
    }

    let mut path_wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();

    // ── Step 1: Open the process token ───────────────────────────────────────
    let token = {
        let mut raw = HANDLE::default();
        // SAFETY: GetCurrentProcess() always returns a valid pseudo-handle that
        // doesn't need to be closed. OpenProcessToken writes a real handle into `raw`.
        unsafe {
            OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut raw)
                .map_err(|e| Error::Config(format!("OpenProcessToken failed: {e}")))?;
        }
        OwnedHandle(raw) // handle is now closed automatically on any early return
    };

    // ── Step 2: Query buffer size, then fill TOKEN_USER ──────────────────────
    let token_user_buf: Vec<u8> = {
        let mut needed = 0u32;
        // SAFETY: Passing a null/zero-length buffer is the standard Win32 pattern for
        // querying the required size. The call is expected to fail with
        // ERROR_INSUFFICIENT_BUFFER; we intentionally discard the error and use
        // only the `needed` value it writes.
        unsafe {
            let _ = GetTokenInformation(token.0, TokenUser, None, 0, &raw mut needed);
        }

        let mut buf = vec![0u8; needed as usize];
        // SAFETY: buf has exactly `needed` bytes as reported by the size query above.
        // token.0 remains valid because `token` (OwnedHandle) has not been dropped.
        unsafe {
            GetTokenInformation(
                token.0,
                TokenUser,
                Some(buf.as_mut_ptr().cast()),
                needed,
                &raw mut needed,
            )
            .map_err(|e| Error::Config(format!("GetTokenInformation failed: {e}")))?;
        }
        buf
    };
    drop(token); // handle no longer needed; OwnedHandle closes it here

    // ── Step 3: Extract the SID pointer from the buffer ──────────────────────
    //
    // TOKEN_USER contains a PSID that points *into* the same allocation as `token_user_buf`.
    // We copy the struct into properly aligned stack memory to avoid reading from a
    // potentially mis-aligned Vec<u8>, but the PSID field still points into `token_user_buf`,
    // so that Vec must remain alive for the rest of this function.
    let user_sid = {
        let mut slot = MaybeUninit::<TOKEN_USER>::uninit();
        // SAFETY: `token_user_buf` was filled by GetTokenInformation with a valid TOKEN_USER
        // layout. copy_nonoverlapping works byte-by-byte so the source alignment doesn't matter;
        // the destination (slot) is stack-allocated and correctly aligned for TOKEN_USER.
        unsafe {
            std::ptr::copy_nonoverlapping(
                token_user_buf.as_ptr(),
                slot.as_mut_ptr().cast::<u8>(),
                std::mem::size_of::<TOKEN_USER>(),
            );
            slot.assume_init().User.Sid
        }
    };

    // ── Step 4: Build a DACL granting the current user full access ───────────
    let access = EXPLICIT_ACCESS_W {
        grfAccessPermissions: 0x001F_01FF, // FILE_ALL_ACCESS
        grfAccessMode: SET_ACCESS,
        grfInheritance: NO_INHERITANCE,
        Trustee: TRUSTEE_W {
            TrusteeForm: TRUSTEE_IS_SID,
            // user_sid.0 is a raw pointer into token_user_buf, which is still alive
            ptstrName: PWSTR(user_sid.0.cast::<u16>()),
            ..Default::default()
        },
    };

    let acl = {
        let mut raw: *mut ACL = std::ptr::null_mut();
        // SAFETY: `access` is a fully initialised EXPLICIT_ACCESS_W entry.
        // SetEntriesInAclW allocates the ACL via LocalAlloc and writes its address into `raw`.
        unsafe {
            SetEntriesInAclW(Some(&[access]), None, &raw mut raw)
                .ok()
                .map_err(|e| Error::Config(format!("SetEntriesInAclW failed: {e:?}")))?;
        }
        LocalAlloced(raw) // ACL is now freed automatically on any early return
    };

    // ── Step 5: Apply the DACL to the target path ────────────────────────────
    // SAFETY: path_wide is a null-terminated UTF-16 string. acl.0 is a valid ACL
    // allocated by SetEntriesInAclW; it remains valid because `acl` has not been dropped.
    let result = unsafe {
        SetNamedSecurityInfoW(
            PWSTR(path_wide.as_mut_ptr()),
            SE_FILE_OBJECT,
            DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
            None,
            None,
            Some(acl.0),
            None,
        )
    };
    // `acl` is dropped here, freeing the LocalAlloc memory

    result
        .ok()
        .map_err(|e| Error::Config(format!("SetNamedSecurityInfoW failed: {e:?}")))
}

#[cfg(not(any(unix, windows)))]
fn set_secure_acl(_path: &Path) -> Result<()> {
    Ok(())
}

// =============================================================================
// Tests
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_secure_file_permissions() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        fs::write(&path, "test").unwrap();
        set_secure_file_permissions(&path).unwrap();

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            assert_eq!(
                fs::metadata(&path).unwrap().permissions().mode() & 0o777,
                0o600
            );
        }
    }

    #[test]
    fn test_secure_dir_permissions() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("secure");
        fs::create_dir_all(&path).unwrap();
        set_secure_dir_permissions(&path).unwrap();

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            assert_eq!(
                fs::metadata(&path).unwrap().permissions().mode() & 0o777,
                0o700
            );
        }
    }
}