Personalisierung: Kiosk abbauen, Anmeldebildschirm nach Kontoänderung neu (ADR-0041, 24.10.2026)
Rolle personalisierung baut bei present und absent Konto, Kiosk, PAM-Zeile, Wächter und LightDM-Konfiguration ab; Wrapper erst nach Neustart von LightDM. persoenliche-konten.py startet LightDM nach angelegtem, gesperrtem, entsperrtem oder entferntem Konto neu (nur ohne Benutzersitzung); Hilfsskript wird immer abgelegt, damit der Schnellweg auch beim ersten Konto greift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82b68dc64f
commit
f975938e3c
10 files changed
+144
-809
No files matched your search
@@ -32,6 +32,44 @@ GRUENER_HINTERGRUND = "/usr/share/backgrounds/linuxmint/sele_ring_green.jpg"
|
||||
|
||||
VEYON_MASTER_GRUPPE = "tuxflotte-veyon-master"
|
||||
|
||||
# Ändert sich, welche Konten der Anmeldebildschirm zeigt (angelegt, gesperrt,
|
||||
# entsperrt, entfernt), startet das Skript LightDM am Ende neu, aber nur ohne
|
||||
# laufende Benutzersitzung. slick-greeter übernimmt solche Änderungen nicht
|
||||
# live (Liebherr-84, 18.10.2026). Seit dem vereinfachten Ablauf (ADR-0041,
|
||||
# Ergänzung 24.10.2026) wartet der Benutzer nach dem Neustart am Gerät auf
|
||||
# sein Konto; ohne den Neustart des Anmeldebildschirms stünde es erst nach
|
||||
# dem nächsten Start des Geräts in der Liste. Hier und nicht in der Rolle:
|
||||
# Der Agent ruft das Skript schon direkt nach dem Check-in auf.
|
||||
_anmeldebildschirm_geaendert = False
|
||||
|
||||
|
||||
def anmeldebildschirm_vormerken():
|
||||
global _anmeldebildschirm_geaendert
|
||||
_anmeldebildschirm_geaendert = True
|
||||
|
||||
|
||||
def benutzersitzung_laeuft():
|
||||
ergebnis = ausfuehren("loginctl", "list-sessions", "--no-legend")
|
||||
for zeile in (ergebnis.stdout or "").splitlines():
|
||||
teile = zeile.split()
|
||||
if not teile:
|
||||
continue
|
||||
klasse = ausfuehren("loginctl", "show-session", teile[0], "--property=Class", "--value").stdout.strip()
|
||||
if klasse == "user":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def anmeldebildschirm_auffrischen():
|
||||
# try-restart: Läuft LightDM noch nicht (früh beim Start) oder gar nicht
|
||||
# (GDM übernimmt neue Konten selbst), passiert nichts.
|
||||
if benutzersitzung_laeuft():
|
||||
print("Anmeldebildschirm: Benutzersitzung läuft, Änderung erscheint nach dem Abmelden")
|
||||
return
|
||||
if ausfuehren("systemctl", "is-active", "--quiet", "lightdm.service").returncode == 0:
|
||||
ausfuehren("systemctl", "try-restart", "lightdm.service")
|
||||
print("Anmeldebildschirm neu gestartet")
|
||||
|
||||
|
||||
def ausfuehren(*befehl, eingabe=None):
|
||||
return subprocess.run(befehl, input=eingabe, capture_output=True, text=True)
|
||||
@@ -178,6 +216,7 @@ def anlegen(konto):
|
||||
os.makedirs(MARKER_DIR, mode=0o700, exist_ok=True)
|
||||
with open(marker_pfad(name), "w", encoding="utf-8") as f:
|
||||
f.write(konto_id + "\n")
|
||||
anmeldebildschirm_vormerken()
|
||||
return "angelegt", None
|
||||
|
||||
|
||||
@@ -188,6 +227,7 @@ def aktiv(konto):
|
||||
ausfuehren("chage", "--expiredate", "-1", name)
|
||||
if im_anmeldebildschirm(name, True):
|
||||
ausfuehren("systemctl", "try-restart", "accounts-daemon.service")
|
||||
anmeldebildschirm_vormerken()
|
||||
return "aktiv", None
|
||||
|
||||
|
||||
@@ -200,6 +240,7 @@ def sperren(konto):
|
||||
sitzungen_beenden(name)
|
||||
if im_anmeldebildschirm(name, False):
|
||||
ausfuehren("systemctl", "try-restart", "accounts-daemon.service")
|
||||
anmeldebildschirm_vormerken()
|
||||
return "gesperrt", None
|
||||
|
||||
|
||||
@@ -216,6 +257,7 @@ def entfernen(konto):
|
||||
# 12: Home konnte nicht ganz entfernt werden, das Konto ist weg.
|
||||
if ergebnis.returncode not in (0, 12):
|
||||
return "fehler", f"userdel: {ergebnis.stderr.strip()[:300]}"
|
||||
anmeldebildschirm_vormerken()
|
||||
try:
|
||||
os.remove(os.path.join(ACCOUNTS_DIR, name))
|
||||
except FileNotFoundError:
|
||||
@@ -332,6 +374,10 @@ def main():
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(ergebnisse, f)
|
||||
|
||||
# Erst nach dem Ergebnis: der Neustart darf die Meldung nicht aufhalten.
|
||||
if _anmeldebildschirm_geaendert:
|
||||
anmeldebildschirm_auffrischen()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,12 +7,22 @@
|
||||
# Ein Konto, das nicht in der Liste steht, fasst die Rolle nie an - auch
|
||||
# nicht bei leerer Liste (ein älterer Server oder Agent schickt keine).
|
||||
|
||||
# Immer ablegen, auch ohne Konten: Der Agent ruft das Skript direkt nach dem
|
||||
# Check-in auf (Schnellweg), aber nur, wenn es schon da ist. Sonst entstünde
|
||||
# das allererste Konto eines Geräts erst mitten im Ansible-Lauf, und die
|
||||
# zugesagten 2 Minuten nach dem Neustart reichten nicht (ADR-0041, Ergänzung
|
||||
# 24.10.2026).
|
||||
- name: Ensure helper script directory exists
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/lib/tuxflotte
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Install helper for personal accounts
|
||||
ansible.builtin.copy:
|
||||
src: persoenliche-konten.py
|
||||
dest: /usr/local/lib/tuxflotte/persoenliche-konten.py
|
||||
mode: "0755"
|
||||
when: tuxflotte_persoenliche_konten | length > 0
|
||||
|
||||
- name: Write desired personal accounts (root only)
|
||||
ansible.builtin.copy:
|
||||
@@ -31,7 +41,7 @@
|
||||
- /run/tuxflotte/agent/persoenliche-konten-soll.json
|
||||
- /run/tuxflotte/agent/persoenliche-konten.json
|
||||
register: tuxflotte_konten_lauf
|
||||
changed_when: tuxflotte_konten_lauf.stdout | regex_search('-> (angelegt|gesperrt|entfernt)|Passwort zurückgesetzt|Hintergrund gesetzt') is not none
|
||||
changed_when: tuxflotte_konten_lauf.stdout | regex_search('-> (angelegt|gesperrt|entfernt)|Passwort zurückgesetzt|Hintergrund gesetzt|Anmeldebildschirm neu gestartet') is not none
|
||||
when: tuxflotte_persoenliche_konten | length > 0
|
||||
|
||||
- name: Remove desired personal accounts file
|
||||
|
||||
@@ -1,14 +1 @@
|
||||
tuxflotte_state: present
|
||||
# Formular der Personalisierung in der Kundenplattform (ADR-0041); kiosk=1
|
||||
# blendet dort "Abmelden" ein. Der Kiosk lässt nur Navigation innerhalb
|
||||
# dieser Herkunft zu.
|
||||
tuxflotte_personalisierung_url: "https://flotten-management.tuxflotte.de/personalisierung?kiosk=1"
|
||||
# "Passwort vergessen?" (Eintrag nach der Personalisierung); Gerät und
|
||||
# Kiosk-Token hängt die Rolle aus den Check-in-Optionen an.
|
||||
tuxflotte_personalisierung_vergessen_url: "https://flotten-management.tuxflotte.de/personalisierung/vergessen?kiosk=1"
|
||||
# Ohne Eingabe endet die Kiosk-Sitzung nach dieser Zeit.
|
||||
tuxflotte_personalisierung_leerlauf_minuten: 15
|
||||
# Rückfall für "Passwort vergessen?" nach Fehleingabe: Anmeldebildschirm
|
||||
# neu starten, weil slick-greeter neu eingeblendete Einträge nicht live
|
||||
# übernimmt (auf Liebherr-84 beobachtet, 18.10.2026).
|
||||
tuxflotte_personalisierung_greeter_neustart: true
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
#
|
||||
# Gibt die Anmeldung am Konto "Personalisierung" frei oder nimmt sie weg
|
||||
# (ADR-0041, Ergänzungen 18.10.2026). Frei nur, wenn beides gilt:
|
||||
# - der Server bietet sie an (Merkerdatei, von der Rolle gesetzt; Inhalt
|
||||
# ist der Modus: "personalisieren" ohne persönliches Konto oder bei
|
||||
# laufendem Code, "passwort" mit Konto - dann heißt der Eintrag
|
||||
# "Passwort vergessen?" und der Kiosk öffnet die Seite dafür; in diesem
|
||||
# Modus nur, solange nach einer Fehleingabe die Frist in AUFGEDECKT
|
||||
# läuft, gesetzt von personalisierung-wacht), und
|
||||
# - das laufende LightDM nutzt schon den Kiosk-Wrapper. LightDM liest
|
||||
# seine Konfiguration nur beim Start; vorher bekäme das passwortlose
|
||||
# Konto den vollen Desktop.
|
||||
# Sonst: keine passwortlose PAM-Freigabe, Shell nologin und in
|
||||
# AccountsService SystemAccount=true. Nur die Shell reichte nicht: auf
|
||||
# Liebherr-84 (Mint 22.3) stand das Konto danach weiter im Greeter, weil
|
||||
# AccountsService es als bekanntes Konto im Cache hält. SystemAccount ist
|
||||
# derselbe Weg, mit dem persoenliche-konten gesperrte Konten ausblendet.
|
||||
#
|
||||
# Aufruf: "start" aus ExecStartPre von lightdm.service (LightDM startet
|
||||
# gleich mit der aktuellen Konfiguration), ohne Argument von der Rolle.
|
||||
# Ausgabe: "frei" oder "gesperrt", bei einer Änderung mit " geaendert".
|
||||
set -eu
|
||||
|
||||
KONF=/etc/lightdm/lightdm.conf.d/85-tuxflotte-personalisierung.conf
|
||||
MERKER=/etc/tuxflotte/personalisierung-anbieten
|
||||
PAM=/etc/pam.d/lightdm
|
||||
ACCOUNTS=/var/lib/AccountsService/users/personalisierung
|
||||
AUFGEDECKT=/run/tuxflotte/personalisierung-aufgedeckt-bis
|
||||
ZEILE='auth sufficient pam_succeed_if.so user = personalisierung'
|
||||
MUSTER='^auth[[:space:]]+sufficient[[:space:]]+pam_succeed_if\.so[[:space:]]+user[[:space:]]+=[[:space:]]+personalisierung[[:space:]]*$'
|
||||
|
||||
wrapper_aktiv() {
|
||||
[ -f "$KONF" ] || return 1
|
||||
[ "${1:-}" = start ] && return 0
|
||||
pid=$(systemctl show -p MainPID --value lightdm.service 2>/dev/null || true)
|
||||
[ "${pid:-0}" -gt 0 ] 2>/dev/null || return 1
|
||||
laufzeit=$(ps -o etimes= -p "$pid" | tr -d ' ')
|
||||
[ -n "$laufzeit" ] || return 1
|
||||
[ $(( $(date +%s) - laufzeit )) -gt "$(stat -c %Y "$KONF")" ]
|
||||
}
|
||||
|
||||
modus=$(head -n 1 "$MERKER" 2>/dev/null || true)
|
||||
soll=gesperrt
|
||||
if [ -f "$MERKER" ] && wrapper_aktiv "${1:-}"; then
|
||||
if [ "$modus" = passwort ]; then
|
||||
bis=$(head -n 1 "$AUFGEDECKT" 2>/dev/null || echo 0)
|
||||
if [ "${bis:-0}" -gt "$(date +%s)" ] 2>/dev/null; then
|
||||
soll=frei
|
||||
fi
|
||||
else
|
||||
soll=frei
|
||||
fi
|
||||
fi
|
||||
if [ "$modus" = passwort ]; then
|
||||
anzeigename='Passwort vergessen?'
|
||||
else
|
||||
anzeigename='Personalisierung'
|
||||
fi
|
||||
|
||||
geaendert=""
|
||||
if [ "$soll" = frei ]; then
|
||||
if [ -f "$PAM" ] && ! grep -Eq "$MUSTER" "$PAM"; then
|
||||
sed -i "1i $ZEILE" "$PAM"
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
shell=/bin/bash
|
||||
else
|
||||
if [ -f "$PAM" ] && grep -Eq "$MUSTER" "$PAM"; then
|
||||
sed -i -E "/$MUSTER/d" "$PAM"
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
shell=/usr/sbin/nologin
|
||||
fi
|
||||
|
||||
if getent passwd personalisierung >/dev/null; then
|
||||
if [ "$(getent passwd personalisierung | cut -d: -f7)" != "$shell" ]; then
|
||||
usermod -s "$shell" personalisierung
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
# Anzeigename im Greeter (GECOS); AccountsService liest ihn neu ein.
|
||||
if [ "$(getent passwd personalisierung | cut -d: -f5)" != "$anzeigename" ]; then
|
||||
usermod -c "$anzeigename" personalisierung
|
||||
geaendert=" geaendert"
|
||||
accounts_geaendert=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# AccountsService: SystemAccount=true blendet aus, ohne den Schlüssel zeigt
|
||||
# der Greeter das Konto. Andere Einträge der Datei bleiben stehen.
|
||||
if getent passwd personalisierung >/dev/null; then
|
||||
if [ "$soll" = frei ]; then
|
||||
if [ -f "$ACCOUNTS" ] && grep -q '^SystemAccount=' "$ACCOUNTS"; then
|
||||
sed -i '/^SystemAccount=/d' "$ACCOUNTS"
|
||||
accounts_geaendert=1
|
||||
fi
|
||||
elif ! { [ -f "$ACCOUNTS" ] && grep -qx 'SystemAccount=true' "$ACCOUNTS"; }; then
|
||||
mkdir -p "$(dirname "$ACCOUNTS")"
|
||||
if [ -f "$ACCOUNTS" ] && grep -q '^\[User\]' "$ACCOUNTS"; then
|
||||
sed -i '/^SystemAccount=/d' "$ACCOUNTS"
|
||||
sed -i '/^\[User\]/a SystemAccount=true' "$ACCOUNTS"
|
||||
else
|
||||
printf '[User]\nSystemAccount=true\n' >> "$ACCOUNTS"
|
||||
fi
|
||||
accounts_geaendert=1
|
||||
fi
|
||||
if [ -n "${accounts_geaendert:-}" ]; then
|
||||
geaendert=" geaendert"
|
||||
# Beim Start von LightDM (ExecStartPre) läuft accounts-daemon meist
|
||||
# noch nicht und liest die Datei selbst; try-restart tut dann nichts.
|
||||
systemctl try-restart accounts-daemon.service 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$soll$geaendert"
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
#
|
||||
# "Passwort vergessen?" erst nach einer Fehleingabe (ADR-0041, Ergänzung
|
||||
# 18.10.2026): Der Eintrag ist im Modus "passwort" ausgeblendet. Dieser
|
||||
# Dienst liest das Journal von LightDM mit. Schlägt die Anmeldung eines
|
||||
# persönlichen Kontos fehl, blendet er den Eintrag für SICHTBAR_SEKUNDEN ein;
|
||||
# nach einer erfolgreichen Anmeldung eines persönlichen Kontos oder nach
|
||||
# Ablauf der Zeit wieder aus. Geschaltet wird über personalisierung-anmeldung
|
||||
# (liest AUFGEDECKT_PATH); an der PAM-Konfiguration ändert sich nichts.
|
||||
#
|
||||
# Rückfall (--greeter-neustart): Übernimmt slick-greeter einen neu
|
||||
# eingeblendeten Eintrag nicht in die laufende Liste, startet der Dienst
|
||||
# LightDM neu - nur, wenn keine Benutzersitzung läuft.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
AUFGEDECKT_PATH = "/run/tuxflotte/personalisierung-aufgedeckt-bis"
|
||||
SCHALTER = "/usr/local/lib/tuxflotte/personalisierung-anmeldung"
|
||||
MERKER = "/etc/tuxflotte/personalisierung-anbieten"
|
||||
KONTEN_DIR = "/var/lib/tuxflotte/persoenliche-konten"
|
||||
SICHTBAR_SEKUNDEN = 15 * 60
|
||||
PRUEF_ABSTAND_SEKUNDEN = 30
|
||||
|
||||
FEHLER_MUSTER = re.compile(r"pam_unix\(lightdm:auth\): authentication failure;.*\buser=([a-z][a-z0-9._-]*)")
|
||||
ERFOLG_MUSTER = re.compile(r"pam_unix\(lightdm:session\): session opened for user ([a-z][a-z0-9._-]*)")
|
||||
|
||||
|
||||
def log(meldung):
|
||||
print(meldung, flush=True)
|
||||
|
||||
|
||||
def persoenliche_konten():
|
||||
"""Anmeldenamen aus den Markern der Rolle persoenliche-konten."""
|
||||
|
||||
try:
|
||||
return {n for n in os.listdir(KONTEN_DIR) if "." not in n}
|
||||
except OSError:
|
||||
return set()
|
||||
|
||||
|
||||
def modus():
|
||||
try:
|
||||
with open(MERKER, encoding="utf-8") as f:
|
||||
return f.readline().strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def aufgedeckt_bis():
|
||||
try:
|
||||
with open(AUFGEDECKT_PATH, encoding="utf-8") as f:
|
||||
return int(f.read().strip() or 0)
|
||||
except (OSError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def entscheiden(zeile, konten, jetzt, bis):
|
||||
"""
|
||||
Reine Entscheidung je Journalzeile: ("aufdecken", name), ("verbergen",
|
||||
name) oder None. Getrennt von den Seiteneffekten, damit sie sich ohne
|
||||
Gerät prüfen lässt.
|
||||
"""
|
||||
|
||||
treffer = FEHLER_MUSTER.search(zeile)
|
||||
if treffer and treffer.group(1) in konten:
|
||||
return ("aufdecken", treffer.group(1))
|
||||
treffer = ERFOLG_MUSTER.search(zeile)
|
||||
if treffer and treffer.group(1) in konten and bis > jetzt:
|
||||
return ("verbergen", treffer.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def schalten():
|
||||
ergebnis = subprocess.run([SCHALTER], capture_output=True, text=True)
|
||||
return (ergebnis.stdout or "").strip()
|
||||
|
||||
|
||||
def benutzersitzung_laeuft():
|
||||
ergebnis = subprocess.run(["loginctl", "list-sessions", "--no-legend"], capture_output=True, text=True)
|
||||
for zeile in (ergebnis.stdout or "").splitlines():
|
||||
sitzung = zeile.split()[0] if zeile.split() else ""
|
||||
if not sitzung:
|
||||
continue
|
||||
klasse = subprocess.run(
|
||||
["loginctl", "show-session", sitzung, "--property=Class", "--value"],
|
||||
capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
if klasse == "user":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def aufdecken(name, greeter_neustart):
|
||||
os.makedirs(os.path.dirname(AUFGEDECKT_PATH), exist_ok=True)
|
||||
with open(AUFGEDECKT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(f"{int(time.time()) + SICHTBAR_SEKUNDEN}\n")
|
||||
log(f"Fehleingabe für {name}: Passwort vergessen? eingeblendet ({schalten()})")
|
||||
if greeter_neustart and not benutzersitzung_laeuft():
|
||||
# Kurz warten, damit die Fehlermeldung am Bildschirm zu sehen war.
|
||||
time.sleep(2)
|
||||
subprocess.run(["systemctl", "restart", "lightdm.service"])
|
||||
log("Anmeldebildschirm neu gestartet")
|
||||
|
||||
|
||||
def verbergen(grund):
|
||||
try:
|
||||
os.remove(AUFGEDECKT_PATH)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
log(f"{grund}: Passwort vergessen? ausgeblendet ({schalten()})")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--greeter-neustart", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Ohne Filter auf die Kennung: unter welcher SYSLOG_IDENTIFIER die
|
||||
# PAM-Meldungen von LightDM landen, ist nicht auf jeder Distribution
|
||||
# gleich; die Muster prüfen "lightdm:auth"/"lightdm:session" selbst.
|
||||
journal = subprocess.Popen(
|
||||
["journalctl", "--follow", "--lines=0", "--output=cat"],
|
||||
stdout=subprocess.PIPE, text=True, bufsize=1,
|
||||
)
|
||||
while True:
|
||||
bereit, _, _ = select.select([journal.stdout], [], [], PRUEF_ABSTAND_SEKUNDEN)
|
||||
jetzt = int(time.time())
|
||||
if bereit:
|
||||
zeile = journal.stdout.readline()
|
||||
if not zeile:
|
||||
log("journalctl beendet")
|
||||
sys.exit(1)
|
||||
if modus() != "passwort":
|
||||
continue
|
||||
aktion = entscheiden(zeile, persoenliche_konten(), jetzt, aufgedeckt_bis())
|
||||
if aktion and aktion[0] == "aufdecken":
|
||||
aufdecken(aktion[1], args.greeter_neustart)
|
||||
elif aktion and aktion[0] == "verbergen":
|
||||
verbergen(f"Anmeldung von {aktion[1]}")
|
||||
bis = aufgedeckt_bis()
|
||||
if bis and bis <= jetzt:
|
||||
verbergen("Zeit abgelaufen")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,184 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
#
|
||||
# Kiosk für das Konto "Personalisierung" (ADR-0041, Ergänzung 14.10.2026):
|
||||
# zeigt das Personalisierungsformular im Vollbild, ohne Fenstermanager. Es
|
||||
# gibt keine Adresszeile, keine neuen Fenster, keine Dateidialoge, keine
|
||||
# Downloads und kein Kontextmenü. Navigiert wird nur innerhalb der Herkunft der
|
||||
# Startadresse (Schema, Host, Port). Daten bleiben nur im Speicher.
|
||||
#
|
||||
# Beenden: die Seite verlinkt "tuxflotte:abmelden", oder nach der
|
||||
# Leerlaufzeit ohne Eingabe. Mit dem Programm endet die Sitzung, LightDM
|
||||
# kehrt zum Anmeldebildschirm zurück.
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import time
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
from gi.repository import Gdk, GLib, Gtk, WebKit2 # noqa: E402
|
||||
|
||||
ABMELDEN = "tuxflotte:abmelden"
|
||||
|
||||
|
||||
def herkunft(uri):
|
||||
teile = urlsplit(uri)
|
||||
port = teile.port or {"https": 443, "http": 80}.get(teile.scheme)
|
||||
return (teile.scheme, teile.hostname, port)
|
||||
|
||||
|
||||
class Kiosk:
|
||||
def __init__(self, url, leerlauf_minuten):
|
||||
self.start_url = url
|
||||
self.erlaubt = herkunft(url)
|
||||
self.leerlauf = leerlauf_minuten * 60
|
||||
self.letzte_eingabe = time.monotonic()
|
||||
|
||||
kontext = WebKit2.WebContext.new_ephemeral()
|
||||
kontext.set_preferred_languages(["de-DE", "de"])
|
||||
kontext.connect("download-started", self.on_download)
|
||||
|
||||
self.view = WebKit2.WebView.new_with_context(kontext)
|
||||
settings = self.view.get_settings()
|
||||
settings.set_enable_developer_extras(False)
|
||||
settings.set_javascript_can_open_windows_automatically(False)
|
||||
settings.set_allow_file_access_from_file_urls(False)
|
||||
settings.set_allow_universal_access_from_file_urls(False)
|
||||
settings.set_enable_write_console_messages_to_stdout(False)
|
||||
|
||||
self.view.connect("decide-policy", self.on_policy)
|
||||
self.view.connect("create", lambda *_: None)
|
||||
self.view.connect("context-menu", lambda *_: True)
|
||||
self.view.connect("run-file-chooser", lambda *_: True)
|
||||
self.view.connect("print", lambda *_: True)
|
||||
self.view.connect("permission-request", self.on_permission)
|
||||
self.view.connect("authenticate", self.on_authenticate)
|
||||
self.view.connect("load-failed", self.on_load_failed)
|
||||
self.view.connect("load-failed-with-tls-errors", self.on_tls_error)
|
||||
|
||||
self.fenster = Gtk.Window()
|
||||
self.fenster.set_decorated(False)
|
||||
self.fenster.add(self.view)
|
||||
self.fenster.connect("destroy", lambda *_: Gtk.main_quit())
|
||||
for signal in ("key-press-event", "button-press-event", "motion-notify-event", "scroll-event"):
|
||||
self.fenster.connect(signal, self.on_eingabe)
|
||||
self.view.connect(signal, self.on_eingabe)
|
||||
|
||||
# Ohne Fenstermanager wirkt fullscreen() nicht zuverlässig: Größe
|
||||
# und Lage selbst auf den Hauptbildschirm setzen.
|
||||
anzeige = Gdk.Display.get_default()
|
||||
monitor = anzeige.get_primary_monitor() or anzeige.get_monitor(0)
|
||||
geo = monitor.get_geometry()
|
||||
self.fenster.move(geo.x, geo.y)
|
||||
self.fenster.set_default_size(geo.width, geo.height)
|
||||
self.fenster.fullscreen()
|
||||
|
||||
GLib.timeout_add_seconds(30, self.pruefe_leerlauf)
|
||||
self.view.load_uri(self.start_url)
|
||||
self.fenster.show_all()
|
||||
self.fenster.present()
|
||||
self.view.grab_focus()
|
||||
|
||||
def beenden(self):
|
||||
Gtk.main_quit()
|
||||
|
||||
def on_eingabe(self, *_):
|
||||
self.letzte_eingabe = time.monotonic()
|
||||
return False
|
||||
|
||||
def pruefe_leerlauf(self):
|
||||
if time.monotonic() - self.letzte_eingabe > self.leerlauf:
|
||||
self.beenden()
|
||||
return False
|
||||
return True
|
||||
|
||||
def on_policy(self, _view, entscheidung, art):
|
||||
if art == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
|
||||
entscheidung.ignore()
|
||||
return True
|
||||
if art == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
|
||||
uri = entscheidung.get_navigation_action().get_request().get_uri()
|
||||
if uri == ABMELDEN:
|
||||
entscheidung.ignore()
|
||||
GLib.idle_add(self.beenden)
|
||||
return True
|
||||
if herkunft(uri) != self.erlaubt:
|
||||
entscheidung.ignore()
|
||||
return True
|
||||
return False
|
||||
if art == WebKit2.PolicyDecisionType.RESPONSE:
|
||||
if not entscheidung.is_mime_type_supported():
|
||||
entscheidung.ignore()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_download(self, _kontext, download):
|
||||
download.cancel()
|
||||
|
||||
def on_permission(self, _view, anfrage):
|
||||
anfrage.deny()
|
||||
return True
|
||||
|
||||
def on_authenticate(self, _view, anfrage):
|
||||
anfrage.cancel()
|
||||
return True
|
||||
|
||||
def fehlerseite(self, text):
|
||||
start = html.escape(self.start_url, quote=True)
|
||||
self.view.load_alternate_html(
|
||||
f"""<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<style>
|
||||
body {{ font-family: sans-serif; background: #f4f4f4; color: #222;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
height: 100vh; margin: 0; }}
|
||||
main {{ max-width: 34em; text-align: center; }}
|
||||
a {{ display: inline-block; margin: 1em .5em; padding: .7em 1.4em;
|
||||
background: #2a6db0; color: #fff; text-decoration: none; border-radius: 4px; }}
|
||||
</style></head><body><main>
|
||||
<h1>Keine Verbindung</h1>
|
||||
<p>{html.escape(text)}</p>
|
||||
<a href="{start}">Erneut versuchen</a>
|
||||
<a href="{ABMELDEN}">Abmelden</a>
|
||||
</main></body></html>""",
|
||||
self.start_url,
|
||||
None,
|
||||
)
|
||||
|
||||
def on_load_failed(self, _view, _ereignis, _uri, fehler):
|
||||
# Von uns selbst abgelehnte oder abgebrochene Ladevorgänge sind keine
|
||||
# Störung.
|
||||
if fehler.domain == "WebKitPolicyError":
|
||||
return True
|
||||
if fehler.domain == "WebKitNetworkError" and fehler.code == WebKit2.NetworkError.CANCELLED:
|
||||
return True
|
||||
self.fehlerseite(
|
||||
"Die Personalisierung ist gerade nicht erreichbar. Bitte prüfe die "
|
||||
"Netzwerkverbindung und versuche es noch einmal."
|
||||
)
|
||||
return True
|
||||
|
||||
def on_tls_error(self, *_):
|
||||
self.fehlerseite(
|
||||
"Die Verbindung zu Tuxflotte ist nicht vertrauenswürdig. Bitte wende "
|
||||
"dich an die Verwaltung deiner Einrichtung."
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Tuxflotte-Kiosk")
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--leerlauf-minuten", type=int, default=15)
|
||||
args = parser.parse_args()
|
||||
|
||||
Kiosk(args.url, args.leerlauf_minuten)
|
||||
Gtk.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,319 +1,38 @@
|
||||
# Merkmal "Personalisierung möglich" (ADR-0041, Ergänzung 14.10.2026):
|
||||
# richtet das passwortlose Konto "Personalisierung" am Anmeldebildschirm ein,
|
||||
# das nur einen Kiosk mit dem Personalisierungsformular startet. Die
|
||||
# Rolle personalisiert niemanden; das persönliche Konto legt später eine
|
||||
# gerätespezifische Rolle an, nachdem ein Aktivierungscode eingelöst ist.
|
||||
# Abwählen entfernt nur das Konto "Personalisierung" und den Kiosk, nicht
|
||||
# bereits angelegte persönliche Konten.
|
||||
#
|
||||
# Nur LightDM (Mint, Debian/Xfce). GDM (Ubuntu) kennt keinen
|
||||
# session-wrapper, dort folgt ein eigener Weg; bis dahin wird übersprungen.
|
||||
|
||||
- name: Ensure marker directory exists
|
||||
ansible.builtin.file:
|
||||
path: /run/tuxflotte/agent/applied
|
||||
state: directory
|
||||
mode: "0755"
|
||||
# Merkmal "Personalisierung möglich" (ADR-0041). Seit dem vereinfachten
|
||||
# Ablauf (Ergänzung 24.10.2026) richtet das Merkmal am Gerät nichts mehr ein:
|
||||
# Es legt nur fest, ob der Kunden-Admin beim Gerät eine Adresse eintragen
|
||||
# darf, alles Weitere läuft über Mail-Link und Webseite. Die Rolle baut
|
||||
# deshalb bei present wie bei absent ab, was frühere Stände eingerichtet
|
||||
# haben: das passwortlose Konto "personalisierung" mit Kiosk am
|
||||
# Anmeldebildschirm, die PAM-Freigabe, den Wächter für "Passwort vergessen?"
|
||||
# und die LightDM-Konfiguration. Persönliche Konten bleiben unberührt, sie
|
||||
# gehören zur Rolle persoenliche-konten.
|
||||
|
||||
- name: Check for LightDM PAM configuration
|
||||
ansible.builtin.stat:
|
||||
path: /etc/pam.d/lightdm
|
||||
register: tuxflotte_pers_lightdm
|
||||
|
||||
# Den Wrapper der Distribution ermitteln, in LightDMs Lesereihenfolge (die
|
||||
# letzte Angabe gilt), ohne unsere eigene Datei. Debian setzt
|
||||
# /etc/X11/Xsession, Mint/Ubuntu verlassen sich auf den eingebauten Standard
|
||||
# lightdm-session.
|
||||
- name: Determine the distribution's LightDM session wrapper
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
wrapper=$(
|
||||
for f in /usr/share/lightdm/lightdm.conf.d/*.conf \
|
||||
/usr/local/share/lightdm/lightdm.conf.d/*.conf \
|
||||
/etc/xdg/lightdm/lightdm.conf.d/*.conf \
|
||||
/etc/lightdm/lightdm.conf.d/*.conf \
|
||||
/etc/lightdm/lightdm.conf; do
|
||||
[ -f "$f" ] || continue
|
||||
[ "$f" = /etc/lightdm/lightdm.conf.d/85-tuxflotte-personalisierung.conf ] && continue
|
||||
sed -n 's/^[[:space:]]*session-wrapper[[:space:]]*=[[:space:]]*//p' "$f"
|
||||
done | tail -n 1
|
||||
)
|
||||
wrapper=${wrapper:-lightdm-session}
|
||||
case "$wrapper" in
|
||||
/*) ;;
|
||||
*) wrapper=$(PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin command -v "$wrapper" || true) ;;
|
||||
esac
|
||||
if [ -n "$wrapper" ] && [ -x "$wrapper" ] && [ "$wrapper" != /usr/local/lib/tuxflotte/lightdm-sitzung ]; then
|
||||
echo "$wrapper"
|
||||
fi
|
||||
register: tuxflotte_pers_wrapper
|
||||
changed_when: false
|
||||
when: tuxflotte_state == "present" and tuxflotte_pers_lightdm.stat.exists
|
||||
|
||||
- name: Decide whether the kiosk login can be set up here
|
||||
ansible.builtin.set_fact:
|
||||
tuxflotte_pers_aktiv: >-
|
||||
{{ tuxflotte_state == "present" and tuxflotte_pers_lightdm.stat.exists
|
||||
and (tuxflotte_pers_wrapper.stdout | default('')) != '' }}
|
||||
tuxflotte_personalisierung_wrapper_original: "{{ tuxflotte_pers_wrapper.stdout | default('') }}"
|
||||
|
||||
- name: Report skipped setup (no LightDM or no usable session wrapper)
|
||||
ansible.builtin.debug:
|
||||
msg: "Personalisierung: kein LightDM oder kein Sitzungs-Wrapper gefunden, Einrichtung übersprungen"
|
||||
when: tuxflotte_state == "present" and not (tuxflotte_pers_aktiv | bool)
|
||||
|
||||
# --- present -----------------------------------------------------------------
|
||||
|
||||
- name: Install kiosk dependencies
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- python3-gi
|
||||
- gir1.2-gtk-3.0
|
||||
- gir1.2-webkit2-4.1
|
||||
- x11-xserver-utils
|
||||
state: present
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# Kein Passwort (gesperrt), Anmeldung nur über die PAM-Freigabe von
|
||||
# personalisierung-anmeldung und nur am Anmeldebildschirm. Shell und
|
||||
# Kommentar (= Anzeigename im Greeter, "Personalisierung" oder "Passwort
|
||||
# vergessen?") setzt ebenfalls das Skript; der Kommentar kennzeichnet das
|
||||
# Konto zugleich als unseres (siehe Abwählen).
|
||||
- name: Ensure personalisierung account exists
|
||||
ansible.builtin.user:
|
||||
name: personalisierung
|
||||
create_home: true
|
||||
password_lock: true
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Ensure helper script directory exists
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/lib/tuxflotte
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Install kiosk program
|
||||
ansible.builtin.copy:
|
||||
src: tuxflotte-kiosk.py
|
||||
dest: /usr/local/lib/tuxflotte/tuxflotte-kiosk.py
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Install kiosk session
|
||||
ansible.builtin.template:
|
||||
src: personalisierung-sitzung.j2
|
||||
dest: /usr/local/lib/tuxflotte/personalisierung-sitzung
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Install LightDM session wrapper
|
||||
ansible.builtin.template:
|
||||
src: lightdm-sitzung.j2
|
||||
dest: /usr/local/lib/tuxflotte/lightdm-sitzung
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Ensure LightDM drop-in directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/lightdm/lightdm.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# Wirkt ab dem nächsten Start von LightDM (Neustart des Geräts); LightDM wird
|
||||
# nicht im laufenden Betrieb neu gestartet, das beendete eine Sitzung.
|
||||
- name: Use the Tuxflotte session wrapper in LightDM
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/lightdm/lightdm.conf.d/85-tuxflotte-personalisierung.conf
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
[Seat:*]
|
||||
session-wrapper=/usr/local/lib/tuxflotte/lightdm-sitzung
|
||||
mode: "0644"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# Passwortlose Anmeldung: eigene PAM-Freigabe nur für dieses Konto, nicht über
|
||||
# die Gruppe nopasswdlogin (guest-session entfernt deren Regel beim Abwählen
|
||||
# vollständig). Gesetzt und entfernt von personalisierung-anmeldung, nicht
|
||||
# hier: frei nur, solange die Verwaltung das Konto anbietet (Check-in-Option
|
||||
# anmeldung_anbieten, ADR-0041 Ergänzung 18.10.2026) und LightDM schon mit
|
||||
# dem Kiosk-Wrapper läuft. Beim ersten Einrichten also erst nach dem
|
||||
# nächsten Start von LightDM, vorher bekäme das Konto den vollen Desktop.
|
||||
- name: Ensure Tuxflotte config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/tuxflotte
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# Modus vom Server (ADR-0041, Ergänzungen 18.10.2026): personalisieren,
|
||||
# passwort ("Passwort vergessen?") oder aus. Ältere Server schicken nur
|
||||
# anmeldung_anbieten.
|
||||
- name: Determine the mode of the personalisierung entry
|
||||
ansible.builtin.set_fact:
|
||||
tuxflotte_pers_modus: >-
|
||||
{{ tuxflotte_optionen.modus
|
||||
| default('personalisieren' if (tuxflotte_optionen.anmeldung_anbieten | default(true) | bool) else 'aus') }}
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Record the mode (first line, read by personalisierung-anmeldung)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/tuxflotte/personalisierung-anbieten
|
||||
content: "{{ tuxflotte_pers_modus }}\n# Verwaltet von Tuxflotte (Rolle personalisierung)\n"
|
||||
mode: "0644"
|
||||
when: (tuxflotte_pers_aktiv | bool) and tuxflotte_pers_modus in ['personalisieren', 'passwort']
|
||||
|
||||
- name: Record that the account is not offered
|
||||
ansible.builtin.file:
|
||||
path: /etc/tuxflotte/personalisierung-anbieten
|
||||
state: absent
|
||||
when: (tuxflotte_pers_aktiv | bool) and tuxflotte_pers_modus not in ['personalisieren', 'passwort']
|
||||
|
||||
# Lesbar für das Kiosk-Konto. Das Token erlaubt nur, dem Besitzer einen Link
|
||||
# an seine hinterlegte Adresse zu schicken (vom Server begrenzt).
|
||||
- name: Record the "Passwort vergessen?" address
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/tuxflotte/personalisierung-vergessen-url
|
||||
content: "{{ tuxflotte_personalisierung_vergessen_url }}&geraet={{ tuxflotte_optionen.geraet | urlencode }}&t={{ tuxflotte_optionen.kiosk_token | urlencode }}\n"
|
||||
mode: "0644"
|
||||
when: (tuxflotte_pers_aktiv | bool) and (tuxflotte_optionen.kiosk_token | default('')) != ''
|
||||
|
||||
- name: Remove the "Passwort vergessen?" address (no token)
|
||||
ansible.builtin.file:
|
||||
path: /etc/tuxflotte/personalisierung-vergessen-url
|
||||
state: absent
|
||||
when: (tuxflotte_pers_aktiv | bool) and (tuxflotte_optionen.kiosk_token | default('')) == ''
|
||||
|
||||
- name: Install login switch
|
||||
ansible.builtin.copy:
|
||||
src: personalisierung-anmeldung
|
||||
dest: /usr/local/lib/tuxflotte/personalisierung-anmeldung
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# "Passwort vergessen?" erst nach einer Fehleingabe (ADR-0041, Ergänzung
|
||||
# 18.10.2026): liest das LightDM-Journal mit und blendet den Eintrag für
|
||||
# 15 Minuten ein. Mit tuxflotte_personalisierung_greeter_neustart startet er
|
||||
# danach den Anmeldebildschirm neu (Rückfall, falls slick-greeter neue
|
||||
# Einträge nicht live übernimmt).
|
||||
- name: Install watcher for failed logins
|
||||
ansible.builtin.copy:
|
||||
src: personalisierung-wacht.py
|
||||
dest: /usr/local/lib/tuxflotte/personalisierung-wacht.py
|
||||
mode: "0755"
|
||||
register: tuxflotte_pers_wacht_skript
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Install watcher service
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/tuxflotte-personalisierung-wacht.service
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
[Unit]
|
||||
Description=Tuxflotte: "Passwort vergessen?" nach Fehleingabe einblenden
|
||||
After=systemd-journald.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /usr/local/lib/tuxflotte/personalisierung-wacht.py{{ ' --greeter-neustart' if tuxflotte_personalisierung_greeter_neustart | bool else '' }}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
mode: "0644"
|
||||
register: tuxflotte_pers_wacht_unit
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Run watcher service
|
||||
ansible.builtin.systemd:
|
||||
name: tuxflotte-personalisierung-wacht.service
|
||||
enabled: true
|
||||
state: "{{ 'restarted' if (tuxflotte_pers_wacht_skript is changed or tuxflotte_pers_wacht_unit is changed) else 'started' }}"
|
||||
daemon_reload: "{{ tuxflotte_pers_wacht_unit is changed }}"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Ensure lightdm.service drop-in directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/lightdm.service.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# Beim Start von LightDM (Neustart des Geräts) gilt die neue Konfiguration
|
||||
# sofort; "-": ein fehlendes Skript hält LightDM nicht auf.
|
||||
- name: Apply the login switch when LightDM starts
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/lightdm.service.d/85-tuxflotte-personalisierung.conf
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
[Service]
|
||||
ExecStartPre=-/usr/local/lib/tuxflotte/personalisierung-anmeldung start
|
||||
mode: "0644"
|
||||
register: tuxflotte_pers_dropin
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Reload systemd for the lightdm.service drop-in
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: (tuxflotte_pers_aktiv | bool) and tuxflotte_pers_dropin is changed
|
||||
|
||||
- name: Apply the login switch now
|
||||
ansible.builtin.command: /usr/local/lib/tuxflotte/personalisierung-anmeldung
|
||||
register: tuxflotte_pers_anmeldung
|
||||
changed_when: "'geaendert' in tuxflotte_pers_anmeldung.stdout"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# Kein Prozess des Kiosk-Kontos überlebt das Abmelden. Eigene Datei, nicht
|
||||
# die der Gastsitzung (die verschwindet beim Abwählen von guest-session);
|
||||
# logind führt die KillOnlyUsers-Listen beider Dateien zusammen.
|
||||
- name: Ensure logind drop-in directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/logind.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Kill leftover personalisierung processes on logout (logind)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/logind.conf.d/50-tuxflotte-personalisierung.conf
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
[Login]
|
||||
KillUserProcesses=yes
|
||||
KillOnlyUsers=personalisierung
|
||||
mode: "0644"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
- name: Record that the personalisierung role ran
|
||||
ansible.builtin.copy:
|
||||
dest: /run/tuxflotte/agent/applied/personalisierung.marker
|
||||
content: "{{ ansible_date_time.iso8601 }}\n"
|
||||
mode: "0644"
|
||||
when: tuxflotte_pers_aktiv | bool
|
||||
|
||||
# --- absent (Auftrag abgewählt) ------------------------------------------------
|
||||
|
||||
- name: Remove passwordless login for personalisierung (Auftrag abgewählt)
|
||||
- name: Remove passwordless login for personalisierung
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/lightdm
|
||||
regexp: '^auth\s+sufficient\s+pam_succeed_if\.so\s+user\s+=\s+personalisierung\s*$'
|
||||
state: absent
|
||||
when: tuxflotte_state == "absent" and tuxflotte_pers_lightdm.stat.exists
|
||||
when: tuxflotte_pers_lightdm.stat.exists
|
||||
|
||||
# lightdm-sitzung bleibt bewusst liegen: ein laufendes LightDM liest seine
|
||||
# Konfiguration erst beim nächsten Start neu und ruft bis dahin weiter
|
||||
# diesen Wrapper auf. Ohne die Datei wäre bis zum Neustart keine Anmeldung
|
||||
# möglich. Ohne Konto "personalisierung" reicht er nur noch durch.
|
||||
- name: Stop watcher service (Auftrag abgewählt)
|
||||
- name: Stop watcher service
|
||||
ansible.builtin.systemd:
|
||||
name: tuxflotte-personalisierung-wacht.service
|
||||
enabled: false
|
||||
state: stopped
|
||||
failed_when: false
|
||||
when: tuxflotte_state == "absent"
|
||||
|
||||
- name: Remove kiosk login files (Auftrag abgewählt)
|
||||
# lightdm-sitzung fehlt hier bewusst: Ein laufendes LightDM liest seine
|
||||
# Konfiguration erst beim nächsten Start neu und ruft bis dahin weiter diesen
|
||||
# Wrapper auf. Ohne die Datei wäre bis zum Neustart keine Anmeldung möglich.
|
||||
# Ohne Konto "personalisierung" reicht er nur noch durch; entfernt wird er
|
||||
# unten, sobald LightDM neu gestartet ist.
|
||||
- name: Remove kiosk login files
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
@@ -329,48 +48,98 @@
|
||||
- /etc/systemd/system/tuxflotte-personalisierung-wacht.service
|
||||
- /usr/local/lib/tuxflotte/personalisierung-wacht.py
|
||||
- /run/tuxflotte/personalisierung-aufgedeckt-bis
|
||||
- /run/tuxflotte/agent/applied/personalisierung.marker
|
||||
register: tuxflotte_pers_entfernt
|
||||
when: tuxflotte_state == "absent"
|
||||
|
||||
- name: Reload systemd after removing the drop-in (Auftrag abgewählt)
|
||||
- name: Reload systemd after removing units and drop-ins
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: tuxflotte_state == "absent" and tuxflotte_pers_entfernt is changed
|
||||
when: tuxflotte_pers_entfernt is changed
|
||||
|
||||
- name: Read the personalisierung account (Auftrag abgewählt)
|
||||
- name: Read the personalisierung account
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: personalisierung
|
||||
register: tuxflotte_pers_konto
|
||||
failed_when: false
|
||||
when: tuxflotte_state == "absent"
|
||||
|
||||
# Nur das Konto entfernen, das diese Rolle angelegt hat (Kommentarfeld,
|
||||
# je nach Modus einer der beiden Anzeigenamen).
|
||||
- name: Decide whether the account is ours (Auftrag abgewählt)
|
||||
# Nur das Konto entfernen, das diese Rolle angelegt hat (Kommentarfeld, je
|
||||
# nach früherem Modus einer der beiden Anzeigenamen).
|
||||
- name: Decide whether the account is ours
|
||||
ansible.builtin.set_fact:
|
||||
tuxflotte_pers_konto_unser: >-
|
||||
{{ (tuxflotte_pers_konto.ansible_facts.getent_passwd.personalisierung | default([]) | length > 3)
|
||||
and tuxflotte_pers_konto.ansible_facts.getent_passwd.personalisierung[3] in ["Personalisierung", "Passwort vergessen?"] }}
|
||||
when: tuxflotte_state == "absent"
|
||||
|
||||
- name: End a running kiosk session (Auftrag abgewählt)
|
||||
- name: End a running kiosk session
|
||||
ansible.builtin.command: loginctl terminate-user personalisierung
|
||||
register: tuxflotte_pers_terminate
|
||||
changed_when: tuxflotte_pers_terminate.rc == 0
|
||||
failed_when: false
|
||||
when: tuxflotte_state == "absent" and (tuxflotte_pers_konto_unser | bool)
|
||||
when: tuxflotte_pers_konto_unser | bool
|
||||
|
||||
- name: Remove personalisierung account and home (Auftrag abgewählt)
|
||||
- name: Remove personalisierung account and home
|
||||
ansible.builtin.user:
|
||||
name: personalisierung
|
||||
state: absent
|
||||
remove: true
|
||||
force: true
|
||||
when: tuxflotte_state == "absent" and (tuxflotte_pers_konto_unser | bool)
|
||||
register: tuxflotte_pers_konto_entfernt
|
||||
when: tuxflotte_pers_konto_unser | bool
|
||||
|
||||
- name: Remove personalisierung marker (Auftrag abgewählt)
|
||||
# AccountsService hielt das Konto auf Liebherr-84 im Cache (ADR-0041,
|
||||
# Ergänzung 18.10.2026); mit dem Konto verschwindet auch sein Eintrag.
|
||||
- name: Remove AccountsService entry of the personalisierung account
|
||||
ansible.builtin.file:
|
||||
path: /run/tuxflotte/agent/applied/personalisierung.marker
|
||||
path: /var/lib/AccountsService/users/personalisierung
|
||||
state: absent
|
||||
when: tuxflotte_state == "absent"
|
||||
when: tuxflotte_pers_konto_unser | bool
|
||||
|
||||
# slick-greeter übernimmt das verschwundene Konto nicht live; ohne Neustart
|
||||
# stünde der Eintrag bis zum nächsten Start des Geräts da. Nur ohne laufende
|
||||
# Benutzersitzung, sonst beendete der Neustart sie (wie bisher beim Wächter).
|
||||
- name: Restart the login screen so the entry disappears
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
for sitzung in $(loginctl list-sessions --no-legend | awk '{print $1}'); do
|
||||
if [ "$(loginctl show-session "$sitzung" --property=Class --value)" = user ]; then
|
||||
echo "Benutzersitzung läuft"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
systemctl try-restart lightdm.service
|
||||
echo "neu gestartet"
|
||||
register: tuxflotte_pers_greeter
|
||||
changed_when: "'neu gestartet' in tuxflotte_pers_greeter.stdout"
|
||||
when: tuxflotte_pers_konto_entfernt is changed
|
||||
|
||||
# Den Wrapper erst entfernen, wenn das laufende LightDM nach dem Abbau seiner
|
||||
# Konfiguration gestartet wurde. Der Zeitpunkt, ab dem das gilt, steht in der
|
||||
# Merkerdatei (beim ersten Lauf nach dem Abbau gesetzt). Läuft kein LightDM
|
||||
# (GDM oder gerade gestoppt), liest ein späterer Start die Konfiguration ohne
|
||||
# den Wrapper.
|
||||
- name: Remove the old session wrapper once LightDM no longer uses it
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
WRAPPER=/usr/local/lib/tuxflotte/lightdm-sitzung
|
||||
SEIT=/var/lib/tuxflotte/personalisierung-wrapper-ungenutzt-seit
|
||||
if [ ! -e "$WRAPPER" ]; then
|
||||
rm -f "$SEIT"
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -s "$SEIT" ]; then
|
||||
mkdir -p "$(dirname "$SEIT")"
|
||||
date +%s > "$SEIT"
|
||||
echo "vorgemerkt"
|
||||
exit 0
|
||||
fi
|
||||
pid=$(systemctl show -p MainPID --value lightdm.service 2>/dev/null || echo 0)
|
||||
if [ "${pid:-0}" -gt 0 ] 2>/dev/null; then
|
||||
laufzeit=$(ps -o etimes= -p "$pid" | tr -d ' ')
|
||||
[ -n "$laufzeit" ] || exit 0
|
||||
[ $(( $(date +%s) - laufzeit )) -gt "$(head -n 1 "$SEIT")" ] || exit 0
|
||||
fi
|
||||
rm -f "$WRAPPER" "$SEIT"
|
||||
echo "entfernt"
|
||||
register: tuxflotte_pers_wrapper
|
||||
changed_when: "'entfernt' in tuxflotte_pers_wrapper.stdout"
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
#
|
||||
# LightDM-session-wrapper: das Konto "personalisierung" bekommt statt der
|
||||
# gewählten Sitzung den Kiosk, alle anderen laufen unverändert über den
|
||||
# Wrapper der Distribution. Der Kiosk wird dabei wie ein Sitzungskommando
|
||||
# übergeben, damit die übliche Sitzungsumgebung (Profil, D-Bus,
|
||||
# Xsession.d) auch für ihn gilt.
|
||||
if [ "$(id -un)" = "personalisierung" ]; then
|
||||
set -- /usr/local/lib/tuxflotte/personalisierung-sitzung
|
||||
fi
|
||||
exec {{ tuxflotte_personalisierung_wrapper_original }} "$@"
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Verwaltet von Tuxflotte (Rolle personalisierung)
|
||||
xset s off -dpms 2>/dev/null || true
|
||||
# Mit eingerichtetem persönlichem Konto ist der Eintrag "Passwort vergessen?"
|
||||
# und der Kiosk öffnet die Seite dafür (Adresse mit Gerät und Kiosk-Token).
|
||||
url={{ tuxflotte_personalisierung_url | quote }}
|
||||
if [ "$(head -n 1 /etc/tuxflotte/personalisierung-anbieten 2>/dev/null)" = passwort ] &&
|
||||
[ -s /etc/tuxflotte/personalisierung-vergessen-url ]; then
|
||||
url=$(head -n 1 /etc/tuxflotte/personalisierung-vergessen-url)
|
||||
fi
|
||||
exec /usr/bin/python3 /usr/local/lib/tuxflotte/tuxflotte-kiosk.py \
|
||||
--url "$url" \
|
||||
--leerlauf-minuten {{ tuxflotte_personalisierung_leerlauf_minuten | int }}
|
||||
@@ -91,7 +91,8 @@
|
||||
tags: persoenliche-konten
|
||||
tags: persoenliche-konten
|
||||
|
||||
# Konto "Personalisierung" mit Kiosk am Anmeldebildschirm (ADR-0041).
|
||||
# Merkmal "Personalisierung möglich" (ADR-0041): baut seit 24.10.2026
|
||||
# nur noch Konto "Personalisierung" und Kiosk früherer Stände ab.
|
||||
- include_role:
|
||||
name: personalisierung
|
||||
apply:
|
||||
|
||||
Reference in new issue
Block a user