Wer schreiben darf, darf löschen? [update]

Implementierung eines Drop-Only Archiv-Verzeichnisses unter Linux

Wer schreiben darf, darf löschen! Diese Aussage ging mir durch den Kopf, als ein Kunde mir vor Kurzem am Telefon von seiner Idee erzählte: Für das interne Auftragsmanagement soll eine Ablagestruktur auf dem Samba-basierten Fileserver realisiert werden. Die Herausforderung: Die Ablagestruktur soll eine Art "Drop-only"-Ordner sein. Benutzer einer bestimmte Gruppe sollen beliebig Dateien in den Ablageordner schreiben dürfen, anschließend sollen die Dateien allerdings unveränderbar sein. Kein nachträgliches Bearbeiten, kein Umbenennen und schon gar kein Löschen - auch nicht für den Ersteller der Datei.

Problem: Wer schreiben darf, darf löschen

Mein erster Gedanke zu dieser Idee war der eingangs erwähnte: "Wer schreiben darf, darf löschen!" Wenn unter Linux einem Benutzer Schreibrechte auf einem Verzeichnis eingeräumt werden, kann dieser Benutzer in diesem Verzeichnis im Endeffekt alles: Dateien erzeugen, umbenennen und löschen. Dabei ist es total egal, wie die Rechte für die betroffenen Dateien gesetzt sind.

Weil ich bei solchen Problemfragen aber ungern "das geht nicht" sage, sagte ich am Telefon eher so etwas wie "Irgendwie geht das bestimmt - ich denke darüber nach".

Und tatsächlich - so ein Szenario lässt sich umsetzen, und der zugehörende Aufwand hält sich dabei in Grenzen.

Baustein 1: Das Sticky-Bit

Meine Gedankengänge liefen ungefähr folgendermaßen ab:

  1. Wer unter Linux in einem Verzeichnis Schreibrechte hat, hat gleichzeitig auch das Recht, Dateien umzubenennen und zu löschen.

  2. Als eines der drei "Zusatzrechte" im Dateisystem gibt es allerdings das Sticky-Bit. Dieses sorgt - auf ein Verzeichnis angewendet - dafür, dass in diesem Verzeichnis eine Datei nur von ihrem Eigentümer gelöscht oder umbenannt werden kann. Eingesetzt wird das Sticky-Bit unter Linux z.B. auf dem "/tmp"-Verzeichnis. Dadurch kann in diesem Verzeichnis zwar jeder Benutzer Vollzugriff (Schreibrechte) haben, die Hoheit über die Dateien unterhalb von "/tmp" liegt allerdings komplett beim Ersteller der jeweiligen Datei.

  3. Bleibt noch das Problem, dass auch die selber hinzugefügten Dateien vor dem Überschreiben durch den Eigentümer geschützt werden sollen. Wie wäre es, wenn man direkt nach dem Erstellen einer Datei ihren Eigentümer anpasst. Dann hat man die Datei aus dem Wirkungsbereich des Erstellers entzogen.
Die Punkte 1 und 2 lassen sich schnell umsetzen: Angenommen, das Verzeichnis "/srv/archiv" soll für Mitglieder der Gruppe "archiv" als Dokumentenarchiv nutzbar sein, könnte die Erstkonfiguration des Verzeichnisses in etwa so aussehen:
root@host:~# mkdir /srv/archiv
root@host:~# chgrp archiv /srv/archiv
root@host:~# chmod 770 /srv/archiv
root@host:~# chmod o+t /srv/archiv
root@host:~# ls -ld /srv/archiv
drwxrwx--- 2 root archiv 4096 Feb 17 13:12 /srv/archiv
Die Punkte 1 und 2 wären somit umgesetzt. Bleibt Punkt 3. Für die Lösung dieses Problems lässt sich das "inotify"-Framework verwenden. Mit den zum Framework gehörenden Werkzeugen lässt sich nahezu in Echtzeit auf Dateisystemoperationen reagieren.

Baustein 2: inotifywait

Mit dem Werkzeug "inotifywait" lassen sich sehr effizient Dateisystemänderungen überwachen, um dann entsprechend zu reagieren. Bei Ubuntu findet sich das Tool im Paket "inotify-tools".
Mit den richtigen Parametern aufgerufen, überwacht inotifywait das Archiv-Verzeichnis und berichtet fortlaufend über erkannte Änderungen.

Folgender Aufruf überwacht "/srv/archiv" rekursiv und gibt die erkannten Änderungen in Semikolon-getrennten Feldern aus. 
root@host:~# inotifywait -m -r --format "%w;%f;%e" /srv/archiv

Im folgenden die Ausgabe nach dem Erstellen von einer Datei im überwachten Verzeichnis:

root@host:~# inotifywait -m -r --format "%w;%f;%e" /srv/archiv
Setting up watches.  
Watches established.
/srv/archiv/;beispiel;CREATE
/srv/archiv/;beispiel;OPEN
/srv/archiv/;beispiel;ATTRIB
/srv/archiv/;beispiel;CLOSE_WRITE,CLOSE
Mit einem kleinen Skript lassen sich die so erkannten Änderungen einlesen und die notwendigen Aktionen anstoßen. Wenn wir zum Beispiel nach dem Schreiben einer Datei (nach dem Event "CLOSE_WRITE") die betreffende Datei mit "chown root:root" dem Benutzer root übergeben, hätten wir unsere Aufgabenstellung gelöst,

Alles zusammen im Skript

Um die Lösung abzurunden, sollten wir auch die initialen Berechtigungen auf das Archiv-Verzeichnis sowie auch die Rechte für alle nachträglich erstellten Verzeichnisse beachten. Das unten aufgeführte Beispiel-Skript setzt die gewünschten Dateisystemberechtigungen per ACL (die Gruppe "archiv" darf schreiben). Bei neu erstellten Dateien, wird der Eigentümer auf den Benutzer root geändert. Bei neu erstellten Verzeichnissen (Flag "ISDIR") werden die geforderten ACLs an das Verzeichnis gehängt:

#!/bin/bash
WATCHDIR="/srv/archiv/"
ACL="group:archiv:rwx"

# alles gehört root
chown -R root:root $WATCHDIR

# per ACL die Rechte für die Gruppe "archiv" setzen
setfacl --set $ACL $WATCHDIR

# Die Berechtigungen für alle bereits vorhandenen Verzeichnisse
find $WATCHDIR -type d --exec chmod o+t {} \;
find $WATCHDIR -type d --exec setfacl --set $ACL {} \;

while /bin/true ; do
        while IFS=\; read DIR FILE ACTION ; do
                case $ACTION in 
                        *ISDIR*) 
                                setfacl --set $ACL $DIR/$FILE
                                chmod o+t "$DIR/$FILE"
                                chown root:root "$DIR/$FILE"
                                ;;
                        *CLOSE_WRITE*)
                                chown root:root "$DIR/$FILE"
                                ;;
                esac 
        done < <(inotifywait -m -r \
                             -e close_write -e create \
                             --format "%w;%f;%e" $WATCHDIR )
done

Wer schreiben darf, darf nicht immer löschen

Zumindest dann nicht, wenn wir auf Verzeichnisebene das Sticky-Bit verwenden und mit Hilfe von inotifywait den Eigentümer von neu erstellten Dateien direkt anpassen. 

Update vom 30.06.2017

Wie sich herausgestellt hat, gibt es einen Spezialfall, für den die obige Lösung nicht korrekt funktioniert: Wenn Dateien vom Benutzer nicht in den Drop-Only Ordner kopiert, sondern auf Dateisystem-Ebene verschoben werden. Dann wird nämlich das inotify-event "MOVED_TO" generiert, auf das das Skript auch generieren muss. Und da die Benutzer möglicherweise auch ganze Verzeichnisse in den Ordner verschieben, sollte der chown-Aufruf für diesen Fall rekursiv erfolgen. Folgendes Listing zeigt das optimierte Skript. Die geänderten Zeilen habe ich markiert.
#!/bin/bash
WATCHDIR="/srv/archiv/"
ACL="group:archiv:rwx"

# alles gehört root
chown -R root:root $WATCHDIR

# per ACL die Rechte für die Gruppe "archiv" setzen
setfacl --set $ACL $WATCHDIR

# Die Berechtigungen für alle bereits vorhandenen Verzeichnisse
find $WATCHDIR -type d --exec chmod o+t {} \;
find $WATCHDIR -type d --exec setfacl --set $ACL {} \;

while /bin/true ; do
        while IFS=\; read DIR FILE ACTION ; do
                case $ACTION in 
                        *ISDIR*) 
                                setfacl --set $ACL $DIR/$FILE
                                chmod o+t "$DIR/$FILE"
                                chown -R root:root "$DIR/$FILE"
                                ;;
                        *CLOSE_WRITE*|*MOVED_TO*)
                                chown root:root "$DIR/$FILE"
                                ;;
                esac 
        done < <(inotifywait -m -r \
                             -e close_write -e create -e moved_to\
                             --format "%w;%f;%e" $WATCHDIR )
done