schulsync 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. schulsync-0.1.0/.github/workflows/ci.yml +46 -0
  2. schulsync-0.1.0/.github/workflows/release.yml +64 -0
  3. schulsync-0.1.0/.gitignore +23 -0
  4. schulsync-0.1.0/CHANGELOG.md +21 -0
  5. schulsync-0.1.0/LICENSE +21 -0
  6. schulsync-0.1.0/PKG-INFO +229 -0
  7. schulsync-0.1.0/README.md +193 -0
  8. schulsync-0.1.0/docs/beispiel-report.html +1248 -0
  9. schulsync-0.1.0/docs/dsgvo.md +85 -0
  10. schulsync-0.1.0/docs/funktionsweise.md +126 -0
  11. schulsync-0.1.0/docs/images/apply-2026.svg +442 -0
  12. schulsync-0.1.0/docs/images/plan-2026.svg +414 -0
  13. schulsync-0.1.0/docs/images/plan-idempotent.svg +121 -0
  14. schulsync-0.1.0/docs/images/report-2026.png +0 -0
  15. schulsync-0.1.0/examples/lehrkraefte.csv +6 -0
  16. schulsync-0.1.0/examples/schild-export-2025.csv +150 -0
  17. schulsync-0.1.0/examples/schild-export-2026-nachzuegler.csv +151 -0
  18. schulsync-0.1.0/examples/schild-export-2026.csv +150 -0
  19. schulsync-0.1.0/examples/schild-export-legacy-cp1252.csv +6 -0
  20. schulsync-0.1.0/examples/schulsync.example.yaml +42 -0
  21. schulsync-0.1.0/lab/Dockerfile +23 -0
  22. schulsync-0.1.0/lab/README.md +28 -0
  23. schulsync-0.1.0/lab/docker-compose.yml +18 -0
  24. schulsync-0.1.0/lab/entrypoint.sh +25 -0
  25. schulsync-0.1.0/lab/schulsync.lab.yaml +25 -0
  26. schulsync-0.1.0/pyproject.toml +81 -0
  27. schulsync-0.1.0/scripts/erzeuge_beispieldaten.py +161 -0
  28. schulsync-0.1.0/scripts/erzeuge_readme_bilder.py +108 -0
  29. schulsync-0.1.0/src/schulsync/__init__.py +7 -0
  30. schulsync-0.1.0/src/schulsync/applier.py +185 -0
  31. schulsync-0.1.0/src/schulsync/cli.py +423 -0
  32. schulsync-0.1.0/src/schulsync/config.py +141 -0
  33. schulsync-0.1.0/src/schulsync/csvsource.py +194 -0
  34. schulsync-0.1.0/src/schulsync/ldapclient.py +392 -0
  35. schulsync-0.1.0/src/schulsync/models.py +167 -0
  36. schulsync-0.1.0/src/schulsync/passwords.py +42 -0
  37. schulsync-0.1.0/src/schulsync/planner.py +172 -0
  38. schulsync-0.1.0/src/schulsync/report.py +94 -0
  39. schulsync-0.1.0/src/schulsync/templates/briefe.html.j2 +56 -0
  40. schulsync-0.1.0/src/schulsync/templates/report.html.j2 +206 -0
  41. schulsync-0.1.0/src/schulsync/usernames.py +91 -0
  42. schulsync-0.1.0/tests/integration/conftest.py +59 -0
  43. schulsync-0.1.0/tests/integration/test_schuljahreswechsel.py +212 -0
  44. schulsync-0.1.0/tests/test_csvsource.py +115 -0
  45. schulsync-0.1.0/tests/test_planner.py +153 -0
  46. schulsync-0.1.0/tests/test_usernames.py +72 -0
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint-und-unit:
10
+ name: Lint & Unit-Tests (Python ${{ matrix.python }})
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python: ["3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - name: Installieren
21
+ run: pip install -e ".[dev]"
22
+ - name: Ruff (Lint)
23
+ run: ruff check src tests scripts
24
+ - name: Unit-Tests
25
+ run: pytest -q
26
+
27
+ integration:
28
+ name: Integration gegen echten Samba-AD-DC
29
+ runs-on: ubuntu-latest
30
+ needs: lint-und-unit
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ - uses: actions/setup-python@v6
34
+ with:
35
+ python-version: "3.12"
36
+ - name: Installieren
37
+ run: pip install -e ".[dev]"
38
+ - name: Lab-DC starten (Samba AD, Domäne SCHULE.LOCAL)
39
+ run: docker compose -f lab/docker-compose.yml up -d --build --wait
40
+ - name: Integrationstests
41
+ env:
42
+ SCHULSYNC_LDAP_PASSWORT: Lab-Kennwort-2026
43
+ run: pytest -m integration -o addopts='' -q
44
+ - name: Lab-Logs bei Fehlern
45
+ if: failure()
46
+ run: docker logs schulsync-lab-ad --tail 100
@@ -0,0 +1,64 @@
1
+ name: Release
2
+
3
+ # Auslöser: ein Versions-Tag (v0.1.0, v0.2.0, …).
4
+ # Veröffentlicht auf PyPI via Trusted Publishing (OIDC, keine Tokens)
5
+ # und legt ein GitHub-Release mit den Build-Artefakten an.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ build:
13
+ name: Paket bauen
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.12"
20
+ - name: Bauen (sdist + wheel)
21
+ run: |
22
+ pip install build twine
23
+ python -m build
24
+ twine check dist/*
25
+ - uses: actions/upload-artifact@v7
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ pypi:
31
+ name: Auf PyPI veröffentlichen
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment:
35
+ name: pypi
36
+ url: https://pypi.org/p/schulsync
37
+ permissions:
38
+ id-token: write # OIDC für PyPI Trusted Publishing
39
+ steps:
40
+ - uses: actions/download-artifact@v8
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
45
+
46
+ github-release:
47
+ name: GitHub-Release anlegen
48
+ needs: build
49
+ runs-on: ubuntu-latest
50
+ permissions:
51
+ contents: write
52
+ steps:
53
+ - uses: actions/checkout@v7
54
+ - uses: actions/download-artifact@v8
55
+ with:
56
+ name: dist
57
+ path: dist/
58
+ - name: Release erstellen
59
+ env:
60
+ GH_TOKEN: ${{ github.token }}
61
+ run: |
62
+ gh release create "$GITHUB_REF_NAME" dist/* \
63
+ --title "SchulSync $GITHUB_REF_NAME" \
64
+ --notes "Änderungen: siehe [CHANGELOG.md](https://github.com/${GITHUB_REPOSITORY}/blob/main/CHANGELOG.md)"
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+
13
+ # SchulSync – erzeugte Dateien mit personenbezogenen Daten NIEMALS committen!
14
+ zugangsdaten-*.csv
15
+ briefe/
16
+ report-*.html
17
+ schulsync.yaml
18
+ !examples/schulsync.example.yaml
19
+
20
+ # OS / IDE
21
+ .DS_Store
22
+ .idea/
23
+ .vscode/
@@ -0,0 +1,21 @@
1
+ # Änderungen
2
+
3
+ ## 0.1.0 – 2026-07
4
+
5
+ Erste Veröffentlichung.
6
+
7
+ * Deklarativer Soll/Ist-Abgleich (plan/apply) zwischen Schulverwaltungs-CSV
8
+ und Active Directory: Anlegen, Klassenwechsel, Umbenennen, Reaktivieren,
9
+ Deaktivieren.
10
+ * CSV-Profile für SchILD-NRW und ASV-BW, generisches Profil mit freier
11
+ Spaltenzuordnung; automatische Erkennung von Kodierung (UTF-8/Windows-1252)
12
+ und Trennzeichen.
13
+ * Benutzernamen mit Transliteration (Umlaute, Türkisch, Polnisch …),
14
+ sAMAccountName-20-Zeichen-Grenze, domänenweiter Kollisionsprüfung.
15
+ * Sicherheitsnetze: Dry-Run als Standard, Notbremse gegen unvollständige
16
+ Exporte, Konflikte blockieren den Lauf, TLS-Zwang.
17
+ * Löschkonzept: Abgänger-OU mit Frist-Stempel, `cleanup` löscht erst nach
18
+ Ablauf der Aufbewahrungsfrist.
19
+ * HTML-Diff-Report und druckfertige Zugangsdaten-Briefe pro Klasse.
20
+ * Docker-Lab mit echtem Samba-AD-DC; Integrationstests decken den
21
+ kompletten Konten-Lebenszyklus ab und laufen in der CI.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Haydar Kozat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: schulsync
3
+ Version: 0.1.0
4
+ Summary: Schuljahreswechsel ohne Handarbeit: deklarative Synchronisation von Schülerkonten aus der Schulverwaltung (SchILD-NRW, ASV-BW, CSV) ins Active Directory.
5
+ Project-URL: Homepage, https://github.com/haydarkozat/schulsync
6
+ Project-URL: Repository, https://github.com/haydarkozat/schulsync
7
+ Project-URL: Issues, https://github.com/haydarkozat/schulsync/issues
8
+ Author-email: Haydar Kozat <haydarkozat@hotmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: active-directory,asv-bw,dsgvo,ldap,samba,schild-nrw,schule,schuljahreswechsel,schulverwaltung
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: German
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Education
23
+ Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: jinja2>=3.1
26
+ Requires-Dist: ldap3>=2.9.1
27
+ Requires-Dist: pydantic>=2.7
28
+ Requires-Dist: pyyaml>=6.0
29
+ Requires-Dist: rich>=13.7
30
+ Requires-Dist: typer>=0.12
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.5; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # SchulSync
38
+
39
+ **Schuljahreswechsel ohne Handarbeit: Schulverwaltung → Active Directory.**
40
+
41
+ [![CI](https://github.com/haydarkozat/schulsync/actions/workflows/ci.yml/badge.svg)](https://github.com/haydarkozat/schulsync/actions/workflows/ci.yml)
42
+ ![Python](https://img.shields.io/badge/Python-3.11%2B-3776ab?logo=python&logoColor=white)
43
+ ![Getestet gegen](https://img.shields.io/badge/getestet%20gegen-Samba%20AD%20DC-1a7f4e)
44
+ ![Lizenz](https://img.shields.io/badge/Lizenz-MIT-blue)
45
+
46
+ Jeden September dasselbe Ritual: Der neue Export aus der Schulverwaltung
47
+ liegt vor, und irgendjemand aus der Schul-IT klickt sich durch hunderte
48
+ Konten – neue Fünftklässler anlegen, alle Klassen eine Stufe
49
+ weiterschieben, Abgänger deaktivieren, Passwortzettel drucken. Zwei Tage
50
+ Handarbeit, und ein Tippfehler bedeutet, dass ein Kind am ersten Schultag
51
+ nicht ins WLAN kommt.
52
+
53
+ SchulSync macht daraus **einen Befehl mit Vorschau**:
54
+
55
+ ![schulsync plan](docs/images/plan-2026.svg)
56
+
57
+ ## Das Prinzip: Terraform für Schülerkonten
58
+
59
+ Der CSV-Export der Schulverwaltung (SchILD-NRW, ASV-BW oder beliebige
60
+ Spalten) beschreibt den **Soll-Zustand**. Das Active Directory ist der
61
+ **Ist-Zustand**. SchulSync berechnet die Differenz – und führt sie erst
62
+ aus, wenn ein Mensch den Plan gesehen und bestätigt hat.
63
+
64
+ ```
65
+ schulsync plan export.csv # Was würde passieren? Ändert garantiert nichts.
66
+ schulsync apply export.csv # Plan ausführen – nach Bestätigung.
67
+ ```
68
+
69
+ * **Idempotent:** derselbe Export zweimal ⇒ beim zweiten Mal „nichts zu
70
+ tun". Abgebrochene Läufe repariert der nächste Lauf von selbst.
71
+ * **Ein Modell für alles:** Schuljahreswechsel, Zuzug im Februar,
72
+ Namensänderung nach Heirat – immer derselbe Soll/Ist-Vergleich.
73
+ * **Nachvollziehbar:** jeder Lauf auf Wunsch als HTML-Report für die
74
+ Ablage (Verfahrensnachweis).
75
+
76
+ ## Was SchulSync erledigt
77
+
78
+ | | |
79
+ |---|---|
80
+ | ➕ **Anlegen** | Konto in der Klassen-OU, Klassen- & Sammelgruppen, Home-Verzeichnis-Attribute, merkbares Initialpasswort (`Tiger-Wolke-47`), Passwortwechsel bei erster Anmeldung erzwungen |
81
+ | ⇄ **Klassenwechsel** | OU-Umzug + Gruppentausch (`Klasse-5A` → `Klasse-6A`) |
82
+ | ✎ **Umbenennen** | Namensänderung ohne neuen Benutzernamen – niemand verliert Profil oder Dateien |
83
+ | ↻ **Reaktivieren** | Rückkehrer bekommen ihr altes Konto wieder statt eines Duplikats |
84
+ | ⏻ **Deaktivieren** | Abgänger: gesperrt, aus allen Gruppen entfernt, in die Abgänger-OU, mit Frist-Stempel |
85
+ | 🗑 **Löschen** | `schulsync cleanup` löscht erst nach Ablauf der Aufbewahrungsfrist – dokumentiertes Löschkonzept statt Karteileichen |
86
+ | ✉ **Briefe** | druckfertige Zugangsdaten-Briefe pro Klasse aus einem Befehl |
87
+
88
+ Dazu die Details, die man erst nach ein paar hundert Schülerkonten zu
89
+ schätzen weiß: Umlaut- und Sonderzeichen-Transliteration
90
+ (`Yılmaz → yilmaz`, `Đorđević → dordevic`), die 20-Zeichen-Grenze von
91
+ `sAMAccountName`, Kollisionszähler gegen **alle** Konten der Domäne
92
+ (`emma.fischer2`), automatische Erkennung von Kodierung (UTF-8 ↔
93
+ Windows-1252) und Trennzeichen.
94
+
95
+ ## Eingebaute Sicherheitsnetze
96
+
97
+ * **Dry-Run zuerst:** `plan` hat keinerlei Schreibzugriff – Vorschau ist
98
+ der Normalfall, nicht das Extra.
99
+ * **Notbremse:** Ein Export, der mehr als die Hälfte der aktiven Konten
100
+ deaktivieren würde (typisch: halber Export, falsche Berichtsvorlage),
101
+ wird als Konflikt blockiert – bevor er Schaden anrichtet.
102
+ * **Konflikt heißt Stopp:** doppelte IDs, Konten ohne Quell-ID → Meldung
103
+ statt Automatik-Raten. Exit-Code 2, nichts wurde geändert.
104
+ * **Nur die eigene OU:** Konten außerhalb von `OU=SchulSync` werden nie
105
+ angefasst. Das Dienstkonto braucht keine Domain-Admin-Rechte, nur
106
+ Delegierung auf diese OU.
107
+ * **Verschlüsselung erzwungen:** LDAPS bzw. STARTTLS – unverschlüsselt
108
+ verweigert SchulSync den Dienst. Passwörter stehen nie in Dateien,
109
+ nur in `SCHULSYNC_LDAP_PASSWORT`.
110
+
111
+ ![schulsync apply](docs/images/apply-2026.svg)
112
+
113
+ ## In 5 Minuten ausprobieren – mit echtem AD
114
+
115
+ Das Repo bringt ein Docker-Lab mit: ein wegwerfbarer **Samba Active
116
+ Directory Domain Controller** (Domäne `SCHULE.LOCAL`) plus fiktive
117
+ Beispieldaten für zwei Schuljahre.
118
+
119
+ ```bash
120
+ git clone https://github.com/haydarkozat/schulsync && cd schulsync
121
+ python3 -m venv .venv && source .venv/bin/activate
122
+ pip install -e .
123
+
124
+ # 1) Lab-AD hochfahren (einmalig ~1 Minute)
125
+ docker compose -f lab/docker-compose.yml up -d --build --wait
126
+ export SCHULSYNC_LDAP_PASSWORT="Lab-Kennwort-2026"
127
+
128
+ # 2) Schuljahr 2025/26 einspielen: 149 Schüler:innen + 5 Lehrkräfte
129
+ schulsync apply examples/schild-export-2025.csv \
130
+ --lehrkraefte examples/lehrkraefte.csv --ja \
131
+ --config lab/schulsync.lab.yaml
132
+
133
+ # 3) Der Schuljahreswechsel: erst ansehen …
134
+ schulsync plan examples/schild-export-2026.csv \
135
+ --lehrkraefte examples/lehrkraefte.csv \
136
+ --report report.html --config lab/schulsync.lab.yaml
137
+
138
+ # … dann ausführen: 24 anlegen, 125 aufrücken, 24 Abgänger, 2 Umbenennungen
139
+ schulsync apply examples/schild-export-2026.csv \
140
+ --lehrkraefte examples/lehrkraefte.csv --ja \
141
+ --config lab/schulsync.lab.yaml
142
+
143
+ # 4) Zugangsdaten-Briefe für die neuen 5er drucken
144
+ schulsync briefe zugangsdaten-*.csv --loeschen --config lab/schulsync.lab.yaml
145
+ ```
146
+
147
+ Danach lohnt ein Blick in den HTML-Report ([Beispiel](docs/beispiel-report.html)):
148
+
149
+ [![HTML-Report](docs/images/report-2026.png)](docs/beispiel-report.html)
150
+
151
+ Und weil SchulSync idempotent ist, sagt derselbe Befehl direkt danach
152
+ schlicht die Wahrheit:
153
+
154
+ ![idempotenter zweiter Lauf](docs/images/plan-idempotent.svg)
155
+
156
+ ## Im echten Schulnetz
157
+
158
+ ```bash
159
+ cp examples/schulsync.example.yaml schulsync.yaml # anpassen: Server, Basis-DN, Schema
160
+ export SCHULSYNC_LDAP_PASSWORT='…' # Bind-Passwort des Dienstkontos
161
+ schulsync check # Verbindung & Konfiguration prüfen
162
+ schulsync validate export.csv # Export prüfen (Kodierung, Dubletten)
163
+ schulsync plan export.csv --report plan.html # Vorschau für die Ablage
164
+ schulsync apply export.csv # mit Rückfrage ausführen
165
+ ```
166
+
167
+ Funktioniert gegen **Windows Server AD** und **Samba AD** (ab Werk wird
168
+ gegen einen echten Samba-DC integrationsgetestet, s. u.). Für die
169
+ Drift-Überwachung: `schulsync plan export.csv --check` liefert Exit-Code 3,
170
+ sobald AD und Schulverwaltung auseinanderlaufen – fertig ist der
171
+ Nightly-Check im Monitoring.
172
+
173
+ | Kommando | Zweck |
174
+ |---|---|
175
+ | `schulsync check` | Konfiguration & AD-Verbindung prüfen (nur lesend) |
176
+ | `schulsync validate <csv>` | Export prüfen: Kodierung, Spalten, Dubletten |
177
+ | `schulsync plan <csv>` | Soll/Ist-Vergleich als Vorschau (Dry-Run) |
178
+ | `schulsync apply <csv>` | Plan ausführen (mit Bestätigung bzw. `--ja`) |
179
+ | `schulsync briefe <csv>` | Zugangsdaten-Briefe je Klasse als druckfertiges HTML |
180
+ | `schulsync cleanup` | Abgänger nach Ablauf der Aufbewahrungsfrist endgültig löschen |
181
+
182
+ ## Warum nicht …?
183
+
184
+ * **… Microsoft School Data Sync?** Synct in die Cloud (Entra ID/Teams),
185
+ nicht ins on-prem AD, und setzt M365-Schulverträge voraus. SchulSync
186
+ läuft dort, wo viele Schulträger ihre Konten wirklich verwalten: im
187
+ lokalen Active Directory – ohne dass Schülerdaten das Haus verlassen.
188
+ * **… UCS@school?** Stark, aber ein kompletter Plattformwechsel samt
189
+ eigenem Ökosystem. SchulSync ist ein Werkzeug, keine Plattform: es
190
+ arbeitet mit dem AD, das schon da ist.
191
+ * **… das gewachsene PowerShell-Skript?** Kennt jede Schul-IT. Meist ohne
192
+ Dry-Run, ohne Idempotenz, ohne Notbremse, ohne Löschkonzept – und der
193
+ Autor ist nicht mehr an der Schule. Genau diese Lücken schließt
194
+ SchulSync, mit Tests statt Hoffnung.
195
+
196
+ ## Qualität
197
+
198
+ * **60+ Tests**, darunter eine Integrationssuite, die in der CI einen
199
+ **echten Samba-AD-DC im Docker-Container** hochfährt und den kompletten
200
+ Lebenszyklus durchspielt: Erstbefüllung → Idempotenz →
201
+ Schuljahreswechsel → Rückkehrer-Reaktivierung → Notbremse →
202
+ Fristablauf & Löschung.
203
+ * Kein Mock spielt AD – was hier grün ist, lief gegen LDAP, TLS und
204
+ `unicodePwd`-Realität.
205
+ * [docs/funktionsweise.md](docs/funktionsweise.md) erklärt die
206
+ Designentscheidungen (warum `CN = Benutzername`, warum `employeeNumber`
207
+ als Schlüssel, warum diese Ausführungsreihenfolge).
208
+ * [docs/dsgvo.md](docs/dsgvo.md): Datenminimierung, Löschkonzept,
209
+ TLS-Zwang – Datenschutz als Designgrundlage, nicht als Fußnote.
210
+
211
+ > **Hinweis zu den Beispieldaten:** Alle Namen in `examples/` sind fiktiv
212
+ > und werden von `scripts/erzeuge_beispieldaten.py` deterministisch
213
+ > erzeugt. Es sind keine echten Schülerdaten – und es sollten auch nie
214
+ > welche ins Repo gelangen (`.gitignore` hilft nach).
215
+
216
+ ## Über dieses Projekt
217
+
218
+ Ich habe 16 Jahre lang Schul-IT betrieben – zuletzt als
219
+ IT-Systemadministrator im türkischen FATİH-Programm (dem Pendant zum
220
+ DigitalPakt Schule) mit rund 5.000 Nutzerkonten. Den Schuljahreswechsel
221
+ habe ich oft genug von Hand gemacht, um zu wissen, welche Fehler um
222
+ 3 Uhr nachts passieren. SchulSync ist das Werkzeug, das ich mir damals
223
+ gewünscht hätte.
224
+
225
+ **Haydar Kozat** ·
226
+ [GitHub](https://github.com/haydarkozat) ·
227
+ [LinkedIn](https://linkedin.com/in/haydar-kozat)
228
+
229
+ Lizenz: [MIT](LICENSE)
@@ -0,0 +1,193 @@
1
+ # SchulSync
2
+
3
+ **Schuljahreswechsel ohne Handarbeit: Schulverwaltung → Active Directory.**
4
+
5
+ [![CI](https://github.com/haydarkozat/schulsync/actions/workflows/ci.yml/badge.svg)](https://github.com/haydarkozat/schulsync/actions/workflows/ci.yml)
6
+ ![Python](https://img.shields.io/badge/Python-3.11%2B-3776ab?logo=python&logoColor=white)
7
+ ![Getestet gegen](https://img.shields.io/badge/getestet%20gegen-Samba%20AD%20DC-1a7f4e)
8
+ ![Lizenz](https://img.shields.io/badge/Lizenz-MIT-blue)
9
+
10
+ Jeden September dasselbe Ritual: Der neue Export aus der Schulverwaltung
11
+ liegt vor, und irgendjemand aus der Schul-IT klickt sich durch hunderte
12
+ Konten – neue Fünftklässler anlegen, alle Klassen eine Stufe
13
+ weiterschieben, Abgänger deaktivieren, Passwortzettel drucken. Zwei Tage
14
+ Handarbeit, und ein Tippfehler bedeutet, dass ein Kind am ersten Schultag
15
+ nicht ins WLAN kommt.
16
+
17
+ SchulSync macht daraus **einen Befehl mit Vorschau**:
18
+
19
+ ![schulsync plan](docs/images/plan-2026.svg)
20
+
21
+ ## Das Prinzip: Terraform für Schülerkonten
22
+
23
+ Der CSV-Export der Schulverwaltung (SchILD-NRW, ASV-BW oder beliebige
24
+ Spalten) beschreibt den **Soll-Zustand**. Das Active Directory ist der
25
+ **Ist-Zustand**. SchulSync berechnet die Differenz – und führt sie erst
26
+ aus, wenn ein Mensch den Plan gesehen und bestätigt hat.
27
+
28
+ ```
29
+ schulsync plan export.csv # Was würde passieren? Ändert garantiert nichts.
30
+ schulsync apply export.csv # Plan ausführen – nach Bestätigung.
31
+ ```
32
+
33
+ * **Idempotent:** derselbe Export zweimal ⇒ beim zweiten Mal „nichts zu
34
+ tun". Abgebrochene Läufe repariert der nächste Lauf von selbst.
35
+ * **Ein Modell für alles:** Schuljahreswechsel, Zuzug im Februar,
36
+ Namensänderung nach Heirat – immer derselbe Soll/Ist-Vergleich.
37
+ * **Nachvollziehbar:** jeder Lauf auf Wunsch als HTML-Report für die
38
+ Ablage (Verfahrensnachweis).
39
+
40
+ ## Was SchulSync erledigt
41
+
42
+ | | |
43
+ |---|---|
44
+ | ➕ **Anlegen** | Konto in der Klassen-OU, Klassen- & Sammelgruppen, Home-Verzeichnis-Attribute, merkbares Initialpasswort (`Tiger-Wolke-47`), Passwortwechsel bei erster Anmeldung erzwungen |
45
+ | ⇄ **Klassenwechsel** | OU-Umzug + Gruppentausch (`Klasse-5A` → `Klasse-6A`) |
46
+ | ✎ **Umbenennen** | Namensänderung ohne neuen Benutzernamen – niemand verliert Profil oder Dateien |
47
+ | ↻ **Reaktivieren** | Rückkehrer bekommen ihr altes Konto wieder statt eines Duplikats |
48
+ | ⏻ **Deaktivieren** | Abgänger: gesperrt, aus allen Gruppen entfernt, in die Abgänger-OU, mit Frist-Stempel |
49
+ | 🗑 **Löschen** | `schulsync cleanup` löscht erst nach Ablauf der Aufbewahrungsfrist – dokumentiertes Löschkonzept statt Karteileichen |
50
+ | ✉ **Briefe** | druckfertige Zugangsdaten-Briefe pro Klasse aus einem Befehl |
51
+
52
+ Dazu die Details, die man erst nach ein paar hundert Schülerkonten zu
53
+ schätzen weiß: Umlaut- und Sonderzeichen-Transliteration
54
+ (`Yılmaz → yilmaz`, `Đorđević → dordevic`), die 20-Zeichen-Grenze von
55
+ `sAMAccountName`, Kollisionszähler gegen **alle** Konten der Domäne
56
+ (`emma.fischer2`), automatische Erkennung von Kodierung (UTF-8 ↔
57
+ Windows-1252) und Trennzeichen.
58
+
59
+ ## Eingebaute Sicherheitsnetze
60
+
61
+ * **Dry-Run zuerst:** `plan` hat keinerlei Schreibzugriff – Vorschau ist
62
+ der Normalfall, nicht das Extra.
63
+ * **Notbremse:** Ein Export, der mehr als die Hälfte der aktiven Konten
64
+ deaktivieren würde (typisch: halber Export, falsche Berichtsvorlage),
65
+ wird als Konflikt blockiert – bevor er Schaden anrichtet.
66
+ * **Konflikt heißt Stopp:** doppelte IDs, Konten ohne Quell-ID → Meldung
67
+ statt Automatik-Raten. Exit-Code 2, nichts wurde geändert.
68
+ * **Nur die eigene OU:** Konten außerhalb von `OU=SchulSync` werden nie
69
+ angefasst. Das Dienstkonto braucht keine Domain-Admin-Rechte, nur
70
+ Delegierung auf diese OU.
71
+ * **Verschlüsselung erzwungen:** LDAPS bzw. STARTTLS – unverschlüsselt
72
+ verweigert SchulSync den Dienst. Passwörter stehen nie in Dateien,
73
+ nur in `SCHULSYNC_LDAP_PASSWORT`.
74
+
75
+ ![schulsync apply](docs/images/apply-2026.svg)
76
+
77
+ ## In 5 Minuten ausprobieren – mit echtem AD
78
+
79
+ Das Repo bringt ein Docker-Lab mit: ein wegwerfbarer **Samba Active
80
+ Directory Domain Controller** (Domäne `SCHULE.LOCAL`) plus fiktive
81
+ Beispieldaten für zwei Schuljahre.
82
+
83
+ ```bash
84
+ git clone https://github.com/haydarkozat/schulsync && cd schulsync
85
+ python3 -m venv .venv && source .venv/bin/activate
86
+ pip install -e .
87
+
88
+ # 1) Lab-AD hochfahren (einmalig ~1 Minute)
89
+ docker compose -f lab/docker-compose.yml up -d --build --wait
90
+ export SCHULSYNC_LDAP_PASSWORT="Lab-Kennwort-2026"
91
+
92
+ # 2) Schuljahr 2025/26 einspielen: 149 Schüler:innen + 5 Lehrkräfte
93
+ schulsync apply examples/schild-export-2025.csv \
94
+ --lehrkraefte examples/lehrkraefte.csv --ja \
95
+ --config lab/schulsync.lab.yaml
96
+
97
+ # 3) Der Schuljahreswechsel: erst ansehen …
98
+ schulsync plan examples/schild-export-2026.csv \
99
+ --lehrkraefte examples/lehrkraefte.csv \
100
+ --report report.html --config lab/schulsync.lab.yaml
101
+
102
+ # … dann ausführen: 24 anlegen, 125 aufrücken, 24 Abgänger, 2 Umbenennungen
103
+ schulsync apply examples/schild-export-2026.csv \
104
+ --lehrkraefte examples/lehrkraefte.csv --ja \
105
+ --config lab/schulsync.lab.yaml
106
+
107
+ # 4) Zugangsdaten-Briefe für die neuen 5er drucken
108
+ schulsync briefe zugangsdaten-*.csv --loeschen --config lab/schulsync.lab.yaml
109
+ ```
110
+
111
+ Danach lohnt ein Blick in den HTML-Report ([Beispiel](docs/beispiel-report.html)):
112
+
113
+ [![HTML-Report](docs/images/report-2026.png)](docs/beispiel-report.html)
114
+
115
+ Und weil SchulSync idempotent ist, sagt derselbe Befehl direkt danach
116
+ schlicht die Wahrheit:
117
+
118
+ ![idempotenter zweiter Lauf](docs/images/plan-idempotent.svg)
119
+
120
+ ## Im echten Schulnetz
121
+
122
+ ```bash
123
+ cp examples/schulsync.example.yaml schulsync.yaml # anpassen: Server, Basis-DN, Schema
124
+ export SCHULSYNC_LDAP_PASSWORT='…' # Bind-Passwort des Dienstkontos
125
+ schulsync check # Verbindung & Konfiguration prüfen
126
+ schulsync validate export.csv # Export prüfen (Kodierung, Dubletten)
127
+ schulsync plan export.csv --report plan.html # Vorschau für die Ablage
128
+ schulsync apply export.csv # mit Rückfrage ausführen
129
+ ```
130
+
131
+ Funktioniert gegen **Windows Server AD** und **Samba AD** (ab Werk wird
132
+ gegen einen echten Samba-DC integrationsgetestet, s. u.). Für die
133
+ Drift-Überwachung: `schulsync plan export.csv --check` liefert Exit-Code 3,
134
+ sobald AD und Schulverwaltung auseinanderlaufen – fertig ist der
135
+ Nightly-Check im Monitoring.
136
+
137
+ | Kommando | Zweck |
138
+ |---|---|
139
+ | `schulsync check` | Konfiguration & AD-Verbindung prüfen (nur lesend) |
140
+ | `schulsync validate <csv>` | Export prüfen: Kodierung, Spalten, Dubletten |
141
+ | `schulsync plan <csv>` | Soll/Ist-Vergleich als Vorschau (Dry-Run) |
142
+ | `schulsync apply <csv>` | Plan ausführen (mit Bestätigung bzw. `--ja`) |
143
+ | `schulsync briefe <csv>` | Zugangsdaten-Briefe je Klasse als druckfertiges HTML |
144
+ | `schulsync cleanup` | Abgänger nach Ablauf der Aufbewahrungsfrist endgültig löschen |
145
+
146
+ ## Warum nicht …?
147
+
148
+ * **… Microsoft School Data Sync?** Synct in die Cloud (Entra ID/Teams),
149
+ nicht ins on-prem AD, und setzt M365-Schulverträge voraus. SchulSync
150
+ läuft dort, wo viele Schulträger ihre Konten wirklich verwalten: im
151
+ lokalen Active Directory – ohne dass Schülerdaten das Haus verlassen.
152
+ * **… UCS@school?** Stark, aber ein kompletter Plattformwechsel samt
153
+ eigenem Ökosystem. SchulSync ist ein Werkzeug, keine Plattform: es
154
+ arbeitet mit dem AD, das schon da ist.
155
+ * **… das gewachsene PowerShell-Skript?** Kennt jede Schul-IT. Meist ohne
156
+ Dry-Run, ohne Idempotenz, ohne Notbremse, ohne Löschkonzept – und der
157
+ Autor ist nicht mehr an der Schule. Genau diese Lücken schließt
158
+ SchulSync, mit Tests statt Hoffnung.
159
+
160
+ ## Qualität
161
+
162
+ * **60+ Tests**, darunter eine Integrationssuite, die in der CI einen
163
+ **echten Samba-AD-DC im Docker-Container** hochfährt und den kompletten
164
+ Lebenszyklus durchspielt: Erstbefüllung → Idempotenz →
165
+ Schuljahreswechsel → Rückkehrer-Reaktivierung → Notbremse →
166
+ Fristablauf & Löschung.
167
+ * Kein Mock spielt AD – was hier grün ist, lief gegen LDAP, TLS und
168
+ `unicodePwd`-Realität.
169
+ * [docs/funktionsweise.md](docs/funktionsweise.md) erklärt die
170
+ Designentscheidungen (warum `CN = Benutzername`, warum `employeeNumber`
171
+ als Schlüssel, warum diese Ausführungsreihenfolge).
172
+ * [docs/dsgvo.md](docs/dsgvo.md): Datenminimierung, Löschkonzept,
173
+ TLS-Zwang – Datenschutz als Designgrundlage, nicht als Fußnote.
174
+
175
+ > **Hinweis zu den Beispieldaten:** Alle Namen in `examples/` sind fiktiv
176
+ > und werden von `scripts/erzeuge_beispieldaten.py` deterministisch
177
+ > erzeugt. Es sind keine echten Schülerdaten – und es sollten auch nie
178
+ > welche ins Repo gelangen (`.gitignore` hilft nach).
179
+
180
+ ## Über dieses Projekt
181
+
182
+ Ich habe 16 Jahre lang Schul-IT betrieben – zuletzt als
183
+ IT-Systemadministrator im türkischen FATİH-Programm (dem Pendant zum
184
+ DigitalPakt Schule) mit rund 5.000 Nutzerkonten. Den Schuljahreswechsel
185
+ habe ich oft genug von Hand gemacht, um zu wissen, welche Fehler um
186
+ 3 Uhr nachts passieren. SchulSync ist das Werkzeug, das ich mir damals
187
+ gewünscht hätte.
188
+
189
+ **Haydar Kozat** ·
190
+ [GitHub](https://github.com/haydarkozat) ·
191
+ [LinkedIn](https://linkedin.com/in/haydar-kozat)
192
+
193
+ Lizenz: [MIT](LICENSE)