Rolle kiosk: Vollbild-Browser mit Whitelist am Anmeldebildschirm, optional Autostart (roadmap/kiosk.md)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-26 13:52:14 +01:00
1 parent a778bf67a6
commit 9070313731
7 files changed
+880

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
tuxflotte_state: absent
tuxflotte_optionen: {}
+104
View File
@@ -0,0 +1,104 @@
#!/bin/sh
# Verwaltet von Tuxflotte (Rolle kiosk)
#
# Gibt die Anmeldung am Konto "kiosk" frei oder nimmt sie weg (Muster:
# personalisierung-anmeldung bis ansible-content f975938). Frei nur, wenn
# beides gilt:
# - die Rolle hat eine gültige Konfiguration abgelegt (KIOSK), 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.
# Frei: passwortlose PAM-Freigabe, Shell bash, im Greeter sichtbar, mit
# AUTOSTART zusätzlich automatische Anmeldung (wirkt ab dem nächsten Start
# von LightDM, zusammen mit dem Wrapper).
# Sonst: keine Freigabe, Shell nologin, in AccountsService
# SystemAccount=true (sonst bleibt das Konto im Greeter stehen), keine
# automatische Anmeldung.
#
# 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/86-tuxflotte-kiosk.conf
AUTOLOGIN=/etc/lightdm/lightdm.conf.d/87-tuxflotte-kiosk-autostart.conf
KIOSK=/etc/tuxflotte/kiosk.json
AUTOSTART=/etc/tuxflotte/kiosk-autostart
PAM=/etc/pam.d/lightdm
ACCOUNTS=/var/lib/AccountsService/users/kiosk
ZEILE='auth sufficient pam_succeed_if.so user = kiosk'
MUSTER='^auth[[:space:]]+sufficient[[:space:]]+pam_succeed_if\.so[[:space:]]+user[[:space:]]+=[[:space:]]+kiosk[[: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")" ]
}
soll=gesperrt
if [ -s "$KIOSK" ] && getent passwd kiosk >/dev/null && wrapper_aktiv "${1:-}"; then
soll=frei
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
# Automatische Anmeldung nur zusammen mit dem Wrapper: Beide Dateien liest
# LightDM beim selben Start.
if [ "$soll" = frei ] && [ -f "$AUTOSTART" ]; then
if [ ! -f "$AUTOLOGIN" ]; then
printf '%s\n' '# Verwaltet von Tuxflotte (Rolle kiosk)' '[Seat:*]' 'autologin-user=kiosk' 'autologin-user-timeout=0' > "$AUTOLOGIN"
geaendert=" geaendert"
fi
elif [ -f "$AUTOLOGIN" ]; then
rm -f "$AUTOLOGIN"
geaendert=" geaendert"
fi
if getent passwd kiosk >/dev/null; then
if [ "$(getent passwd kiosk | cut -d: -f7)" != "$shell" ]; then
usermod -s "$shell" kiosk
geaendert=" geaendert"
fi
# AccountsService: SystemAccount=true blendet aus, ohne den Schlüssel
# zeigt der Greeter das Konto. Andere Einträge der Datei bleiben stehen.
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 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"
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Verwaltet von Tuxflotte (Rolle kiosk)
#
# Sitzung des Kontos "kiosk": startet den Kiosk-Browser immer wieder.
# Rückgabe 0 = Abmelden (zurück zum Anmeldebildschirm, bei Autostart
# meldet LightDM gleich wieder an), 3 = gewollter Neustart mit der
# Startseite, sonst (Absturz, neue Konfiguration) nach kurzer Pause.
while :; do
/usr/bin/python3 /usr/local/lib/tuxflotte/tuxflotte-kiosk.py --konfig /etc/tuxflotte/kiosk.json
case $? in
0) exit 0 ;;
3) ;;
*) sleep 3 ;;
esac
done
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/python3
# Verwaltet von Tuxflotte (Rolle kiosk)
#
# Kiosk-Browser (platform-docs roadmap/kiosk.md, 26.10.2026): zeigt die
# Startseite im Vollbild, ohne Fenstermanager, Adresszeile, neue Fenster,
# Dateidialoge, Downloads und Kontextmenü. Daten bleiben nur im Speicher.
# Weiterentwicklung des Personalisierungs-Kiosks (Rolle personalisierung bis
# f975938).
#
# Whitelist: geprüft werden nur Navigationen (Seite und Rahmen), nicht
# Bilder, Skripte oder Schriften. Navigiert der Benutzer aus der Whitelist
# hinaus, endet das Programm mit NEUSTART; kiosk-sitzung startet es frisch
# mit der Startseite. Navigationen ohne Benutzeraktion (fremde Rahmen,
# Skripte) werden nur still blockiert.
#
# Leerlauf: nach der eingestellten Zeit ohne Eingabe ebenfalls NEUSTART,
# aber nur, wenn seit dem Start jemand etwas eingegeben hat.
#
# Beenden (Abmelden, Rückgabe 0): Knopf in der Leiste, Strg+Umschalt+Q oder
# ein Link auf "tuxflotte:abmelden".
import argparse
import html
import json
import subprocess
import sys
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"
NEUSTART = 3
def whitelist_eintrag(text):
""""https://www.Example.org/katalog/" -> ("www.example.org", "/katalog")."""
text = text.strip()
if not text or text.startswith("#"):
return None
if "://" not in text:
text = "http://" + text
teile = urlsplit(text)
if not teile.hostname:
return None
pfad = teile.path.rstrip("/")
return (teile.hostname.lower().removeprefix("*."), pfad)
def ja(wert):
return wert is True or str(wert).strip().lower() in ("true", "1", "on", "yes")
def host_passt(host, domain):
return host == domain or host.endswith("." + domain)
class Kiosk:
def __init__(self, konfig):
self.start_url = konfig["startseite"]
self.frei = ja(konfig.get("frei_surfen"))
self.leerlauf = max(0, int(konfig.get("leerlauf_minuten", 10))) * 60
self.rueckgabe = 0
start_host = (urlsplit(self.start_url).hostname or "").lower()
self.whitelist = [(start_host, "")]
for zeile in konfig.get("whitelist", []):
eintrag = whitelist_eintrag(zeile)
if eintrag:
self.whitelist.append(eintrag)
self.letzte_eingabe = time.monotonic()
self.benutzt = False
if ja(konfig.get("bildschirm_an")):
subprocess.run(["xset", "s", "off", "-dpms"], check=False)
kontext = WebKit2.WebContext.new_ephemeral()
kontext.set_preferred_languages(["de-DE", "de"])
kontext.connect("download-started", lambda _k, download: download.cancel())
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", self.on_create)
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-changed", self.on_load_changed)
self.view.connect("load-failed", self.on_load_failed)
self.view.connect("load-failed-with-tls-errors", self.on_tls_error)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
if not ja(konfig.get("ohne_leiste")):
box.pack_start(self.leiste(), False, False, 0)
box.pack_start(self.view, True, True, 0)
self.fenster = Gtk.Window()
self.fenster.set_decorated(False)
self.fenster.add(box)
self.fenster.connect("destroy", lambda *_: Gtk.main_quit())
self.fenster.connect("key-press-event", self.on_taste)
for signal in ("key-press-event", "button-press-event", "motion-notify-event", "scroll-event", "touch-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()
if self.leerlauf:
GLib.timeout_add_seconds(15, self.pruefe_leerlauf)
self.view.load_uri(self.start_url)
self.fenster.show_all()
self.fenster.present()
self.view.grab_focus()
def leiste(self):
leiste = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
leiste.set_border_width(4)
for text, aktion in (
("← Zurück", lambda *_: self.view.go_back()),
("⌂ Startseite", lambda *_: self.view.load_uri(self.start_url)),
):
knopf = Gtk.Button(label=text)
knopf.set_can_focus(False)
knopf.connect("clicked", aktion)
leiste.pack_start(knopf, False, False, 0)
abmelden = Gtk.Button(label="Abmelden")
abmelden.set_can_focus(False)
abmelden.connect("clicked", lambda *_: self.beenden(0))
leiste.pack_end(abmelden, False, False, 0)
return leiste
def beenden(self, rueckgabe):
self.rueckgabe = rueckgabe
Gtk.main_quit()
return False
def erlaubt(self, uri):
teile = urlsplit(uri)
if teile.scheme not in ("http", "https") or not teile.hostname:
return False
if self.frei:
return True
host = teile.hostname.lower()
pfad = teile.path or "/"
for domain, anfang in self.whitelist:
if host_passt(host, domain) and (not anfang or pfad == anfang or pfad.startswith(anfang + "/")):
return True
return False
def on_eingabe(self, *_):
self.letzte_eingabe = time.monotonic()
self.benutzt = True
return False
def on_taste(self, _fenster, ereignis):
strg = ereignis.state & Gdk.ModifierType.CONTROL_MASK
umschalt = ereignis.state & Gdk.ModifierType.SHIFT_MASK
taste = Gdk.keyval_to_lower(ereignis.keyval)
if strg and umschalt and taste == Gdk.KEY_q:
return self.beenden(0) or True
if strg and taste in (Gdk.KEY_plus, Gdk.KEY_KP_Add, Gdk.KEY_equal):
self.view.set_zoom_level(min(self.view.get_zoom_level() + 0.1, 3.0))
return True
if strg and taste in (Gdk.KEY_minus, Gdk.KEY_KP_Subtract):
self.view.set_zoom_level(max(self.view.get_zoom_level() - 0.1, 0.5))
return True
if strg and taste in (Gdk.KEY_0, Gdk.KEY_KP_0):
self.view.set_zoom_level(1.0)
return True
return False
def pruefe_leerlauf(self):
if self.benutzt and time.monotonic() - self.letzte_eingabe > self.leerlauf:
self.beenden(NEUSTART)
return False
return True
def verlassen(self, aktion):
"""Ziel außerhalb der Whitelist: Neustart, wenn der Benutzer es
ausgelöst hat, sonst nur blockieren."""
if aktion.is_user_gesture() or aktion.get_navigation_type() in (
WebKit2.NavigationType.LINK_CLICKED,
WebKit2.NavigationType.FORM_SUBMITTED,
WebKit2.NavigationType.FORM_RESUBMITTED,
):
GLib.idle_add(self.beenden, NEUSTART)
def on_create(self, _view, aktion):
# target="_blank" und window.open: im selben Fenster öffnen.
uri = aktion.get_request().get_uri()
if self.erlaubt(uri):
self.view.load_uri(uri)
else:
self.verlassen(aktion)
return None
def on_policy(self, _view, entscheidung, art):
if art in (WebKit2.PolicyDecisionType.NAVIGATION_ACTION, WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION):
aktion = entscheidung.get_navigation_action()
uri = aktion.get_request().get_uri()
if uri == ABMELDEN:
entscheidung.ignore()
GLib.idle_add(self.beenden, 0)
return True
if uri.startswith("about:"):
return False
if not self.erlaubt(uri):
entscheidung.ignore()
self.verlassen(aktion)
return True
return False
if art == WebKit2.PolicyDecisionType.RESPONSE:
if not entscheidung.is_mime_type_supported():
entscheidung.ignore()
return True
return False
def on_load_changed(self, view, ereignis):
# Sicherheitsnetz: Was trotzdem als Seite geladen wird, muss erlaubt
# sein.
if ereignis == WebKit2.LoadEvent.COMMITTED:
uri = view.get_uri() or ""
if uri and not uri.startswith("about:") and not self.erlaubt(uri) and uri != self.start_url:
GLib.idle_add(self.beenden, NEUSTART)
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>
</main></body></html>""",
self.start_url,
None,
)
# Ohne Bedienung (Anzeigetafel) nach einer Minute selbst neu laden.
GLib.timeout_add_seconds(60, self.erneut_laden)
def erneut_laden(self):
self.view.load_uri(self.start_url)
return False
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 Seite ist gerade nicht erreichbar. Bitte prüfe die Netzwerkverbindung.")
return True
def on_tls_error(self, *_):
self.fehlerseite("Die Verbindung 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("--konfig", default="/etc/tuxflotte/kiosk.json")
args = parser.parse_args()
with open(args.konfig, encoding="utf-8") as datei:
konfig = json.load(datei)
kiosk = Kiosk(konfig)
Gtk.main()
sys.exit(kiosk.rueckgabe)
if __name__ == "__main__":
main()
+426
View File
@@ -0,0 +1,426 @@
# Merkmal "Kiosk" (platform-docs roadmap/kiosk.md, 26.10.2026): passwortloses
# Konto "kiosk" am Anmeldebildschirm, das statt eines Desktops nur den
# Kiosk-Browser mit der Startseite startet; optional automatische Anmeldung
# (Autostart, z.B. Bibliothek, Schwarzes Brett). Gastsitzung und andere
# Konten bleiben daneben nutzbar.
#
# Optionen (kundenplattform optionen_felder.py, an OE oder Gruppe gesetzt):
# kiosk_startseite (Pflicht, http/https), kiosk_whitelist (eine Domain je
# Zeile, optional mit Pfad), kiosk_frei_surfen, kiosk_anzeigename,
# kiosk_leerlauf_minuten (leer = 10, 0 = nie), kiosk_ohne_leiste,
# kiosk_bildschirm_an, kiosk_autostart. Ohne gültige Startseite wird nichts
# eingerichtet bzw. ein bestehender Kiosk abgebaut.
#
# Nur LightDM (Mint, Debian/Xfce). GDM (Ubuntu) kennt keinen
# session-wrapper; dort wird übersprungen.
- name: Ensure marker directory exists
ansible.builtin.file:
path: /run/tuxflotte/agent/applied
state: directory
mode: "0755"
- name: Check for LightDM PAM configuration
ansible.builtin.stat:
path: /etc/pam.d/lightdm
register: tuxflotte_kiosk_lightdm
- name: Evaluate kiosk options
ansible.builtin.set_fact:
tuxflotte_kiosk_startseite: "{{ tuxflotte_optionen.get('kiosk_startseite') | default('', true) | trim }}"
tuxflotte_kiosk_whitelist_roh: "{{ tuxflotte_optionen.get('kiosk_whitelist') | default('', true) }}"
tuxflotte_kiosk_anzeigename: >-
{{ (tuxflotte_optionen.get('kiosk_anzeigename') | default('', true) | regex_replace('[:\n\r,]', ' ') | trim)[:40]
| default('Kiosk', true) }}
tuxflotte_kiosk_leerlauf: "{{ tuxflotte_optionen.get('kiosk_leerlauf_minuten') | default('10', true) | string | trim }}"
- name: Decide whether the kiosk is configured
ansible.builtin.set_fact:
tuxflotte_kiosk_konfiguriert: >-
{{ tuxflotte_state == "present"
and (tuxflotte_kiosk_startseite is match('^https?://[^\s/]+[^\s]*$')) }}
# Den Wrapper der Distribution ermitteln, in LightDMs Lesereihenfolge (die
# letzte Angabe gilt), ohne Dateien von Tuxflotte (auch nicht die der
# früheren Personalisierung).
- 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
case "$f" in *tuxflotte*) continue ;; esac
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
case "$wrapper" in /usr/local/lib/tuxflotte/*) wrapper="" ;; esac
if [ -n "$wrapper" ] && [ -x "$wrapper" ]; then
echo "$wrapper"
fi
register: tuxflotte_kiosk_wrapper
changed_when: false
when: tuxflotte_kiosk_konfiguriert | bool and tuxflotte_kiosk_lightdm.stat.exists
- name: Decide whether the kiosk can be set up here
ansible.builtin.set_fact:
tuxflotte_kiosk_aktiv: >-
{{ (tuxflotte_kiosk_konfiguriert | bool) and tuxflotte_kiosk_lightdm.stat.exists
and (tuxflotte_kiosk_wrapper.stdout | default('')) != '' }}
tuxflotte_kiosk_wrapper_original: "{{ tuxflotte_kiosk_wrapper.stdout | default('') }}"
- name: Report skipped setup
ansible.builtin.debug:
msg: >-
Kiosk: {{ 'keine gültige Startseite (http/https) in den Optionen'
if not (tuxflotte_kiosk_konfiguriert | bool)
else 'kein LightDM oder kein Sitzungs-Wrapper gefunden' }}, nicht eingerichtet
when: tuxflotte_state == "present" and not (tuxflotte_kiosk_aktiv | bool)
# Läuft außer dem Kiosk eine Benutzersitzung, wird LightDM nicht neu
# gestartet (das beendete sie); die Änderung wirkt dann beim nächsten Start.
- name: Check for running user sessions other than the kiosk
ansible.builtin.shell: |
set -eu
for sitzung in $(loginctl list-sessions --no-legend | awk '{print $1}'); do
werte=$(loginctl show-session "$sitzung" --property=Class --property=State --property=Name)
echo "$werte" | grep -qx Class=user || continue
echo "$werte" | grep -qx State=closing && continue
echo "$werte" | grep -qx Name=kiosk && continue
echo "belegt"
exit 0
done
echo "frei"
register: tuxflotte_kiosk_sitzungen
changed_when: false
failed_when: false
# --- einrichten ----------------------------------------------------------------
- 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
register: tuxflotte_kiosk_pakete
until: tuxflotte_kiosk_pakete is succeeded
retries: 10
delay: 30
when: tuxflotte_kiosk_aktiv | bool
# Kein Passwort (gesperrt), Anmeldung nur über die PAM-Freigabe von
# kiosk-anmeldung. Die Shell setzt das Skript; der Kommentar ist der
# Anzeigename im Greeter.
- name: Ensure kiosk account exists
ansible.builtin.user:
name: kiosk
comment: "{{ tuxflotte_kiosk_anzeigename }}"
create_home: true
password_lock: true
register: tuxflotte_kiosk_konto
when: tuxflotte_kiosk_aktiv | bool
- name: Ensure Tuxflotte directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- /usr/local/lib/tuxflotte
- /etc/tuxflotte
- /var/lib/tuxflotte
when: tuxflotte_kiosk_aktiv | bool
# Kennzeichnet das Konto als unseres (für das Abwählen).
- name: Record that the kiosk account belongs to this role
ansible.builtin.copy:
dest: /var/lib/tuxflotte/kiosk-konto
content: "kiosk\n"
mode: "0644"
when: tuxflotte_kiosk_aktiv | bool
# Eine Vormerkung aus einem früheren Abbau gälte sonst für den neuen
# Wrapper (siehe Abbau unten).
- name: Clear an old wrapper removal note
ansible.builtin.file:
path: /var/lib/tuxflotte/kiosk-wrapper-ungenutzt-seit
state: absent
when: tuxflotte_kiosk_aktiv | bool
- name: Install kiosk program and scripts
ansible.builtin.copy:
src: "{{ item }}"
dest: "/usr/local/lib/tuxflotte/{{ item }}"
mode: "0755"
loop:
- tuxflotte-kiosk.py
- kiosk-sitzung
- kiosk-anmeldung
register: tuxflotte_kiosk_programm
when: tuxflotte_kiosk_aktiv | bool
- name: Install LightDM session wrapper
ansible.builtin.template:
src: kiosk-wrapper.j2
dest: /usr/local/lib/tuxflotte/kiosk-wrapper
mode: "0755"
when: tuxflotte_kiosk_aktiv | bool
# Lesbar für das Kiosk-Konto, keine Zugangsdaten.
- name: Write kiosk configuration
ansible.builtin.copy:
dest: /etc/tuxflotte/kiosk.json
content: "{{ tuxflotte_kiosk_konfig | to_nice_json }}\n"
mode: "0644"
vars:
tuxflotte_kiosk_konfig:
startseite: "{{ tuxflotte_kiosk_startseite }}"
whitelist: >-
{{ (tuxflotte_kiosk_whitelist_roh if tuxflotte_kiosk_whitelist_roh is string
else tuxflotte_kiosk_whitelist_roh | join('\n')).splitlines()
| map('trim') | select | list }}
frei_surfen: "{{ tuxflotte_optionen.get('kiosk_frei_surfen', false) | bool }}"
leerlauf_minuten: "{{ tuxflotte_kiosk_leerlauf | int(10) if tuxflotte_kiosk_leerlauf is match('^[0-9]+$') else 10 }}"
ohne_leiste: "{{ tuxflotte_optionen.get('kiosk_ohne_leiste', false) | bool }}"
bildschirm_an: "{{ tuxflotte_optionen.get('kiosk_bildschirm_an', false) | bool }}"
register: tuxflotte_kiosk_json
when: tuxflotte_kiosk_aktiv | bool
- name: Record the autostart wish
ansible.builtin.copy:
dest: /etc/tuxflotte/kiosk-autostart
content: "# Verwaltet von Tuxflotte (Rolle kiosk)\n"
mode: "0644"
register: tuxflotte_kiosk_autostart_an
when: (tuxflotte_kiosk_aktiv | bool) and (tuxflotte_optionen.get('kiosk_autostart', false) | bool)
- name: Remove the autostart wish
ansible.builtin.file:
path: /etc/tuxflotte/kiosk-autostart
state: absent
register: tuxflotte_kiosk_autostart_aus
when: (tuxflotte_kiosk_aktiv | bool) and not (tuxflotte_optionen.get('kiosk_autostart', false) | bool)
- name: Ensure LightDM drop-in directory exists
ansible.builtin.file:
path: /etc/lightdm/lightdm.conf.d
state: directory
mode: "0755"
when: tuxflotte_kiosk_aktiv | bool
- name: Use the kiosk session wrapper in LightDM
ansible.builtin.copy:
dest: /etc/lightdm/lightdm.conf.d/86-tuxflotte-kiosk.conf
content: |
# Verwaltet von Tuxflotte (Rolle kiosk)
[Seat:*]
session-wrapper=/usr/local/lib/tuxflotte/kiosk-wrapper
mode: "0644"
register: tuxflotte_kiosk_lightdm_konf
when: tuxflotte_kiosk_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_kiosk_aktiv | bool
# Beim Start von LightDM 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/86-tuxflotte-kiosk.conf
content: |
# Verwaltet von Tuxflotte (Rolle kiosk)
[Service]
ExecStartPre=-/usr/local/lib/tuxflotte/kiosk-anmeldung start
mode: "0644"
register: tuxflotte_kiosk_dropin
when: tuxflotte_kiosk_aktiv | bool
- name: Reload systemd for the lightdm.service drop-in
ansible.builtin.systemd:
daemon_reload: true
when: (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_dropin is changed
- name: Ensure logind drop-in directory exists
ansible.builtin.file:
path: /etc/systemd/logind.conf.d
state: directory
mode: "0755"
when: tuxflotte_kiosk_aktiv | bool
# Kein Prozess des Kiosk-Kontos überlebt das Abmelden; logind führt die
# KillOnlyUsers-Listen aller Dateien zusammen.
- name: Kill leftover kiosk processes on logout (logind)
ansible.builtin.copy:
dest: /etc/systemd/logind.conf.d/50-tuxflotte-kiosk.conf
content: |
# Verwaltet von Tuxflotte (Rolle kiosk)
[Login]
KillUserProcesses=yes
KillOnlyUsers=kiosk
mode: "0644"
when: tuxflotte_kiosk_aktiv | bool
- name: Apply the login switch now
ansible.builtin.command: /usr/local/lib/tuxflotte/kiosk-anmeldung
register: tuxflotte_kiosk_anmeldung
changed_when: "'geaendert' in tuxflotte_kiosk_anmeldung.stdout"
when: tuxflotte_kiosk_aktiv | bool
- name: Refresh the greeter's account data after a name change
ansible.builtin.systemd:
name: accounts-daemon.service
state: restarted
failed_when: false
when: (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto is changed
# Neue Konfiguration: der laufende Kiosk-Browser endet, kiosk-sitzung
# startet ihn mit ihr neu.
- name: Restart a running kiosk browser with the new configuration
ansible.builtin.command: pkill -u kiosk -f /usr/local/lib/tuxflotte/tuxflotte-kiosk.py
register: tuxflotte_kiosk_pkill
changed_when: tuxflotte_kiosk_pkill.rc == 0
failed_when: false
when: (tuxflotte_kiosk_aktiv | bool) and (tuxflotte_kiosk_json is changed or tuxflotte_kiosk_programm is changed)
# Wrapper und automatische Anmeldung liest LightDM nur beim Start. Ohne
# fremde Benutzersitzung gleich neu starten (eine laufende Kiosk-Sitzung
# endet dabei), sonst wirkt es beim nächsten Start des Geräts.
- name: Restart the login screen to apply wrapper or autostart
ansible.builtin.command: systemctl try-restart lightdm.service
when: >-
(tuxflotte_kiosk_aktiv | bool)
and (tuxflotte_kiosk_sitzungen.stdout | default('')) == 'frei'
and (tuxflotte_kiosk_lightdm_konf is changed or tuxflotte_kiosk_dropin is changed
or tuxflotte_kiosk_autostart_an is changed or tuxflotte_kiosk_autostart_aus is changed)
- name: Record that the kiosk role ran
ansible.builtin.copy:
dest: /run/tuxflotte/agent/applied/kiosk.marker
content: "{{ ansible_date_time.iso8601 }}\n"
mode: "0644"
when: tuxflotte_kiosk_aktiv | bool
# --- abbauen (abgewählt oder ohne gültige Startseite) --------------------------
- name: Remove passwordless login for kiosk
ansible.builtin.lineinfile:
path: /etc/pam.d/lightdm
regexp: '^auth\s+sufficient\s+pam_succeed_if\.so\s+user\s+=\s+kiosk\s*$'
state: absent
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_lightdm.stat.exists
- name: Check whether the kiosk account is ours
ansible.builtin.stat:
path: /var/lib/tuxflotte/kiosk-konto
register: tuxflotte_kiosk_konto_unser
when: not (tuxflotte_kiosk_aktiv | bool)
- name: End a running kiosk session
ansible.builtin.command: loginctl terminate-user kiosk
register: tuxflotte_kiosk_terminate
changed_when: tuxflotte_kiosk_terminate.rc == 0
failed_when: false
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto_unser.stat.exists
# kiosk-wrapper bleibt bewusst liegen: Ein laufendes LightDM ruft ihn bis
# zu seinem nächsten Start weiter auf; ohne Konto "kiosk" reicht er nur
# durch. Entfernt wird er weiter unten.
- name: Remove kiosk files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/lightdm/lightdm.conf.d/86-tuxflotte-kiosk.conf
- /etc/lightdm/lightdm.conf.d/87-tuxflotte-kiosk-autostart.conf
- /etc/systemd/system/lightdm.service.d/86-tuxflotte-kiosk.conf
- /etc/systemd/logind.conf.d/50-tuxflotte-kiosk.conf
- /etc/tuxflotte/kiosk.json
- /etc/tuxflotte/kiosk-autostart
- /usr/local/lib/tuxflotte/tuxflotte-kiosk.py
- /usr/local/lib/tuxflotte/kiosk-sitzung
- /usr/local/lib/tuxflotte/kiosk-anmeldung
register: tuxflotte_kiosk_entfernt
when: not (tuxflotte_kiosk_aktiv | bool)
- name: Reload systemd after removing the drop-in
ansible.builtin.systemd:
daemon_reload: true
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_entfernt is changed
- name: Remove kiosk account and home
ansible.builtin.user:
name: kiosk
state: absent
remove: true
force: true
register: tuxflotte_kiosk_konto_entfernt
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto_unser.stat.exists
- name: Remove AccountsService entry and ownership note of the kiosk account
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /var/lib/AccountsService/users/kiosk
- /var/lib/tuxflotte/kiosk-konto
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto_unser.stat.exists
# slick-greeter übernimmt das verschwundene Konto nicht live.
- name: Restart the login screen after removal
ansible.builtin.command: systemctl try-restart lightdm.service
when: >-
not (tuxflotte_kiosk_aktiv | bool)
and (tuxflotte_kiosk_sitzungen.stdout | default('')) == 'frei'
and (tuxflotte_kiosk_konto_entfernt is changed or tuxflotte_kiosk_entfernt is changed)
# Den Wrapper erst entfernen, wenn das laufende LightDM nach dem Abbau seiner
# Konfiguration gestartet wurde (Zeitpunkt in der Merkerdatei, beim ersten
# Lauf nach dem Abbau gesetzt). Läuft kein LightDM, liest ein späterer Start
# die Konfiguration ohne den Wrapper.
- name: Remove the session wrapper once LightDM no longer uses it
ansible.builtin.shell: |
set -eu
WRAPPER=/usr/local/lib/tuxflotte/kiosk-wrapper
SEIT=/var/lib/tuxflotte/kiosk-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_kiosk_wrapper_weg
changed_when: "'entfernt' in tuxflotte_kiosk_wrapper_weg.stdout"
when: not (tuxflotte_kiosk_aktiv | bool)
- name: Remove kiosk marker
ansible.builtin.file:
path: /run/tuxflotte/agent/applied/kiosk.marker
state: absent
when: not (tuxflotte_kiosk_aktiv | bool)
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# Verwaltet von Tuxflotte (Rolle kiosk)
#
# LightDM-session-wrapper: das Konto "kiosk" bekommt statt der gewählten
# Sitzung den Kiosk, alle anderen laufen unverändert über den Wrapper der
# Distribution. Der Kiosk wird wie ein Sitzungskommando übergeben, damit
# die übliche Sitzungsumgebung (Profil, D-Bus, Xsession.d) auch für ihn
# gilt (wie früher bei der Rolle personalisierung).
if [ "$(id -un)" = "kiosk" ]; then
set -- /usr/local/lib/tuxflotte/kiosk-sitzung
fi
exec {{ tuxflotte_kiosk_wrapper_original }} "$@"
+11
View File
@@ -102,6 +102,17 @@
tuxflotte_optionen: "{{ tuxflotte_auftrag_optionen.get('personalisierung', {}) }}"
tags: personalisierung
# Merkmal "Kiosk" (roadmap/kiosk.md, 26.10.2026): Konto mit Vollbild-
# Browser am Anmeldebildschirm, optional Autostart.
- include_role:
name: kiosk
apply:
tags: kiosk
vars:
tuxflotte_state: "{{ tuxflotte_auftrag_states.get('kiosk', 'absent') }}"
tuxflotte_optionen: "{{ tuxflotte_auftrag_optionen.get('kiosk', {}) }}"
tags: kiosk
- include_role:
name: brave
apply: