fix: tudo que o Guará grava como root pertence ao usuário que o abriu
Sessões, CSV, relatórios, imagens e arquivos recuperados (e as pastas criadas) passam ao dono do sudo/pkexec, e a pasta sugerida é a do usuário, não /root. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
682ba8b943
commit
354caaf8f0
8 files changed
+160
-48
No files matched your search
@@ -8,6 +8,7 @@ Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/);
|
|||||||
- Abrir uma sessão salva falhava por inteiro quando a origem (por exemplo `/dev/sdb`) não estava acessível, e a mensagem de erro era vaga. Agora a sessão sempre abre: dá para navegar, exportar e gerar relatório, e o botão "Escolher a origem…" reconecta o disco ou uma imagem para voltar a pré-visualizar e recuperar.
|
- Abrir uma sessão salva falhava por inteiro quando a origem (por exemplo `/dev/sdb`) não estava acessível, e a mensagem de erro era vaga. Agora a sessão sempre abre: dá para navegar, exportar e gerar relatório, e o botão "Escolher a origem…" reconecta o disco ou uma imagem para voltar a pré-visualizar e recuperar.
|
||||||
- Mensagens claras quando o arquivo de sessão não pode ser lido (por exemplo, criado como administrador em `/root`) ou não é uma sessão válida.
|
- Mensagens claras quando o arquivo de sessão não pode ser lido (por exemplo, criado como administrador em `/root`) ou não é uma sessão válida.
|
||||||
- Teste automático garante que sessões gravadas pela v0.1.0 continuam abrindo.
|
- Teste automático garante que sessões gravadas pela v0.1.0 continuam abrindo.
|
||||||
|
- Rodando como administrador (sudo/pkexec), as pastas sugeridas passam a ser as do usuário que abriu o Guará e tudo que ele grava (sessões, CSV, relatórios, imagens e arquivos recuperados, com as pastas criadas) fica com o dono e as permissões desse usuário, e não do root.
|
||||||
|
|
||||||
## [0.3.0] - 2026-10-18
|
## [0.3.0] - 2026-10-18
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Cada arquivo tem um `.sha256` ao lado. Compilar do código: `cargo build --relea
|
|||||||
|
|
||||||
> Os binários ainda **não são assinados**: o Windows SmartScreen e o Gatekeeper do macOS vão avisar. Confira o SHA-256.
|
> Os binários ainda **não são assinados**: o Windows SmartScreen e o Gatekeeper do macOS vão avisar. Confira o SHA-256.
|
||||||
|
|
||||||
Ler discos brutos exige privilégios: **administrador** (Windows), `sudo` (Linux/macOS).
|
Ler discos brutos exige privilégios: **administrador** (Windows), `sudo` (Linux/macOS). Mesmo como root, o Guará grava sessões, relatórios e arquivos recuperados com o dono e as permissões do usuário que o abriu.
|
||||||
|
|
||||||
## Uso (terminal)
|
## Uso (terminal)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub fn create_image(
|
|||||||
dest.display()
|
dest.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
crate::owner::give_to_user(dest);
|
||||||
let size = src.size();
|
let size = src.size();
|
||||||
let mut rep = ImageReport::default();
|
let mut rep = ImageReport::default();
|
||||||
let mut hasher = MultiHasher::new();
|
let mut hasher = MultiHasher::new();
|
||||||
@@ -81,7 +82,7 @@ pub fn create_image(
|
|||||||
rep.md5 = h.md5;
|
rep.md5 = h.md5;
|
||||||
rep.sha1 = h.sha1;
|
rep.sha1 = h.sha1;
|
||||||
let map = dest.with_extension("map.json");
|
let map = dest.with_extension("map.json");
|
||||||
std::fs::write(map, serde_json::to_vec_pretty(&rep)?)?;
|
crate::owner::write_owned(&map, serde_json::to_vec_pretty(&rep)?)?;
|
||||||
Ok(rep)
|
Ok(rep)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod fs;
|
|||||||
pub mod hashing;
|
pub mod hashing;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
pub mod owner;
|
||||||
pub mod partition;
|
pub mod partition;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
//! Quando o Guará roda como administrador (sudo/pkexec), tudo que ele grava para
|
||||||
|
//! o usuário (sessões, relatórios, arquivos recuperados) pertence ao usuário que
|
||||||
|
//! o abriu, e não ao root, e as pastas sugeridas ficam na pasta desse usuário.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub uid: u32,
|
||||||
|
pub gid: u32,
|
||||||
|
pub home: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// O usuário real por trás de um processo root iniciado por sudo/pkexec.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn invoking_user() -> Option<User> {
|
||||||
|
// SAFETY: geteuid não tem pré-condições.
|
||||||
|
if unsafe { libc::geteuid() } != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let uid = std::env::var("SUDO_UID")
|
||||||
|
.ok()
|
||||||
|
.or_else(|| std::env::var("PKEXEC_UID").ok())
|
||||||
|
.and_then(|v| v.parse::<u32>().ok())
|
||||||
|
.filter(|&u| u != 0);
|
||||||
|
let name = std::env::var("SUDO_USER").ok().filter(|u| u != "root");
|
||||||
|
let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
|
||||||
|
lookup_user(&passwd, uid, name.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procura o usuário (por uid ou, na falta dele, por nome) num conteúdo no formato do /etc/passwd.
|
||||||
|
pub fn lookup_user(passwd: &str, uid: Option<u32>, name: Option<&str>) -> Option<User> {
|
||||||
|
for l in passwd.lines() {
|
||||||
|
let f: Vec<&str> = l.split(':').collect();
|
||||||
|
if f.len() < 6 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (Ok(fuid), Ok(fgid)) = (f[2].parse::<u32>(), f[3].parse::<u32>()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if uid == Some(fuid) || (uid.is_none() && name == Some(f[0])) {
|
||||||
|
return Some(User {
|
||||||
|
uid: fuid,
|
||||||
|
gid: fgid,
|
||||||
|
home: PathBuf::from(f[5]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn invoking_user() -> Option<User> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pasta pessoal para sugerir ao usuário (a dele, mesmo se estivermos como root).
|
||||||
|
pub fn user_home() -> PathBuf {
|
||||||
|
if let Some(u) = invoking_user() {
|
||||||
|
return u.home;
|
||||||
|
}
|
||||||
|
PathBuf::from(
|
||||||
|
std::env::var("HOME")
|
||||||
|
.or_else(|_| std::env::var("USERPROFILE"))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entrega o arquivo/pasta ao usuário que abriu o Guará (não faz nada fora do modo root).
|
||||||
|
pub fn give_to_user(path: &Path) {
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(u) = invoking_user() {
|
||||||
|
let _ = std::os::unix::fs::lchown(path, Some(u.uid), Some(u.gid));
|
||||||
|
}
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let _ = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Como `create_dir_all`, mas as pastas criadas ficam com o dono certo.
|
||||||
|
pub fn create_dir_all_owned(path: &Path) -> io::Result<()> {
|
||||||
|
let mut missing = vec![];
|
||||||
|
let mut cur = Some(path);
|
||||||
|
while let Some(p) = cur {
|
||||||
|
if p.as_os_str().is_empty() || p.exists() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
missing.push(p.to_path_buf());
|
||||||
|
cur = p.parent();
|
||||||
|
}
|
||||||
|
std::fs::create_dir_all(path)?;
|
||||||
|
for p in missing.iter().rev() {
|
||||||
|
give_to_user(p);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Como `fs::write`, mas o arquivo fica com o dono certo.
|
||||||
|
pub fn write_owned(path: &Path, bytes: impl AsRef<[u8]>) -> io::Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
create_dir_all_owned(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(path, bytes)?;
|
||||||
|
give_to_user(path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const PASSWD: &str = "root:x:0:0:root:/root:/bin/bash\nlucas:x:1000:1000:Lucas:/home/lucas:/bin/bash\nmaria:x:1001:1002::/home/maria:/bin/zsh\nlinha-quebrada\n";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acha_o_usuario_por_uid_ou_nome() {
|
||||||
|
let u = lookup_user(PASSWD, Some(1000), None).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(u.uid, u.gid, u.home.to_str().unwrap()),
|
||||||
|
(1000, 1000, "/home/lucas")
|
||||||
|
);
|
||||||
|
let m = lookup_user(PASSWD, None, Some("maria")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
(m.uid, m.gid, m.home.to_str().unwrap()),
|
||||||
|
(1001, 1002, "/home/maria")
|
||||||
|
);
|
||||||
|
assert!(lookup_user(PASSWD, Some(4242), None).is_none());
|
||||||
|
// o uid manda sobre o nome
|
||||||
|
assert_eq!(
|
||||||
|
lookup_user(PASSWD, Some(1001), Some("lucas")).unwrap().uid,
|
||||||
|
1001
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fora_do_modo_root_nao_muda_dono_e_grava_normalmente() {
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let f = d.path().join("a/b/sessao.json");
|
||||||
|
write_owned(&f, b"{}").unwrap();
|
||||||
|
assert_eq!(std::fs::read(&f).unwrap(), b"{}");
|
||||||
|
assert!(create_dir_all_owned(&d.path().join("x/y")).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ pub struct RecoverReport {
|
|||||||
|
|
||||||
/// Recusa destinos que ficam no mesmo dispositivo da origem.
|
/// Recusa destinos que ficam no mesmo dispositivo da origem.
|
||||||
pub fn check_destination(source_path: Option<&Path>, dest: &Path) -> Result<()> {
|
pub fn check_destination(source_path: Option<&Path>, dest: &Path) -> Result<()> {
|
||||||
std::fs::create_dir_all(dest)?;
|
crate::owner::create_dir_all_owned(dest)?;
|
||||||
let dest = dest.canonicalize()?;
|
let dest = dest.canonicalize()?;
|
||||||
let Some(sp) = source_path else { return Ok(()) };
|
let Some(sp) = source_path else { return Ok(()) };
|
||||||
let sp_str = sp.to_string_lossy().to_string();
|
let sp_str = sp.to_string_lossy().to_string();
|
||||||
@@ -79,7 +79,7 @@ pub fn recover(
|
|||||||
cancelled: &(dyn Fn() -> bool + Sync),
|
cancelled: &(dyn Fn() -> bool + Sync),
|
||||||
progress: &mut dyn FnMut(usize, usize, u64),
|
progress: &mut dyn FnMut(usize, usize, u64),
|
||||||
) -> Result<RecoverReport> {
|
) -> Result<RecoverReport> {
|
||||||
std::fs::create_dir_all(dest)?;
|
crate::owner::create_dir_all_owned(dest)?;
|
||||||
let mut report = RecoverReport {
|
let mut report = RecoverReport {
|
||||||
destination: dest.display().to_string(),
|
destination: dest.display().to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -94,7 +94,7 @@ pub fn recover(
|
|||||||
let rel = target_rel(f);
|
let rel = target_rel(f);
|
||||||
let mut path = dest.join(&rel);
|
let mut path = dest.join(&rel);
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
if let Err(e) = std::fs::create_dir_all(parent) {
|
if let Err(e) = crate::owner::create_dir_all_owned(parent) {
|
||||||
report.failed.push((f.id, e.to_string()));
|
report.failed.push((f.id, e.to_string()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -144,8 +144,8 @@ pub fn recover(
|
|||||||
"algoritmos_hash": ["MD5", "SHA-1", "SHA-256"],
|
"algoritmos_hash": ["MD5", "SHA-1", "SHA-256"],
|
||||||
"relatorio": &report,
|
"relatorio": &report,
|
||||||
});
|
});
|
||||||
std::fs::write(
|
crate::owner::write_owned(
|
||||||
dest.join("guara-manifesto.json"),
|
&dest.join("guara-manifesto.json"),
|
||||||
serde_json::to_vec_pretty(&manifest)?,
|
serde_json::to_vec_pretty(&manifest)?,
|
||||||
)?;
|
)?;
|
||||||
Ok(report)
|
Ok(report)
|
||||||
@@ -176,6 +176,7 @@ pub fn write_file(src: &dyn Source, f: &Found, path: &Path) -> Result<Hashes> {
|
|||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(path)?;
|
.open(path)?;
|
||||||
|
crate::owner::give_to_user(path);
|
||||||
let mut hasher = MultiHasher::new();
|
let mut hasher = MultiHasher::new();
|
||||||
match &f.data {
|
match &f.data {
|
||||||
DataLoc::Resident { bytes } => {
|
DataLoc::Resident { bytes } => {
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ impl Engine {
|
|||||||
Some(q) => q.select(&s.found),
|
Some(q) => q.select(&s.found),
|
||||||
None => s.found.iter().collect(),
|
None => s.found.iter().collect(),
|
||||||
};
|
};
|
||||||
std::fs::write(path, crate::report::csv(&items))?;
|
crate::owner::write_owned(Path::new(path), crate::report::csv(&items))?;
|
||||||
Ok(items.len())
|
Ok(items.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +479,7 @@ impl Engine {
|
|||||||
recoveries: &s.recoveries,
|
recoveries: &s.recoveries,
|
||||||
source_hashes: hashes.as_ref(),
|
source_hashes: hashes.as_ref(),
|
||||||
});
|
});
|
||||||
std::fs::write(path, html)?;
|
crate::owner::write_owned(Path::new(path), html)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +607,7 @@ impl Engine {
|
|||||||
recoveries: s.recoveries.clone(),
|
recoveries: s.recoveries.clone(),
|
||||||
started_unix: s.started_unix,
|
started_unix: s.started_unix,
|
||||||
};
|
};
|
||||||
std::fs::write(path, serde_json::to_vec(&data)?)?;
|
crate::owner::write_owned(Path::new(path), serde_json::to_vec(&data)?)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,9 +816,7 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
"session_load" => json!({ "job": self.load_session(&s("path")?)? }),
|
"session_load" => json!({ "job": self.load_session(&s("path")?)? }),
|
||||||
"defaults" => {
|
"defaults" => {
|
||||||
let home = std::env::var("HOME")
|
let home = crate::owner::user_home().to_string_lossy().to_string();
|
||||||
.or_else(|_| std::env::var("USERPROFILE"))
|
|
||||||
.unwrap_or_default();
|
|
||||||
json!({ "home": home, "sep": std::path::MAIN_SEPARATOR.to_string() })
|
json!({ "home": home, "sep": std::path::MAIN_SEPARATOR.to_string() })
|
||||||
}
|
}
|
||||||
"version" => {
|
"version" => {
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ pub fn config_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
if let Some(home) = invoking_user_home() {
|
if let Some(u) = crate::owner::invoking_user() {
|
||||||
return default_dir(home);
|
return default_dir(u.home);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_dir(PathBuf::from(
|
default_dir(PathBuf::from(
|
||||||
@@ -69,25 +69,6 @@ fn default_dir(home: PathBuf) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Se somos root por sudo/pkexec, a home de quem invocou.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn invoking_user_home() -> Option<PathBuf> {
|
|
||||||
// SAFETY: geteuid não tem pré-condições.
|
|
||||||
if unsafe { libc::geteuid() } != 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let user = std::env::var("SUDO_USER").ok().filter(|u| u != "root");
|
|
||||||
let uid = std::env::var("PKEXEC_UID").ok();
|
|
||||||
let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
|
|
||||||
for l in passwd.lines() {
|
|
||||||
let f: Vec<&str> = l.split(':').collect();
|
|
||||||
if f.len() >= 6 && (user.as_deref() == Some(f[0]) || uid.as_deref() == Some(f[2])) {
|
|
||||||
return Some(PathBuf::from(f[5]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file() -> PathBuf {
|
fn file() -> PathBuf {
|
||||||
config_dir().join("settings.json")
|
config_dir().join("settings.json")
|
||||||
}
|
}
|
||||||
@@ -100,19 +81,6 @@ pub fn load() -> Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(s: &Settings) -> Result<()> {
|
pub fn save(s: &Settings) -> Result<()> {
|
||||||
let dir = config_dir();
|
crate::owner::write_owned(&file(), serde_json::to_vec_pretty(s)?)?;
|
||||||
std::fs::create_dir_all(&dir)?;
|
|
||||||
let path = file();
|
|
||||||
std::fs::write(&path, serde_json::to_vec_pretty(s)?)?;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
// Como root, devolve a posse ao dono da pasta para o app normal conseguir regravar.
|
|
||||||
use std::os::unix::fs::MetadataExt;
|
|
||||||
if unsafe { libc::geteuid() } == 0 {
|
|
||||||
if let Ok(m) = std::fs::metadata(&dir) {
|
|
||||||
let _ = std::os::unix::fs::chown(&path, Some(m.uid()), Some(m.gid()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user