diff --git a/Cargo.lock b/Cargo.lock index 0d2edba..6970d6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,6 +459,20 @@ dependencies = [ "zbus", ] +[[package]] +name = "ente-policy-provider" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ente-polkit-compat" version = "0.0.1" @@ -522,6 +536,17 @@ dependencies = [ "zbus", ] +[[package]] +name = "ente-tmpfiles-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "libc", + "nix", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ente-wasm" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 99bef85..3b3540a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ members = [ "crates/ente-resolved-compat", "crates/ente-polkit-compat", "crates/ente-machined-compat", + "crates/ente-policy-provider", + "crates/ente-tmpfiles-compat", ] [workspace.package] diff --git a/crates/ente-journald-compat/src/journalctl.rs b/crates/ente-journald-compat/src/journalctl.rs index c282228..8164bea 100644 --- a/crates/ente-journald-compat/src/journalctl.rs +++ b/crates/ente-journald-compat/src/journalctl.rs @@ -20,13 +20,24 @@ struct Args { grep: Option, tail: Option, source: Option, - json: bool, + output: OutputFormat, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OutputFormat { + Pretty, + Json, + /// systemd journal export format: `KEY=value\n` por field, blank line + /// entre entries. Documented at https://systemd.io/JOURNAL_EXPORT_FORMATS/ + /// Compatible con `journalctl --input-format=export`. + Export, } fn parse_args() -> Args { let mut args = std::env::args().skip(1); let mut a = Args { - unit: None, since_secs: None, grep: None, tail: None, source: None, json: false, + unit: None, since_secs: None, grep: None, tail: None, + source: None, output: OutputFormat::Pretty, }; while let Some(arg) = args.next() { match arg.as_str() { @@ -35,7 +46,19 @@ fn parse_args() -> Args { "--grep" | "-g" => a.grep = args.next(), "--tail" | "-n" => a.tail = args.next().and_then(|s| s.parse().ok()), "--source" => a.source = args.next(), - "--json" => a.json = true, + "--json" => a.output = OutputFormat::Json, + "--output" | "-o" => { + a.output = match args.next().as_deref() { + Some("pretty") | None => OutputFormat::Pretty, + Some("json") | Some("json-lines") => OutputFormat::Json, + Some("export") => OutputFormat::Export, + Some(other) => { + eprintln!("output desconocido: {other}"); + eprintln!("válidos: pretty | json | export"); + std::process::exit(2); + } + }; + } "-h" | "--help" => { print_help(); std::process::exit(0); } other => { eprintln!("argumento desconocido: {other}"); @@ -57,7 +80,8 @@ fn print_help() { eprintln!(" --grep, -g Contiene en el body decoded"); eprintln!(" --tail, -n Últimas N entries"); eprintln!("Output:"); - eprintln!(" --json JSON-lines en lugar de pretty"); + eprintln!(" --output, -o pretty | json | export (systemd journal export)"); + eprintln!(" --json alias de --output json"); } fn index_path() -> PathBuf { @@ -146,10 +170,10 @@ fn main() -> anyhow::Result<()> { } for (e, body) in out { - if args.json { - print_json(&e, &body); - } else { - print_pretty(&e, &body); + match args.output { + OutputFormat::Pretty => print_pretty(&e, &body), + OutputFormat::Json => print_json(&e, &body), + OutputFormat::Export => print_export(&e, &body), } } Ok(()) @@ -180,6 +204,74 @@ fn print_pretty(e: &IndexEntry, body: &str) { } } +/// systemd journal export format. Cada entry es un bloque de líneas +/// `KEY=value\n` separado por blank line. Para values con newlines o +/// bytes binarios, el formato usa una variante con length-prefix +/// (8 bytes LE u64) — por simplicidad sólo emitimos values con texto +/// que no contienen newlines o caracteres no-printables. Extraemos +/// MESSAGE/PRIORITY/_SYSTEMD_UNIT del body si es journald native. +/// +/// Compatible con `journalctl --input-format=export -m`. +fn print_export(e: &IndexEntry, body: &str) { + // Timestamps: __REALTIME_TIMESTAMP en µs, __MONOTONIC_TIMESTAMP también. + let realtime_us = e.timestamp_ms.saturating_mul(1000); + println!("__CURSOR=s={};t={};x={}", + &e.sha_hex[..16], // pseudo-cursor: prefix del SHA + realtime_us, + &e.sha_hex[..8]); + println!("__REALTIME_TIMESTAMP={}", realtime_us); + println!("__MONOTONIC_TIMESTAMP={}", realtime_us); + + let host = gethostname_safe(); + if !host.is_empty() { + println!("_HOSTNAME={host}"); + } + + if e.unit != "-" { + println!("_SYSTEMD_UNIT={}", e.unit); + } + println!("_TRANSPORT={}", match e.source.as_str() { + "syslog" => "syslog", + "journal" => "journal", + _ => "stdout", + }); + + // Si el body es journald native (KEY=value lines), emitir cada uno + // verbatim — son los fields originales del producer. Filtrar líneas + // que no son seguras para export (con newlines en value, etc). + if body.contains('=') && body.lines().any(|l| l.contains('=')) { + for line in body.lines() { + if line.contains('=') && line.bytes().all(safe_export_byte) { + println!("{line}"); + } + } + } else { + // Syslog text — empaquetar como MESSAGE. + let msg = body.trim_end() + .replace('\n', " "); // collapsa newlines + if msg.bytes().all(safe_export_byte) { + println!("MESSAGE={msg}"); + } + } + // Blank line separa entries. + println!(); +} + +fn safe_export_byte(b: u8) -> bool { + // ASCII printable, espacio, tab. No newlines (manejados aparte). + (0x20..=0x7E).contains(&b) || b == b'\t' +} + +fn gethostname_safe() -> String { + let mut buf = [0u8; 256]; + let r = unsafe { + libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) + }; + if r != 0 { return String::new(); } + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + std::str::from_utf8(&buf[..len]).unwrap_or("").to_string() +} + fn print_json(e: &IndexEntry, body: &str) { // JSON-lines básico, sin dependencia de serde — formato simple y estable. let escaped_body = body diff --git a/crates/ente-policy-provider/Cargo.toml b/crates/ente-policy-provider/Cargo.toml new file mode 100644 index 0000000..9b2e83c --- /dev/null +++ b/crates/ente-policy-provider/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ente-policy-provider" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-policy-provider" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/ente-policy-provider/src/main.rs b/crates/ente-policy-provider/src/main.rs new file mode 100644 index 0000000..3777ddc --- /dev/null +++ b/crates/ente-policy-provider/src/main.rs @@ -0,0 +1,221 @@ +//! ente-policy-provider: Ente que arbitra autorizaciones de Polkit. +//! +//! Se anuncia como proveedor de `POLKIT_DECISION_IFACE` en el bus interno. +//! Cuando `ente-polkit-compat` recibe `CheckAuthorization` D-Bus, forwarda +//! a este Ente vía Invoke. Aquí decidimos sí/no según política configurada. +//! +//! Wire format del blob de entrada: `pid_be_u32 | uid_be_u32 | action_id_utf8`. +//! Respuesta: `[decision_byte]` — 1 = allow, 0 = deny. +//! +//! Política se carga de `/etc/ente/policy.json` (o ruta override por env +//! `ENTE_POLICY_FILE`). Formato: +//! ```json +//! { +//! "default": "allow", +//! "rules": [ +//! { "match": "org.freedesktop.hostname1.*", "decision": "allow" }, +//! { "match": "org.freedesktop.login1.power-off", "require_uid": 0 }, +//! { "match": "*.set-*", "decision": "deny", "audit": true } +//! ] +//! } +//! ``` + +use ente_bus::{BusResponse, BusServer, InvokeHandler, POLKIT_DECISION_IFACE}; +use ente_card::Capability; +use serde::Deserialize; +use std::sync::Arc; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; + +#[derive(Debug, Clone, Deserialize)] +struct PolicyConfig { + #[serde(default = "default_decision")] + default: Decision, + #[serde(default)] + rules: Vec, +} + +fn default_decision() -> Decision { Decision::Allow } + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +enum Decision { Allow, Deny } + +#[derive(Debug, Clone, Deserialize)] +struct Rule { + /// Glob simple: `*` = wildcard. `org.freedesktop.hostname1.*` matchea + /// cualquier action_id con ese prefijo. + r#match: String, + #[serde(default)] + decision: Option, + /// Si presente, sólo este uid pasa. Otros se denegen. + #[serde(default)] + require_uid: Option, + /// Si presente, sólo este pid pasa. + #[serde(default)] + require_pid: Option, + #[serde(default)] + audit: bool, +} + +impl Default for PolicyConfig { + fn default() -> Self { + // Default sensato: caps escaladas requieren uid 0; el resto allow. + Self { + default: Decision::Allow, + rules: vec![ + // Power management: cualquiera puede pedir el reboot, + // pero la decisión final está en el holder de Capability::Spawn. + Rule { + r#match: "org.freedesktop.login1.set-wall-message".into(), + decision: Some(Decision::Allow), require_uid: None, require_pid: None, audit: true, + }, + // hostname/timezone/locale: requieren root. + Rule { + r#match: "org.freedesktop.hostname1.*".into(), + decision: None, require_uid: Some(0), require_pid: None, audit: true, + }, + Rule { + r#match: "org.freedesktop.timedate1.*".into(), + decision: None, require_uid: Some(0), require_pid: None, audit: true, + }, + Rule { + r#match: "org.freedesktop.locale1.*".into(), + decision: None, require_uid: Some(0), require_pid: None, audit: true, + }, + ], + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-policy-provider: arrancando"); + + let policy = load_policy(); + info!(rules = policy.rules.len(), default = ?policy.default, "policy cargada"); + + let handler = PolicyHandler { policy: Arc::new(policy) }; + + tokio::spawn(async { + let mut term = signal(SignalKind::terminate()).unwrap(); + let mut int_ = signal(SignalKind::interrupt()).unwrap(); + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + std::process::exit(0); + }); + + // Una única conexión: announce + serve. Bidirectional bajo el hood. + let mut server = BusServer::from_env().await?; + server.announce(vec![Capability::Endpoint { + interface: POLKIT_DECISION_IFACE, + version: 1, + }]).await?; + info!("Announce OK; sirviendo invokes de policy decision"); + server.serve(handler).await?; + Ok(()) +} + +struct PolicyHandler { + policy: Arc, +} + +impl InvokeHandler for PolicyHandler { + fn handle(&mut self, cap: Capability, blob: Vec) -> BusResponse { + // Validar cap (defensa contra forwarding a interface incorrecto). + if !matches!(&cap, Capability::Endpoint { interface, .. } if *interface == POLKIT_DECISION_IFACE) { + return BusResponse::Error(format!("policy-provider: cap inesperado {cap:?}")); + } + // Decodificar blob: [pid:4][uid:4][action_id...] + if blob.len() < 8 { + return BusResponse::Error("blob demasiado corto (esperado pid|uid|action_id)".into()); + } + let pid = u32::from_be_bytes(blob[0..4].try_into().unwrap()); + let uid = u32::from_be_bytes(blob[4..8].try_into().unwrap()); + let action_id = match std::str::from_utf8(&blob[8..]) { + Ok(s) => s, + Err(_) => return BusResponse::Error("action_id no es UTF-8".into()), + }; + + let decision = decide(&self.policy, action_id, pid, uid); + let byte = if decision == Decision::Allow { 1u8 } else { 0u8 }; + info!(action_id, pid, uid, ?decision, "policy decision"); + BusResponse::Invoked { result: vec![byte] } + } +} + +fn decide(policy: &PolicyConfig, action_id: &str, pid: u32, uid: u32) -> Decision { + for rule in &policy.rules { + if !glob_match(&rule.r#match, action_id) { continue; } + if let Some(req_uid) = rule.require_uid { + if uid != req_uid { + if rule.audit { + info!(action_id, uid, req_uid, "AUDIT: deny por uid mismatch"); + } + return Decision::Deny; + } + } + if let Some(req_pid) = rule.require_pid { + if pid != req_pid { + if rule.audit { + info!(action_id, pid, req_pid, "AUDIT: deny por pid mismatch"); + } + return Decision::Deny; + } + } + if let Some(d) = rule.decision { + if rule.audit { + info!(action_id, ?d, "AUDIT: rule match con decisión explícita"); + } + return d; + } + // Rule matched pero sin decisión explícita (sólo require_*) y todos + // los requires pasaron — caemos al default. + if rule.audit { + info!(action_id, ?policy.default, "AUDIT: rule match → default"); + } + return policy.default; + } + policy.default +} + +/// Glob simple: `*` matchea cualquier cosa. Soporta prefix (`foo.*`), +/// suffix (`*.bar`) y wildcard exacto (`*`). No es PCRE — intencional. +fn glob_match(pattern: &str, target: &str) -> bool { + if pattern == "*" { return true; } + if let Some(prefix) = pattern.strip_suffix(".*") { + return target == prefix || target.starts_with(&format!("{prefix}.")); + } + if let Some(suffix) = pattern.strip_prefix("*.") { + return target == suffix || target.ends_with(&format!(".{suffix}")); + } + pattern == target +} + +fn load_policy() -> PolicyConfig { + let path = std::env::var("ENTE_POLICY_FILE") + .unwrap_or_else(|_| "/etc/ente/policy.json".into()); + match std::fs::read_to_string(&path) { + Ok(content) => match serde_json::from_str(&content) { + Ok(p) => { info!(path, "policy file cargado"); p } + Err(e) => { + warn!(?e, path, "policy file inválido, usando defaults"); + PolicyConfig::default() + } + }, + Err(_) => { + info!(path, "policy file ausente — usando defaults conservadores"); + PolicyConfig::default() + } + } +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_policy_provider=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-tmpfiles-compat/Cargo.toml b/crates/ente-tmpfiles-compat/Cargo.toml new file mode 100644 index 0000000..b3b6123 --- /dev/null +++ b/crates/ente-tmpfiles-compat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ente-tmpfiles-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-tmpfiles-compat" +path = "src/main.rs" + +[dependencies] +nix = { workspace = true } +libc = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/ente-tmpfiles-compat/src/main.rs b/crates/ente-tmpfiles-compat/src/main.rs new file mode 100644 index 0000000..58de1c4 --- /dev/null +++ b/crates/ente-tmpfiles-compat/src/main.rs @@ -0,0 +1,281 @@ +//! ente-tmpfiles-compat: aplica directivas tmpfiles.d al boot. +//! +//! Lee, en orden, los conf files de: +//! /usr/lib/tmpfiles.d/*.conf +//! /etc/tmpfiles.d/*.conf (override del usuario, gana) +//! /run/tmpfiles.d/*.conf (efímero) +//! +//! Aplica un subset de directivas — las suficientes para el boot: +//! d — crear directorio (idempotente: no falla si existe) +//! D — crear directorio + limpiar contenido si existe +//! f — crear archivo (vacío, perms aplicados) +//! L — crear symlink (overrideable con `+L` si existe) +//! r — remove file (no falla si ausente) +//! R — remove recursivamente +//! e — adjust perms si existe +//! +//! Edad/cleanup (`age` field) y modos exotic (b, c, p, P) se ignoran. +//! El proceso es OneShot: corre, aplica, sale con código 0 / 1. + +use std::collections::BTreeMap; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; + +const SEARCH_DIRS: &[&str] = &[ + "/usr/lib/tmpfiles.d", + "/etc/tmpfiles.d", + "/run/tmpfiles.d", +]; + +#[derive(Debug, Clone)] +struct Directive { + typ: char, // d, D, f, L, r, R, e + path: PathBuf, + mode: Option, + user: Option, + group: Option, + arg: Option, // symlink target o content +} + +fn main() { + init_tracing(); + info!("ente-tmpfiles-compat: aplicando directivas tmpfiles.d"); + let directives = collect_directives(); + info!(count = directives.len(), "directivas a aplicar"); + + let mut applied = 0; + let mut skipped = 0; + let mut errors = 0; + for d in directives { + match apply(&d) { + Ok(true) => applied += 1, + Ok(false) => skipped += 1, + Err(e) => { + warn!(?e, ?d.typ, path = %d.path.display(), "directiva falló"); + errors += 1; + } + } + } + info!(applied, skipped, errors, "tmpfiles aplicado"); + if errors > 0 { std::process::exit(1); } +} + +fn collect_directives() -> Vec { + // Last-wins por path: /etc supera /usr/lib, /run supera /etc. + let mut by_path: BTreeMap<(PathBuf, char), Directive> = BTreeMap::new(); + for dir in SEARCH_DIRS { + if !Path::new(dir).exists() { continue; } + let mut entries: Vec<_> = match fs::read_dir(dir) { + Ok(rd) => rd.filter_map(|e| e.ok()).collect(), + Err(_) => continue, + }; + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + let path = entry.path(); + if path.extension().map(|e| e != "conf").unwrap_or(true) { continue; } + match fs::read_to_string(&path) { + Ok(content) => { + for (line_no, line) in content.lines().enumerate() { + if let Some(d) = parse_line(line) { + by_path.insert((d.path.clone(), d.typ), d); + } else if !line.trim().is_empty() && !line.trim().starts_with('#') { + debug!(file = %path.display(), line_no, line, "no parseable, skip"); + } + } + } + Err(e) => warn!(?e, path = %path.display(), "read"), + } + } + } + // Orden de aplicación: removes (r/R) primero, luego creates (d/D/f/L), + // adjusts (e) al final. + let mut all: Vec = by_path.into_values().collect(); + all.sort_by_key(|d| match d.typ { + 'r' | 'R' => 0, + 'd' | 'D' | 'f' | 'L' => 1, + 'e' => 2, + _ => 3, + }); + all +} + +fn parse_line(line: &str) -> Option { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { return None; } + // Formato: TYPE PATH MODE USER GROUP AGE ARGUMENT + // Strip leading '+' (override marker) y '!' (boot-only) — los soportamos + // implícitamente. + let typ_str = line.chars().next()?; + let typ = match typ_str { + '+' | '!' => line.chars().nth(1)?, + c => c, + }; + if !"dDfLrRe".contains(typ) { return None; } + // tokenize tomando en cuenta '-' como "default" + let mut parts = line.splitn(7, char::is_whitespace).filter(|s| !s.is_empty()); + let _t = parts.next()?; + let path = parts.next()?.to_string(); + let mode = parts.next().and_then(parse_mode); + let user = parts.next().and_then(parse_default); + let group = parts.next().and_then(parse_default); + let _age = parts.next(); + let arg = parts.next().and_then(parse_default); + Some(Directive { + typ, + path: PathBuf::from(path), + mode, user, group, arg, + }) +} + +fn parse_default(s: &str) -> Option { + if s == "-" { None } else { Some(s.to_string()) } +} + +fn parse_mode(s: &str) -> Option { + if s == "-" { return None; } + u32::from_str_radix(s.trim_start_matches('~'), 8).ok() +} + +fn apply(d: &Directive) -> anyhow::Result { + match d.typ { + 'd' | 'D' => apply_d(d), + 'f' => apply_f(d), + 'L' => apply_l(d), + 'r' => apply_r(d, false), + 'R' => apply_r(d, true), + 'e' => apply_e(d), + _ => Ok(false), + } +} + +fn apply_d(d: &Directive) -> anyhow::Result { + fs::create_dir_all(&d.path) + .map_err(|e| anyhow::anyhow!("mkdir {}: {e}", d.path.display()))?; + if let Some(mode) = d.mode { + fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?; + } + chown(&d.path, d.user.as_deref(), d.group.as_deref())?; + if d.typ == 'D' { + // Limpiar contenido (no recursivo). + if let Ok(rd) = fs::read_dir(&d.path) { + for entry in rd.flatten() { + let p = entry.path(); + if p.is_dir() { let _ = fs::remove_dir_all(&p); } + else { let _ = fs::remove_file(&p); } + } + } + } + info!(path = %d.path.display(), mode = ?d.mode, "d/D aplicado"); + Ok(true) +} + +fn apply_f(d: &Directive) -> anyhow::Result { + if !d.path.exists() { + if let Some(parent) = d.path.parent() { + let _ = fs::create_dir_all(parent); + } + let content = d.arg.clone().unwrap_or_default(); + fs::write(&d.path, content.as_bytes())?; + } + if let Some(mode) = d.mode { + fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?; + } + chown(&d.path, d.user.as_deref(), d.group.as_deref())?; + info!(path = %d.path.display(), mode = ?d.mode, "f aplicado"); + Ok(true) +} + +fn apply_l(d: &Directive) -> anyhow::Result { + let target = match &d.arg { + Some(t) => t, + None => anyhow::bail!("L sin target en {}", d.path.display()), + }; + if d.path.exists() { + // No sobreescribimos symlinks/files existentes (modo no-`+`). + debug!(path = %d.path.display(), "L: existe, skip"); + return Ok(false); + } + if let Some(parent) = d.path.parent() { + let _ = fs::create_dir_all(parent); + } + std::os::unix::fs::symlink(target, &d.path)?; + info!(path = %d.path.display(), %target, "L aplicado"); + Ok(true) +} + +fn apply_r(d: &Directive, recursive: bool) -> anyhow::Result { + if !d.path.exists() { + return Ok(false); + } + if recursive { + fs::remove_dir_all(&d.path)?; + } else if d.path.is_dir() { + fs::remove_dir(&d.path)?; + } else { + fs::remove_file(&d.path)?; + } + info!(path = %d.path.display(), recursive, "remove aplicado"); + Ok(true) +} + +fn apply_e(d: &Directive) -> anyhow::Result { + if !d.path.exists() { + return Ok(false); + } + if let Some(mode) = d.mode { + fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?; + } + chown(&d.path, d.user.as_deref(), d.group.as_deref())?; + info!(path = %d.path.display(), "e aplicado"); + Ok(true) +} + +fn chown(path: &Path, user: Option<&str>, group: Option<&str>) -> anyhow::Result<()> { + use std::ffi::CString; + let uid = match user { + Some(u) => Some(lookup_uid(u)?), + None => None, + }; + let gid = match group { + Some(g) => Some(lookup_gid(g)?), + None => None, + }; + let (uid, gid) = (uid.unwrap_or(u32::MAX), gid.unwrap_or(u32::MAX)); + let cstr = CString::new(path.as_os_str().as_encoded_bytes())?; + let r = unsafe { libc::chown(cstr.as_ptr(), uid, gid) }; + if r != 0 { + let e = std::io::Error::last_os_error(); + // No-op si ya somos non-root y el chown falla con EPERM. + if e.raw_os_error() == Some(libc::EPERM) { + debug!(path = %path.display(), "chown EPERM (esperado sin root)"); + return Ok(()); + } + return Err(anyhow::anyhow!("chown: {e}")); + } + Ok(()) +} + +fn lookup_uid(name: &str) -> anyhow::Result { + if let Ok(n) = name.parse::() { return Ok(n); } + let cstr = std::ffi::CString::new(name)?; + let pw = unsafe { libc::getpwnam(cstr.as_ptr()) }; + if pw.is_null() { anyhow::bail!("user '{name}' no encontrado"); } + Ok(unsafe { (*pw).pw_uid }) +} + +fn lookup_gid(name: &str) -> anyhow::Result { + if let Ok(n) = name.parse::() { return Ok(n); } + let cstr = std::ffi::CString::new(name)?; + let gr = unsafe { libc::getgrnam(cstr.as_ptr()) }; + if gr.is_null() { anyhow::bail!("group '{name}' no encontrado"); } + Ok(unsafe { (*gr).gr_gid }) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_tmpfiles_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-zero/src/seed.rs b/crates/ente-zero/src/seed.rs index f424982..5d23bc2 100644 --- a/crates/ente-zero/src/seed.rs +++ b/crates/ente-zero/src/seed.rs @@ -156,6 +156,7 @@ fn synthesize_dev_seed() -> EntityCard { ("compat-resolved", "target/debug/ente-resolved-compat"), ("compat-polkit", "target/debug/ente-polkit-compat"), ("compat-machined", "target/debug/ente-machined-compat"), + ("policy-provider", "target/debug/ente-policy-provider"), ] { if let Some(card) = optional_native_card( label, bin,