Compare commits

...

29 Commits

Author SHA1 Message Date
4376069b11 NFS pointed to wrong place 2025-11-24 15:37:12 +01:00
c285ae81af Test with NFS
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 15:20:31 +01:00
5bfe4866a4 Deploy version 0.955
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
2025-11-24 13:48:35 +01:00
f7799675d5 Typo in template fixed 2025-11-24 13:46:13 +01:00
c125427b8d ArgoCD resolved 2025-11-24 13:43:00 +01:00
a14a80f7bd Design tweaks
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 13:38:53 +01:00
477143b3ff Merge pull request 'fix: add argocd ignore-healthcheck and ingressClassName to Ingress' (#13) from improvements/argocd-service-fix into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Reviewed-on: #13
2025-11-24 11:02:52 +00:00
fc404f6755 Merge pull request 'troubleshooting ingress' (#12) from improvements/frontend into development
Reviewed-on: #12
2025-11-24 10:56:18 +00:00
fe7c55eceb Merge branch 'development' into improvements/frontend 2025-11-24 10:56:10 +00:00
bb01174bd2 fix: add argocd ignore-healthcheck and ingressClassName to Ingress
- Add ignore-healthcheck annotation to prevent 'Processing' state
- Add ingressClassName: traefik for proper ingress controller binding
2025-11-24 11:34:20 +01:00
38ce55d8fd troubleshooting ingress 2025-11-24 10:22:09 +00:00
d439741339 Merge pull request 'feature/login' (#11) from feature/login into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 19s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 3s
Reviewed-on: #11
2025-11-24 10:20:48 +00:00
bc75cac6cd Merge branch 'development' into feature/login 2025-11-24 10:20:32 +00:00
47c264e8e1 fix: update search tests to match actual template output
- Change expected 'Suchresultate' to 'Suchergebnisse' matching results.html
- Change expected 'Keine Resultate' to 'Keine Ergebnisse gefunden'
- Replace test_search_result_logging with test_search_result_structure
- Remove unused unittest.mock import
2025-11-24 11:19:09 +01:00
4d0ed116dd test: add comprehensive authentication test suite
- Add 21 test cases covering login, logout, and password change functionality
- Test both success and failure scenarios for authentication flows
- Verify proper redirects to main page instead of admin
- Test user menu display for authenticated vs anonymous users
- Test CSRF protection and POST requirement for logout
- Test password validation and error handling
- All tests passing, ensuring authentication feature works correctly
2025-11-24 10:46:10 +01:00
ceb6e13447 fix: resolve logout 405 error by using POST method
- Change logout link from GET anchor to POST form
- Add CSRF token for security
- Style button to match dropdown menu appearance
2025-11-24 10:39:40 +01:00
7e9059a9aa feat: implement user authentication with login/logout functionality
- Add user login screen with German interface
- Add user icon and dropdown menu in header for authenticated users
- Add password change functionality with proper redirects
- Configure authentication URLs and settings
- Ensure all auth functions redirect to main page instead of admin
- Complete openspec change proposal for login feature
2025-11-24 10:37:23 +01:00
ccf31e4ef4 troubleshooting ingress 2025-11-21 19:14:22 +01:00
94e047c7ff Ingress troubleshooting
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 19s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2025-11-21 16:39:32 +01:00
57f2210c77 Rootmismatch - redeploying
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 30s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2025-11-21 16:20:37 +01:00
1745596d14 Deploy after Kubernetes fuckup
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 28s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2025-11-20 15:32:24 +01:00
e923624aec Metadata moved to end of document 2025-11-19 13:49:45 +01:00
3649878b7d Ignore file maintenance 2025-11-18 11:36:06 +01:00
179e7d41b3 Merge pull request 'feature/oblique' (#10) from feature/oblique into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 3m19s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 8s
Reviewed-on: #10
2025-11-18 08:18:49 +00:00
5f95042cad Replaced NGINX ingress. Fingers crossed. 2025-11-18 08:46:16 +01:00
9a3ab1234a "Erklärungs" corrected 2025-11-12 12:27:52 +01:00
7591de99e0 Documentation for file import and diagram service added 2025-11-10 16:01:41 +01:00
b2e2da95b2 Badges align properly now
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
2025-11-10 13:46:31 +01:00
8ec294c8a8 Changed startseite to show names 2025-11-10 12:56:08 +01:00
29 changed files with 1751 additions and 66 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ keys/
node_modules/
package-lock.json
package.json
AGENT*.md
# Diagram cache directory
media/diagram_cache/
.env

View File

@@ -0,0 +1,491 @@
# Dokumenten-Import: Anleitung
Diese Anleitung beschreibt, wie Dokumente und Vorgaben mittels des `import-document` Management Commands in die VorgabenUI importiert werden können.
## Übersicht
Der `import-document` Command ermöglicht es, strukturierte Textdateien zu importieren, die Dokumente mit Einleitung, Geltungsbereich und Vorgaben enthalten. Das Format ist speziell für die einfache Erfassung und Pflege von IT-Sicherheitsstandards konzipiert.
## Grundlegende Verwendung
### Minimaler Aufruf
```bash
python manage.py import-document <dateipfad> \
--nummer <dokumentnummer> \
--name "<dokumentname>" \
--dokumententyp "<typ>"
```
### Beispiel
```bash
python manage.py import-document Documentation/import\ formats/r009.txt \
--nummer "R0009" \
--name "Serversysteme" \
--dokumententyp "Standard IT-Sicherheit"
```
## Parameter
### Pflichtparameter
| Parameter | Beschreibung | Beispiel |
|-----------|--------------|----------|
| `file_path` | Pfad zur Importdatei | `Documentation/import formats/r009.txt` |
| `--nummer` | Dokumentnummer (eindeutig) | `R0009`, `R0066` |
| `--name` | Dokumentname | `"Logging"`, `"Serversysteme"` |
| `--dokumententyp` | Dokumententyp (muss bereits existieren) | `"Standard IT-Sicherheit"` |
### Optionale Parameter
| Parameter | Beschreibung | Verwendung |
|-----------|--------------|------------|
| `--gueltigkeit_von` | Startdatum der Gültigkeit | `--gueltigkeit_von 2024-01-01` |
| `--gueltigkeit_bis` | Enddatum der Gültigkeit | `--gueltigkeit_bis 2025-12-31` |
| `--dry-run` | Testlauf ohne Datenbankänderungen | `--dry-run` |
| `--verbose` | Ausführliche Ausgabe (mit `--dry-run`) | `--dry-run --verbose` |
| `--purge` | Löscht bestehende Einleitung/Geltungsbereich/Vorgaben vor Import | `--purge` |
### Dry-Run Modus
Der Dry-Run Modus ist besonders nützlich zum Testen:
```bash
python manage.py import-document r009.txt \
--nummer "R0009" \
--name "Serversysteme" \
--dokumententyp "Standard IT-Sicherheit" \
--dry-run --verbose
```
Dies zeigt an, was importiert würde, ohne die Datenbank zu ändern.
### Purge Modus
**Achtung:** Der Purge-Modus löscht alle bestehenden Vorgaben des Dokuments!
```bash
python manage.py import-document r009.txt \
--nummer "R0009" \
--name "Serversysteme" \
--dokumententyp "Standard IT-Sicherheit" \
--purge
```
Nutzen Sie `--dry-run --purge` zuerst, um zu sehen, was gelöscht würde.
## Dateiformat
### Grundstruktur
Die Importdatei ist eine Textdatei (UTF-8 kodiert) mit speziellen Trennzeichen `>>>` am Zeilenanfang.
### Aufbau
```
>>>Einleitung
>>>text
[Einleitungstext]
>>>Geltungsbereich
>>>text
[Geltungsbereichstext]
>>>Vorgabe [Thema]
>>>Nummer [nummer]
>>>Titel
[Titel der Vorgabe]
>>>Kurztext
>>>Text
[Kurztext-Inhalt]
>>>Langtext
>>>Text
[Langtext-Inhalt]
>>>Stichworte
[Stichwort1, Stichwort2, ...]
>>>Checkliste
[Frage 1]
[Frage 2]
```
### Abschnitt-Trenner
Jeder Abschnitt beginnt mit `>>>` gefolgt vom Abschnittstyp:
- `>>>Einleitung` - Startet den Einleitungsbereich
- `>>>Geltungsbereich` - Startet den Geltungsbereichsbereich
- `>>>Vorgabe [Thema]` - Startet eine neue Vorgabe mit angegebenem Thema
### Abschnitttypen für Textinhalte
Nach `>>>Einleitung`, `>>>Geltungsbereich`, `>>>Kurztext` oder `>>>Langtext` folgt ein Abschnitttyp:
| Typ | Verwendung | Beispiel |
|-----|------------|----------|
| `>>>text` oder `>>>Text` | Normaler Fliesstext | `>>>text` |
| `>>>liste geordnet` oder `>>>Liste geordnet` | Nummerierte Liste | `>>>Liste geordnet` |
| `>>>liste ungeordnet` oder `>>>Liste ungeordnet` | Aufzählungsliste | `>>>liste ungeordnet` |
**Hinweis:** Gross-/Kleinschreibung und Bindestriche (`liste-ungeordnet`) werden normalisiert.
## Beispiele
### Beispiel 1: Einfaches Dokument mit einer Vorgabe
```
>>>Einleitung
>>>text
Dieser Standard definiert Anforderungen für XYZ.
>>>Geltungsbereich
>>>text
Dieser Standard gilt für alle Systeme im BIT.
>>>Vorgabe Technik
>>>Nummer 1
>>>Titel
Verschlüsselung verwenden
>>>Kurztext
>>>Text
Alle Verbindungen müssen verschlüsselt sein.
>>>Langtext
>>>Text
Die Verschlüsselung muss mindestens TLS 1.2 verwenden.
Es sind folgende Cipher Suites erlaubt:
>>>Liste ungeordnet
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
>>>Stichworte
Verschlüsselung, TLS, Kryptographie
>>>Checkliste
TLS 1.2 oder höher ist konfiguriert
Schwache Cipher Suites sind deaktiviert
```
### Beispiel 2: Mehrere Vorgaben mit gleichem Thema
```
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
CMDB-Erfassung
>>>Kurztext
>>>Text
Alle Assets müssen in der CMDB erfasst sein.
>>>Langtext
>>>Text
Jedes Asset muss dokumentiert werden...
>>>Vorgabe Organisation
>>>Nummer 2
>>>Titel
Verantwortlichkeiten
>>>Kurztext
>>>Text
Verantwortlichkeiten müssen definiert sein.
>>>Langtext
>>>Text
Für jedes System muss ein Verantwortlicher...
```
### Beispiel 3: Verschiedene Themen
```
>>>Vorgabe Technik
>>>Nummer 1
>>>Titel
Firewall-Konfiguration
>>>Kurztext
>>>Text
Lokale Firewall muss aktiviert sein.
>>>Langtext
>>>Text
Details zur Firewall-Konfiguration...
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Dokumentation
>>>Kurztext
>>>Text
Konfiguration muss dokumentiert sein.
>>>Langtext
>>>Text
Die Firewall-Regeln müssen...
>>>Vorgabe Informationen
>>>Nummer 1
>>>Titel
Datenklassifizierung
>>>Kurztext
>>>Text
Daten müssen klassifiziert werden.
>>>Langtext
>>>Text
Klassifizierungsstufen sind...
```
### Beispiel 4: Mehrere Textabschnitte
```
>>>Vorgabe Technik
>>>Nummer 1
>>>Titel
Komplexe Anforderung
>>>Kurztext
>>>Text
Erste Zusammenfassung.
>>>Langtext
>>>Text
Erster Absatz mit Details.
>>>Text
Zweiter Absatz mit weiteren Informationen.
>>>Liste ungeordnet
Punkt 1
Punkt 2
Punkt 3
>>>Text
Abschliessender Text nach der Liste.
```
## Vorgaben-Struktur
### Vorgabe-Header
```
>>>Vorgabe [Thema]
```
Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Übliche Themen:
- Organisation
- Technik
- Informationen
- Systeme
- Anwendungen
- Zonen
### Vorgabe-Felder
#### Nummer (Pflicht)
```
>>>Nummer 1
```
oder inline:
```
>>>Nummer: 1
```
Die Nummer wird als Integer gespeichert. Sie ist eindeutig innerhalb eines Dokuments und Themas.
#### Titel (Pflicht)
```
>>>Titel
Titel der Vorgabe
```
oder inline:
```
>>>Titel: Titel der Vorgabe
```
#### Kurztext (Optional)
```
>>>Kurztext
>>>Text
Kurze Zusammenfassung der Vorgabe.
```
#### Langtext (Optional)
```
>>>Langtext
>>>Text
Ausführliche Beschreibung der Vorgabe.
>>>Liste ungeordnet
Punkt 1
Punkt 2
```
#### Stichworte (Optional)
Komma-getrennte Liste:
```
>>>Stichworte
Firewall, Netzwerk, Sicherheit
```
oder als Block:
```
>>>Stichworte
>>>Text
Firewall, Netzwerk, Sicherheit
```
**Hinweis:** Stichworte werden automatisch in der Datenbank angelegt, falls sie noch nicht existieren.
#### Checkliste (Optional)
```
>>>Checkliste
Ist die Firewall aktiviert?
Sind alle unnötigen Ports geschlossen?
Wurde die Konfiguration dokumentiert?
```
Jede Zeile wird als separate Checklistenfrage gespeichert.
## Tipps und Best Practices
### 1. Dry-Run vor Import
Führen Sie immer zuerst einen Dry-Run durch:
```bash
python manage.py import-document datei.txt \
--nummer "R0XXX" \
--name "Test" \
--dokumententyp "Standard IT-Sicherheit" \
--dry-run --verbose
```
### 2. Themen vorab erstellen
Stellen Sie sicher, dass alle verwendeten Themen in der Datenbank existieren:
```python
python manage.py shell
>>> from dokumente.models import Thema
>>> Thema.objects.get_or_create(name="Organisation")
>>> Thema.objects.get_or_create(name="Technik")
>>> Thema.objects.get_or_create(name="Informationen")
```
### 3. Dokumententyp vorab erstellen
Der Dokumententyp muss existieren:
```python
python manage.py shell
>>> from dokumente.models import Dokumententyp
>>> Dokumententyp.objects.get_or_create(
... name="Standard IT-Sicherheit",
... defaults={"verantwortliche_ve": "SR-SUR-SEC"}
... )
```
### 4. Abschnitttypen prüfen
Folgende Abschnitttypen müssen in der Datenbank existieren:
- `text`
- `liste geordnet`
- `liste ungeordnet`
- `code`
- `diagramm`
Prüfen Sie diese in der Autorenumgebung unter "Abschnitttypen".
### 5. UTF-8 Kodierung
Stellen Sie sicher, dass Ihre Importdatei UTF-8 kodiert ist, besonders bei Umlauten (ä, ö, ü) und Sonderzeichen.
### 6. Versionierung mit Purge
Beim Re-Import mit `--purge`:
```bash
# 1. Backup erstellen
python manage.py dumpdata dokumente.Dokument dokumente.Vorgabe > backup.json
# 2. Import mit Purge
python manage.py import-document datei.txt \
--nummer "R0009" \
--name "Serversysteme" \
--dokumententyp "Standard IT-Sicherheit" \
--purge
```
### 7. Mehrzeilige Inhalte
Mehrzeiliger Text wird automatisch erkannt:
```
>>>Langtext
>>>Text
Dies ist Zeile 1.
Dies ist Zeile 2.
Dies ist ein neuer Absatz.
```
### 8. Leerzeilen
Leerzeilen innerhalb eines Abschnitts werden beibehalten. Eine Leerzeile nach einem `>>>`-Trenner wird ignoriert.
## Fehlerbehebung
### "Dokumententyp does not exist"
Der angegebene Dokumententyp existiert nicht in der Datenbank.
**Lösung:** Erstellen Sie den Dokumententyp in der Autorenumgebung oder per Shell.
### "Thema not found, skipping Vorgabe"
Das in der Vorgabe verwendete Thema existiert nicht.
**Lösung:** Erstellen Sie das Thema in der Autorenumgebung oder passen Sie die Importdatei an.
### "AbschnittTyp not found"
Ein verwendeter Abschnitttyp existiert nicht.
**Lösung:**
- Prüfen Sie die Schreibweise (Gross-/Kleinschreibung wird normalisiert)
- Erstellen Sie den Abschnitttyp in der Autorenumgebung
- Standardtypen: `text`, `liste geordnet`, `liste ungeordnet`
### Vorgabe wird nicht importiert
Prüfen Sie:
- Ist `>>>Nummer` gesetzt?
- Ist `>>>Titel` gesetzt?
- Existiert das Thema?
Verwenden Sie `--dry-run --verbose` für detaillierte Informationen.
## Weitere Informationen
### Beispieldateien
Beispieldateien finden Sie in:
- `Documentation/import formats/r009.txt`
- `Documentation/import formats/r0126.txt`
### Export-Format
Der Export verwendet JSON statt Textformat. Für JSON-Export siehe:
```bash
python manage.py export_json --output dokumente.json
```
Oder über die Web-Oberfläche: `/dokumente/R0066/?format=json`
### Verwandte Commands
- `export_json` - Exportiert Dokumente als JSON
- `sanity_check_vorgaben` - Prüft Vorgaben auf Konflikte
- `clear_diagram_cache` - Löscht Diagramm-Cache
## Kontakt
Bei Fragen oder Problemen wenden Sie sich an das Information Security Management BIT.

544
Documentation/diagramm.md Normal file
View File

@@ -0,0 +1,544 @@
# Diagramme in VorgabenUI
Diese Anleitung beschreibt, wie Diagramme in Textabschnitten verwendet werden können. VorgabenUI nutzt [Kroki](https://kroki.io/) zur Generierung von Diagrammen aus Textbeschreibungen.
## Übersicht
Der Abschnitttyp **"diagramm"** ermöglicht es, verschiedene Diagrammtypen direkt in Vorgaben, Einleitungen und Geltungsbereichen einzubinden. Die Diagramme werden aus Textbeschreibungen automatisch als SVG-Grafiken generiert und gecacht.
## Grundlegende Verwendung
### Format
```
[Diagrammtyp]
option: [HTML-Attribute] (optional)
[Diagramm-Code]
```
**Zeile 1:** Diagrammtyp (z.B. `plantuml`, `mermaid`, `graphviz`)
**Zeile 2 (optional):** `option:` gefolgt von HTML-Attributen für die Bildgrösse
**Zeile 3+:** Der eigentliche Diagramm-Quellcode
### Standard-Einstellungen
Wenn keine `option:`-Zeile angegeben wird, wird das Diagramm mit `width="100%"` dargestellt.
### Grösse anpassen
Um die Grösse des Diagramms anzupassen, verwenden Sie die `option:`-Zeile:
```
plantuml
option: width="50%"
@startuml
...
@enduml
```
Weitere Beispiele für Optionen:
```
option: width="800px"
option: width="60%" style="max-width: 600px;"
option: height="400px"
option: width="100%" class="img-fluid"
```
## Unterstützte Diagrammtypen
Kroki unterstützt über 20 verschiedene Diagrammtypen. Die wichtigsten sind:
| Typ | Beschreibung | Verwendung |
|-----|--------------|------------|
| `plantuml` | UML-Diagramme, Sequenzdiagramme, Aktivitätsdiagramme | Prozesse, Architekturen |
| `mermaid` | Flussdiagramme, Sequenzdiagramme, Gantt-Charts | Prozessabläufe, Zeitpläne |
| `graphviz` | Gerichtete/ungerichtete Graphen | Abhängigkeiten, Netzwerke |
| `blockdiag` | Block-Diagramme | Systemübersichten |
| `nwdiag` | Netzwerk-Diagramme | Netzwerktopologien |
| `seqdiag` | Sequenz-Diagramme | Kommunikationsabläufe |
| `c4plantuml` | C4-Modell Diagramme | Software-Architektur |
| `ditaa` | ASCII-Art zu Diagrammen | Einfache Skizzen |
| `excalidraw` | Hand-gezeichnete Diagramme | Präsentationen |
| `bpmn` | Business Process Model Notation | Geschäftsprozesse |
Vollständige Liste: https://kroki.io/
## Beispiele
### PlantUML: Sequenzdiagramm
```
plantuml
option: width="80%"
@startuml
Alice -> Bob: Authentifizierungsanfrage
Bob --> Alice: Authentifizierungsantwort
Alice -> Bob: Weitere Anfrage
Alice <-- Bob: Weitere Antwort
@enduml
```
**Verwendung:** Darstellung von Kommunikationsabläufen, API-Interaktionen
### PlantUML: Aktivitätsdiagramm
```
plantuml
@startuml
start
:Sicherheitsrichtlinie prüfen;
if (Richtlinie erfüllt?) then (ja)
:Zugriff gewähren;
else (nein)
:Zugriff verweigern;
:Incident loggen;
endif
stop
@enduml
```
**Verwendung:** Prozesse, Entscheidungsabläufe, Workflows
### PlantUML: Komponentendiagramm
```
plantuml
option: width="70%"
@startuml
package "Web-Tier" {
[Web-Server]
[Load Balancer]
}
package "App-Tier" {
[Application Server]
[Cache]
}
package "Data-Tier" {
[Datenbank]
}
[Load Balancer] --> [Web-Server]
[Web-Server] --> [Application Server]
[Application Server] --> [Cache]
[Application Server] --> [Datenbank]
@enduml
```
**Verwendung:** System-Architekturen, Komponenten-Übersicht
### Mermaid: Flussdiagramm
```
mermaid
option: width="75%"
flowchart TD
Start[Log-Event empfangen] --> Check{Schweregrad?}
Check -->|CRITICAL| Alert[Alert auslösen]
Check -->|WARNING| Log[In Log-System speichern]
Check -->|INFO| Archive[Archivieren]
Alert --> SIEM[An SIEM weiterleiten]
Log --> SIEM
SIEM --> End[Fertig]
Archive --> End
```
**Verwendung:** Prozessabläufe, Entscheidungsbäume
### Mermaid: Sequenzdiagramm
```
mermaid
sequenceDiagram
participant Benutzer
participant Firewall
participant Server
Benutzer->>Firewall: Verbindungsanfrage
Firewall->>Firewall: Regel-Prüfung
alt Regel erlaubt
Firewall->>Server: Weiterleitung
Server-->>Firewall: Antwort
Firewall-->>Benutzer: Antwort
else Regel blockiert
Firewall-->>Benutzer: Verbindung abgelehnt
end
```
**Verwendung:** Kommunikationsabläufe mit Bedingungen
### GraphViz: Abhängigkeitsgraph
```
graphviz
digraph G {
rankdir=LR;
node [shape=box, style=rounded];
"R0066\nLogging" -> "R0009\nServersysteme";
"R0066\nLogging" -> "R0126\nNetzwerksicherheit";
"R0009\nServersysteme" -> "R0135\nInformationssicherheit";
"R0126\nNetzwerksicherheit" -> "R0135\nInformationssicherheit";
"R0135\nInformationssicherheit" [style="rounded,filled", fillcolor=lightblue];
}
```
**Verwendung:** Abhängigkeiten zwischen Dokumenten/Standards
### GraphViz: Netzwerk-Topologie
```
graphviz
option: width="100%"
graph G {
layout=neato;
Internet [shape=cloud];
Firewall [shape=box, style=filled, fillcolor=red];
DMZ [shape=box, style=filled, fillcolor=yellow];
LAN [shape=box, style=filled, fillcolor=green];
Internet -- Firewall [label="WAN"];
Firewall -- DMZ [label="DMZ-Segment"];
Firewall -- LAN [label="Internes Netz"];
}
```
**Verwendung:** Netzwerk-Segmentierung, Zonen-Modelle
### BlockDiag: System-Übersicht
```
blockdiag
blockdiag {
orientation = portrait;
Client -> "Load Balancer" -> "Web-Server 1", "Web-Server 2";
"Web-Server 1", "Web-Server 2" -> "App-Server";
"App-Server" -> "Datenbank Primary";
"Datenbank Primary" -> "Datenbank Replica";
"Load Balancer" [color = "lightblue"];
"App-Server" [color = "lightgreen"];
"Datenbank Primary" [color = "pink"];
}
```
**Verwendung:** Einfache System-Diagramme, Datenflüsse
### NwDiag: Netzwerk-Diagramm
```
nwdiag
nwdiag {
network dmz {
address = "192.168.1.0/24";
web01 [address = "192.168.1.10"];
web02 [address = "192.168.1.11"];
}
network internal {
address = "10.0.0.0/8";
web01 [address = "10.0.1.10"];
web02 [address = "10.0.1.11"];
app01 [address = "10.0.2.10"];
db01 [address = "10.0.3.10"];
}
}
```
**Verwendung:** Netzwerk-Topologien mit IP-Adressen
### SeqDiag: Authentifizierungsablauf
```
seqdiag
seqdiag {
Benutzer -> System: Benutzername + Passwort;
System -> LDAP: Authentifizierung prüfen;
LDAP --> System: Erfolgreich;
System -> 2FA: 2FA-Code anfordern;
2FA --> Benutzer: Code per SMS;
Benutzer -> System: 2FA-Code eingeben;
System --> Benutzer: Zugriff gewährt;
}
```
**Verwendung:** Authentifizierungs- und Autorisierungsabläufe
### C4 PlantUML: System-Kontext
```
c4plantuml
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
Person(admin, "Administrator", "BIT System-Administrator")
System(vorgaben, "VorgabenUI", "IT-Sicherheits-Vorgaben Portal")
System_Ext(ldap, "LDAP", "Authentifizierung")
Rel(admin, vorgaben, "Verwaltet Vorgaben")
Rel(vorgaben, ldap, "Authentifiziert über")
@enduml
```
**Verwendung:** Software-Architektur nach C4-Modell
## Best Practices
### 1. Diagramm-Grösse anpassen
Verwenden Sie die `option:`-Zeile, um die Grösse für bessere Lesbarkeit anzupassen:
```
plantuml
option: width="60%"
...
```
Für sehr detaillierte Diagramme:
```
plantuml
option: width="100%" style="max-width: 1200px;"
...
```
### 2. Einfachheit bevorzugen
Halten Sie Diagramme einfach und fokussiert. Zu komplexe Diagramme sind schwer zu lesen.
**Gut:**
```
mermaid
flowchart LR
A[Start] --> B{Prüfung}
B -->|OK| C[Weiter]
B -->|Fehler| D[Abbruch]
```
**Zu komplex:** Vermeiden Sie Diagramme mit mehr als 15-20 Elementen.
### 3. Konsistente Stil-Wahl
Verwenden Sie für ähnliche Konzepte den gleichen Diagrammtyp:
- **Prozesse:** Mermaid Flowchart oder PlantUML Activity
- **Kommunikation:** Mermaid/PlantUML Sequence
- **Architektur:** PlantUML Component oder C4
- **Netzwerke:** NwDiag oder GraphViz
### 4. Beschriftungen auf Deutsch
Verwenden Sie deutsche Beschriftungen für bessere Verständlichkeit:
```
mermaid
flowchart TD
Start[Anfrage empfangen] --> Prüfung{Berechtigung?}
Prüfung -->|Ja| Zugriff[Zugriff gewähren]
Prüfung -->|Nein| Ablehnung[Zugriff verweigern]
```
### 5. Farben sparsam einsetzen
Nutzen Sie Farben zur Hervorhebung wichtiger Elemente:
```
graphviz
digraph {
node [shape=box];
Kritisch [style=filled, fillcolor=red, fontcolor=white];
Wichtig [style=filled, fillcolor=orange];
Normal [style=filled, fillcolor=lightgreen];
Kritisch -> Wichtig -> Normal;
}
```
### 6. Lesbarkeit testen
Nach dem Speichern prüfen, ob das Diagramm in der Webansicht gut lesbar ist. Bei Bedarf:
- Grösse anpassen (`option: width="..."`)
- Diagramm vereinfachen
- Anderen Diagrammtyp wählen
## Fehlerbehandlung
### Diagramm wird nicht angezeigt
**Mögliche Ursachen:**
1. **Syntaxfehler im Diagramm-Code**
- Prüfen Sie die Syntax gemäss Dokumentation des Diagrammtyps
- Testen Sie den Code auf https://kroki.io/
2. **Falscher Diagrammtyp**
- Erste Zeile muss genau dem Kroki-Typ entsprechen
- Beispiel: `plantuml` (nicht `PlantUML` oder `plant-uml`)
3. **Kroki-Server nicht erreichbar**
- Bei Fehlermeldung "Error generating diagram" Server prüfen
### Fehlersuche
1. **Code auf kroki.io testen:**
```
https://kroki.io/
```
Geben Sie dort den Code ein und testen Sie die Generierung.
2. **Diagramm-Cache leeren:**
```bash
python manage.py clear_diagram_cache
```
3. **Logs prüfen:**
Fehler werden im Application-Log ausgegeben.
## Häufig verwendete Diagramm-Szenarien
### IT-Sicherheit: Bedrohungsmodell
```
mermaid
flowchart TB
Asset[Schützenswertes Asset]
Threat[Bedrohung]
Vuln[Schwachstelle]
Control[Sicherheitsmassnahme]
Threat -->|nutzt aus| Vuln
Vuln -->|betrifft| Asset
Control -->|schliesst| Vuln
Control -->|schützt| Asset
```
### Netzwerk-Segmentierung
```
nwdiag
nwdiag {
network internet {
address = "Internet";
internet [shape = cloud];
}
network dmz {
address = "DMZ (192.168.1.0/24)";
fw [address = "192.168.1.1"];
web [address = "192.168.1.10"];
}
network internal {
address = "Intern (10.0.0.0/8)";
fw [address = "10.0.0.1"];
app [address = "10.0.1.10"];
db [address = "10.0.2.10"];
}
internet -- fw;
}
```
### Patch-Management Prozess
```
plantuml
@startuml
start
:Patch-Benachrichtigung empfangen;
:Schweregrad bewerten;
if (Kritischer Patch?) then (ja)
:Sofort in Test-Umgebung testen;
if (Test erfolgreich?) then (ja)
:Notfall-Change erstellen;
:In Produktion ausrollen;
else (nein)
:Vendor kontaktieren;
endif
else (nein)
:In regulären Patch-Zyklus einplanen;
:Testen im Change-Window;
:Reguläres Rollout;
endif
:Dokumentation aktualisieren;
stop
@enduml
```
### Zugriffskontrolle
```
plantuml
@startuml
skinparam actorStyle awesome
actor Benutzer
actor Administrator
actor "Security Team" as ST
rectangle "VorgabenUI" {
usecase "Vorgaben lesen" as UC1
usecase "Vorgaben bearbeiten" as UC2
usecase "Vorgaben publizieren" as UC3
usecase "Sicherheitsreview" as UC4
}
Benutzer --> UC1
Administrator --> UC1
Administrator --> UC2
ST --> UC4
UC2 .> UC4 : <<extends>>
UC4 --> UC3
@enduml
```
## Weiterführende Ressourcen
- **Kroki Dokumentation:** https://kroki.io/
- **PlantUML Dokumentation:** https://plantuml.com/
- **Mermaid Dokumentation:** https://mermaid.js.org/
- **GraphViz Dokumentation:** https://graphviz.org/
- **Live-Editor zum Testen:** https://kroki.io/
## Tipps für spezifische Diagrammtypen
### PlantUML Tipps
```
@startuml
' Kommentare mit Apostroph
skinparam backgroundColor transparent
skinparam defaultFontName Arial
@enduml
```
### Mermaid Tipps
```
%%{ init: { 'theme': 'base' } }%%
flowchart LR
%% Kommentare mit doppeltem Prozent
```
### GraphViz Tipps
```
digraph {
// Kommentare mit doppeltem Slash
rankdir=LR; // Links nach Rechts
// rankdir=TB; // Top nach Bottom
}
```
## Support
Bei Fragen oder Problemen mit Diagrammen:
1. Code auf https://kroki.io/ testen
2. Syntax-Dokumentation des jeweiligen Diagrammtyps konsultieren
3. Diagramm-Cache leeren: `python manage.py clear_diagram_cache`
4. Bei technischen Problemen: Information Security Management BIT kontaktieren

View File

@@ -0,0 +1,92 @@
# Modelle (App: dokumente)
Kurzbeschreibungen der Modelle in dokumente/models.py.
## Dokumententyp
- Zweck: Kategorisierung von Dokumenten (z. B. Richtlinie, Verfahren).
- Wichtige Felder: `name` (CharField, PK), `verantwortliche_ve` (CharField).
- Besonderheiten: `__str__()` gibt `name` zurück.
- Meta: `verbose_name` und `verbose_name_plural` gesetzt.
## Person
- Zweck: Repräsentiert Personen (Autoren, Prüfer).
- Wichtige Felder: `name` (CharField, PK), `funktion` (CharField).
- Beziehungen: Many-to-many mit Dokument über `autoren` und `pruefende`.
- Besonderheiten: `__str__()` gibt `name` zurück; `ordering = ['name']`.
- Meta: `verbose_name_plural = "Personen"`.
## Thema
- Zweck: Thematische Einordnung von Vorgaben.
- Wichtige Felder: `name` (CharField, PK), `erklaerung` (TextField, optional).
- Besonderheiten: `__str__()` gibt `name` zurück.
## Dokument
- Zweck: Hauptobjekt; ein einzelnes Dokument mit Metadaten.
- Wichtige Felder:
- `nummer` (CharField, PK)
- `dokumententyp` (FK → Dokumententyp, on_delete=PROTECT)
- `name` (CharField)
- `autoren`, `pruefende` (ManyToManyField → Person)
- `gueltigkeit_von`, `gueltigkeit_bis` (DateField, optional)
- `aktiv` (BooleanField)
- `signatur_cso`, `anhaenge` (Metadaten)
- Besonderheiten: `__str__()` formatiert als "nummer name".
- Meta: `verbose_name` / `verbose_name_plural`.
## Vorgabe
- Zweck: Einzelne Vorgabe / Anforderung innerhalb eines Dokuments.
- Wichtige Felder:
- `order` (IntegerField) — Sortierreihenfolge
- `nummer` (IntegerField) — Nummer innerhalb Thema/Dokument
- `dokument` (FK → Dokument, CASCADE, related_name='vorgaben')
- `thema` (FK → Thema, PROTECT)
- `titel` (CharField)
- `referenzen` (M2M → Referenz, optional)
- `stichworte` (M2M → Stichwort, optional)
- `relevanz` (M2M → Rolle, optional)
- `gueltigkeit_von`, `gueltigkeit_bis` (Datum/Felder)
- Beziehungen: zu Dokument, Thema, Referenzen, Stichworte, Rollen.
- Wichtige Methoden:
- `Vorgabennummer()` — generiert eine lesbare Kennung (z. B. "DOK. T. N").
- `get_status(check_date, verbose)` — liefert "future", "active" oder "expired" oder eine deutsche Statusbeschreibung, abhängig von Gültigkeitsdaten.
- `sanity_check_vorgaben()` (static) — findet Konflikte zwischen Vorgaben mit gleicher Nummer/Thema/Dokument, deren Zeiträume sich überschneiden.
- `clean()` — ruft `find_conflicts()` auf und wirft ValidationError bei Konflikten.
- `find_conflicts()` — prüft Konflikte mit bestehenden Vorgaben (ohne sich selbst).
- `_date_ranges_intersect(...)` (static) — prüft, ob sich zwei Datumsbereiche überschneiden (None = offen).
- Besonderheiten: `__str__()` gibt "Vorgabennummer: titel" zurück.
- Meta: `ordering = ['order']`, `verbose_name_plural = "Vorgaben"`.
## VorgabeLangtext, VorgabeKurztext
- Zweck: Textabschnitts-Modelle, erben von `Textabschnitt` (aus abschnitte.models).
- Wichtige Felder: je ein FK `abschnitt` → Vorgabe.
- Besonderheit: konkrete Untertypen für Lang- und Kurztexte; Meta-`verbose_name` gesetzt.
## Geltungsbereich, Einleitung
- Zweck: Dokumentbezogene Textabschnitte (erben von `Textabschnitt`).
- Wichtige Felder: FK zum `Dokument` (`geltungsbereich` bzw. `einleitung`).
- Meta: `verbose_name`/`verbose_name_plural` gesetzt.
## Checklistenfrage
- Zweck: Einzelne Frage für Checklisten zu einer Vorgabe.
- Wichtige Felder: `vorgabe` (FK → Vorgabe, related_name="checklistenfragen"), `frage` (CharField).
- Besonderheiten: `__str__()` gibt `frage` zurück.
## VorgabenTable
- Zweck: Proxy-Modell für Vorgabe zur Darstellung (Tabellenansicht).
- Besonderheiten: kein eigenes Schema; nur Meta-Attribute (`proxy = True`, `verbose_name`).
## Changelog
- Zweck: Änderungsverzeichnis-Eintrag für ein Dokument.
- Wichtige Felder:
- `dokument` (FK → Dokument, related_name='changelog')
- `autoren` (M2M → Person)
- `datum` (DateField)
- `aenderung` (TextField)
- Besonderheiten: `__str__()` formatiert als "datum dokumentnummer".
- Meta: `verbose_name` / `verbose_name_plural`.
Hinweise zur Pflege
- Wichtige Relationen nutzen häufig on_delete=PROTECT, um versehentliche Löschungen zu vermeiden.
- Viele Modelle haben CharField-Primärschlüssel (z. B. `nummer`, `name`).
- Validierungslogik für zeitliche Konflikte ist in Vorgabe implementiert (clean / find_conflicts).
- Textabschnitt-Modelle erben Verhalten aus `abschnitte.models.Textabschnitt` — dort sind Anzeige- und Inhaltsregeln definiert.

View File

@@ -152,6 +152,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
NESTED_ADMIN_LAZY_INLINES = True
# Authentication settings
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = 'login'
#LOGGING = {
# "version": 1,
# "handlers" :{

View File

@@ -18,6 +18,7 @@ from django.contrib import admin
from django.urls import include, path, re_path
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views
import dokumente.views
import pages.views
import referenzen.views
@@ -32,6 +33,11 @@ urlpatterns = [
path('stichworte/', include("stichworte.urls")),
path('referenzen/', referenzen.views.tree, name="referenz_tree"),
path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
path('password_change/', auth_views.PasswordChangeView.as_view(template_name='registration/password_change.html', success_url='/'), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name='registration/password_change_done.html'), name='password_change_done'),
]
# Serve static files

View File

@@ -5,7 +5,8 @@ metadata:
namespace: vorgabenui
spec:
accessModes:
- ReadWriteOnce
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 2Gi

View File

@@ -25,7 +25,7 @@ spec:
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.951-oblique
image: git.baumann.gr/adebaumann/vui:0.957
imagePullPolicy: Always
ports:
- containerPort: 8000
@@ -63,6 +63,8 @@ spec:
selector:
app: django
ports:
- port: 8000
- name: http
protocol: TCP
port: 8000
targetPort: 8000

View File

@@ -4,8 +4,9 @@ metadata:
name: django
namespace: vorgabenui
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
argocd.argoproj.io/ignore-healthcheck: "true"
spec:
ingressClassName: traefik
rules:
- host: vorgabenportal.knowyoursecurity.com
http:

15
argocd/nfs-pv.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: django-data-pv
namespace: vorgabenui
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
server: 192.168.17.199
path: /mnt/user/vorgabenui

View File

@@ -0,0 +1,8 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate

Binary file not shown.

View File

@@ -19,37 +19,13 @@
<strong>Historische Version vom {{ standard.check_date }}</strong>
</div>
{% endif %}
<!-- Metadata -->
<div class="row mb-4">
<div class="col-md-12">
<dl class="row">
<dt class="col-sm-3">Autoren:</dt>
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
<dt class="col-sm-3">Prüfende:</dt>
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
<dt class="col-sm-3">Gültigkeit:</dt>
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
</dl>
<p>
<a href="{% url 'standard_json' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.json">
JSON herunterladen
</a>
</p>
</div>
</div>
<!-- Einleitung -->
{% if standard.einleitung_html %}
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Einleitung</h2>
<h2>Einleitung</h2>
</div>
<div class="card-body">
{% for typ, html in standard.einleitung_html %}
@@ -67,7 +43,7 @@
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Geltungsbereich</h2>
<h2>Geltungsbereich</h2>
</div>
<div class="card-body">
{% for typ, html in standard.geltungsbereich_html %}
@@ -96,22 +72,20 @@
<a id="{{ vorgabe.Vorgabennummer }}"></a>
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center flex-wrap">
<h3 class="h5 mb-0 flex-grow-1">
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
{% endif %}
</h3>
<div class="d-flex align-items-center gap-2">
<span class="badge badge-info">{{ vorgabe.thema }}</span>
{% if vorgabe.relevanzset %}
<span class="badge badge-secondary">
Relevanz: {{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
</div>
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<h3>
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
{% endif %}
</h3>
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; white-space: nowrap;">
<span class="badge badge-info">{{ vorgabe.thema }}</span>
{% if vorgabe.relevanzset %}
<span class="badge badge-secondary">
Relevanz: {{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
</div>
</div>
@@ -149,7 +123,7 @@
{% endif %}
<!-- Stichworte und Referenzen -->
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6;">
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6; padding-left: 0.5en;">
<p class="mb-2">
<strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %}
@@ -177,5 +151,29 @@
</div>
{% endif %}
{% endfor %}
<!-- Metadata -->
<h2>Metadaten</h2>
<div class="row mb-4">
<div class="col-md-12">
<dl class="row">
<dt class="col-sm-3">Autoren:</dt>
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
<dt class="col-sm-3">Prüfende:</dt>
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
<dt class="col-sm-3">Gültigkeit:</dt>
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
</dl>
<p>
<a href="{% url 'standard_json' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.json">
JSON herunterladen
</a>
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@ metadata:
name: django
namespace: vorgabenui
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
traefik.ingress.kubernetes.io/router.middlewares: "vorgabenui-vorgabenui-rewrite@kubernetescrd"
spec:
rules:
- host: vorgabenui.adebaumann.com

15
k8s/nfs-pv.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: django-data-pv
namespace: vorgabenui
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
server: 192.168.17.199
path: /mnt/user/vorgabenui

View File

@@ -0,0 +1,8 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate

View File

@@ -5,7 +5,8 @@ metadata:
namespace: vorgabenui
spec:
accessModes:
- ReadWriteOnce
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 2Gi

View File

@@ -0,0 +1,9 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: vorgabenui-rewrite
namespace: vorgabenui
spec:
stripPrefix:
prefixes:
- "/"

View File

@@ -0,0 +1,5 @@
## 1. Add user login functionality
- [x] add a login screen for users
- [x] add an icon for logged in user on the top right corner of all page
- [x] add a menu to log out and change password on the user icon
- [x] all functions should go back to the main page, not the django admin page

63
openspec/project.md Normal file
View File

@@ -0,0 +1,63 @@
# Project Context
## Purpose
This is a Django-based document management system for regulatory documents (Dokumente) and their provisions (Vorgaben). It manages validity periods, conflicts between overlapping provisions, references, keywords, and roles. The system supports importing documents, checking for compliance, and maintaining changelogs.
## Tech Stack
- Python 3.x
- Django 5.2.5
- SQLite (development), PostgreSQL (production)
- Django MPTT for tree structures
- Django Nested Admin for inline editing
- Kubernetes for deployment
- ArgoCD for continuous deployment
- Traefik for ingress
- Gunicorn for WSGI server
## Project Conventions
### Code Style
- Language: German for user-facing strings and model names, English for code comments and internal naming
- Imports: Standard library first, then Django, then third-party, then local apps
- Model naming: German nouns (Dokument, Vorgabe, Person)
- Field naming: German for field names, English Django conventions
- Class naming: PascalCase for models, snake_case for functions/variables
- All models have __str__ methods returning meaningful German strings
- Use verbose_name and verbose_name_plural in Meta classes (German)
### Architecture Patterns
- Django apps: abschnitte, dokumente, referenzen, rollen, stichworte, pages
- MPTT for hierarchical text sections
- Foreign keys with on_delete=models.PROTECT for important relationships
- Many-to-many with descriptive related_name
- Proxy models for different views (e.g., VorgabenTable)
- Management commands for data operations
### Testing Strategy
- Django test framework
- Test class names in English, methods in English
- Comprehensive model tests
- Test both success and error cases
- Run with `python manage.py test`
### Git Workflow
- Standard Git workflow
- Commits in English
- Use Gitea workflows for CI/CD
## Domain Context
The system manages regulatory documents with numbered provisions that have validity dates. Provisions can conflict if they have overlapping date ranges for the same document, theme, and number. The system includes sanity checks for conflicts, diagram caching for visualization, and JSON export functionality.
## Important Constraints
- German language for all user interfaces and data
- Strict validation of date ranges to prevent overlapping provisions
- Documents have types, authors, reviewers, and validity periods
- Provisions linked to themes, references, keywords, and relevant roles
- Active/inactive status for documents
## External Dependencies
- Django ecosystem: MPTT, nested-admin, revproxy
- Kubernetes cluster for deployment
- ArgoCD for GitOps
- Traefik for load balancing
- External diagram services (diagramm_proxy)

View File

@@ -41,6 +41,37 @@
alt="Zur Startseite" />
<h1>Vorgaben Informatiksicherheit BIT</h1>
</a>
<!-- User Menu -->
{% if user.is_authenticated %}
<div class="user-menu" style="position: absolute; top: 20px; right: 20px; z-index: 1000;">
<div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none; color: #000; display: flex; align-items: center;">
<span style="font-size: 24px; margin-right: 8px;">👤</span>
<span class="hidden-xs" style="margin-left: 0;">{{ user.username }}</span>
<span class="caret" style="margin-left: 8px;"></span>
</a>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="{% url 'password_change' %}">Passwort ändern</a></li>
<li class="divider"></li>
<li>
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;">
Abmelden
</button>
</form>
</li>
</ul>
</div>
</div>
{% else %}
<div class="user-menu" style="position: absolute; top: 20px; right: 20px; z-index: 1000;">
<a href="{% url 'login' %}" class="btn btn-sm btn-primary" style="text-decoration: none;">
Anmelden
</a>
</div>
{% endif %}
</header>
<!-- Main Navigation -->
@@ -180,7 +211,7 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.951" }}</p>
<p class="text-muted">Version {{ version|default:"0.957" }}</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Anmelden{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Anmelden</h3>
</div>
<div class="panel-body">
<form method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
<p>Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es erneut.</p>
</div>
{% endif %}
<div class="form-group">
<label for="id_username">Benutzername:</label>
<input type="text" name="username" class="form-control" id="id_username" required autofocus>
</div>
<div class="form-group">
<label for="id_password">Passwort:</label>
<input type="password" name="password" class="form-control" id="id_password" required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Anmelden</button>
</div>
<input type="hidden" name="next" value="{{ next }}">
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Passwort ändern{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Passwort ändern</h3>
</div>
<div class="panel-body">
<form method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
<p>Bitte korrigieren Sie die Fehler unten.</p>
</div>
{% endif %}
<div class="form-group">
<label for="id_old_password">Aktuelles Passwort:</label>
<input type="password" name="old_password" class="form-control" id="id_old_password" required>
{% if form.old_password.errors %}
<div class="text-danger">{{ form.old_password.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="id_new_password1">Neues Passwort:</label>
<input type="password" name="new_password1" class="form-control" id="id_new_password1" required>
{% if form.new_password1.errors %}
<div class="text-danger">{{ form.new_password1.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="id_new_password2">Neues Passwort bestätigen:</label>
<input type="password" name="new_password2" class="form-control" id="id_new_password2" required>
{% if form.new_password2.errors %}
<div class="text-danger">{{ form.new_password2.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Passwort ändern</button>
<a href="/" class="btn btn-default">Abbrechen</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Passwort geändert{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Passwort erfolgreich geändert</h3>
</div>
<div class="panel-body">
<div class="alert alert-success">
<p>Ihr Passwort wurde erfolgreich geändert.</p>
</div>
<p>
<a href="/" class="btn btn-primary">Zurück zur Startseite</a>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -24,7 +24,7 @@
<a href="{% url 'standard_detail' nummer=standard.nummer %}"
class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h3 class="h5 mb-1">{{ standard.nummer }} - {{ standard.titel }}</h3>
<h3 class="h5 mb-1">{{ standard.nummer }} - {{ standard.name }}</h3>
{% if standard.status %}
<small class="text-muted">{{ standard.status }}</small>
{% endif %}

266
pages/test_auth.py Normal file
View File

@@ -0,0 +1,266 @@
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from django.core.exceptions import ValidationError
class AuthenticationTest(TestCase):
"""Test login, logout, and password change functionality"""
def setUp(self):
self.client = Client()
self.test_user = User.objects.create_user(
username='testuser',
password='testpass123',
email='test@example.com'
)
self.staff_user = User.objects.create_user(
username='staffuser',
password='staffpass123',
email='staff@example.com'
)
self.staff_user.is_staff = True
self.staff_user.save()
def test_login_page_loads(self):
"""Test that login page loads correctly"""
response = self.client.get(reverse('login'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Anmelden')
self.assertContains(response, 'Benutzername:')
self.assertContains(response, 'Passwort:')
def test_login_valid_credentials(self):
"""Test successful login with valid credentials"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass123'
})
self.assertEqual(response.status_code, 302) # Redirect after login
self.assertRedirects(response, '/') # Should redirect to main page
# Check user is logged in
response = self.client.get('/')
self.assertContains(response, 'testuser') # Username should appear in header
def test_login_invalid_credentials(self):
"""Test login with invalid credentials shows error"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'wrongpassword'
})
self.assertEqual(response.status_code, 200) # Stay on login page
self.assertContains(response, 'Ihr Benutzername und Passwort stimmen nicht überein')
def test_login_empty_credentials(self):
"""Test login with empty credentials"""
response = self.client.post(reverse('login'), {
'username': '',
'password': ''
})
self.assertEqual(response.status_code, 200) # Stay on login page
# Django's form validation should handle this
def test_logout_functionality(self):
"""Test logout functionality"""
# First login
self.client.login(username='testuser', password='testpass123')
# Verify user is logged in
response = self.client.get('/')
self.assertContains(response, 'testuser')
# Logout using POST
response = self.client.post(reverse('logout'))
self.assertEqual(response.status_code, 302) # Redirect after logout
self.assertRedirects(response, '/') # Should redirect to main page
# Verify user is logged out
response = self.client.get('/')
self.assertNotContains(response, 'testuser')
self.assertContains(response, 'Anmelden') # Should show login link
def test_logout_requires_post(self):
"""Test that logout requires POST method"""
# Login first
self.client.login(username='testuser', password='testpass123')
# Try GET logout (should fail with 405)
response = self.client.get(reverse('logout'))
self.assertEqual(response.status_code, 405) # Method Not Allowed
# User should still be logged in
response = self.client.get('/')
self.assertContains(response, 'testuser')
def test_password_change_page_loads(self):
"""Test that password change page loads for authenticated users"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('password_change'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Passwort ändern')
self.assertContains(response, 'Aktuelles Passwort:')
self.assertContains(response, 'Neues Passwort:')
self.assertContains(response, 'Neues Passwort bestätigen:')
def test_password_change_requires_authentication(self):
"""Test that password change page requires authentication"""
response = self.client.get(reverse('password_change'))
self.assertEqual(response.status_code, 302) # Redirect to login
# Should redirect to login page
self.assertIn(reverse('login'), response.url)
def test_password_change_valid(self):
"""Test successful password change"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'newpass456',
'new_password2': 'newpass456'
})
self.assertEqual(response.status_code, 302) # Redirect after success
self.assertRedirects(response, '/') # Should redirect to main page
# Verify new password works
self.client.logout()
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'newpass456'
})
self.assertEqual(response.status_code, 302) # Successful login
def test_password_change_wrong_old_password(self):
"""Test password change with wrong old password"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'wrongpassword',
'new_password1': 'newpass456',
'new_password2': 'newpass456'
})
self.assertEqual(response.status_code, 200) # Stay on form page
self.assertContains(response, 'Bitte korrigieren Sie die Fehler unten')
def test_password_change_mismatched_new_passwords(self):
"""Test password change with mismatched new passwords"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'newpass456',
'new_password2': 'differentpass789'
})
self.assertEqual(response.status_code, 200) # Stay on form page
self.assertContains(response, 'Bitte korrigieren Sie die Fehler unten')
def test_password_change_same_as_old_password(self):
"""Test password change with same password as old"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'testpass123',
'new_password2': 'testpass123'
})
# Django's default validators don't prevent same password, so it should succeed
self.assertEqual(response.status_code, 302) # Redirect after success
self.assertRedirects(response, '/') # Should redirect to main page
def test_password_change_cancel_button(self):
"""Test password change cancel button"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('password_change'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Abbrechen')
# The cancel button should link to main page
self.assertContains(response, 'href="/"')
def test_user_menu_display_for_authenticated_user(self):
"""Test that user menu displays correctly for authenticated users"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'testuser') # Username in menu
self.assertContains(response, 'Passwort ändern') # Password change link
self.assertContains(response, 'Abmelden') # Logout link
self.assertNotContains(response, 'Anmelden') # Should not show login link
def test_login_link_display_for_anonymous_user(self):
"""Test that login link displays for anonymous users"""
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Anmelden') # Should show login link
self.assertNotContains(response, 'testuser') # Should not show username
self.assertNotContains(response, 'Passwort ändern') # Should not show password change
self.assertNotContains(response, 'Abmelden') # Should not show logout
def test_staff_user_menu(self):
"""Test that staff users see appropriate menu"""
self.client.login(username='staffuser', password='staffpass123')
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'staffuser') # Username in menu
self.assertContains(response, 'Passwort ändern') # Password change link
self.assertContains(response, 'Abmelden') # Logout link
def test_login_redirect_to_main_page(self):
"""Test that successful login redirects to main page"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass123'
}, follow=True)
self.assertEqual(response.status_code, 200)
# Should end up on main page
self.assertContains(response, 'Vorgaben Informatiksicherheit')
def test_logout_redirect_to_main_page(self):
"""Test that logout redirects to main page"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('logout'), follow=True)
self.assertEqual(response.status_code, 200)
# Should end up on main page
self.assertContains(response, 'Vorgaben Informatiksicherheit')
# Should show login link for anonymous users
self.assertContains(response, 'Anmelden')
def test_password_change_redirect_to_main_page(self):
"""Test that successful password change redirects to main page"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'newpass456',
'new_password2': 'newpass456'
}, follow=True)
self.assertEqual(response.status_code, 200)
# Should end up on main page
self.assertContains(response, 'Vorgaben Informatiksicherheit')
def test_csrf_token_present_in_forms(self):
"""Test that CSRF tokens are present in authentication forms"""
# Login form
response = self.client.get(reverse('login'))
self.assertContains(response, 'csrfmiddlewaretoken')
# Password change form
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('password_change'))
self.assertContains(response, 'csrfmiddlewaretoken')
def test_login_with_next_parameter(self):
"""Test login with next parameter for redirect"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass123',
'next': '/dokumente/'
})
self.assertEqual(response.status_code, 302)
# Should redirect to the specified next page
self.assertRedirects(response, '/dokumente/')

View File

@@ -4,7 +4,6 @@ from django.utils import timezone
from datetime import date, timedelta
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Geltungsbereich, Dokumententyp, Thema
from stichworte.models import Stichwort
from unittest.mock import patch
import re
@@ -67,24 +66,24 @@ class SearchViewTest(TestCase):
"""Test POST request with valid search term"""
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für Test')
self.assertContains(response, 'Suchergebnisse')
def test_search_case_insensitive(self):
"""Test that search is case insensitive"""
# Search for lowercase
response = self.client.post('/search/', {'q': 'test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für test')
self.assertContains(response, 'Suchergebnisse für "test"')
# Search for uppercase
response = self.client.post('/search/', {'q': 'TEST'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für TEST')
self.assertContains(response, 'Suchergebnisse für "TEST"')
# Search for mixed case
response = self.client.post('/search/', {'q': 'TeSt'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für TeSt')
self.assertContains(response, 'Suchergebnisse für "TeSt"')
def test_search_in_kurztext(self):
"""Test search in Kurztext content"""
@@ -114,7 +113,7 @@ class SearchViewTest(TestCase):
"""Test search with no results"""
response = self.client.post('/search/', {'q': 'NichtVorhanden'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Keine Resultate für "NichtVorhanden"')
self.assertContains(response, 'Keine Ergebnisse gefunden')
def test_search_expired_vorgabe_not_included(self):
"""Test that expired Vorgaben are not included in results"""
@@ -160,8 +159,8 @@ class SearchViewTest(TestCase):
"""Test that HTML tags are stripped from search input"""
response = self.client.post('/search/', {'q': '<script>alert("xss")</script>Test'})
self.assertEqual(response.status_code, 200)
# Should search for "alert('xss')Test" after HTML tag removal
self.assertContains(response, 'Suchresultate für alert(&quot;xss&quot;)Test')
# Should search for "alert("xss")Test" after HTML tag removal
self.assertContains(response, 'Suchergebnisse für "alert')
def test_search_invalid_characters_validation(self):
"""Test validation for invalid characters"""
@@ -206,7 +205,7 @@ class SearchViewTest(TestCase):
self.assertEqual(response.status_code, 200)
# The input should be preserved (escaped) in the form
# Since HTML tags are stripped, we expect "Test" to be searched
self.assertContains(response, 'Suchresultate für Test')
self.assertContains(response, 'Suchergebnisse für "Test"')
def test_search_xss_prevention_in_results(self):
"""Test that search terms are escaped in results to prevent XSS"""
@@ -218,15 +217,14 @@ class SearchViewTest(TestCase):
self.assertEqual(response.status_code, 200)
# The script tag should be escaped in the output
# Note: This depends on how the template renders the content
self.assertContains(response, 'Suchresultate für term')
self.assertContains(response, 'Suchergebnisse für "term"')
@patch('pages.views.pprint.pp')
def test_search_result_logging(self, mock_pprint):
"""Test that search results are logged for debugging"""
def test_search_result_structure(self):
"""Test that search results have expected structure"""
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
# Verify that pprint.pp was called with the result
mock_pprint.assert_called_once()
# Verify the results page is rendered with correct structure
self.assertContains(response, 'Suchergebnisse für "Test"')
def test_search_multiple_documents(self):
"""Test search across multiple documents"""

View File

@@ -31,3 +31,4 @@ class Referenzerklaerung (Textabschnitt):
class Meta:
verbose_name="Erklärung"
verbose_name_plural="Erklärungen"

View File

@@ -15,3 +15,4 @@ class Stichworterklaerung (Textabschnitt):
class Meta:
verbose_name="Erklärung"
verbose_name_plural="Erklärungen"