Fix: Mount-Verzeichnisse bekommen explizite Rechte statt sich auf Umask zu verlassen

mount_base_dir (von allen Paaren gemeinsam genutzt) sowie die versteckten
Backing-Verzeichnisse eines Paares wurden bisher nur per create_dir_all()
angelegt, ohne eigenes chmod/chown - die tatsaechlichen Zugriffsrechte hingen
also allein vom Umask des root-Prozesses ab statt von einer bewussten
Entscheidung.

- activate_symlink() setzt den gemeinsamen Elternordner (mount_base_dir) jetzt
  hart auf 0755, unabhaengig vom Umask.
- reconcile::mount_side() chownt das jeweilige Backing-Verzeichnis eines
  Paares mit gesetztem owner_user auf diesen Nutzer und setzt 0700 (neue
  Funktion target::ensure_owner_only_dir) - insbesondere fuer NFS relevant,
  wo es (anders als CIFS/WebDAV) keine uid=/gid=-Mount-Option gibt, die den
  Zugriff nach dem Mounten sonst regeln wuerde.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 13:34:06 +02:00
co-authored by Claude Sonnet 5
parent f0c0a69b9c
commit bd319523a7
3 changed files with 75 additions and 4 deletions
+9 -4
View File
@@ -105,9 +105,12 @@ Jedes Laufwerkspaar bekommt sein **eigenes** Unterverzeichnis unter `settings.mo
root, siehe oben) - `/run/media` ist auf den meisten Systemen bereits die übliche Konvention root, siehe oben) - `/run/media` ist auf den meisten Systemen bereits die übliche Konvention
für eingebundene Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) und liegt auf `tmpfs`, für eingebundene Wechseldatenträger/Netzlaufwerke (z. B. udisks2/GNOME) und liegt auf `tmpfs`,
muss also nie persistieren. Der Mountpoint selbst (und alle nötigen Elternverzeichnisse) werden muss also nie persistieren. Der Mountpoint selbst (und alle nötigen Elternverzeichnisse) werden
bei jedem `mount`/`watch`-Lauf automatisch angelegt, falls sie fehlen. Ist für ein Paar bei jedem `mount`/`watch`-Lauf automatisch angelegt, falls sie fehlen - `mount_base_dir` selbst
`owner_user` gesetzt, bekommt genau dieser Nutzer über die Mount-Optionen (`uid=`/`gid=`, siehe fest mit `0755` (durchquerbar für jeden lokalen Nutzer, unabhängig vom Umask des root-Prozesses),
"Architekturentscheidungen" unten) vollen Zugriff auf den Inhalt - die Trennung passiert also das jeweilige Backing-Verzeichnis (siehe unten) bei gesetztem `owner_user` zusätzlich fest
`chown`t und `0700` (nur der Owner). Ist für ein Paar `owner_user` gesetzt, bekommt genau dieser
Nutzer bei CIFS/WebDAV zusätzlich über die Mount-Optionen (`uid=`/`gid=`, siehe
"Architekturentscheidungen" unten) vollen Zugriff auf den *Inhalt* - die Trennung passiert also
über Zugriffsrechte, nicht über getrennte Mountpoint-Namensräume pro Nutzer. über Zugriffsrechte, nicht über getrennte Mountpoint-Namensräume pro Nutzer.
Der Standard lässt sich in `config.toml` unter `[settings] mount_base_dir = "..."` jederzeit Der Standard lässt sich in `config.toml` unter `[settings] mount_base_dir = "..."` jederzeit
@@ -207,7 +210,9 @@ Eintragen ausgegeben:
vollen Zugriff darauf, sofern nicht per `--owner-user` (nicht-interaktiv) oder im vollen Zugriff darauf, sofern nicht per `--owner-user` (nicht-interaktiv) oder im
interaktiven Prompt bewusst ein anderer Nutzer angegeben bzw. abgelehnt wird. Bei NFS gibt es interaktiven Prompt bewusst ein anderer Nutzer angegeben bzw. abgelehnt wird. Bei NFS gibt es
keine clientseitige `uid=`/`gid=`-Option, Zugriff bestimmt dort ausschließlich der Server über keine clientseitige `uid=`/`gid=`-Option, Zugriff bestimmt dort ausschließlich der Server über
die tatsächlichen Datei-Eigentümer/-Rechte des Exports. die tatsächlichen Datei-Eigentümer/-Rechte des Exports - `owner_user` wirkt sich hier nur auf
das lokale Backing-Verzeichnis selbst aus (`chown`+`0700`, s. o.), nicht auf das, was der
Server tatsächlich exportiert.
- **davfs2-Konfiguration**: SmartMount setzt in `davfs2.conf` automatisch `gui_optimize 1` - **davfs2-Konfiguration**: SmartMount setzt in `davfs2.conf` automatisch `gui_optimize 1`
(bündelt PROPFIND-Anfragen, wichtig für grafische Dateimanager) sowie `buf_size 16384` (bündelt PROPFIND-Anfragen, wichtig für grafische Dateimanager) sowie `buf_size 16384`
(deutlich über dem Standard von 16 KiB) - Letzteres behebt ein bekanntes Praxisproblem, bei (deutlich über dem Standard von 16 KiB) - Letzteres behebt ein bekanntes Praxisproblem, bei
+60
View File
@@ -15,6 +15,7 @@
//! kurz während eines Umschaltens (siehe [`crate::reconcile`]) mehr als ein Backing-Verzeichnis //! kurz während eines Umschaltens (siehe [`crate::reconcile`]) mehr als ein Backing-Verzeichnis
//! gemountet - im Ruhezustand immer nur eines, wie ursprünglich vorgesehen. //! gemountet - im Ruhezustand immer nur eines, wie ursprünglich vorgesehen.
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide, MountKind}; use crate::config::{CloudSide, DrivePair, GlobalSettings, LocalSide, MountKind};
@@ -159,6 +160,21 @@ fn resolve_uid_gid(username: &str) -> Result<(u32, u32)> {
Ok((run_id(username, "-u")?, run_id(username, "-g")?)) Ok((run_id(username, "-u")?, run_id(username, "-g")?))
} }
/// Macht `owner` (Nutzer + dessen primäre Gruppe) zum alleinigen Besitzer von `dir` (`0700`).
/// Für das Backing-Verzeichnis eines Paares mit gesetztem `owner_user` - unabhängig vom
/// Mount-Typ, also auch für NFS, wo es (anders als bei CIFS/WebDAV) keine `uid=`/`gid=`-
/// Mount-Option gibt, die die nach dem Mounten sichtbaren Rechte übersteuern könnte: das
/// Verzeichnis selbst gehört so wenigstens `owner` - reicht z. B. aus, um es zu betreten, bevor/
/// falls etwas gemountet ist, ersetzt aber nicht die serverseitige NFS-Export-Rechtevergabe
/// (siehe README, Abschnitt "Voller Zugriff für einen bestimmten Nutzer").
pub(crate) fn ensure_owner_only_dir(dir: &Path, owner: &str) -> Result<()> {
let (uid, gid) = resolve_uid_gid(owner)?;
std::os::unix::fs::chown(dir, Some(uid), Some(gid)).map_err(|e| Error::io(dir, e))?;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
.map_err(|e| Error::io(dir, e))?;
Ok(())
}
fn run_id(username: &str, flag: &str) -> Result<u32> { fn run_id(username: &str, flag: &str) -> Result<u32> {
let output = std::process::Command::new("id") let output = std::process::Command::new("id")
.arg(flag) .arg(flag)
@@ -263,6 +279,13 @@ pub fn activate_symlink(pair: &DrivePair, side: Side) -> Result<()> {
if let Some(parent) = link_path.parent() { if let Some(parent) = link_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?; std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
// `mount_base_dir` ist von allen Paaren (mit ggf. unterschiedlichem `owner_user`)
// gemeinsam genutzt - hart auf `0755` setzen statt sich auf das Umask des root-Prozesses
// zu verlassen, damit jeder lokale Nutzer bis zum eigentlichen Mountpoint durchqueren
// kann (die eigentliche Zugriffsbeschränkung passiert weiter unten je Paar, siehe
// `ensure_owner_only_dir`/die `uid=`/`gid=`-Mount-Optionen).
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o755))
.map_err(|e| Error::io(parent, e))?;
} }
let target = backing_dir(pair, side); let target = backing_dir(pair, side);
@@ -466,4 +489,41 @@ mod tests {
let err = activate_symlink(&pair, Side::Local).unwrap_err(); let err = activate_symlink(&pair, Side::Local).unwrap_err();
assert!(err.to_string().contains("real directory")); assert!(err.to_string().contains("real directory"));
} }
#[test]
fn ensure_owner_only_dir_sets_mode_0700() {
let user = std::env::var("USER").expect("USER env var set in test environment");
let dir = tempfile::tempdir().expect("tempdir");
ensure_owner_only_dir(dir.path(), &user).expect("ensure_owner_only_dir");
let mode = std::fs::metadata(dir.path())
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o700);
}
#[test]
fn ensure_owner_only_dir_fails_clearly_for_unknown_user() {
let dir = tempfile::tempdir().expect("tempdir");
let err = ensure_owner_only_dir(dir.path(), "no-such-user-xyz").unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[test]
fn activate_symlink_sets_shared_parent_dir_to_0755() {
let dir = tempfile::tempdir().expect("tempdir");
let parent = dir.path().join("mount-base");
let mut pair = sample_pair();
pair.mount_point = parent.join("pair-1");
activate_symlink(&pair, Side::Local).expect("activate local");
let mode = std::fs::metadata(&parent)
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o755);
}
} }
+6
View File
@@ -282,6 +282,12 @@ async fn mount_side(
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?; target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
std::fs::create_dir_all(&mount_target.mount_point) std::fs::create_dir_all(&mount_target.mount_point)
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?; .map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
// Backing-Verzeichnis auf `owner_user` chownen (0700) - relevant vor allem für NFS, wo es
// (anders als CIFS/WebDAV) keine `uid=`/`gid=`-Mount-Option gibt, die den Zugriff nach dem
// Mounten sonst regeln würde (siehe `target::ensure_owner_only_dir`).
if let Some(owner) = &pair.owner_user {
target::ensure_owner_only_dir(&mount_target.mount_point, owner)?;
}
let backend = mount::backend_for(target::side_kind(pair, side)); let backend = mount::backend_for(target::side_kind(pair, side));
backend.check_available()?; backend.check_available()?;